Skip to content

Commit

Permalink
refactor: use ProductIngredient instead of a generic Map
Browse files Browse the repository at this point in the history
test: ProductIngredient
  • Loading branch information
VaiTon committed Feb 18, 2023
1 parent 1bda5e1 commit 9ec3be4
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,69 @@ import openfoodfacts.github.scrachx.openfood.models.Barcode

sealed class AnalyticsEvent(val category: String, val action: String, val name: String?, val value: Float?) {

object ProductSearch : AnalyticsEvent("search", "completed", null, null)
object ProductSearch :
AnalyticsEvent("search", "completed", null, null)

object ProductSearchStart : AnalyticsEvent("search", "started", null, null)
object ProductSearchStart :
AnalyticsEvent("search", "started", null, null)

data class BarcodeDecoder(val success: Boolean = false, val duration: Float = 0f) : AnalyticsEvent("scanner", "scanning", success.toString(), duration)
data class BarcodeDecoder(val success: Boolean = false, val duration: Float = 0f) :
AnalyticsEvent("scanner", "scanning", success.toString(), duration)

data class ScannedBarcode(val barcode: String) : AnalyticsEvent("scanner", "scanned", barcode, null)
data class ScannedBarcode(val barcode: String) :
AnalyticsEvent("scanner", "scanned", barcode, null)

data class ScannedBarcodeResultExpanded(val barcode: String?) :
AnalyticsEvent("scanner", "result-expanded", barcode, null)

data class AllergenAlertCreated(val allergenTag: String) :
AnalyticsEvent("allergen-alerts", "created", allergenTag, null)

data class ProductCreated(val barcode: String?) : AnalyticsEvent("products", "created", barcode, null)
data class ProductCreated(val barcode: String?) :
AnalyticsEvent("products", "created", barcode, null)

data class ProductEdited(val barcode: String?) : AnalyticsEvent("products", "edited", barcode, null)
data class ProductEdited(val barcode: String?) :
AnalyticsEvent("products", "edited", barcode, null)

data class ProductIngredientsPictureEdited(val barcode: String?) :
AnalyticsEvent("products", "edited-ingredients-picture", barcode, null)

object UserLogin : AnalyticsEvent("user-account", "login", null, null)
object UserLogin :
AnalyticsEvent("user-account", "login", null, null)

object UserLogout : AnalyticsEvent("user-account", "logout", null, null)
object UserLogout :
AnalyticsEvent("user-account", "logout", null, null)

object RobotoffLoginPrompt : AnalyticsEvent("user-account", "login-prompt", "robotoff", null)
object RobotoffLoginPrompt :
AnalyticsEvent("user-account", "login-prompt", "robotoff", null)

object RobotoffLoggedInAfterPrompt : AnalyticsEvent("user-account", "logged-in-after-prompt", "robotoff", null)
object RobotoffLoggedInAfterPrompt :
AnalyticsEvent("user-account", "logged-in-after-prompt", "robotoff", null)

object ShoppingListCreated : AnalyticsEvent("shopping-lists", "created", null, null)
object ShoppingListCreated :
AnalyticsEvent("shopping-lists", "created", null, null)

data class ShoppingListProductAdded(val barcode: String) :
AnalyticsEvent("shopping-lists", "add_product", barcode, null)

data class ShoppingListProductRemoved(val barcode: String) :
AnalyticsEvent("shopping-lists", "remove_product", barcode, null)

object ShoppingListShared : AnalyticsEvent("shopping-lists", "shared", null, null)
object ShoppingListShared :
AnalyticsEvent("shopping-lists", "shared", null, null)

object ShoppingListExported : AnalyticsEvent("shopping-lists", "exported", null, null)
object ShoppingListExported :
AnalyticsEvent("shopping-lists", "exported", null, null)

data class IngredientAnalysisEnabled(val type: String) :
AnalyticsEvent("ingredient-analysis", "enabled", type, null)

data class IngredientAnalysisDisabled(val type: String) :
AnalyticsEvent("ingredient-analysis", "disabled", type, null)

data class AddProductToComparison(val barcode: String) : AnalyticsEvent("products", "compare-add", barcode, null)
data class AddProductToComparison(val barcode: Barcode) :
AnalyticsEvent("products", "compare-add", barcode.raw, null)

data class CompareProducts(val count: Float) : AnalyticsEvent("products", "compare-multiple", null, count)
data class CompareProducts(val count: Float) :
AnalyticsEvent("products", "compare-multiple", null, count)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import openfoodfacts.github.scrachx.openfood.analytics.AnalyticsEvent
import openfoodfacts.github.scrachx.openfood.analytics.MatomoAnalytics
import openfoodfacts.github.scrachx.openfood.models.Barcode
import openfoodfacts.github.scrachx.openfood.models.Product
import openfoodfacts.github.scrachx.openfood.models.entities.additive.AdditiveName
import openfoodfacts.github.scrachx.openfood.repositories.ProductRepository
Expand All @@ -38,7 +39,7 @@ class ProductCompareViewModel @Inject constructor(
private val _loadingVisibleFlow = MutableStateFlow(false)
val loadingVisibleFlow = _loadingVisibleFlow.asStateFlow()

fun barcodeDetected(barcode: String) {
fun barcodeDetected(barcode: Barcode) {
viewModelScope.launch {
if (isProductAlreadyAdded(barcode)) {
emitSideEffect(SideEffect.ProductAlreadyAdded)
Expand All @@ -50,11 +51,11 @@ class ProductCompareViewModel @Inject constructor(

fun addProductToCompare(product: Product) {
viewModelScope.launch(dispatchers.Default) {
if (isProductAlreadyAdded(product.code)) {
if (isProductAlreadyAdded(product.barcode)) {
emitSideEffect(SideEffect.ProductAlreadyAdded)
} else {
_loadingVisibleFlow.emit(true)
matomoAnalytics.trackEvent(AnalyticsEvent.AddProductToComparison(product.code))
matomoAnalytics.trackEvent(AnalyticsEvent.AddProductToComparison(product.barcode))
val result = withContext(dispatchers.IO) {
CompareProduct(product, fetchAdditives(product))
}
Expand All @@ -70,8 +71,8 @@ class ProductCompareViewModel @Inject constructor(
return localeManager.getLanguage()
}

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

private fun updateProductList(item: CompareProduct) {
Expand All @@ -92,7 +93,7 @@ class ProductCompareViewModel @Inject constructor(
.filter { it.isNotNull }
}

private suspend fun fetchProduct(barcode: String) {
private suspend fun fetchProduct(barcode: Barcode) {
_loadingVisibleFlow.emit(true)
withContext(dispatchers.IO) {
try {
Expand All @@ -118,7 +119,7 @@ class ProductCompareViewModel @Inject constructor(

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

sealed class SideEffect {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ class IngredientsAnalysisViewModel @Inject constructor(
this@IngredientsAnalysisViewModel.product.emit(product)
}

val ingredients: Flow<List<ProductIngredient>?> = product
.map(productRepository::getIngredients)
.map(Result<List<ProductIngredient>>::getOrNull)
val ingredients: Flow<List<ProductIngredient>?> = product.map(productRepository::getIngredients)

}
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,10 @@ class IngredientsWithTagDialogFragment : DialogFragment() {
binding.iconFrame.background =
ResourcesCompat.getDrawable(requireActivity().resources, R.drawable.rounded_button, requireActivity().theme)
?.apply {
colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(Color.parseColor(color),
BlendModeCompat.SRC_IN)
colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(
Color.parseColor(color),
BlendModeCompat.SRC_IN
)
}
binding.title.text = name
binding.cb.let {
Expand All @@ -84,33 +86,49 @@ class IngredientsWithTagDialogFragment : DialogFragment() {
}
}
var messageToBeShown =
HtmlCompat.fromHtml(getString(R.string.ingredients_in_this_product_are,
name!!.lowercase(Locale.getDefault())), HtmlCompat.FROM_HTML_MODE_LEGACY)
HtmlCompat.fromHtml(
getString(
R.string.ingredients_in_this_product_are,
name!!.lowercase(Locale.getDefault())
), HtmlCompat.FROM_HTML_MODE_LEGACY
)
val showHelpTranslate = tag != null && tag.contains("unknown")

if (arguments.getBoolean(PHOTOS_TO_BE_VALIDATED_KEY, false)) {
messageToBeShown = HtmlCompat.fromHtml(getString(R.string.unknown_status_missing_ingredients),
HtmlCompat.FROM_HTML_MODE_LEGACY)
messageToBeShown = HtmlCompat.fromHtml(
getString(R.string.unknown_status_missing_ingredients),
HtmlCompat.FROM_HTML_MODE_LEGACY
)
binding.image.setImageResource(R.drawable.ic_add_a_photo_dark_48dp)
binding.image.setOnClickListener { goToAddPhoto() }
binding.helpNeeded.text = HtmlCompat.fromHtml(getString(R.string.add_photo_to_extract_ingredients),
HtmlCompat.FROM_HTML_MODE_LEGACY)
binding.helpNeeded.text = HtmlCompat.fromHtml(
getString(R.string.add_photo_to_extract_ingredients),
HtmlCompat.FROM_HTML_MODE_LEGACY
)
binding.helpNeeded.setOnClickListener { goToAddPhoto() }
} else if (tag != null && ambiguousIngredient != null) {
messageToBeShown =
HtmlCompat.fromHtml(getString(R.string.unknown_status_ambiguous_ingredients, ambiguousIngredient),
HtmlCompat.FROM_HTML_MODE_LEGACY)
HtmlCompat.fromHtml(
getString(R.string.unknown_status_ambiguous_ingredients, ambiguousIngredient),
HtmlCompat.FROM_HTML_MODE_LEGACY
)
binding.helpNeeded.visibility = View.GONE
} else if (showHelpTranslate && arguments.getBoolean(MISSING_INGREDIENTS_KEY, false)) {
picasso.load(ingredientsImageUrl).into(binding.image)

binding.image.setOnClickListener { goToExtract() }
messageToBeShown = HtmlCompat.fromHtml(getString(R.string.unknown_status_missing_ingredients),
HtmlCompat.FROM_HTML_MODE_LEGACY)
messageToBeShown = HtmlCompat.fromHtml(
getString(R.string.unknown_status_missing_ingredients),
HtmlCompat.FROM_HTML_MODE_LEGACY
)

binding.helpNeeded.text =
HtmlCompat.fromHtml(getString(R.string.help_extract_ingredients,
typeName.lowercase(Locale.getDefault())), HtmlCompat.FROM_HTML_MODE_LEGACY)
HtmlCompat.fromHtml(
getString(
R.string.help_extract_ingredients,
typeName.lowercase(Locale.getDefault())
), HtmlCompat.FROM_HTML_MODE_LEGACY
)
binding.helpNeeded.setOnClickListener { goToExtract() }
binding.helpNeeded.visibility = View.VISIBLE

Expand All @@ -137,8 +155,10 @@ class IngredientsWithTagDialogFragment : DialogFragment() {
if (!TextUtils.isEmpty(ingredients)) {
messageToBeShown = HtmlCompat.fromHtml(
"${
getString(R.string.ingredients_in_this_product,
name.lowercase(Locale.getDefault()))
getString(
R.string.ingredients_in_this_product,
name.lowercase(Locale.getDefault())
)
}$ingredients",
HtmlCompat.FROM_HTML_MODE_LEGACY
)
Expand Down Expand Up @@ -211,13 +231,20 @@ class IngredientsWithTagDialogFragment : DialogFragment() {
} else {
val showIngredients = config.name.showIngredients
if (showIngredients != null) {
putSerializable(INGREDIENTS_KEY,
getMatchingIngredientsText(product, showIngredients.split(":").toTypedArray()))
putSerializable(
INGREDIENTS_KEY,
getMatchingIngredientsText(product, showIngredients.split(":").toTypedArray())
)
}
val ambiguousIngredient = product.ingredients
.filter { it.containsKey(config.type) && it.containsValue("maybe") }
.filter { it.containsKey("text") }
.mapNotNull { it["text"] }
.filter {
it.additionalProperties.containsKey(config.type) && it.additionalProperties.containsValue(
"maybe"
)
}
.mapNotNull { it.text }


if (ambiguousIngredient.isNotEmpty()) {
putString(AMBIGUOUS_INGREDIENT_KEY, ambiguousIngredient.joinToString(","))
}
Expand All @@ -231,8 +258,8 @@ class IngredientsWithTagDialogFragment : DialogFragment() {
): String? {

val matchingIngredients = product.ingredients
.filter { ingredients[1] == it[ingredients[0]] }
.mapNotNull { it["text"] as String? }
.filter { ingredients[1] == it.additionalProperties[ingredients[0]] }
.mapNotNull { it.text }
.map { it.lowercase(Locale.getDefault()).replace("_", "") }


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import openfoodfacts.github.scrachx.openfood.features.product.view.ingredients_a
import openfoodfacts.github.scrachx.openfood.models.ProductIngredient

class IngredientAnalysisRecyclerAdapter(
private val productIngredients: List<ProductIngredient>,
private val activity: Activity
private val productIngredients: List<ProductIngredient>,
private val activity: Activity,
) : RecyclerView.Adapter<IngredientAnalysisViewHolder>(), CustomTabActivityHelper.ConnectionCallback {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IngredientAnalysisViewHolder {
Expand All @@ -26,13 +26,18 @@ class IngredientAnalysisRecyclerAdapter(

override fun onBindViewHolder(holder: IngredientAnalysisViewHolder, position: Int) {
val id = productIngredients[position].id.replace("\"", "")
val name = productIngredients[position].text.replace("\"", "") //removes quotations
holder.tvIngredientName.text = name
val name = productIngredients[position].text?.replace("\"", "") //removes quotations
holder.tvIngredientName.text = name ?: id
holder.tvIngredientName.setOnClickListener {
val customTabsIntent = CustomTabsIntent.Builder().build().apply {
intent.putExtra("android.intent.extra.REFERRER", Uri.parse("android-app:https://" + activity.packageName))
}
CustomTabActivityHelper.openCustomTab(activity, customTabsIntent, Uri.parse("${activity.getString(R.string.website)}ingredient/$id"), WebViewFallback())
CustomTabActivityHelper.openCustomTab(
activity,
customTabsIntent,
Uri.parse("${activity.getString(R.string.website)}ingredient/$id"),
WebViewFallback()
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContract
import openfoodfacts.github.scrachx.openfood.models.Barcode
import openfoodfacts.github.scrachx.openfood.utils.Intent

/**
* Returns a string containing the product barcode.
*/
class SimpleScanActivityContract : ActivityResultContract<Unit, String?>() {
class SimpleScanActivityContract : ActivityResultContract<Unit, Barcode?>() {

companion object {
const val KEY_SCANNED_BARCODE = "scanned_barcode"
Expand All @@ -19,11 +20,14 @@ class SimpleScanActivityContract : ActivityResultContract<Unit, String?>() {
return Intent<SimpleScanActivity>(context)
}

override fun parseResult(resultCode: Int, intent: Intent?): String? {
override fun parseResult(resultCode: Int, intent: Intent?): Barcode? {
val bundle = intent?.extras ?: return null
if (resultCode == Activity.RESULT_OK && bundle.containsKey(KEY_SCANNED_BARCODE)) {
return bundle.getString(KEY_SCANNED_BARCODE, null)
}
return null
bundle.getString(KEY_SCANNED_BARCODE, null)


return if (resultCode == Activity.RESULT_OK && bundle.containsKey(KEY_SCANNED_BARCODE)) {
Barcode(bundle.getString(KEY_SCANNED_BARCODE, null))
} else
null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class Product : SearchProduct() {
val imageFrontUrl: String? = null

@JsonProperty(ApiFields.Keys.INGREDIENTS)
val ingredients = arrayListOf<Map<String, Any>>()
val ingredients = arrayListOf<ProductIngredient>()

@JsonProperty(ApiFields.Keys.INGREDIENTS_ANALYSIS_TAGS)
val ingredientsAnalysisTags = arrayListOf<String>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@ import java.io.Serializable

/**
* Represents an ingredient of the product
*
* @param rank The rank, set -1 if no rank returned
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder("text", "id", "rank", "percent")
data class ProductIngredient(
@JsonProperty("text") val text: String,
@JsonProperty("text") val text: String? = null,
@JsonProperty("id") val id: String,
@JsonProperty("rank") val rank: Long = 0,
@JsonProperty("rank") val rank: Long = -1,
@JsonProperty("percent") val percent: String? = null,
@JsonProperty("percent_estimate") val percentEstimate: Float? = null,
@JsonProperty("percent_min") val percentMin: Float? = null,
@JsonProperty("percent_max") val percentMax: Float? = null,
@JsonProperty("vegan") val vegan: String? = null,
@JsonProperty("vegetarian") val vegetarian: String? = null,
@JsonProperty("has_sub_ingredients") val hasSubIngredients: String? = null,
) : Serializable {


Expand Down
Loading

0 comments on commit 9ec3be4

Please sign in to comment.