From 1efb9ef82094ab185fead29ccb6ad843fd5ec7a9 Mon Sep 17 00:00:00 2001 From: Kartikay Sharma Date: Wed, 10 Feb 2021 09:23:33 +0530 Subject: [PATCH 01/12] ML kit implemented --- app/build.gradle.kts | 6 + .../openfood/features/PreferencesFragment.kt | 29 +++ .../features/scan/ContinuousScanActivity.kt | 181 +++++++++++++++--- .../res/layout/activity_continuous_scan.xml | 6 + app/src/main/res/values/pref_keys.xml | 5 + app/src/main/res/xml/preferences.xml | 6 + 6 files changed, 206 insertions(+), 27 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3564b1ec92c0..930bb8520af4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -69,6 +69,12 @@ dependencies { implementation("androidx.startup:startup-runtime:1.0.0") + // ML Kit barcode Scanner + implementation ("com.google.mlkit:barcode-scanning:16.1.1") + // fotoapparat library + implementation ("io.fotoapparat:fotoapparat:2.7.0") + + kapt("com.google.dagger:dagger-compiler:2.30.1") implementation("com.google.dagger:dagger:2.30.1") compileOnly("javax.annotation:javax.annotation-api:1.3.2") diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/PreferencesFragment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/PreferencesFragment.kt index 28c219194351..279bcd250e3d 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/PreferencesFragment.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/PreferencesFragment.kt @@ -150,6 +150,35 @@ class PreferencesFragment : PreferenceFragmentCompat(), INavigationItem, OnShare true } + requirePreference(getString(R.string.pref_scanner_type_key)).let { + it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + if(newValue == true){ + MaterialDialog.Builder(requireActivity()).run { + title("New: Enhanced “MLKit” scanner") + content(R.string.pref_mlkit) + positiveText("Proceed") + onPositive { _, _ -> + it.isChecked = false + settings.edit { putBoolean(getString(R.string.pref_scanner_type_key), newValue as Boolean) } + Toast.makeText(requireActivity(), getString(R.string.changes_saved), Toast.LENGTH_SHORT).show() + } + negativeText("Cancel") + onNegative { + dialog, _ -> dialog.dismiss() + it.isChecked = false + settings.edit { putBoolean(getString(R.string.pref_scanner_type_key), false) } + } + show() + } + } + else{ + it.isChecked = false + settings.edit { putBoolean(getString(R.string.pref_scanner_type_key), newValue as Boolean) } + } + true + } + } + val countryLabels = mutableListOf() val countryTags = mutableListOf() diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt index 333a545f5065..69d7404c9e63 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt @@ -17,7 +17,11 @@ package openfoodfacts.github.scrachx.openfood.features.scan import android.content.Context import android.content.Intent +import android.graphics.Bitmap import android.hardware.Camera +import android.hardware.camera2.CameraCaptureSession +import android.hardware.camera2.CameraDevice +import android.hardware.camera2.CameraManager import android.os.Bundle import android.util.Log import android.view.* @@ -40,6 +44,8 @@ import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback import com.google.android.material.bottomsheet.BottomSheetBehavior.from +import com.google.mlkit.vision.barcode.* +import com.google.mlkit.vision.common.InputImage import com.google.zxing.BarcodeFormat import com.google.zxing.ResultPoint import com.google.zxing.client.android.BeepManager @@ -50,6 +56,11 @@ import com.mikepenz.iconics.IconicsColor import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsSize import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import io.fotoapparat.Fotoapparat +import io.fotoapparat.configuration.CameraConfiguration +import io.fotoapparat.configuration.UpdateConfiguration +import io.fotoapparat.parameter.ScaleType +import io.fotoapparat.selector.* import io.reactivex.Completable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable @@ -104,6 +115,10 @@ class ContinuousScanActivity : AppCompatActivity() { private val client by lazy { OpenFoodAPIClient(this@ContinuousScanActivity) } private val cameraPref by lazy { getSharedPreferences("camera", 0) } + private val settings by lazy { getSharedPreferences("prefs", 0) } + + private lateinit var fotoapparat: Fotoapparat + private val commonDisp = CompositeDisposable() private var productDisp: Disposable? = null private var hintBarcodeDisp: Disposable? = null @@ -117,6 +132,7 @@ class ContinuousScanActivity : AppCompatActivity() { private var analysisTagsEmpty = true private var productShowing = false private var beepActive = false + private var useMLScanner = false private var offlineSavedProduct: OfflineSavedProduct? = null private var product: Product? = null @@ -138,6 +154,7 @@ class ContinuousScanActivity : AppCompatActivity() { internal fun showProduct(barcode: String) { productShowing = true binding.barcodeScanner.visibility = View.GONE + binding.cameraView.visibility = View.GONE binding.barcodeScanner.pause() binding.imageForScreenshotGenerationOnly.visibility = View.VISIBLE setShownProduct(barcode) @@ -432,6 +449,7 @@ class ContinuousScanActivity : AppCompatActivity() { _binding = ActivityContinuousScanBinding.inflate(layoutInflater) setContentView(binding.root) + useMLScanner = settings.getBoolean("select_scanner",false) binding.toggleFlash.setOnClickListener { toggleFlash() } binding.buttonMore.setOnClickListener { showMoreSettings() } @@ -475,32 +493,114 @@ class ContinuousScanActivity : AppCompatActivity() { } // Setup barcode scanner - binding.barcodeScanner.barcodeView.decoderFactory = DefaultDecoderFactory(BARCODE_FORMATS) - binding.barcodeScanner.setStatusText(null) - binding.barcodeScanner.barcodeView.cameraSettings.run { - requestedCameraId = cameraState - isAutoFocusEnabled = autoFocusActive + + if (!useMLScanner) { + binding.barcodeScanner.visibility = View.VISIBLE + binding.cameraView.visibility = View.GONE + binding.barcodeScanner.barcodeView.decoderFactory = DefaultDecoderFactory(BARCODE_FORMATS) + binding.barcodeScanner.setStatusText(null) + binding.barcodeScanner.barcodeView.cameraSettings.run { + requestedCameraId = cameraState + isAutoFocusEnabled = autoFocusActive + } + + // Start continuous scanner + binding.barcodeScanner.decodeContinuous(barcodeScanCallback) + beepManager = BeepManager(this) + } else { + binding.barcodeScanner.visibility = View.GONE + binding.cameraView.visibility = View.VISIBLE + + val config = CameraConfiguration( + flashMode = if(flashActive) torch() else off(), + previewResolution = highestResolution(), // we want to have the highest possible preview resolution + previewFpsRange = highestFps(), // we want to have the best frame rate + focusMode = firstAvailable( // use the first focus mode which is supported by device + continuousFocusVideo(), + autoFocus(), // if continuous focus is not available on device, auto focus will be used + fixed() // if even auto focus is not available - fixed focus mode will be used + ), + frameProcessor = { frame -> + processFrame(frame.image, frame.rotation, frame.size.height, frame.size.width) + } + ) + + fotoapparat = Fotoapparat( + context = this, + cameraConfiguration = config, + scaleType = ScaleType.CenterCrop, + view = binding.cameraView + ) } + binding.quickViewSearchByBarcode.setOnEditorActionListener(barcodeInputListener) + binding.bottomNavigation.bottomNavigation.installBottomNavigation(this) // Setup popup menu setupPopupMenu() + } + + private fun processFrame(byteArray:ByteArray, rotation:Int, height:Int, width:Int) + { + val inputImage = InputImage.fromByteArray( + byteArray, + width, + height, + rotation, + InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12 + ) + + val options = BarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_UPC_A, + Barcode.FORMAT_UPC_E, + Barcode.FORMAT_EAN_13, + Barcode.FORMAT_EAN_8, + Barcode.FORMAT_CODE_39, + Barcode.FORMAT_CODE_93, + Barcode.FORMAT_CODE_128) + .build() + + val barcodeScanner = BarcodeScanning.getClient(options) + + val task = barcodeScanner.process(inputImage) + task.addOnSuccessListener { barCodesList -> + for (barcodeObject in barCodesList) { + val barcodeValue = barcodeObject.rawValue + Log.d("Barcode", "The code "+ barcodeValue.toString()) + mlBarcodeCallback(barcodeValue) + } + } - // Start continuous scanner - binding.barcodeScanner.decodeContinuous(barcodeScanCallback) - beepManager = BeepManager(this) - binding.quickViewSearchByBarcode.setOnEditorActionListener(barcodeInputListener) - binding.bottomNavigation.bottomNavigation.installBottomNavigation(this) + } + + private fun mlBarcodeCallback(barcodeValue:String?){ + + hintBarcodeDisp?.dispose() + + // Prevent duplicate scans + if (barcodeValue == null || barcodeValue.isEmpty() || barcodeValue == lastBarcode) return + + val invalidBarcode = daoSession.invalidBarcodeDao.queryBuilder() + .where(InvalidBarcodeDao.Properties.Barcode.eq(barcodeValue)) + .unique() + + // Scanned barcode is in the list of invalid barcodes, do nothing + if (invalidBarcode != null) return + + lastBarcode = barcodeValue.also { if (!isFinishing) setShownProduct(it) } } override fun onStart() { super.onStart() + if(useMLScanner) { + fotoapparat.start() + } EventBus.getDefault().register(this) } override fun onResume() { super.onResume() binding.bottomNavigation.bottomNavigation.selectNavigationItem(R.id.scan_bottom_nav) - if (quickViewBehavior.state != BottomSheetBehavior.STATE_EXPANDED) { + if(!useMLScanner && quickViewBehavior.state != BottomSheetBehavior.STATE_EXPANDED) { binding.barcodeScanner.resume() } } @@ -511,12 +611,17 @@ class ContinuousScanActivity : AppCompatActivity() { } override fun onPause() { - binding.barcodeScanner.pause() + if(!useMLScanner ) { + binding.barcodeScanner.pause() + } super.onPause() } override fun onStop() { EventBus.getDefault().unregister(this) + if(useMLScanner) { + fotoapparat.stop() + } super.onStop() } @@ -568,7 +673,6 @@ class ContinuousScanActivity : AppCompatActivity() { it.menu.findItem(R.id.toggleAutofocus).isChecked = true } } - } override fun attachBaseContext(newBase: Context) = super.attachBaseContext(LocaleHelper.onCreate(newBase)) @@ -587,30 +691,53 @@ class ContinuousScanActivity : AppCompatActivity() { || product!!.ingredientsText == "" private fun toggleCamera() { - val settings = binding.barcodeScanner.barcodeView.cameraSettings - if (binding.barcodeScanner.barcodeView.isPreviewActive) { - binding.barcodeScanner.pause() - } - cameraState = if (settings.requestedCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) { - Camera.CameraInfo.CAMERA_FACING_FRONT - } else { - Camera.CameraInfo.CAMERA_FACING_BACK + if(!useMLScanner) { + val settings = binding.barcodeScanner.barcodeView.cameraSettings + if (binding.barcodeScanner.barcodeView.isPreviewActive) { + binding.barcodeScanner.pause() + } + cameraState = if (settings.requestedCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) { + Camera.CameraInfo.CAMERA_FACING_FRONT + } else { + Camera.CameraInfo.CAMERA_FACING_BACK + } + settings.requestedCameraId = cameraState + binding.barcodeScanner.barcodeView.cameraSettings = settings + cameraPref.edit { putInt(SETTING_STATE, cameraState) } + binding.barcodeScanner.resume() + } else{ + fotoapparat.switchTo( + if () + lensPosition = front() + ) + + } - settings.requestedCameraId = cameraState - binding.barcodeScanner.barcodeView.cameraSettings = settings - cameraPref.edit { putInt(SETTING_STATE, cameraState) } - binding.barcodeScanner.resume() } private fun toggleFlash() { cameraPref.edit { if (flashActive) { - binding.barcodeScanner.setTorchOff() + if(useMLScanner){ + fotoapparat.updateConfiguration( + UpdateConfiguration( flashMode= off() ) + ) + } + else { + binding.barcodeScanner.setTorchOff() + } flashActive = false binding.toggleFlash.setImageResource(R.drawable.ic_flash_off_white_24dp) putBoolean(SETTING_FLASH, false) } else { - binding.barcodeScanner.setTorchOn() + if(useMLScanner){ + fotoapparat.updateConfiguration( + UpdateConfiguration( flashMode= torch() ) + ) + } + else { + binding.barcodeScanner.setTorchOn() + } flashActive = true binding.toggleFlash.setImageResource(R.drawable.ic_flash_on_white_24dp) putBoolean(SETTING_FLASH, true) diff --git a/app/src/main/res/layout/activity_continuous_scan.xml b/app/src/main/res/layout/activity_continuous_scan.xml index cfaf07573b90..db5e51777c8a 100644 --- a/app/src/main/res/layout/activity_continuous_scan.xml +++ b/app/src/main/res/layout/activity_continuous_scan.xml @@ -41,6 +41,12 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> + + + Preferred energy unit volumeUnitPreference + select_scanner Preferred volume unit deleteSearchHistoryPreference @@ -97,4 +98,8 @@ Crop new images Enables crop action on new images + We included a new option to more reliably scan barcodes. While this scanner works on your device, using machine learning, please be aware that it is a proprietary component provided by Google, governed by this privacy policy, and that some limited telemetry might be sent back to Google’s servers to improve their software. As noted in the MLKit terms, this won’t include any information about the products you scan and the telemetry is anonymized. Choosing MLKit scanner implies that you accept those terms. +Note: you can switch back at any time between the 2 scanners in the Settings. + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index b254784d562c..e4c28384994a 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -46,6 +46,12 @@ android:title="@string/pref_country_title" app:summary="@string/pref_country_summary" /> + + Date: Thu, 11 Feb 2021 07:41:41 +0530 Subject: [PATCH 02/12] added camera classes --- app/build.gradle.kts | 4 +- .../openfood/camera/CameraReticleAnimator.kt | 106 ++++ .../scrachx/openfood/camera/CameraSizePair.kt | 41 ++ .../scrachx/openfood/camera/CameraSource.kt | 529 ++++++++++++++++++ .../openfood/camera/CameraSourcePreview.kt | 148 +++++ .../scrachx/openfood/camera/FrameMetadata.kt | 20 + .../scrachx/openfood/camera/FrameProcessor.kt | 29 + .../openfood/camera/FrameProcessorBase.kt | 106 ++++ .../scrachx/openfood/camera/GraphicOverlay.kt | 120 ++++ .../scrachx/openfood/camera/WorkflowModel.kt | 66 +++ .../openfood/features/PreferencesFragment.kt | 2 +- .../features/scan/ContinuousScanActivity.kt | 197 ++++--- .../scanner/BarcodeConfirmingGraphic.kt | 53 ++ .../openfood/scanner/BarcodeGraphicBase.kt | 73 +++ .../openfood/scanner/BarcodeProcessor.kt | 98 ++++ .../openfood/scanner/BarcodeReticleGraphic.kt | 62 ++ .../openfood/utils/CameraPreferenceUtils.kt | 71 +++ .../scrachx/openfood/utils/CameraUtils.kt | 104 ++++ .../scrachx/openfood/utils/ScopedExecutor.kt | 37 ++ .../scrachx/openfood/utils/inputInfo.kt | 33 ++ .../res/layout/activity_continuous_scan.xml | 10 +- .../res/layout/camera_preview_overlay.xml | 26 + app/src/main/res/values/colors.xml | 8 + app/src/main/res/values/dimens.xml | 5 + app/src/main/res/values/strings.xml | 24 + 25 files changed, 1885 insertions(+), 87 deletions(-) create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraReticleAnimator.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSizePair.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSourcePreview.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameMetadata.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameProcessor.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameProcessorBase.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/GraphicOverlay.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/WorkflowModel.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeConfirmingGraphic.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeGraphicBase.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeProcessor.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeReticleGraphic.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/CameraPreferenceUtils.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/CameraUtils.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/ScopedExecutor.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/inputInfo.kt create mode 100644 app/src/main/res/layout/camera_preview_overlay.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 930bb8520af4..69e8c13449e9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -71,8 +71,8 @@ dependencies { // ML Kit barcode Scanner implementation ("com.google.mlkit:barcode-scanning:16.1.1") - // fotoapparat library - implementation ("io.fotoapparat:fotoapparat:2.7.0") + // ___ library + implementation ("android.arch.lifecycle:extensions:1.1.1") kapt("com.google.dagger:dagger-compiler:2.30.1") diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraReticleAnimator.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraReticleAnimator.kt new file mode 100644 index 000000000000..0fb3d3499797 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraReticleAnimator.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package openfoodfacts.github.scrachx.openfood.camera + +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import androidx.interpolator.view.animation.FastOutSlowInInterpolator + +/** Custom animator for the object or barcode reticle in live camera. */ +class CameraReticleAnimator(graphicOverlay: GraphicOverlay) { + + /** Returns the scale value of ripple alpha ranges in [0, 1]. */ + var rippleAlphaScale = 0f + private set + + /** Returns the scale value of ripple size ranges in [0, 1]. */ + var rippleSizeScale = 0f + private set + + /** Returns the scale value of ripple stroke width ranges in [0, 1]. */ + var rippleStrokeWidthScale = 1f + private set + + private val animatorSet: AnimatorSet + + init { + val rippleFadeInAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(DURATION_RIPPLE_FADE_IN_MS) + rippleFadeInAnimator.addUpdateListener { animation -> + rippleAlphaScale = animation.animatedValue as Float + graphicOverlay.postInvalidate() + } + + val rippleFadeOutAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(DURATION_RIPPLE_FADE_OUT_MS) + rippleFadeOutAnimator.startDelay = START_DELAY_RIPPLE_FADE_OUT_MS + rippleFadeOutAnimator.addUpdateListener { animation -> + rippleAlphaScale = animation.animatedValue as Float + graphicOverlay.postInvalidate() + } + + val rippleExpandAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(DURATION_RIPPLE_EXPAND_MS) + rippleExpandAnimator.startDelay = START_DELAY_RIPPLE_EXPAND_MS + rippleExpandAnimator.interpolator = FastOutSlowInInterpolator() + rippleExpandAnimator.addUpdateListener { animation -> + rippleSizeScale = animation.animatedValue as Float + graphicOverlay.postInvalidate() + } + + val rippleStrokeWidthShrinkAnimator = + ValueAnimator.ofFloat(1f, 0.5f).setDuration(DURATION_RIPPLE_STROKE_WIDTH_SHRINK_MS) + rippleStrokeWidthShrinkAnimator.startDelay = START_DELAY_RIPPLE_STROKE_WIDTH_SHRINK_MS + rippleStrokeWidthShrinkAnimator.interpolator = FastOutSlowInInterpolator() + rippleStrokeWidthShrinkAnimator.addUpdateListener { animation -> + rippleStrokeWidthScale = animation.animatedValue as Float + graphicOverlay.postInvalidate() + } + + val fakeAnimatorForRestartDelay = ValueAnimator.ofInt(0, 0).setDuration(DURATION_RESTART_DORMANCY_MS) + fakeAnimatorForRestartDelay.startDelay = START_DELAY_RESTART_DORMANCY_MS + animatorSet = AnimatorSet() + animatorSet.playTogether( + rippleFadeInAnimator, + rippleFadeOutAnimator, + rippleExpandAnimator, + rippleStrokeWidthShrinkAnimator, + fakeAnimatorForRestartDelay + ) + } + + fun start() { + if (!animatorSet.isRunning) animatorSet.start() + } + + fun cancel() { + animatorSet.cancel() + rippleAlphaScale = 0f + rippleSizeScale = 0f + rippleStrokeWidthScale = 1f + } + + companion object { + + private const val DURATION_RIPPLE_FADE_IN_MS: Long = 333 + private const val DURATION_RIPPLE_FADE_OUT_MS: Long = 500 + private const val DURATION_RIPPLE_EXPAND_MS: Long = 833 + private const val DURATION_RIPPLE_STROKE_WIDTH_SHRINK_MS: Long = 833 + private const val DURATION_RESTART_DORMANCY_MS: Long = 1333 + private const val START_DELAY_RIPPLE_FADE_OUT_MS: Long = 667 + private const val START_DELAY_RIPPLE_EXPAND_MS: Long = 333 + private const val START_DELAY_RIPPLE_STROKE_WIDTH_SHRINK_MS: Long = 333 + private const val START_DELAY_RESTART_DORMANCY_MS: Long = 1167 + } +} diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSizePair.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSizePair.kt new file mode 100644 index 000000000000..9147626f66a7 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSizePair.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package openfoodfacts.github.scrachx.openfood.camera + +import android.hardware.Camera +import com.google.android.gms.common.images.Size + +/** + * Stores a preview size and a corresponding same-aspect-ratio picture size. To avoid distorted + * preview images on some devices, the picture size must be set to a size that is the same aspect + * ratio as the preview size or the preview may end up being distorted. If the picture size is null, + * then there is no picture size with the same aspect ratio as the preview size. + */ +class CameraSizePair { + val preview: Size + val picture: Size? + + constructor(previewSize: Camera.Size, pictureSize: Camera.Size?) { + preview = Size(previewSize.width, previewSize.height) + picture = pictureSize?.let { Size(it.width, it.height) } + } + + constructor(previewSize: Size, pictureSize: Size?) { + preview = previewSize + picture = pictureSize + } +} diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt new file mode 100644 index 000000000000..bad4df0a6457 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt @@ -0,0 +1,529 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package openfoodfacts.github.scrachx.openfood.camera + +import android.content.Context +import android.graphics.ImageFormat +import android.hardware.Camera +import android.hardware.Camera.CameraInfo +import android.hardware.Camera.Parameters +import android.util.Log +import android.view.Surface +import android.view.SurfaceHolder +import android.view.WindowManager +import com.google.android.gms.common.images.Size +import openfoodfacts.github.scrachx.openfood.R +import openfoodfacts.github.scrachx.openfood.utils.CameraPreferenceUtils +import openfoodfacts.github.scrachx.openfood.utils.CameraUtils.ASPECT_RATIO_TOLERANCE +import openfoodfacts.github.scrachx.openfood.utils.CameraUtils.generateValidPreviewSizeList +import openfoodfacts.github.scrachx.openfood.utils.CameraUtils.isPortraitMode +import java.io.IOException +import java.nio.ByteBuffer +import java.util.IdentityHashMap +import kotlin.math.abs +import kotlin.math.ceil + +/** + * Manages the camera and allows UI updates on top of it (e.g. overlaying extra Graphics). This + * receives preview frames from the camera at a specified rate, sends those frames to detector as + * fast as it is able to process. + * + * + * This camera source makes a best effort to manage processing on preview frames as fast as + * possible, while at the same time minimizing lag. As such, frames may be dropped if the detector + * is unable to keep up with the rate of frames generated by the camera. + */ +@Suppress("DEPRECATION") +class CameraSource(private val graphicOverlay: GraphicOverlay) { + + private var camera: Camera? = null + private var rotationDegrees: Int = 0 + + /** Returns the preview size that is currently in use by the underlying camera. */ + internal var previewSize: Size? = null + private set + + /** + * Dedicated thread and associated runnable for calling into the detector with frames, as the + * frames become available from the camera. + */ + private var processingThread: Thread? = null + private val processingRunnable = FrameProcessingRunnable() + + private val processorLock = Object() + private var frameProcessor: FrameProcessor? = null + + /** + * Map to convert between a byte array, received from the camera, and its associated byte buffer. + * We use byte buffers internally because this is a more efficient way to call into native code + * later (avoids a potential copy). + * + * + * **Note:** uses IdentityHashMap here instead of HashMap because the behavior of an array's + * equals, hashCode and toString methods is both useless and unexpected. IdentityHashMap enforces + * identity ('==') check on the keys. + */ + private val bytesToByteBuffer = IdentityHashMap() + private val context: Context = graphicOverlay.context + + /** + * Opens the camera and starts sending preview frames to the underlying detector. The supplied + * surface holder is used for the preview so frames can be displayed to the user. + * + * @param surfaceHolder the surface holder to use for the preview frames. + * @throws IOException if the supplied surface holder could not be used as the preview display. + */ + @Synchronized + @Throws(IOException::class) + internal fun start(surfaceHolder: SurfaceHolder) { + if (camera != null) return + + camera = createCamera().apply { + setPreviewDisplay(surfaceHolder) + startPreview() + } + + processingThread = Thread(processingRunnable).apply { + processingRunnable.setActive(true) + start() + } + } + + /** + * Closes the camera and stops sending frames to the underlying frame detector. + * + * + * This camera source may be restarted again by calling [.start]. + * + * + * Call [.release] instead to completely shut down this camera source and release the + * resources of the underlying detector. + */ + @Synchronized + internal fun stop() { + processingRunnable.setActive(false) + processingThread?.let { + try { + // Waits for the thread to complete to ensure that we can't have multiple threads executing + // at the same time (i.e., which would happen if we called start too quickly after stop). + it.join() + } catch (e: InterruptedException) { + Log.e(TAG, "Frame processing thread interrupted on stop.") + } + processingThread = null + } + + camera?.let { + it.stopPreview() + it.setPreviewCallbackWithBuffer(null) + try { + it.setPreviewDisplay(null) + } catch (e: Exception) { + Log.e(TAG, "Failed to clear camera preview: $e") + } + it.release() + camera = null + } + + // Release the reference to any image buffers, since these will no longer be in use. + bytesToByteBuffer.clear() + } + + /** Stops the camera and releases the resources of the camera and underlying detector. */ + fun release() { + graphicOverlay.clear() + synchronized(processorLock) { + stop() + frameProcessor?.stop() + } + } + + fun setFrameProcessor(processor: FrameProcessor) { + graphicOverlay.clear() + synchronized(processorLock) { + frameProcessor?.stop() + frameProcessor = processor + } + } + + fun updateFlashMode(flashMode: String) { + val parameters = camera?.parameters + parameters?.flashMode = flashMode + camera?.parameters = parameters + } + + /** + * Opens the camera and applies the user settings. + * + * @throws IOException if camera cannot be found or preview cannot be processed. + */ + @Throws(IOException::class) + private fun createCamera(): Camera { + val camera = Camera.open() ?: throw IOException("There is no back-facing camera.") + val parameters = camera.parameters + setPreviewAndPictureSize(camera, parameters) + setRotation(camera, parameters) + + val previewFpsRange = selectPreviewFpsRange(camera) + ?: throw IOException("Could not find suitable preview frames per second range.") + parameters.setPreviewFpsRange( + previewFpsRange[Parameters.PREVIEW_FPS_MIN_INDEX], + previewFpsRange[Parameters.PREVIEW_FPS_MAX_INDEX] + ) + + parameters.previewFormat = IMAGE_FORMAT + + if (parameters.supportedFocusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { + parameters.focusMode = Parameters.FOCUS_MODE_CONTINUOUS_VIDEO + } else { + Log.i(TAG, "Camera auto focus is not supported on this device.") + } + + camera.parameters = parameters + + camera.setPreviewCallbackWithBuffer(processingRunnable::setNextFrame) + + // Four frame buffers are needed for working with the camera: + // + // one for the frame that is currently being executed upon in doing detection + // one for the next pending frame to process immediately upon completing detection + // two for the frames that the camera uses to populate future preview images + // + // Through trial and error it appears that two free buffers, in addition to the two buffers + // used in this code, are needed for the camera to work properly. Perhaps the camera has one + // thread for acquiring images, and another thread for calling into user code. If only three + // buffers are used, then the camera will spew thousands of warning messages when detection + // takes a non-trivial amount of time. + previewSize?.let { + camera.addCallbackBuffer(createPreviewBuffer(it)) + camera.addCallbackBuffer(createPreviewBuffer(it)) + camera.addCallbackBuffer(createPreviewBuffer(it)) + camera.addCallbackBuffer(createPreviewBuffer(it)) + } + + return camera + } + + @Throws(IOException::class) + private fun setPreviewAndPictureSize(camera: Camera, parameters: Parameters) { + + // Gives priority to the preview size specified by the user if exists. + val sizePair: CameraSizePair = CameraPreferenceUtils.getUserSpecifiedPreviewSize(context) ?: run { + // Camera preview size is based on the landscape mode, so we need to also use the aspect + // ration of display in the same mode for comparison. + val displayAspectRatioInLandscape: Float = + if (isPortraitMode(graphicOverlay.context)) { + graphicOverlay.height.toFloat() / graphicOverlay.width + } else { + graphicOverlay.width.toFloat() / graphicOverlay.height + } + selectSizePair(camera, displayAspectRatioInLandscape) + } ?: throw IOException("Could not find suitable preview size.") + + previewSize = sizePair.preview.also { + Log.v(TAG, "Camera preview size: $it") + parameters.setPreviewSize(it.width, it.height) + CameraPreferenceUtils.saveStringPreference(context, R.string.pref_key_rear_camera_preview_size, it.toString()) + } + + sizePair.picture?.let { pictureSize -> + Log.v(TAG, "Camera picture size: $pictureSize") + parameters.setPictureSize(pictureSize.width, pictureSize.height) + CameraPreferenceUtils.saveStringPreference( + context, R.string.pref_key_rear_camera_picture_size, pictureSize.toString() + ) + } + } + + /** + * Calculates the correct rotation for the given camera id and sets the rotation in the + * parameters. It also sets the camera's display orientation and rotation. + * + * @param parameters the camera parameters for which to set the rotation. + */ + private fun setRotation(camera: Camera, parameters: Parameters) { + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val degrees = when (val deviceRotation = windowManager.defaultDisplay.rotation) { + Surface.ROTATION_0 -> 0 + Surface.ROTATION_90 -> 90 + Surface.ROTATION_180 -> 180 + Surface.ROTATION_270 -> 270 + else -> { + Log.e(TAG, "Bad device rotation value: $deviceRotation") + 0 + } + } + + val cameraInfo = CameraInfo() + Camera.getCameraInfo(CAMERA_FACING_BACK, cameraInfo) + val angle = (cameraInfo.orientation - degrees + 360) % 360 + this.rotationDegrees = angle + camera.setDisplayOrientation(angle) + parameters.setRotation(angle) + } + + /** + * Creates one buffer for the camera preview callback. The size of the buffer is based off of the + * camera preview size and the format of the camera image. + * + * @return a new preview buffer of the appropriate size for the current camera settings. + */ + private fun createPreviewBuffer(previewSize: Size): ByteArray { + val bitsPerPixel = ImageFormat.getBitsPerPixel(IMAGE_FORMAT) + val sizeInBits = previewSize.height.toLong() * previewSize.width.toLong() * bitsPerPixel.toLong() + val bufferSize = ceil(sizeInBits / 8.0).toInt() + 1 + + // Creating the byte array this way and wrapping it, as opposed to using .allocate(), + // should guarantee that there will be an array to work with. + val byteArray = ByteArray(bufferSize) + val byteBuffer = ByteBuffer.wrap(byteArray) + check(!(!byteBuffer.hasArray() || !byteBuffer.array()!!.contentEquals(byteArray))) { + // This should never happen. If it does, then we wouldn't be passing the preview content to + // the underlying detector later. + "Failed to create valid buffer for camera source." + } + + bytesToByteBuffer[byteArray] = byteBuffer + return byteArray + } + + /** + * This runnable controls access to the underlying receiver, calling it to process frames when + * available from the camera. This is designed to run detection on frames as fast as possible + * (i.e., without unnecessary context switching or waiting on the next frame). + * + * + * While detection is running on a frame, new frames may be received from the camera. As these + * frames come in, the most recent frame is held onto as pending. As soon as detection and its + * associated processing is done for the previous frame, detection on the mostly recently received + * frame will immediately start on the same thread. + */ + private inner class FrameProcessingRunnable internal constructor() : Runnable { + + // This lock guards all of the member variables below. + private val lock = Object() + private var active = true + + // These pending variables hold the state associated with the new frame awaiting processing. + private var pendingFrameData: ByteBuffer? = null + + /** Marks the runnable as active/not active. Signals any blocked threads to continue. */ + internal fun setActive(active: Boolean) { + synchronized(lock) { + this.active = active + lock.notifyAll() + } + } + + /** + * Sets the frame data received from the camera. This adds the previous unused frame buffer (if + * present) back to the camera, and keeps a pending reference to the frame data for future use. + */ + internal fun setNextFrame(data: ByteArray, camera: Camera) { + synchronized(lock) { + pendingFrameData?.let { + camera.addCallbackBuffer(it.array()) + pendingFrameData = null + } + + if (!bytesToByteBuffer.containsKey(data)) { + Log.d( + TAG, + "Skipping frame. Could not find ByteBuffer associated with the image data from the camera." + ) + return + } + + pendingFrameData = bytesToByteBuffer[data] + + // Notify the processor thread if it is waiting on the next frame (see below). + lock.notifyAll() + } + } + + /** + * As long as the processing thread is active, this executes on frames continuously. + * The next pending frame is either immediately available or hasn't been received yet. Once it + * is available, we transfer the frame info to local variables and run detection on that frame. + * It immediately loops back for the next frame without pausing. + * + * + * If detection takes longer than the time in between new frames from the camera, this will + * mean that this loop will run without ever waiting on a frame, avoiding any context switching + * or frame acquisition time latency. + * + * + * If you find that this is using more CPU than you'd like, you should probably decrease the + * FPS setting above to allow for some idle time in between frames. + */ + override fun run() { + var data: ByteBuffer? + + while (true) { + synchronized(lock) { + while (active && pendingFrameData == null) { + try { + // Wait for the next frame to be received from the camera, since we don't have it yet. + lock.wait() + } catch (e: InterruptedException) { + Log.e(TAG, "Frame processing loop terminated.", e) + return + } + } + + if (!active) { + // Exit the loop once this camera source is stopped or released. We check this here, + // immediately after the wait() above, to handle the case where setActive(false) had + // been called, triggering the termination of this loop. + return + } + + // Hold onto the frame data locally, so that we can use this for detection + // below. We need to clear pendingFrameData to ensure that this buffer isn't + // recycled back to the camera before we are done using that data. + data = pendingFrameData + pendingFrameData = null + } + + try { + synchronized(processorLock) { + val frameMetadata = FrameMetadata(previewSize!!.width, previewSize!!.height, rotationDegrees) + data?.let { + frameProcessor?.process(it, frameMetadata, graphicOverlay) + } + } + } catch (t: Exception) { + Log.e(TAG, "Exception thrown from receiver.", t) + } finally { + data?.let { + camera?.addCallbackBuffer(it.array()) + } + } + } + } + } + + companion object { + + const val CAMERA_FACING_BACK = CameraInfo.CAMERA_FACING_BACK + + private const val TAG = "CameraSource" + + private const val IMAGE_FORMAT = ImageFormat.NV21 + private const val MIN_CAMERA_PREVIEW_WIDTH = 400 + private const val MAX_CAMERA_PREVIEW_WIDTH = 1300 + private const val DEFAULT_REQUESTED_CAMERA_PREVIEW_WIDTH = 640 + private const val DEFAULT_REQUESTED_CAMERA_PREVIEW_HEIGHT = 360 + private const val REQUESTED_CAMERA_FPS = 30.0f + + /** + * Selects the most suitable preview and picture size, given the display aspect ratio in landscape + * mode. + * + * + * It's firstly trying to pick the one that has closest aspect ratio to display view with its + * width be in the specified range [[.MIN_CAMERA_PREVIEW_WIDTH], [ ][.MAX_CAMERA_PREVIEW_WIDTH]]. If there're multiple candidates, choose the one having longest + * width. + * + * + * If the above looking up failed, chooses the one that has the minimum sum of the differences + * between the desired values and the actual values for width and height. + * + * + * Even though we only need to find the preview size, it's necessary to find both the preview + * size and the picture size of the camera together, because these need to have the same aspect + * ratio. On some hardware, if you would only set the preview size, you will get a distorted + * image. + * + * @param camera the camera to select a preview size from + * @return the selected preview and picture size pair + */ + private fun selectSizePair(camera: Camera, displayAspectRatioInLandscape: Float): CameraSizePair? { + val validPreviewSizes = generateValidPreviewSizeList(camera) + + var selectedPair: CameraSizePair? = null + // Picks the preview size that has closest aspect ratio to display view. + var minAspectRatioDiff = Float.MAX_VALUE + + for (sizePair in validPreviewSizes) { + val previewSize = sizePair.preview + if (previewSize.width < MIN_CAMERA_PREVIEW_WIDTH || previewSize.width > MAX_CAMERA_PREVIEW_WIDTH) { + continue + } + + val previewAspectRatio = previewSize.width.toFloat() / previewSize.height.toFloat() + val aspectRatioDiff = abs(displayAspectRatioInLandscape - previewAspectRatio) + if (abs(aspectRatioDiff - minAspectRatioDiff) < ASPECT_RATIO_TOLERANCE) { + if (selectedPair == null || selectedPair.preview.width < sizePair.preview.width) { + selectedPair = sizePair + } + } else if (aspectRatioDiff < minAspectRatioDiff) { + minAspectRatioDiff = aspectRatioDiff + selectedPair = sizePair + } + } + + if (selectedPair == null) { + // Picks the one that has the minimum sum of the differences between the desired values and + // the actual values for width and height. + var minDiff = Integer.MAX_VALUE + for (sizePair in validPreviewSizes) { + val size = sizePair.preview + val diff = + abs(size.width - DEFAULT_REQUESTED_CAMERA_PREVIEW_WIDTH) + + abs(size.height - DEFAULT_REQUESTED_CAMERA_PREVIEW_HEIGHT) + if (diff < minDiff) { + selectedPair = sizePair + minDiff = diff + } + } + } + + return selectedPair + } + + /** + * Selects the most suitable preview frames per second range. + * + * @param camera the camera to select a frames per second range from + * @return the selected preview frames per second range + */ + private fun selectPreviewFpsRange(camera: Camera): IntArray? { + // The camera API uses integers scaled by a factor of 1000 instead of floating-point frame + // rates. + val desiredPreviewFpsScaled = (REQUESTED_CAMERA_FPS * 1000f).toInt() + + // The method for selecting the best range is to minimize the sum of the differences between + // the desired value and the upper and lower bounds of the range. This may select a range + // that the desired value is outside of, but this is often preferred. For example, if the + // desired frame rate is 29.97, the range (30, 30) is probably more desirable than the + // range (15, 30). + var selectedFpsRange: IntArray? = null + var minDiff = Integer.MAX_VALUE + for (range in camera.parameters.supportedPreviewFpsRange) { + val deltaMin = desiredPreviewFpsScaled - range[Parameters.PREVIEW_FPS_MIN_INDEX] + val deltaMax = desiredPreviewFpsScaled - range[Parameters.PREVIEW_FPS_MAX_INDEX] + val diff = abs(deltaMin) + abs(deltaMax) + if (diff < minDiff) { + selectedFpsRange = range + minDiff = diff + } + } + return selectedFpsRange + } + } +} diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSourcePreview.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSourcePreview.kt new file mode 100644 index 000000000000..92be41d3a394 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSourcePreview.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package openfoodfacts.github.scrachx.openfood.camera + +import android.content.Context +import android.util.AttributeSet +import android.util.Log +import android.view.SurfaceHolder +import android.view.SurfaceView +import android.widget.FrameLayout +import com.google.android.gms.common.images.Size +import openfoodfacts.github.scrachx.openfood.R +import openfoodfacts.github.scrachx.openfood.utils.CameraUtils.isPortraitMode +import java.io.IOException + +/** Preview the camera image in the screen. */ +class CameraSourcePreview(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs) { + + private val surfaceView: SurfaceView = SurfaceView(context).apply { + holder.addCallback(SurfaceCallback()) + addView(this) + } + private var graphicOverlay: GraphicOverlay? = null + private var startRequested = false + private var surfaceAvailable = false + private var cameraSource: CameraSource? = null + private var cameraPreviewSize: Size? = null + + override fun onFinishInflate() { + super.onFinishInflate() + graphicOverlay = findViewById(R.id.camera_preview_graphic_overlay) + } + + @Throws(IOException::class) + fun start(cameraSource: CameraSource) { + this.cameraSource = cameraSource + startRequested = true + startIfReady() + } + + fun stop() { + cameraSource?.let { + it.stop() + cameraSource = null + startRequested = false + } + } + + @Throws(IOException::class) + private fun startIfReady() { + if (startRequested && surfaceAvailable) { + cameraSource?.start(surfaceView.holder) + requestLayout() + graphicOverlay?.let { overlay -> + cameraSource?.let { + overlay.setCameraInfo(it) + } + overlay.clear() + } + startRequested = false + } + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + val layoutWidth = right - left + val layoutHeight = bottom - top + + cameraSource?.previewSize?.let { cameraPreviewSize = it } + + val previewSizeRatio = cameraPreviewSize?.let { size -> + if (isPortraitMode(context)) { + // Camera's natural orientation is landscape, so need to swap width and height. + size.height.toFloat() / size.width + } else { + size.width.toFloat() / size.height + } + } ?: layoutWidth.toFloat() / layoutHeight.toFloat() + + // Match the width of the child view to its parent. + val childHeight = (layoutWidth / previewSizeRatio).toInt() + if (childHeight <= layoutHeight) { + for (i in 0 until childCount) { + getChildAt(i).layout(0, 0, layoutWidth, childHeight) + } + } else { + // When the child view is too tall to be fitted in its parent: If the child view is + // static overlay view container (contains views such as bottom prompt chip), we apply + // the size of the parent view to it. Otherwise, we offset the top/bottom position + // equally to position it in the center of the parent. + val excessLenInHalf = (childHeight - layoutHeight) / 2 + for (i in 0 until childCount) { + val childView = getChildAt(i) + when (childView.id) { + R.id.static_overlay_container -> { + childView.layout(0, 0, layoutWidth, layoutHeight) + } + else -> { + childView.layout( + 0, -excessLenInHalf, layoutWidth, layoutHeight + excessLenInHalf + ) + } + } + } + } + + try { + startIfReady() + } catch (e: IOException) { + Log.e(TAG, "Could not start camera source.", e) + } + } + + private inner class SurfaceCallback : SurfaceHolder.Callback { + override fun surfaceCreated(surface: SurfaceHolder) { + surfaceAvailable = true + try { + startIfReady() + } catch (e: IOException) { + Log.e(TAG, "Could not start camera source.", e) + } + } + + override fun surfaceDestroyed(surface: SurfaceHolder) { + surfaceAvailable = false + } + + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + } + } + + companion object { + private const val TAG = "CameraSourcePreview" + } +} diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameMetadata.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameMetadata.kt new file mode 100644 index 000000000000..3397ef8dd9b1 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameMetadata.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package openfoodfacts.github.scrachx.openfood.camera + +/** Metadata info of a camera frame. */ +class FrameMetadata(val width: Int, val height: Int, val rotation: Int) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameProcessor.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameProcessor.kt new file mode 100644 index 000000000000..962fcc504af9 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameProcessor.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package openfoodfacts.github.scrachx.openfood.camera + +import java.nio.ByteBuffer + +/** An interface to process the input camera frame and perform detection on it. */ +interface FrameProcessor { + + /** Processes the input frame with the underlying detector. */ + fun process(data: ByteBuffer, frameMetadata: FrameMetadata, graphicOverlay: GraphicOverlay) + + /** Stops the underlying detector and release resources. */ + fun stop() +} diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameProcessorBase.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameProcessorBase.kt new file mode 100644 index 000000000000..0ffd48fe35ee --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameProcessorBase.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package openfoodfacts.github.scrachx.openfood.camera + +import android.os.SystemClock +import android.util.Log +import androidx.annotation.GuardedBy +import com.google.android.gms.tasks.OnFailureListener +import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.TaskExecutors +import com.google.mlkit.vision.common.InputImage +import openfoodfacts.github.scrachx.openfood.utils.CameraInputInfo +import openfoodfacts.github.scrachx.openfood.utils.InputInfo +import openfoodfacts.github.scrachx.openfood.utils.ScopedExecutor +import java.nio.ByteBuffer + +/** Abstract base class of [FrameProcessor]. */ +abstract class +FrameProcessorBase : FrameProcessor { + + // To keep the latest frame and its metadata. + @GuardedBy("this") + private var latestFrame: ByteBuffer? = null + + @GuardedBy("this") + private var latestFrameMetaData: FrameMetadata? = null + + // To keep the frame and metadata in process. + @GuardedBy("this") + private var processingFrame: ByteBuffer? = null + + @GuardedBy("this") + private var processingFrameMetaData: FrameMetadata? = null + private val executor = ScopedExecutor(TaskExecutors.MAIN_THREAD) + + @Synchronized + override fun process( + data: ByteBuffer, + frameMetadata: FrameMetadata, + graphicOverlay: GraphicOverlay + ) { + latestFrame = data + latestFrameMetaData = frameMetadata + if (processingFrame == null && processingFrameMetaData == null) { + processLatestFrame(graphicOverlay) + } + } + + @Synchronized + private fun processLatestFrame(graphicOverlay: GraphicOverlay) { + processingFrame = latestFrame + processingFrameMetaData = latestFrameMetaData + latestFrame = null + latestFrameMetaData = null + val frame = processingFrame ?: return + val frameMetaData = processingFrameMetaData ?: return + val image = InputImage.fromByteBuffer( + frame, + frameMetaData.width, + frameMetaData.height, + frameMetaData.rotation, + InputImage.IMAGE_FORMAT_NV21 + ) + val startMs = SystemClock.elapsedRealtime() + detectInImage(image) + .addOnSuccessListener(executor) { results: T -> + Log.d(TAG, "Latency is: ${SystemClock.elapsedRealtime() - startMs}") + this@FrameProcessorBase.onSuccess(CameraInputInfo(frame, frameMetaData), results, graphicOverlay) + processLatestFrame(graphicOverlay) + } + .addOnFailureListener(executor) { e -> OnFailureListener { this@FrameProcessorBase.onFailure(it) } } + } + + override fun stop() { + executor.shutdown() + } + + protected abstract fun detectInImage(image: InputImage): Task + + /** Be called when the detection succeeds. */ + protected abstract fun onSuccess( + inputInfo: InputInfo, + results: T, + graphicOverlay: GraphicOverlay + ) + + protected abstract fun onFailure(e: Exception) + + companion object { + private const val TAG = "FrameProcessorBase" + } +} diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/GraphicOverlay.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/GraphicOverlay.kt new file mode 100644 index 000000000000..815ae2ffe391 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/GraphicOverlay.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package openfoodfacts.github.scrachx.openfood.camera + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.RectF +import android.util.AttributeSet +import android.view.View +import openfoodfacts.github.scrachx.openfood.utils.CameraUtils.isPortraitMode +import java.util.ArrayList + +/** + * A view which renders a series of custom graphics to be overlaid on top of an associated preview + * (i.e., the camera preview). The creator can add graphics objects, update the objects, and remove + * them, triggering the appropriate drawing and invalidation within the view. + * + * + * Supports scaling and mirroring of the graphics relative the camera's preview properties. The + * idea is that detection items are expressed in terms of a preview size, but need to be scaled up + * to the full view size, and also mirrored in the case of the front-facing camera. + * + * + * Associated [Graphic] items should use [.translateX] and [ ][.translateY] to convert to view coordinate from the preview's coordinate. + */ +class GraphicOverlay(context: Context, attrs: AttributeSet) : View(context, attrs) { + private val lock = Any() + + private var previewWidth: Int = 0 + private var widthScaleFactor = 1.0f + private var previewHeight: Int = 0 + private var heightScaleFactor = 1.0f + private val graphics = ArrayList() + + /** + * Base class for a custom graphics object to be rendered within the graphic overlay. Subclass + * this and implement the [Graphic.draw] method to define the graphics element. Add + * instances to the overlay using [GraphicOverlay.add]. + */ + abstract class Graphic protected constructor(protected val overlay: GraphicOverlay) { + protected val context: Context = overlay.context + + /** Draws the graphic on the supplied canvas. */ + abstract fun draw(canvas: Canvas) + } + + /** Removes all graphics from the overlay. */ + fun clear() { + synchronized(lock) { + graphics.clear() + } + postInvalidate() + } + + /** Adds a graphic to the overlay. */ + fun add(graphic: Graphic) { + synchronized(lock) { + graphics.add(graphic) + } + } + + /** + * Sets the camera attributes for size and facing direction, which informs how to transform image + * coordinates later. + */ + fun setCameraInfo(cameraSource: CameraSource) { + val previewSize = cameraSource.previewSize ?: return + if (isPortraitMode(context)) { + // Swap width and height when in portrait, since camera's natural orientation is landscape. + previewWidth = previewSize.height + previewHeight = previewSize.width + } else { + previewWidth = previewSize.width + previewHeight = previewSize.height + } + } + + fun translateX(x: Float): Float = x * widthScaleFactor + fun translateY(y: Float): Float = y * heightScaleFactor + + /** + * Adjusts the `rect`'s coordinate from the preview's coordinate system to the view + * coordinate system. + */ + fun translateRect(rect: Rect) = RectF( + translateX(rect.left.toFloat()), + translateY(rect.top.toFloat()), + translateX(rect.right.toFloat()), + translateY(rect.bottom.toFloat()) + ) + + /** Draws the overlay with its associated graphic objects. */ + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + if (previewWidth > 0 && previewHeight > 0) { + widthScaleFactor = width.toFloat() / previewWidth + heightScaleFactor = height.toFloat() / previewHeight + } + + synchronized(lock) { + graphics.forEach { it.draw(canvas) } + } + } +} diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/WorkflowModel.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/WorkflowModel.kt new file mode 100644 index 000000000000..d50893917447 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/WorkflowModel.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package openfoodfacts.github.scrachx.openfood.camera + +import android.app.Application +import android.content.Context +import androidx.annotation.MainThread +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import com.google.mlkit.vision.barcode.Barcode + +/** View model for handling application workflow based on camera preview. */ +class WorkflowModel(application: Application) : AndroidViewModel(application) { + + val workflowState = MutableLiveData() + val detectedBarcode = MutableLiveData() + + var isCameraLive = false + private set + + + private val context: Context + get() = getApplication().applicationContext + + /** + * State set of the application workflow. + */ + enum class WorkflowState { + NOT_STARTED, + DETECTING, + DETECTED, + CONFIRMING, + CONFIRMED, + SEARCHING, + SEARCHED + } + + @MainThread + fun setWorkflowState(workflowState: WorkflowState) { + this.workflowState.value = workflowState + } + + + fun markCameraLive() { + isCameraLive = true + } + + fun markCameraFrozen() { + isCameraLive = false + } + +} diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/PreferencesFragment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/PreferencesFragment.kt index 279bcd250e3d..f112a64ce2a9 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/PreferencesFragment.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/PreferencesFragment.kt @@ -158,7 +158,7 @@ class PreferencesFragment : PreferenceFragmentCompat(), INavigationItem, OnShare content(R.string.pref_mlkit) positiveText("Proceed") onPositive { _, _ -> - it.isChecked = false + it.isChecked = true settings.edit { putBoolean(getString(R.string.pref_scanner_type_key), newValue as Boolean) } Toast.makeText(requireActivity(), getString(R.string.changes_saved), Toast.LENGTH_SHORT).show() } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt index 69d7404c9e63..0c7f98d5b1b0 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt @@ -17,11 +17,7 @@ package openfoodfacts.github.scrachx.openfood.features.scan import android.content.Context import android.content.Intent -import android.graphics.Bitmap import android.hardware.Camera -import android.hardware.camera2.CameraCaptureSession -import android.hardware.camera2.CameraDevice -import android.hardware.camera2.CameraManager import android.os.Bundle import android.util.Log import android.view.* @@ -40,12 +36,12 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.commit +import androidx.lifecycle.ViewModelProviders import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback import com.google.android.material.bottomsheet.BottomSheetBehavior.from import com.google.mlkit.vision.barcode.* -import com.google.mlkit.vision.common.InputImage import com.google.zxing.BarcodeFormat import com.google.zxing.ResultPoint import com.google.zxing.client.android.BeepManager @@ -56,11 +52,6 @@ import com.mikepenz.iconics.IconicsColor import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsSize import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial -import io.fotoapparat.Fotoapparat -import io.fotoapparat.configuration.CameraConfiguration -import io.fotoapparat.configuration.UpdateConfiguration -import io.fotoapparat.parameter.ScaleType -import io.fotoapparat.selector.* import io.reactivex.Completable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable @@ -70,6 +61,11 @@ import openfoodfacts.github.scrachx.openfood.AppFlavors.OFF import openfoodfacts.github.scrachx.openfood.AppFlavors.isFlavors import openfoodfacts.github.scrachx.openfood.R import openfoodfacts.github.scrachx.openfood.app.OFFApplication +import openfoodfacts.github.scrachx.openfood.camera.CameraSource +import openfoodfacts.github.scrachx.openfood.camera.CameraSourcePreview +import openfoodfacts.github.scrachx.openfood.camera.GraphicOverlay +import openfoodfacts.github.scrachx.openfood.camera.WorkflowModel +import openfoodfacts.github.scrachx.openfood.camera.WorkflowModel.* import openfoodfacts.github.scrachx.openfood.databinding.ActivityContinuousScanBinding import openfoodfacts.github.scrachx.openfood.features.ImagesManageActivity import openfoodfacts.github.scrachx.openfood.features.compare.ProductCompareActivity @@ -92,6 +88,7 @@ import openfoodfacts.github.scrachx.openfood.models.entities.analysistagconfig.A import openfoodfacts.github.scrachx.openfood.models.eventbus.ProductNeedsRefreshEvent import openfoodfacts.github.scrachx.openfood.network.ApiFields import openfoodfacts.github.scrachx.openfood.network.OpenFoodAPIClient +import openfoodfacts.github.scrachx.openfood.scanner.BarcodeProcessor import openfoodfacts.github.scrachx.openfood.utils.* import openfoodfacts.github.scrachx.openfood.utils.Utils.daoSession import org.greenrobot.eventbus.EventBus @@ -117,7 +114,12 @@ class ContinuousScanActivity : AppCompatActivity() { private val settings by lazy { getSharedPreferences("prefs", 0) } - private lateinit var fotoapparat: Fotoapparat + private var cameraSource: CameraSource? = null + private var preview: CameraSourcePreview? = null + private var graphicOverlay: GraphicOverlay? = null + private var workflowModel: WorkflowModel? = null + private var currentWorkflowState: WorkflowState? = null + private val commonDisp = CompositeDisposable() private var productDisp: Disposable? = null @@ -154,7 +156,7 @@ class ContinuousScanActivity : AppCompatActivity() { internal fun showProduct(barcode: String) { productShowing = true binding.barcodeScanner.visibility = View.GONE - binding.cameraView.visibility = View.GONE + binding.cameraPreview.visibility = View.GONE binding.barcodeScanner.pause() binding.imageForScreenshotGenerationOnly.visibility = View.VISIBLE setShownProduct(barcode) @@ -496,7 +498,7 @@ class ContinuousScanActivity : AppCompatActivity() { if (!useMLScanner) { binding.barcodeScanner.visibility = View.VISIBLE - binding.cameraView.visibility = View.GONE + binding.cameraPreview.visibility = View.GONE binding.barcodeScanner.barcodeView.decoderFactory = DefaultDecoderFactory(BARCODE_FORMATS) binding.barcodeScanner.setStatusText(null) binding.barcodeScanner.barcodeView.cameraSettings.run { @@ -507,30 +509,22 @@ class ContinuousScanActivity : AppCompatActivity() { // Start continuous scanner binding.barcodeScanner.decodeContinuous(barcodeScanCallback) beepManager = BeepManager(this) + } else { binding.barcodeScanner.visibility = View.GONE - binding.cameraView.visibility = View.VISIBLE - - val config = CameraConfiguration( - flashMode = if(flashActive) torch() else off(), - previewResolution = highestResolution(), // we want to have the highest possible preview resolution - previewFpsRange = highestFps(), // we want to have the best frame rate - focusMode = firstAvailable( // use the first focus mode which is supported by device - continuousFocusVideo(), - autoFocus(), // if continuous focus is not available on device, auto focus will be used - fixed() // if even auto focus is not available - fixed focus mode will be used - ), - frameProcessor = { frame -> - processFrame(frame.image, frame.rotation, frame.size.height, frame.size.width) - } - ) - - fotoapparat = Fotoapparat( - context = this, - cameraConfiguration = config, - scaleType = ScaleType.CenterCrop, - view = binding.cameraView - ) + binding.cameraPreview.visibility = View.VISIBLE + + preview = binding.cameraPreview + graphicOverlay = findViewById(R.id.camera_preview_graphic_overlay).apply { +// setOnClickListener(this@Conti) + Log.i("inside","else-- before cameraSource") + cameraSource = CameraSource(this) + Log.i("inside","else-- after cameraSource") + + } + + setUpWorkflowModel() + } binding.quickViewSearchByBarcode.setOnEditorActionListener(barcodeInputListener) @@ -539,37 +533,67 @@ class ContinuousScanActivity : AppCompatActivity() { setupPopupMenu() } - private fun processFrame(byteArray:ByteArray, rotation:Int, height:Int, width:Int) - { - val inputImage = InputImage.fromByteArray( - byteArray, - width, - height, - rotation, - InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12 - ) + private fun setUpWorkflowModel() { + workflowModel = ViewModelProviders.of(this).get(WorkflowModel::class.java) + + // Observes the workflow state changes, if happens, update the overlay view indicators and + // camera preview state. + workflowModel!!.workflowState.observe(this, androidx.lifecycle.Observer{ workflowState -> + if (workflowState == null ) { + return@Observer + } + + currentWorkflowState = workflowState + + when (workflowState) { + WorkflowState.DETECTING -> { + startCameraPreview() + } + WorkflowState.CONFIRMING -> { + startCameraPreview() + } + WorkflowState.SEARCHING -> { + stopCameraPreview() + } + WorkflowState.DETECTED, WorkflowState.SEARCHED -> { + stopCameraPreview() + } + } + + }) + + workflowModel?.detectedBarcode?.observe(this, { barcode -> + if (barcode != null) { + mlBarcodeCallback(barcode.rawValue ) + Log.i("inside","barcode "+barcode.rawValue) + + } + }) + } + - val options = BarcodeScannerOptions.Builder() - .setBarcodeFormats(Barcode.FORMAT_UPC_A, - Barcode.FORMAT_UPC_E, - Barcode.FORMAT_EAN_13, - Barcode.FORMAT_EAN_8, - Barcode.FORMAT_CODE_39, - Barcode.FORMAT_CODE_93, - Barcode.FORMAT_CODE_128) - .build() - - val barcodeScanner = BarcodeScanning.getClient(options) - - val task = barcodeScanner.process(inputImage) - task.addOnSuccessListener { barCodesList -> - for (barcodeObject in barCodesList) { - val barcodeValue = barcodeObject.rawValue - Log.d("Barcode", "The code "+ barcodeValue.toString()) - mlBarcodeCallback(barcodeValue) + private fun startCameraPreview() { + val workflowModel = this.workflowModel ?: return + val cameraSource = this.cameraSource ?: return + if (!workflowModel.isCameraLive) { + try { + workflowModel.markCameraLive() + preview?.start(cameraSource) + } catch (e: IOException) { + Log.e("ContinuousScanActivity", "Failed to start camera preview!", e) + cameraSource.release() + this.cameraSource = null } } + } + private fun stopCameraPreview() { + val workflowModel = this.workflowModel ?: return + if (workflowModel.isCameraLive) { + workflowModel.markCameraFrozen() +// flashButton?.isSelected = false + preview?.stop() + } } private fun mlBarcodeCallback(barcodeValue:String?){ @@ -590,19 +614,30 @@ class ContinuousScanActivity : AppCompatActivity() { } override fun onStart() { + Log.i("inside","onstart") super.onStart() - if(useMLScanner) { - fotoapparat.start() - } EventBus.getDefault().register(this) } override fun onResume() { + Log.i("inside","onresume") super.onResume() binding.bottomNavigation.bottomNavigation.selectNavigationItem(R.id.scan_bottom_nav) if(!useMLScanner && quickViewBehavior.state != BottomSheetBehavior.STATE_EXPANDED) { binding.barcodeScanner.resume() } + else if(useMLScanner && quickViewBehavior.state != BottomSheetBehavior.STATE_EXPANDED) { + workflowModel?.markCameraFrozen() + currentWorkflowState = WorkflowState.NOT_STARTED + cameraSource?.setFrameProcessor(BarcodeProcessor(graphicOverlay!!, workflowModel!!)) + workflowModel?.setWorkflowState(WorkflowState.DETECTING) + } + } + + override fun onPostResume() { + super.onPostResume() + // Back to working state after the bottom sheet is dismissed. + ViewModelProviders.of(this).get(WorkflowModel::class.java).setWorkflowState(WorkflowState.DETECTING) } override fun onSaveInstanceState(outState: Bundle) { @@ -614,20 +649,26 @@ class ContinuousScanActivity : AppCompatActivity() { if(!useMLScanner ) { binding.barcodeScanner.pause() } + else{ + Log.i("inside","onPause") + currentWorkflowState = WorkflowState.NOT_STARTED + stopCameraPreview() + } super.onPause() } override fun onStop() { EventBus.getDefault().unregister(this) - if(useMLScanner) { - fotoapparat.stop() - } super.onStop() } override fun onDestroy() { + Log.i("inside","onDestroy") summaryProductPresenter?.dispose() + cameraSource?.release() + cameraSource = null + // Dispose all RxJava disposable hintBarcodeDisp?.dispose() commonDisp.dispose() @@ -705,23 +746,17 @@ class ContinuousScanActivity : AppCompatActivity() { binding.barcodeScanner.barcodeView.cameraSettings = settings cameraPref.edit { putInt(SETTING_STATE, cameraState) } binding.barcodeScanner.resume() - } else{ - fotoapparat.switchTo( - if () - lensPosition = front() - ) - - } + } private fun toggleFlash() { cameraPref.edit { if (flashActive) { if(useMLScanner){ - fotoapparat.updateConfiguration( - UpdateConfiguration( flashMode= off() ) - ) +// fotoapparat.updateConfiguration( +// UpdateConfiguration( flashMode= off() ) +// ) } else { binding.barcodeScanner.setTorchOff() @@ -731,9 +766,9 @@ class ContinuousScanActivity : AppCompatActivity() { putBoolean(SETTING_FLASH, false) } else { if(useMLScanner){ - fotoapparat.updateConfiguration( - UpdateConfiguration( flashMode= torch() ) - ) +// fotoapparat.updateConfiguration( +// UpdateConfiguration( flashMode= torch() ) +// ) } else { binding.barcodeScanner.setTorchOn() diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeConfirmingGraphic.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeConfirmingGraphic.kt new file mode 100644 index 000000000000..af39b602e603 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeConfirmingGraphic.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package openfoodfacts.github.scrachx.openfood.scanner + +import android.graphics.Canvas +import android.graphics.Path +import com.google.mlkit.vision.barcode.Barcode +import openfoodfacts.github.scrachx.openfood.camera.GraphicOverlay +import openfoodfacts.github.scrachx.openfood.utils.CameraPreferenceUtils + +/** Guides user to move camera closer to confirm the detected barcode. */ +internal class BarcodeConfirmingGraphic(overlay: GraphicOverlay, private val barcode: Barcode) : + BarcodeGraphicBase(overlay) { + + override fun draw(canvas: Canvas) { + super.draw(canvas) + + // Draws a highlighted path to indicate the current progress to meet size requirement. + val sizeProgress = CameraPreferenceUtils.getProgressToMeetBarcodeSizeRequirement(overlay, barcode) + val path = Path() + if (sizeProgress > 0.95f) { + // To have a completed path with all corners rounded. + path.moveTo(boxRect.left, boxRect.top) + path.lineTo(boxRect.right, boxRect.top) + path.lineTo(boxRect.right, boxRect.bottom) + path.lineTo(boxRect.left, boxRect.bottom) + path.close() + } else { + path.moveTo(boxRect.left, boxRect.top + boxRect.height() * sizeProgress) + path.lineTo(boxRect.left, boxRect.top) + path.lineTo(boxRect.left + boxRect.width() * sizeProgress, boxRect.top) + + path.moveTo(boxRect.right, boxRect.bottom - boxRect.height() * sizeProgress) + path.lineTo(boxRect.right, boxRect.bottom) + path.lineTo(boxRect.right - boxRect.width() * sizeProgress, boxRect.bottom) + } + canvas.drawPath(path, pathPaint) + } +} diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeGraphicBase.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeGraphicBase.kt new file mode 100644 index 000000000000..cacf04f92fb1 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeGraphicBase.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package openfoodfacts.github.scrachx.openfood.scanner + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.CornerPathEffect +import android.graphics.Paint +import android.graphics.Paint.Style +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.RectF +import androidx.core.content.ContextCompat +import openfoodfacts.github.scrachx.openfood.R +import openfoodfacts.github.scrachx.openfood.camera.GraphicOverlay +import openfoodfacts.github.scrachx.openfood.utils.CameraPreferenceUtils.getBarcodeReticleBox + +internal abstract class BarcodeGraphicBase(overlay: GraphicOverlay) : GraphicOverlay.Graphic(overlay) { + + private val boxPaint: Paint = Paint().apply { + color = ContextCompat.getColor(context, R.color.barcode_reticle_stroke) + style = Style.STROKE + strokeWidth = context.resources.getDimensionPixelOffset(R.dimen.barcode_reticle_stroke_width).toFloat() + } + + private val scrimPaint: Paint = Paint().apply { + color = ContextCompat.getColor(context, R.color.barcode_reticle_background) + } + + private val eraserPaint: Paint = Paint().apply { + strokeWidth = boxPaint.strokeWidth + xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) + } + + val boxCornerRadius: Float = + context.resources.getDimensionPixelOffset(R.dimen.barcode_reticle_corner_radius).toFloat() + + val pathPaint: Paint = Paint().apply { + color = Color.WHITE + style = Style.STROKE + strokeWidth = boxPaint.strokeWidth + pathEffect = CornerPathEffect(boxCornerRadius) + } + + val boxRect: RectF = getBarcodeReticleBox(overlay) + + override fun draw(canvas: Canvas) { + // Draws the dark background scrim and leaves the box area clear. + canvas.drawRect(0f, 0f, canvas.width.toFloat(), canvas.height.toFloat(), scrimPaint) + // As the stroke is always centered, so erase twice with FILL and STROKE respectively to clear + // all area that the box rect would occupy. + eraserPaint.style = Style.FILL + canvas.drawRoundRect(boxRect, boxCornerRadius, boxCornerRadius, eraserPaint) + eraserPaint.style = Style.STROKE + canvas.drawRoundRect(boxRect, boxCornerRadius, boxCornerRadius, eraserPaint) + // Draws the box. + canvas.drawRoundRect(boxRect, boxCornerRadius, boxCornerRadius, boxPaint) + } +} diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeProcessor.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeProcessor.kt new file mode 100644 index 000000000000..ba7b0318fa5a --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeProcessor.kt @@ -0,0 +1,98 @@ +package openfoodfacts.github.scrachx.openfood.scanner + +import android.util.Log +import androidx.annotation.MainThread +import com.google.android.gms.tasks.Task +import com.google.mlkit.vision.barcode.Barcode +import com.google.mlkit.vision.barcode.BarcodeScannerOptions +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.common.InputImage +import openfoodfacts.github.scrachx.openfood.camera.CameraReticleAnimator +import openfoodfacts.github.scrachx.openfood.camera.FrameProcessorBase +import openfoodfacts.github.scrachx.openfood.camera.GraphicOverlay +import openfoodfacts.github.scrachx.openfood.camera.WorkflowModel +import openfoodfacts.github.scrachx.openfood.camera.WorkflowModel.WorkflowState +import openfoodfacts.github.scrachx.openfood.utils.CameraPreferenceUtils +import openfoodfacts.github.scrachx.openfood.utils.InputInfo +import java.io.IOException + + +/** A processor to run the barcode detector. */ +class BarcodeProcessor(graphicOverlay: GraphicOverlay, private val workflowModel: WorkflowModel) : + FrameProcessorBase>() { + + private val options = BarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_UPC_A, + Barcode.FORMAT_UPC_E, + Barcode.FORMAT_EAN_13, + Barcode.FORMAT_EAN_8, + Barcode.FORMAT_CODE_39, + Barcode.FORMAT_CODE_93, + Barcode.FORMAT_CODE_128) + .build() + + private val scanner = BarcodeScanning.getClient(options) + private val cameraReticleAnimator: CameraReticleAnimator = CameraReticleAnimator(graphicOverlay) + + override fun detectInImage(image: InputImage): Task> = + scanner.process(image) + + @MainThread + override fun onSuccess( + inputInfo: InputInfo, + results: List, + graphicOverlay: GraphicOverlay + ) { + + if (!workflowModel.isCameraLive) return + + Log.d(TAG, "Barcode result size: ${results.size}") + + // Picks the barcode, if exists, that covers the center of graphic overlay. + + val barcodeInCenter = results.firstOrNull { barcode -> + val boundingBox = barcode.boundingBox ?: return@firstOrNull false + val box = graphicOverlay.translateRect(boundingBox) + box.contains(graphicOverlay.width / 2f, graphicOverlay.height / 2f) + } + + graphicOverlay.clear() + if (barcodeInCenter == null) { + cameraReticleAnimator.start() + graphicOverlay.add(BarcodeReticleGraphic(graphicOverlay, cameraReticleAnimator)) + workflowModel.setWorkflowState(WorkflowModel.WorkflowState.DETECTING) + } else { + cameraReticleAnimator.cancel() + val sizeProgress = CameraPreferenceUtils.getProgressToMeetBarcodeSizeRequirement(graphicOverlay, barcodeInCenter) + if (sizeProgress < 1) { + // Barcode in the camera view is too small, so prompt user to move camera closer. + graphicOverlay.add(BarcodeConfirmingGraphic(graphicOverlay, barcodeInCenter)) + workflowModel.setWorkflowState(WorkflowState.CONFIRMING) + } else { + // Barcode size in the camera view is sufficient. + workflowModel.setWorkflowState(WorkflowState.DETECTED) + workflowModel.detectedBarcode.setValue(barcodeInCenter) + + } + } + graphicOverlay.invalidate() + } + + override fun onFailure(e: Exception) { + Log.e(TAG, "Barcode detection failed!", e) + } + + override fun stop() { + super.stop() + try { + scanner.close() + } catch (e: IOException) { + Log.e(TAG, "Failed to close barcode detector!", e) + } + } + + companion object { + private const val TAG = "BarcodeProcessor" + } + +} \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeReticleGraphic.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeReticleGraphic.kt new file mode 100644 index 000000000000..f68ca0fedef6 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeReticleGraphic.kt @@ -0,0 +1,62 @@ +// * Copyright 2020 Google LLC +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * https://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. + +package openfoodfacts.github.scrachx.openfood.scanner + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Paint.Style +import android.graphics.RectF +import androidx.core.content.ContextCompat +import openfoodfacts.github.scrachx.openfood.R +import openfoodfacts.github.scrachx.openfood.camera.CameraReticleAnimator +import openfoodfacts.github.scrachx.openfood.camera.GraphicOverlay + +/** + * A camera reticle that locates at the center of canvas to indicate the system is active but has + * not detected a barcode yet. + */ +internal class BarcodeReticleGraphic(overlay: GraphicOverlay, private val animator: CameraReticleAnimator) : + BarcodeGraphicBase(overlay) { + + private val ripplePaint: Paint + private val rippleSizeOffset: Int + private val rippleStrokeWidth: Int + private val rippleAlpha: Int + + init { + val resources = overlay.resources + ripplePaint = Paint() + ripplePaint.style = Style.STROKE + ripplePaint.color = ContextCompat.getColor(context, R.color.reticle_ripple) + rippleSizeOffset = resources.getDimensionPixelOffset(R.dimen.barcode_reticle_ripple_size_offset) + rippleStrokeWidth = resources.getDimensionPixelOffset(R.dimen.nav_bar_height) + rippleAlpha = ripplePaint.alpha + } + + override fun draw(canvas: Canvas) { + super.draw(canvas) + // Draws the ripple to simulate the breathing animation effect. + ripplePaint.alpha = (rippleAlpha * animator.rippleAlphaScale).toInt() + ripplePaint.strokeWidth = rippleStrokeWidth * animator.rippleStrokeWidthScale + val offset = rippleSizeOffset * animator.rippleSizeScale + val rippleRect = RectF( + boxRect.left - offset, + boxRect.top - offset, + boxRect.right + offset, + boxRect.bottom + offset + ) + canvas.drawRoundRect(rippleRect, boxCornerRadius, boxCornerRadius, ripplePaint) + } +} diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/CameraPreferenceUtils.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/CameraPreferenceUtils.kt new file mode 100644 index 000000000000..0dc4c9d4123d --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/CameraPreferenceUtils.kt @@ -0,0 +1,71 @@ +package openfoodfacts.github.scrachx.openfood.utils + +import android.content.Context +import android.graphics.RectF +import android.preference.PreferenceManager +import androidx.annotation.StringRes +import com.google.android.gms.common.images.Size +import com.google.mlkit.vision.barcode.Barcode +import openfoodfacts.github.scrachx.openfood.R +import openfoodfacts.github.scrachx.openfood.camera.CameraSizePair +import openfoodfacts.github.scrachx.openfood.camera.GraphicOverlay + +object CameraPreferenceUtils { + + fun saveStringPreference(context: Context, @StringRes prefKeyId: Int, value: String?) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putString(context.getString(prefKeyId), value) + .apply() + } + + + fun getUserSpecifiedPreviewSize(context: Context): CameraSizePair? { + return try { + val previewSizePrefKey = context.getString(R.string.pref_key_rear_camera_preview_size) + val pictureSizePrefKey = context.getString(R.string.pref_key_rear_camera_picture_size) + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + CameraSizePair( + Size.parseSize(sharedPreferences.getString(previewSizePrefKey, null)), + Size.parseSize(sharedPreferences.getString(pictureSizePrefKey, null)) + ) + } catch (e: Exception) { + null + } + } + + fun getProgressToMeetBarcodeSizeRequirement( + overlay: GraphicOverlay, + barcode: Barcode + ): Float { + val context = overlay.context + return if (getBooleanPref(context, R.string.pref_key_enable_barcode_size_check, false)) { + val reticleBoxWidth = getBarcodeReticleBox(overlay).width() + val barcodeWidth = overlay.translateX(barcode.boundingBox?.width()?.toFloat() ?: 0f) + val requiredWidth = reticleBoxWidth * getIntPref(context, R.string.pref_key_minimum_barcode_width, 50) / 100 + (barcodeWidth / requiredWidth).coerceAtMost(1f) + } else { + 1f + } + } + + private fun getBooleanPref(context: Context, @StringRes prefKeyId: Int, defaultValue: Boolean): Boolean = + PreferenceManager.getDefaultSharedPreferences(context).getBoolean(context.getString(prefKeyId), defaultValue) + + private fun getIntPref(context: Context, @StringRes prefKeyId: Int, defaultValue: Int): Int { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + val prefKey = context.getString(prefKeyId) + return sharedPreferences.getInt(prefKey, defaultValue) + } + + fun getBarcodeReticleBox(overlay: GraphicOverlay): RectF { + val context = overlay.context + val overlayWidth = overlay.width.toFloat() + val overlayHeight = overlay.height.toFloat() + val boxWidth = overlayWidth * getIntPref(context, R.string.pref_key_barcode_reticle_width, 80) / 100 + val boxHeight = overlayHeight * getIntPref(context, R.string.pref_key_barcode_reticle_height, 35) / 100 + val cx = overlayWidth / 2 + val cy = overlayHeight / 2 + return RectF(cx - boxWidth / 2, cy - boxHeight / 2, cx + boxWidth / 2, cy + boxHeight / 2) + } +} \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/CameraUtils.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/CameraUtils.kt new file mode 100644 index 000000000000..2a18f30f7eb8 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/CameraUtils.kt @@ -0,0 +1,104 @@ +package openfoodfacts.github.scrachx.openfood.utils + +import android.content.Context +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.graphics.* +import android.hardware.Camera +import android.util.Log +import com.google.mlkit.vision.common.InputImage +import openfoodfacts.github.scrachx.openfood.camera.CameraSizePair +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +import java.util.ArrayList +import kotlin.math.abs + + + +object CameraUtils { + + /** + * If the absolute difference between aspect ratios is less than this tolerance, they are + * considered to be the same aspect ratio. + */ + const val ASPECT_RATIO_TOLERANCE = 0.01f + + + /** + * Check if the camera is in portrait mode. + * + * @return true if installed, false otherwise. + */ + fun isPortraitMode(context: Context): Boolean = + context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT + + + /** + * Generates a list of acceptable preview sizes. Preview sizes are not acceptable if there is not + * a corresponding picture size of the same aspect ratio. If there is a corresponding picture size + * of the same aspect ratio, the picture size is paired up with the preview size. + * + * + * This is necessary because even if we don't use still pictures, the still picture size must + * be set to a size that is the same aspect ratio as the preview size we choose. Otherwise, the + * preview images may be distorted on some devices. + */ + fun generateValidPreviewSizeList(camera: Camera): List { + val parameters = camera.parameters + val supportedPreviewSizes = parameters.supportedPreviewSizes + val supportedPictureSizes = parameters.supportedPictureSizes + val validPreviewSizes = ArrayList() + for (previewSize in supportedPreviewSizes) { + val previewAspectRatio = previewSize.width.toFloat() / previewSize.height.toFloat() + + // By looping through the picture sizes in order, we favor the higher resolutions. + // We choose the highest resolution in order to support taking the full resolution + // picture later. + for (pictureSize in supportedPictureSizes) { + val pictureAspectRatio = pictureSize.width.toFloat() / pictureSize.height.toFloat() + if (abs(previewAspectRatio - pictureAspectRatio) < ASPECT_RATIO_TOLERANCE) { + validPreviewSizes.add(CameraSizePair(previewSize, pictureSize)) + break + } + } + } + // If there are no picture sizes with the same aspect ratio as any preview sizes, allow all of + // the preview sizes and hope that the camera can handle it. Probably unlikely, but we still + // account for it. + if (validPreviewSizes.isEmpty()) { + Log.w("Utils", "No preview sizes have a corresponding same-aspect-ratio picture size.") + for (previewSize in supportedPreviewSizes) { + // The null picture size will let us know that we shouldn't set a picture size. + validPreviewSizes.add(CameraSizePair(previewSize, null)) + } + } + + return validPreviewSizes + } + + + /** Convert NV21 format byte buffer to bitmap. */ + fun convertToBitmap(data: ByteBuffer, width: Int, height: Int, rotationDegrees: Int): Bitmap? { + data.rewind() + val imageInBuffer = ByteArray(data.limit()) + data.get(imageInBuffer, 0, imageInBuffer.size) + try { + val image = YuvImage( + imageInBuffer, InputImage.IMAGE_FORMAT_NV21, width, height, null + ) + val stream = ByteArrayOutputStream() + image.compressToJpeg(Rect(0, 0, width, height), 80, stream) + val bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size()) + stream.close() + + // Rotate the image back to straight. + val matrix = Matrix() + matrix.postRotate(rotationDegrees.toFloat()) + return Bitmap.createBitmap(bmp, 0, 0, bmp.width, bmp.height, matrix, true) + } catch (e: java.lang.Exception) { + Log.e("Camera Utils", "Error: " + e.message) + } + return null + } + +} \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/ScopedExecutor.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/ScopedExecutor.kt new file mode 100644 index 000000000000..d436a5233a6a --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/ScopedExecutor.kt @@ -0,0 +1,37 @@ +package openfoodfacts.github.scrachx.openfood.utils + +import java.util.concurrent.Executor +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Wraps an existing executor to provide a [.shutdown] method that allows subsequent + * cancellation of submitted runnables. + */ +class ScopedExecutor(private val executor: Executor) : Executor { + private val shutdown = AtomicBoolean() + override fun execute(command: Runnable) { + // Return early if this object has been shut down. + if (shutdown.get()) { + return + } + executor.execute { + + // Check again in case it has been shut down in the mean time. + if (shutdown.get()) { + return@execute + } + command.run() + } + } + + /** + * After this method is called, no runnables that have been submitted or are subsequently + * submitted will start to execute, turning this executor into a no-op. + * + * + * Runnables that have already started to execute will continue. + */ + fun shutdown() { + shutdown.set(true) + } +} \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/inputInfo.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/inputInfo.kt new file mode 100644 index 000000000000..6f9a3868f618 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/inputInfo.kt @@ -0,0 +1,33 @@ +package openfoodfacts.github.scrachx.openfood.utils + +import android.graphics.Bitmap +import openfoodfacts.github.scrachx.openfood.camera.FrameMetadata +import java.nio.ByteBuffer + +interface InputInfo { + fun getBitmap(): Bitmap +} + +class CameraInputInfo( + private val frameByteBuffer: ByteBuffer, + private val frameMetadata: FrameMetadata +) : InputInfo { + + private var bitmap: Bitmap? = null + + @Synchronized + override fun getBitmap(): Bitmap { + return bitmap ?: let { + bitmap = CameraUtils.convertToBitmap( + frameByteBuffer, frameMetadata.width, frameMetadata.height, frameMetadata.rotation + ) + bitmap!! + } + } +} + +class BitmapInputInfo(private val bitmap: Bitmap) : InputInfo { + override fun getBitmap(): Bitmap { + return bitmap + } +} diff --git a/app/src/main/res/layout/activity_continuous_scan.xml b/app/src/main/res/layout/activity_continuous_scan.xml index db5e51777c8a..4d9cb24f7959 100644 --- a/app/src/main/res/layout/activity_continuous_scan.xml +++ b/app/src/main/res/layout/activity_continuous_scan.xml @@ -41,10 +41,14 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> - + android:layout_height="match_parent"> + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 6741341d4f92..3ffd074ac4e0 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -34,6 +34,7 @@ #ffffff #000000 #808080 + #00000000 #FF0000 #FF6600 @@ -61,6 +62,13 @@ #d7ccc8 + #9AFFFFFF + #1F000000 + #9AFFFFFF + @color/white + #99000000 + #40000000 + #fafafa #f5f5f5 #eeeeee diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 7ddcfc50e2dc..ae9bbd410cdd 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -70,4 +70,9 @@ 12sp 12sp + + 4dp + 40dp + 4dp + 8dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aeab3dc791d5..9bbd17878f4f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1095,4 +1095,28 @@ (Required to calculate the Eco-Score) Examples: USA, France, Italy Send a report email + + + Camera + rcpvs + rcpts + Rear camera preview size + + + Barcode detection + barcode_brw + Barcode reticle width + Relative to the camera view width, ranges from 50% to 95% + barcode_brh + Barcode reticle height + Relative to the camera view height, ranges from 20% to 80% + barcode_ebsc + Enable barcode size check + Will prompt user to move camera closer if the detected barcode is too small + barcode_mbw + Minimum barcode width + Relative to the reticle width, ranges from 20% to 80% (only applicable when barcode size check enabled) + barcode_dlbr + Delay loading barcode result + Will show the loading spinner for 2s From a04431422b33f61947166568fc6d6520a5607412 Mon Sep 17 00:00:00 2001 From: Kartikay Sharma Date: Sun, 14 Feb 2021 04:56:16 +0530 Subject: [PATCH 03/12] stable --- .../scrachx/openfood/camera/CameraSource.kt | 33 ++++++--- .../scrachx/openfood/camera/WorkflowModel.kt | 1 - .../features/scan/ContinuousScanActivity.kt | 68 +++++++++++++++---- .../openfood/scanner/BarcodeProcessor.kt | 4 +- .../openfood/scanner/BarcodeReticleGraphic.kt | 2 +- .../openfood/utils/CameraPreferenceUtils.kt | 3 +- .../res/layout/camera_preview_overlay.xml | 2 +- 7 files changed, 84 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt index bad4df0a6457..baa36dbf45a4 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt @@ -21,6 +21,7 @@ import android.graphics.ImageFormat import android.hardware.Camera import android.hardware.Camera.CameraInfo import android.hardware.Camera.Parameters +import android.util.DisplayMetrics import android.util.Log import android.view.Surface import android.view.SurfaceHolder @@ -33,10 +34,11 @@ import openfoodfacts.github.scrachx.openfood.utils.CameraUtils.generateValidPrev import openfoodfacts.github.scrachx.openfood.utils.CameraUtils.isPortraitMode import java.io.IOException import java.nio.ByteBuffer -import java.util.IdentityHashMap +import java.util.* import kotlin.math.abs import kotlin.math.ceil + /** * Manages the camera and allows UI updates on top of it (e.g. overlaying extra Graphics). This * receives preview frames from the camera at a specified rate, sends those frames to detector as @@ -221,18 +223,23 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { @Throws(IOException::class) private fun setPreviewAndPictureSize(camera: Camera, parameters: Parameters) { - // Gives priority to the preview size specified by the user if exists. - val sizePair: CameraSizePair = CameraPreferenceUtils.getUserSpecifiedPreviewSize(context) ?: run { - // Camera preview size is based on the landscape mode, so we need to also use the aspect - // ration of display in the same mode for comparison. - val displayAspectRatioInLandscape: Float = + // Camera preview size is based on the landscape mode, so we need to also use the aspect + // ration of display in the same mode for comparison. + var height = 0 + val displayAspectRatioInLandscape: Float = if (isPortraitMode(graphicOverlay.context)) { + height = graphicOverlay.height graphicOverlay.height.toFloat() / graphicOverlay.width + } else { + height = graphicOverlay.width graphicOverlay.width.toFloat() / graphicOverlay.height } - selectSizePair(camera, displayAspectRatioInLandscape) - } ?: throw IOException("Could not find suitable preview size.") + + // Gives priority to the preview size specified by the user if exists. + Log.i(TAG,height.toString()) + val sizePair: CameraSizePair = selectSizePair(camera, displayAspectRatioInLandscape, height) + ?: throw IOException("Could not find suitable preview size.") previewSize = sizePair.preview.also { Log.v(TAG, "Camera preview size: $it") @@ -417,6 +424,9 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { } } + + + companion object { const val CAMERA_FACING_BACK = CameraInfo.CAMERA_FACING_BACK @@ -452,7 +462,7 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { * @param camera the camera to select a preview size from * @return the selected preview and picture size pair */ - private fun selectSizePair(camera: Camera, displayAspectRatioInLandscape: Float): CameraSizePair? { + private fun selectSizePair(camera: Camera, displayAspectRatioInLandscape: Float, height:Int): CameraSizePair? { val validPreviewSizes = generateValidPreviewSizeList(camera) var selectedPair: CameraSizePair? = null @@ -461,7 +471,9 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { for (sizePair in validPreviewSizes) { val previewSize = sizePair.preview - if (previewSize.width < MIN_CAMERA_PREVIEW_WIDTH || previewSize.width > MAX_CAMERA_PREVIEW_WIDTH) { + Log.i(TAG, "pre view size = "+ previewSize.toString()) + if (previewSize.width < MIN_CAMERA_PREVIEW_WIDTH || previewSize.width > MAX_CAMERA_PREVIEW_WIDTH + || previewSize.height(R.id.camera_preview_graphic_overlay).apply { -// setOnClickListener(this@Conti) - Log.i("inside","else-- before cameraSource") + setOnClickListener{ + quickViewBehavior.state = BottomSheetBehavior.STATE_HIDDEN + workflowModel?.setWorkflowState(WorkflowState.DETECTING) + startCameraPreview() + } cameraSource = CameraSource(this) - Log.i("inside","else-- after cameraSource") - } setUpWorkflowModel() - } binding.quickViewSearchByBarcode.setOnEditorActionListener(barcodeInputListener) @@ -533,6 +538,7 @@ class ContinuousScanActivity : AppCompatActivity() { setupPopupMenu() } + private fun setUpWorkflowModel() { workflowModel = ViewModelProviders.of(this).get(WorkflowModel::class.java) @@ -564,15 +570,15 @@ class ContinuousScanActivity : AppCompatActivity() { workflowModel?.detectedBarcode?.observe(this, { barcode -> if (barcode != null) { - mlBarcodeCallback(barcode.rawValue ) + mlBarcodeCallback(barcode.rawValue) Log.i("inside","barcode "+barcode.rawValue) - } }) } private fun startCameraPreview() { + Log.i("inside","startCamerra Preview") val workflowModel = this.workflowModel ?: return val cameraSource = this.cameraSource ?: return if (!workflowModel.isCameraLive) { @@ -588,10 +594,10 @@ class ContinuousScanActivity : AppCompatActivity() { } private fun stopCameraPreview() { + Log.i("inside","stopCamerra Preview") val workflowModel = this.workflowModel ?: return if (workflowModel.isCameraLive) { workflowModel.markCameraFrozen() -// flashButton?.isSelected = false preview?.stop() } } @@ -635,6 +641,7 @@ class ContinuousScanActivity : AppCompatActivity() { } override fun onPostResume() { + Log.i("inside","omPostResume") super.onPostResume() // Back to working state after the bottom sheet is dismissed. ViewModelProviders.of(this).get(WorkflowModel::class.java).setWorkflowState(WorkflowState.DETECTING) @@ -846,6 +853,7 @@ class ContinuousScanActivity : AppCompatActivity() { } fun collapseBottomSheet() { + Log.i("inside","collapse bottomm screen") quickViewBehavior.state = BottomSheetBehavior.STATE_HIDDEN } @@ -857,14 +865,31 @@ class ContinuousScanActivity : AppCompatActivity() { private inner class QuickViewCallback : BottomSheetCallback() { private var previousSlideOffset = 0f override fun onStateChanged(bottomSheet: View, newState: Int) { + Log.i("inside","onStateChanged") + Log.i("inside","new state = " + newState) + when (newState) { BottomSheetBehavior.STATE_HIDDEN -> { lastBarcode = null binding.txtProductCallToAction.visibility = View.GONE } - BottomSheetBehavior.STATE_COLLAPSED -> binding.barcodeScanner.resume() - BottomSheetBehavior.STATE_DRAGGING -> if (product == null) { - quickViewBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + BottomSheetBehavior.STATE_COLLAPSED -> { + if (useMLScanner) { + workflowModel?.setWorkflowState(WorkflowState.DETECTED) + Log.i("inside", "stop camers Preview from line 871") + stopCameraPreview() + } else { + binding.barcodeScanner.pause() + } + } + else -> { + if (useMLScanner) { + workflowModel?.setWorkflowState(WorkflowState.DETECTED) + Log.i("inside", "stop camers Preview from line 888") + stopCameraPreview() + } else { + binding.barcodeScanner.pause() + } } } if (binding.quickViewSearchByBarcode.visibility == View.VISIBLE) { @@ -878,6 +903,7 @@ class ContinuousScanActivity : AppCompatActivity() { } override fun onSlide(bottomSheet: View, slideOffset: Float) { + Log.i("inside","onSlide") val slideDelta = slideOffset - previousSlideOffset if (binding.quickViewSearchByBarcode.visibility != View.VISIBLE && binding.quickViewProgress.visibility != View.VISIBLE) { if (slideOffset > 0.01f || slideOffset < -0.01f) { @@ -888,13 +914,27 @@ class ContinuousScanActivity : AppCompatActivity() { if (slideOffset > 0.01f) { binding.quickViewDetails.visibility = View.GONE binding.quickViewTags.visibility = View.GONE - binding.barcodeScanner.pause() + if(useMLScanner){ + Log.i("inside", "stop camers Preview from line 903") + workflowModel?.setWorkflowState(WorkflowState.DETECTED) + stopCameraPreview() + } + else { + binding.barcodeScanner.pause() + } if (slideDelta > 0 && productViewFragment != null) { productViewFragment!!.bottomSheetWillGrow() binding.bottomNavigation.bottomNavigation.visibility = View.GONE } } else { - binding.barcodeScanner.resume() + if(useMLScanner){ + workflowModel?.setWorkflowState(WorkflowState.DETECTING) + Log.i("inside", "startCamera Preview from line 915") + startCameraPreview() + } + else { + binding.barcodeScanner.resume() + } binding.quickViewDetails.visibility = View.VISIBLE binding.quickViewTags.visibility = if (analysisTagsEmpty) View.GONE else View.VISIBLE binding.bottomNavigation.bottomNavigation.visibility = View.VISIBLE diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeProcessor.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeProcessor.kt index ba7b0318fa5a..dc4d80808e51 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeProcessor.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeProcessor.kt @@ -1,5 +1,7 @@ package openfoodfacts.github.scrachx.openfood.scanner +import android.animation.ValueAnimator +import android.graphics.Camera import android.util.Log import androidx.annotation.MainThread import com.google.android.gms.tasks.Task @@ -60,7 +62,7 @@ class BarcodeProcessor(graphicOverlay: GraphicOverlay, private val workflowModel if (barcodeInCenter == null) { cameraReticleAnimator.start() graphicOverlay.add(BarcodeReticleGraphic(graphicOverlay, cameraReticleAnimator)) - workflowModel.setWorkflowState(WorkflowModel.WorkflowState.DETECTING) + workflowModel.setWorkflowState(WorkflowState.DETECTING) } else { cameraReticleAnimator.cancel() val sizeProgress = CameraPreferenceUtils.getProgressToMeetBarcodeSizeRequirement(graphicOverlay, barcodeInCenter) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeReticleGraphic.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeReticleGraphic.kt index f68ca0fedef6..051b2da531a7 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeReticleGraphic.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeReticleGraphic.kt @@ -41,7 +41,7 @@ internal class BarcodeReticleGraphic(overlay: GraphicOverlay, private val animat ripplePaint.style = Style.STROKE ripplePaint.color = ContextCompat.getColor(context, R.color.reticle_ripple) rippleSizeOffset = resources.getDimensionPixelOffset(R.dimen.barcode_reticle_ripple_size_offset) - rippleStrokeWidth = resources.getDimensionPixelOffset(R.dimen.nav_bar_height) + rippleStrokeWidth = resources.getDimensionPixelOffset(R.dimen.barcode_reticle_ripple_stroke_width) rippleAlpha = ripplePaint.alpha } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/CameraPreferenceUtils.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/CameraPreferenceUtils.kt index 0dc4c9d4123d..17ab93ad8a39 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/CameraPreferenceUtils.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/CameraPreferenceUtils.kt @@ -3,6 +3,7 @@ package openfoodfacts.github.scrachx.openfood.utils import android.content.Context import android.graphics.RectF import android.preference.PreferenceManager +import android.view.WindowManager import androidx.annotation.StringRes import com.google.android.gms.common.images.Size import com.google.mlkit.vision.barcode.Barcode @@ -63,7 +64,7 @@ object CameraPreferenceUtils { val overlayWidth = overlay.width.toFloat() val overlayHeight = overlay.height.toFloat() val boxWidth = overlayWidth * getIntPref(context, R.string.pref_key_barcode_reticle_width, 80) / 100 - val boxHeight = overlayHeight * getIntPref(context, R.string.pref_key_barcode_reticle_height, 35) / 100 + val boxHeight = overlayHeight * getIntPref(context, R.string.pref_key_barcode_reticle_height, 40) / 100 val cx = overlayWidth / 2 val cy = overlayHeight / 2 return RectF(cx - boxWidth / 2, cy - boxHeight / 2, cx + boxWidth / 2, cy + boxHeight / 2) diff --git a/app/src/main/res/layout/camera_preview_overlay.xml b/app/src/main/res/layout/camera_preview_overlay.xml index a67f126c3914..24fa05af182a 100644 --- a/app/src/main/res/layout/camera_preview_overlay.xml +++ b/app/src/main/res/layout/camera_preview_overlay.xml @@ -19,7 +19,7 @@ android:layout_height="@dimen/button_height_normal" android:layout_gravity="center" android:indeterminate="true" - android:visibility="visible"/> + android:visibility="gone"/> From 7d65e782b0ab9c3554e0bc0029178b9b19fe7f47 Mon Sep 17 00:00:00 2001 From: Kartik Date: Sun, 14 Feb 2021 18:12:47 +0530 Subject: [PATCH 04/12] preview-screen fixed --- .../scrachx/openfood/camera/CameraSource.kt | 19 +++++---- .../openfood/camera/CameraSourcePreview.kt | 41 ++++++++++--------- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt index baa36dbf45a4..b675c8fd2d34 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt @@ -226,19 +226,24 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { // Camera preview size is based on the landscape mode, so we need to also use the aspect // ration of display in the same mode for comparison. var height = 0 + var width = 0 + val displayAspectRatioInLandscape: Float = if (isPortraitMode(graphicOverlay.context)) { - height = graphicOverlay.height + width = graphicOverlay.height + height = graphicOverlay.width graphicOverlay.height.toFloat() / graphicOverlay.width } else { - height = graphicOverlay.width + width = graphicOverlay.width + height = graphicOverlay.height graphicOverlay.width.toFloat() / graphicOverlay.height } // Gives priority to the preview size specified by the user if exists. - Log.i(TAG,height.toString()) - val sizePair: CameraSizePair = selectSizePair(camera, displayAspectRatioInLandscape, height) + Log.i(TAG," height = " + height.toString()) + Log.i(TAG," width = " + width.toString()) + val sizePair: CameraSizePair = selectSizePair(camera, displayAspectRatioInLandscape,width) ?: throw IOException("Could not find suitable preview size.") previewSize = sizePair.preview.also { @@ -250,9 +255,6 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { sizePair.picture?.let { pictureSize -> Log.v(TAG, "Camera picture size: $pictureSize") parameters.setPictureSize(pictureSize.width, pictureSize.height) - CameraPreferenceUtils.saveStringPreference( - context, R.string.pref_key_rear_camera_picture_size, pictureSize.toString() - ) } } @@ -472,8 +474,7 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { for (sizePair in validPreviewSizes) { val previewSize = sizePair.preview Log.i(TAG, "pre view size = "+ previewSize.toString()) - if (previewSize.width < MIN_CAMERA_PREVIEW_WIDTH || previewSize.width > MAX_CAMERA_PREVIEW_WIDTH - || previewSize.height MAX_CAMERA_PREVIEW_WIDTH) { continue } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSourcePreview.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSourcePreview.kt index 92be41d3a394..90372d45c1e0 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSourcePreview.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSourcePreview.kt @@ -81,39 +81,42 @@ class CameraSourcePreview(context: Context, attrs: AttributeSet) : FrameLayout(c cameraSource?.previewSize?.let { cameraPreviewSize = it } - val previewSizeRatio = cameraPreviewSize?.let { size -> + val previewWidth = cameraPreviewSize?.let { size -> if (isPortraitMode(context)) { // Camera's natural orientation is landscape, so need to swap width and height. - size.height.toFloat() / size.width + size.height } else { - size.width.toFloat() / size.height + size.width } - } ?: layoutWidth.toFloat() / layoutHeight.toFloat() + } ?: layoutWidth.toInt() + + val previewHeight = cameraPreviewSize?.let { size -> + if (isPortraitMode(context)) { + // Camera's natural orientation is landscape, so need to swap width and height. + size.width + } else { + size.height + } + } ?: layoutHeight // Match the width of the child view to its parent. - val childHeight = (layoutWidth / previewSizeRatio).toInt() - if (childHeight <= layoutHeight) { + if (layoutWidth*previewHeight <= layoutHeight*previewWidth) { + val scaledChildWidth = previewWidth * layoutHeight / previewHeight; + for (i in 0 until childCount) { - getChildAt(i).layout(0, 0, layoutWidth, childHeight) + getChildAt(i).layout((layoutWidth - scaledChildWidth) / 2, 0, + (layoutWidth + scaledChildWidth) / 2, height) } } else { // When the child view is too tall to be fitted in its parent: If the child view is // static overlay view container (contains views such as bottom prompt chip), we apply // the size of the parent view to it. Otherwise, we offset the top/bottom position // equally to position it in the center of the parent. - val excessLenInHalf = (childHeight - layoutHeight) / 2 + val scaledChildHeight = previewHeight * layoutWidth / previewWidth; + for (i in 0 until childCount) { - val childView = getChildAt(i) - when (childView.id) { - R.id.static_overlay_container -> { - childView.layout(0, 0, layoutWidth, layoutHeight) - } - else -> { - childView.layout( - 0, -excessLenInHalf, layoutWidth, layoutHeight + excessLenInHalf - ) - } - } + getChildAt(i).layout(0, (layoutHeight - scaledChildHeight) / 2, + width, (layoutHeight + scaledChildHeight) / 2) } } From 795dd4a9e2312c046a28033f035303cba7559704 Mon Sep 17 00:00:00 2001 From: Kartik Date: Mon, 15 Feb 2021 21:35:19 +0530 Subject: [PATCH 05/12] beep disabled, flash implemeted for ML trouble scanning fixed --- .../scrachx/openfood/camera/CameraSource.kt | 33 +++++--- .../openfood/camera/CameraSourcePreview.kt | 2 +- .../features/scan/ContinuousScanActivity.kt | 78 ++++++++++--------- 3 files changed, 66 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt index b675c8fd2d34..00a31d887bd7 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt @@ -21,7 +21,6 @@ import android.graphics.ImageFormat import android.hardware.Camera import android.hardware.Camera.CameraInfo import android.hardware.Camera.Parameters -import android.util.DisplayMetrics import android.util.Log import android.view.Surface import android.view.SurfaceHolder @@ -52,7 +51,7 @@ import kotlin.math.ceil @Suppress("DEPRECATION") class CameraSource(private val graphicOverlay: GraphicOverlay) { - private var camera: Camera? = null + var camera: Camera? = null private var rotationDegrees: Int = 0 /** Returns the preview size that is currently in use by the underlying camera. */ @@ -162,9 +161,29 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { } } - fun updateFlashMode(flashMode: String) { + fun updateFlashMode(flashActive: Boolean) { + Log.i("CSSAA", "inside update flash mode") val parameters = camera?.parameters - parameters?.flashMode = flashMode + if(flashActive) { + parameters?.flashMode = Camera.Parameters.FLASH_MODE_TORCH + } else{ + parameters?.flashMode = Camera.Parameters.FLASH_MODE_OFF + } + camera?.parameters = parameters + } + + fun setFocusMode(autoFocusActive:Boolean) { + val parameters = camera?.parameters + if(autoFocusActive) { + if (parameters?.supportedFocusModes?.contains(Parameters.FOCUS_MODE_AUTO) == true) { + parameters.focusMode = Parameters.FOCUS_MODE_AUTO + } else { + Log.i(TAG, "Camera auto focus is not supported on this device.") + } + } + else{ + parameters?.focusMode = null + } camera?.parameters = parameters } @@ -189,11 +208,7 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { parameters.previewFormat = IMAGE_FORMAT - if (parameters.supportedFocusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { - parameters.focusMode = Parameters.FOCUS_MODE_CONTINUOUS_VIDEO - } else { - Log.i(TAG, "Camera auto focus is not supported on this device.") - } + setFocusMode(false) camera.parameters = parameters diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSourcePreview.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSourcePreview.kt index 90372d45c1e0..cfc452c57089 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSourcePreview.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSourcePreview.kt @@ -88,7 +88,7 @@ class CameraSourcePreview(context: Context, attrs: AttributeSet) : FrameLayout(c } else { size.width } - } ?: layoutWidth.toInt() + } ?: layoutWidth val previewHeight = cameraPreviewSize?.let { size -> if (isPortraitMode(context)) { diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt index 73ace765f60a..f795a1d86f8f 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt @@ -452,7 +452,7 @@ class ContinuousScanActivity : AppCompatActivity() { _binding = ActivityContinuousScanBinding.inflate(layoutInflater) setContentView(binding.root) - useMLScanner = settings.getBoolean("select_scanner",false) + useMLScanner = settings.getBoolean(getString(R.string.pref_scanner_type_key),false) binding.toggleFlash.setOnClickListener { toggleFlash() } binding.buttonMore.setOnClickListener { showMoreSettings() } @@ -466,7 +466,6 @@ class ContinuousScanActivity : AppCompatActivity() { binding.quickViewTags.isNestedScrollingEnabled = false - // The system bars are visible. hideSystemUI() @@ -496,7 +495,6 @@ class ContinuousScanActivity : AppCompatActivity() { } // Setup barcode scanner - if (!useMLScanner) { binding.barcodeScanner.visibility = View.VISIBLE binding.cameraPreview.visibility = View.GONE @@ -518,22 +516,25 @@ class ContinuousScanActivity : AppCompatActivity() { } else { binding.cameraPreview.visibility = View.VISIBLE binding.barcodeScanner.visibility = View.GONE - preview = binding.cameraPreview + graphicOverlay = findViewById(R.id.camera_preview_graphic_overlay).apply { + + cameraSource = CameraSource(this).apply { + this.setFocusMode(autoFocusActive) + } setOnClickListener{ quickViewBehavior.state = BottomSheetBehavior.STATE_HIDDEN workflowModel?.setWorkflowState(WorkflowState.DETECTING) startCameraPreview() } - cameraSource = CameraSource(this) } - setUpWorkflowModel() } binding.quickViewSearchByBarcode.setOnEditorActionListener(barcodeInputListener) binding.bottomNavigation.bottomNavigation.installBottomNavigation(this) + // Setup popup menu setupPopupMenu() } @@ -710,9 +711,15 @@ class ContinuousScanActivity : AppCompatActivity() { private fun setupPopupMenu() { popupMenu = PopupMenu(this, binding.buttonMore).also { it.menuInflater.inflate(R.menu.popup_menu, it.menu) + // turn flash on if flashActive true in pref if (flashActive) { - binding.barcodeScanner.setTorchOn() - binding.toggleFlash.setImageResource(R.drawable.ic_flash_on_white_24dp) + if(useMLScanner) { + cameraSource!!.updateFlashMode(flashActive) + binding.toggleFlash.setImageResource(R.drawable.ic_flash_on_white_24dp) + } else{ + binding.barcodeScanner.setTorchOn() + binding.toggleFlash.setImageResource(R.drawable.ic_flash_on_white_24dp) + } } if (beepActive) { it.menu.findItem(R.id.toggleBeep).isChecked = true @@ -758,37 +765,38 @@ class ContinuousScanActivity : AppCompatActivity() { } private fun toggleFlash() { + Log.i("CSSAA", "inside toggle flash") cameraPref.edit { if (flashActive) { - if(useMLScanner){ -// fotoapparat.updateConfiguration( -// UpdateConfiguration( flashMode= off() ) -// ) - } - else { - binding.barcodeScanner.setTorchOff() - } flashActive = false binding.toggleFlash.setImageResource(R.drawable.ic_flash_off_white_24dp) putBoolean(SETTING_FLASH, false) + + if(useMLScanner){ + cameraSource?.updateFlashMode(flashActive) + } else { + binding.barcodeScanner.setTorchOff() + } } else { + flashActive = true + binding.toggleFlash.setImageResource(R.drawable.ic_flash_on_white_24dp) + putBoolean(SETTING_FLASH, true) + if(useMLScanner){ -// fotoapparat.updateConfiguration( -// UpdateConfiguration( flashMode= torch() ) -// ) + cameraSource!!.updateFlashMode(flashActive) } else { binding.barcodeScanner.setTorchOn() } - flashActive = true - binding.toggleFlash.setImageResource(R.drawable.ic_flash_on_white_24dp) - putBoolean(SETTING_FLASH, true) } } } private fun showMoreSettings() { popupMenu?.let { + if(useMLScanner) { + it.menu.findItem(R.id.toggleBeep).setEnabled(false) + } it.setOnMenuItemClickListener { item: MenuItem -> when (item.itemId) { R.id.toggleBeep -> { @@ -800,31 +808,27 @@ class ContinuousScanActivity : AppCompatActivity() { } } R.id.toggleAutofocus -> { - if (binding.barcodeScanner.barcodeView.isPreviewActive) { - binding.barcodeScanner.pause() - } - val settings = binding.barcodeScanner.barcodeView.cameraSettings autoFocusActive = !autoFocusActive - settings.isAutoFocusEnabled = autoFocusActive item.isChecked = autoFocusActive - cameraPref.edit { putBoolean(SETTING_FOCUS, autoFocusActive) } - binding.barcodeScanner.resume() - binding.barcodeScanner.barcodeView.cameraSettings = settings + if(useMLScanner){ + cameraSource!!.setFocusMode(autoFocusActive) + } else { + if (binding.barcodeScanner.barcodeView.isPreviewActive) { + binding.barcodeScanner.pause() + } + val settings = binding.barcodeScanner.barcodeView.cameraSettings + settings.isAutoFocusEnabled = autoFocusActive + } } R.id.troubleScanning -> { hideAllViews() - hintBarcodeDisp?.dispose() - + hintBarcodeDisp!!.dispose() binding.quickView.setOnClickListener(null) binding.quickViewSearchByBarcode.text = null binding.quickViewSearchByBarcode.visibility = View.VISIBLE - binding.quickView.visibility = View.INVISIBLE - quickViewBehavior.state = BottomSheetBehavior.STATE_EXPANDED - commonDisp.add(Completable.timer(500, TimeUnit.MILLISECONDS) - .doOnComplete { binding.quickView.visibility = View.VISIBLE } - .subscribeOn(AndroidSchedulers.mainThread()).subscribe()) + quickViewBehavior.state = BottomSheetBehavior.STATE_COLLAPSED binding.quickViewSearchByBarcode.requestFocus() } R.id.toggleCamera -> toggleCamera() From 9d3045d51be04b2b54f46fa9cdea83cb75caf15a Mon Sep 17 00:00:00 2001 From: Kartik Date: Tue, 16 Feb 2021 01:17:21 +0530 Subject: [PATCH 06/12] Switch camera, autofocus implemented --- .../scrachx/openfood/camera/CameraSource.kt | 48 ++++++++++++------- .../features/scan/ContinuousScanActivity.kt | 10 +++- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt index 00a31d887bd7..f7178a821189 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt @@ -26,6 +26,7 @@ import android.view.Surface import android.view.SurfaceHolder import android.view.WindowManager import com.google.android.gms.common.images.Size +import com.google.zxing.client.android.camera.open.OpenCameraInterface import openfoodfacts.github.scrachx.openfood.R import openfoodfacts.github.scrachx.openfood.utils.CameraPreferenceUtils import openfoodfacts.github.scrachx.openfood.utils.CameraUtils.ASPECT_RATIO_TOLERANCE @@ -54,6 +55,8 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { var camera: Camera? = null private var rotationDegrees: Int = 0 + var requestedCameraId = CameraInfo.CAMERA_FACING_BACK + /** Returns the preview size that is currently in use by the underlying camera. */ internal var previewSize: Size? = null private set @@ -162,7 +165,6 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { } fun updateFlashMode(flashActive: Boolean) { - Log.i("CSSAA", "inside update flash mode") val parameters = camera?.parameters if(flashActive) { parameters?.flashMode = Camera.Parameters.FLASH_MODE_TORCH @@ -172,19 +174,28 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { camera?.parameters = parameters } - fun setFocusMode(autoFocusActive:Boolean) { - val parameters = camera?.parameters + fun setFocusMode(autoFocusActive: Boolean) { if(autoFocusActive) { + val parameters = camera?.parameters if (parameters?.supportedFocusModes?.contains(Parameters.FOCUS_MODE_AUTO) == true) { parameters.focusMode = Parameters.FOCUS_MODE_AUTO } else { Log.i(TAG, "Camera auto focus is not supported on this device.") } + camera?.parameters = parameters + } else { + camera?.cancelAutoFocus() } - else{ - parameters?.focusMode = null + } + + fun switchCamera() : Camera { + camera?.release() + if(requestedCameraId == CAMERA_FACING_BACK){ + requestedCameraId = CAMERA_FACING_FRONT + } else{ + requestedCameraId = CAMERA_FACING_BACK } - camera?.parameters = parameters + return createCamera() } /** @@ -194,7 +205,7 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { */ @Throws(IOException::class) private fun createCamera(): Camera { - val camera = Camera.open() ?: throw IOException("There is no back-facing camera.") + val camera = Camera.open(requestedCameraId) ?: throw IOException("There is no back-facing camera.") val parameters = camera.parameters setPreviewAndPictureSize(camera, parameters) setRotation(camera, parameters) @@ -208,7 +219,7 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { parameters.previewFormat = IMAGE_FORMAT - setFocusMode(false) + setFocusMode(true) camera.parameters = parameters @@ -256,19 +267,19 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { } // Gives priority to the preview size specified by the user if exists. - Log.i(TAG," height = " + height.toString()) - Log.i(TAG," width = " + width.toString()) - val sizePair: CameraSizePair = selectSizePair(camera, displayAspectRatioInLandscape,width) +// Log.i(TAG," height = " + height.toString()) +// Log.i(TAG," width = " + width.toString()) + val sizePair: CameraSizePair = selectSizePair(camera, displayAspectRatioInLandscape, width) ?: throw IOException("Could not find suitable preview size.") previewSize = sizePair.preview.also { - Log.v(TAG, "Camera preview size: $it") +// Log.v(TAG, "Camera preview size: $it") parameters.setPreviewSize(it.width, it.height) CameraPreferenceUtils.saveStringPreference(context, R.string.pref_key_rear_camera_preview_size, it.toString()) } sizePair.picture?.let { pictureSize -> - Log.v(TAG, "Camera picture size: $pictureSize") +// Log.v(TAG, "Camera picture size: $pictureSize") parameters.setPictureSize(pictureSize.width, pictureSize.height) } } @@ -366,8 +377,8 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { if (!bytesToByteBuffer.containsKey(data)) { Log.d( - TAG, - "Skipping frame. Could not find ByteBuffer associated with the image data from the camera." + TAG, + "Skipping frame. Could not find ByteBuffer associated with the image data from the camera." ) return } @@ -447,6 +458,7 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { companion object { const val CAMERA_FACING_BACK = CameraInfo.CAMERA_FACING_BACK + const val CAMERA_FACING_FRONT = CameraInfo.CAMERA_FACING_FRONT private const val TAG = "CameraSource" @@ -479,7 +491,7 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { * @param camera the camera to select a preview size from * @return the selected preview and picture size pair */ - private fun selectSizePair(camera: Camera, displayAspectRatioInLandscape: Float, height:Int): CameraSizePair? { + private fun selectSizePair(camera: Camera, displayAspectRatioInLandscape: Float, height: Int): CameraSizePair? { val validPreviewSizes = generateValidPreviewSizeList(camera) var selectedPair: CameraSizePair? = null @@ -488,7 +500,7 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { for (sizePair in validPreviewSizes) { val previewSize = sizePair.preview - Log.i(TAG, "pre view size = "+ previewSize.toString()) +// Log.i(TAG, "pre view size = "+ previewSize.toString()) if (previewSize.width < MIN_CAMERA_PREVIEW_WIDTH || previewSize.width > MAX_CAMERA_PREVIEW_WIDTH) { continue } @@ -506,7 +518,7 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { } if (selectedPair == null) { - Log.i(TAG, "inside selected pair null") +// Log.i(TAG, "inside selected pair null") // Picks the one that has the minimum sum of the differences between the desired values and // the actual values for width and height. var minDiff = Integer.MAX_VALUE diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt index f795a1d86f8f..e27952c6ac46 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt @@ -521,6 +521,8 @@ class ContinuousScanActivity : AppCompatActivity() { graphicOverlay = findViewById(R.id.camera_preview_graphic_overlay).apply { cameraSource = CameraSource(this).apply { + requestedCameraId = cameraState + Log.i("CSSAA", "inside on create CameraSource apply") this.setFocusMode(autoFocusActive) } setOnClickListener{ @@ -760,6 +762,10 @@ class ContinuousScanActivity : AppCompatActivity() { binding.barcodeScanner.barcodeView.cameraSettings = settings cameraPref.edit { putInt(SETTING_STATE, cameraState) } binding.barcodeScanner.resume() + } else { + stopCameraPreview() + cameraSource?.switchCamera() + startCameraPreview() } } @@ -795,7 +801,7 @@ class ContinuousScanActivity : AppCompatActivity() { private fun showMoreSettings() { popupMenu?.let { if(useMLScanner) { - it.menu.findItem(R.id.toggleBeep).setEnabled(false) + it.menu.findItem(R.id.toggleBeep).isEnabled = false } it.setOnMenuItemClickListener { item: MenuItem -> when (item.itemId) { @@ -813,7 +819,7 @@ class ContinuousScanActivity : AppCompatActivity() { cameraPref.edit { putBoolean(SETTING_FOCUS, autoFocusActive) } if(useMLScanner){ - cameraSource!!.setFocusMode(autoFocusActive) + cameraSource?.setFocusMode(autoFocusActive) } else { if (binding.barcodeScanner.barcodeView.isPreviewActive) { binding.barcodeScanner.pause() From cd7d5d57534c66f03a8a670a09b96489316c0530 Mon Sep 17 00:00:00 2001 From: Kartik Date: Tue, 16 Feb 2021 23:27:14 +0530 Subject: [PATCH 07/12] code cleaned --- app/build.gradle.kts | 4 +- .../openfood/camera/CameraReticleAnimator.kt | 19 +---- .../scrachx/openfood/camera/CameraSizePair.kt | 31 ++----- .../scrachx/openfood/camera/CameraSource.kt | 84 ++++++------------- .../openfood/camera/CameraSourcePreview.kt | 49 +++-------- .../scrachx/openfood/camera/FrameMetadata.kt | 19 +---- .../scrachx/openfood/camera/FrameProcessor.kt | 19 +---- .../openfood/camera/FrameProcessorBase.kt | 23 ++--- .../scrachx/openfood/camera/GraphicOverlay.kt | 33 +++----- .../scrachx/openfood/camera/WorkflowModel.kt | 29 +------ .../openfood/features/PreferencesFragment.kt | 9 +- .../features/scan/ContinuousScanActivity.kt | 55 ++++++------ .../scanner/BarcodeConfirmingGraphic.kt | 23 ++--- .../openfood/scanner/BarcodeGraphicBase.kt | 17 +--- .../openfood/scanner/BarcodeProcessor.kt | 24 +++--- .../openfood/scanner/BarcodeReticleGraphic.kt | 13 --- .../openfood/utils/CameraPreferenceUtils.kt | 72 ---------------- .../scrachx/openfood/utils/CameraUtils.kt | 26 +++++- .../scrachx/openfood/utils/inputInfo.kt | 7 -- .../res/layout/camera_preview_overlay.xml | 23 +++-- app/src/main/res/values/colors.xml | 3 +- app/src/main/res/values/dimens.xml | 5 ++ app/src/main/res/values/pref_keys.xml | 8 +- app/src/main/res/values/strings.xml | 32 ++----- app/src/main/res/xml/preferences.xml | 5 +- 25 files changed, 186 insertions(+), 446 deletions(-) delete mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/CameraPreferenceUtils.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 69e8c13449e9..a7d9330af22f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -68,12 +68,10 @@ dependencies { implementation("androidx.work:work-rxjava2:2.4.0") implementation("androidx.startup:startup-runtime:1.0.0") - // ML Kit barcode Scanner implementation ("com.google.mlkit:barcode-scanning:16.1.1") - // ___ library - implementation ("android.arch.lifecycle:extensions:1.1.1") + implementation ("android.arch.lifecycle:extensions:1.1.1") kapt("com.google.dagger:dagger-compiler:2.30.1") implementation("com.google.dagger:dagger:2.30.1") diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraReticleAnimator.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraReticleAnimator.kt index 0fb3d3499797..dd922348d897 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraReticleAnimator.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraReticleAnimator.kt @@ -1,18 +1,3 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package openfoodfacts.github.scrachx.openfood.camera @@ -20,7 +5,9 @@ import android.animation.AnimatorSet import android.animation.ValueAnimator import androidx.interpolator.view.animation.FastOutSlowInInterpolator -/** Custom animator for the object or barcode reticle in live camera. */ +/** + * Custom animator for the object or barcode reticle in live camera. + */ class CameraReticleAnimator(graphicOverlay: GraphicOverlay) { /** Returns the scale value of ripple alpha ranges in [0, 1]. */ diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSizePair.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSizePair.kt index 9147626f66a7..e08abffb91d3 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSizePair.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSizePair.kt @@ -1,18 +1,4 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +@file:Suppress("DEPRECATION") package openfoodfacts.github.scrachx.openfood.camera @@ -25,17 +11,10 @@ import com.google.android.gms.common.images.Size * ratio as the preview size or the preview may end up being distorted. If the picture size is null, * then there is no picture size with the same aspect ratio as the preview size. */ -class CameraSizePair { - val preview: Size - val picture: Size? - constructor(previewSize: Camera.Size, pictureSize: Camera.Size?) { - preview = Size(previewSize.width, previewSize.height) - picture = pictureSize?.let { Size(it.width, it.height) } - } +class CameraSizePair(previewSize: Camera.Size, pictureSize: Camera.Size?) { + + val preview: Size = Size(previewSize.width, previewSize.height) + val picture: Size? = pictureSize?.let { Size(it.width, it.height) } - constructor(previewSize: Size, pictureSize: Size?) { - preview = previewSize - picture = pictureSize - } } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt index f7178a821189..948f4c72eea0 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt @@ -1,18 +1,4 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +@file:Suppress("DEPRECATION") package openfoodfacts.github.scrachx.openfood.camera @@ -26,9 +12,6 @@ import android.view.Surface import android.view.SurfaceHolder import android.view.WindowManager import com.google.android.gms.common.images.Size -import com.google.zxing.client.android.camera.open.OpenCameraInterface -import openfoodfacts.github.scrachx.openfood.R -import openfoodfacts.github.scrachx.openfood.utils.CameraPreferenceUtils import openfoodfacts.github.scrachx.openfood.utils.CameraUtils.ASPECT_RATIO_TOLERANCE import openfoodfacts.github.scrachx.openfood.utils.CameraUtils.generateValidPreviewSizeList import openfoodfacts.github.scrachx.openfood.utils.CameraUtils.isPortraitMode @@ -49,7 +32,7 @@ import kotlin.math.ceil * possible, while at the same time minimizing lag. As such, frames may be dropped if the detector * is unable to keep up with the rate of frames generated by the camera. */ -@Suppress("DEPRECATION") + class CameraSource(private val graphicOverlay: GraphicOverlay) { var camera: Camera? = null @@ -167,9 +150,9 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { fun updateFlashMode(flashActive: Boolean) { val parameters = camera?.parameters if(flashActive) { - parameters?.flashMode = Camera.Parameters.FLASH_MODE_TORCH + parameters?.flashMode = Parameters.FLASH_MODE_TORCH } else{ - parameters?.flashMode = Camera.Parameters.FLASH_MODE_OFF + parameters?.flashMode = Parameters.FLASH_MODE_OFF } camera?.parameters = parameters } @@ -190,11 +173,12 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { fun switchCamera() : Camera { camera?.release() - if(requestedCameraId == CAMERA_FACING_BACK){ - requestedCameraId = CAMERA_FACING_FRONT - } else{ - requestedCameraId = CAMERA_FACING_BACK - } + requestedCameraId = + if(requestedCameraId == CAMERA_FACING_BACK){ + CAMERA_FACING_FRONT + } else{ + CAMERA_FACING_BACK + } return createCamera() } @@ -251,35 +235,23 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { // Camera preview size is based on the landscape mode, so we need to also use the aspect // ration of display in the same mode for comparison. - var height = 0 - var width = 0 - - val displayAspectRatioInLandscape: Float = - if (isPortraitMode(graphicOverlay.context)) { - width = graphicOverlay.height - height = graphicOverlay.width - graphicOverlay.height.toFloat() / graphicOverlay.width - - } else { - width = graphicOverlay.width - height = graphicOverlay.height - graphicOverlay.width.toFloat() / graphicOverlay.height - } + val displayAspectRatioInLandscape: Float = + if (isPortraitMode(graphicOverlay.context)) { + graphicOverlay.height.toFloat() / graphicOverlay.width + } else { + graphicOverlay.width.toFloat() / graphicOverlay.height + } - // Gives priority to the preview size specified by the user if exists. -// Log.i(TAG," height = " + height.toString()) -// Log.i(TAG," width = " + width.toString()) - val sizePair: CameraSizePair = selectSizePair(camera, displayAspectRatioInLandscape, width) + val sizePair: CameraSizePair = selectSizePair(camera, displayAspectRatioInLandscape) ?: throw IOException("Could not find suitable preview size.") previewSize = sizePair.preview.also { -// Log.v(TAG, "Camera preview size: $it") + Log.v(TAG, "Camera preview size: $it") parameters.setPreviewSize(it.width, it.height) - CameraPreferenceUtils.saveStringPreference(context, R.string.pref_key_rear_camera_preview_size, it.toString()) } sizePair.picture?.let { pictureSize -> -// Log.v(TAG, "Camera picture size: $pictureSize") + Log.v(TAG, "Camera picture size: $pictureSize") parameters.setPictureSize(pictureSize.width, pictureSize.height) } } @@ -326,7 +298,7 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { // should guarantee that there will be an array to work with. val byteArray = ByteArray(bufferSize) val byteBuffer = ByteBuffer.wrap(byteArray) - check(!(!byteBuffer.hasArray() || !byteBuffer.array()!!.contentEquals(byteArray))) { + check(!(!byteBuffer.hasArray() || !byteBuffer.array().contentEquals(byteArray))) { // This should never happen. If it does, then we wouldn't be passing the preview content to // the underlying detector later. "Failed to create valid buffer for camera source." @@ -347,7 +319,7 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { * associated processing is done for the previous frame, detection on the mostly recently received * frame will immediately start on the same thread. */ - private inner class FrameProcessingRunnable internal constructor() : Runnable { + private inner class FrameProcessingRunnable : Runnable { // This lock guards all of the member variables below. private val lock = Object() @@ -357,7 +329,7 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { private var pendingFrameData: ByteBuffer? = null /** Marks the runnable as active/not active. Signals any blocked threads to continue. */ - internal fun setActive(active: Boolean) { + fun setActive(active: Boolean) { synchronized(lock) { this.active = active lock.notifyAll() @@ -368,7 +340,7 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { * Sets the frame data received from the camera. This adds the previous unused frame buffer (if * present) back to the camera, and keeps a pending reference to the frame data for future use. */ - internal fun setNextFrame(data: ByteArray, camera: Camera) { + fun setNextFrame(data: ByteArray, camera: Camera) { synchronized(lock) { pendingFrameData?.let { camera.addCallbackBuffer(it.array()) @@ -402,7 +374,7 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { * or frame acquisition time latency. * * - * If you find that this is using more CPU than you'd like, you should probably decrease the + * If this is using more CPU than preferred, decrease the * FPS setting above to allow for some idle time in between frames. */ override fun run() { @@ -475,8 +447,8 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { * * * It's firstly trying to pick the one that has closest aspect ratio to display view with its - * width be in the specified range [[.MIN_CAMERA_PREVIEW_WIDTH], [ ][.MAX_CAMERA_PREVIEW_WIDTH]]. If there're multiple candidates, choose the one having longest - * width. + * width be in the specified range [[.MIN_CAMERA_PREVIEW_WIDTH], [ ][.MAX_CAMERA_PREVIEW_WIDTH]]. + * If there're multiple candidates, choose the one having longest width. * * * If the above looking up failed, chooses the one that has the minimum sum of the differences @@ -491,7 +463,7 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { * @param camera the camera to select a preview size from * @return the selected preview and picture size pair */ - private fun selectSizePair(camera: Camera, displayAspectRatioInLandscape: Float, height: Int): CameraSizePair? { + private fun selectSizePair(camera: Camera, displayAspectRatioInLandscape: Float): CameraSizePair? { val validPreviewSizes = generateValidPreviewSizeList(camera) var selectedPair: CameraSizePair? = null @@ -500,7 +472,6 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { for (sizePair in validPreviewSizes) { val previewSize = sizePair.preview -// Log.i(TAG, "pre view size = "+ previewSize.toString()) if (previewSize.width < MIN_CAMERA_PREVIEW_WIDTH || previewSize.width > MAX_CAMERA_PREVIEW_WIDTH) { continue } @@ -518,7 +489,6 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { } if (selectedPair == null) { -// Log.i(TAG, "inside selected pair null") // Picks the one that has the minimum sum of the differences between the desired values and // the actual values for width and height. var minDiff = Integer.MAX_VALUE diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSourcePreview.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSourcePreview.kt index cfc452c57089..e4d6b3ca66b4 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSourcePreview.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSourcePreview.kt @@ -1,18 +1,3 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package openfoodfacts.github.scrachx.openfood.camera @@ -27,7 +12,9 @@ import openfoodfacts.github.scrachx.openfood.R import openfoodfacts.github.scrachx.openfood.utils.CameraUtils.isPortraitMode import java.io.IOException -/** Preview the camera image in the screen. */ +/** + * Preview the camera image in the screen. + */ class CameraSourcePreview(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs) { private val surfaceView: SurfaceView = SurfaceView(context).apply { @@ -79,40 +66,30 @@ class CameraSourcePreview(context: Context, attrs: AttributeSet) : FrameLayout(c val layoutWidth = right - left val layoutHeight = bottom - top - cameraSource?.previewSize?.let { cameraPreviewSize = it } - - val previewWidth = cameraPreviewSize?.let { size -> - if (isPortraitMode(context)) { - // Camera's natural orientation is landscape, so need to swap width and height. - size.height - } else { - size.width - } - } ?: layoutWidth + var previewWidth: Int = layoutWidth + var previewHeight: Int = layoutHeight - val previewHeight = cameraPreviewSize?.let { size -> + cameraPreviewSize?.let { size -> if (isPortraitMode(context)) { // Camera's natural orientation is landscape, so need to swap width and height. - size.width + previewWidth = size.height + previewHeight = size.width } else { - size.height + previewWidth = size.width + previewHeight = size.height } - } ?: layoutHeight + } // Match the width of the child view to its parent. if (layoutWidth*previewHeight <= layoutHeight*previewWidth) { - val scaledChildWidth = previewWidth * layoutHeight / previewHeight; + val scaledChildWidth = previewWidth * layoutHeight / previewHeight for (i in 0 until childCount) { getChildAt(i).layout((layoutWidth - scaledChildWidth) / 2, 0, (layoutWidth + scaledChildWidth) / 2, height) } } else { - // When the child view is too tall to be fitted in its parent: If the child view is - // static overlay view container (contains views such as bottom prompt chip), we apply - // the size of the parent view to it. Otherwise, we offset the top/bottom position - // equally to position it in the center of the parent. - val scaledChildHeight = previewHeight * layoutWidth / previewWidth; + val scaledChildHeight = previewHeight * layoutWidth / previewWidth for (i in 0 until childCount) { getChildAt(i).layout(0, (layoutHeight - scaledChildHeight) / 2, diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameMetadata.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameMetadata.kt index 3397ef8dd9b1..66837022917a 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameMetadata.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameMetadata.kt @@ -1,20 +1,7 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package openfoodfacts.github.scrachx.openfood.camera -/** Metadata info of a camera frame. */ +/** + * Metadata info of a camera frame. + */ class FrameMetadata(val width: Int, val height: Int, val rotation: Int) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameProcessor.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameProcessor.kt index 962fcc504af9..9d4f03b9fdf1 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameProcessor.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameProcessor.kt @@ -1,24 +1,11 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package openfoodfacts.github.scrachx.openfood.camera import java.nio.ByteBuffer -/** An interface to process the input camera frame and perform detection on it. */ +/** + * An interface to process the input camera frame and perform detection on it. + */ interface FrameProcessor { /** Processes the input frame with the underlying detector. */ diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameProcessorBase.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameProcessorBase.kt index 0ffd48fe35ee..306d7ae37629 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameProcessorBase.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/FrameProcessorBase.kt @@ -1,18 +1,3 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package openfoodfacts.github.scrachx.openfood.camera @@ -45,6 +30,7 @@ FrameProcessorBase : FrameProcessor { @GuardedBy("this") private var processingFrameMetaData: FrameMetadata? = null + private val executor = ScopedExecutor(TaskExecutors.MAIN_THREAD) @Synchronized @@ -66,6 +52,7 @@ FrameProcessorBase : FrameProcessor { processingFrameMetaData = latestFrameMetaData latestFrame = null latestFrameMetaData = null + val frame = processingFrame ?: return val frameMetaData = processingFrameMetaData ?: return val image = InputImage.fromByteBuffer( @@ -82,7 +69,7 @@ FrameProcessorBase : FrameProcessor { this@FrameProcessorBase.onSuccess(CameraInputInfo(frame, frameMetaData), results, graphicOverlay) processLatestFrame(graphicOverlay) } - .addOnFailureListener(executor) { e -> OnFailureListener { this@FrameProcessorBase.onFailure(it) } } + .addOnFailureListener(executor) { OnFailureListener { this@FrameProcessorBase.onFailure(it) } } } override fun stop() { @@ -91,7 +78,9 @@ FrameProcessorBase : FrameProcessor { protected abstract fun detectInImage(image: InputImage): Task - /** Be called when the detection succeeds. */ + /** + * Be called when the detection succeeds. + */ protected abstract fun onSuccess( inputInfo: InputInfo, results: T, diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/GraphicOverlay.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/GraphicOverlay.kt index 815ae2ffe391..73358bcf1b6c 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/GraphicOverlay.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/GraphicOverlay.kt @@ -1,18 +1,3 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package openfoodfacts.github.scrachx.openfood.camera @@ -26,10 +11,8 @@ import openfoodfacts.github.scrachx.openfood.utils.CameraUtils.isPortraitMode import java.util.ArrayList /** - * A view which renders a series of custom graphics to be overlaid on top of an associated preview - * (i.e., the camera preview). The creator can add graphics objects, update the objects, and remove - * them, triggering the appropriate drawing and invalidation within the view. - * + * A view which renders custom graphics overlaid on top of an associated preview + * (i.e., the camera preview). * * Supports scaling and mirroring of the graphics relative the camera's preview properties. The * idea is that detection items are expressed in terms of a preview size, but need to be scaled up @@ -59,7 +42,9 @@ class GraphicOverlay(context: Context, attrs: AttributeSet) : View(context, attr abstract fun draw(canvas: Canvas) } - /** Removes all graphics from the overlay. */ + /** + * Removes all graphics from the overlay. + */ fun clear() { synchronized(lock) { graphics.clear() @@ -67,7 +52,9 @@ class GraphicOverlay(context: Context, attrs: AttributeSet) : View(context, attr postInvalidate() } - /** Adds a graphic to the overlay. */ + /** + * Adds a graphic to the overlay. + */ fun add(graphic: Graphic) { synchronized(lock) { graphics.add(graphic) @@ -104,7 +91,9 @@ class GraphicOverlay(context: Context, attrs: AttributeSet) : View(context, attr translateY(rect.bottom.toFloat()) ) - /** Draws the overlay with its associated graphic objects. */ + /** + * Draws the overlay with its associated graphic objects. + */ override fun onDraw(canvas: Canvas) { super.onDraw(canvas) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/WorkflowModel.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/WorkflowModel.kt index 52ba1af9aa5a..eca3551ff3c5 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/WorkflowModel.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/WorkflowModel.kt @@ -1,29 +1,15 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package openfoodfacts.github.scrachx.openfood.camera import android.app.Application -import android.content.Context import androidx.annotation.MainThread import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import com.google.mlkit.vision.barcode.Barcode -/** View model for handling application workflow based on camera preview. */ +/** + * View model for handling application workflow based on camera preview. + */ class WorkflowModel(application: Application) : AndroidViewModel(application) { val workflowState = MutableLiveData() @@ -32,10 +18,6 @@ class WorkflowModel(application: Application) : AndroidViewModel(application) { var isCameraLive = false private set - - private val context: Context - get() = getApplication().applicationContext - /** * State set of the application workflow. */ @@ -43,9 +25,7 @@ class WorkflowModel(application: Application) : AndroidViewModel(application) { NOT_STARTED, DETECTING, DETECTED, - CONFIRMING, - SEARCHING, - SEARCHED + CONFIRMING } @MainThread @@ -53,7 +33,6 @@ class WorkflowModel(application: Application) : AndroidViewModel(application) { this.workflowState.value = workflowState } - fun markCameraLive() { isCameraLive = true } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/PreferencesFragment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/PreferencesFragment.kt index f112a64ce2a9..eaa8b9d2dfe9 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/PreferencesFragment.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/PreferencesFragment.kt @@ -154,15 +154,15 @@ class PreferencesFragment : PreferenceFragmentCompat(), INavigationItem, OnShare it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> if(newValue == true){ MaterialDialog.Builder(requireActivity()).run { - title("New: Enhanced “MLKit” scanner") - content(R.string.pref_mlkit) - positiveText("Proceed") + title(R.string.preference_choose_scanner_dialog_title) + content(R.string.preference_choose_scanner_dialog_body) + positiveText(R.string.proceed) onPositive { _, _ -> it.isChecked = true settings.edit { putBoolean(getString(R.string.pref_scanner_type_key), newValue as Boolean) } Toast.makeText(requireActivity(), getString(R.string.changes_saved), Toast.LENGTH_SHORT).show() } - negativeText("Cancel") + negativeText(R.string.dialog_cancel) onNegative { dialog, _ -> dialog.dismiss() it.isChecked = false @@ -174,6 +174,7 @@ class PreferencesFragment : PreferenceFragmentCompat(), INavigationItem, OnShare else{ it.isChecked = false settings.edit { putBoolean(getString(R.string.pref_scanner_type_key), newValue as Boolean) } + Toast.makeText(requireActivity(), getString(R.string.changes_saved), Toast.LENGTH_SHORT).show() } true } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt index e27952c6ac46..9ce2a0afb3b4 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt @@ -23,7 +23,6 @@ import android.util.Log import android.view.* import android.view.Gravity.CENTER import android.view.inputmethod.EditorInfo -import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import android.widget.TextView.OnEditorActionListener @@ -42,7 +41,7 @@ import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback import com.google.android.material.bottomsheet.BottomSheetBehavior.from -import com.google.mlkit.vision.barcode.* +import com.google.android.material.chip.Chip import com.google.zxing.BarcodeFormat import com.google.zxing.ResultPoint import com.google.zxing.client.android.BeepManager @@ -120,7 +119,7 @@ class ContinuousScanActivity : AppCompatActivity() { private var graphicOverlay: GraphicOverlay? = null private var workflowModel: WorkflowModel? = null private var currentWorkflowState: WorkflowState? = null - + private var promptChip: Chip? = null private val commonDisp = CompositeDisposable() private var productDisp: Disposable? = null @@ -135,6 +134,9 @@ class ContinuousScanActivity : AppCompatActivity() { private var analysisTagsEmpty = true private var productShowing = false private var beepActive = false + /** + boolean to determine if MLKit Scanner is to be used + */ private var useMLScanner = false private var offlineSavedProduct: OfflineSavedProduct? = null @@ -159,6 +161,7 @@ class ContinuousScanActivity : AppCompatActivity() { binding.barcodeScanner.visibility = View.GONE binding.cameraPreview.visibility = View.GONE binding.barcodeScanner.pause() + stopCameraPreview() binding.imageForScreenshotGenerationOnly.visibility = View.VISIBLE setShownProduct(barcode) } @@ -522,8 +525,7 @@ class ContinuousScanActivity : AppCompatActivity() { cameraSource = CameraSource(this).apply { requestedCameraId = cameraState - Log.i("CSSAA", "inside on create CameraSource apply") - this.setFocusMode(autoFocusActive) + setFocusMode(autoFocusActive) } setOnClickListener{ quickViewBehavior.state = BottomSheetBehavior.STATE_HIDDEN @@ -531,6 +533,8 @@ class ContinuousScanActivity : AppCompatActivity() { startCameraPreview() } } + promptChip = findViewById(R.id.bottom_prompt_chip) + setUpWorkflowModel() } @@ -556,17 +560,19 @@ class ContinuousScanActivity : AppCompatActivity() { when (workflowState) { WorkflowState.DETECTING -> { + promptChip?.visibility = View.VISIBLE + promptChip?.setText(R.string.prompt_point_at_a_barcode) startCameraPreview() } WorkflowState.CONFIRMING -> { + promptChip?.visibility = View.VISIBLE + promptChip?.setText(R.string.prompt_move_camera_closer) startCameraPreview() } - WorkflowState.SEARCHING -> { - stopCameraPreview() - } - WorkflowState.DETECTED, WorkflowState.SEARCHED -> { + WorkflowState.DETECTED -> { stopCameraPreview() } + else -> promptChip?.visibility = View.GONE } }) @@ -574,22 +580,22 @@ class ContinuousScanActivity : AppCompatActivity() { workflowModel?.detectedBarcode?.observe(this, { barcode -> if (barcode != null) { mlBarcodeCallback(barcode.rawValue) - Log.i("inside","barcode "+barcode.rawValue) + Log.i(LOG_TAG,"barcode ="+barcode.rawValue) } }) } private fun startCameraPreview() { - Log.i("inside","startCamerra Preview") val workflowModel = this.workflowModel ?: return val cameraSource = this.cameraSource ?: return + if (!workflowModel.isCameraLive) { try { workflowModel.markCameraLive() preview?.start(cameraSource) } catch (e: IOException) { - Log.e("ContinuousScanActivity", "Failed to start camera preview!", e) + Log.e(LOG_TAG, "Failed to start camera preview!", e) cameraSource.release() this.cameraSource = null } @@ -597,8 +603,8 @@ class ContinuousScanActivity : AppCompatActivity() { } private fun stopCameraPreview() { - Log.i("inside","stopCamerra Preview") val workflowModel = this.workflowModel ?: return + if (workflowModel.isCameraLive) { workflowModel.markCameraFrozen() preview?.stop() @@ -606,7 +612,6 @@ class ContinuousScanActivity : AppCompatActivity() { } private fun mlBarcodeCallback(barcodeValue:String?){ - hintBarcodeDisp?.dispose() // Prevent duplicate scans @@ -623,15 +628,14 @@ class ContinuousScanActivity : AppCompatActivity() { } override fun onStart() { - Log.i("inside","onstart") super.onStart() EventBus.getDefault().register(this) } override fun onResume() { - Log.i("inside","onresume") super.onResume() binding.bottomNavigation.bottomNavigation.selectNavigationItem(R.id.scan_bottom_nav) + if(!useMLScanner && quickViewBehavior.state != BottomSheetBehavior.STATE_EXPANDED) { binding.barcodeScanner.resume() } @@ -644,7 +648,6 @@ class ContinuousScanActivity : AppCompatActivity() { } override fun onPostResume() { - Log.i("inside","omPostResume") super.onPostResume() // Back to working state after the bottom sheet is dismissed. ViewModelProviders.of(this).get(WorkflowModel::class.java).setWorkflowState(WorkflowState.DETECTING) @@ -660,7 +663,6 @@ class ContinuousScanActivity : AppCompatActivity() { binding.barcodeScanner.pause() } else{ - Log.i("inside","onPause") currentWorkflowState = WorkflowState.NOT_STARTED stopCameraPreview() } @@ -673,7 +675,6 @@ class ContinuousScanActivity : AppCompatActivity() { } override fun onDestroy() { - Log.i("inside","onDestroy") summaryProductPresenter?.dispose() cameraSource?.release() @@ -716,7 +717,7 @@ class ContinuousScanActivity : AppCompatActivity() { // turn flash on if flashActive true in pref if (flashActive) { if(useMLScanner) { - cameraSource!!.updateFlashMode(flashActive) + cameraSource?.updateFlashMode(flashActive) binding.toggleFlash.setImageResource(R.drawable.ic_flash_on_white_24dp) } else{ binding.barcodeScanner.setTorchOn() @@ -747,6 +748,7 @@ class ContinuousScanActivity : AppCompatActivity() { || product!!.ingredientsText == null || product!!.ingredientsText == "" + @Suppress("deprecation") private fun toggleCamera() { if(!useMLScanner) { val settings = binding.barcodeScanner.barcodeView.cameraSettings @@ -771,7 +773,6 @@ class ContinuousScanActivity : AppCompatActivity() { } private fun toggleFlash() { - Log.i("CSSAA", "inside toggle flash") cameraPref.edit { if (flashActive) { flashActive = false @@ -826,6 +827,9 @@ class ContinuousScanActivity : AppCompatActivity() { } val settings = binding.barcodeScanner.barcodeView.cameraSettings settings.isAutoFocusEnabled = autoFocusActive + binding.barcodeScanner.resume() + binding.barcodeScanner.barcodeView.cameraSettings = settings + } } R.id.troubleScanning -> { @@ -863,7 +867,6 @@ class ContinuousScanActivity : AppCompatActivity() { } fun collapseBottomSheet() { - Log.i("inside","collapse bottomm screen") quickViewBehavior.state = BottomSheetBehavior.STATE_HIDDEN } @@ -874,9 +877,8 @@ class ContinuousScanActivity : AppCompatActivity() { private inner class QuickViewCallback : BottomSheetCallback() { private var previousSlideOffset = 0f + override fun onStateChanged(bottomSheet: View, newState: Int) { - Log.i("inside","onStateChanged") - Log.i("inside","new state = " + newState) when (newState) { BottomSheetBehavior.STATE_HIDDEN -> { @@ -886,7 +888,6 @@ class ContinuousScanActivity : AppCompatActivity() { BottomSheetBehavior.STATE_COLLAPSED -> { if (useMLScanner) { workflowModel?.setWorkflowState(WorkflowState.DETECTED) - Log.i("inside", "stop camers Preview from line 871") stopCameraPreview() } else { binding.barcodeScanner.pause() @@ -895,7 +896,6 @@ class ContinuousScanActivity : AppCompatActivity() { else -> { if (useMLScanner) { workflowModel?.setWorkflowState(WorkflowState.DETECTED) - Log.i("inside", "stop camers Preview from line 888") stopCameraPreview() } else { binding.barcodeScanner.pause() @@ -913,7 +913,6 @@ class ContinuousScanActivity : AppCompatActivity() { } override fun onSlide(bottomSheet: View, slideOffset: Float) { - Log.i("inside","onSlide") val slideDelta = slideOffset - previousSlideOffset if (binding.quickViewSearchByBarcode.visibility != View.VISIBLE && binding.quickViewProgress.visibility != View.VISIBLE) { if (slideOffset > 0.01f || slideOffset < -0.01f) { @@ -925,7 +924,6 @@ class ContinuousScanActivity : AppCompatActivity() { binding.quickViewDetails.visibility = View.GONE binding.quickViewTags.visibility = View.GONE if(useMLScanner){ - Log.i("inside", "stop camers Preview from line 903") workflowModel?.setWorkflowState(WorkflowState.DETECTED) stopCameraPreview() } @@ -939,7 +937,6 @@ class ContinuousScanActivity : AppCompatActivity() { } else { if(useMLScanner){ workflowModel?.setWorkflowState(WorkflowState.DETECTING) - Log.i("inside", "startCamera Preview from line 915") startCameraPreview() } else { diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeConfirmingGraphic.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeConfirmingGraphic.kt index af39b602e603..b1cfcf6d51c0 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeConfirmingGraphic.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeConfirmingGraphic.kt @@ -1,18 +1,3 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package openfoodfacts.github.scrachx.openfood.scanner @@ -20,9 +5,11 @@ import android.graphics.Canvas import android.graphics.Path import com.google.mlkit.vision.barcode.Barcode import openfoodfacts.github.scrachx.openfood.camera.GraphicOverlay -import openfoodfacts.github.scrachx.openfood.utils.CameraPreferenceUtils +import openfoodfacts.github.scrachx.openfood.utils.CameraUtils -/** Guides user to move camera closer to confirm the detected barcode. */ +/** + * Guides user to move camera closer to confirm the detected barcode. + */ internal class BarcodeConfirmingGraphic(overlay: GraphicOverlay, private val barcode: Barcode) : BarcodeGraphicBase(overlay) { @@ -30,7 +17,7 @@ internal class BarcodeConfirmingGraphic(overlay: GraphicOverlay, private val bar super.draw(canvas) // Draws a highlighted path to indicate the current progress to meet size requirement. - val sizeProgress = CameraPreferenceUtils.getProgressToMeetBarcodeSizeRequirement(overlay, barcode) + val sizeProgress = CameraUtils.getProgressToMeetBarcodeSizeRequirement(overlay, barcode) val path = Path() if (sizeProgress > 0.95f) { // To have a completed path with all corners rounded. diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeGraphicBase.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeGraphicBase.kt index cacf04f92fb1..2989e840d0f4 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeGraphicBase.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeGraphicBase.kt @@ -1,18 +1,3 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package openfoodfacts.github.scrachx.openfood.scanner @@ -27,7 +12,7 @@ import android.graphics.RectF import androidx.core.content.ContextCompat import openfoodfacts.github.scrachx.openfood.R import openfoodfacts.github.scrachx.openfood.camera.GraphicOverlay -import openfoodfacts.github.scrachx.openfood.utils.CameraPreferenceUtils.getBarcodeReticleBox +import openfoodfacts.github.scrachx.openfood.utils.CameraUtils.getBarcodeReticleBox internal abstract class BarcodeGraphicBase(overlay: GraphicOverlay) : GraphicOverlay.Graphic(overlay) { diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeProcessor.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeProcessor.kt index dc4d80808e51..cacf96ee338d 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeProcessor.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeProcessor.kt @@ -1,7 +1,6 @@ + package openfoodfacts.github.scrachx.openfood.scanner -import android.animation.ValueAnimator -import android.graphics.Camera import android.util.Log import androidx.annotation.MainThread import com.google.android.gms.tasks.Task @@ -14,24 +13,26 @@ import openfoodfacts.github.scrachx.openfood.camera.FrameProcessorBase import openfoodfacts.github.scrachx.openfood.camera.GraphicOverlay import openfoodfacts.github.scrachx.openfood.camera.WorkflowModel import openfoodfacts.github.scrachx.openfood.camera.WorkflowModel.WorkflowState -import openfoodfacts.github.scrachx.openfood.utils.CameraPreferenceUtils +import openfoodfacts.github.scrachx.openfood.utils.CameraUtils import openfoodfacts.github.scrachx.openfood.utils.InputInfo import java.io.IOException - -/** A processor to run the barcode detector. */ +/** + * A processor to run the barcode detector. + */ class BarcodeProcessor(graphicOverlay: GraphicOverlay, private val workflowModel: WorkflowModel) : FrameProcessorBase>() { private val options = BarcodeScannerOptions.Builder() - .setBarcodeFormats(Barcode.FORMAT_UPC_A, + .setBarcodeFormats( + Barcode.FORMAT_UPC_A, Barcode.FORMAT_UPC_E, Barcode.FORMAT_EAN_13, Barcode.FORMAT_EAN_8, Barcode.FORMAT_CODE_39, Barcode.FORMAT_CODE_93, - Barcode.FORMAT_CODE_128) - .build() + Barcode.FORMAT_CODE_128 + ).build() private val scanner = BarcodeScanning.getClient(options) private val cameraReticleAnimator: CameraReticleAnimator = CameraReticleAnimator(graphicOverlay) @@ -65,16 +66,15 @@ class BarcodeProcessor(graphicOverlay: GraphicOverlay, private val workflowModel workflowModel.setWorkflowState(WorkflowState.DETECTING) } else { cameraReticleAnimator.cancel() - val sizeProgress = CameraPreferenceUtils.getProgressToMeetBarcodeSizeRequirement(graphicOverlay, barcodeInCenter) + val sizeProgress = CameraUtils.getProgressToMeetBarcodeSizeRequirement(graphicOverlay, barcodeInCenter) if (sizeProgress < 1) { // Barcode in the camera view is too small, so prompt user to move camera closer. graphicOverlay.add(BarcodeConfirmingGraphic(graphicOverlay, barcodeInCenter)) workflowModel.setWorkflowState(WorkflowState.CONFIRMING) } else { // Barcode size in the camera view is sufficient. - workflowModel.setWorkflowState(WorkflowState.DETECTED) - workflowModel.detectedBarcode.setValue(barcodeInCenter) - + workflowModel.setWorkflowState(WorkflowState.DETECTED) + workflowModel.detectedBarcode.setValue(barcodeInCenter) } } graphicOverlay.invalidate() diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeReticleGraphic.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeReticleGraphic.kt index 051b2da531a7..e7eb6e7366b5 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeReticleGraphic.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeReticleGraphic.kt @@ -1,16 +1,3 @@ -// * Copyright 2020 Google LLC -// * -// * Licensed under the Apache License, Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * https://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. package openfoodfacts.github.scrachx.openfood.scanner diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/CameraPreferenceUtils.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/CameraPreferenceUtils.kt deleted file mode 100644 index 17ab93ad8a39..000000000000 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/CameraPreferenceUtils.kt +++ /dev/null @@ -1,72 +0,0 @@ -package openfoodfacts.github.scrachx.openfood.utils - -import android.content.Context -import android.graphics.RectF -import android.preference.PreferenceManager -import android.view.WindowManager -import androidx.annotation.StringRes -import com.google.android.gms.common.images.Size -import com.google.mlkit.vision.barcode.Barcode -import openfoodfacts.github.scrachx.openfood.R -import openfoodfacts.github.scrachx.openfood.camera.CameraSizePair -import openfoodfacts.github.scrachx.openfood.camera.GraphicOverlay - -object CameraPreferenceUtils { - - fun saveStringPreference(context: Context, @StringRes prefKeyId: Int, value: String?) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit() - .putString(context.getString(prefKeyId), value) - .apply() - } - - - fun getUserSpecifiedPreviewSize(context: Context): CameraSizePair? { - return try { - val previewSizePrefKey = context.getString(R.string.pref_key_rear_camera_preview_size) - val pictureSizePrefKey = context.getString(R.string.pref_key_rear_camera_picture_size) - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) - CameraSizePair( - Size.parseSize(sharedPreferences.getString(previewSizePrefKey, null)), - Size.parseSize(sharedPreferences.getString(pictureSizePrefKey, null)) - ) - } catch (e: Exception) { - null - } - } - - fun getProgressToMeetBarcodeSizeRequirement( - overlay: GraphicOverlay, - barcode: Barcode - ): Float { - val context = overlay.context - return if (getBooleanPref(context, R.string.pref_key_enable_barcode_size_check, false)) { - val reticleBoxWidth = getBarcodeReticleBox(overlay).width() - val barcodeWidth = overlay.translateX(barcode.boundingBox?.width()?.toFloat() ?: 0f) - val requiredWidth = reticleBoxWidth * getIntPref(context, R.string.pref_key_minimum_barcode_width, 50) / 100 - (barcodeWidth / requiredWidth).coerceAtMost(1f) - } else { - 1f - } - } - - private fun getBooleanPref(context: Context, @StringRes prefKeyId: Int, defaultValue: Boolean): Boolean = - PreferenceManager.getDefaultSharedPreferences(context).getBoolean(context.getString(prefKeyId), defaultValue) - - private fun getIntPref(context: Context, @StringRes prefKeyId: Int, defaultValue: Int): Int { - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) - val prefKey = context.getString(prefKeyId) - return sharedPreferences.getInt(prefKey, defaultValue) - } - - fun getBarcodeReticleBox(overlay: GraphicOverlay): RectF { - val context = overlay.context - val overlayWidth = overlay.width.toFloat() - val overlayHeight = overlay.height.toFloat() - val boxWidth = overlayWidth * getIntPref(context, R.string.pref_key_barcode_reticle_width, 80) / 100 - val boxHeight = overlayHeight * getIntPref(context, R.string.pref_key_barcode_reticle_height, 40) / 100 - val cx = overlayWidth / 2 - val cy = overlayHeight / 2 - return RectF(cx - boxWidth / 2, cy - boxHeight / 2, cx + boxWidth / 2, cy + boxHeight / 2) - } -} \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/CameraUtils.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/CameraUtils.kt index 2a18f30f7eb8..ee56b6849a83 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/CameraUtils.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/CameraUtils.kt @@ -1,16 +1,19 @@ +@file:Suppress("DEPRECATION") + package openfoodfacts.github.scrachx.openfood.utils import android.content.Context -import android.content.pm.PackageManager import android.content.res.Configuration import android.graphics.* import android.hardware.Camera import android.util.Log +import com.google.mlkit.vision.barcode.Barcode import com.google.mlkit.vision.common.InputImage import openfoodfacts.github.scrachx.openfood.camera.CameraSizePair +import openfoodfacts.github.scrachx.openfood.camera.GraphicOverlay import java.io.ByteArrayOutputStream import java.nio.ByteBuffer -import java.util.ArrayList +import java.util.* import kotlin.math.abs @@ -23,7 +26,6 @@ object CameraUtils { */ const val ASPECT_RATIO_TOLERANCE = 0.01f - /** * Check if the camera is in portrait mode. * @@ -32,7 +34,6 @@ object CameraUtils { fun isPortraitMode(context: Context): Boolean = context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT - /** * Generates a list of acceptable preview sizes. Preview sizes are not acceptable if there is not * a corresponding picture size of the same aspect ratio. If there is a corresponding picture size @@ -101,4 +102,21 @@ object CameraUtils { return null } + fun getProgressToMeetBarcodeSizeRequirement(overlay: GraphicOverlay, barcode: Barcode): Float { + val reticleBoxWidth = getBarcodeReticleBox(overlay).width() + val barcodeWidth = overlay.translateX(barcode.boundingBox?.width()?.toFloat() ?: 0f) + val requiredWidth = reticleBoxWidth * 50/100 + return (barcodeWidth / requiredWidth).coerceAtMost(1f) + } + + fun getBarcodeReticleBox(overlay: GraphicOverlay): RectF { + val overlayWidth = overlay.width.toFloat() + val overlayHeight = overlay.height.toFloat() + val boxWidth = overlayWidth * 80 / 100 + val boxHeight = overlayHeight * 40 / 100 + val cx = overlayWidth / 2 + val cy = overlayHeight / 2 + return RectF(cx - boxWidth / 2, cy - boxHeight / 2, cx + boxWidth / 2, cy + boxHeight / 2) + } + } \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/inputInfo.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/inputInfo.kt index 6f9a3868f618..cf09d8a3ac95 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/inputInfo.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/inputInfo.kt @@ -12,7 +12,6 @@ class CameraInputInfo( private val frameByteBuffer: ByteBuffer, private val frameMetadata: FrameMetadata ) : InputInfo { - private var bitmap: Bitmap? = null @Synchronized @@ -25,9 +24,3 @@ class CameraInputInfo( } } } - -class BitmapInputInfo(private val bitmap: Bitmap) : InputInfo { - override fun getBitmap(): Bitmap { - return bitmap - } -} diff --git a/app/src/main/res/layout/camera_preview_overlay.xml b/app/src/main/res/layout/camera_preview_overlay.xml index 24fa05af182a..65edf450a090 100644 --- a/app/src/main/res/layout/camera_preview_overlay.xml +++ b/app/src/main/res/layout/camera_preview_overlay.xml @@ -13,13 +13,22 @@ android:layout_height="match_parent" android:background="@color/transparent"> - + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 3ffd074ac4e0..e8d542523dc1 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -34,7 +34,6 @@ #ffffff #000000 #808080 - #00000000 #FF0000 #FF6600 @@ -62,6 +61,8 @@ #d7ccc8 + + #00000000 #9AFFFFFF #1F000000 #9AFFFFFF diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index ae9bbd410cdd..4786fce95d8f 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -71,8 +71,13 @@ 12sp 12sp + 4dp 40dp 4dp 8dp + 14sp + 28dp + 8dp + diff --git a/app/src/main/res/values/pref_keys.xml b/app/src/main/res/values/pref_keys.xml index 6e9f6ca33249..b50ae598a36a 100644 --- a/app/src/main/res/values/pref_keys.xml +++ b/app/src/main/res/values/pref_keys.xml @@ -27,7 +27,6 @@ Preferred energy unit volumeUnitPreference - select_scanner Preferred volume unit deleteSearchHistoryPreference @@ -98,8 +97,9 @@ Crop new images Enables crop action on new images - We included a new option to more reliably scan barcodes. While this scanner works on your device, using machine learning, please be aware that it is a proprietary component provided by Google, governed by this privacy policy, and that some limited telemetry might be sent back to Google’s servers to improve their software. As noted in the MLKit terms, this won’t include any information about the products you scan and the telemetry is anonymized. Choosing MLKit scanner implies that you accept those terms. -Note: you can switch back at any time between the 2 scanners in the Settings. - + Use MLKit Scanner + Increases Scanning efficiency. + Switch off to use Classic Barcode Scanner. + select_scanner \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9bbd17878f4f..14127cbbcd0f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -694,7 +694,16 @@ Preferences Upload anyway Changes saved successfully + Proceed Choose language + New: Enhanced “MLKit” scanner + We have included a new option to more reliably scan barcodes. While this scanner works on your device, using machine learning, + please be aware that it is a proprietary component provided by Google, governed by this privacy policy, and that some limited telemetry might be sent back to Google’s servers + to improve their software. As noted in the MLKit terms, this won’t include any information about the products you scan and the telemetry is anonymized. Choosing MLKit scanner + implies that you accept those terms\nNote: You can switch back at any time between the 2 scanners in the Settings. + + Point your camera at a barcode + Move closer to detect Please check your connection. @@ -1096,27 +1105,4 @@ Examples: USA, France, Italy Send a report email - - Camera - rcpvs - rcpts - Rear camera preview size - - - Barcode detection - barcode_brw - Barcode reticle width - Relative to the camera view width, ranges from 50% to 95% - barcode_brh - Barcode reticle height - Relative to the camera view height, ranges from 20% to 80% - barcode_ebsc - Enable barcode size check - Will prompt user to move camera closer if the detected barcode is too small - barcode_mbw - Minimum barcode width - Relative to the reticle width, ranges from 20% to 80% (only applicable when barcode size check enabled) - barcode_dlbr - Delay loading barcode result - Will show the loading spinner for 2s diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index e4c28384994a..c63e0e568dab 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -49,8 +49,9 @@ + android:title="@string/pref_scanner_startup_title" + app:summaryOff="@string/pref_scanner_startup_summaryOff" + android:summaryOn="@string/pref_scanner_startup_summaryOn"/> Date: Fri, 19 Feb 2021 09:14:32 +0530 Subject: [PATCH 08/12] minor bug fix in cam pref and flash front cam --- .../scrachx/openfood/camera/CameraSource.kt | 34 ++++++++++++++----- .../features/scan/ContinuousScanActivity.kt | 24 ++++++------- app/src/main/res/values/pref_keys.xml | 4 +-- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt index 948f4c72eea0..4a45987824da 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt @@ -40,6 +40,10 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { var requestedCameraId = CameraInfo.CAMERA_FACING_BACK + var requestedFocusState = true + + var requestedFlashState = false + /** Returns the preview size that is currently in use by the underlying camera. */ internal var previewSize: Size? = null private set @@ -149,7 +153,7 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { fun updateFlashMode(flashActive: Boolean) { val parameters = camera?.parameters - if(flashActive) { + if(flashActive && requestedCameraId == CAMERA_FACING_BACK) { parameters?.flashMode = Parameters.FLASH_MODE_TORCH } else{ parameters?.flashMode = Parameters.FLASH_MODE_OFF @@ -158,17 +162,18 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { } fun setFocusMode(autoFocusActive: Boolean) { + val parameters = camera?.parameters if(autoFocusActive) { - val parameters = camera?.parameters - if (parameters?.supportedFocusModes?.contains(Parameters.FOCUS_MODE_AUTO) == true) { - parameters.focusMode = Parameters.FOCUS_MODE_AUTO + Log.i(TAG,"Supported focus mode = " + parameters!!.supportedFocusModes) + if(parameters.supportedFocusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { + parameters.focusMode = Parameters.FOCUS_MODE_CONTINUOUS_VIDEO } else { - Log.i(TAG, "Camera auto focus is not supported on this device.") + Log.i(TAG, "Camera continuous mode is not supported on this device.") } - camera?.parameters = parameters } else { - camera?.cancelAutoFocus() + parameters!!.focusMode = Parameters.FOCUS_MODE_FIXED } + camera?.parameters = parameters } fun switchCamera() : Camera { @@ -189,7 +194,8 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { */ @Throws(IOException::class) private fun createCamera(): Camera { - val camera = Camera.open(requestedCameraId) ?: throw IOException("There is no back-facing camera.") + val camera = Camera.open(requestedCameraId) ?: throw IOException("Requested Camera not installed.") + val parameters = camera.parameters setPreviewAndPictureSize(camera, parameters) setRotation(camera, parameters) @@ -203,7 +209,17 @@ class CameraSource(private val graphicOverlay: GraphicOverlay) { parameters.previewFormat = IMAGE_FORMAT - setFocusMode(true) + if(requestedFlashState){ + parameters.flashMode = Parameters.FLASH_MODE_TORCH + } + + if(requestedFocusState){ + if(parameters.supportedFocusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { + parameters.focusMode = Parameters.FOCUS_MODE_CONTINUOUS_VIDEO + } else { + Log.i(TAG, "Camera continuous mode is not supported on this device.") + } + } camera.parameters = parameters diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt index 9ce2a0afb3b4..6bbe57dc0bd1 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt @@ -525,7 +525,8 @@ class ContinuousScanActivity : AppCompatActivity() { cameraSource = CameraSource(this).apply { requestedCameraId = cameraState - setFocusMode(autoFocusActive) + requestedFlashState = flashActive + requestedFocusState = autoFocusActive } setOnClickListener{ quickViewBehavior.state = BottomSheetBehavior.STATE_HIDDEN @@ -716,12 +717,9 @@ class ContinuousScanActivity : AppCompatActivity() { it.menuInflater.inflate(R.menu.popup_menu, it.menu) // turn flash on if flashActive true in pref if (flashActive) { - if(useMLScanner) { - cameraSource?.updateFlashMode(flashActive) - binding.toggleFlash.setImageResource(R.drawable.ic_flash_on_white_24dp) - } else{ + binding.toggleFlash.setImageResource(R.drawable.ic_flash_on_white_24dp) + if(!useMLScanner) { binding.barcodeScanner.setTorchOn() - binding.toggleFlash.setImageResource(R.drawable.ic_flash_on_white_24dp) } } if (beepActive) { @@ -750,19 +748,21 @@ class ContinuousScanActivity : AppCompatActivity() { @Suppress("deprecation") private fun toggleCamera() { + + cameraState = if (cameraState == Camera.CameraInfo.CAMERA_FACING_BACK) { + Camera.CameraInfo.CAMERA_FACING_FRONT + } else { + Camera.CameraInfo.CAMERA_FACING_BACK + } + cameraPref.edit { putInt(SETTING_STATE, cameraState) } + if(!useMLScanner) { val settings = binding.barcodeScanner.barcodeView.cameraSettings if (binding.barcodeScanner.barcodeView.isPreviewActive) { binding.barcodeScanner.pause() } - cameraState = if (settings.requestedCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) { - Camera.CameraInfo.CAMERA_FACING_FRONT - } else { - Camera.CameraInfo.CAMERA_FACING_BACK - } settings.requestedCameraId = cameraState binding.barcodeScanner.barcodeView.cameraSettings = settings - cameraPref.edit { putInt(SETTING_STATE, cameraState) } binding.barcodeScanner.resume() } else { stopCameraPreview() diff --git a/app/src/main/res/values/pref_keys.xml b/app/src/main/res/values/pref_keys.xml index b50ae598a36a..6a7de000c6a0 100644 --- a/app/src/main/res/values/pref_keys.xml +++ b/app/src/main/res/values/pref_keys.xml @@ -98,8 +98,8 @@ Enables crop action on new images Use MLKit Scanner - Increases Scanning efficiency. - Switch off to use Classic Barcode Scanner. + Increases Scanning efficiency. + Switch off to use Classic Barcode Scanner. select_scanner \ No newline at end of file From 91f7e18a523b94e67bf84b51ac8bc2560286972c Mon Sep 17 00:00:00 2001 From: Kartikay Sharma Date: Wed, 24 Feb 2021 23:02:38 +0000 Subject: [PATCH 09/12] platform build dimension added --- app/build.gradle.kts | 12 +- .../features/scan/ContinuousScanActivity.kt | 817 ++++++++++++++++++ .../res/layout/activity_continuous_scan.xml | 345 ++++++++ .../github/scrachx/openfood/AppFlavors.kt | 2 +- .../openfood/features/PreferencesFragment.kt | 2 + .../openfood/camera/CameraReticleAnimator.kt | 0 .../scrachx/openfood/camera/CameraSizePair.kt | 0 .../scrachx/openfood/camera/CameraSource.kt | 0 .../openfood/camera/CameraSourcePreview.kt | 0 .../scrachx/openfood/camera/FrameMetadata.kt | 0 .../scrachx/openfood/camera/FrameProcessor.kt | 0 .../openfood/camera/FrameProcessorBase.kt | 0 .../scrachx/openfood/camera/GraphicOverlay.kt | 0 .../scrachx/openfood/camera/WorkflowModel.kt | 0 .../features/scan/ContinuousScanActivity.kt | 1 + .../scanner/BarcodeConfirmingGraphic.kt | 0 .../openfood/scanner/BarcodeGraphicBase.kt | 0 .../openfood/scanner/BarcodeProcessor.kt | 0 .../openfood/scanner/BarcodeReticleGraphic.kt | 0 .../scrachx/openfood/utils/CameraUtils.kt | 0 .../scrachx/openfood/utils/InputInfo.kt} | 0 .../scrachx/openfood/utils/ScopedExecutor.kt | 0 .../res/layout/activity_continuous_scan.xml | 0 .../res/layout/camera_preview_overlay.xml | 0 24 files changed, 1177 insertions(+), 2 deletions(-) create mode 100644 app/src/fdroid/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt create mode 100644 app/src/fdroid/res/layout/activity_continuous_scan.xml rename app/src/{main => playstore}/java/openfoodfacts/github/scrachx/openfood/camera/CameraReticleAnimator.kt (100%) rename app/src/{main => playstore}/java/openfoodfacts/github/scrachx/openfood/camera/CameraSizePair.kt (100%) rename app/src/{main => playstore}/java/openfoodfacts/github/scrachx/openfood/camera/CameraSource.kt (100%) rename app/src/{main => playstore}/java/openfoodfacts/github/scrachx/openfood/camera/CameraSourcePreview.kt (100%) rename app/src/{main => playstore}/java/openfoodfacts/github/scrachx/openfood/camera/FrameMetadata.kt (100%) rename app/src/{main => playstore}/java/openfoodfacts/github/scrachx/openfood/camera/FrameProcessor.kt (100%) rename app/src/{main => playstore}/java/openfoodfacts/github/scrachx/openfood/camera/FrameProcessorBase.kt (100%) rename app/src/{main => playstore}/java/openfoodfacts/github/scrachx/openfood/camera/GraphicOverlay.kt (100%) rename app/src/{main => playstore}/java/openfoodfacts/github/scrachx/openfood/camera/WorkflowModel.kt (100%) rename app/src/{main => playstore}/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt (99%) rename app/src/{main => playstore}/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeConfirmingGraphic.kt (100%) rename app/src/{main => playstore}/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeGraphicBase.kt (100%) rename app/src/{main => playstore}/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeProcessor.kt (100%) rename app/src/{main => playstore}/java/openfoodfacts/github/scrachx/openfood/scanner/BarcodeReticleGraphic.kt (100%) rename app/src/{main => playstore}/java/openfoodfacts/github/scrachx/openfood/utils/CameraUtils.kt (100%) rename app/src/{main/java/openfoodfacts/github/scrachx/openfood/utils/inputInfo.kt => playstore/java/openfoodfacts/github/scrachx/openfood/utils/InputInfo.kt} (100%) rename app/src/{main => playstore}/java/openfoodfacts/github/scrachx/openfood/utils/ScopedExecutor.kt (100%) rename app/src/{main => playstore}/res/layout/activity_continuous_scan.xml (100%) rename app/src/{main => playstore}/res/layout/camera_preview_overlay.xml (100%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a7d9330af22f..8e6f2c15cff0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -203,7 +203,7 @@ android { dataBinding = true } - flavorDimensions("versionCode") + flavorDimensions("versionCode", "platform") defaultConfig { @@ -276,6 +276,7 @@ android { buildConfigField("String", "OFWEBSITE", "\"https://world.openfoodfacts.org/\"") buildConfigField("String", "WIKIDATA", "\"https://www.wikidata.org/wiki/Special:EntityData/\"") buildConfigField("String", "STATICURL", "\"https://static.openfoodfacts.org\"") + dimension = "versionCode" } create("obf") { applicationId = "openfoodfacts.github.scrachx.openbeauty" @@ -286,6 +287,7 @@ android { buildConfigField("String", "OFWEBSITE", "\"https://world.openbeautyfacts.org/\"") buildConfigField("String", "WIKIDATA", "\"https://www.wikidata.org/wiki/Special:EntityData/\"") buildConfigField("String", "STATICURL", "\"https://static.openbeautyfacts.org\"") + dimension = "versionCode" } create("opff") { applicationId = "org.openpetfoodfacts.scanner" @@ -296,6 +298,7 @@ android { buildConfigField("String", "OFWEBSITE", "\"https://world.openpetfoodfacts.org/\"") buildConfigField("String", "WIKIDATA", "\"https://www.wikidata.org/wiki/Special:EntityData/\"") buildConfigField("String", "STATICURL", "\"https://static.openpetfoodfacts.org\"") + dimension = "versionCode" } create("opf") { applicationId = "org.openproductsfacts.scanner" @@ -306,6 +309,13 @@ android { buildConfigField("String", "OFWEBSITE", "\"https://world.openproductsfacts.org/\"") buildConfigField("String", "WIKIDATA", "\"https://www.wikidata.org/wiki/Special:EntityData/\"") buildConfigField("String", "STATICURL", "\"https://static.openproductsfacts.org\"") + dimension = "versionCode" + } + create("playstore") { + dimension = "platform" + } + create("fdroid") { + dimension = "platform" } } diff --git a/app/src/fdroid/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt b/app/src/fdroid/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt new file mode 100644 index 000000000000..ea83dfdb10b6 --- /dev/null +++ b/app/src/fdroid/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt @@ -0,0 +1,817 @@ +/* + * Copyright 2016-2020 Open Food Facts + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package openfoodfacts.github.scrachx.openfood.features.scan + +import android.content.Context +import android.content.Intent +import android.hardware.Camera +import android.os.Bundle +import android.util.Log +import android.view.* +import android.view.Gravity.CENTER +import android.view.inputmethod.EditorInfo +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.TextView.OnEditorActionListener +import android.widget.Toast +import androidx.annotation.StringRes +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.PopupMenu +import androidx.core.content.ContextCompat +import androidx.core.content.edit +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.fragment.app.FragmentTransaction +import androidx.fragment.app.commit +import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback +import com.google.android.material.bottomsheet.BottomSheetBehavior.from +import com.google.zxing.BarcodeFormat +import com.google.zxing.ResultPoint +import com.google.zxing.client.android.BeepManager +import com.journeyapps.barcodescanner.BarcodeCallback +import com.journeyapps.barcodescanner.BarcodeResult +import com.journeyapps.barcodescanner.DefaultDecoderFactory +import com.mikepenz.iconics.IconicsColor +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.IconicsSize +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import io.reactivex.Completable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import openfoodfacts.github.scrachx.openfood.AppFlavors.OFF +import openfoodfacts.github.scrachx.openfood.AppFlavors.isFlavors +import openfoodfacts.github.scrachx.openfood.R +import openfoodfacts.github.scrachx.openfood.app.OFFApplication +import openfoodfacts.github.scrachx.openfood.databinding.ActivityContinuousScanBinding +import openfoodfacts.github.scrachx.openfood.features.ImagesManageActivity +import openfoodfacts.github.scrachx.openfood.features.compare.ProductCompareActivity +import openfoodfacts.github.scrachx.openfood.features.listeners.CommonBottomListenerInstaller.installBottomNavigation +import openfoodfacts.github.scrachx.openfood.features.listeners.CommonBottomListenerInstaller.selectNavigationItem +import openfoodfacts.github.scrachx.openfood.features.product.edit.ProductEditActivity +import openfoodfacts.github.scrachx.openfood.features.product.view.ProductViewActivity.ShowIngredientsAction +import openfoodfacts.github.scrachx.openfood.features.product.view.ProductViewFragment +import openfoodfacts.github.scrachx.openfood.features.product.view.ingredients_analysis.IngredientsWithTagDialogFragment +import openfoodfacts.github.scrachx.openfood.features.product.view.summary.AbstractSummaryProductPresenter +import openfoodfacts.github.scrachx.openfood.features.product.view.summary.IngredientAnalysisTagsAdapter +import openfoodfacts.github.scrachx.openfood.features.product.view.summary.SummaryProductPresenter +import openfoodfacts.github.scrachx.openfood.models.InvalidBarcodeDao +import openfoodfacts.github.scrachx.openfood.models.Product +import openfoodfacts.github.scrachx.openfood.models.entities.OfflineSavedProduct +import openfoodfacts.github.scrachx.openfood.models.entities.OfflineSavedProductDao +import openfoodfacts.github.scrachx.openfood.models.entities.allergen.AllergenHelper +import openfoodfacts.github.scrachx.openfood.models.entities.allergen.AllergenName +import openfoodfacts.github.scrachx.openfood.models.entities.analysistagconfig.AnalysisTagConfig +import openfoodfacts.github.scrachx.openfood.models.eventbus.ProductNeedsRefreshEvent +import openfoodfacts.github.scrachx.openfood.network.ApiFields +import openfoodfacts.github.scrachx.openfood.network.OpenFoodAPIClient +import openfoodfacts.github.scrachx.openfood.utils.* +import openfoodfacts.github.scrachx.openfood.utils.Utils.daoSession +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import java.io.IOException +import java.util.* +import java.util.concurrent.TimeUnit + +class ContinuousScanActivity : AppCompatActivity() { + + private var _binding: ActivityContinuousScanBinding? = null + private val binding get() = _binding!! + + private lateinit var beepManager: BeepManager + private lateinit var quickViewBehavior: BottomSheetBehavior + private lateinit var bottomSheetCallback: BottomSheetCallback + private lateinit var errorDrawable: VectorDrawableCompat + + private val barcodeInputListener = BarcodeInputListener() + private val barcodeScanCallback = BarcodeScannerCallback() + + private val client by lazy { OpenFoodAPIClient(this@ContinuousScanActivity) } + private val cameraPref by lazy { getSharedPreferences("camera", 0) } + + private val commonDisp = CompositeDisposable() + private var productDisp: Disposable? = null + private var hintBarcodeDisp: Disposable? = null + + private var peekLarge = 0 + private var peekSmall = 0 + + private var cameraState = 0 + private var autoFocusActive = false + private var flashActive = false + private var analysisTagsEmpty = true + private var productShowing = false + private var beepActive = false + + private var offlineSavedProduct: OfflineSavedProduct? = null + private var product: Product? = null + private var lastBarcode: String? = null + private var productViewFragment: ProductViewFragment? = null + + private var popupMenu: PopupMenu? = null + private var summaryProductPresenter: SummaryProductPresenter? = null + + private val productActivityResultLauncher = registerForActivityResult(ProductEditActivity.EditProductContract()) + { result -> if (result) lastBarcode?.let { setShownProduct(it) } } + + /** + * Used by screenshot tests. + * + * @param barcode barcode to serach + */ + @Suppress("unused") + internal fun showProduct(barcode: String) { + productShowing = true + binding.barcodeScanner.visibility = View.GONE + binding.barcodeScanner.pause() + binding.imageForScreenshotGenerationOnly.visibility = View.VISIBLE + setShownProduct(barcode) + } + + /** + * Makes network call and search for the product in the database + * + * @param barcode Barcode to be searched + */ + private fun setShownProduct(barcode: String) { + if (isFinishing) return + + // Dispose the previous call if not ended. + productDisp?.dispose() + summaryProductPresenter?.dispose() + + // First, try to show if we have an offline saved product in the db + offlineSavedProduct = OfflineProductService.getOfflineProductByBarcode(barcode).also { product -> + product?.let { showOfflineSavedDetails(it) } + } + + // Then query the online db + productDisp = client.getProductStateFull(barcode, Utils.HEADER_USER_AGENT_SCAN) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSubscribe { + hideAllViews() + quickViewBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + binding.quickView.setOnClickListener(null) + binding.quickViewProgress.visibility = View.VISIBLE + binding.quickViewProgressText.visibility = View.VISIBLE + binding.quickViewProgressText.text = getString(R.string.loading_product, barcode) + } + .doOnError { + try { + // A network error happened + if (it is IOException) { + hideAllViews() + val offlineSavedProduct = daoSession.offlineSavedProductDao!!.queryBuilder() + .where(OfflineSavedProductDao.Properties.Barcode.eq(barcode)) + .unique() + tryDisplayOffline(offlineSavedProduct, barcode, R.string.addProductOffline) + binding.quickView.setOnClickListener { navigateToProductAddition(barcode) } + } else { + binding.quickViewProgress.visibility = View.GONE + binding.quickViewProgressText.visibility = View.GONE + Toast.makeText(this, R.string.txtConnectionError, Toast.LENGTH_LONG).run { + setGravity(CENTER, 0, 0) + show() + } + Log.i(LOG_TAG, it.message, it) + } + } catch (err: Exception) { + Log.w(LOG_TAG, err.message, err) + } + } + .subscribe { productState -> + //clear product tags + analysisTagsEmpty = true + binding.quickViewTags.adapter = null + binding.quickViewProgress.visibility = View.GONE + binding.quickViewProgressText.visibility = View.GONE + if (productState.status == 0L) { + tryDisplayOffline(offlineSavedProduct, barcode, R.string.product_not_found) + } else { + val product = productState.product!! + this.product = product + + // If we're here from comparison -> add product, return to comparison activity + if (intent.getBooleanExtra(ProductCompareActivity.KEY_COMPARE_PRODUCT, false)) { + startActivity(Intent(this@ContinuousScanActivity, ProductCompareActivity::class.java).apply { + putExtra(ProductCompareActivity.KEY_PRODUCT_FOUND, true) + + val productsToCompare = intent.extras!!.getSerializable(ProductCompareActivity.KEY_PRODUCTS_TO_COMPARE) as ArrayList + if (productsToCompare.contains(product)) { + putExtra(ProductCompareActivity.KEY_PRODUCT_ALREADY_EXISTS, true) + } else { + productsToCompare.add(product) + } + putExtra(ProductCompareActivity.KEY_PRODUCTS_TO_COMPARE, productsToCompare) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + }) + } + + // Add product to scan history + productDisp = client.addToHistory(product).subscribeOn(Schedulers.io()).subscribe() + showAllViews() + binding.txtProductCallToAction.let { + it.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0) + it.background = ContextCompat.getDrawable(this, R.drawable.rounded_quick_view_text) + it.setText(if (isProductIncomplete()) R.string.product_not_complete else R.string.scan_tooltip) + it.visibility = View.VISIBLE + } + + setupSummary(product) + quickViewBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + + showProductFullScreen() + binding.quickViewProductNotFound.visibility = View.GONE + binding.quickViewProductNotFoundButton.visibility = View.GONE + + // Set product name, prefer offline + if (offlineSavedProduct != null && !offlineSavedProduct?.name.isNullOrEmpty()) { + binding.quickViewName.text = offlineSavedProduct!!.name + } else if (product.productName == null || product.productName == "") { + binding.quickViewName.setText(R.string.productNameNull) + } else { + binding.quickViewName.text = product.productName + } + + // Set product additives + val addTags = product.additivesTags + binding.quickViewAdditives.text = when { + addTags.isNotEmpty() -> resources.getQuantityString(R.plurals.productAdditives, addTags.size, addTags.size) + product.statesTags.contains(ApiFields.StateTags.INGREDIENTS_COMPLETED) -> getString(R.string.productAdditivesNone) + else -> getString(R.string.productAdditivesUnknown) + } + + // Show nutriscore in quickView only if app flavour is OFF and the product has one + quickViewCheckNutriScore(product) + + // Show nova group in quickView only if app flavour is OFF and the product has one + quickViewCheckNova(product) + + // If the product has an ecoscore, show it instead of the CO2 icon + quickViewCheckEcoScore(product) + + // Create the product view fragment and add it to the layout + val newProductViewFragment = ProductViewFragment.newInstance(productState) + supportFragmentManager.commit { + replace(R.id.frame_layout, newProductViewFragment) + setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) + } + productViewFragment = newProductViewFragment + } + } + } + + private fun quickViewCheckNutriScore(product: Product) = if (isFlavors(OFF)) { + binding.quickViewNutriScore.visibility = View.VISIBLE + binding.quickViewNutriScore.setImageResource(product.getNutriScoreResource()) + } else { + binding.quickViewNutriScore.visibility = View.GONE + } + + private fun quickViewCheckNova(product: Product) = if (isFlavors(OFF)) { + binding.quickViewNovaGroup.visibility = View.VISIBLE + binding.quickViewAdditives.visibility = View.VISIBLE + binding.quickViewNovaGroup.setImageResource(product.getNovaGroupResource()) + } else { + binding.quickViewNovaGroup.visibility = View.GONE + } + + private fun quickViewCheckEcoScore(product: Product) = if (isFlavors(OFF)) { + binding.quickViewEcoscoreIcon.setImageResource(product.getEcoscoreResource()) + binding.quickViewEcoscoreIcon.visibility = View.VISIBLE + } else { + binding.quickViewEcoscoreIcon.visibility = View.GONE + } + + private fun tryDisplayOffline( + offlineSavedProduct: OfflineSavedProduct?, + barcode: String, + @StringRes errorMsg: Int + ) = if (offlineSavedProduct != null) showOfflineSavedDetails(offlineSavedProduct) + else showProductNotFound(getString(errorMsg, barcode)) + + private fun setupSummary(product: Product) { + binding.callToActionImageProgress.visibility = View.VISIBLE + + summaryProductPresenter = SummaryProductPresenter(product, object : AbstractSummaryProductPresenter() { + override fun showAllergens(allergens: List) { + val data = AllergenHelper.computeUserAllergen(product, allergens) + binding.callToActionImageProgress.visibility = View.GONE + if (data.isEmpty()) return + val iconicsDrawable = IconicsDrawable(this@ContinuousScanActivity, GoogleMaterial.Icon.gmd_warning) + .color(IconicsColor.colorInt(ContextCompat.getColor(this@ContinuousScanActivity, R.color.white))) + .size(IconicsSize.dp(24)) + binding.txtProductCallToAction.setCompoundDrawablesWithIntrinsicBounds(iconicsDrawable, null, null, null) + binding.txtProductCallToAction.background = ContextCompat.getDrawable(this@ContinuousScanActivity, R.drawable.rounded_quick_view_text_warn) + binding.txtProductCallToAction.text = if (data.incomplete) { + getString(R.string.product_incomplete_message) + } else { + "${getString(R.string.product_allergen_prompt)}\n${data.allergens.joinToString(", ")}" + } + } + + override fun showAnalysisTags(analysisTags: List) { + super.showAnalysisTags(analysisTags) + if (analysisTags.isEmpty()) { + binding.quickViewTags.visibility = View.GONE + analysisTagsEmpty = true + return + } + binding.quickViewTags.visibility = View.VISIBLE + analysisTagsEmpty = false + val adapter = IngredientAnalysisTagsAdapter(this@ContinuousScanActivity, analysisTags) + adapter.setOnItemClickListener { view: View?, _ -> + if (view == null) return@setOnItemClickListener + IngredientsWithTagDialogFragment.newInstance(product, view.getTag(R.id.analysis_tag_config) as AnalysisTagConfig).run { + show(supportFragmentManager, "fragment_ingredients_with_tag") + onDismissListener = { adapter.filterVisibleTags() } + } + } + + binding.quickViewTags.adapter = adapter + } + }).also { + it.loadAllergens { binding.callToActionImageProgress.visibility = View.GONE } + it.loadAnalysisTags() + } + } + + private fun showProductNotFound(text: String) { + hideAllViews() + quickViewBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + binding.quickView.setOnClickListener { lastBarcode?.let { navigateToProductAddition(it) } } + binding.quickViewProductNotFound.text = text + binding.quickViewProductNotFound.visibility = View.VISIBLE + binding.quickViewProductNotFoundButton.visibility = View.VISIBLE + binding.quickViewProductNotFoundButton.setOnClickListener { lastBarcode?.let { navigateToProductAddition(it) } } + } + + private fun showProductFullScreen() { + quickViewBehavior.peekHeight = peekLarge + binding.quickView.let { + it.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + it.requestLayout() + it.rootView.requestLayout() + } + } + + private fun showOfflineSavedDetails(offlineSavedProduct: OfflineSavedProduct) { + showAllViews() + val pName = offlineSavedProduct.name + binding.quickViewName.text = if (!pName.isNullOrEmpty()) pName else getString(R.string.productNameNull) + binding.txtProductCallToAction.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0) + binding.txtProductCallToAction.background = ContextCompat.getDrawable(this@ContinuousScanActivity, R.drawable.rounded_quick_view_text) + binding.txtProductCallToAction.setText(R.string.product_not_complete) + binding.txtProductCallToAction.visibility = View.VISIBLE + binding.quickViewSlideUpIndicator.visibility = View.GONE + quickViewBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + } + + private fun navigateToProductAddition(productBarcode: String) { + navigateToProductAddition(Product().apply { + code = productBarcode + lang = LocaleHelper.getLanguage(this@ContinuousScanActivity) + }) + } + + private fun navigateToProductAddition(product: Product?) { + productActivityResultLauncher.launch(product) + } + + private fun showAllViews() { + binding.quickViewSlideUpIndicator.visibility = View.VISIBLE + binding.quickViewName.visibility = View.VISIBLE + binding.frameLayout.visibility = View.VISIBLE + binding.quickViewAdditives.visibility = View.VISIBLE + if (!analysisTagsEmpty) { + binding.quickViewTags.visibility = View.VISIBLE + } else { + binding.quickViewTags.visibility = View.GONE + } + } + + private fun hideAllViews() { + binding.quickViewSearchByBarcode.visibility = View.GONE + binding.quickViewProgress.visibility = View.GONE + binding.quickViewProgressText.visibility = View.GONE + binding.quickViewSlideUpIndicator.visibility = View.GONE + binding.quickViewName.visibility = View.GONE + binding.frameLayout.visibility = View.GONE + binding.quickViewAdditives.visibility = View.GONE + + binding.quickViewNutriScore.visibility = View.GONE + binding.quickViewNovaGroup.visibility = View.GONE + binding.quickViewCo2Icon.visibility = View.GONE + binding.quickViewEcoscoreIcon.visibility = View.GONE + + binding.quickViewProductNotFound.visibility = View.GONE + binding.quickViewProductNotFoundButton.visibility = View.GONE + binding.txtProductCallToAction.visibility = View.GONE + binding.quickViewTags.visibility = View.GONE + } + + + override fun onCreate(savedInstanceState: Bundle?) { + OFFApplication.appComponent.inject(this) + super.onCreate(savedInstanceState) + _binding = ActivityContinuousScanBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.toggleFlash.setOnClickListener { toggleFlash() } + binding.buttonMore.setOnClickListener { showMoreSettings() } + + actionBar?.hide() + + peekLarge = resources.getDimensionPixelSize(R.dimen.scan_summary_peek_large) + peekSmall = resources.getDimensionPixelSize(R.dimen.scan_summary_peek_small) + + errorDrawable = VectorDrawableCompat.create(resources, R.drawable.ic_product_silhouette, null) + ?: error("Could not create vector drawable.") + + binding.quickViewTags.isNestedScrollingEnabled = false + + + // The system bars are visible. + hideSystemUI() + + hintBarcodeDisp = Completable.timer(15, TimeUnit.SECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .doOnComplete { + if (productShowing) return@doOnComplete + + hideAllViews() + quickViewBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + binding.quickViewSearchByBarcode.visibility = View.VISIBLE + binding.quickViewSearchByBarcode.requestFocus() + }.subscribe() + + quickViewBehavior = from(binding.quickView) + + // Initial state + quickViewBehavior.state = BottomSheetBehavior.STATE_HIDDEN + + bottomSheetCallback = QuickViewCallback() + quickViewBehavior.addBottomSheetCallback(bottomSheetCallback) + cameraPref.let { + beepActive = it.getBoolean(SETTING_RING, false) + flashActive = it.getBoolean(SETTING_FLASH, false) + autoFocusActive = it.getBoolean(SETTING_FOCUS, true) + cameraState = it.getInt(SETTING_STATE, 0) + } + + // Setup barcode scanner + binding.barcodeScanner.barcodeView.decoderFactory = DefaultDecoderFactory(BARCODE_FORMATS) + binding.barcodeScanner.setStatusText(null) + binding.barcodeScanner.setOnClickListener { + quickViewBehavior.state = BottomSheetBehavior.STATE_HIDDEN + binding.barcodeScanner.resume() + } + binding.barcodeScanner.barcodeView.cameraSettings.run { + requestedCameraId = cameraState + isAutoFocusEnabled = autoFocusActive + } + + // Setup popup menu + setupPopupMenu() + + // Start continuous scanner + binding.barcodeScanner.decodeContinuous(barcodeScanCallback) + beepManager = BeepManager(this) + binding.quickViewSearchByBarcode.setOnEditorActionListener(barcodeInputListener) + binding.bottomNavigation.bottomNavigation.installBottomNavigation(this) + } + + override fun onStart() { + super.onStart() + EventBus.getDefault().register(this) + } + + override fun onResume() { + super.onResume() + binding.bottomNavigation.bottomNavigation.selectNavigationItem(R.id.scan_bottom_nav) + if (quickViewBehavior.state != BottomSheetBehavior.STATE_EXPANDED) { + binding.barcodeScanner.resume() + } + } + + override fun onSaveInstanceState(outState: Bundle) { + productDisp?.dispose() + super.onSaveInstanceState(outState) + } + + override fun onPause() { + binding.barcodeScanner.pause() + super.onPause() + } + + override fun onStop() { + EventBus.getDefault().unregister(this) + super.onStop() + } + + override fun onDestroy() { + summaryProductPresenter?.dispose() + + // Dispose all RxJava disposable + hintBarcodeDisp?.dispose() + commonDisp.dispose() + + // Remove bottom sheet callback as it uses binding + quickViewBehavior.removeBottomSheetCallback(bottomSheetCallback) + _binding = null + super.onDestroy() + } + + + @Subscribe + fun onEventBusProductNeedsRefreshEvent(event: ProductNeedsRefreshEvent) { + if (event.barcode == lastBarcode) { + runOnUiThread { setShownProduct(event.barcode) } + } + } + + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + //status bar will remain visible if user presses home and then reopens the activity + // hence hiding status bar again + hideSystemUI() + } + + private fun hideSystemUI() { + WindowInsetsControllerCompat(window, binding.root).hide(WindowInsetsCompat.Type.statusBars()) + this.actionBar?.hide() + } + + + private fun setupPopupMenu() { + popupMenu = PopupMenu(this, binding.buttonMore).also { + it.menuInflater.inflate(R.menu.popup_menu, it.menu) + if (flashActive) { + binding.barcodeScanner.setTorchOn() + binding.toggleFlash.setImageResource(R.drawable.ic_flash_on_white_24dp) + } + if (beepActive) { + it.menu.findItem(R.id.toggleBeep).isChecked = true + } + if (autoFocusActive) { + it.menu.findItem(R.id.toggleAutofocus).isChecked = true + } + } + + } + + override fun attachBaseContext(newBase: Context) = super.attachBaseContext(LocaleHelper.onCreate(newBase)) + + private fun isProductIncomplete() = if (product == null) { + false + } else product!!.imageFrontUrl == null + || product!!.imageFrontUrl == "" + || product!!.quantity == null + || product!!.quantity == "" + || product!!.productName == null + || product!!.productName == "" + || product!!.brands == null + || product!!.brands == "" + || product!!.ingredientsText == null + || product!!.ingredientsText == "" + + private fun toggleCamera() { + val settings = binding.barcodeScanner.barcodeView.cameraSettings + if (binding.barcodeScanner.barcodeView.isPreviewActive) { + binding.barcodeScanner.pause() + } + cameraState = if (settings.requestedCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) { + Camera.CameraInfo.CAMERA_FACING_FRONT + } else { + Camera.CameraInfo.CAMERA_FACING_BACK + } + settings.requestedCameraId = cameraState + binding.barcodeScanner.barcodeView.cameraSettings = settings + cameraPref.edit { putInt(SETTING_STATE, cameraState) } + binding.barcodeScanner.resume() + } + + private fun toggleFlash() { + cameraPref.edit { + if (flashActive) { + binding.barcodeScanner.setTorchOff() + flashActive = false + binding.toggleFlash.setImageResource(R.drawable.ic_flash_off_white_24dp) + putBoolean(SETTING_FLASH, false) + } else { + binding.barcodeScanner.setTorchOn() + flashActive = true + binding.toggleFlash.setImageResource(R.drawable.ic_flash_on_white_24dp) + putBoolean(SETTING_FLASH, true) + } + } + } + + private fun showMoreSettings() { + popupMenu?.let { + it.setOnMenuItemClickListener { item: MenuItem -> + when (item.itemId) { + R.id.toggleBeep -> { + beepActive = !beepActive + item.isChecked = beepActive + cameraPref.edit { + putBoolean(SETTING_RING, beepActive) + apply() + } + } + R.id.toggleAutofocus -> { + if (binding.barcodeScanner.barcodeView.isPreviewActive) { + binding.barcodeScanner.pause() + } + val settings = binding.barcodeScanner.barcodeView.cameraSettings + autoFocusActive = !autoFocusActive + settings.isAutoFocusEnabled = autoFocusActive + item.isChecked = autoFocusActive + + cameraPref.edit { putBoolean(SETTING_FOCUS, autoFocusActive) } + + binding.barcodeScanner.resume() + binding.barcodeScanner.barcodeView.cameraSettings = settings + } + R.id.troubleScanning -> { + hideAllViews() + hintBarcodeDisp!!.dispose() + binding.quickView.setOnClickListener(null) + binding.quickViewSearchByBarcode.text = null + binding.quickViewSearchByBarcode.visibility = View.VISIBLE + quickViewBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + binding.quickViewSearchByBarcode.requestFocus() + } + R.id.toggleCamera -> toggleCamera() + } + true + } + it.show() + } + } + + /** + * Overridden to collapse bottom view after a back action from edit form. + */ + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + quickViewBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == ImagesManageActivity.REQUEST_EDIT_IMAGE && (resultCode == RESULT_OK || resultCode == RESULT_CANCELED)) { + lastBarcode?.let { setShownProduct(it) } + } else if (resultCode == RESULT_OK && requestCode == LOGIN_ACTIVITY_REQUEST_CODE) { + navigateToProductAddition(product) + } + } + + fun collapseBottomSheet() { + quickViewBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + + fun showIngredientsTab(action: ShowIngredientsAction) { + quickViewBehavior.state = BottomSheetBehavior.STATE_EXPANDED + productViewFragment?.showIngredientsTab(action) + } + + private inner class QuickViewCallback : BottomSheetCallback() { + private var previousSlideOffset = 0f + override fun onStateChanged(bottomSheet: View, newState: Int) { + when (newState) { + BottomSheetBehavior.STATE_HIDDEN -> { + lastBarcode = null + binding.txtProductCallToAction.visibility = View.GONE + } + else -> { + binding.barcodeScanner.pause() + } + } + + + if (binding.quickViewSearchByBarcode.visibility == View.VISIBLE) { + quickViewBehavior.peekHeight = peekSmall + bottomSheet.layoutParams.height = quickViewBehavior.peekHeight + } else { + quickViewBehavior.peekHeight = peekLarge + bottomSheet.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + } + bottomSheet.requestLayout() + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) { + val slideDelta = slideOffset - previousSlideOffset + if (binding.quickViewSearchByBarcode.visibility != View.VISIBLE && binding.quickViewProgress.visibility != View.VISIBLE) { + if (slideOffset > 0.01f || slideOffset < -0.01f) { + binding.txtProductCallToAction.visibility = View.GONE + } else if (binding.quickViewProductNotFound.visibility != View.VISIBLE) { + binding.txtProductCallToAction.visibility = View.VISIBLE + } + if (slideOffset > 0.01f) { + binding.quickViewDetails.visibility = View.GONE + binding.quickViewTags.visibility = View.GONE + binding.barcodeScanner.pause() + if (slideDelta > 0 && productViewFragment != null) { + productViewFragment!!.bottomSheetWillGrow() + binding.bottomNavigation.bottomNavigation.visibility = View.GONE + } + } else { + binding.barcodeScanner.resume() + binding.quickViewDetails.visibility = View.VISIBLE + binding.quickViewTags.visibility = if (analysisTagsEmpty) View.GONE else View.VISIBLE + binding.bottomNavigation.bottomNavigation.visibility = View.VISIBLE + if (binding.quickViewProductNotFound.visibility != View.VISIBLE) { + binding.txtProductCallToAction.visibility = View.VISIBLE + } + } + } + previousSlideOffset = slideOffset + } + } + + private inner class BarcodeInputListener : OnEditorActionListener { + override fun onEditorAction(textView: TextView, actionId: Int, event: KeyEvent?): Boolean { + // When user search from "having trouble" edit text + if (actionId != EditorInfo.IME_ACTION_SEARCH) return false + + Utils.hideKeyboard(this@ContinuousScanActivity) + hideSystemUI() + + // Check for barcode validity + val barcodeText = textView.text.toString() + // For debug only: the barcode 1 is used for test + if (barcodeText.isEmpty() || (barcodeText.length <= 2 && ApiFields.Defaults.DEBUG_BARCODE != barcodeText) || !isBarcodeValid(barcodeText)) { + textView.requestFocus() + textView.error = getString(R.string.txtBarcodeNotValid) + return true + } + lastBarcode = barcodeText + textView.visibility = View.GONE + setShownProduct(barcodeText) + return true + } + } + + private inner class BarcodeScannerCallback : BarcodeCallback { + override fun barcodeResult(result: BarcodeResult) { + hintBarcodeDisp?.dispose() + + // Prevent duplicate scans + if (result.text == null || result.text.isEmpty() || result.text == lastBarcode) return + + val invalidBarcode = daoSession.invalidBarcodeDao.queryBuilder() + .where(InvalidBarcodeDao.Properties.Barcode.eq(result.text)) + .unique() + // Scanned barcode is in the list of invalid barcodes, do nothing + if (invalidBarcode != null) return + + if (beepActive) { + beepManager.playBeepSound() + } + lastBarcode = result.text.also { if (!isFinishing) setShownProduct(it) } + + } + + // Here possible results are useless but we must implement this + override fun possibleResultPoints(resultPoints: List) = Unit + } + + companion object { + val showSelectScannerPref = false + + private const val LOGIN_ACTIVITY_REQUEST_CODE = 2 + val BARCODE_FORMATS = listOf( + BarcodeFormat.UPC_A, + BarcodeFormat.UPC_E, + BarcodeFormat.EAN_13, + BarcodeFormat.EAN_8, + BarcodeFormat.RSS_14, + BarcodeFormat.CODE_39, + BarcodeFormat.CODE_93, + BarcodeFormat.CODE_128, + BarcodeFormat.ITF + ) + private const val SETTING_RING = "ring" + private const val SETTING_FLASH = "flash" + private const val SETTING_FOCUS = "focus" + private const val SETTING_STATE = "cameraState" + private val LOG_TAG = this::class.simpleName!! + } +} diff --git a/app/src/fdroid/res/layout/activity_continuous_scan.xml b/app/src/fdroid/res/layout/activity_continuous_scan.xml new file mode 100644 index 000000000000..cfaf07573b90 --- /dev/null +++ b/app/src/fdroid/res/layout/activity_continuous_scan.xml @@ -0,0 +1,345 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +