diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/adapters/CalculatedNutrimentsGridAdapter.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/adapters/CalculatedNutrimentsGridAdapter.kt index ed1b1d5c914d..520bc7869bf7 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/adapters/CalculatedNutrimentsGridAdapter.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/adapters/CalculatedNutrimentsGridAdapter.kt @@ -9,14 +9,13 @@ class CalculatedNutrimentsGridAdapter(private val nutrimentListItems: List + private val nutrimentListItems: List ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NutrimentViewHolder { val isViewTypeHeader = viewType == TYPE_HEADER val layoutResourceId = if (isViewTypeHeader) R.layout.nutriment_item_list_header else R.layout.nutriment_item_list - val v = LayoutInflater.from(parent.context).inflate(layoutResourceId, parent, false) + val view = LayoutInflater.from(parent.context).inflate(layoutResourceId, parent, false) return if (isViewTypeHeader) { - var displayServing = false - for (nutriment in nutrimentListItems) { - val servingValue = nutriment.servingValue - if (servingValue.isBlank()) displayServing = true - } - NutrimentViewHolder.NutrimentHeaderViewHolder(v, displayServing) + val displayServing = nutrimentListItems.any { !it.servingValueStr.isNullOrBlank() } + NutrimentViewHolder.NutrimentHeaderViewHolder(view, displayServing) } else { - NutrimentViewHolder.NutrimentListViewHolder(v) + NutrimentViewHolder.NutrimentListViewHolder(view) } } @@ -40,8 +36,7 @@ open class NutrimentsGridAdapter( } is NutrimentViewHolder.NutrimentListViewHolder -> { val item = nutrimentListItems[position] - holder.fillNutrimentValue(item) - holder.fillServingValue(item) + holder.fillNutriment(item) } } } @@ -69,25 +64,42 @@ open class NutrimentsGridAdapter( private val vNutrimentServingValue: TextView = v.findViewById(R.id.nutriment_serving_value) private val vNutrimentValue: TextView = v.findViewById(R.id.nutriment_value) - fun fillNutrimentValue(item: NutrimentListItem) { + fun fillNutriment(item: NutrimentListItem) { + fillNutrimentName(item) + fillNutrimentValue(item) + fillServingValue(item) + } + + + private fun fillNutrimentName(item: NutrimentListItem) { vNutrimentName.text = item.title - vNutrimentValue.append("${item.modifier} ${item.value} ${item.unit}") } - fun fillServingValue(item: NutrimentListItem) { - val servingValue = item.servingValue - if (servingValue.isBlank()) { + private fun fillNutrimentValue(item: NutrimentListItem) { + val value = item.value + if (value == null) { + vNutrimentValue.visibility = View.INVISIBLE + } else { + vNutrimentValue.visibility = View.VISIBLE + vNutrimentValue.text = "${item.modifierStr} $value ${item.unitStr}" + } + } + + private fun fillServingValue(item: NutrimentListItem) { + val servingValue = item.servingValueStr + if (servingValue == null) { vNutrimentServingValue.visibility = View.GONE } else { - vNutrimentServingValue.append(String.format("%s %s %s", - item.modifier, - servingValue, - item.unit)) + vNutrimentServingValue.visibility = View.VISIBLE + vNutrimentServingValue.text = "${item.modifierStr} $servingValue ${item.unitStr}" } } } - internal class NutrimentHeaderViewHolder(itemView: View, displayServing: Boolean) : NutrimentViewHolder(itemView) { + internal class NutrimentHeaderViewHolder( + itemView: View, + displayServing: Boolean + ) : NutrimentViewHolder(itemView) { val vNutrimentValue: TextView = itemView.findViewById(R.id.nutriment_value) private val nutrimentServingValue: TextView = itemView.findViewById(R.id.nutriment_serving_value) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/additives/AdditiveFragmentHelper.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/additives/AdditiveFragmentHelper.kt index 528b42121317..a692fca80132 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/additives/AdditiveFragmentHelper.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/additives/AdditiveFragmentHelper.kt @@ -1,6 +1,5 @@ package openfoodfacts.github.scrachx.openfood.features.additives -import android.text.SpannableStringBuilder import android.text.method.LinkMovementMethod import android.text.style.ClickableSpan import android.text.style.DynamicDrawableSpan @@ -9,10 +8,10 @@ import android.view.View import android.widget.TextView import androidx.core.content.ContextCompat import androidx.core.text.bold +import androidx.core.text.buildSpannedString import androidx.core.text.color import androidx.core.text.inSpans import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.fasterxml.jackson.databind.JsonNode import kotlinx.coroutines.launch @@ -44,14 +43,14 @@ object AdditiveFragmentHelper { ) = additivesView.run { movementMethod = LinkMovementMethod.getInstance() isClickable = true - text = SpannableStringBuilder() - .bold { append(fragment.getString(R.string.txtAdditives)) } - .apply { - additives.forEach { - append("\n") - append(getAdditiveTag(it, apiClientForWikiData, fragment, fragment)) - } + text = buildSpannedString { + bold { append(fragment.getString(R.string.txtAdditives)) } + + additives.forEach { + append("\n") + append(getAdditiveTag(it, apiClientForWikiData, fragment)) } + } } /** @@ -64,14 +63,13 @@ object AdditiveFragmentHelper { private fun getAdditiveTag( additive: AdditiveName, wikidataClient: WikiDataApiClient, - fragment: BaseFragment, - lifecycleOwner: LifecycleOwner + fragment: BaseFragment ): CharSequence { val activity = fragment.requireActivity() val clickableSpan = object : ClickableSpan() { override fun onClick(view: View) { if (additive.isWikiDataIdPresent) { - lifecycleOwner.lifecycleScope.launch { + fragment.lifecycleScope.launch { val result = wikidataClient.getEntityData(additive.wikiDataId) getOnWikiResponse(activity, additive)(result) } @@ -81,30 +79,31 @@ object AdditiveFragmentHelper { } } - return SpannableStringBuilder().also { - it.inSpans(clickableSpan) { append(additive.name) } + return buildSpannedString { + inSpans(clickableSpan) { append(additive.name) } // if the additive has an overexposure risk ("high" or "moderate") then append the warning message to it if (additive.hasOverexposureData()) { val isHighRisk = additive.overexposureRisk.equals("high", true) - val riskIcon = ( - if (isHighRisk) ContextCompat.getDrawable(activity, R.drawable.ic_additive_high_risk) - else ContextCompat.getDrawable(activity, R.drawable.ic_additive_moderate_risk) - )?.apply { - setBounds(0, 0, this.intrinsicWidth, this.intrinsicHeight) - }!! + val riskIcon = if (isHighRisk) + ContextCompat.getDrawable(activity, R.drawable.ic_additive_high_risk)!! + else + ContextCompat.getDrawable(activity, R.drawable.ic_additive_moderate_risk)!! + riskIcon.setBounds(0, 0, riskIcon.intrinsicWidth, riskIcon.intrinsicHeight) + val riskWarningStr = if (isHighRisk) fragment.getString(R.string.overexposure_high) else fragment.getString(R.string.overexposure_moderate) + val riskWarningColor = if (isHighRisk) ContextCompat.getColor(activity, R.color.overexposure_high) else ContextCompat.getColor(activity, R.color.overexposure_moderate) - it.append(" ") - it.inSpans(ImageSpan(riskIcon, DynamicDrawableSpan.ALIGN_BOTTOM)) { it.append("-") } - it.append(" ") - it.color(riskWarningColor) { append(riskWarningStr) } + append(" ") + inSpans(ImageSpan(riskIcon, DynamicDrawableSpan.ALIGN_BOTTOM)) { append("-") } + append(" ") + color(riskWarningColor) { append(riskWarningStr) } } } } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/compare/ProductCompareAdapter.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/compare/ProductCompareAdapter.kt index 50f6b385108c..12ababab37e3 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/compare/ProductCompareAdapter.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/compare/ProductCompareAdapter.kt @@ -19,7 +19,6 @@ import android.Manifest.permission import android.app.Activity import android.content.pm.PackageManager.PERMISSION_GRANTED import android.os.Build -import android.text.SpannableStringBuilder import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -28,6 +27,7 @@ import android.widget.TextView import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat.checkSelfPermission import androidx.core.text.bold +import androidx.core.text.buildSpannedString import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.squareup.picasso.Picasso @@ -149,10 +149,11 @@ class ProductCompareAdapter( // Quantity if (!product.quantity.isNullOrBlank()) { - holder.binding.productComparisonQuantity.text = SpannableStringBuilder() - .bold { append(activity.getString(R.string.compare_quantity)) } - .append(" ") - .append(product.quantity) + holder.binding.productComparisonQuantity.text = buildSpannedString { + bold { append(activity.getString(R.string.compare_quantity)) } + append(" ") + append(product.quantity) + } } else { holder.binding.productComparisonQuantity.visibility = View.INVISIBLE } @@ -160,10 +161,11 @@ class ProductCompareAdapter( // Brands val brands = product.brands if (!brands.isNullOrBlank()) { - holder.binding.productComparisonBrand.text = SpannableStringBuilder() - .bold { append(activity.getString(R.string.compare_brands)) } - .append(" ") - .append(brands.split(",").joinToString(", ") { it.trim() }) + holder.binding.productComparisonBrand.text = buildSpannedString { + bold { append(activity.getString(R.string.compare_brands)) } + append(" ") + append(brands.split(",").joinToString(", ") { it.trim() }) + } } else { //TODO: product brand placeholder goes here } @@ -202,10 +204,11 @@ class ProductCompareAdapter( private fun loadAdditives(additiveNames: List, view: TextView) { if (additiveNames.isEmpty()) return - view.text = SpannableStringBuilder() - .bold { append(activity.getString(R.string.compare_additives)) } - .append("\n") - .append(additiveNames.joinToString("\n") { it.name }) + view.text = buildSpannedString { + bold { append(activity.getString(R.string.compare_additives)) } + append("\n") + append(additiveNames.joinToString("\n") { it.name }) + } updateCardsHeight() } @@ -229,42 +232,42 @@ class ProductCompareAdapter( } if (fat != null || salt != null || saturatedFat != null || sugars != null) { - val fatNutriment = nutriments[Nutriments.FAT] + val fatNutriment = nutriments[Nutriment.FAT] if (fat != null && fatNutriment != null) { val fatNutrimentLevel = fat.getLocalize(activity) levelItems += NutrientLevelItem( activity.getString(R.string.compare_fat), - fatNutriment.displayStringFor100g, + fatNutriment.getPer100gDisplayString(), fatNutrimentLevel, fat.getImgRes() ) } - val saturatedFatNutriment = nutriments[Nutriments.SATURATED_FAT] + val saturatedFatNutriment = nutriments[Nutriment.SATURATED_FAT] if (saturatedFat != null && saturatedFatNutriment != null) { val saturatedFatLocalize = saturatedFat.getLocalize(activity) levelItems += NutrientLevelItem( activity.getString(R.string.compare_saturated_fat), - saturatedFatNutriment.displayStringFor100g, + saturatedFatNutriment.getPer100gDisplayString(), saturatedFatLocalize, saturatedFat.getImgRes() ) } - val sugarsNutriment = nutriments[Nutriments.SUGARS] + val sugarsNutriment = nutriments[Nutriment.SUGARS] if (sugars != null && sugarsNutriment != null) { val sugarsLocalize = sugars.getLocalize(activity) levelItems += NutrientLevelItem( activity.getString(R.string.compare_sugars), - sugarsNutriment.displayStringFor100g, + sugarsNutriment.getPer100gDisplayString(), sugarsLocalize, sugars.getImgRes() ) } - val saltNutriment = nutriments[Nutriments.SALT] + val saltNutriment = nutriments[Nutriment.SALT] if (salt != null && saltNutriment != null) { val saltLocalize = salt.getLocalize(activity) levelItems += NutrientLevelItem( activity.getString(R.string.compare_salt), - saltNutriment.displayStringFor100g, + saltNutriment.getPer100gDisplayString(), saltLocalize, salt.getImgRes() ) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/ingredients/EditIngredientsFragment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/ingredients/EditIngredientsFragment.kt index 930ed0921dfb..f09c6baf791c 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/ingredients/EditIngredientsFragment.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/ingredients/EditIngredientsFragment.kt @@ -252,9 +252,10 @@ class EditIngredientsFragment : ProductEditFragment() { * @param tag Tag associated with the allergen */ private fun getTracesName(languageCode: String, tag: String): String { - return daoSession.allergenNameDao!!.queryBuilder() - .where(AllergenNameDao.Properties.AllergenTag.eq(tag), AllergenNameDao.Properties.LanguageCode.eq(languageCode)) - .unique()?.name ?: tag + return daoSession.allergenNameDao.unique { + where(AllergenNameDao.Properties.AllergenTag.eq(tag)) + where(AllergenNameDao.Properties.LanguageCode.eq(languageCode)) + }?.name ?: tag } override fun onDestroyView() { diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/ingredients/EditIngredientsViewModel.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/ingredients/EditIngredientsViewModel.kt index 69b5109b910c..9a52568d6394 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/ingredients/EditIngredientsViewModel.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/ingredients/EditIngredientsViewModel.kt @@ -6,6 +6,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import openfoodfacts.github.scrachx.openfood.models.DaoSession import openfoodfacts.github.scrachx.openfood.models.entities.allergen.AllergenNameDao import openfoodfacts.github.scrachx.openfood.utils.LocaleManager +import openfoodfacts.github.scrachx.openfood.utils.list import javax.inject.Inject @HiltViewModel @@ -15,12 +16,10 @@ class EditIngredientsViewModel @Inject constructor( ) : ViewModel() { val allergens = liveData { - daoSession.allergenNameDao.queryBuilder() - .where(AllergenNameDao.Properties.LanguageCode.eq(localeManager.getLanguage())) - .orderDesc(AllergenNameDao.Properties.Name) - .list() - .map { it.name } - .let { emit(it) } + daoSession.allergenNameDao.list { + where(AllergenNameDao.Properties.LanguageCode.eq(localeManager.getLanguage())) + orderDesc(AllergenNameDao.Properties.Name) + }?.map { it.name }.let { emit(it) } } } \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/nutrition/ProductEditNutritionFactsFragment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/nutrition/ProductEditNutritionFactsFragment.kt index 59bd1116a858..507cb0a8fa70 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/nutrition/ProductEditNutritionFactsFragment.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/nutrition/ProductEditNutritionFactsFragment.kt @@ -29,12 +29,13 @@ import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.widget.* import android.widget.AdapterView.OnItemSelectedListener +import androidx.annotation.StringRes import androidx.core.net.toFile import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope -import com.afollestad.materialdialogs.MaterialDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.TextInputLayout import com.squareup.picasso.Callback import com.squareup.picasso.Picasso @@ -53,12 +54,7 @@ import openfoodfacts.github.scrachx.openfood.features.product.edit.ProductEditAc import openfoodfacts.github.scrachx.openfood.features.shared.views.CustomValidatingEditTextView import openfoodfacts.github.scrachx.openfood.images.ProductImage import openfoodfacts.github.scrachx.openfood.models.* -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_DV -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_GRAM -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_LITER -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_MICROGRAM -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_MILLIGRAM -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_MILLILITRE +import openfoodfacts.github.scrachx.openfood.models.MeasurementUnit.* import openfoodfacts.github.scrachx.openfood.models.entities.OfflineSavedProduct import openfoodfacts.github.scrachx.openfood.network.ApiFields import openfoodfacts.github.scrachx.openfood.network.ApiFields.Defaults.NUTRITION_DATA_PER_100G @@ -66,7 +62,6 @@ import openfoodfacts.github.scrachx.openfood.network.ApiFields.Defaults.NUTRITIO import openfoodfacts.github.scrachx.openfood.network.OpenFoodAPIClient import openfoodfacts.github.scrachx.openfood.utils.* import openfoodfacts.github.scrachx.openfood.utils.FileDownloader.download -import openfoodfacts.github.scrachx.openfood.utils.UnitUtils.UNIT_IU import openfoodfacts.github.scrachx.openfood.utils.Utils.getRoundNumber import java.io.File import java.text.Collator @@ -236,47 +231,51 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { product!!.servingSize ?.takeUnless { it.isEmpty() } // Splits the serving size into value and unit. Example: "15g" into "15" and "g" - ?.let { updateServingSizeFrom(it) } + ?.let { updateServingSize(it) } if (view == null) return val nutriments = product!!.nutriments - binding.energyKj.setText(nutriments.getEnergyKjValue(isDataPerServing)) - binding.energyKcal.setText(nutriments.getEnergyKcalValue(isDataPerServing)) + binding.energyKj.setText(nutriments.getEnergyKjValue(isDataPerServing)?.let { getRoundNumber(it) }) + binding.energyKcal.setText(nutriments.getEnergyKcalValue(isDataPerServing)?.let { getRoundNumber(it) }) // Fill default nutriments fields - (view as ViewGroup).getViewsByType(CustomValidatingEditTextView::class.java).forEach { view -> - var nutrientShortName = view.entryName + for (view in (view as ViewGroup).getViewsByType(CustomValidatingEditTextView::class.java)) { + var nutrimentShortName = view.entryName // Workaround for saturated-fat - if (nutrientShortName == "saturated_fat") nutrientShortName = "saturated-fat" + if (nutrimentShortName == "saturated_fat") nutrimentShortName = "saturated-fat" // Skip serving size and energy view, we already filled them - if (view === binding.servingSize || view === binding.energyKcal || view === binding.energyKj) return@forEach + if (view === binding.servingSize || view === binding.energyKcal || view === binding.energyKj) continue // Get the value - val value = if (isDataPer100g) nutriments[nutrientShortName]?.for100g else nutriments[nutrientShortName]?.forServing - if (value.isNullOrEmpty()) return@forEach + val nutriment = Nutriment.findbyKey(nutrimentShortName) ?: error("Cannot find nutrient $nutrimentShortName") + val value = (if (isDataPer100g) nutriments[nutriment]?.per100gInUnit else nutriments[nutriment]?.perServingInUnit) ?: continue - view.setText(value) - view.unitSpinner?.setSelection(getSelectedUnitFromShortName(nutriments, nutrientShortName)) - view.modSpinner?.setSelection(getSelectedModifierFromShortName(nutriments, nutrientShortName)) + view.setText(getRoundNumber(value)) + view.unitSpinner?.setSelection(getUnitIndexUnitFromShortName(nutriments, nutriment) ?: 0) + view.modSpinner?.setSelection(getModifierIndexFromShortName(nutriments, nutriment)) } // Set the values of all the other nutrients if defined and create new row in the tableLayout. - PARAMS_OTHER_NUTRIENTS.withIndex().forEach { (i, nutrient) -> - val nutrientShortName = getShortName(nutrient) + for ((i, nutrient) in PARAMS_OTHER_NUTRIENTS.withIndex()) { + val nutrimentShortName = getShortName(nutrient) - val value = if (isDataPer100g) nutriments[nutrientShortName]?.for100g else nutriments[nutrientShortName]?.forServing - if (value.isNullOrEmpty()) return@forEach + val nutriment = Nutriment.findbyKey(nutrimentShortName) ?: error("Cannot find nutrient $nutrimentShortName") + val measurement = if (isDataPer100g) { + nutriments[nutriment]?.per100gInUnit + } else { + nutriments[nutriment]?.perServingInUnit + } ?: continue - val unitIndex = getSelectedUnitFromShortName(nutriments, nutrientShortName) - val modIndex = getSelectedModifierFromShortName(nutriments, nutrientShortName) + val unitIndex = getUnitIndexUnitFromShortName(nutriments, nutriment) + val modIndex = getModifierIndexFromShortName(nutriments, nutriment) usedNutrientsIndexes += i val nutrientNames = resources.getStringArray(R.array.nutrients_array) - addNutrientRow(i, nutrientNames[i], true, value, unitIndex, modIndex) + addNutrientRow(i, nutrientNames[i], true, measurement.value, unitIndex ?: 0, modIndex) } } @@ -293,23 +292,26 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { loadNutritionImage(imagePath!!) } - private fun getSelectedUnitFromShortName(nutriments: Nutriments, nutrientShortName: String): Int = - getSelectedUnit(nutrientShortName, nutriments[nutrientShortName]?.unit) + private fun getUnitIndexUnitFromShortName(nutriments: ProductNutriments, nutriment: Nutriment): Int? = + nutriments[nutriment]?.unit?.let { getUnitIndex(nutriment, it) } + + private fun getUnitIndex(nutrient: Nutriment, unit: MeasurementUnit): Int { + require(nutrient != Nutriment.ENERGY_KCAL && nutrient != Nutriment.ENERGY_KJ) { "Nutrient cannot be energy" } + + return getAllUnitsIndex(unit) + } - private fun getSelectedUnit(nutrientShortName: String?, unit: String?) = if (unit != null) { - if (Nutriments.ENERGY_KCAL == nutrientShortName || Nutriments.ENERGY_KJ == nutrientShortName) - throw IllegalArgumentException("Nutrient cannot be energy") - else getPositionInAllUnitArray(unit) - } else 0 + private fun getModifierIndexFromShortName(nutriments: ProductNutriments, nutriment: Nutriment): Int = + getModifierIndex(nutriments[nutriment]?.modifier) - private fun getSelectedModifierFromShortName(nutriments: Nutriments, nutrientShortName: String): Int = - getPositionInModifierArray(nutriments[nutrientShortName]?.modifier ?: "") + private fun updateServingSize(servingSize: String) { + val parts = servingSize.split(Regex("(?<=\\D)(?=\\d)|(?<=\\d)(?=\\D)")) + binding.servingSize.setText(parts[0]) - private fun updateServingSizeFrom(servingSize: String) { - val part = servingSize.split(Regex("(?<=\\D)(?=\\d)|(?<=\\d)(?=\\D)")).toTypedArray() - binding.servingSize.setText(part[0]) - if (part.size > 1) { - binding.servingSize.unitSpinner?.setSelection(getPositionInServingUnitArray(part[1].trim { it <= ' ' })) + if (parts.size > 1) { + val symbol = parts[1].trim { it <= ' ' } + val unit = MeasurementUnit.findBySymbol(symbol)!! + binding.servingSize.unitSpinner?.setSelection(getServingUnitIndex(unit)) } } @@ -331,41 +333,40 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { binding.nutritionFactsLayout.visibility = View.GONE } - productDetails[ApiFields.Keys.NUTRITION_DATA_PER]?.let { nutritionDataPer -> - // can be "100g" or "serving" - updateSelectedDataPer(nutritionDataPer) - } - productDetails[ApiFields.Keys.SERVING_SIZE]?.let { - // Splits the serving size into value and unit. Example: "15g" into "15" and "g" - updateServingSizeFrom(it) - } + // can be "100g" or "serving" + productDetails[ApiFields.Keys.NUTRITION_DATA_PER]?.let(::updateSelectedDataPer) + + // Splits the serving size into value and unit. Example: "15g" into "15" and "g" + productDetails[ApiFields.Keys.SERVING_SIZE]?.let(::updateServingSize) - (binding.root as ViewGroup).getViewsByType(CustomValidatingEditTextView::class.java).forEach { view -> + for (view in (binding.root as ViewGroup).getViewsByType(CustomValidatingEditTextView::class.java)) { val nutrientShortName = view.entryName - if (nutrientShortName == binding.servingSize.entryName) { - return@forEach - } + if (nutrientShortName == binding.servingSize.entryName) continue + val nutrientCompleteName = getCompleteEntryName(view) - val value = productDetails[nutrientCompleteName] - if (value != null) { + + productDetails[nutrientCompleteName]?.let { value -> view.setText(value) - view.unitSpinner?.setSelection(getSelectedUnit(nutrientShortName, productDetails[nutrientCompleteName + ApiFields.Suffix.UNIT])) + val unit = productDetails[nutrientCompleteName + ApiFields.Suffix.UNIT] ?: return@let + view.unitSpinner?.setSelection(getUnitIndex(Nutriment.findbyKey(nutrientShortName)!!, MeasurementUnit.findBySymbol(unit)!!)) } } - //set the values of all the other nutrients if defined and create new row in the tableLayout. + // Set the values of all the other nutrients if defined and create new row in the tableLayout. for ((i, completeNutrientName) in PARAMS_OTHER_NUTRIENTS.withIndex()) { if (productDetails[completeNutrientName] == null) continue var unitIndex = 0 var modIndex = 0 - val value = productDetails[completeNutrientName] - if (productDetails[completeNutrientName + ApiFields.Suffix.UNIT] != null) { - unitIndex = getPositionInAllUnitArray(productDetails[completeNutrientName + ApiFields.Suffix.UNIT]) + val value = productDetails[completeNutrientName]?.toFloatOrNull() ?: continue + + productDetails[completeNutrientName + ApiFields.Suffix.UNIT]?.let { + unitIndex = getAllUnitsIndex(MeasurementUnit.findBySymbol(it)!!) } - if (productDetails[completeNutrientName + ApiFields.Suffix.MODIFIER] != null) { - modIndex = getPositionInAllUnitArray(productDetails[completeNutrientName + ApiFields.Suffix.MODIFIER]) + productDetails[completeNutrientName + ApiFields.Suffix.MODIFIER]?.let { + modIndex = getAllUnitsIndex(MeasurementUnit.findBySymbol(it)!!) } - usedNutrientsIndexes.add(i) + usedNutrientsIndexes += i + val nutrients = resources.getStringArray(R.array.nutrients_array) addNutrientRow(i, nutrients[i], true, value, unitIndex, modIndex) } @@ -412,20 +413,23 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { binding.btnEditImageNutritionFacts.visibility = View.VISIBLE } - private fun getSelectedUnit(i: Int) = NUTRIENTS_UNITS[i] + private fun getUnitIndex(index: Int) = NUTRIENTS_UNITS[index] /** * @param unit The unit corresponding to which the index is to be returned. * @return returns the index to be set to the spinner. */ - private fun getPositionInAllUnitArray(unit: String?) = - NUTRIENTS_UNITS.indexOfFirst { it.equals(unit, true) }.coerceAtLeast(0) + private fun getAllUnitsIndex(unit: MeasurementUnit) = + NUTRIENTS_UNITS.indexOfFirst { it == unit }.coerceAtLeast(0) + + private fun getModifierIndex(modifier: Modifier?) = + MODIFIERS.indexOf(modifier).coerceAtLeast(0) - private fun getPositionInModifierArray(mod: String) = - MODIFIERS.indexOfFirst { it == mod }.coerceAtLeast(0) + private fun getServingUnitIndex(unit: MeasurementUnit) = + SERVING_UNITS.indexOfFirst { it == unit }.coerceAtLeast(0) - private fun getPositionInServingUnitArray(unit: String) = - SERVING_UNITS.indexOfFirst { it.equals(unit, ignoreCase = true) }.coerceAtLeast(0) + private fun getServingUnitIndex(symbol: String) = + SERVING_UNITS.indexOfFirst { it.sym == symbol }.coerceAtLeast(0) private fun addNutritionFactsImage() { val path = imagePath @@ -460,26 +464,32 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { private fun CustomValidatingEditTextView.checkValue(value: Float) = sequenceOf( checkPh(value), checkAlcohol(value), - checkEnergyField(value), + checkEnergy(value), checkCarbohydrate(value), - checkPerServing() + checkServingSize() ).firstOrNull { it != ValueState.NOT_TESTED } ?: this.checkAsGram(value) - private fun CustomValidatingEditTextView.checkAsGram(value: Float): ValueState { - val valid = convertToGrams(value, unitSpinner!!.selectedItemPosition) <= referenceValueInGram - return if (!valid) { - this.showError(getString(R.string.max_nutrient_val_msg)) + private fun CustomValidatingEditTextView.requireToValidate(condition: Boolean, @StringRes errorMsg: Int): ValueState { + return if (condition) ValueState.VALID + else { + showError(context.getString(errorMsg)) ValueState.NOT_VALID - } else ValueState.VALID + } + } + + private fun CustomValidatingEditTextView.checkAsGram(value: Float): ValueState { + val valid = convertToGrams(value, unitSpinner!!.selectedItemPosition)!!.value <= referenceValueInGram + + return requireToValidate(valid, R.string.max_nutrient_val_msg) } private fun CustomValidatingEditTextView.checkValue() { val wasValid = isError() - //if no value, we suppose it's valid + // If no value, we suppose it's valid if (isBlank()) { cancelError() // If per serving is set must be not blank - checkPerServing() + checkServingSize() } else { val value = this.getFloatValue() if (value == null) { @@ -501,7 +511,7 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { if (isCarbohydrateRelated(this)) { binding.carbohydrates.checkValue() } - if (binding.servingSize.entryName == entryName) { + if (entryName == binding.servingSize.entryName) { checkAllValues() } } @@ -513,22 +523,20 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { } private fun updateSodiumValue() { - if (requireActivity().currentFocus === binding.salt) { - val saltValue = binding.salt.getDoubleValue() - if (saltValue != null) { - val sodiumValue = UnitUtils.saltToSodium(saltValue) - binding.sodium.setText(getRoundNumber(sodiumValue)) - } + if (requireActivity().currentFocus !== binding.salt) return + + binding.salt.getFloatValue()?.let { + val sodiumValue = it.saltToSodium() + binding.sodium.setText(getRoundNumber(sodiumValue)) } } private fun updateSaltValue() { - if (requireActivity().currentFocus === binding.sodium) { - val sodiumValue = binding.sodium.getDoubleValue() - if (sodiumValue != null) { - val saltValue = UnitUtils.sodiumToSalt(sodiumValue) - binding.salt.setText(getRoundNumber(saltValue)) - } + if (requireActivity().currentFocus !== binding.sodium) return + + binding.sodium.getFloatValue()?.let { + val saltValue = it.sodiumToSalt() + binding.salt.setText(getRoundNumber(saltValue)) } } @@ -554,11 +562,9 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { } // For every nutrition field add it to map if updated - allEditViews.forEach { - if (binding.servingSize.entryName == it.entryName) return@forEach - if (it.isNotEmpty()) { - addNutrientToMapIfUpdated(it, targetMap) - } + for (view in allEditViews) { + if (view.entryName == binding.servingSize.entryName || !view.isNotEmpty()) continue + addNutrientToMapIfUpdated(view, targetMap) } } @@ -596,38 +602,37 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { editTextView: CustomValidatingEditTextView, targetMap: MutableMap ) { - val productNutriments = product?.nutriments ?: Nutriments() + val productNutriments = product?.nutriments ?: ProductNutriments() - val shortName = editTextView.entryName - val oldProductNutriment = productNutriments[shortName] + val shortName = editTextView.entryName.replace("_", "-") + val nutriment = Nutriment.requireByKey(shortName) + val oldProductNutriment = productNutriments[nutriment] - var oldValue: String? = null - var oldUnit: String? = null - var oldMod: String? = null + var oldValue: Float? = null + var oldUnit: MeasurementUnit? = null + var oldMod: Modifier? = null if (oldProductNutriment != null) { oldUnit = oldProductNutriment.unit oldMod = oldProductNutriment.modifier oldValue = if (isDataPer100g) - oldProductNutriment.for100gInUnits + oldProductNutriment.per100gInUnit.value else - oldProductNutriment.forServingInUnits + oldProductNutriment.perServingInUnit!!.value } - val valueChanged = editTextView.isContentDifferent(oldValue) + val valueChanged = editTextView.isValueDifferent(oldValue) // Check unit and modifier for changes - var newUnit: String? = null - var newMod: String? = null + var newUnit: MeasurementUnit? = null + var newMod: Modifier? = null if (editTextView.hasUnit()) { editTextView.unitSpinner?.let { - newUnit = getSelectedUnit(it.selectedItemPosition) + newUnit = getUnitIndex(it.selectedItemPosition) } } - editTextView.modSpinner?.let { - newMod = it.selectedItem.toString() - } + editTextView.modSpinner?.let { newMod = Modifier.findBySymbol(it.selectedItem.toString()) } val unitChanged = oldUnit == null || oldUnit != newUnit val modChanged = oldMod == null || oldMod != newMod @@ -652,16 +657,18 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { // Add unit field {nutrient-id}_unit to map if (editTextView.hasUnit() && editTextView.unitSpinner != null) { - val selectedUnit = getSelectedUnit(editTextView.unitSpinner!!.selectedItemPosition) - targetMap[fieldName + ApiFields.Suffix.UNIT] = Html.escapeHtml(selectedUnit) + val selectedUnit = getUnitIndex(editTextView.unitSpinner!!.selectedItemPosition) + targetMap[fieldName + ApiFields.Suffix.UNIT] = Html.escapeHtml( + selectedUnit.sym + ) } // Take modifier from attached spinner, add to value if not the default one var mod = "" if (editTextView.modSpinner != null) { - val selectedMod = editTextView.modSpinner!!.selectedItem.toString() - if (DEFAULT_MODIFIER != selectedMod) { - mod = selectedMod + val selectedMod = Modifier.findBySymbol(editTextView.modSpinner!!.selectedItem.toString()) + if (selectedMod != null && DEFAULT_MODIFIER != selectedMod) { + mod = selectedMod.sym } } // The suffix can either be _serving or _100g depending on user input @@ -687,25 +694,26 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { get() { var reference = 100f if (binding.radioGroup.checkedRadioButtonId != R.id.for100g_100ml) { - reference = binding.servingSize.getFloatValueOr(reference) - reference = UnitUtils.convertToGrams(reference, SERVING_UNITS[binding.servingSize.unitSpinner!!.selectedItemPosition]) + val value = binding.servingSize.getFloatValueOr(100f) + reference = measure(value, SERVING_UNITS[binding.servingSize.unitSpinner!!.selectedItemPosition]) + .grams + .value } return reference } private fun displayAddNutrientDialog() { val nutrients = resources.getStringArray(R.array.nutrients_array) - .mapIndexedNotNull { index, nutrient -> - if (usedNutrientsIndexes.contains(index)) null else nutrient - } + .filterIndexed { index, _ -> index !in usedNutrientsIndexes } .sortedWith(Collator.getInstance(Locale.getDefault())) + .toTypedArray() - MaterialDialog.Builder(requireActivity()) - .title(R.string.choose_nutrient) - .items(nutrients) - .itemsCallback { _, _, _, text -> - usedNutrientsIndexes.add(nutrients.indexOf(text)) - val textView = addNutrientRow(nutrients.indexOf(text), text.toString()) + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(R.string.choose_nutrient) + .setItems(nutrients) { _, index -> + val text = nutrients[index] + usedNutrientsIndexes += index + val textView = addNutrientRow(index, text) allEditViews.add(textView) textView.addValidListener() @@ -726,7 +734,7 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { index: Int, hint: String, preFillValues: Boolean = false, - value: String? = null, + value: Float? = null, unitSelectedIndex: Int = 0, modSelectedIndex: Int = 0 ): CustomValidatingEditTextView { @@ -738,6 +746,7 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { rowView.findViewById(R.id.value_til).hint = hint val nutrientShortName = getShortName(nutrientCompleteName) + val nutriment = Nutriment.requireByKey(nutrientShortName) val editText = rowView.findViewById(R.id.value) editText.entryName = nutrientShortName @@ -750,24 +759,28 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { editText.imeOptions = EditorInfo.IME_ACTION_DONE editText.requestFocus() - if (preFillValues) editText.setText(value) + if (preFillValues && value != null) editText.setText(getRoundNumber(value)) // Setup unit spinner val unitSpinner = rowView.findViewById(R.id.spinner_unit) val modSpinner = rowView.findViewById(R.id.spinner_mod) - when (nutrientShortName) { - Nutriments.PH -> { + when (nutriment) { + Nutriment.PH -> { unitSpinner.visibility = View.INVISIBLE } - Nutriments.STARCH -> { - val arrayAdapter = - ArrayAdapter(requireActivity(), android.R.layout.simple_spinner_item, requireActivity().resources.getStringArray(R.array.weights_array)) - arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + Nutriment.STARCH -> { + val arrayAdapter = ArrayAdapter( + requireActivity(), + android.R.layout.simple_spinner_item, + requireActivity().resources.getStringArray(R.array.weights_array) + ).apply { + setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + } unitSpinner.adapter = arrayAdapter starchEditText = editText } - Nutriments.VITAMIN_A, Nutriments.VITAMIN_D, Nutriments.VITAMIN_E -> { + Nutriment.VITAMIN_A, Nutriment.VITAMIN_D, Nutriment.VITAMIN_E -> { val adapter = ArrayAdapter( requireActivity(), android.R.layout.simple_spinner_item, @@ -782,8 +795,8 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { unitSpinner.setSelection(unitSelectedIndex) modSpinner.setSelection(modSelectedIndex) } - } catch (t: Exception) { - sentryAnalytics.record(IllegalStateException("Can't find weight units for nutriment: $nutrientShortName", t)) + } catch (err: Exception) { + sentryAnalytics.record(IllegalStateException("Can't find weight units for nutriment: $nutrientShortName", err)) closeScreenWithAlert() } @@ -798,7 +811,7 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { private fun isCarbohydrateRelated(editText: CustomValidatingEditTextView): Boolean { val entryName = editText.entryName - return binding.sugars.entryName == entryName || starchEditText != null && entryName == starchEditText!!.entryName + return entryName == binding.sugars.entryName || (starchEditText != null && entryName == starchEditText!!.entryName) } /** @@ -808,98 +821,83 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { private fun CustomValidatingEditTextView.checkCarbohydrate(value: Float): ValueState { if (binding.carbohydrates.entryName != entryName) return ValueState.NOT_TESTED - val res = this.checkAsGram(value) + val res = checkAsGram(value) - if (ValueState.NOT_VALID == res) return res + if (res == ValueState.NOT_VALID) return res - var carbsValue = binding.carbohydrates.getFloatValueOr(0f) - var sugarValue = binding.sugars.getFloatValueOr(0f) + val carbsValue = binding.carbohydrates.getFloatValueOr(0f) + val sugarValue = binding.sugars.getFloatValueOr(0f) // Check that value of (sugar + starch) is not greater than value of carbohydrates // Convert all the values to grams - carbsValue = convertToGrams(carbsValue, binding.carbohydrates.unitSpinner!!.selectedItemPosition) - sugarValue = convertToGrams(sugarValue, binding.sugars.unitSpinner!!.selectedItemPosition) - - val newStarch = convertToGrams(starchValue, starchUnitSelectedIndex).toDouble() + val carbsInG = convertToGrams(carbsValue, binding.carbohydrates.unitSpinner!!.selectedItemPosition)!! + val sugarInG = convertToGrams(sugarValue, binding.sugars.unitSpinner!!.selectedItemPosition)!! + val newStarchInG = convertToGrams(getStarchValue(), getStarchUnitSelectedIndex())!! - return if (sugarValue + newStarch > carbsValue) { - binding.carbohydrates.showError(getString(R.string.error_in_carbohydrate_value)) - ValueState.NOT_VALID - } else ValueState.VALID + return requireToValidate(sugarInG.value + newStarchInG.value <= carbsInG.value, R.string.error_in_carbohydrate_value) } /** * Validate serving size value entered by user */ - private fun CustomValidatingEditTextView.checkPerServing(): ValueState { - return if (binding.servingSize.entryName == entryName) { - if (isDataPer100g) { - return ValueState.VALID - } - val value = binding.servingSize.getFloatValueOr(0f) - if (value <= 0) { - showError(getString(R.string.error_nutrient_serving_data)) - return ValueState.NOT_VALID - } - ValueState.VALID - } else ValueState.NOT_TESTED + private fun CustomValidatingEditTextView.checkServingSize(): ValueState { + if (entryName != binding.servingSize.entryName) return ValueState.NOT_TESTED + if (isDataPer100g) return ValueState.VALID + + val value = binding.servingSize.getFloatValueOr(0f) + return requireToValidate(value > 0, R.string.error_nutrient_serving_data) } /** - * Validate oh value according to [Nutriments.PH] + * Validate oh value according to [Nutriment.PH] * @param value quality value with known prefix */ private fun CustomValidatingEditTextView.checkPh(value: Float): ValueState { - return if (Nutriments.PH == entryName) { - val maxPhValue = 14.0 - if (value > maxPhValue || value >= maxPhValue && this.isModifierEqualsToGreaterThan()) { - setText(maxPhValue.toString()) - } - ValueState.VALID - } else ValueState.NOT_TESTED + if (entryName != Nutriment.PH.key) return ValueState.NOT_TESTED + + val maxPhValue = 14f + // Coerce the value + if (value > maxPhValue || value == maxPhValue && this.isModifierEqualsToGreaterThan()) { + setText(maxPhValue.toString()) + modSpinner?.setSelection(0) + } + + return ValueState.VALID } /** * Validate energy value entered by user */ - private fun CustomValidatingEditTextView.checkEnergyField(value: Float): ValueState { - when (entryName) { - binding.energyKcal.entryName -> { - var energyInKcal = value - if (binding.radioGroup.checkedRadioButtonId != R.id.for100g_100ml) { - energyInKcal *= 100.0f / referenceValueInGram - } - val isValid = energyInKcal <= 2000f - if (!isValid) { - showError(getString(R.string.max_energy_val_msg)) - } - return if (isValid) ValueState.VALID else ValueState.NOT_VALID - } - binding.energyKj.entryName -> { - var energyInKj = value - if (binding.radioGroup.checkedRadioButtonId != R.id.for100g_100ml) { - energyInKj *= 100.0f / referenceValueInGram - } - val isValid = energyInKj <= 8368000f - if (!isValid) { - showError(getString(R.string.max_energy_val_msg)) - } - return if (isValid) ValueState.VALID else ValueState.NOT_VALID - } - else -> return ValueState.NOT_TESTED + private fun CustomValidatingEditTextView.checkEnergy(value: Float): ValueState { + if (entryName != binding.energyKcal.entryName && entryName != binding.energyKj.entryName) return ValueState.NOT_TESTED + + var energy = value + + if (binding.radioGroup.checkedRadioButtonId != R.id.for100g_100ml) { + energy *= 100.0f / referenceValueInGram + } + + val isValid = when (entryName) { + binding.energyKcal.entryName -> energy <= 2000f + binding.energyKj.entryName -> energy <= 8368000f + else -> true } + + return requireToValidate(isValid, R.string.max_energy_val_msg) } /** * validate alcohol content entered by user */ - private fun CustomValidatingEditTextView.checkAlcohol(value: Float) = - if (binding.alcohol.entryName == entryName) { - if (value > 100) { - showError("This value is over 100") - ValueState.NOT_VALID - } else ValueState.VALID - } else ValueState.NOT_TESTED + private fun CustomValidatingEditTextView.checkAlcohol(value: Float): ValueState { + if (entryName != binding.alcohol.entryName) return ValueState.NOT_TESTED + + return if (value <= 100) ValueState.VALID + else { + showError("This value is over 100") // TODO: i18n + ValueState.NOT_VALID + } + } override fun showImageProgress() { if (!isAdded) return @@ -909,12 +907,8 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { binding.btnEditImageNutritionFacts.visibility = View.INVISIBLE } - private val starchValue - get() = if (starchEditText == null) 0F else starchEditText!!.getFloatValue() ?: 0F - - - private val starchUnitSelectedIndex - get() = if (starchEditText == null) 0 else starchEditText!!.unitSpinner!!.selectedItemPosition + private fun getStarchValue() = starchEditText?.getFloatValue() ?: 0F + private fun getStarchUnitSelectedIndex() = starchEditText?.unitSpinner?.selectedItemPosition ?: 0 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) @@ -965,15 +959,15 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { * @param index 1 represents milligrams, 2 represents micrograms * @return return the converted value */ - private fun convertToGrams(value: Float, index: Int): Float { - val unit = NUTRIENTS_UNITS[index] - // Can't be converted to grams. - return if (UNIT_DV == unit || UNIT_IU == unit) 0F - else UnitUtils.convertToGrams(value, unit) + private fun convertToGrams(value: Float, index: Int): Measurement? { + return when (val unit = NUTRIENTS_UNITS[index]) { + UNIT_DV, UNIT_IU -> null // Can't be converted to grams. + else -> measure(value, unit).grams + } } private fun Spinner.setOnItemSelectedListener( - block: ( + onItemSelected: ( parent: AdapterView<*>?, view: View?, position: Int, @@ -982,7 +976,7 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { ) { this.onItemSelectedListener = object : OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) = - block(parent, view, position, id) + onItemSelected(parent, view, position, id) override fun onNothingSelected(parent: AdapterView<*>?) = Unit // This is not possible } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/overview/EditOverviewFragment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/overview/EditOverviewFragment.kt index 14453f329264..8c95758c242e 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/overview/EditOverviewFragment.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/overview/EditOverviewFragment.kt @@ -19,7 +19,6 @@ import android.content.Intent import android.content.SharedPreferences import android.net.Uri import android.os.Bundle -import android.text.SpannableStringBuilder import android.util.Log import android.view.LayoutInflater import android.view.View @@ -27,6 +26,7 @@ import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.Toast import androidx.core.net.toFile +import androidx.core.text.buildSpannedString import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import com.canhub.cropper.CropImage @@ -378,12 +378,10 @@ class EditOverviewFragment : ProductEditFragment() { * @return returns the name of the country if found in the db or else returns the tag itself. */ private fun getCountryName(languageCode: String?, tag: String) = - daoSession.countryNameDao.queryBuilder() - .where( - CountryNameDao.Properties.CountyTag.eq(tag), - CountryNameDao.Properties.LanguageCode.eq(languageCode) - ) - .unique()?.name ?: tag + daoSession.countryNameDao.unique { + where(CountryNameDao.Properties.CountyTag.eq(tag)) + where(CountryNameDao.Properties.LanguageCode.eq(languageCode)) + }?.name ?: tag /** * @param languageCode 2 letter language code. example de, en etc. @@ -391,12 +389,10 @@ class EditOverviewFragment : ProductEditFragment() { * @return returns the name of the label if found in the db or else returns the tag itself. */ private fun getLabelName(languageCode: String?, tag: String) = - daoSession.labelNameDao.queryBuilder() - .where( - LabelNameDao.Properties.LabelTag.eq(tag), - LabelNameDao.Properties.LanguageCode.eq(languageCode) - ).unique() - ?.name ?: tag + daoSession.labelNameDao.unique { + where(LabelNameDao.Properties.LabelTag.eq(tag)) + where(LabelNameDao.Properties.LanguageCode.eq(languageCode)) + }?.name ?: tag /** * @param languageCode 2 letter language code. example en, fr etc. @@ -404,20 +400,16 @@ class EditOverviewFragment : ProductEditFragment() { * @return returns the name of the category (example Plant-based foods and beverages) if found in the db or else returns the tag itself. */ private fun getCategoryName(languageCode: String?, tag: String): String { - return daoSession.categoryNameDao - .queryBuilder() - .where( - CategoryNameDao.Properties.CategoryTag.eq(tag), - CategoryNameDao.Properties.LanguageCode.eq(languageCode) - ).unique() - ?.name ?: tag + return daoSession.categoryNameDao.unique { + where(CategoryNameDao.Properties.CategoryTag.eq(tag)) + where(CategoryNameDao.Properties.LanguageCode.eq(languageCode)) + }?.name ?: tag } private fun getEmbCode(embTag: String) = - daoSession.tagDao.queryBuilder() - .where(TagDao.Properties.Id.eq(embTag)) - .unique() - ?.name ?: embTag + daoSession.tagDao.unique { + where(TagDao.Properties.Id.eq(embTag)) + }?.name ?: embTag /** @@ -573,8 +565,10 @@ class EditOverviewFragment : ProductEditFragment() { this.languageCode = languageCode val productLocale = LocaleUtils.parseLocale(languageCode) - binding.language.text = SpannableStringBuilder(getString(R.string.product_language)) - .append(productLocale.getDisplayName(appLocale).replaceFirstChar { it.titlecase(appLocale) }) + binding.language.text = buildSpannedString { + append(getString(R.string.product_language)) + append(productLocale.getDisplayName(appLocale).replaceFirstChar { it.titlecase(appLocale) }) + } val activity = activity (activity as? ProductEditActivity)?.setProductLanguageCode(languageCode) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/overview/EditOverviewViewModel.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/overview/EditOverviewViewModel.kt index c65d11cf9ac8..cf10c1196ba3 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/overview/EditOverviewViewModel.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/overview/EditOverviewViewModel.kt @@ -12,6 +12,7 @@ import openfoodfacts.github.scrachx.openfood.models.entities.country.CountryName import openfoodfacts.github.scrachx.openfood.models.entities.label.LabelNameDao import openfoodfacts.github.scrachx.openfood.models.entities.store.StoreNameDao import openfoodfacts.github.scrachx.openfood.utils.LocaleManager +import openfoodfacts.github.scrachx.openfood.utils.list import javax.inject.Inject @HiltViewModel @@ -22,45 +23,39 @@ class EditOverviewViewModel @Inject constructor( private val appLang by lazy { localeManager.getLanguage() } internal val suggestCategories = liveData { - daoSession.categoryNameDao.queryBuilder() - .where(CategoryNameDao.Properties.LanguageCode.eq(appLang)) - .orderDesc(CategoryNameDao.Properties.Name).list() - .mapNotNull { it.name } - .let { emit(it) } + daoSession.categoryNameDao.list { + where(CategoryNameDao.Properties.LanguageCode.eq(appLang)) + orderDesc(CategoryNameDao.Properties.Name) + }?.mapNotNull { it.name }.let { emit(it) } } internal val suggestCountries = liveData { - daoSession.countryNameDao.queryBuilder() - .where(CountryNameDao.Properties.LanguageCode.eq(appLang)) - .orderDesc(CountryNameDao.Properties.Name).list() - .mapNotNull { it.name } - .let { emit(it) } + daoSession.countryNameDao.list { + where(CountryNameDao.Properties.LanguageCode.eq(appLang)) + orderDesc(CountryNameDao.Properties.Name) + }?.mapNotNull { it.name }.let { emit(it) } } internal val suggestLabels = liveData { - daoSession.labelNameDao.queryBuilder() - .where(LabelNameDao.Properties.LanguageCode.eq(appLang)) - .orderDesc(LabelNameDao.Properties.Name).list() - .mapNotNull { it.name } - .let { emit(it) } + daoSession.labelNameDao.list { + where(LabelNameDao.Properties.LanguageCode.eq(appLang)) + orderDesc(LabelNameDao.Properties.Name) + }?.mapNotNull { it.name }.let { emit(it) } } internal val suggestStores = liveData { - daoSession.storeNameDao.queryBuilder() - .where(StoreNameDao.Properties.LanguageCode.eq(appLang)) - .orderDesc(StoreNameDao.Properties.Name).list() - .mapNotNull { it.name } - .let { emit(it) } - + daoSession.storeNameDao.list { + where(StoreNameDao.Properties.LanguageCode.eq(appLang)) + orderDesc(StoreNameDao.Properties.Name).list() + }.mapNotNull { it.name }.let { emit(it) } } internal val suggestBrands = liveData { - daoSession.brandNameDao.queryBuilder() - .where(BrandNameDao.Properties.LanguageCode.eq(appLang)) - .orderDesc(BrandNameDao.Properties.Name).list() - .mapNotNull { it.name } - .let { emit(it) } + daoSession.brandNameDao.list { + where(BrandNameDao.Properties.LanguageCode.eq(appLang)) + orderDesc(BrandNameDao.Properties.Name) + }?.mapNotNull { it.name }.let { emit(it) } } internal val product = MutableLiveData() diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/CalculateDetailsActivity.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/CalculateDetailsActivity.kt index 2f267209fada..146fd1ef43d2 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/CalculateDetailsActivity.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/CalculateDetailsActivity.kt @@ -5,29 +5,29 @@ import android.content.Intent import android.os.Bundle import android.view.MenuItem import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL import androidx.recyclerview.widget.LinearLayoutManager import openfoodfacts.github.scrachx.openfood.R import openfoodfacts.github.scrachx.openfood.databinding.CalculateDetailsBinding import openfoodfacts.github.scrachx.openfood.features.adapters.CalculatedNutrimentsGridAdapter import openfoodfacts.github.scrachx.openfood.features.shared.BaseActivity import openfoodfacts.github.scrachx.openfood.models.* -import openfoodfacts.github.scrachx.openfood.utils.UnitUtils.convertToGrams +import openfoodfacts.github.scrachx.openfood.models.MeasurementUnit +import openfoodfacts.github.scrachx.openfood.utils.Measurement +import openfoodfacts.github.scrachx.openfood.utils.grams import openfoodfacts.github.scrachx.openfood.utils.isPerServingInLiter +import openfoodfacts.github.scrachx.openfood.utils.measure import java.util.* import kotlin.properties.Delegates class CalculateDetailsActivity : BaseActivity() { private val nutrimentListItems = mutableListOf() - private val nutriMap = hashMapOf( - Nutriments.SALT to R.string.nutrition_salt, - Nutriments.SODIUM to R.string.nutrition_sodium, - Nutriments.ALCOHOL to R.string.nutrition_alcohol - ) - private lateinit var nutriments: Nutriments + + private lateinit var nutriments: ProductNutriments private lateinit var product: Product - private lateinit var spinnerValue: String private var weight by Delegates.notNull() + private lateinit var unitOfMeasurement: MeasurementUnit override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -41,139 +41,144 @@ class CalculateDetailsActivity : BaseActivity() { supportActionBar!!.setDisplayHomeAsUpEnabled(true) val product = intent.getSerializableExtra(KEY_PRODUCT) as Product? - val spinnerValue = intent.getStringExtra(KEY_SPINNER_VALUE) + val weight = intent.getFloatExtra(KEY_WEIGHT, -1f) + val unit = intent.getStringExtra(KEY_SPINNER_VALUE)?.let { MeasurementUnit.findBySymbol(it) } requireNotNull(product) { "${this::class.simpleName} created without product intent extra." } - requireNotNull(spinnerValue) { "${this::class.simpleName} created without spinner value intent extra." } + requireNotNull(unit) { "${this::class.simpleName} created without spinner value intent extra." } require(weight != -1f) { "${this::class.simpleName} created with weight = -1" } this.product = product - this.spinnerValue = spinnerValue + this.unitOfMeasurement = unit this.weight = weight this.nutriments = product.nutriments - binding.resultTextView.text = getString(R.string.display_fact, "$weight $spinnerValue") - binding.nutriments.setHasFixedSize(true) + binding.resultTextView.text = getString(R.string.display_fact, "$weight ${unit.sym}") - // use a linear layout manager - val mLayoutManager = LinearLayoutManager(this) - binding.nutriments.layoutManager = mLayoutManager + binding.nutriments.setHasFixedSize(true) + binding.nutriments.layoutManager = LinearLayoutManager(this) binding.nutriments.isNestedScrollingEnabled = false // use VERTICAL divider - val dividerItemDecoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) - binding.nutriments.addItemDecoration(dividerItemDecoration) + binding.nutriments.addItemDecoration(DividerItemDecoration(this, VERTICAL)) // Header hack nutrimentListItems += NutrimentListItem(product.isPerServingInLiter() ?: false) + val portion = measure(weight, unit) + // Energy - val energyKcal = nutriments[Nutriments.ENERGY_KCAL] + val energyKcal = nutriments[Nutriment.ENERGY_KCAL] if (energyKcal != null) { nutrimentListItems += NutrimentListItem( getString(R.string.nutrition_energy_short_name), - calculateCalories(weight, spinnerValue).toString(), - energyKcal.forServingInUnits, - Units.ENERGY_KCAL, - energyKcal.getModifierIfNotDefault(), + calculateCalories(portion).value, + energyKcal.perServingInUnit?.value, + MeasurementUnit.ENERGY_KCAL, + energyKcal.modifier, ) } - val energyKj = nutriments[Nutriments.ENERGY_KJ] + val energyKj = nutriments[Nutriment.ENERGY_KJ] if (energyKj != null) { nutrimentListItems += NutrimentListItem( getString(R.string.nutrition_energy_short_name), - calculateKj(weight, spinnerValue).toString(), - energyKj.forServingInUnits, - Units.ENERGY_KJ.lowercase(Locale.getDefault()), - energyKj.getModifierIfNotDefault(), + calculateKj(portion).value, + energyKj.perServingInUnit?.value, + MeasurementUnit.ENERGY_KJ, + energyKj.modifier, ) } // Fat - val fat = nutriments[Nutriments.FAT] + val fat = nutriments[Nutriment.FAT] if (fat != null) { nutrimentListItems += BoldNutrimentListItem( getString(R.string.nutrition_fat), - fat.getForPortion(weight, spinnerValue), - fat.forServingInUnits, + fat.getForPortion(portion).value, + fat.perServingInUnit?.value, fat.unit, - fat.getModifierIfNotDefault() + fat.modifier ) - nutrimentListItems.addAll(getNutrimentItems(nutriments, Nutriments.FAT_MAP)) + nutrimentListItems += getNutrimentItems(nutriments, FAT_MAP) } // Carbohydrates - val carbohydrates = nutriments[Nutriments.CARBOHYDRATES] + val carbohydrates = nutriments[Nutriment.CARBOHYDRATES] if (carbohydrates != null) { nutrimentListItems += BoldNutrimentListItem( getString(R.string.nutrition_carbohydrate), - carbohydrates.getForPortion(weight, spinnerValue), - carbohydrates.forServingInUnits, + carbohydrates.getForPortion(portion).value, + carbohydrates.perServingInUnit?.value, carbohydrates.unit, - carbohydrates.getModifierIfNotDefault() + carbohydrates.modifier ) - nutrimentListItems += getNutrimentItems(nutriments, Nutriments.CARBO_MAP) + nutrimentListItems += getNutrimentItems(nutriments, CARBO_MAP) } - // fiber - nutrimentListItems += getNutrimentItems(nutriments, Collections.singletonMap(Nutriments.FIBER, R.string.nutrition_fiber)) + // Fiber + nutrimentListItems += getNutrimentItems(nutriments, mapOf(Nutriment.FIBER to R.string.nutrition_fiber)) // Proteins - val proteins = nutriments[Nutriments.PROTEINS] + val proteins = nutriments[Nutriment.PROTEINS] if (proteins != null) { nutrimentListItems += BoldNutrimentListItem( getString(R.string.nutrition_proteins), - proteins.getForPortion(weight, spinnerValue), - proteins.forServingInUnits, + proteins.getForPortion(portion).value, + proteins.perServingInUnit?.value, proteins.unit, - proteins.getModifierIfNotDefault() + proteins.modifier ) - nutrimentListItems += getNutrimentItems(nutriments, Nutriments.PROT_MAP) + nutrimentListItems += getNutrimentItems(nutriments, PROT_MAP) } // salt and alcohol - nutrimentListItems += getNutrimentItems(nutriments, nutriMap) + nutrimentListItems += getNutrimentItems( + nutriments, mapOf( + Nutriment.SALT to R.string.nutrition_salt, + Nutriment.SODIUM to R.string.nutrition_sodium, + Nutriment.ALCOHOL to R.string.nutrition_alcohol + ) + ) // Vitamins if (nutriments.hasVitamins) { nutrimentListItems += BoldNutrimentListItem(getString(R.string.nutrition_vitamins)) - nutrimentListItems += getNutrimentItems(nutriments, Nutriments.VITAMINS_MAP) + nutrimentListItems += getNutrimentItems(nutriments, VITAMINS_MAP) } // Minerals if (nutriments.hasMinerals) { nutrimentListItems += BoldNutrimentListItem(getString(R.string.nutrition_minerals)) - nutrimentListItems += getNutrimentItems(nutriments, Nutriments.MINERALS_MAP) + nutrimentListItems += getNutrimentItems(nutriments, MINERALS_MAP) } binding.nutriments.adapter = CalculatedNutrimentsGridAdapter(nutrimentListItems) } - private fun getNutrimentItems(nutriments: Nutriments, nutrimentMap: Map): List { + private fun getNutrimentItems(nutriments: ProductNutriments, nutrimentMap: Map): List { return nutrimentMap.mapNotNull { (name, stringRes) -> - val nutriment = nutriments[name] - if (nutriment != null) { - NutrimentListItem( - getString(stringRes), - nutriment.getForPortion(weight, spinnerValue), - nutriment.forServingInUnits, - nutriment.unit, - nutriment.getModifierIfNotDefault() - ) - } else null + val nutriment = nutriments[name] ?: return@mapNotNull null + + NutrimentListItem( + getString(stringRes), + nutriment.getForPortion(measure(weight, unitOfMeasurement)).value, + nutriment.perServingInUnit?.value, + nutriment.unit, + nutriment.modifier + ) } } - private fun calculateCalories(weight: Float, unit: String?): Float { - val energy100gCal = product.nutriments[Nutriments.ENERGY_KCAL]!!.for100gInUnits.toFloat() - val weightGrams = convertToGrams(weight, unit) - return energy100gCal / 100 * weightGrams + private fun calculateCalories(portion: Measurement): Measurement { + val energy100gCal = product.nutriments[Nutriment.ENERGY_KCAL]!!.per100gInG + val portionGrams = portion.grams.value + return Measurement(energy100gCal.value / 100 * portionGrams, energy100gCal.unit) } - private fun calculateKj(weight: Float, unit: String?): Float { - val energy100gKj = product.nutriments[Nutriments.ENERGY_KJ]!!.for100gInUnits.toFloat() - val weightGrams = convertToGrams(weight, unit) - return energy100gKj / 100 * weightGrams + private fun calculateKj(portion: Measurement): Measurement { + val energy100gKj = product.nutriments[Nutriment.ENERGY_KJ]!!.per100gInG + val weightGrams = portion.grams.value + return Measurement(energy100gKj.value / 100 * weightGrams, energy100gKj.unit) } override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { @@ -190,17 +195,6 @@ class CalculateDetailsActivity : BaseActivity() { private const val KEY_SPINNER_VALUE = "spinnerValue" private const val KEY_WEIGHT = "weight" - @Deprecated( - "Use {@link #start(Context, Product, String, float)}", - ReplaceWith( - "start(context, product, spinnerValue, weight.toFloat())", - "openfoodfacts.github.scrachx.openfood.features.product.view.CalculateDetailsActivity.Companion.start" - ) - ) - fun start(context: Context, product: Product, spinnerValue: String, weight: String) { - start(context, product, spinnerValue, weight.toFloat()) - } - fun start(context: Context, product: Product, spinnerValue: String, weight: Float) { context.startActivity(Intent(context, CalculateDetailsActivity::class.java).apply { putExtra(KEY_PRODUCT, product) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/CategoryProductHelper.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/CategoryProductHelper.kt index 4cc2b1e55238..b9ffe9914dcb 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/CategoryProductHelper.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/CategoryProductHelper.kt @@ -1,14 +1,15 @@ package openfoodfacts.github.scrachx.openfood.features.product.view import android.graphics.Typeface -import android.text.SpannableStringBuilder import android.text.Spanned +import android.text.SpannedString import android.text.method.LinkMovementMethod import android.text.style.* import android.view.View import android.widget.TextView import androidx.core.content.ContextCompat import androidx.core.text.bold +import androidx.core.text.buildSpannedString import androidx.core.text.color import androidx.core.text.inSpans import androidx.lifecycle.lifecycleScope @@ -38,16 +39,15 @@ object CategoryProductHelper { view.visibility = View.VISIBLE view.movementMethod = LinkMovementMethod.getInstance() view.isClickable = true - view.text = SpannableStringBuilder() - .bold { append(fragment.getString(R.string.txtCategories)) } - .append(" ") - .apply { - // Add all the categories to text view and link them to wikidata if possible - categories.map { getCategoriesTag(it, fragment, apiClient) }.forEachIndexed { i, el -> - append(el) - if (i != categories.size) append(", ") - } + view.text = buildSpannedString { + bold { append(fragment.getString(R.string.txtCategories)) } + append(" ") + // Add all the categories to text view and link them to wikidata if possible + categories.map { getCategoriesTag(it, fragment, apiClient) }.forEachIndexed { i, el -> + append(el) + if (i != categories.size) append(", ") } + } // Show alcohol health warning if (categories.any { it.categoryTag == "en:alcoholic-beverages" }) { showAlcoholAlert(alcoholAlertText, fragment) @@ -58,7 +58,7 @@ object CategoryProductHelper { category: CategoryName, fragment: BaseFragment, apiClient: WikiDataApiClient - ): SpannableStringBuilder { + ): SpannedString { val clickableSpan = object : ClickableSpan() { override fun onClick(view: View) { if (category.isWikiDataIdPresent == true) { @@ -87,18 +87,13 @@ object CategoryProductHelper { } } - val span = SpannableStringBuilder() - .inSpans(clickableSpan) { append(category.name) } - if (category.isNull) { - // Span to make text italic - span.setSpan( - StyleSpan(Typeface.ITALIC), - 0, - span.length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ) + return buildSpannedString { + inSpans(clickableSpan) { append(category.name) } + if (category.isNull) { + // Span to make text italic + setSpan(StyleSpan(Typeface.ITALIC), 0, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } } - return span } private fun showAlcoholAlert(alcoholAlertText: TextView, fragment: BaseFragment) { @@ -111,11 +106,13 @@ object CategoryProductHelper { } val riskAlcoholConsumption = fragment.getString(R.string.risk_alcohol_consumption) alcoholAlertText.visibility = View.VISIBLE - alcoholAlertText.text = SpannableStringBuilder() - .inSpans(ImageSpan(alcoholAlertIcon, DynamicDrawableSpan.ALIGN_BOTTOM)) { append("-") } - .append(" ") - .color(ContextCompat.getColor(context, R.color.red)) { + + alcoholAlertText.text = buildSpannedString { + inSpans(ImageSpan(alcoholAlertIcon, DynamicDrawableSpan.ALIGN_BOTTOM)) { append("-") } + append(" ") + color(ContextCompat.getColor(context, R.color.red)) { append(riskAlcoholConsumption) } + } } } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/attribute/ProductAttributeFragment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/attribute/ProductAttributeFragment.kt index eb1eec451c45..9b7bf472b8b2 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/attribute/ProductAttributeFragment.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/attribute/ProductAttributeFragment.kt @@ -116,7 +116,8 @@ class ProductAttributeFragment : BottomSheetDialogFragment() { val id = arguments.getLong(ARG_ID) if (searchType == SearchType.ADDITIVE) { daoSession.additiveNameDao.queryBuilder() - .where(AdditiveNameDao.Properties.Id.eq(id)).unique() + .where(AdditiveNameDao.Properties.Id.eq(id)) + .unique() ?.let { updateContent(view, it) } } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/contributors/ContributorsFragment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/contributors/ContributorsFragment.kt index 709e67849517..252fd024dbab 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/contributors/ContributorsFragment.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/contributors/ContributorsFragment.kt @@ -1,13 +1,13 @@ package openfoodfacts.github.scrachx.openfood.features.product.view.contributors import android.os.Bundle -import android.text.SpannableStringBuilder import android.text.Spanned import android.text.method.LinkMovementMethod import android.text.style.ClickableSpan import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.text.buildSpannedString import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint import openfoodfacts.github.scrachx.openfood.R @@ -86,12 +86,16 @@ class ContributorsFragment : BaseFragment() { if (product.editors.isNotEmpty()) { binding.otherEditorsTxt.movementMethod = LinkMovementMethod.getInstance() - binding.otherEditorsTxt.text = SpannableStringBuilder(getString(R.string.other_editors)) - .append(" ") - .append(product.editors.joinToString(", ") { editor -> - getContributorsTag(editor).subSequence(0, editor.length) - }) - .append(getContributorsTag(product.editors.last())) + binding.otherEditorsTxt.text = buildSpannedString { + append(getString(R.string.other_editors)) + append(" ") + product.editors.map { getContributorsTag(it).subSequence(0, it.length) } + .forEachIndexed { i, el -> + if (i > 0) append(", ") + append(el) + } + append(getContributorsTag(product.editors.last())) + } } else { binding.otherEditorsTxt.visibility = View.INVISIBLE } @@ -122,7 +126,8 @@ class ContributorsFragment : BaseFragment() { val clickableSpan = object : ClickableSpan() { override fun onClick(view: View) = start(requireContext(), SearchType.CONTRIBUTOR, contributor) } - return SpannableStringBuilder(contributor).apply { + return buildSpannedString { + append(contributor) setSpan(clickableSpan, 0, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) append(" ") } @@ -132,7 +137,8 @@ class ContributorsFragment : BaseFragment() { val clickableSpan = object : ClickableSpan() { override fun onClick(view: View) = start(requireContext(), SearchType.STATE, stateTag) } - return SpannableStringBuilder(stateName).apply { + return buildSpannedString { + append(stateName) setSpan(clickableSpan, 0, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) } } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/environment/EnvironmentProductFragment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/environment/EnvironmentProductFragment.kt index 783a56406b02..d938ef544999 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/environment/EnvironmentProductFragment.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/environment/EnvironmentProductFragment.kt @@ -4,7 +4,6 @@ import android.app.Activity import android.content.Intent import android.content.SharedPreferences import android.os.Bundle -import android.text.SpannableStringBuilder import android.text.method.LinkMovementMethod import android.view.LayoutInflater import android.view.View @@ -12,6 +11,7 @@ import android.view.ViewGroup import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat.FROM_HTML_MODE_COMPACT import androidx.core.text.bold +import androidx.core.text.buildSpannedString import com.squareup.picasso.Picasso import dagger.hilt.android.AndroidEntryPoint import io.reactivex.rxkotlin.addTo @@ -24,7 +24,7 @@ import openfoodfacts.github.scrachx.openfood.features.product.edit.ProductEditAc import openfoodfacts.github.scrachx.openfood.features.product.view.ProductViewActivity import openfoodfacts.github.scrachx.openfood.features.shared.BaseFragment import openfoodfacts.github.scrachx.openfood.images.ProductImage -import openfoodfacts.github.scrachx.openfood.models.Nutriments +import openfoodfacts.github.scrachx.openfood.models.Nutriment import openfoodfacts.github.scrachx.openfood.models.Product import openfoodfacts.github.scrachx.openfood.models.ProductImageField import openfoodfacts.github.scrachx.openfood.models.ProductState @@ -104,12 +104,13 @@ class EnvironmentProductFragment : BaseFragment() { mUrlImage = imagePackagingUrl } - val carbonFootprintNutriment = nutriments[Nutriments.CARBON_FOOTPRINT] + val carbonFootprintNutriment = nutriments[Nutriment.CARBON_FOOTPRINT] if (carbonFootprintNutriment != null) { - binding.textCarbonFootprint.text = SpannableStringBuilder() - .bold { append(getString(R.string.textCarbonFootprint)) } - .append(carbonFootprintNutriment.for100gInUnits) - .append(carbonFootprintNutriment.unit) + binding.textCarbonFootprint.text = buildSpannedString { + bold { append(getString(R.string.textCarbonFootprint)) } + append(Utils.getRoundNumber(carbonFootprintNutriment.per100gInUnit)) + append(carbonFootprintNutriment.unit.sym) + } } else { binding.carbonFootprintCv.visibility = View.GONE } @@ -124,10 +125,11 @@ class EnvironmentProductFragment : BaseFragment() { val packaging = product.packaging if (!packaging.isNullOrEmpty()) { - binding.packagingText.text = SpannableStringBuilder() - .bold { append(getString(R.string.packaging_environmentTab)) } - .append(" ") - .append(packaging.replace(",", ", ")) + binding.packagingText.text = buildSpannedString { + bold { append(getString(R.string.packaging_environmentTab)) } + append(" ") + append(packaging.replace(",", ", ")) + } } else { binding.packagingCv.visibility = View.GONE } @@ -135,9 +137,10 @@ class EnvironmentProductFragment : BaseFragment() { val recyclingInstructionsToDiscard = product.recyclingInstructionsToDiscard if (!recyclingInstructionsToDiscard.isNullOrEmpty()) { // TODO: 02/03/2021 i18n - binding.recyclingInstructionToDiscard.text = SpannableStringBuilder() - .bold { append("Recycling instructions - To discard: ") } - .append(recyclingInstructionsToDiscard) + binding.recyclingInstructionToDiscard.text = buildSpannedString { + bold { append("Recycling instructions - To discard: ") } + append(recyclingInstructionsToDiscard) + } } else { binding.recyclingInstructionsDiscardCv.visibility = View.GONE } @@ -145,9 +148,10 @@ class EnvironmentProductFragment : BaseFragment() { val recyclingInstructionsToRecycle = product.recyclingInstructionsToRecycle if (!recyclingInstructionsToRecycle.isNullOrEmpty()) { // TODO: 02/03/2021 i18n - binding.recyclingInstructionToRecycle.text = SpannableStringBuilder() - .bold { append("Recycling instructions - To recycle: ") } - .append(recyclingInstructionsToRecycle) + binding.recyclingInstructionToRecycle.text = buildSpannedString { + bold { append("Recycling instructions - To recycle: ") } + append(recyclingInstructionsToRecycle) + } } else { binding.recyclingInstructionsRecycleCv.visibility = View.GONE } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/ingredients/IngredientsProductFragment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/ingredients/IngredientsProductFragment.kt index 91b4cb8819a4..21c433ba58a1 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/ingredients/IngredientsProductFragment.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/ingredients/IngredientsProductFragment.kt @@ -22,8 +22,8 @@ import android.graphics.Bitmap import android.graphics.Typeface import android.net.Uri import android.os.Bundle -import android.text.SpannableStringBuilder import android.text.Spanned +import android.text.SpannedString import android.text.method.LinkMovementMethod import android.text.style.ClickableSpan import android.text.style.StyleSpan @@ -31,11 +31,12 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.browser.customtabs.CustomTabsIntent +import androidx.core.net.toUri import androidx.core.text.bold +import androidx.core.text.buildSpannedString import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.viewpager2.widget.ViewPager2 -import com.afollestad.materialdialogs.MaterialDialog import com.squareup.picasso.Picasso import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @@ -58,7 +59,6 @@ import openfoodfacts.github.scrachx.openfood.features.product.edit.ProductEditAc import openfoodfacts.github.scrachx.openfood.features.product.edit.ProductEditActivity.Companion.KEY_STATE import openfoodfacts.github.scrachx.openfood.features.product.edit.ProductEditActivity.PerformOCRContract import openfoodfacts.github.scrachx.openfood.features.product.edit.ProductEditActivity.SendUpdatedImgContract -import openfoodfacts.github.scrachx.openfood.features.search.ProductSearchActivity.Companion.start import openfoodfacts.github.scrachx.openfood.features.shared.BaseFragment import openfoodfacts.github.scrachx.openfood.images.ProductImage import openfoodfacts.github.scrachx.openfood.models.DaoSession @@ -75,6 +75,7 @@ import openfoodfacts.github.scrachx.openfood.utils.* import openfoodfacts.github.scrachx.openfood.utils.ProductInfoState.* import java.io.File import javax.inject.Inject +import openfoodfacts.github.scrachx.openfood.features.search.ProductSearchActivity.Companion.start as startSearch @AndroidEntryPoint class IngredientsProductFragment : BaseFragment() { @@ -164,38 +165,45 @@ class IngredientsProductFragment : BaseFragment() { binding.viewModel = viewModel viewModel.vitaminsTags.observe(viewLifecycleOwner) { - if (it.isNotEmpty()) { + if (it.isEmpty()) binding.cvVitaminsTagsText.visibility = View.GONE + else { binding.cvVitaminsTagsText.visibility = View.VISIBLE - binding.vitaminsTagsText.text = SpannableStringBuilder() - .bold { append(getString(R.string.vitamin_tags_text)) } - .append(tagListToString(it)) - } else binding.cvVitaminsTagsText.visibility = View.GONE + binding.vitaminsTagsText.text = buildSpannedString { + bold { append(getString(R.string.vitamin_tags_text)) } + append(tagListToString(it)) + } + } } viewModel.aminoAcidTagsList.observe(viewLifecycleOwner) { - if (it.isNotEmpty()) { + if (it.isEmpty()) binding.cvAminoAcidTagsText.visibility = View.GONE + else { binding.cvAminoAcidTagsText.visibility = View.VISIBLE - binding.aminoAcidTagsText.text = SpannableStringBuilder() - .bold { append(getString(R.string.amino_acid_tags_text)) } - .append(tagListToString(it)) - } else binding.cvAminoAcidTagsText.visibility = View.GONE + binding.aminoAcidTagsText.text = buildSpannedString { + bold { append(getString(R.string.amino_acid_tags_text)) } + append(tagListToString(it)) + } + } } viewModel.mineralTags.observe(viewLifecycleOwner) { - if (it.isNotEmpty()) { + if (it.isEmpty()) binding.cvMineralTagsText.visibility = View.GONE + else { binding.cvMineralTagsText.visibility = View.VISIBLE - binding.mineralTagsText.text = SpannableStringBuilder() - .bold { append(getString(R.string.mineral_tags_text)) } - .append(tagListToString(it)) - } else binding.cvMineralTagsText.visibility = View.GONE + binding.mineralTagsText.text = buildSpannedString { + bold { append(getString(R.string.mineral_tags_text)) } + append(tagListToString(it)) + } + } } viewModel.mineralTags.observe(viewLifecycleOwner) { if (it.isNotEmpty()) { binding.otherNutritionTags.visibility = View.VISIBLE - binding.otherNutritionTags.text = SpannableStringBuilder() - .bold { append(getString(R.string.other_tags_text)) } - .append(tagListToString(it)) + binding.otherNutritionTags.text = buildSpannedString { + bold { append(getString(R.string.other_tags_text)) } + append(tagListToString(it)) + } } } @@ -220,8 +228,7 @@ class IngredientsProductFragment : BaseFragment() { viewModel.product.value = product - binding.textAdditiveProduct.text = SpannableStringBuilder() - .bold { append(getString(R.string.txtAdditives)) } + binding.textAdditiveProduct.text = buildSpannedString { bold { append(getString(R.string.txtAdditives)) } } setAdditivesState(Loading) viewModel.additives.observe(viewLifecycleOwner) { additives -> @@ -254,7 +261,7 @@ class IngredientsProductFragment : BaseFragment() { val allergens = getAllergens() if (!product.getIngredientsText(langCode).isNullOrEmpty()) { binding.cvTextIngredientProduct.visibility = View.VISIBLE - var txtIngredients = SpannableStringBuilder(product.getIngredientsText(langCode)!!.replace("_", "")) + var txtIngredients = buildSpannedString { append(product.getIngredientsText(langCode)!!.replace("_", "")) } txtIngredients = boldAllergens(txtIngredients, allergens) if (product.getIngredientsText(langCode).isNullOrEmpty()) { binding.extractIngredientsPrompt.visibility = View.VISIBLE @@ -279,19 +286,26 @@ class IngredientsProductFragment : BaseFragment() { if (!product.traces.isNullOrBlank()) { val language = localeManager.getLanguage() binding.cvTextTraceProduct.visibility = View.VISIBLE + binding.textTraceProduct.movementMethod = LinkMovementMethod.getInstance() - binding.textTraceProduct.text = SpannableStringBuilder() - .bold { append(getString(R.string.txtTraces)) } - binding.textTraceProduct.append(" ") - val traces = product.traces.split(",") - - binding.textTraceProduct.append(traces.joinToString(", ") { - getSearchLinkText( - getTracesName(language, it), - SearchType.TRACE, - requireActivity() - ) - }) + binding.textTraceProduct.text = buildSpannedString { + bold { append(getString(R.string.txtTraces)) } + append(" ") + + val traces = product.traces.split(",") + traces.map { + getSearchLinkText( + getTracesName(language, it), + SearchType.TRACE, + requireActivity() + ) + }.forEachIndexed { i, el -> + append(el) + if (i != traces.size) append(", ") + } + } + + } else { binding.cvTextTraceProduct.visibility = View.GONE } @@ -301,7 +315,7 @@ class IngredientsProductFragment : BaseFragment() { binding.novaExplanation.text = Utils.getNovaGroupExplanation(novaGroups, requireContext()) ?: "" binding.novaGroup.setImageResource(product.getNovaGroupResource()) binding.novaGroup.setOnClickListener { - val uri = Uri.parse(getString(R.string.url_nova_groups)) + val uri = getString(R.string.url_nova_groups).toUri() val tabsIntent = CustomTabsHelper.getCustomTabsIntent(requireContext(), customTabActivityHelper.session) CustomTabActivityHelper.openCustomTab(requireActivity(), tabsIntent, uri, WebViewFallback()) } @@ -311,9 +325,11 @@ class IngredientsProductFragment : BaseFragment() { } private fun getTracesName(languageCode: String, tag: String): String { - val allergenName = daoSession.allergenNameDao.queryBuilder() - .where(AllergenNameDao.Properties.AllergenTag.eq(tag), AllergenNameDao.Properties.LanguageCode.eq(languageCode)) - .unique() + val allergenName = daoSession.allergenNameDao.unique { + where(AllergenNameDao.Properties.AllergenTag.eq(tag)) + where(AllergenNameDao.Properties.LanguageCode.eq(languageCode)) + } + return if (allergenName != null) allergenName.name else tag } @@ -322,7 +338,6 @@ class IngredientsProductFragment : BaseFragment() { private fun getAllergensTag(allergen: AllergenName): CharSequence { - val ssb = SpannableStringBuilder() val clickableSpan: ClickableSpan = object : ClickableSpan() { override fun onClick(view: View) { if (allergen.isWikiDataIdPresent) { @@ -336,22 +351,24 @@ class IngredientsProductFragment : BaseFragment() { } } } else { - start(requireContext(), SearchType.ALLERGEN, allergen.allergenTag, allergen.name) + startSearch(requireContext(), SearchType.ALLERGEN, allergen.allergenTag, allergen.name) } } } - ssb.append(allergen.name) - ssb.setSpan(clickableSpan, 0, ssb.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + return buildSpannedString { + append(allergen.name) + setSpan(clickableSpan, 0, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - // If allergen is not in the taxonomy list then italicize it - if (!allergen.isNotNull) { - ssb.setSpan(StyleSpan(Typeface.ITALIC), 0, ssb.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + // If allergen is not in the taxonomy list then italicize it + if (!allergen.isNotNull) { + setSpan(StyleSpan(Typeface.ITALIC), 0, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } } - return ssb } - private fun boldAllergens(ingredientsText: CharSequence, allergenTags: List): SpannableStringBuilder { - return SpannableStringBuilder(ingredientsText).also { ssb -> + private fun boldAllergens(ingredientsText: CharSequence, allergenTags: List): SpannedString { + return buildSpannedString { + append(ingredientsText) INGREDIENT_REGEX.findAll(ingredientsText).forEach { match -> val allergenTxt = match.value @@ -366,11 +383,12 @@ class IngredientsProductFragment : BaseFragment() { if (")" in match.value) { end -= 1 } - ssb.setSpan(StyleSpan(Typeface.BOLD), start, end, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + setSpan(StyleSpan(Typeface.BOLD), start, end, Spanned.SPAN_INCLUSIVE_INCLUSIVE) } } - ssb.insert(0, SpannableStringBuilder() - .bold { append(getString(R.string.txtIngredients) + ' ') }) + insert(0, buildSpannedString { + bold { append(getString(R.string.txtIngredients) + ' ') } + }) } } @@ -417,10 +435,14 @@ class IngredientsProductFragment : BaseFragment() { is Empty -> binding.textSubstanceProduct.visibility = View.GONE is Data -> { binding.textSubstanceProduct.movementMethod = LinkMovementMethod.getInstance() - binding.textSubstanceProduct.text = SpannableStringBuilder() - .bold { append(getString(R.string.txtSubstances)) } - .append(" ") - .append(state.data.joinToString(", ") { getAllergensTag(it) }) + binding.textSubstanceProduct.text = buildSpannedString { + bold { append(getString(R.string.txtSubstances)) } + append(" ") + state.data.map { getAllergensTag(it) }.forEachIndexed { i, el -> + append(el) + if (i != state.data.size) append(", ") + } + } } } } @@ -451,18 +473,13 @@ class IngredientsProductFragment : BaseFragment() { } private fun showSignInDialog() { - MaterialDialog.Builder(requireContext()).apply { - title(R.string.sign_in_to_edit) - positiveText(R.string.txtSignIn) - negativeText(R.string.dialog_cancel) - onPositive { dialog, _ -> + buildSignInDialog(requireContext(), + onPositive = { dialog, _ -> loginLauncher.launch(Unit) dialog.dismiss() - } - onNegative { dialog, _ -> dialog.dismiss() } - build() - show() - } + }, + onNegative = { d, _ -> d.dismiss() } + ).show() } private fun openFullScreen() { diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/nutrition/NutritionProductFragment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/nutrition/NutritionProductFragment.kt index 7864960e3273..d6f55769e066 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/nutrition/NutritionProductFragment.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/nutrition/NutritionProductFragment.kt @@ -22,8 +22,6 @@ import android.content.pm.PackageManager import android.graphics.Bitmap import android.net.Uri import android.os.Bundle -import android.text.SpannableStringBuilder -import android.text.Spanned import android.text.method.LinkMovementMethod import android.text.style.ClickableSpan import android.view.LayoutInflater @@ -36,10 +34,12 @@ import android.widget.Button import android.widget.EditText import android.widget.Spinner import androidx.browser.customtabs.CustomTabsIntent -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat +import androidx.core.app.ActivityCompat.requestPermissions +import androidx.core.content.ContextCompat.checkSelfPermission import androidx.core.net.toUri import androidx.core.text.bold +import androidx.core.text.buildSpannedString +import androidx.core.text.inSpans import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration @@ -71,12 +71,7 @@ import openfoodfacts.github.scrachx.openfood.features.shared.BaseFragment import openfoodfacts.github.scrachx.openfood.features.shared.adapters.NutrientLevelListAdapter import openfoodfacts.github.scrachx.openfood.images.ProductImage import openfoodfacts.github.scrachx.openfood.models.* -import openfoodfacts.github.scrachx.openfood.models.Nutriments.Companion.ALCOHOL -import openfoodfacts.github.scrachx.openfood.models.Nutriments.Companion.FAT -import openfoodfacts.github.scrachx.openfood.models.Nutriments.Companion.SALT -import openfoodfacts.github.scrachx.openfood.models.Nutriments.Companion.SATURATED_FAT -import openfoodfacts.github.scrachx.openfood.models.Nutriments.Companion.SODIUM -import openfoodfacts.github.scrachx.openfood.models.Nutriments.Companion.SUGARS +import openfoodfacts.github.scrachx.openfood.models.MeasurementUnit.* import openfoodfacts.github.scrachx.openfood.models.entities.SendProduct import openfoodfacts.github.scrachx.openfood.network.ApiFields import openfoodfacts.github.scrachx.openfood.network.OpenFoodAPIClient @@ -119,7 +114,9 @@ class NutritionProductFragment : BaseFragment(), CustomTabActivityHelper.Connect /** * Boolean to determine if image should be loaded or not */ - private val isLowBatteryMode by lazy { requireContext().isDisableImageLoad() && requireContext().isBatteryLevelLow() } + private val isLowBatteryMode by lazy { + requireContext().isDisableImageLoad() && requireContext().isBatteryLevelLow() + } private var nutrientsImageUrl: String? = null private var mSendProduct: SendProduct? = null @@ -191,18 +188,17 @@ class NutritionProductFragment : BaseFragment(), CustomTabActivityHelper.Connect } val nutriments = product.nutriments - if (Nutriments.CARBON_FOOTPRINT !in nutriments) { + if (Nutriment.CARBON_FOOTPRINT !in nutriments) { binding.textCarbonFootprint.visibility = GONE } setupNutrientItems(nutriments) - //checks the flags and accordingly sets the text of the prompt + // Checks the flags and accordingly sets the text of the prompt showPrompts() binding.textNutriScoreInfo.isClickable = true binding.textNutriScoreInfo.movementMethod = LinkMovementMethod.getInstance() - val spannableStringBuilder = SpannableStringBuilder() val clickableSpan = object : ClickableSpan() { override fun onClick(view: View) { val customTabsIntent = CustomTabsIntent.Builder().build() @@ -218,32 +214,34 @@ class NutritionProductFragment : BaseFragment(), CustomTabActivityHelper.Connect ) } } - spannableStringBuilder.append(getString(R.string.txtNutriScoreInfo)) - spannableStringBuilder.setSpan( - clickableSpan, - 0, - spannableStringBuilder.length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ) - binding.textNutriScoreInfo.text = spannableStringBuilder - var servingSize = product.servingSize - if (servingSize.isNullOrEmpty()) { + binding.textNutriScoreInfo.text = buildSpannedString { + inSpans(clickableSpan) { + append(getString(R.string.txtNutriScoreInfo)) + } + } + + var servingSize: Measurement? = null + val servingSizeString = product.servingSize + if (servingSizeString.isNullOrEmpty()) { binding.textServingSize.visibility = GONE binding.servingSizeCardView.visibility = GONE } else { val pref = sharedPreferences.getString(getString(R.string.pref_volume_unit_key), "l") if (pref.equals("oz", true)) { - servingSize = UnitUtils.getServingInOz(servingSize) - } else if (pref.equals("l", true) && servingSize.contains("oz", true)) { - servingSize = UnitUtils.getServingInL(servingSize) + servingSize = UnitUtils.getServingInOz(servingSizeString) + } else if (pref.equals("l", true) && servingSizeString.contains("oz", true)) { + servingSize = UnitUtils.getServingInL(servingSizeString) } - binding.textServingSize.text = SpannableStringBuilder() - .bold { append(getString(R.string.txtServingSize)) } - .append(" ") - .append(servingSize) + servingSize?.let { + binding.textServingSize.text = buildSpannedString { + bold { append(getString(R.string.txtServingSize)) } + append(" ") + append("${Utils.getRoundNumber(it.value)} ${it.unit}") + } + } } if (arguments != null) { @@ -299,87 +297,72 @@ class NutritionProductFragment : BaseFragment(), CustomTabActivityHelper.Connect nutrimentListItems += NutrimentListItem(inVolume == true) // Energy - val energyKcal = nutriments[Nutriments.ENERGY_KCAL] + val energyKcal = nutriments[Nutriment.ENERGY_KCAL] if (energyKcal != null) { nutrimentListItems += NutrimentListItem( getString(R.string.nutrition_energy_kcal), - nutriments.getEnergyKcalValue(false), - nutriments.getEnergyKcalValue(true), - Units.ENERGY_KCAL, - energyKcal.getModifierIfNotDefault() + nutriments.getEnergyKcalValue(false)?.value, + nutriments.getEnergyKcalValue(true)?.value, + ENERGY_KCAL, + energyKcal.modifier ) } - val energyKj = nutriments[Nutriments.ENERGY_KJ] + val energyKj = nutriments[Nutriment.ENERGY_KJ] if (energyKj != null) { nutrimentListItems += NutrimentListItem( getString(R.string.nutrition_energy_kj), - nutriments.getEnergyKjValue(false), - nutriments.getEnergyKjValue(true), - Units.ENERGY_KJ, - energyKj.getModifierIfNotDefault() + nutriments.getEnergyKjValue(false)?.value, + nutriments.getEnergyKjValue(true)?.value, + ENERGY_KJ, + energyKj.modifier ) } // Fat - val fat2 = nutriments[FAT] + val fat2 = nutriments[Nutriment.FAT] if (fat2 != null) { - nutrimentListItems += BoldNutrimentListItem( - getString(R.string.nutrition_fat), - fat2.for100gInUnits, - fat2.forServingInUnits, - fat2.unit, - fat2.getModifierIfNotDefault() - ) - nutrimentListItems.addAll(getNutrimentItems(nutriments, Nutriments.FAT_MAP)) + nutrimentListItems += BoldNutrimentListItem(getString(R.string.nutrition_fat), fat2) + nutrimentListItems.addAll(getNutrimentItems(nutriments, FAT_MAP)) } // Carbohydrates - val carbohydrates = nutriments[Nutriments.CARBOHYDRATES] + val carbohydrates = nutriments[Nutriment.CARBOHYDRATES] if (carbohydrates != null) { nutrimentListItems += BoldNutrimentListItem( getString(R.string.nutrition_carbohydrate), - carbohydrates.for100gInUnits, - carbohydrates.forServingInUnits, - carbohydrates.unit, - carbohydrates.getModifierIfNotDefault() + carbohydrates ) - nutrimentListItems += getNutrimentItems(nutriments, Nutriments.CARBO_MAP) + nutrimentListItems += getNutrimentItems(nutriments, CARBO_MAP) } // fiber - nutrimentListItems += getNutrimentItems(nutriments, Collections.singletonMap(Nutriments.FIBER, R.string.nutrition_fiber)) + nutrimentListItems += getNutrimentItems(nutriments, mapOf(Nutriment.FIBER to R.string.nutrition_fiber)) // Proteins - val proteins = nutriments[Nutriments.PROTEINS] + val proteins = nutriments[Nutriment.PROTEINS] if (proteins != null) { - nutrimentListItems += BoldNutrimentListItem( - getString(R.string.nutrition_proteins), - proteins.for100gInUnits, - proteins.forServingInUnits, - proteins.unit, - proteins.getModifierIfNotDefault() - ) - nutrimentListItems += getNutrimentItems(nutriments, Nutriments.PROT_MAP) + nutrimentListItems += BoldNutrimentListItem(getString(R.string.nutrition_proteins), proteins) + nutrimentListItems += getNutrimentItems(nutriments, PROT_MAP) } // salt and alcohol - val map = hashMapOf( - SALT to R.string.nutrition_salt, - SODIUM to R.string.nutrition_sodium, - ALCOHOL to R.string.nutrition_alcohol + val map = mapOf( + Nutriment.SALT to R.string.nutrition_salt, + Nutriment.SODIUM to R.string.nutrition_sodium, + Nutriment.ALCOHOL to R.string.nutrition_alcohol ) nutrimentListItems += getNutrimentItems(nutriments, map) // Vitamins if (nutriments.hasVitamins) { nutrimentListItems += BoldNutrimentListItem(getString(R.string.nutrition_vitamins)) - nutrimentListItems += getNutrimentItems(nutriments, Nutriments.VITAMINS_MAP) + nutrimentListItems += getNutrimentItems(nutriments, VITAMINS_MAP) } // Minerals if (nutriments.hasMinerals) { nutrimentListItems += BoldNutrimentListItem(getString(R.string.nutrition_minerals)) - nutrimentListItems += getNutrimentItems(nutriments, Nutriments.MINERALS_MAP) + nutrimentListItems += getNutrimentItems(nutriments, MINERALS_MAP) } // Show nutrition table and nutrition per portion button if nutritional values are available @@ -390,7 +373,7 @@ class NutritionProductFragment : BaseFragment(), CustomTabActivityHelper.Connect } - private fun setupNutrientItems(nutriments: Nutriments) { + private fun setupNutrientItems(nutriments: ProductNutriments) { val levelItemList = mutableListOf() val nutrientLevels = product.nutrientLevels var fat: NutrimentLevel? = null @@ -416,44 +399,44 @@ class NutritionProductFragment : BaseFragment(), CustomTabActivityHelper.Connect nutritionScoreUri = getString(R.string.nutriscore_uri).toUri() customTabActivityHelper!!.mayLaunchUrl(nutritionScoreUri, null, null) - val fatNutriment = nutriments[FAT] + val fatNutriment = nutriments[Nutriment.FAT] if (fat != null && fatNutriment != null) { val fatNutrimentLevel = fat.getLocalize(requireActivity()) levelItemList += NutrientLevelItem( getString(R.string.txtFat), - fatNutriment.displayStringFor100g, + fatNutriment.getPer100gDisplayString(), fatNutrimentLevel, fat.getImgRes(), ) } - val saturatedFatNutriment = nutriments[SATURATED_FAT] + val saturatedFatNutriment = nutriments[Nutriment.SATURATED_FAT] if (saturatedFat != null && saturatedFatNutriment != null) { val saturatedFatLocalize = saturatedFat.getLocalize(requireActivity()) levelItemList += NutrientLevelItem( getString(R.string.txtSaturatedFat), - saturatedFatNutriment.displayStringFor100g, + saturatedFatNutriment.getPer100gDisplayString(), saturatedFatLocalize, saturatedFat.getImgRes(), ) } - val sugarsNutriment = nutriments[SUGARS] + val sugarsNutriment = nutriments[Nutriment.SUGARS] if (sugars != null && sugarsNutriment != null) { val sugarsLocalize = sugars.getLocalize(requireActivity()) levelItemList += NutrientLevelItem( getString(R.string.txtSugars), - sugarsNutriment.displayStringFor100g, + sugarsNutriment.getPer100gDisplayString(), sugarsLocalize, sugars.getImgRes(), ) } - val saltNutriment = nutriments[SALT] + val saltNutriment = nutriments[Nutriment.SALT] if (salt != null && saltNutriment != null) { levelItemList += NutrientLevelItem( getString(R.string.txtSalt), - saltNutriment.displayStringFor100g, + saltNutriment.getPer100gDisplayString(), salt.getLocalize(requireActivity()), salt.getImgRes() ) @@ -504,16 +487,10 @@ class NutritionProductFragment : BaseFragment(), CustomTabActivityHelper.Connect } } - private fun getNutrimentItems(nutriments: Nutriments, nutrimentMap: Map): List { - return nutrimentMap.mapNotNull { (key, value) -> - val nutriment = nutriments[key] ?: return@mapNotNull null - NutrimentListItem( - getString(value), - nutriment.for100gInUnits, - nutriment.forServingInUnits, - if (value == R.string.ph) "" else nutriment.unit, - nutriment.getModifierIfNotDefault(), - ) + private fun getNutrimentItems(productNutriments: ProductNutriments, nutrimentMap: Map): List { + return nutrimentMap.mapNotNull { (nutriment, stringRes) -> + val productNutriment = productNutriments[nutriment] ?: return@mapNotNull null + NutrimentListItem(getString(stringRes), productNutriment) } } @@ -536,10 +513,13 @@ class NutritionProductFragment : BaseFragment(), CustomTabActivityHelper.Connect ) } else { // take a picture - if (ContextCompat.checkSelfPermission(requireActivity(), permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(requireActivity(), arrayOf(permission.CAMERA), MY_PERMISSIONS_REQUEST_CAMERA) - } else { - EasyImage.openCamera(this, 0) + when { + checkSelfPermission(requireActivity(), permission.CAMERA) != PackageManager.PERMISSION_GRANTED -> { + requestPermissions(requireActivity(), arrayOf(permission.CAMERA), MY_PERMISSIONS_REQUEST_CAMERA) + } + else -> { + EasyImage.openCamera(this, 0) + } } } } @@ -554,32 +534,32 @@ class NutritionProductFragment : BaseFragment(), CustomTabActivityHelper.Connect build() }.apply { show() } - val dialogView = dialog.customView ?: return - - val etWeight = dialogView.findViewById(R.id.edit_text_weight) - val spinner = dialogView.findViewById(R.id.spinner_weight) - spinner.onItemSelectedListener = object : OnItemSelectedListener { - override fun onItemSelected(adapterView: AdapterView<*>?, view: View, i: Int, l: Long) { - - val btn = dialog.findViewById(R.id.txt_calories_result) as Button - - btn.setOnClickListener { - val toFloatOrNull = etWeight.text.toString().toFloatOrNull() - if (etWeight.text.toString().isEmpty() || toFloatOrNull == null) { - Snackbar.make(binding.root, resources.getString(R.string.please_enter_weight), LENGTH_SHORT).show() - } else { - CalculateDetailsActivity.start( - requireActivity(), - product, - spinner.selectedItem.toString(), - toFloatOrNull - ) - dialog.dismiss() + + val weightText = dialog.findViewById(R.id.edit_text_weight) as EditText + + (dialog.findViewById(R.id.spinner_weight) as Spinner).apply { + onItemSelectedListener = object : OnItemSelectedListener { + override fun onNothingSelected(adapterView: AdapterView<*>?) = Unit // We don't care + + override fun onItemSelected(adapterView: AdapterView<*>?, view: View, i: Int, l: Long) { + val btn = dialog.findViewById(R.id.txt_calories_result) as Button + + btn.setOnClickListener { + val weight = weightText.text.toString().toFloatOrNull() + if (weightText.text.isEmpty() || weight == null) { + Snackbar.make(binding.root, resources.getString(R.string.please_enter_weight), LENGTH_SHORT).show() + } else { + CalculateDetailsActivity.start( + requireActivity(), + product, + selectedItem.toString(), + weight + ) + dialog.dismiss() + } } } } - - override fun onNothingSelected(adapterView: AdapterView<*>?) = Unit // We don't care } } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/summary/SummaryProductFragment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/summary/SummaryProductFragment.kt index b8dd28f3371f..a317d4249351 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/summary/SummaryProductFragment.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/summary/SummaryProductFragment.kt @@ -22,7 +22,6 @@ import android.content.res.ColorStateList import android.graphics.Color import android.net.Uri import android.os.Bundle -import android.text.SpannableStringBuilder import android.text.method.LinkMovementMethod import android.text.style.ClickableSpan import android.util.Log @@ -35,6 +34,7 @@ import androidx.browser.customtabs.CustomTabsIntent import androidx.core.content.res.ResourcesCompat import androidx.core.net.toUri import androidx.core.text.bold +import androidx.core.text.buildSpannedString import androidx.core.text.inSpans import androidx.core.view.updatePadding import androidx.lifecycle.lifecycleScope @@ -76,7 +76,7 @@ import openfoodfacts.github.scrachx.openfood.features.productlists.ProductListsA import openfoodfacts.github.scrachx.openfood.features.search.ProductSearchActivity import openfoodfacts.github.scrachx.openfood.features.shared.BaseFragment import openfoodfacts.github.scrachx.openfood.features.shared.adapters.NutrientLevelListAdapter -import openfoodfacts.github.scrachx.openfood.features.shared.views.QuestionDialog +import openfoodfacts.github.scrachx.openfood.features.shared.views.showQuestionDialog import openfoodfacts.github.scrachx.openfood.images.ProductImage import openfoodfacts.github.scrachx.openfood.models.* import openfoodfacts.github.scrachx.openfood.models.entities.ListedProduct @@ -275,14 +275,17 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { product = productState.product!! presenter = SummaryProductPresenter(localeManager.getLanguage(), product, this, productRepository) - binding.categoriesText.text = SpannableStringBuilder() - .bold { append(getString(R.string.txtCategories)) } + binding.categoriesText.text = buildSpannedString { + bold { append(getString(R.string.txtCategories)) } + } - binding.labelsText.text = SpannableStringBuilder() - .bold { append(getString(R.string.txtLabels)) } + binding.labelsText.text = buildSpannedString { + bold { append(getString(R.string.txtLabels)) } + } - binding.textAdditiveProduct.text = SpannableStringBuilder() - .bold { append(getString(R.string.txtAdditives)) } + binding.textAdditiveProduct.text = buildSpannedString { + bold { append(getString(R.string.txtAdditives)) } + } // Refresh visibility of UI components binding.textBrandProduct.visibility = View.VISIBLE @@ -351,24 +354,26 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { if (product.embTags.isNotEmpty() && product.embTags.toString().trim { it <= ' ' } != "[]") { binding.embText.movementMethod = LinkMovementMethod.getInstance() - binding.embText.text = SpannableStringBuilder() - .bold { append(getString(R.string.txtEMB)) } - binding.embText.append(" ") - val embTags = product.embTags.toString() - .removeSurrounding("[", "]") - .split(", ") + binding.embText.text = buildSpannedString { + bold { append(getString(R.string.txtEMB)) } + append(" ") - embTags.withIndex().forEach { (i, embTag) -> - if (i > 0) binding.embText.append(", ") - binding.embText.append( - getSearchLinkText( - getEmbCode(embTag).trim { it <= ' ' }, - SearchType.EMB, - requireActivity() - ) - ) + product.embTags.toString() + .removeSurrounding("[", "]") + .split(", ") + .map { + getSearchLinkText( + getEmbCode(it).trim { it <= ' ' }, + SearchType.EMB, + requireActivity() + ) + }.forEachIndexed { i, embTag -> + if (i > 0) append(", ") + + append(embTag) + } } } else { binding.embText.visibility = View.GONE @@ -403,40 +408,40 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { nutritionScoreUri = Uri.parse(getString(R.string.nutriscore_uri)) customTabActivityHelper.mayLaunchUrl(nutritionScoreUri, null, null) binding.cvNutritionLights.visibility = View.VISIBLE - val fatNutriment = nutriments[Nutriments.FAT] + val fatNutriment = nutriments[Nutriment.FAT] if (fat != null && fatNutriment != null) { levelItems += NutrientLevelItem( getString(R.string.txtFat), - fatNutriment.displayStringFor100g, + fatNutriment.getPer100gDisplayString(), fat.getLocalize(requireContext()), fat.getImgRes(), ) } - val saturatedFatNutriment = nutriments[Nutriments.SATURATED_FAT] + val saturatedFatNutriment = nutriments[Nutriment.SATURATED_FAT] if (saturatedFat != null && saturatedFatNutriment != null) { val saturatedFatLocalize = saturatedFat.getLocalize(requireContext()) levelItems += NutrientLevelItem( getString(R.string.txtSaturatedFat), - saturatedFatNutriment.displayStringFor100g, + saturatedFatNutriment.getPer100gDisplayString(), saturatedFatLocalize, saturatedFat.getImgRes() ) } - val sugarsNutriment = nutriments[Nutriments.SUGARS] + val sugarsNutriment = nutriments[Nutriment.SUGARS] if (sugars != null && sugarsNutriment != null) { levelItems += NutrientLevelItem( getString(R.string.txtSugars), - sugarsNutriment.displayStringFor100g, + sugarsNutriment.getPer100gDisplayString(), sugars.getLocalize(requireContext()), sugars.getImgRes(), ) } - val saltNutriment = nutriments[Nutriments.SALT] + val saltNutriment = nutriments[Nutriment.SALT] if (salt != null && saltNutriment != null) { val saltLocalize = salt.getLocalize(requireContext()) levelItems += NutrientLevelItem( getString(R.string.txtSalt), - saltNutriment.displayStringFor100g, + saltNutriment.getPer100gDisplayString(), saltLocalize, salt.getImgRes(), ) @@ -459,11 +464,11 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { binding.scoresLayout.visibility = View.GONE } - //to be sure that top of the product view is visible at start + // To be sure that top of the product view is visible at start binding.textNameProduct.requestFocus() binding.textNameProduct.clearFocus() - //Set refreshing animation to false after all processing is done + // Set refreshing animation to false after all processing is done super.refreshView(productState) } @@ -480,7 +485,10 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { binding.imageGrade.setImageResource(nutriScoreResource) binding.imageGrade.setOnClickListener { nutritionScoreUri?.let { uri -> - val customTabsIntent = CustomTabsHelper.getCustomTabsIntent(requireContext(), customTabActivityHelper.session) + val customTabsIntent = CustomTabsHelper.getCustomTabsIntent( + requireContext(), + customTabActivityHelper.session + ) CustomTabActivityHelper.openCustomTab(requireActivity(), customTabsIntent, uri, WebViewFallback()) } } @@ -510,9 +518,9 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { binding.listChips.removeAllViews() lifecycleScope.launch { - val lists = daoSession.listedProductDao.queryBuilder() - .where(ListedProductDao.Properties.Barcode.eq(product.code)) - .list() + val lists = daoSession.listedProductDao.list { + where(ListedProductDao.Properties.Barcode.eq(product.code)) + } if (lists.isNotEmpty()) { binding.actionAddToListButtonLayout.background = ResourcesCompat.getDrawable(resources, R.color.grey_300, null) @@ -520,16 +528,24 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { binding.listChips.visibility = View.VISIBLE } lists.forEach { list -> - val chip = Chip(context) - chip.text = list.listName - // set a random color to the chip's background, we want a dark background as our text color is white so we will limit our rgb to 180 - val chipColor = Color.rgb(Random.nextInt(180), Random.nextInt(180), Random.nextInt(180)) - chip.chipBackgroundColor = ColorStateList.valueOf(chipColor) - chip.setTextColor(Color.WHITE) + val chipColor = Color.rgb( + Random.nextInt(180), + Random.nextInt(180), + Random.nextInt(180) + ) - // open list when the user clicks on chip - chip.setOnClickListener { ProductListActivity.start(requireContext(), list.listId, list.listName) } + val chip = Chip(context).apply { + text = list.listName + + chipBackgroundColor = ColorStateList.valueOf(chipColor) + setTextColor(Color.WHITE) + + // open list when the user clicks on chip + setOnClickListener { + ProductListActivity.start(requireContext(), list.listId, list.listName) + } + } binding.listChips.addView(chip) } @@ -539,9 +555,9 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { private fun refreshStatesTagsPrompt() { //checks the product states_tags to determine which prompt to be shown val statesTags = product.statesTags - showCategoryPrompt = statesTags.contains("en:categories-to-be-completed") && !hasCategoryInsightQuestion - showNutrientPrompt = statesTags.contains("en:nutrition-facts-to-be-completed") && product.noNutritionData != "on" - showEcoScorePrompt = statesTags.contains("en:categories-completed") && (product.ecoscore.isNullOrEmpty() || product.ecoscore.equals("unknown", true)) + showCategoryPrompt = "en:categories-to-be-completed" in statesTags && !hasCategoryInsightQuestion + showNutrientPrompt = "en:nutrition-facts-to-be-completed" in statesTags && product.noNutritionData != "on" + showEcoScorePrompt = "en:categories-completed" in statesTags && (product.ecoscore.isNullOrEmpty() || product.ecoscore.equals("unknown", true)) Log.d(LOG_TAG, "Show category prompt: $showCategoryPrompt") Log.d(LOG_TAG, "Show nutrient prompt: $showNutrientPrompt") @@ -554,15 +570,12 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { binding.addNutriscorePrompt.visibility = View.VISIBLE when { showNutrientPrompt && showCategoryPrompt -> { - // showNutrientPrompt and showCategoryPrompt true binding.addNutriscorePrompt.text = getString(R.string.add_nutrient_category_prompt_text) } showNutrientPrompt -> { - // showNutrientPrompt true binding.addNutriscorePrompt.text = getString(R.string.add_nutrient_prompt_text) } showCategoryPrompt -> { - // showCategoryPrompt true binding.addNutriscorePrompt.text = getString(R.string.add_category_prompt_text) } else -> binding.addNutriscorePrompt.visibility = View.GONE @@ -631,9 +644,11 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { binding.productAllergenAlertLayout.visibility = View.VISIBLE return } - binding.productAllergenAlertText.text = SpannableStringBuilder(getString(R.string.product_allergen_prompt)) - .append("\n") - .append(data.allergens.joinToString(", ")) + binding.productAllergenAlertText.text = buildSpannedString { + append(getString(R.string.product_allergen_prompt)) + append("\n") + append(data.allergens.joinToString(", ")) + } binding.productAllergenAlertLayout.visibility = View.VISIBLE } @@ -643,9 +658,11 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { if (!question.isEmpty()) { productQuestion = question - binding.productQuestionText.text = SpannableStringBuilder(question.questionText) - .append("\n") - .append(question.value) + binding.productQuestionText.text = buildSpannedString { + append(question.questionText) + append("\n") + append(question.value) + } binding.productQuestionLayout.visibility = View.VISIBLE hasCategoryInsightQuestion = question.insightType == "category" } else { @@ -660,7 +677,7 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { } private fun onProductQuestionClick(productQuestion: Question) { - QuestionDialog(requireContext()).apply { + showQuestionDialog(requireContext()) { backgroundColor = R.color.colorPrimaryDark question = productQuestion.questionText value = productQuestion.value @@ -683,7 +700,7 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { onCancelListener = { it.dismiss() } - }.show() + } } @@ -731,19 +748,20 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { binding.labelsText.isClickable = true binding.labelsText.movementMethod = LinkMovementMethod.getInstance() - binding.labelsText.text = SpannableStringBuilder() - .bold { append(getString(R.string.txtLabels)) } - .apply { - state.data.map(::getLabelTag).forEachIndexed { i, el -> - append(el) - if (i != state.data.size) append(", ") - } + binding.labelsText.text = buildSpannedString { + bold { append(getString(R.string.txtLabels)) } + state.data.map(::getLabelTag).forEachIndexed { i, el -> + append(el) + if (i != state.data.size) append(", ") } + + } } is ProductInfoState.Loading -> { - binding.labelsText.text = SpannableStringBuilder() - .bold { append(getString(R.string.txtLabels)) } - .append(getString(R.string.txtLoading)) + binding.labelsText.text = buildSpannedString { + bold { append(getString(R.string.txtLabels)) } + append(getString(R.string.txtLoading)) + } } is ProductInfoState.Empty -> { @@ -758,9 +776,10 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { requireActivity().runOnUiThread { when (state) { is ProductInfoState.Loading -> { - binding.categoriesText.text = SpannableStringBuilder() - .bold { append(getString(R.string.txtCategories)) } - .append(getString(R.string.txtLoading)) + binding.categoriesText.text = buildSpannedString { + bold { append(getString(R.string.txtCategories)) } + append(getString(R.string.txtLoading)) + } } is ProductInfoState.Empty -> { binding.categoriesText.visibility = View.GONE @@ -808,7 +827,9 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { } } } - return SpannableStringBuilder().inSpans(clickableSpan) { append(label.name) } + return buildSpannedString { + inSpans(clickableSpan) { append(label.name) } + } } 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 7233ee88e875..c2ab3aded66d 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 @@ -209,9 +209,9 @@ class ContinuousScanActivity : BaseActivity(), IProductView { // A network error happened if (err is IOException) { hideAllViews() - val offlineSavedProduct = daoSession.offlineSavedProductDao!!.queryBuilder() - .where(OfflineSavedProductDao.Properties.Barcode.eq(barcode)) - .unique() + val offlineSavedProduct = daoSession.offlineSavedProductDao.unique { + where(OfflineSavedProductDao.Properties.Barcode.eq(barcode)) + } tryDisplayOffline(offlineSavedProduct, barcode, R.string.addProductOffline) binding.quickView.setOnClickListener { navigateToProductAddition(barcode) } } else { @@ -568,9 +568,9 @@ class ContinuousScanActivity : BaseActivity(), IProductView { // Prevent duplicate scans if (barcodeValue.isEmpty() || barcodeValue == lastBarcode) return - val invalidBarcode = daoSession.invalidBarcodeDao.queryBuilder() - .where(InvalidBarcodeDao.Properties.Barcode.eq(barcodeValue)) - .unique() + val invalidBarcode = daoSession.invalidBarcodeDao.unique { + where(InvalidBarcodeDao.Properties.Barcode.eq(barcodeValue)) + } // Scanned barcode is in the list of invalid barcodes, do nothing if (invalidBarcode != null) return @@ -827,9 +827,9 @@ class ContinuousScanActivity : BaseActivity(), IProductView { // 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() + val invalidBarcode = daoSession.invalidBarcodeDao.unique { + where(InvalidBarcodeDao.Properties.Barcode.eq(result.text)) + } // Scanned barcode is in the list of invalid barcodes, do nothing if (invalidBarcode != null) return diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scanhistory/ScanHistoryViewModel.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scanhistory/ScanHistoryViewModel.kt index ed70f7e01f2a..0d29160c5a40 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scanhistory/ScanHistoryViewModel.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scanhistory/ScanHistoryViewModel.kt @@ -17,6 +17,7 @@ import openfoodfacts.github.scrachx.openfood.models.HistoryProductDao import openfoodfacts.github.scrachx.openfood.network.OpenFoodAPIClient import openfoodfacts.github.scrachx.openfood.utils.LocaleManager import openfoodfacts.github.scrachx.openfood.utils.SortType +import openfoodfacts.github.scrachx.openfood.utils.list import javax.inject.Inject @HiltViewModel @@ -52,7 +53,7 @@ class ScanHistoryViewModel @Inject constructor( unorderedProductState.postValue(FetchProductsState.Loading) withContext(Dispatchers.IO) { - val barcodes = daoSession.historyProductDao.queryBuilder().list().map { it.barcode } + val barcodes = daoSession.historyProductDao.list().map { it.barcode } if (barcodes.isNotEmpty()) { try { client.getProductsByBarcode(barcodes) @@ -77,7 +78,7 @@ class ScanHistoryViewModel @Inject constructor( } } - val updatedProducts = daoSession.historyProductDao.queryBuilder().list() + val updatedProducts = daoSession.historyProductDao.list() unorderedProductState.postValue(FetchProductsState.Data(updatedProducts)) } } @@ -101,7 +102,7 @@ class ScanHistoryViewModel @Inject constructor( unorderedProductState.postValue(FetchProductsState.Error) } - val products = daoSession.historyProductDao.queryBuilder().list() + val products = daoSession.historyProductDao.list() unorderedProductState.postValue(FetchProductsState.Data(products)) } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/shared/adapters/NutrientLevelListAdapter.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/shared/adapters/NutrientLevelListAdapter.kt index 91049800b2a6..4f4b931ec8f5 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/shared/adapters/NutrientLevelListAdapter.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/shared/adapters/NutrientLevelListAdapter.kt @@ -1,7 +1,6 @@ package openfoodfacts.github.scrachx.openfood.features.shared.adapters import android.content.Context -import android.text.SpannableStringBuilder import android.view.LayoutInflater import android.view.View import android.view.View.NO_ID @@ -10,19 +9,22 @@ import android.widget.ImageView import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources import androidx.core.text.bold +import androidx.core.text.buildSpannedString import androidx.recyclerview.widget.RecyclerView import openfoodfacts.github.scrachx.openfood.R import openfoodfacts.github.scrachx.openfood.features.shared.adapters.NutrientLevelListAdapter.NutrientViewHolder import openfoodfacts.github.scrachx.openfood.models.NutrientLevelItem class NutrientLevelListAdapter( - private val context: Context, - private val nutrientLevelItems: List + private val context: Context, + private val nutrientLevelItems: List ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - NutrientViewHolder(LayoutInflater.from(parent.context) - .inflate(R.layout.nutrient_lvl_list_item, parent, false)) + NutrientViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.nutrient_lvl_list_item, parent, false) + ) override fun onBindViewHolder(holder: NutrientViewHolder, position: Int) { val (category, value, label, icon) = nutrientLevelItems[position] @@ -34,13 +36,12 @@ class NutrientLevelListAdapter( holder.imgIcon.visibility = View.VISIBLE } - holder.txtTitle.let { - it.text = "" - it.append(value) - it.append(" ") - it.append(SpannableStringBuilder().bold { append(category) }) - it.append("\n") - it.append(label) + holder.txtTitle.text = buildSpannedString { + append(value) + append(" ") + bold { append(category) } + append("\n") + append(label) } } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/shared/views/QuestionDialog.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/shared/views/QuestionDialog.kt index d5a0a2750386..28290594ec73 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/shared/views/QuestionDialog.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/shared/views/QuestionDialog.kt @@ -97,4 +97,10 @@ class QuestionDialog(context: Context) { fun dismiss() = dialog.dismiss() -} \ No newline at end of file +} + +inline fun showQuestionDialog(context: Context, dialogAction: QuestionDialog.() -> Unit) { + val dialog = QuestionDialog(context) + dialog.dialogAction() + dialog.show() +} diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/BoldNutrimentListItem.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/BoldNutrimentListItem.kt index 19f9ae6a7ab0..6cbf76eff9e2 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/BoldNutrimentListItem.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/BoldNutrimentListItem.kt @@ -1,7 +1,8 @@ package openfoodfacts.github.scrachx.openfood.models -import android.text.SpannableStringBuilder import androidx.core.text.bold +import androidx.core.text.buildSpannedString +import openfoodfacts.github.scrachx.openfood.utils.Utils.getRoundNumber /** * Header with bold values @@ -11,15 +12,29 @@ import androidx.core.text.bold * @param unit */ class BoldNutrimentListItem( - title: CharSequence, - value: CharSequence = "", - servingValue: CharSequence = "", - unit: CharSequence = "", - modifier: CharSequence = "" + title: CharSequence, + value: Float? = null, + servingValue: Float? = null, + unit: MeasurementUnit? = null, + modifier: Modifier? = null ) : NutrimentListItem( - SpannableStringBuilder().bold { append(title) }, - SpannableStringBuilder().bold { append(value) }, - SpannableStringBuilder().bold { append(servingValue) }, - SpannableStringBuilder().bold { append(unit) }, - SpannableStringBuilder().bold { append(modifier) } -) \ No newline at end of file + bold(title), + value?.let { bold(getRoundNumber(it)) }, + servingValue?.let { bold(getRoundNumber(it)) }, + unit?.let { bold(it.sym) }, + modifier?.let { bold(it.nullIfDefault()?.sym ?: "") } +) { + constructor( + title: CharSequence, + nutriment: ProductNutriments.ProductNutriment + ) : this( + title, + nutriment.per100gInUnit.value, + nutriment.perServingInUnit?.value, + nutriment.unit, + nutriment.modifier + ) +} + +private fun bold(msg: CharSequence) = buildSpannedString { bold { append(msg) } } + diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/MeasurementUnit.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/MeasurementUnit.kt new file mode 100644 index 000000000000..742653f1954b --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/MeasurementUnit.kt @@ -0,0 +1,42 @@ +package openfoodfacts.github.scrachx.openfood.models + +import openfoodfacts.github.scrachx.openfood.models.MeasurementUnit.* +import org.jetbrains.annotations.Contract + +/** + * @param sym The symbol of the unit. The symbol of GRAM is "g". + */ +enum class MeasurementUnit(val sym: String) { + ENERGY_KJ("kj"), + ENERGY_KCAL("kcal"), + UNIT_KILOGRAM("kg"), + UNIT_GRAM("g"), + UNIT_MILLIGRAM("mg"), + UNIT_MICROGRAM("µg"), + UNIT_DV("% DV"), + UNIT_LITER("l"), + UNIT_DECILITRE("dl"), + UNIT_CENTILITRE("cl"), + UNIT_MILLILITRE("ml"), + UNIT_OZ("oz"), + UNIT_IU("IU"); + + companion object { + fun findBySymbol(symbol: String) = values().find { it.sym == symbol } + } +} + +val DEFAULT_UNIT = UNIT_GRAM +val ENERGY_UNITS = listOf(ENERGY_KCAL, ENERGY_KJ) + +/** + * All the values given by the api are in gram. For all unit it's possible to convert back to th + * + * @receiver the initial unit + * @return if the unit is % DV, the api gives the value in g + */ +@Contract(pure = true) +internal fun MeasurementUnit.getRealUnit(): MeasurementUnit = when (this) { + UNIT_DV, UNIT_IU -> UNIT_GRAM + else -> this +} \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/Modifier.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/Modifier.kt new file mode 100644 index 000000000000..2f064fff9a75 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/Modifier.kt @@ -0,0 +1,26 @@ +package openfoodfacts.github.scrachx.openfood.models + +import openfoodfacts.github.scrachx.openfood.models.Modifier.* + +enum class Modifier(val sym: String) { + GREATER_THAN(">"), + EQUALS_TO("="), + LESS_THAN("<"); + + companion object { + fun findBySymbol(symbol: String) = values().find { it.sym == symbol } + } +} + +fun Modifier.nullIfDefault() = if (this != DEFAULT_MODIFIER) this else null + +inline fun Modifier.ifNotDefault(block: (Modifier) -> Unit) { + if (this != DEFAULT_MODIFIER) block(this) +} + +inline fun Modifier.ifDefault(block: (Modifier) -> Unit) { + if (this == DEFAULT_MODIFIER) block(this) +} + +val MODIFIERS = arrayOf(EQUALS_TO, LESS_THAN, GREATER_THAN) +val DEFAULT_MODIFIER = EQUALS_TO diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/Nutriment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/Nutriment.kt new file mode 100644 index 000000000000..9f9b3bb4be55 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/Nutriment.kt @@ -0,0 +1,184 @@ +package openfoodfacts.github.scrachx.openfood.models + +import openfoodfacts.github.scrachx.openfood.R +import openfoodfacts.github.scrachx.openfood.models.Nutriment.* + +enum class Nutriment(val key: String) { + ENERGY_KCAL("energy-kcal"), + ENERGY_KJ("energy-kj"), + ENERGY_FROM_FAT("energy-from-fat"), + FAT("fat"), + SATURATED_FAT("saturated-fat"), + BUTYRIC_ACID("butyric-acid"), + CAPROIC_ACID("caproic-acid"), + CAPRYLIC_ACID("caprylic-acid"), + CAPRIC_ACID("capric-acid"), + LAURIC_ACID("lauric-acid"), + MYRISTIC_ACID("myristic-acid"), + PALMITIC_ACID("palmitic-acid"), + STEARIC_ACID("stearic-acid"), + ARACHIDIC_ACID("arachidic-acid"), + BEHENIC_ACID("behenic-acid"), + LIGNOCERIC_ACID("lignoceric-acid"), + CEROTIC_ACID("cerotic-acid"), + MONTANIC_ACID("montanic-acid"), + MELISSIC_ACID("melissic-acid"), + MONOUNSATURATED_FAT("monounsaturated-fat"), + POLYUNSATURATED_FAT("polyunsaturated-fat"), + OMEGA_3_FAT("omega-3-fat"), + ALPHA_LINOLENIC_ACID("alpha-linolenic-acid"), + EICOSAPENTAENOIC_ACID("eicosapentaenoic-acid"), + DOCOSAHEXAENOIC_ACID("docosahexaenoic-acid"), + OMEGA_6_FAT("omega-6-fat"), + LINOLEIC_ACID("linoleic-acid"), + ARACHIDONIC_ACID("arachidonic-acid"), + GAMMA_LINOLENIC_ACID("gamma-linolenic-acid"), + DIHOMO_GAMMA_LINOLENIC_ACID("dihomo-gamma-linolenic-acid"), + OMEGA_9_FAT("omega-9-fat"), + OLEIC_ACID("oleic-acid"), + ELAIDIC_ACID("elaidic-acid"), + GONDOIC_ACID("gondoic-acid"), + MEAD_ACID("mead-acid"), + ERUCIC_ACID("erucic-acid"), + NERVONIC_ACID("nervonic-acid"), + TRANS_FAT("trans-fat"), + CHOLESTEROL("cholesterol"), + CARBOHYDRATES("carbohydrates"), + SUGARS("sugars"), + SUCROSE("sucrose"), + GLUCOSE("glucose"), + FRUCTOSE("fructose"), + LACTOSE("lactose"), + MALTOSE("maltose"), + MALTODEXTRINS("maltodextrins"), + STARCH("starch"), + POLYOLS("polyols"), + FIBER("fiber"), + PROTEINS("proteins"), + CASEIN("casein"), + SERUM_PROTEINS("serum-proteins"), + NUCLEOTIDES("nucleotides"), + SALT("salt"), + SODIUM("sodium"), + ALCOHOL("alcohol"), + VITAMIN_A("vitamin-a"), + BETA_CAROTENE("beta-carotene"), + VITAMIN_D("vitamin-d"), + VITAMIN_E("vitamin-e"), + VITAMIN_K("vitamin-k"), + VITAMIN_C("vitamin-c"), + VITAMIN_B1("vitamin-b1"), + VITAMIN_B2("vitamin-b2"), + VITAMIN_PP("vitamin-pp"), + VITAMIN_B6("vitamin-b6"), + VITAMIN_B9("vitamin-b9"), + WATER_HARDNESS("water-hardness"), + GLYCEMIC_INDEX("glycemic-index"), + NUTRITION_SCORE_UK("nutrition-score-uk"), + NUTRITION_SCORE_FR("nutrition-score-fr"), + CARBON_FOOTPRINT("carbon-footprint"), + CHLOROPHYL("chlorophyl"), + COCOA("cocoa"), + COLLAGEN_MEAT_PROTEIN_RATIO("collagen-meat-protein-ratio"), + FRUITS_VEGETABLES_NUTS("fruits-vegetables-nuts"), + PH("ph"), + TAURINE("taurine"), + CAFFEINE("caffeine"), + IODINE("iodine"), + MOLYBDENUM("molybdenum"), + CHROMIUM("chromium"), + SELENIUM("selenium"), + FLUORIDE("fluoride"), + MANGANESE("manganese"), + COPPER("copper"), + ZINC("zinc"), + VITAMIN_B12("vitamin-b12"), + BIOTIN("biotin"), + PANTOTHENIC_ACID("pantothenic-acid"), + SILICA("silica"), + BICARBONATE("bicarbonate"), + POTASSIUM("potassium"), + CHLORIDE("chloride"), + CALCIUM("calcium"), + PHOSPHORUS("phosphorus"), + IRON("iron"), + MAGNESIUM("magnesium"); + + companion object { + fun findbyKey(key: String) = values().find { it.key == key } + fun requireByKey(key: String) = findbyKey(key) ?: error("Cannot find nutriment with key '$key'") + } +} + +val MINERALS_MAP = mapOf( + SILICA to R.string.silica, + BICARBONATE to R.string.bicarbonate, + POTASSIUM to R.string.potassium, + CHLORIDE to R.string.chloride, + CALCIUM to R.string.calcium, + CALCIUM to R.string.calcium, + PHOSPHORUS to R.string.phosphorus, + IRON to R.string.iron, + MAGNESIUM to R.string.magnesium, + ZINC to R.string.zinc, + COPPER to R.string.copper, + MANGANESE to R.string.manganese, + FLUORIDE to R.string.fluoride, + SELENIUM to R.string.selenium, + CHROMIUM to R.string.chromium, + MOLYBDENUM to R.string.molybdenum, + IODINE to R.string.iodine, + CAFFEINE to R.string.caffeine, + TAURINE to R.string.taurine, + PH to R.string.ph, + FRUITS_VEGETABLES_NUTS to R.string.fruits_vegetables_nuts, + COLLAGEN_MEAT_PROTEIN_RATIO to R.string.collagen_meat_protein_ratio, + COCOA to R.string.cocoa, + CHLOROPHYL to R.string.chlorophyl + +) + + +val FAT_MAP = mapOf( + SATURATED_FAT to R.string.nutrition_satured_fat, + MONOUNSATURATED_FAT to R.string.nutrition_monounsaturatedFat, + POLYUNSATURATED_FAT to R.string.nutrition_polyunsaturatedFat, + OMEGA_3_FAT to R.string.nutrition_omega3, + OMEGA_6_FAT to R.string.nutrition_omega6, + OMEGA_9_FAT to R.string.nutrition_omega9, + TRANS_FAT to R.string.nutrition_trans_fat, + CHOLESTEROL to R.string.nutrition_cholesterol +) + +val CARBO_MAP = mapOf( + SUGARS to R.string.nutrition_sugars, + SUCROSE to R.string.nutrition_sucrose, + GLUCOSE to R.string.nutrition_glucose, + FRUCTOSE to R.string.nutrition_fructose, + LACTOSE to R.string.nutrition_lactose, + MALTOSE to R.string.nutrition_maltose, + MALTODEXTRINS to R.string.nutrition_maltodextrins +) + +val PROT_MAP = mapOf( + CASEIN to R.string.nutrition_casein, + SERUM_PROTEINS to R.string.nutrition_serum_proteins, + NUCLEOTIDES to R.string.nutrition_nucleotides +) + +val VITAMINS_MAP = mapOf( + VITAMIN_A to R.string.vitamin_a, + BETA_CAROTENE to R.string.vitamin_a, + VITAMIN_D to R.string.vitamin_d, + VITAMIN_E to R.string.vitamin_e, + VITAMIN_K to R.string.vitamin_k, + VITAMIN_C to R.string.vitamin_c, + VITAMIN_B1 to R.string.vitamin_b1, + VITAMIN_B2 to R.string.vitamin_b2, + VITAMIN_PP to R.string.vitamin_pp, + VITAMIN_B6 to R.string.vitamin_b6, + VITAMIN_B9 to R.string.vitamin_b9, + VITAMIN_B12 to R.string.vitamin_b12, + BIOTIN to R.string.biotin, + PANTOTHENIC_ACID to R.string.pantothenic_acid +) \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/NutrimentListItem.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/NutrimentListItem.kt index 02010cab5310..508bae7001d9 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/NutrimentListItem.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/NutrimentListItem.kt @@ -8,21 +8,54 @@ import openfoodfacts.github.scrachx.openfood.utils.Utils.getRoundNumber * @param title name of nutriment * @param value value of nutriment per 100g * @param servingValue value of nutriment per serving - * @param unit unit of nutriment - * @param modifier one of the following: "<", ">", or "~" + * @param unitStr unit of nutriment + * @param modifierStr one of the following: "<", ">", or "~" */ open class NutrimentListItem( - internal val title: CharSequence?, - value: CharSequence?, - servingValue: CharSequence?, - val unit: CharSequence?, - val modifier: CharSequence?, - val displayVolumeHeader: Boolean = false + internal val title: CharSequence?, + value: CharSequence?, + servingValue: CharSequence?, + val unitStr: CharSequence?, + val modifierStr: CharSequence?, + val displayVolumeHeader: Boolean = false ) { - val servingValue = if (servingValue.isNullOrBlank()) "" else getRoundNumber(servingValue) - val value = value?.let { getRoundNumber(it) } ?: "" + val servingValueStr = servingValue?.takeIf { it.isNotBlank() }?.let { getRoundNumber(it) } + val value = value?.let { getRoundNumber(it) } - constructor(volumeHeader: Boolean) : - this(null, null, null, null, null, volumeHeader) + constructor(volumeHeader: Boolean) : this( + null, + null, + null, + null, + null, + volumeHeader + ) + + constructor( + title: CharSequence?, + value: Float?, + servingValue: Float?, + unit: MeasurementUnit, + modifier: Modifier, + displayVolumeHeader: Boolean = false + ) : this( + title, + value?.let { getRoundNumber(it) }, + servingValue?.let { getRoundNumber(it) }, + unit.sym, + modifier.nullIfDefault()?.sym ?: "", + displayVolumeHeader + ) + + constructor( + title: CharSequence, + nutriment: ProductNutriments.ProductNutriment + ) : this( + title, + nutriment.per100gInUnit.value, + nutriment.perServingInUnit?.value, + nutriment.unit, + nutriment.modifier + ) } \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/Nutriments.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/Nutriments.kt deleted file mode 100644 index 394b763c890c..000000000000 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/Nutriments.kt +++ /dev/null @@ -1,339 +0,0 @@ -package openfoodfacts.github.scrachx.openfood.models - -import android.util.Log -import com.fasterxml.jackson.annotation.JsonAnyGetter -import com.fasterxml.jackson.annotation.JsonAnySetter -import com.fasterxml.jackson.annotation.JsonInclude -import openfoodfacts.github.scrachx.openfood.R -import openfoodfacts.github.scrachx.openfood.network.ApiFields -import openfoodfacts.github.scrachx.openfood.utils.DEFAULT_MODIFIER -import openfoodfacts.github.scrachx.openfood.utils.UnitUtils.convertFromGram -import openfoodfacts.github.scrachx.openfood.utils.UnitUtils.convertToGrams -import openfoodfacts.github.scrachx.openfood.utils.Utils.getRoundNumber -import openfoodfacts.github.scrachx.openfood.utils.getModifierNonDefault -import org.apache.commons.lang3.StringUtils -import org.jetbrains.annotations.Contract -import java.io.Serializable -import java.util.* - -/** - * JSON representation of the product nutriments entry - * - * @see [JSON Structure](http://en.wiki.openfoodfacts.org/API.JSON_interface) - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -class Nutriments : Serializable { - companion object { - private const val serialVersionUID = 1L - const val DEFAULT_UNIT = "g" - const val ENERGY_KCAL = "energy-kcal" - const val ENERGY_KJ = "energy-kj" - const val ENERGY_FROM_FAT = "energy-from-fat" - const val FAT = "fat" - const val SATURATED_FAT = "saturated-fat" - const val BUTYRIC_ACID = "butyric-acid" - const val CAPROIC_ACID = "caproic-acid" - const val CAPRYLIC_ACID = "caprylic-acid" - const val CAPRIC_ACID = "capric-acid" - const val LAURIC_ACID = "lauric-acid" - const val MYRISTIC_ACID = "myristic-acid" - const val PALMITIC_ACID = "palmitic-acid" - const val STEARIC_ACID = "stearic-acid" - const val ARACHIDIC_ACID = "arachidic-acid" - const val BEHENIC_ACID = "behenic-acid" - const val LIGNOCERIC_ACID = "lignoceric-acid" - const val CEROTIC_ACID = "cerotic-acid" - const val MONTANIC_ACID = "montanic-acid" - const val MELISSIC_ACID = "melissic-acid" - const val MONOUNSATURATED_FAT = "monounsaturated-fat" - const val POLYUNSATURATED_FAT = "polyunsaturated-fat" - const val OMEGA_3_FAT = "omega-3-fat" - const val ALPHA_LINOLENIC_ACID = "alpha-linolenic-acid" - const val EICOSAPENTAENOIC_ACID = "eicosapentaenoic-acid" - const val DOCOSAHEXAENOIC_ACID = "docosahexaenoic-acid" - const val OMEGA_6_FAT = "omega-6-fat" - const val LINOLEIC_ACID = "linoleic-acid" - const val ARACHIDONIC_ACID = "arachidonic-acid" - const val GAMMA_LINOLENIC_ACID = "gamma-linolenic-acid" - const val DIHOMO_GAMMA_LINOLENIC_ACID = "dihomo-gamma-linolenic-acid" - const val OMEGA_9_FAT = "omega-9-fat" - const val OLEIC_ACID = "oleic-acid" - const val ELAIDIC_ACID = "elaidic-acid" - const val GONDOIC_ACID = "gondoic-acid" - const val MEAD_ACID = "mead-acid" - const val ERUCIC_ACID = "erucic-acid" - const val NERVONIC_ACID = "nervonic-acid" - const val TRANS_FAT = "trans-fat" - const val CHOLESTEROL = "cholesterol" - const val CARBOHYDRATES = "carbohydrates" - const val SUGARS = "sugars" - const val SUCROSE = "sucrose" - const val GLUCOSE = "glucose" - const val FRUCTOSE = "fructose" - const val LACTOSE = "lactose" - const val MALTOSE = "maltose" - const val MALTODEXTRINS = "maltodextrins" - const val STARCH = "starch" - const val POLYOLS = "polyols" - const val FIBER = "fiber" - const val PROTEINS = "proteins" - const val CASEIN = "casein" - const val SERUM_PROTEINS = "serum-proteins" - const val NUCLEOTIDES = "nucleotides" - const val SALT = "salt" - const val SODIUM = "sodium" - const val ALCOHOL = "alcohol" - const val VITAMIN_A = "vitamin-a" - const val BETA_CAROTENE = "beta-carotene" - const val VITAMIN_D = "vitamin-d" - const val VITAMIN_E = "vitamin-e" - const val VITAMIN_K = "vitamin-k" - const val VITAMIN_C = "vitamin-c" - const val VITAMIN_B1 = "vitamin-b1" - const val VITAMIN_B2 = "vitamin-b2" - const val VITAMIN_PP = "vitamin-pp" - const val VITAMIN_B6 = "vitamin-b6" - const val VITAMIN_B9 = "vitamin-b9" - const val WATER_HARDNESS = "water-hardness" - const val GLYCEMIC_INDEX = "glycemic-index" - const val NUTRITION_SCORE_UK = "nutrition-score-uk" - const val NUTRITION_SCORE_FR = "nutrition-score-fr" - const val CARBON_FOOTPRINT = "carbon-footprint" - const val CHLOROPHYL = "chlorophyl" - const val COCOA = "cocoa" - const val COLLAGEN_MEAT_PROTEIN_RATIO = "collagen-meat-protein-ratio" - const val FRUITS_VEGETABLES_NUTS = "fruits-vegetables-nuts" - const val PH = "ph" - const val TAURINE = "taurine" - const val CAFFEINE = "caffeine" - const val IODINE = "iodine" - const val MOLYBDENUM = "molybdenum" - const val CHROMIUM = "chromium" - const val SELENIUM = "selenium" - const val FLUORIDE = "fluoride" - const val MANGANESE = "manganese" - const val COPPER = "copper" - const val ZINC = "zinc" - const val VITAMIN_B12 = "vitamin-b12" - const val BIOTIN = "biotin" - const val PANTOTHENIC_ACID = "pantothenic-acid" - const val SILICA = "silica" - const val BICARBONATE = "bicarbonate" - const val POTASSIUM = "potassium" - const val CHLORIDE = "chloride" - const val CALCIUM = "calcium" - const val PHOSPHORUS = "phosphorus" - const val IRON = "iron" - const val MAGNESIUM = "magnesium" - - @JvmField - val MINERALS_MAP = mapOf( - SILICA to R.string.silica, - BICARBONATE to R.string.bicarbonate, - POTASSIUM to R.string.potassium, - CHLORIDE to R.string.chloride, - CALCIUM to R.string.calcium, - CALCIUM to R.string.calcium, - PHOSPHORUS to R.string.phosphorus, - IRON to R.string.iron, - MAGNESIUM to R.string.magnesium, - ZINC to R.string.zinc, - COPPER to R.string.copper, - MANGANESE to R.string.manganese, - FLUORIDE to R.string.fluoride, - SELENIUM to R.string.selenium, - CHROMIUM to R.string.chromium, - MOLYBDENUM to R.string.molybdenum, - IODINE to R.string.iodine, - CAFFEINE to R.string.caffeine, - TAURINE to R.string.taurine, - PH to R.string.ph, - FRUITS_VEGETABLES_NUTS to R.string.fruits_vegetables_nuts, - COLLAGEN_MEAT_PROTEIN_RATIO to R.string.collagen_meat_protein_ratio, - COCOA to R.string.cocoa, - CHLOROPHYL to R.string.chlorophyl - - ) - - @JvmField - val FAT_MAP = mapOf( - SATURATED_FAT to R.string.nutrition_satured_fat, - MONOUNSATURATED_FAT to R.string.nutrition_monounsaturatedFat, - POLYUNSATURATED_FAT to R.string.nutrition_polyunsaturatedFat, - OMEGA_3_FAT to R.string.nutrition_omega3, - OMEGA_6_FAT to R.string.nutrition_omega6, - OMEGA_9_FAT to R.string.nutrition_omega9, - TRANS_FAT to R.string.nutrition_trans_fat, - CHOLESTEROL to R.string.nutrition_cholesterol - ) - - @JvmField - val CARBO_MAP = mapOf( - SUGARS to R.string.nutrition_sugars, - SUCROSE to R.string.nutrition_sucrose, - GLUCOSE to R.string.nutrition_glucose, - FRUCTOSE to R.string.nutrition_fructose, - LACTOSE to R.string.nutrition_lactose, - MALTOSE to R.string.nutrition_maltose, - MALTODEXTRINS to R.string.nutrition_maltodextrins - ) - - @JvmField - val PROT_MAP = mapOf( - CASEIN to R.string.nutrition_casein, - SERUM_PROTEINS to R.string.nutrition_serum_proteins, - NUCLEOTIDES to R.string.nutrition_nucleotides - ) - - @JvmField - val VITAMINS_MAP = mapOf( - VITAMIN_A to R.string.vitamin_a, - BETA_CAROTENE to R.string.vitamin_a, - VITAMIN_D to R.string.vitamin_d, - VITAMIN_E to R.string.vitamin_e, - VITAMIN_K to R.string.vitamin_k, - VITAMIN_C to R.string.vitamin_c, - VITAMIN_B1 to R.string.vitamin_b1, - VITAMIN_B2 to R.string.vitamin_b2, - VITAMIN_PP to R.string.vitamin_pp, - VITAMIN_B6 to R.string.vitamin_b6, - VITAMIN_B9 to R.string.vitamin_b9, - VITAMIN_B12 to R.string.vitamin_b12, - BIOTIN to R.string.biotin, - PANTOTHENIC_ACID to R.string.pantothenic_acid - ) - - } - - @get:JsonAnyGetter - val additionalProperties = HashMap() - - fun getEnergyKcalValue(isDataPerServing: Boolean) = - if (isDataPerServing) getServing(ENERGY_KCAL) - else get100g(ENERGY_KCAL) - - fun getEnergyKjValue(isDataPerServing: Boolean) = - if (isDataPerServing) getServing(ENERGY_KJ) - else get100g(ENERGY_KJ) - - @JsonAnySetter - fun setAdditionalProperty(name: String, value: Any?) { - additionalProperties[name] = value - if (VITAMINS_MAP.containsKey(name)) { - hasVitamins = true - } else if (MINERALS_MAP.containsKey(name)) { - hasMinerals = true - } - } - - operator fun get(nutrimentName: String) = if (nutrimentName.isEmpty() - || additionalProperties[nutrimentName] == null) null - else Nutriment( - nutrimentName, - additionalProperties[nutrimentName].toString(), - get100g(nutrimentName), - getServing(nutrimentName), - getUnit(nutrimentName), - getModifier(nutrimentName) - ) - - - /** - * @return [StringUtils.EMPTY] if there is no serving value for the specified nutriment - */ - private fun getServing(nutrimentName: String) = getAdditionalProperty(nutrimentName, ApiFields.Suffix.SERVING) - - /** - * @return [StringUtils.EMPTY] if there is no serving value for the specified nutriment - */ - private fun get100g(nutrimentName: String) = getAdditionalProperty(nutrimentName, ApiFields.Suffix.VALUE_100G) - - private fun getUnit(nutrimentName: String) = getAdditionalProperty(nutrimentName, ApiFields.Suffix.UNIT, DEFAULT_UNIT) - - private fun getModifier(nutrimentName: String) = getAdditionalProperty(nutrimentName, ApiFields.Suffix.MODIFIER, DEFAULT_MODIFIER) - - - private fun getAdditionalProperty(nutrimentName: String, suffix: String, defaultValue: String = StringUtils.EMPTY) = - additionalProperties[nutrimentName + suffix]?.toString() ?: defaultValue - - operator fun contains(nutrimentName: String) = additionalProperties.containsKey(nutrimentName) - - - var hasMinerals = false - private set - var hasVitamins = false - private set - - - class Nutriment internal constructor( - val key: String, - val name: String, - val for100g: String, - val forServing: String, - unit: String, - val modifier: String - ) { - fun getModifierIfNotDefault() = getModifierNonDefault(modifier) - val unit = getRealUnit(unit) - - val displayStringFor100g: String - get() { - val builder = StringBuilder() - getModifierNonDefault(modifier).takeIf { it.isNotEmpty() }?.let { builder.append(it).append(" ") } - return builder.append(getRoundNumber(for100gInUnits)).append(" ").append(unit).toString() - } - - /** - * Returns the amount of nutriment per 100g - * of product in the units stored in [Nutriment.unit] - */ - val for100gInUnits get() = getValueInUnits(this.for100g, this.unit) - - /** - * Returns the amount of nutriment per serving - * of product in the units stored in [Nutriment.unit] - */ - val forServingInUnits get() = getValueInUnits(this.forServing, this.unit) - - /** - * Calculates the nutriment value for a given amount of this product. For example, - * calling getForAnyValue(1, "kg") will give you the amount of this nutriment - * given 1 kg of the product. - * - * @param portion amount of this product used to calculate nutriment value - * @param portionUnit units in either "g", "kg", or "mg" to define userSetServing - * @return nutriment value for a given amount of this product - */ - fun getForPortion(portion: Float, portionUnit: String?): String { - val strValue = for100gInUnits - if (strValue.isEmpty() || strValue.contains("%")) return strValue - return try { - val valueFor100g = strValue.toFloat() - val portionInGram = convertToGrams(portion, portionUnit) - getRoundNumber(valueFor100g / 100 * portionInGram).toString() - } catch (e: NumberFormatException) { - Log.w(Nutriments::class.simpleName, "Can't parse value '$strValue'", e) - StringUtils.EMPTY - } - } - - companion object { - private fun getValueInUnits(valueInGramOrMl: String, unit: String): String = when { - valueInGramOrMl.isBlank() -> StringUtils.EMPTY - valueInGramOrMl.isEmpty() || unit == Units.UNIT_GRAM -> valueInGramOrMl - - else -> getRoundNumber(convertFromGram(valueInGramOrMl.toFloat(), unit)).toString() - } - - /** - * All the values given by the api are in gram. For all unit it's possible to convert back to th - * - * @param unit the initial unit - * @return if the unit is % DV, the api gives the value in g - */ - @Contract(pure = true) - private fun getRealUnit(unit: String) = if ("%" !in unit) unit else Units.UNIT_GRAM - } - } - -} diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/Product.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/Product.kt index 3c410733858d..51af7a170a4e 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/Product.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/Product.kt @@ -199,7 +199,7 @@ class Product : SearchProduct() { * The nutriments */ @JsonProperty(ApiFields.Keys.NUTRIMENTS) - var nutriments: Nutriments = Nutriments() + var nutriments: ProductNutriments = ProductNutriments() @JsonProperty(ApiFields.Keys.NUTRITION_DATA_PER) val nutritionDataPer: String? = null diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/ProductNutriments.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/ProductNutriments.kt new file mode 100644 index 000000000000..3f135424ce73 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/ProductNutriments.kt @@ -0,0 +1,146 @@ +package openfoodfacts.github.scrachx.openfood.models + +import com.fasterxml.jackson.annotation.JsonAnyGetter +import com.fasterxml.jackson.annotation.JsonAnySetter +import com.fasterxml.jackson.annotation.JsonInclude +import openfoodfacts.github.scrachx.openfood.models.MeasurementUnit.UNIT_GRAM +import openfoodfacts.github.scrachx.openfood.network.ApiFields.Suffix +import openfoodfacts.github.scrachx.openfood.utils.* +import java.io.Serializable +import java.util.* + +/** + * JSON representation of the product nutriments entry + * + * @see [JSON Structure](http://en.wiki.openfoodfacts.org/API.JSON_interface) + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +class ProductNutriments : Serializable { + companion object { + private const val serialVersionUID = 1L + } + + @get:JsonAnyGetter + val additionalProperties = HashMap() + + fun getEnergyKcalValue(perServing: Boolean) = + if (perServing) getValuePerServing(Nutriment.ENERGY_KCAL) + else getValuePer100g(Nutriment.ENERGY_KCAL) + + fun getEnergyKjValue(isDataPerServing: Boolean) = + if (isDataPerServing) getValuePerServing(Nutriment.ENERGY_KJ) + else getValuePer100g(Nutriment.ENERGY_KJ) + + @JsonAnySetter + fun setAdditionalProperty(name: String, value: Any?) { + additionalProperties[name] = value + if (VITAMINS_MAP.any { it.key.key == name }) { + hasVitamins = true + } else if (MINERALS_MAP.any { it.key.key == name }) { + hasMinerals = true + } + } + + operator fun get(nutriment: Nutriment): ProductNutriment? { + val name = getName(nutriment) + + return if (name == null) null + else ProductNutriment( + nutriment, + name, + getValuePer100g(nutriment)!!, + getValuePerServing(nutriment), + getUnit(nutriment), + getModifier(nutriment) + ) + } + + + private fun getName(nutriment: Nutriment) = getAdditionalProperty(nutriment) + + private fun getValuePerServing(nutriment: Nutriment) = + getAdditionalProperty(nutriment, Suffix.SERVING) + ?.takeIf { it.isNotEmpty() } + ?.toFloat() + ?.let { measure(it, UNIT_GRAM) } + + /** + * @return null if the product is missing a value for the specified nutriment. + */ + private fun getValuePer100g(nutriment: Nutriment) = + getAdditionalProperty(nutriment, Suffix.VALUE_100G) + ?.toFloat() + ?.let { measure(it, UNIT_GRAM) } + + private fun getUnit(nutriment: Nutriment) = getAdditionalProperty(nutriment, Suffix.UNIT) + ?.let { MeasurementUnit.findBySymbol(it) } ?: DEFAULT_UNIT + + private fun getModifier(nutriment: Nutriment) = getAdditionalProperty(nutriment, Suffix.MODIFIER) + ?.let { Modifier.findBySymbol(it) } ?: DEFAULT_MODIFIER + + + private fun getAdditionalProperty(nutrient: Nutriment, suffix: String = "") = + additionalProperties[nutrient.key + suffix]?.toString() + + operator fun contains(nutrimentName: String) = additionalProperties.containsKey(nutrimentName) + operator fun contains(nutriment: Nutriment) = additionalProperties.containsKey(nutriment.key) + + + var hasMinerals = false + private set + + var hasVitamins = false + private set + + + /** + * @param perServingInG can be null if the product serving size is not specified + */ + class ProductNutriment internal constructor( + val nutriment: Nutriment, + val name: String, + val per100gInG: Measurement, + val perServingInG: Measurement?, + unit: MeasurementUnit, + val modifier: Modifier + ) { + val unit = unit.getRealUnit() + + fun getPer100gDisplayString() = buildString { + modifier.ifNotDefault { + append(it.sym) + append(" ") + } + append(per100gInUnit.displayString()) + } + + /** + * Returns the amount of nutriment per 100g + * of product in the units stored in [ProductNutriment.unit] + */ + val per100gInUnit get() = per100gInG.convertTo(unit) + + /** + * Returns the amount of nutriment per serving + * of product in the units stored in [ProductNutriment.unit]. + * + * Can be null if [perServingInG] is null. + */ + val perServingInUnit get() = perServingInG?.convertTo(unit) + + /** + * Calculates the nutriment value for a given amount of this product. For example, + * calling getForAnyValue(1, "kg") will give you the amount of this nutriment + * given 1 kg of the product. + * + * @param portion amount of this product used to calculate nutriment value + * @param portionUnit units in either "g", "kg", or "mg" to define userSetServing + * @return nutriment value for a given amount of this product + */ + fun getForPortion(portion: Measurement) = Measurement( + value = per100gInUnit.value / 100 * portion.grams.value, + unit = per100gInUnit.unit + ) + } + +} diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/Units.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/Units.kt deleted file mode 100644 index e047fa0d2351..000000000000 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/Units.kt +++ /dev/null @@ -1,15 +0,0 @@ -package openfoodfacts.github.scrachx.openfood.models - -object Units { - const val ENERGY_KJ = "kj" - const val ENERGY_KCAL = "kcal" - const val UNIT_KILOGRAM = "kg" - const val UNIT_GRAM = "g" - const val UNIT_MILLIGRAM = "mg" - const val UNIT_MICROGRAM = "µg" - const val UNIT_DV = "% DV" - const val UNIT_LITER = "l" - const val UNIT_DECILITRE = "dl" - const val UNIT_CENTILITRE = "cl" - const val UNIT_MILLILITRE = "ml" -} \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/OpenFoodAPIClient.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/OpenFoodAPIClient.kt index fa92c5df0119..94bd16ff6d35 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/OpenFoodAPIClient.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/OpenFoodAPIClient.kt @@ -437,27 +437,24 @@ class OpenFoodAPIClient @Inject constructor( val MIME_TEXT: MediaType = MediaType.get("text/plain") const val PNG_EXT = ".png" - suspend fun HistoryProductDao.addToHistory(newProd: OfflineSavedProduct): Unit = withContext(IO) { - val savedProduct: HistoryProduct? = queryBuilder() - .where(HistoryProductDao.Properties.Barcode.eq(newProd.barcode)) - .unique() + suspend fun HistoryProductDao.addToHistory(prod: OfflineSavedProduct): Unit = withContext(IO) { + val savedProduct = unique { + where(HistoryProductDao.Properties.Barcode.eq(prod.barcode)) + } - val details = newProd.productDetails + val details = prod.productDetails val hp = HistoryProduct( - newProd.name, + prod.name, details[Keys.ADD_BRANDS], - newProd.imageFrontLocalUrl, - newProd.barcode, + prod.imageFrontLocalUrl, + prod.barcode, details[Keys.QUANTITY], details[Keys.NUTRITION_GRADE_FR], details[Keys.ECOSCORE], - details[Keys.NOVA_GROUPS - ] + details[Keys.NOVA_GROUPS] ) if (savedProduct != null) hp.id = savedProduct.id insertOrReplace(hp) - - return@withContext } /** @@ -466,9 +463,9 @@ class OpenFoodAPIClient @Inject constructor( suspend fun HistoryProductDao.addToHistory(product: Product, language: String): Unit = withContext(IO) { - val savedProduct: HistoryProduct? = queryBuilder() - .where(HistoryProductDao.Properties.Barcode.eq(product.code)) - .unique() + val savedProduct: HistoryProduct? = unique { + where(HistoryProductDao.Properties.Barcode.eq(product.code)) + } val hp = HistoryProduct( product.productName, @@ -494,15 +491,12 @@ class OpenFoodAPIClient @Inject constructor( private fun getUserInfo(): Map { val imgMap = mutableMapOf() - val settings = context.getLoginPreferences() settings.getString("user", null)?.let { imgMap[Keys.USER_COMMENT] = getCommentToUpload(it) if (it.isNotBlank()) imgMap[Keys.USER_ID] = it } - settings.getString("pass", null)?.let { - if (it.isNotBlank()) imgMap[Keys.USER_PASS] = it - } + return imgMap } @@ -511,27 +505,27 @@ class OpenFoodAPIClient @Inject constructor( * * @param login the username */ - fun getCommentToUpload(login: String? = null): String { - val comment = when (BuildConfig.FLAVOR) { - OBF -> StringBuilder("Official Open Beauty Facts Android app") - OPFF -> StringBuilder("Official Open Pet Food Facts Android app") - OPF -> StringBuilder("Official Open Products Facts Android app") - OFF -> StringBuilder("Official Open Food Facts Android app") - else -> StringBuilder("Official Open Food Facts Android app") - } - comment.append(" ").append(context.getVersionName()) + fun getCommentToUpload(login: String? = null) = buildString { + append( + when (BuildConfig.FLAVOR) { + OBF -> StringBuilder("Official Open Beauty Facts Android app") + OPFF -> StringBuilder("Official Open Pet Food Facts Android app") + OPF -> StringBuilder("Official Open Products Facts Android app") + OFF -> StringBuilder("Official Open Food Facts Android app") + else -> StringBuilder("Official Open Food Facts Android app") + } + ) + append(" ") + append(context.getVersionName()) if (login.isNullOrEmpty()) { - comment.append(" (Added by ").append(InstallationUtils.id(context)).append(")") + append(" (Added by ").append(InstallationUtils.id(context)).append(")") } - return comment.toString() } val localeProductNameField get() = "product_name_${localeManager.getLanguage()}" private val fieldsToFetchFacets - get() = Keys.PRODUCT_SEARCH_FIELDS.toMutableList().apply { - add(localeProductNameField) - }.joinToString(",") + get() = (Keys.PRODUCT_SEARCH_FIELDS + localeProductNameField).joinToString(",") suspend fun getEMBCodeSuggestions(term: String) = rawApi.getSuggestions("emb_codes", term) suspend fun getPeriodAfterOpeningSuggestions(term: String) = rawApi.getSuggestions("periods_after_opening", term) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/repositories/TaxonomiesManager.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/repositories/TaxonomiesManager.kt index a15d6f5742bb..d32d71c63cd0 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/repositories/TaxonomiesManager.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/repositories/TaxonomiesManager.kt @@ -89,9 +89,10 @@ class TaxonomiesManager @Inject constructor( ) = withContext(IO) { val lastMod = getLastModifiedDateFromServer(taxonomy) - return@withContext if (lastMod != TAXONOMY_NO_INTERNET) - taxonomy.load(productRepository, lastMod).also { logDownload(taxonomy) } - else emptyList() + if (lastMod != TAXONOMY_NO_INTERNET) { + taxonomy.load(productRepository, lastMod) + .also { logDownload(taxonomy) } + } else emptyList() } private suspend fun checkAndDownloadIfNewer( diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/DaoUtils.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/DaoUtils.kt index da31ec776a50..51894d7dd17b 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/DaoUtils.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/DaoUtils.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import openfoodfacts.github.scrachx.openfood.repositories.Taxonomy import org.greenrobot.greendao.AbstractDao +import org.greenrobot.greendao.query.QueryBuilder import org.jetbrains.annotations.Contract /** @@ -21,4 +22,18 @@ fun logDownload(taxonomy: Taxonomy) { "${Taxonomy::class.simpleName}", "Refreshed taxonomy '${taxonomy::class.simpleName}' from server" ) +} + +inline fun AbstractDao.build(builderAction: QueryBuilder.() -> Unit): QueryBuilder { + val builder = queryBuilder() + builder.builderAction() + return builder +} + +inline fun AbstractDao.unique(builderAction: QueryBuilder.() -> Unit): T? { + return build(builderAction).unique() +} + +inline fun AbstractDao.list(builderAction: QueryBuilder.() -> Unit = {}): List { + return build(builderAction).list() } \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/DateExtensions.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/DateExtensions.kt index 4a6fae42ed14..9f8524e81f38 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/DateExtensions.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/DateExtensions.kt @@ -1,5 +1,3 @@ -@file:JvmName("DateExtensions") - package openfoodfacts.github.scrachx.openfood.utils import android.content.Context diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/EditTextUtils.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/EditTextUtils.kt index 892586485aae..87b6bbd63c44 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/EditTextUtils.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/EditTextUtils.kt @@ -3,7 +3,7 @@ package openfoodfacts.github.scrachx.openfood.utils import android.widget.EditText import com.hootsuite.nachos.NachoTextView import openfoodfacts.github.scrachx.openfood.features.shared.views.CustomValidatingEditTextView -import openfoodfacts.github.scrachx.openfood.models.Nutriments +import openfoodfacts.github.scrachx.openfood.models.Nutriment fun EditText?.getContent() = this?.text?.toString() @@ -18,14 +18,19 @@ fun EditText.isContentDifferent(toCompare: String?): Boolean { || !fieldValue.isNullOrEmpty() && fieldValue == toCompare) } +fun EditText.isValueDifferent(toCompare: Float?): Boolean { + val fieldValue = getContent()?.toFloatOrNull() + return fieldValue != toCompare +} + /** * @return true if the edit text string value is empty or null */ fun EditText?.isEmpty() = this.getContent().isNullOrEmpty() fun NachoTextView.areChipsEquals(toCompare: List) = - chipValues.toTypedArray() contentEquals toCompare.toTypedArray() + chipValues.toTypedArray() contentEquals toCompare.toTypedArray() fun NachoTextView.areChipsDifferent(toCompare: List) = !areChipsEquals(toCompare) -fun CustomValidatingEditTextView.hasUnit() = entryName != Nutriments.PH && entryName != Nutriments.ALCOHOL +fun CustomValidatingEditTextView.hasUnit() = entryName != Nutriment.PH.key && entryName != Nutriment.ALCOHOL.key diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Modifier.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Modifier.kt deleted file mode 100644 index cf30d9f28030..000000000000 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Modifier.kt +++ /dev/null @@ -1,8 +0,0 @@ -package openfoodfacts.github.scrachx.openfood.utils - -const val GREATER_THAN = ">" -const val EQUALS_TO = "=" -const val LESS_THAN = "<" -const val DEFAULT_MODIFIER = EQUALS_TO -@JvmField -val MODIFIERS = arrayOf(EQUALS_TO, LESS_THAN, GREATER_THAN) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/OfflineProductService.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/OfflineProductService.kt index 8a972b08529d..0e497acc9d2d 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/OfflineProductService.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/OfflineProductService.kt @@ -57,10 +57,11 @@ class OfflineProductService @Inject constructor( else getOfflineProductsNotSynced().isNotEmpty() } - fun getOfflineProductByBarcode(barcode: String): OfflineSavedProduct? = - daoSession.offlineSavedProductDao.queryBuilder() - .where(OfflineSavedProductDao.Properties.Barcode.eq(barcode)) - .unique() + fun getOfflineProductByBarcode(barcode: String): OfflineSavedProduct? { + return daoSession.offlineSavedProductDao.unique { + where(OfflineSavedProductDao.Properties.Barcode.eq(barcode)) + } + } /** * Performs network call and uploads the product to the server. @@ -156,18 +157,21 @@ class OfflineProductService @Inject constructor( } } - private fun getOfflineProducts() = - daoSession.offlineSavedProductDao.queryBuilder() - .where(OfflineSavedProductDao.Properties.Barcode.isNotNull) - .where(OfflineSavedProductDao.Properties.Barcode.notEq("")) - .list() - - private fun getOfflineProductsNotSynced() = - daoSession.offlineSavedProductDao.queryBuilder() - .where(OfflineSavedProductDao.Properties.Barcode.isNotNull) - .where(OfflineSavedProductDao.Properties.Barcode.notEq("")) - .where(OfflineSavedProductDao.Properties.IsDataUploaded.notEq(true)) - .list() + private fun getOfflineProducts(): List { + return daoSession.offlineSavedProductDao.list { + where(OfflineSavedProductDao.Properties.Barcode.isNotNull) + where(OfflineSavedProductDao.Properties.Barcode.notEq("")) + } + } + + private fun getOfflineProductsNotSynced(): List { + return daoSession.offlineSavedProductDao.list { + where(OfflineSavedProductDao.Properties.Barcode.isNotNull) + where(OfflineSavedProductDao.Properties.Barcode.notEq("")) + where(OfflineSavedProductDao.Properties.IsDataUploaded.notEq(true)) + } + } + private fun ProductImageField.imageType() = when (this) { FRONT -> "front" diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/ProductExt.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/ProductExt.kt index f0ab52e6e837..3aff898e42e5 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/ProductExt.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/ProductExt.kt @@ -15,7 +15,7 @@ suspend fun OfflineSavedProduct.toState(client: OpenFoodAPIClient): ProductState suspend fun OfflineSavedProduct.toOnlineProduct(client: OpenFoodAPIClient) = toState(client).product -fun Product.isPerServingInLiter() = servingSize?.contains(Units.UNIT_LITER, true) +fun Product.isPerServingInLiter() = servingSize?.contains(MeasurementUnit.UNIT_LITER.sym, true) fun SearchProduct.getProductBrandsQuantityDetails() = StringBuilder().apply { brands?.takeIf { it.isNotEmpty() }?.let { brandStr -> @@ -27,8 +27,7 @@ fun SearchProduct.getProductBrandsQuantityDetails() = StringBuilder().apply { } }.toString() -suspend fun SearchProduct.toProduct(client: OpenFoodAPIClient): Product? = - client.getProductStateFull(this.code).product +suspend fun SearchProduct.toProduct(client: OpenFoodAPIClient): Product? = client.getProductStateFull(this.code).product @DrawableRes private fun getResourceFromEcoscore(ecoscore: String?) = when (ecoscore?.lowercase(Locale.ROOT)) { diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/QuantityParserUtil.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/QuantityParserUtil.kt index af0a47321526..523fffecd77c 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/QuantityParserUtil.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/QuantityParserUtil.kt @@ -4,31 +4,22 @@ import android.util.Log import android.widget.Spinner import android.widget.TextView import openfoodfacts.github.scrachx.openfood.features.shared.views.CustomValidatingEditTextView +import openfoodfacts.github.scrachx.openfood.models.MODIFIERS +import openfoodfacts.github.scrachx.openfood.models.Modifier -fun CustomValidatingEditTextView.isModifierEqualsToGreaterThan() = modSpinner!!.isModifierEqualsToGreaterThan() +fun CustomValidatingEditTextView.isModifierEqualsToGreaterThan() = modSpinner!!.modifier == Modifier.GREATER_THAN -fun Spinner.isModifierEqualsToGreaterThan() = GREATER_THAN == MODIFIERS[selectedItemPosition] +val Spinner.modifier get() = MODIFIERS[selectedItemPosition] fun TextView.isBlank() = text.toString().isBlank() fun TextView.isNotBlank() = !isBlank() -/** - * Retrieve the float value from strings like "> 1.03" - * - * @param initText value to parse - * @return the float value or null if not correct - */ -fun getFloatValue(initText: String?) = getDoubleValue(initText)?.toFloat() - /** * @return the float value or null if not correct * @see .getFloatValue */ -fun TextView.getFloatValue(): Float? { - if (text == null) return null - return getFloatValue(text.toString()) -} +fun TextView.getFloatValue(): Float? = getFloatValue(text?.toString()) fun TextView.getFloatValueOr(defaultValue: Float) = getFloatValue() ?: defaultValue @@ -38,13 +29,22 @@ fun TextView.getFloatValueOr(defaultValue: Float) = getFloatValue() ?: defaultVa * @param initText value to parse * @return the float value or null if not correct */ -fun getDoubleValue(initText: String?) = if (initText.isNullOrBlank()) null +fun getFloatValue(initText: String?) = getDoubleValue(initText)?.toFloat() + +/** + * Retrieve the float value from strings like "1.03" or "1,03" + * + * @param text value to parse + * @return the float value or null if not correct + */ +fun getDoubleValue(text: String?) = if (text.isNullOrBlank()) null else try { - initText.trim().replace(",", ".").toDouble() + text.trim().replace(",", ".").toDouble() } catch (ex: NumberFormatException) { - Log.w("Utils", "Can't parse text '$initText'") + Log.w("Utils", "Can't parse text '$text'") null } + /** * @return the float value or null if not correct * @see [getFloatValue] diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/UnitUtils.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/UnitUtils.kt index 49ae8445441f..43c283d115fa 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/UnitUtils.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/UnitUtils.kt @@ -1,95 +1,56 @@ package openfoodfacts.github.scrachx.openfood.utils -import openfoodfacts.github.scrachx.openfood.models.Units -import openfoodfacts.github.scrachx.openfood.models.Units.ENERGY_KCAL -import openfoodfacts.github.scrachx.openfood.models.Units.ENERGY_KJ -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_CENTILITRE -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_DECILITRE -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_KILOGRAM -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_LITER -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_MICROGRAM -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_MILLIGRAM -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_MILLILITRE +import openfoodfacts.github.scrachx.openfood.models.MeasurementUnit +import openfoodfacts.github.scrachx.openfood.models.MeasurementUnit.* import openfoodfacts.github.scrachx.openfood.utils.Utils.getRoundNumber -import java.util.* -import java.util.regex.Pattern -object UnitUtils { - const val UNIT_IU = "IU" - private const val SALT_PER_SODIUM = 2.54 - private const val KJ_PER_KCAL = 4.184f - private const val OZ_PER_L = 33.814f - /** - * Converts a give quantity's unit to kcal - * - * @param value The value to be converted - * @param originalUnit [Units.ENERGY_KCAL] or [Units.ENERGY_KJ] - * @return return the converted value - */ - fun convertToKiloCalories(value: Int, originalUnit: String) = when { - originalUnit.equals(ENERGY_KJ, true) -> (value / KJ_PER_KCAL).toInt() - originalUnit.equals(ENERGY_KCAL, true) -> value - else -> throw IllegalArgumentException("energyUnit is neither Units.ENERGY_KCAL nor Units.ENERGY_KJ") - } +data class Measurement( + val value: Float, + val unit: MeasurementUnit +) - fun convertToGrams(value: Float, unit: String?) = convertToGrams(value.toDouble(), unit).toFloat() +fun measure(value: Float, unit: MeasurementUnit) = Measurement(value, unit) - /** - * Converts a given quantity's unitOfValue to grams. - * - * @param value The value to be converted - * @param unitOfValue must be a unit from [Units] - * @return return the converted value - */ - fun convertToGrams(value: Double, unitOfValue: String?) = when { - UNIT_MILLIGRAM.equals(unitOfValue, true) -> value / 1000 - UNIT_MICROGRAM.equals(unitOfValue, true) -> value / 1000000 - UNIT_KILOGRAM.equals(unitOfValue, true) -> value * 1000 - UNIT_LITER.equals(unitOfValue, true) -> value * 1000 - UNIT_DECILITRE.equals(unitOfValue, true) -> value * 100 - UNIT_CENTILITRE.equals(unitOfValue, true) -> value * 10 - UNIT_MILLILITRE.equals(unitOfValue, true) -> value +/** + * Converts a given measurement to grams. + * + * @receiver the measurement to convert. + * @return the converted measurement. + */ +fun Measurement.convertToGrams() = Measurement( + when (unit) { + UNIT_MILLIGRAM -> value / 1000f + UNIT_MICROGRAM -> value / 1000000f + UNIT_KILOGRAM -> value * 1000f + UNIT_LITER -> value * 1000f + UNIT_DECILITRE -> value * 100f + UNIT_CENTILITRE -> value * 10f + UNIT_MILLILITRE, UNIT_GRAM -> value //TODO : what about % DV and IU else -> value - } - - fun convertFromGram(valueInGramOrMl: Float, targetUnit: String?) = - convertFromGram(valueInGramOrMl.toDouble(), targetUnit).toFloat() - - fun convertFromGram(valueInGramOrMl: Double, targetUnit: String?) = when (targetUnit) { - UNIT_KILOGRAM, UNIT_LITER -> valueInGramOrMl / 1000 - UNIT_MILLIGRAM -> valueInGramOrMl * 1000 - UNIT_MICROGRAM -> valueInGramOrMl * 1000000 - UNIT_DECILITRE -> valueInGramOrMl / 100 - UNIT_CENTILITRE -> valueInGramOrMl / 10 - else -> valueInGramOrMl - } - - fun saltToSodium(saltValue: Double) = saltValue / SALT_PER_SODIUM - - fun sodiumToSalt(sodiumValue: Double) = sodiumValue * SALT_PER_SODIUM + }, UNIT_GRAM +) +object UnitUtils { /** * Function which returns volume in oz if parameter is in cl, ml, or l * * @param servingSize value to transform * @return volume in oz if servingSize is a volume parameter else return the the parameter unchanged */ - fun getServingInOz(servingSize: String, locale: Locale = Locale.getDefault()): String { - val regex = Pattern.compile("(\\d+(?:\\.\\d+)?)") - val matcher = regex.matcher(servingSize) - matcher.find() - var value = matcher.group(1)!!.toFloat() + fun getServingInOz(servingSize: String): Measurement? { + val match = Regex("(\\d+(?:[.,]\\d+)?)").find(servingSize) ?: return null + var value = match.value.toFloat() value *= when { servingSize.contains("ml", true) -> OZ_PER_L / 1000 servingSize.contains("cl", true) -> OZ_PER_L / 100 servingSize.contains("l", true) -> OZ_PER_L servingSize.contains("oz", true) -> 1f //TODO: HANDLE OTHER CASES, NOT L NOR OZ NOR ML NOR CL - else -> return servingSize + else -> return null } - return "${getRoundNumber(value, locale)} oz" + return Measurement(value, UNIT_OZ) } /** @@ -98,17 +59,66 @@ object UnitUtils { * @param servingSize the value to transform: not null * @return volume in liter if input parameter is a volume parameter else return the parameter unchanged */ - fun getServingInL(servingSize: String, locale: Locale = Locale.getDefault()): String { - val regex = Pattern.compile("(\\d+(?:\\.\\d+)?)") - val matcher = regex.matcher(servingSize) - matcher.find() - var value = matcher.group(1)!!.toFloat() + fun getServingInL(servingSize: String): Measurement? { + val match = Regex("(\\d+(?:\\.\\d+)?)").find(servingSize) ?: return null + var value = match.value.toFloat() value /= when { servingSize.contains("oz", true) -> OZ_PER_L servingSize.contains("l", true) -> 1f // TODO: HANDLE OTHER CASES eg. not in L nor oz - else -> return servingSize + else -> return null } - return "${getRoundNumber(value, locale)} l" + return Measurement(value, UNIT_LITER) } -} \ No newline at end of file +} + + +private const val KJ_PER_KCAL = 4.184f +private const val SALT_PER_SODIUM = 2.54f +private const val OZ_PER_L = 33.814f + +val Measurement.grams get() = convertToGrams() + +fun Measurement.convertEnergyTo(targetUnit: MeasurementUnit): Measurement = when { + unit == targetUnit -> this + unit == ENERGY_KJ && targetUnit == ENERGY_KCAL -> Measurement(value / KJ_PER_KCAL, targetUnit) + unit == ENERGY_KCAL && targetUnit == ENERGY_KJ -> Measurement(value * KJ_PER_KCAL, targetUnit) + else -> throw IllegalArgumentException("Cannot convert from/to NON energy. Use convertTo instead.") +} + +fun Measurement.convertTo(unit: MeasurementUnit): Measurement { + // First convert to grams/ml + val value = grams.value + + // Then to desired unit + return Measurement( + when (unit) { + ENERGY_KJ, ENERGY_KCAL -> + throw IllegalArgumentException("Cannot convert from/to energy. Use convertEnergyTo instead.") + UNIT_DV, UNIT_IU -> + throw IllegalArgumentException("Cannot convert to DV or IU") + UNIT_OZ -> value / 1000f * OZ_PER_L + + UNIT_MICROGRAM -> value * 1000000f + UNIT_MILLIGRAM -> value * 1000f + + UNIT_GRAM, UNIT_MILLILITRE -> value // 1g of water == 1ml + + UNIT_CENTILITRE -> value / 10f + UNIT_DECILITRE -> value / 100f + UNIT_KILOGRAM, UNIT_LITER -> value / 1000f + }, unit + ) +} + +fun Measurement.displayString() = buildString { + append(getRoundNumber(value)) + append(" ") + append(unit.sym) +} + +fun Float.saltToSodium() = this / SALT_PER_SODIUM +fun Float.sodiumToSalt() = this * SALT_PER_SODIUM + +fun Measurement.saltToSodium() = Measurement(value.saltToSodium(), unit) +fun Measurement.sodiumToSalt() = Measurement(value.sodiumToSalt(), unit) \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Utils.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Utils.kt index c9c042b7e255..5330bc4a80fd 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Utils.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Utils.kt @@ -31,7 +31,6 @@ import android.net.NetworkCapabilities import android.net.Uri import android.os.Build import android.os.Environment -import android.text.SpannableStringBuilder import android.text.style.ClickableSpan import android.util.Log import android.view.View @@ -44,6 +43,7 @@ import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils import androidx.core.net.toUri +import androidx.core.text.buildSpannedString import androidx.core.text.inSpans import androidx.core.view.children import androidx.work.* @@ -53,7 +53,6 @@ import com.squareup.picasso.RequestCreator import openfoodfacts.github.scrachx.openfood.BuildConfig import openfoodfacts.github.scrachx.openfood.R import openfoodfacts.github.scrachx.openfood.features.scan.ContinuousScanActivity -import openfoodfacts.github.scrachx.openfood.features.search.ProductSearchActivity.Companion.start import openfoodfacts.github.scrachx.openfood.jobs.ImagesUploaderWorker import openfoodfacts.github.scrachx.openfood.network.ApiFields import org.apache.commons.validator.routines.checkdigit.EAN13CheckDigit @@ -62,6 +61,7 @@ import java.text.DecimalFormat import java.text.DecimalFormatSymbols import java.util.* import java.util.concurrent.TimeUnit +import openfoodfacts.github.scrachx.openfood.features.search.ProductSearchActivity.Companion.start as startSearch private const val LOG_TAG_COMPRESS = "COMPRESS_IMAGE" @@ -142,11 +142,14 @@ object Utils { ?: "?" } - /** - * @see Utils.getRoundNumber - */ - fun getRoundNumber(value: Float, locale: Locale = Locale.getDefault()) = getRoundNumber(value.toString(), locale) - fun getRoundNumber(value: Double, locale: Locale = Locale.getDefault()) = getRoundNumber(value.toString(), locale) + fun getRoundNumber(value: Float, locale: Locale = Locale.getDefault()) = + getRoundNumber(value.toString(), locale) + + fun getRoundNumber(value: Double, locale: Locale = Locale.getDefault()) = + getRoundNumber(value.toString(), locale) + + fun getRoundNumber(measurement: Measurement, locale: Locale = Locale.getDefault()) = + getRoundNumber(measurement.value, locale) /** * Schedules job to download when network is available @@ -224,8 +227,8 @@ fun isAllGranted(grantResults: IntArray) = fun buildSignInDialog( context: Context, - onPositive: (DialogInterface, Int) -> Unit = { _, _ -> }, - onNegative: (DialogInterface, Int) -> Unit = { _, _ -> } + onPositive: (DialogInterface, Int) -> Unit = { d, _ -> d.dismiss() }, + onNegative: (DialogInterface, Int) -> Unit = { d, _ -> d.dismiss() } ): MaterialAlertDialogBuilder = MaterialAlertDialogBuilder(context) .setTitle(R.string.sign_in_to_edit) .setPositiveButton(R.string.txtSignIn) { d, i -> onPositive(d, i) } @@ -274,7 +277,6 @@ const val SPACE = " " const val MY_PERMISSIONS_REQUEST_CAMERA = 1 const val MY_PERMISSIONS_REQUEST_STORAGE = 2 -fun getModifierNonDefault(modifier: String) = if (modifier != DEFAULT_MODIFIER) modifier else "" private val LOG_TAG = Utils::class.simpleName!! @@ -319,9 +321,14 @@ fun getSearchLinkText( text: String, type: SearchType, activityToStart: Activity -): CharSequence = SpannableStringBuilder().inSpans(object : ClickableSpan() { - override fun onClick(view: View) = start(activityToStart, type, text) -}) { append(text) } +): CharSequence { + val clickable = object : ClickableSpan() { + override fun onClick(view: View) = startSearch(activityToStart, type, text) + } + return buildSpannedString { + inSpans(clickable) { append(text) } + } +} internal fun RequestCreator.into(target: ImageView, onSuccess: () -> Unit) { diff --git a/app/src/test/java/openfoodfacts/github/scrachx/openfood/models/NutrimentsTest.kt b/app/src/test/java/openfoodfacts/github/scrachx/openfood/models/ProductNutrimentsTest.kt similarity index 69% rename from app/src/test/java/openfoodfacts/github/scrachx/openfood/models/NutrimentsTest.kt rename to app/src/test/java/openfoodfacts/github/scrachx/openfood/models/ProductNutrimentsTest.kt index cbbb8d6d2208..c05c75975902 100644 --- a/app/src/test/java/openfoodfacts/github/scrachx/openfood/models/NutrimentsTest.kt +++ b/app/src/test/java/openfoodfacts/github/scrachx/openfood/models/ProductNutrimentsTest.kt @@ -1,41 +1,38 @@ package openfoodfacts.github.scrachx.openfood.models import com.google.common.truth.Truth.assertThat -import openfoodfacts.github.scrachx.openfood.models.Nutriments.Companion.SILICA -import openfoodfacts.github.scrachx.openfood.models.Nutriments.Companion.VITAMIN_A -import openfoodfacts.github.scrachx.openfood.models.Nutriments.Companion.VITAMIN_B1 -import openfoodfacts.github.scrachx.openfood.models.Nutriments.Nutriment -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_GRAM -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_KILOGRAM -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_MILLIGRAM -import openfoodfacts.github.scrachx.openfood.utils.UnitUtils.convertFromGram +import openfoodfacts.github.scrachx.openfood.models.MeasurementUnit.* +import openfoodfacts.github.scrachx.openfood.models.ProductNutriments.Companion.SILICA +import openfoodfacts.github.scrachx.openfood.models.ProductNutriments.Companion.VITAMIN_A +import openfoodfacts.github.scrachx.openfood.models.ProductNutriments.Companion.VITAMIN_B1 +import openfoodfacts.github.scrachx.openfood.models.ProductNutriments.ProductNutriment import openfoodfacts.github.scrachx.openfood.utils.Utils.getRoundNumber import org.junit.Before import org.junit.Test -class NutrimentsTest { - private lateinit var nutriments: Nutriments +class ProductNutrimentsTest { + private lateinit var nutriments: ProductNutriments @Before fun setup() { - nutriments = Nutriments().apply { setAdditionalProperty(NUTRIMENT_NAME_KEY, NUTRIMENT_NAME) } + nutriments = ProductNutriments().apply { setAdditionalProperty(NUTRIMENT_NAME_KEY, NUTRIMENT_NAME) } } @Test fun getForAnyValue() { - val valueFor100g = 30.0 - val valueForServing = 60.0 - val nutriment = Nutriment( - "test", - "test", - valueFor100g.toString(), - valueForServing.toString(), - UNIT_MILLIGRAM, - "" + val valueFor100g = 30.0f + val valueForServing = 60.0f + val nutriment = ProductNutriment( + "test", + "test", + valueFor100g.toString(), + valueForServing.toString(), + UNIT_MILLIGRAM, + "" ) - assertThat(nutriment.displayStringFor100g).isEqualTo("${getRoundNumber((30 * 1000).toFloat())} mg") - assertThat(nutriment.getForPortion(1f, UNIT_KILOGRAM)).isEqualTo(getRoundNumber(convertFromGram(valueFor100g * 10, nutriment.unit))) - assertThat(nutriment.getForPortion(1f, UNIT_GRAM)).isEqualTo(getRoundNumber(convertFromGram(valueFor100g / 100, nutriment.unit))) + assertThat(nutriment.getPer100gDisplayString()).isEqualTo("${getRoundNumber((30 * 1000).toFloat())} mg") + assertThat(nutriment.getForPortion(1f, UNIT_KILOGRAM)).isEqualTo(getRoundNumber((valueFor100g * 10f).convertFromGOrMl(nutriment.unit))) + assertThat(nutriment.getForPortion(1f, UNIT_GRAM)).isEqualTo(getRoundNumber((valueFor100g / 100f).convertFromGOrMl(nutriment.unit))) } @Test @@ -47,13 +44,13 @@ class NutrimentsTest { @Test fun `getServing returns correct serving`() { nutriments.setAdditionalProperty(NUTRIMENT_SERVING_KEY, NUTRIMENT_SERVING) - assertThat(nutriments[NUTRIMENT_NAME_KEY]?.forServing).isEqualTo(NUTRIMENT_SERVING) + assertThat(nutriments[NUTRIMENT_NAME_KEY]?.perServingInG).isEqualTo(NUTRIMENT_SERVING) } @Test fun `get100g returns quantity for 100g`() { nutriments.setAdditionalProperty(NUTRIMENT_100G_KEY, NUTRIMENT_100G) - assertThat(nutriments[NUTRIMENT_NAME_KEY]?.for100g).isEqualTo(NUTRIMENT_100G) + assertThat(nutriments[NUTRIMENT_NAME_KEY]?.per100gInG).isEqualTo(NUTRIMENT_100G) } @Test diff --git a/app/src/test/java/openfoodfacts/github/scrachx/openfood/utils/QuantityParserTest.kt b/app/src/test/java/openfoodfacts/github/scrachx/openfood/utils/QuantityParserTest.kt index 5fa84569d995..e733bb25e92b 100644 --- a/app/src/test/java/openfoodfacts/github/scrachx/openfood/utils/QuantityParserTest.kt +++ b/app/src/test/java/openfoodfacts/github/scrachx/openfood/utils/QuantityParserTest.kt @@ -24,12 +24,12 @@ class QuantityParserTest { fun testIsGreaterThan() { val mockSpinner = mock(Spinner::class.java) mockitoWhen(mockSpinner.selectedItemPosition).thenReturn(2) - assertThat(mockSpinner.isModifierEqualsToGreaterThan()).isTrue() + assertThat(mockSpinner.modifier == Modifier.GREATER_THAN).isTrue() mockitoWhen(mockSpinner.selectedItemPosition).thenReturn(1) - assertThat(mockSpinner.isModifierEqualsToGreaterThan()).isFalse() + assertThat(mockSpinner.modifier == Modifier.GREATER_THAN).isFalse() mockitoWhen(mockSpinner.selectedItemPosition).thenReturn(0) - assertThat(mockSpinner.isModifierEqualsToGreaterThan()).isFalse() + assertThat(mockSpinner.modifier == Modifier.GREATER_THAN).isFalse() } } \ No newline at end of file diff --git a/app/src/test/java/openfoodfacts/github/scrachx/openfood/utils/UnitUtilsTest.kt b/app/src/test/java/openfoodfacts/github/scrachx/openfood/utils/UnitUtilsTest.kt index e17d8d5164b1..68e94cbed601 100644 --- a/app/src/test/java/openfoodfacts/github/scrachx/openfood/utils/UnitUtilsTest.kt +++ b/app/src/test/java/openfoodfacts/github/scrachx/openfood/utils/UnitUtilsTest.kt @@ -1,26 +1,11 @@ package openfoodfacts.github.scrachx.openfood.utils import com.google.common.truth.Truth.assertThat -import openfoodfacts.github.scrachx.openfood.models.Units.ENERGY_KCAL -import openfoodfacts.github.scrachx.openfood.models.Units.ENERGY_KJ -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_CENTILITRE -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_DECILITRE -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_GRAM -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_KILOGRAM -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_LITER -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_MICROGRAM -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_MILLIGRAM -import openfoodfacts.github.scrachx.openfood.models.Units.UNIT_MILLILITRE -import openfoodfacts.github.scrachx.openfood.utils.UnitUtils.convertFromGram -import openfoodfacts.github.scrachx.openfood.utils.UnitUtils.convertToGrams -import openfoodfacts.github.scrachx.openfood.utils.UnitUtils.convertToKiloCalories +import openfoodfacts.github.scrachx.openfood.models.MeasurementUnit.* import openfoodfacts.github.scrachx.openfood.utils.UnitUtils.getServingInL import openfoodfacts.github.scrachx.openfood.utils.UnitUtils.getServingInOz -import openfoodfacts.github.scrachx.openfood.utils.UnitUtils.saltToSodium -import openfoodfacts.github.scrachx.openfood.utils.UnitUtils.sodiumToSalt -import org.junit.Assert +import org.junit.Assert.assertThrows import org.junit.Test -import java.util.* /** * @@ -28,73 +13,74 @@ import java.util.* class UnitUtilsTest { @Test fun testGetServingInL() { - assertThat(getServingInL("1l", Locale.getDefault())).isEqualTo("1 l") - assertThat(getServingInL("33.814oz", Locale.getDefault())).isEqualTo("1 l") - assertThat(getServingInL("33.814 oz", Locale.getDefault())).isEqualTo("1 l") + assertThat(getServingInL("1l")).isEqualTo("1 l") + assertThat(getServingInL("33.814oz")).isEqualTo("1 l") + assertThat(getServingInL("33.814 oz")).isEqualTo("1 l") } @Test fun testGetServingInOz() { - assertThat(getServingInOz("1oz", Locale.getDefault())).isEqualTo("1 oz") + assertThat(getServingInOz("1oz")).isEqualTo("1 oz") - assertThat(getServingInOz("0.0295735l", Locale.ENGLISH)).isEqualTo("1 oz") - assertThat(getServingInOz("0.0295735 l", Locale.ENGLISH)).isEqualTo("1 oz") + assertThat(getServingInOz("0.0295735l")).isEqualTo("1 oz") + assertThat(getServingInOz("0.0295735 l")).isEqualTo("1 oz") - assertThat(getServingInOz("2.95735cl", Locale.ENGLISH)).isEqualTo("1 oz") - assertThat(getServingInOz("2.95735 cl", Locale.ENGLISH)).isEqualTo("1 oz") + assertThat(getServingInOz("2.95735cl")).isEqualTo("1 oz") + assertThat(getServingInOz("2.95735 cl")).isEqualTo("1 oz") - assertThat(getServingInOz("29.5735ml", Locale.ENGLISH)).isEqualTo("1 oz") - assertThat(getServingInOz("29.5735 ml", Locale.ENGLISH)).isEqualTo("1 oz") + assertThat(getServingInOz("29.5735ml")).isEqualTo("1 oz") + assertThat(getServingInOz("29.5735 ml")).isEqualTo("1 oz") } // Not finished yet @Test fun testConvertFromGram() { - assertThat(convertFromGram(1f, UNIT_KILOGRAM)).isWithin(DELTA).of(0.001f) - assertThat(convertFromGram(1f, UNIT_GRAM)).isWithin(DELTA).of(1f) - assertThat(convertFromGram(1f, UNIT_MILLIGRAM)).isWithin(DELTA).of(1000f) - assertThat(convertFromGram(1f, UNIT_MICROGRAM)).isWithin(DELTA).of(1000 * 1000f) + assertThat(measure(1f, UNIT_GRAM).convertTo(UNIT_KILOGRAM).value).isWithin(DELTA).of(0.001f) + assertThat(measure(1f, UNIT_GRAM).convertTo(UNIT_GRAM).value).isWithin(DELTA).of(1f) + assertThat(measure(1f, UNIT_GRAM).convertTo(UNIT_MILLIGRAM).value).isWithin(DELTA).of(1000f) + assertThat(measure(1f, UNIT_GRAM).convertTo(UNIT_MICROGRAM).value).isWithin(DELTA).of(1000 * 1000f) } @Test fun testConvertFromMl() { - assertThat(convertFromGram(1f, UNIT_LITER)).isWithin(DELTA).of(0.001f) - assertThat(convertFromGram(1f, UNIT_DECILITRE)).isWithin(DELTA).of(0.01f) - assertThat(convertFromGram(1f, UNIT_CENTILITRE)).isWithin(DELTA).of(0.1f) - assertThat(convertFromGram(1f, UNIT_MILLILITRE)).isWithin(DELTA).of(1f) + assertThat(measure(1f, UNIT_MILLILITRE).convertTo(UNIT_LITER).value).isWithin(DELTA).of(0.001f) + assertThat(measure(1f, UNIT_MILLILITRE).convertTo(UNIT_DECILITRE).value).isWithin(DELTA).of(0.01f) + assertThat(measure(1f, UNIT_MILLILITRE).convertTo(UNIT_CENTILITRE).value).isWithin(DELTA).of(0.1f) + assertThat(measure(1f, UNIT_MILLILITRE).convertTo(UNIT_MILLILITRE).value).isWithin(DELTA).of(1f) } @Test fun testConvertToMl() { - assertThat(convertToGrams(1f, UNIT_LITER)).isWithin(DELTA).of(1000f) - assertThat(convertToGrams(1f, UNIT_DECILITRE)).isWithin(DELTA).of(100f) - assertThat(convertToGrams(1f, UNIT_CENTILITRE)).isWithin(DELTA).of(10f) - assertThat(convertToGrams(1f, UNIT_MILLILITRE)).isWithin(DELTA).of(1f) + assertThat(measure(1f, UNIT_LITER).convertToGrams().value).isWithin(DELTA).of(1000f) + assertThat(measure(1f, UNIT_DECILITRE).convertToGrams().value).isWithin(DELTA).of(100f) + assertThat(measure(1f, UNIT_CENTILITRE).convertToGrams().value).isWithin(DELTA).of(10f) + assertThat(measure(1f, UNIT_MILLILITRE).convertToGrams().value).isWithin(DELTA).of(1f) } @Test fun testConvertToGram() { - assertThat(convertToGrams(1f, UNIT_KILOGRAM)).isWithin(DELTA).of(1000f) - assertThat(convertToGrams(1f, UNIT_GRAM)).isWithin(DELTA).of(1f) - assertThat(convertToGrams(1f, UNIT_MILLIGRAM)).isWithin(DELTA).of(1e-3f) - assertThat(convertToGrams(1f, UNIT_MICROGRAM)).isWithin(DELTA).of(1e-6f) + assertThat(measure(1f, UNIT_KILOGRAM).convertToGrams().value).isWithin(DELTA).of(1000f) + assertThat(measure(1f, UNIT_GRAM).convertToGrams().value).isWithin(DELTA).of(1f) + assertThat(measure(1f, UNIT_MILLIGRAM).convertToGrams().value).isWithin(DELTA).of(1e-3f) + assertThat(measure(1f, UNIT_MICROGRAM).convertToGrams().value).isWithin(DELTA).of(1e-6f) } @Test fun testConvertToKiloCalories() { - assertThat(convertToKiloCalories(100, ENERGY_KJ)).isEqualTo(23) - assertThat(convertToKiloCalories(100, ENERGY_KCAL)).isEqualTo(100) - Assert.assertThrows(IllegalArgumentException::class.java) { convertToKiloCalories(1, UNIT_GRAM) } + assertThat(measure(100f, ENERGY_KJ).convertEnergyTo(ENERGY_KCAL).value).isWithin(DELTA).of(23f) + assertThat(measure(100f, ENERGY_KCAL).convertEnergyTo(ENERGY_KCAL).value).isWithin(DELTA).of(100f) + assertThrows(IllegalArgumentException::class.java) { measure(1f, UNIT_GRAM).convertEnergyTo(ENERGY_KCAL) } } @Test fun testSaltSodiumConversion() { - assertThat(sodiumToSalt(1.0)).isWithin(DELTA.toDouble()).of(2.54) - assertThat(saltToSodium(2.54)).isWithin(DELTA.toDouble()).of(1.0) + assertThat(1.0f.sodiumToSalt()).isWithin(DELTA).of(2.54f) + assertThat(2.54f.saltToSodium()).isWithin(DELTA).of(1.0f) } companion object { private const val DELTA = 1e-5f } + } \ No newline at end of file diff --git a/app/src/test/java/openfoodfacts/github/scrachx/openfood/utils/UtilsTest.kt b/app/src/test/java/openfoodfacts/github/scrachx/openfood/utils/UtilsTest.kt index 2eac27b35e5b..89d83c973005 100644 --- a/app/src/test/java/openfoodfacts/github/scrachx/openfood/utils/UtilsTest.kt +++ b/app/src/test/java/openfoodfacts/github/scrachx/openfood/utils/UtilsTest.kt @@ -74,19 +74,19 @@ class UtilsTest { @Test fun getServingInOz_from_ml() { - assertThat(getServingInOz("100 ml", Locale.getDefault())) + assertThat(getServingInOz("100 ml")) .isEqualTo(String.format(Locale.getDefault(), "%.2f", 3.38) + " oz") } @Test fun getServingInOz_from_cl() { - assertThat(getServingInOz("250 cl", Locale.getDefault())) + assertThat(getServingInOz("250 cl")) .isEqualTo(String.format(Locale.getDefault(), "%.2f", 84.53) + " oz") } @Test fun getServingInOz_from_l() { - assertThat(getServingInOz("3 l", Locale.getDefault())) + assertThat(getServingInOz("3 l")) .isEqualTo(String.format(Locale.getDefault(), "%.2f", 101.44) + " oz") } } \ No newline at end of file