Skip to content

Commit

Permalink
feat: Add simple scan feature (#4236)
Browse files Browse the repository at this point in the history
* Add repository to work with camera prefs

* Add SimpleScan feature

* fix ml camera

* Add tests for SimpleScanViewModel

* Add string to obf

* fix build
  • Loading branch information
naivekook committed Sep 28, 2021
1 parent fb8b86c commit dd59827
Show file tree
Hide file tree
Showing 22 changed files with 852 additions and 141 deletions.
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Expand Up @@ -305,6 +305,11 @@
android:name=".features.scan.ContinuousScanActivity"
android:screenOrientation="portrait"
android:theme="@style/OFFTheme.NoActionBar" />
<activity
android:name=".features.simplescan.SimpleScanActivity"
android:screenOrientation="portrait"
android:theme="@style/OFFTheme.NoActionBar"
tools:ignore="LockedOrientationActivity" />
<activity
android:name=".features.product.edit.ProductEditActivity"
android:screenOrientation="portrait"
Expand Down
Expand Up @@ -6,25 +6,27 @@ import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Bundle
import android.widget.Toast
import androidx.activity.result.launch
import androidx.activity.viewModels
import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
import androidx.core.content.ContextCompat.checkSelfPermission
import androidx.lifecycle.Lifecycle
import androidx.core.view.isVisible
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.squareup.picasso.Picasso
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx2.await
import openfoodfacts.github.scrachx.openfood.R
import openfoodfacts.github.scrachx.openfood.databinding.ActivityProductComparisonBinding
import openfoodfacts.github.scrachx.openfood.features.compare.ProductCompareViewModel.SideEffect
import openfoodfacts.github.scrachx.openfood.features.shared.BaseActivity
import openfoodfacts.github.scrachx.openfood.features.simplescan.SimpleScanActivityContract
import openfoodfacts.github.scrachx.openfood.images.ProductImage
import openfoodfacts.github.scrachx.openfood.listeners.CommonBottomListenerInstaller.installBottomNavigation
import openfoodfacts.github.scrachx.openfood.listeners.CommonBottomListenerInstaller.selectNavigationItem
Expand All @@ -48,8 +50,8 @@ class ProductCompareActivity : BaseActivity() {

private val viewModel: ProductCompareViewModel by viewModels()

private val scanProductContract = registerForActivityResult(ScanProductActivityContract()) { product ->
product?.let { viewModel.addProductToCompare(it) }
private val scanProductContract = registerForActivityResult(SimpleScanActivityContract()) { barcode ->
barcode?.let { viewModel.barcodeDetected(it) }
}

override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -67,18 +69,31 @@ class ProductCompareActivity : BaseActivity() {
}

lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.alreadyExistFlow.collect {
Toast.makeText(this@ProductCompareActivity, getString(R.string.product_already_exists_in_comparison), Toast.LENGTH_SHORT).show()
viewModel.sideEffectFlow
.flowWithLifecycle(lifecycle)
.collect {
when (it) {
is SideEffect.ProductAlreadyAdded -> showProductAlreadyAddedDialog()
is SideEffect.ProductNotFound -> showProductNotFoundDialog()
is SideEffect.ConnectionError -> showConnectionErrorDialog()
}
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.productsFlow.collect { products ->
viewModel.productsFlow
.flowWithLifecycle(lifecycle)
.distinctUntilChanged()
.collect { products ->
createAdapter(products)
}
}
}
lifecycleScope.launch {
viewModel.loadingVisibleFlow
.flowWithLifecycle(lifecycle)
.distinctUntilChanged()
.collect {
binding.comparisonProgressView.isVisible = it
}
}

binding.productComparisonButton.setOnClickListener {
Expand Down Expand Up @@ -161,9 +176,36 @@ class ProductCompareActivity : BaseActivity() {
}
}

private fun showProductAlreadyAddedDialog() {
MaterialAlertDialogBuilder(this)
.setMessage(R.string.product_already_exists_in_comparison)
.setPositiveButton(R.string.ok_button) { dialog, _ ->
dialog.dismiss()
}
.show()
}

private fun showProductNotFoundDialog() {
MaterialAlertDialogBuilder(this)
.setMessage(R.string.txtDialogsContentPowerMode)
.setPositiveButton(R.string.ok_button) { dialog, _ ->
dialog.dismiss()
}
.show()
}

private fun showConnectionErrorDialog() {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.alert_dialog_warning_title)
.setMessage(R.string.txtConnectionError)
.setPositiveButton(R.string.ok_button) { dialog, _ ->
dialog.dismiss()
}
.show()
}

companion object {
const val KEY_PRODUCTS_TO_COMPARE = "products_to_compare"
const val KEY_COMPARE_PRODUCT = "compare_product"

@JvmStatic
fun start(context: Context, product: Product) {
Expand Down
@@ -1,5 +1,6 @@
package openfoodfacts.github.scrachx.openfood.features.compare

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
Expand All @@ -14,37 +15,55 @@ import openfoodfacts.github.scrachx.openfood.analytics.AnalyticsEvent
import openfoodfacts.github.scrachx.openfood.analytics.MatomoAnalytics
import openfoodfacts.github.scrachx.openfood.models.Product
import openfoodfacts.github.scrachx.openfood.models.entities.additive.AdditiveName
import openfoodfacts.github.scrachx.openfood.network.OpenFoodAPIClient
import openfoodfacts.github.scrachx.openfood.repositories.ProductRepository
import openfoodfacts.github.scrachx.openfood.utils.CoroutineDispatchers
import openfoodfacts.github.scrachx.openfood.utils.LocaleManager
import openfoodfacts.github.scrachx.openfood.utils.Utils
import javax.inject.Inject

@HiltViewModel
class ProductCompareViewModel @Inject constructor(
private val productRepository: ProductRepository,
private val localeManager: LocaleManager,
private val matomoAnalytics: MatomoAnalytics,
private val coroutineDispatchers: CoroutineDispatchers
private val coroutineDispatchers: CoroutineDispatchers,
private val openFoodAPIClient: OpenFoodAPIClient,
) : ViewModel() {

private val _alreadyExistFlow = MutableSharedFlow<Unit>()
val alreadyExistFlow = _alreadyExistFlow.asSharedFlow()
private val _sideEffectFlow = MutableSharedFlow<SideEffect>()
val sideEffectFlow = _sideEffectFlow.asSharedFlow()

private val _productsFlow = MutableStateFlow<List<CompareProduct>>(emptyList())
private val _productsFlow = MutableStateFlow(listOf<CompareProduct>())
val productsFlow = _productsFlow.asStateFlow()

private val _loadingVisibleFlow = MutableStateFlow(false)
val loadingVisibleFlow = _loadingVisibleFlow.asStateFlow()

fun barcodeDetected(barcode: String) {
viewModelScope.launch {
if (isProductAlreadyAdded(barcode)) {
emitSideEffect(SideEffect.ProductAlreadyAdded)
} else {
fetchProduct(barcode)
}
}
}

fun addProductToCompare(product: Product) {
viewModelScope.launch {
if (_productsFlow.value.any { it.product.code == product.code }) {
_alreadyExistFlow.emit(Unit)
if (isProductAlreadyAdded(product.code)) {
emitSideEffect(SideEffect.ProductAlreadyAdded)
} else {
_loadingVisibleFlow.emit(true)
matomoAnalytics.trackEvent(AnalyticsEvent.AddProductToComparison(product.code))
val result = withContext(coroutineDispatchers.io()) {
CompareProduct(product, fetchAdditives(product))
}
withContext(coroutineDispatchers.main()) {
updateProductList(result)
}
_loadingVisibleFlow.emit(false)
}
}
}
Expand All @@ -53,6 +72,10 @@ class ProductCompareViewModel @Inject constructor(
return localeManager.getLanguage()
}

private fun isProductAlreadyAdded(barcode: String): Boolean {
return _productsFlow.value.any { it.product.code == barcode }
}

private fun updateProductList(item: CompareProduct) {
val newList = _productsFlow.value + item
_productsFlow.value = newList
Expand All @@ -71,8 +94,38 @@ class ProductCompareViewModel @Inject constructor(
.filter { it.isNotNull }
}

private suspend fun fetchProduct(barcode: String) {
_loadingVisibleFlow.emit(true)
withContext(coroutineDispatchers.io()) {
try {
val product = openFoodAPIClient.getProductStateFull(barcode, userAgent = Utils.HEADER_USER_AGENT_SCAN).product
if (product == null) {
emitSideEffect(SideEffect.ProductNotFound)
} else {
addProductToCompare(product)
}
} catch (t: Throwable) {
Log.w("ProductCompareViewModel", t.message, t)
emitSideEffect(SideEffect.ConnectionError)
}
}
}

private suspend fun emitSideEffect(effect: SideEffect) {
withContext(coroutineDispatchers.main()) {
_sideEffectFlow.emit(effect)
_loadingVisibleFlow.emit(false)
}
}

data class CompareProduct(
val product: Product,
val additiveNames: List<AdditiveName>
)

sealed class SideEffect {
object ProductAlreadyAdded : SideEffect()
object ProductNotFound : SideEffect()
object ConnectionError : SideEffect()
}
}

This file was deleted.

0 comments on commit dd59827

Please sign in to comment.