Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add simple scan feature #4236

Merged
merged 6 commits into from Sep 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.