From ad45456696d15c0c5c93f032638659cf165790bf Mon Sep 17 00:00:00 2001 From: VaiTon Date: Tue, 3 Aug 2021 19:43:54 +0200 Subject: [PATCH] ref: add stability to SummaryProductFragment using coroutines fix: show tagline in HomeFragment --- .../scrachx/openfood/features/HomeFragment.kt | 3 +- .../additives/AdditiveFragmentHelper.kt | 3 +- .../product/view/CalculateDetailsActivity.kt | 118 ++++---- .../product/view/CategoryProductHelper.kt | 123 +++++---- .../product/view/ProductViewFragment.kt | 8 - .../ingredients/IngredientsProductFragment.kt | 89 +++--- .../AbstractSummaryProductPresenter.kt | 46 +--- .../view/summary/ISummaryProductPresenter.kt | 31 +-- .../view/summary/SummaryProductFragment.kt | 257 +++++++++--------- .../view/summary/SummaryProductPresenter.kt | 238 +++++++--------- .../features/scan/ContinuousScanActivity.kt | 18 +- .../features/shared/views/QuestionDialog.kt | 65 ++--- .../scrachx/openfood/models/Question.kt | 16 +- .../entities/allergen/AllergenHelper.kt | 15 +- .../openfood/network/OpenFoodAPIClient.kt | 110 ++++---- .../openfood/network/WikiDataApiClient.kt | 3 +- .../openfood/network/services/ProductsAPI.kt | 193 ++++++------- .../openfood/network/services/WikidataAPI.kt | 3 +- .../openfood/utils/OfflineProductService.kt | 179 ++++++------ .../openfood/utils/ProductInfoState.kt | 6 +- .../github/scrachx/openfood/utils/Utils.kt | 14 +- app/src/main/res/layout/calculate_details.xml | 2 +- 22 files changed, 764 insertions(+), 776 deletions(-) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/HomeFragment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/HomeFragment.kt index c0f8b1bda326..e42e2cf13d2b 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/HomeFragment.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/HomeFragment.kt @@ -213,10 +213,9 @@ class HomeFragment : NavigationBaseFragment() { for (tag in tagLines) { if (appLanguage !in tag.language) continue isLanguageFound = true - if (tag.language == appLanguage) break - taglineURL = tag.tagLine.url binding.tvTagLine.text = tag.tagLine.message + if (tag.language == appLanguage) break } if (!isLanguageFound) { 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 69697ca359bf..7938442206ba 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 @@ -16,7 +16,6 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.fasterxml.jackson.databind.JsonNode import kotlinx.coroutines.launch -import kotlinx.coroutines.rx2.await import openfoodfacts.github.scrachx.openfood.R import openfoodfacts.github.scrachx.openfood.features.search.ProductSearchActivity import openfoodfacts.github.scrachx.openfood.features.shared.BaseFragment @@ -73,7 +72,7 @@ object AdditiveFragmentHelper { override fun onClick(view: View) { if (additive.isWikiDataIdPresent) { lifecycleOwner.lifecycleScope.launch { - val result = apiClientForWikiData.doSomeThing(additive.wikiDataId).await() + val result = apiClientForWikiData.doSomeThing(additive.wikiDataId) getOnWikiResponse(activity, additive)(result) } } else { 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 e5647c33dd06..2f267209fada 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 @@ -2,9 +2,7 @@ package openfoodfacts.github.scrachx.openfood.features.product.view import android.content.Context import android.content.Intent -import android.content.pm.ActivityInfo import android.os.Bundle -import android.util.Log import android.view.MenuItem import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -21,9 +19,9 @@ 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 + 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 product: Product @@ -33,26 +31,22 @@ class CalculateDetailsActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (resources.getBoolean(R.bool.portrait_only)) { - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - } - val binding = CalculateDetailsBinding.inflate(layoutInflater) + val binding = CalculateDetailsBinding.inflate(layoutInflater) setContentView(binding.root) + title = getString(R.string.app_name_long) + setSupportActionBar(binding.toolbar) supportActionBar!!.setDisplayHomeAsUpEnabled(true) - val intent = intent val product = intent.getSerializableExtra(KEY_PRODUCT) as Product? val spinnerValue = intent.getStringExtra(KEY_SPINNER_VALUE) val weight = intent.getFloatExtra(KEY_WEIGHT, -1f) - if (product == null || spinnerValue == null || weight == -1f) { - Log.e(CalculateDetailsActivity::class.simpleName, "Activity instantiated with wrong intent extras.") - finish() - return - } + requireNotNull(product) { "${this::class.simpleName} created without product intent extra." } + requireNotNull(spinnerValue) { "${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 @@ -60,29 +54,29 @@ class CalculateDetailsActivity : BaseActivity() { this.nutriments = product.nutriments binding.resultTextView.text = getString(R.string.display_fact, "$weight $spinnerValue") - binding.nutrimentsRecyclerViewCalc.setHasFixedSize(true) + binding.nutriments.setHasFixedSize(true) // use a linear layout manager val mLayoutManager = LinearLayoutManager(this) - binding.nutrimentsRecyclerViewCalc.layoutManager = mLayoutManager - binding.nutrimentsRecyclerViewCalc.isNestedScrollingEnabled = false + binding.nutriments.layoutManager = mLayoutManager + binding.nutriments.isNestedScrollingEnabled = false // use VERTICAL divider val dividerItemDecoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) - binding.nutrimentsRecyclerViewCalc.addItemDecoration(dividerItemDecoration) + binding.nutriments.addItemDecoration(dividerItemDecoration) // Header hack - nutrimentListItems.add(NutrimentListItem(product.isPerServingInLiter() ?: false)) + nutrimentListItems += NutrimentListItem(product.isPerServingInLiter() ?: false) // Energy val energyKcal = nutriments[Nutriments.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(), + getString(R.string.nutrition_energy_short_name), + calculateCalories(weight, spinnerValue).toString(), + energyKcal.forServingInUnits, + Units.ENERGY_KCAL, + energyKcal.getModifierIfNotDefault(), ) } val energyKj = nutriments[Nutriments.ENERGY_KJ] @@ -100,11 +94,11 @@ class CalculateDetailsActivity : BaseActivity() { val fat = nutriments[Nutriments.FAT] if (fat != null) { nutrimentListItems += BoldNutrimentListItem( - getString(R.string.nutrition_fat), - fat.getForPortion(weight, spinnerValue), - fat.forServingInUnits, - fat.unit, - fat.getModifierIfNotDefault() + getString(R.string.nutrition_fat), + fat.getForPortion(weight, spinnerValue), + fat.forServingInUnits, + fat.unit, + fat.getModifierIfNotDefault() ) nutrimentListItems.addAll(getNutrimentItems(nutriments, Nutriments.FAT_MAP)) } @@ -113,33 +107,33 @@ class CalculateDetailsActivity : BaseActivity() { val carbohydrates = nutriments[Nutriments.CARBOHYDRATES] if (carbohydrates != null) { nutrimentListItems += BoldNutrimentListItem( - getString(R.string.nutrition_carbohydrate), - carbohydrates.getForPortion(weight, spinnerValue), - carbohydrates.forServingInUnits, - carbohydrates.unit, - carbohydrates.getModifierIfNotDefault() + getString(R.string.nutrition_carbohydrate), + carbohydrates.getForPortion(weight, spinnerValue), + carbohydrates.forServingInUnits, + carbohydrates.unit, + carbohydrates.getModifierIfNotDefault() ) - nutrimentListItems.addAll(getNutrimentItems(nutriments, Nutriments.CARBO_MAP)) + nutrimentListItems += getNutrimentItems(nutriments, Nutriments.CARBO_MAP) } // fiber - nutrimentListItems.addAll(getNutrimentItems(nutriments, Collections.singletonMap(Nutriments.FIBER, R.string.nutrition_fiber))) + nutrimentListItems += getNutrimentItems(nutriments, Collections.singletonMap(Nutriments.FIBER, R.string.nutrition_fiber)) // Proteins val proteins = nutriments[Nutriments.PROTEINS] if (proteins != null) { nutrimentListItems += BoldNutrimentListItem( - getString(R.string.nutrition_proteins), - proteins.getForPortion(weight, spinnerValue), - proteins.forServingInUnits, - proteins.unit, - proteins.getModifierIfNotDefault() + getString(R.string.nutrition_proteins), + proteins.getForPortion(weight, spinnerValue), + proteins.forServingInUnits, + proteins.unit, + proteins.getModifierIfNotDefault() ) - nutrimentListItems.addAll(getNutrimentItems(nutriments, Nutriments.PROT_MAP)) + nutrimentListItems += getNutrimentItems(nutriments, Nutriments.PROT_MAP) } // salt and alcohol - nutrimentListItems.addAll(getNutrimentItems(nutriments, nutriMap)) + nutrimentListItems += getNutrimentItems(nutriments, nutriMap) // Vitamins if (nutriments.hasVitamins) { @@ -152,34 +146,34 @@ class CalculateDetailsActivity : BaseActivity() { nutrimentListItems += BoldNutrimentListItem(getString(R.string.nutrition_minerals)) nutrimentListItems += getNutrimentItems(nutriments, Nutriments.MINERALS_MAP) } - binding.nutrimentsRecyclerViewCalc.adapter = CalculatedNutrimentsGridAdapter(nutrimentListItems) + binding.nutriments.adapter = CalculatedNutrimentsGridAdapter(nutrimentListItems) } private fun getNutrimentItems(nutriments: Nutriments, nutrimentMap: Map): List { - return nutrimentMap.mapNotNull { (key, value) -> - val nutriment = nutriments[key] + return nutrimentMap.mapNotNull { (name, stringRes) -> + val nutriment = nutriments[name] if (nutriment != null) { NutrimentListItem( - getString(value), - nutriment.getForPortion(weight, spinnerValue), - nutriment.forServingInUnits, - nutriment.unit, - nutriment.getModifierIfNotDefault() + getString(stringRes), + nutriment.getForPortion(weight, spinnerValue), + nutriment.forServingInUnits, + nutriment.unit, + nutriment.getModifierIfNotDefault() ) } else null } } private fun calculateCalories(weight: Float, unit: String?): Float { - val caloriePer100g = product.nutriments[Nutriments.ENERGY_KCAL]!!.for100gInUnits.toFloat() - val weightInG = convertToGrams(weight, unit) - return caloriePer100g / 100 * weightInG + val energy100gCal = product.nutriments[Nutriments.ENERGY_KCAL]!!.for100gInUnits.toFloat() + val weightGrams = convertToGrams(weight, unit) + return energy100gCal / 100 * weightGrams } private fun calculateKj(weight: Float, unit: String?): Float { - val caloriePer100g = product.nutriments[Nutriments.ENERGY_KJ]!!.for100gInUnits.toFloat() - val weightInG = convertToGrams(weight, unit) - return caloriePer100g / 100 * weightInG + val energy100gKj = product.nutriments[Nutriments.ENERGY_KJ]!!.for100gInUnits.toFloat() + val weightGrams = convertToGrams(weight, unit) + return energy100gKj / 100 * weightGrams } override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { @@ -196,7 +190,13 @@ 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")) + @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()) } 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 da60e51f0301..6fdde04b89de 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 @@ -9,100 +9,111 @@ import android.view.View import android.widget.TextView import androidx.core.content.ContextCompat import androidx.core.text.bold +import androidx.core.text.color import androidx.core.text.inSpans -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.rxkotlin.addTo +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch import openfoodfacts.github.scrachx.openfood.R -import openfoodfacts.github.scrachx.openfood.features.search.ProductSearchActivity.Companion.start +import openfoodfacts.github.scrachx.openfood.features.search.ProductSearchActivity import openfoodfacts.github.scrachx.openfood.features.shared.BaseFragment import openfoodfacts.github.scrachx.openfood.models.entities.category.CategoryName import openfoodfacts.github.scrachx.openfood.network.WikiDataApiClient import openfoodfacts.github.scrachx.openfood.utils.SearchType import openfoodfacts.github.scrachx.openfood.utils.showBottomSheet -class CategoryProductHelper( - private val categoryText: TextView, - private val categories: List, - private val fragment: BaseFragment, - private val apiClient: WikiDataApiClient, - private val disp: CompositeDisposable -) { - var containsAlcohol = false - private set - - fun showCategories() = categoryText.let { - it.movementMethod = LinkMovementMethod.getInstance() - it.isClickable = true - it.movementMethod = LinkMovementMethod.getInstance() - - val text = SpannableStringBuilder() - .bold { append(fragment.getString(R.string.txtCategories)) } - .append(" ") +object CategoryProductHelper { + fun showCategories( + fragment: BaseFragment, + categoryText: TextView, + alcoholAlertText: TextView, + categories: List, + apiClient: WikiDataApiClient, + ) = categoryText.let { view -> if (categories.isEmpty()) { - it.visibility = View.GONE - } else { - it.visibility = View.VISIBLE - // Add all the categories to text view and link them to wikidata is possible - categories.forEach { category -> - // Add category name to text view - text.append(getCategoriesTag(category)) + view.visibility = View.GONE + return@let + } - // Add a comma if not the last item - if (category != categories.last()) text.append(", ") + 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 is possible + append(categories.joinToString { getCategoriesTag(it, fragment, apiClient) }) - if (category.categoryTag == "en:alcoholic-beverages") { - containsAlcohol = true - } } + + if (categories.any { it.categoryTag == "en:alcoholic-beverages" }) { + showAlcoholAlert(alcoholAlertText, fragment) } - it.text = text } - private fun getCategoriesTag(category: CategoryName): CharSequence { + private fun getCategoriesTag( + category: CategoryName, + fragment: BaseFragment, + apiClient: WikiDataApiClient + ): CharSequence { val clickableSpan = object : ClickableSpan() { override fun onClick(view: View) { if (category.isWikiDataIdPresent == true) { - apiClient.doSomeThing(category.wikiDataId!!).subscribe { result -> + fragment.lifecycleScope.launch { + val result = category.wikiDataId?.let { apiClient.doSomeThing(it) } if (result != null) { val activity = fragment.activity if (activity != null && !activity.isFinishing) { showBottomSheet(result, category, activity.supportFragmentManager) - return@subscribe } - } - start(fragment.requireContext(), SearchType.CATEGORY, category.categoryTag!!, category.name!!) - }.addTo(disp) + } else ProductSearchActivity.start( + fragment.requireContext(), + SearchType.CATEGORY, + category.categoryTag!!, + category.name!! + ) + } } else { - start(fragment.requireContext(), SearchType.CATEGORY, category.categoryTag!!, category.name!!) + ProductSearchActivity.start( + fragment.requireContext(), + SearchType.CATEGORY, + category.categoryTag!!, + category.name!! + ) } } } - val spannableStringBuilder = SpannableStringBuilder() - .inSpans(clickableSpan) { append(category.name) } - if (!category.isNotNull) { + val span = SpannableStringBuilder() + .inSpans(clickableSpan) { append(category.name) } + if (category.isNull) { // Span to make text italic - spannableStringBuilder.setSpan(StyleSpan(Typeface.ITALIC), 0, spannableStringBuilder.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + span.setSpan( + StyleSpan(Typeface.ITALIC), + 0, + span.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) } - return spannableStringBuilder + return span } - fun showAlcoholAlert(alcoholAlertText: TextView) { + private fun showAlcoholAlert(alcoholAlertText: TextView, fragment: BaseFragment) { + val context = fragment.requireContext() val alcoholAlertIcon = ContextCompat.getDrawable( - fragment.requireContext(), - R.drawable.ic_alert_alcoholic_beverage + context, + R.drawable.ic_alert_alcoholic_beverage )!!.apply { setBounds(0, 0, intrinsicWidth, intrinsicHeight) } val riskAlcoholConsumption = fragment.getString(R.string.risk_alcohol_consumption) alcoholAlertText.visibility = View.VISIBLE - alcoholAlertText.text = SpannableStringBuilder().apply { - inSpans(ImageSpan(alcoholAlertIcon, DynamicDrawableSpan.ALIGN_BOTTOM)) { append("-") } - append(" ") - inSpans(ForegroundColorSpan(ContextCompat.getColor(fragment.requireContext(), R.color.red))) - { append(riskAlcoholConsumption) } - - } + alcoholAlertText.text = SpannableStringBuilder() + .inSpans(ImageSpan(alcoholAlertIcon, DynamicDrawableSpan.ALIGN_BOTTOM)) { append("-") } + .append(" ") + .color(ContextCompat.getColor(context, R.color.red)) { + append(riskAlcoholConsumption) + } } } \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/ProductViewFragment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/ProductViewFragment.kt index 91e846c0001f..b8019bf36f5e 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/ProductViewFragment.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/ProductViewFragment.kt @@ -15,7 +15,6 @@ import androidx.lifecycle.lifecycleScope import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint -import io.reactivex.disposables.CompositeDisposable import kotlinx.coroutines.launch import openfoodfacts.github.scrachx.openfood.R import openfoodfacts.github.scrachx.openfood.databinding.ActivityProductBinding @@ -42,8 +41,6 @@ class ProductViewFragment : Fragment(), IProductView, OnRefreshListener { private var _binding: ActivityProductBinding? = null private val binding get() = _binding!! - private val disp = CompositeDisposable() - @Inject lateinit var client: OpenFoodAPIClient @@ -78,11 +75,6 @@ class ProductViewFragment : Fragment(), IProductView, OnRefreshListener { binding.navigationBottomInclude.bottomNavigation.installBottomNavigation(requireActivity()) } - override fun onDestroyView() { - disp.dispose() - super.onDestroyView() - } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == LOGIN_ACTIVITY_REQUEST_CODE && resultCode == Activity.RESULT_OK) { 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 a5f27ba64dba..331125a5f7bc 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 @@ -33,11 +33,13 @@ import android.view.ViewGroup import androidx.browser.customtabs.CustomTabsIntent import androidx.core.text.bold 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 io.reactivex.rxkotlin.addTo +import kotlinx.coroutines.launch +import kotlinx.coroutines.rx2.await import openfoodfacts.github.scrachx.openfood.AppFlavors import openfoodfacts.github.scrachx.openfood.AppFlavors.OBF import openfoodfacts.github.scrachx.openfood.AppFlavors.OPF @@ -70,8 +72,7 @@ import openfoodfacts.github.scrachx.openfood.network.OpenFoodAPIClient import openfoodfacts.github.scrachx.openfood.network.WikiDataApiClient import openfoodfacts.github.scrachx.openfood.repositories.ProductRepository import openfoodfacts.github.scrachx.openfood.utils.* -import openfoodfacts.github.scrachx.openfood.utils.ProductInfoState.EMPTY -import openfoodfacts.github.scrachx.openfood.utils.ProductInfoState.LOADING +import openfoodfacts.github.scrachx.openfood.utils.ProductInfoState.* import java.io.File import javax.inject.Inject @@ -114,8 +115,16 @@ class IngredientsProductFragment : BaseFragment() { } private val updateImagesLauncher = registerForActivityResult(SendUpdatedImgContract()) { result -> if (result) onRefresh() } + private val loginLauncher = registerForActivityResult(LoginContract()) - { ProductEditActivity.start(requireContext(), productState.product!!, sendUpdatedIngredientsImage, ingredientExtracted) } + { + ProductEditActivity.start( + requireContext(), + productState.product!!, + sendUpdatedIngredientsImage, + ingredientExtracted + ) + } private lateinit var productState: ProductState private lateinit var customTabActivityHelper: CustomTabActivityHelper @@ -128,8 +137,7 @@ class IngredientsProductFragment : BaseFragment() { private var mSendProduct: SendProduct? = null - var ingredientsImgUrl: String? = null - private set + private var ingredientsImgUrl: String? = null private var sendUpdatedIngredientsImage = false @@ -215,10 +223,10 @@ class IngredientsProductFragment : BaseFragment() { binding.textAdditiveProduct.text = SpannableStringBuilder() .bold { append(getString(R.string.txtAdditives)) } - setAdditivesState(LOADING) + setAdditivesState(Loading) viewModel.additives.observe(viewLifecycleOwner) { additives -> - if (additives.isEmpty()) setAdditivesState(EMPTY) - else showAdditives(additives) + if (additives.isEmpty()) setAdditivesState(Empty) + else setAdditivesState(Data(additives)) } @@ -262,10 +270,10 @@ class IngredientsProductFragment : BaseFragment() { } } - setAllergensState(LOADING) + setAllergensState(Loading) viewModel.allergens.observe(viewLifecycleOwner) { - if (it.isEmpty()) setAllergensState(EMPTY) - else showAllergens(it) + if (it.isEmpty()) setAllergensState(Empty) + else setAllergensState(Data(it)) } if (!product.traces.isNullOrBlank()) { @@ -318,14 +326,15 @@ class IngredientsProductFragment : BaseFragment() { val clickableSpan: ClickableSpan = object : ClickableSpan() { override fun onClick(view: View) { if (allergen.isWikiDataIdPresent) { - wikidataClient.doSomeThing( - allergen.wikiDataId - ).subscribe { result -> + lifecycleScope.launch { + val result = wikidataClient.doSomeThing( + allergen.wikiDataId + ) val activity = activity if (activity?.isFinishing == false) { showBottomSheet(result, allergen, activity.supportFragmentManager) } - }.addTo(disp) + } } else { start(requireContext(), SearchType.ALLERGEN, allergen.allergenTag, allergen.name) } @@ -365,27 +374,20 @@ class IngredientsProductFragment : BaseFragment() { } } - fun showAdditives(additives: List) = - showAdditives(additives, binding.textAdditiveProduct, wikidataClient, this) - private fun setAdditivesState(state: ProductInfoState) { + private fun setAdditivesState(state: ProductInfoState>) { when (state) { - LOADING -> { + is Loading -> { binding.cvTextAdditiveProduct.visibility = View.VISIBLE binding.textAdditiveProduct.append(getString(R.string.txtLoading)) } - EMPTY -> binding.cvTextAdditiveProduct.visibility = View.GONE + is Empty -> binding.cvTextAdditiveProduct.visibility = View.GONE + is Data -> { + showAdditives(state.data, binding.textAdditiveProduct, wikidataClient, this) + } } } - fun showAllergens(allergens: List) { - binding.textSubstanceProduct.movementMethod = LinkMovementMethod.getInstance() - binding.textSubstanceProduct.text = SpannableStringBuilder() - .bold { append(getString(R.string.txtSubstances)) } - .append(" ") - .append(allergens.joinToString(", ") { getAllergensTag(it) }) - } - fun changeIngImage() { sendUpdatedIngredientsImage = true @@ -406,13 +408,20 @@ class IngredientsProductFragment : BaseFragment() { } } - private fun setAllergensState(state: ProductInfoState) { + private fun setAllergensState(state: ProductInfoState>) { when (state) { - LOADING -> { + is Loading -> { binding.textSubstanceProduct.visibility = View.VISIBLE binding.textSubstanceProduct.append(getString(R.string.txtLoading)) } - EMPTY -> binding.textSubstanceProduct.visibility = View.GONE + 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) }) + } } } @@ -477,13 +486,19 @@ class IngredientsProductFragment : BaseFragment() { override fun doOnPhotosPermissionGranted() = newIngredientImage() private fun onPhotoReturned(newPhotoFile: File) { - val image = ProductImage(productState.code!!, ProductImageField.INGREDIENTS, newPhotoFile, localeManager.getLanguage()) - image.filePath = newPhotoFile.absolutePath - client.postImg(image).subscribe().addTo(disp) + val image = ProductImage( + productState.code!!, + ProductImageField.INGREDIENTS, + newPhotoFile, + localeManager.getLanguage() + ).apply { filePath = newPhotoFile.absolutePath } + + lifecycleScope.launch { client.postImg(image).await() } + binding.addPhotoLabel.visibility = View.GONE ingredientsImgUrl = newPhotoFile.absolutePath - Picasso.get() - .load(newPhotoFile) + + picasso.load(newPhotoFile) .fit() .into(binding.imageViewIngredients) } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/summary/AbstractSummaryProductPresenter.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/summary/AbstractSummaryProductPresenter.kt index cfa437958570..3524e27fb885 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/summary/AbstractSummaryProductPresenter.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/summary/AbstractSummaryProductPresenter.kt @@ -25,43 +25,11 @@ import openfoodfacts.github.scrachx.openfood.models.entities.label.LabelName import openfoodfacts.github.scrachx.openfood.utils.ProductInfoState open class AbstractSummaryProductPresenter : ISummaryProductPresenter.View { - override fun showAllergens(allergens: List) { - //empty impl - } - - override fun showProductQuestion(question: Question) { - //empty impl - } - - override fun showAnnotatedInsightToast(annotationResponse: AnnotationResponse) { - //empty impl - } - - override fun showCategories(categories: List) { - //empty impl - } - - override fun showLabels(labelNames: List) { - //empty impl - } - - override fun showCategoriesState(state: ProductInfoState) { - //empty impl - } - - override fun showLabelsState(state: ProductInfoState) { - //empty impl - } - - override fun showAdditives(additives: List) { - //empty impl - } - - override fun showAdditivesState(state: ProductInfoState) { - //empty impl - } - - override fun showAnalysisTags(analysisTags: List) { - //empty impl - } + override fun showAllergens(allergens: List) = Unit + override fun showProductQuestion(question: Question) = Unit + override fun showAnnotatedInsightToast(annotationResponse: AnnotationResponse) = Unit + override fun showCategoriesState(state: ProductInfoState>) = Unit + override fun showLabelsState(state: ProductInfoState>) = Unit + override fun showAdditivesState(state: ProductInfoState>) = Unit + override fun showAnalysisTags(analysisTags: List) = Unit } \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/summary/ISummaryProductPresenter.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/summary/ISummaryProductPresenter.kt index a5a4f30119d2..2afd14f53913 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/summary/ISummaryProductPresenter.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/summary/ISummaryProductPresenter.kt @@ -15,7 +15,6 @@ */ package openfoodfacts.github.scrachx.openfood.features.product.view.summary -import io.reactivex.disposables.Disposable import openfoodfacts.github.scrachx.openfood.models.AnnotationAnswer import openfoodfacts.github.scrachx.openfood.models.AnnotationResponse import openfoodfacts.github.scrachx.openfood.models.Question @@ -30,26 +29,26 @@ import openfoodfacts.github.scrachx.openfood.utils.ProductInfoState * Created by Lobster on 17.03.18. */ interface ISummaryProductPresenter { - interface Actions : Disposable { - fun loadProductQuestion() - fun annotateInsight(insightId: String, annotation: AnnotationAnswer) - fun loadAllergens(runIfError: (() -> Unit)?) - fun loadCategories() - fun loadLabels() - fun loadAdditives() - fun loadAnalysisTags() + interface Actions { + suspend fun loadProductQuestion() + suspend fun loadAllergens() + suspend fun loadCategories() + suspend fun loadLabels() + suspend fun loadAdditives() + suspend fun loadAnalysisTags() + suspend fun annotateInsight(insightId: String, annotation: AnnotationAnswer) } interface View { - fun showAllergens(allergens: List) fun showProductQuestion(question: Question) fun showAnnotatedInsightToast(annotationResponse: AnnotationResponse) - fun showCategories(categories: List) - fun showLabels(labelNames: List) - fun showCategoriesState(state: ProductInfoState) - fun showLabelsState(state: ProductInfoState) - fun showAdditives(additives: List) - fun showAdditivesState(state: ProductInfoState) + + fun showAllergens(allergens: List) + + fun showCategoriesState(state: ProductInfoState>) + fun showLabelsState(state: ProductInfoState>) + fun showAdditivesState(state: ProductInfoState>) + fun showAnalysisTags(analysisTags: List) } } \ No newline at end of file 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 b28b47337729..e15abc5d28f7 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 @@ -23,7 +23,6 @@ import android.graphics.Color 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.util.Log @@ -36,21 +35,21 @@ 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.inSpans import androidx.core.view.updatePadding import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.afollestad.materialdialogs.MaterialDialog import com.google.android.material.chip.Chip import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.BaseTransientBottomBar +import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_SHORT import com.google.android.material.snackbar.Snackbar import com.squareup.picasso.Picasso import dagger.hilt.android.AndroidEntryPoint -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.rxkotlin.addTo +import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import openfoodfacts.github.scrachx.openfood.AppFlavors.OFF import openfoodfacts.github.scrachx.openfood.AppFlavors.isFlavors import openfoodfacts.github.scrachx.openfood.R @@ -122,14 +121,18 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { @Inject lateinit var localeManager: LocaleManager + @Inject + lateinit var productRepository: ProductRepository + private lateinit var presenter: ISummaryProductPresenter.Actions private lateinit var mTagDao: TagDao - private lateinit var product: Product + private lateinit var product: Product private lateinit var customTabActivityHelper: CustomTabActivityHelper - private lateinit var customTabsIntent: CustomTabsIntent + private lateinit var customTabsIntent: CustomTabsIntent private var annotation: AnnotationAnswer? = null + private var hasCategoryInsightQuestion = false private var insightId: String? = null @@ -137,8 +140,8 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { //boolean to determine if image should be loaded or not private val isLowBatteryMode by lazy { requireContext().isDisableImageLoad() && requireContext().isBatteryLevelLow() } private var mUrlImage: String? = null - private var nutritionScoreUri: Uri? = null + private var nutritionScoreUri: Uri? = null private val photoReceiverHandler by lazy { PhotoReceiverHandler(sharedPreferences) { newPhotoFile: File -> //the pictures are uploaded with the correct path @@ -153,6 +156,7 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { } } } + private var productQuestion: Question? = null private val loginThenProcessInsight = registerForActivityResult(LoginContract()) { isLogged -> @@ -161,8 +165,8 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { processInsight() } } - private lateinit var productState: ProductState + private var sendOther = false /**boolean to determine if category prompt should be shown*/ @@ -171,10 +175,10 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { /**boolean to determine if nutrient prompt should be shown*/ private var showNutrientPrompt = false + /**boolean to determine if eco score prompt should be shown*/ private var showEcoScorePrompt = false - override fun onAttach(context: Context) { super.onAttach(context) customTabActivityHelper = CustomTabActivityHelper().apply { @@ -196,9 +200,6 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { return binding.root } - @Inject - lateinit var productRepository: ProductRepository - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -213,12 +214,11 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { binding.productQuestionDismiss.setOnClickListener { binding.productQuestionLayout.visibility = View.GONE } - binding.productQuestionLayout.setOnClickListener { onProductQuestionClick() } + binding.productQuestionLayout.setOnClickListener { productQuestion?.let { onProductQuestionClick(it) } } productState = requireProductState() refreshView(productState) presenter = SummaryProductPresenter(localeManager.getLanguage(), product, this, productRepository) - presenter.addTo(disp) } @@ -227,23 +227,12 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { _binding = null } - private fun onImageListenerError(error: Throwable) { - binding.uploadingImageProgress.visibility = View.GONE - binding.uploadingImageProgressText.visibility = View.GONE - Toast.makeText(requireContext(), error.message, Toast.LENGTH_SHORT).show() - } - override fun onRefresh() { super.onRefresh() refreshView(productState) } - private fun onImageListenerComplete() { - binding.uploadingImageProgress.visibility = View.GONE - binding.uploadingImageProgressText.setText(R.string.image_uploaded_successfully) - } - /** * Starts uploading image to backend * @@ -253,10 +242,18 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { binding.uploadingImageProgress.visibility = View.VISIBLE binding.uploadingImageProgressText.visibility = View.VISIBLE binding.uploadingImageProgressText.setText(R.string.toastSending) - client.postImg(image).observeOn(AndroidSchedulers.mainThread()) - .doOnError { onImageListenerError(it) } - .subscribe { onImageListenerComplete() } - .addTo(disp) + + lifecycleScope.launch { + try { + withContext(IO) { client.postImg(image) } + } catch (err: Exception) { + binding.uploadingImageProgress.visibility = View.GONE + binding.uploadingImageProgressText.visibility = View.GONE + Toast.makeText(requireContext(), err.message, Toast.LENGTH_SHORT).show() + } + binding.uploadingImageProgress.visibility = View.GONE + binding.uploadingImageProgressText.setText(R.string.image_uploaded_successfully) + } } /** @@ -275,7 +272,7 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { override fun refreshView(productState: ProductState) { this.productState = productState product = productState.product!! - presenter = SummaryProductPresenter(localeManager.getLanguage(), product, this, productRepository).apply { addTo(disp) } + presenter = SummaryProductPresenter(localeManager.getLanguage(), product, this, productRepository) binding.categoriesText.text = SpannableStringBuilder() .bold { append(getString(R.string.txtCategories)) } @@ -300,12 +297,14 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { // Checks the product states_tags to determine which prompt to be shown refreshStatesTagsPrompt() - presenter.loadAllergens(null) - presenter.loadCategories() - presenter.loadLabels() - presenter.loadProductQuestion() - presenter.loadAdditives() - presenter.loadAnalysisTags() + lifecycleScope.launch { + presenter.loadAllergens() + presenter.loadCategories() + presenter.loadLabels() + presenter.loadProductQuestion() + presenter.loadAdditives() + presenter.loadAnalysisTags() + } val langCode = localeManager.getLanguage() val imageUrl = product.getImageUrl(langCode) @@ -570,18 +569,18 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { } - override fun showAdditives(additives: List) { - showAdditives(additives, binding.textAdditiveProduct, wikidataClient, this) - } - override fun showAdditivesState(state: ProductInfoState) { + override fun showAdditivesState(state: ProductInfoState>) { requireActivity().runOnUiThread { when (state) { - ProductInfoState.LOADING -> { + is ProductInfoState.Loading -> { binding.textAdditiveProduct.append(getString(R.string.txtLoading)) binding.textAdditiveProduct.visibility = View.VISIBLE } - ProductInfoState.EMPTY -> binding.textAdditiveProduct.visibility = View.GONE + is ProductInfoState.Empty -> binding.textAdditiveProduct.visibility = View.GONE + is ProductInfoState.Data -> { + showAdditives(state.data, binding.textAdditiveProduct, wikidataClient, this) + } } } } @@ -611,28 +610,21 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { return } binding.productAllergenAlertText.text = StringBuilder(resources.getString(R.string.product_allergen_prompt)) - .append("\n").append(data.allergens.joinToString(", ")) + .append("\n") + .append(data.allergens.joinToString(", ")) binding.productAllergenAlertLayout.visibility = View.VISIBLE } - override fun showCategories(categories: List) { - if (categories.isEmpty()) { - binding.categoriesLayout.visibility = View.GONE - } - val categoryProductHelper = CategoryProductHelper(binding.categoriesText, categories, this, wikidataClient, disp) - categoryProductHelper.showCategories() - if (categoryProductHelper.containsAlcohol) { - categoryProductHelper.showAlcoholAlert(binding.textCategoryAlcoholAlert) - } - } override fun showProductQuestion(question: Question) { if (isRemoving) return if (!question.isEmpty()) { productQuestion = question - binding.productQuestionText.text = "${question.questionText}\n${question.value}" + binding.productQuestionText.text = SpannableStringBuilder(question.questionText) + .append("\n") + .append(question.value) binding.productQuestionLayout.visibility = View.VISIBLE hasCategoryInsightQuestion = question.insightType == "category" } else { @@ -646,53 +638,51 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { } } - private fun onProductQuestionClick() { - productQuestion?.let { - QuestionDialog(requireActivity()).run { - backgroundColor = R.color.colorPrimaryDark - question = productQuestion!!.questionText - value = productQuestion!!.value - onPositiveFeedback = { - //init POST request - sendProductInsights(productQuestion!!.insightId, AnnotationAnswer.POSITIVE) - it.dismiss() - } + private fun onProductQuestionClick(productQuestion: Question) { + QuestionDialog(requireContext()).apply { + backgroundColor = R.color.colorPrimaryDark + question = productQuestion.questionText + value = productQuestion.value - onNegativeFeedback = { - sendProductInsights(productQuestion!!.insightId, AnnotationAnswer.NEGATIVE) - it.dismiss() - } - onAmbiguityFeedback = { - sendProductInsights(productQuestion!!.insightId, AnnotationAnswer.AMBIGUITY) - it.dismiss() - } + onPositiveFeedback = { + sendProductInsights(productQuestion.insightId, AnnotationAnswer.POSITIVE) + it.dismiss() + } - onCancelListener = { it.dismiss() } - show() + onNegativeFeedback = { + sendProductInsights(productQuestion.insightId, AnnotationAnswer.NEGATIVE) + it.dismiss() } - } + + onAmbiguityFeedback = { + sendProductInsights(productQuestion.insightId, AnnotationAnswer.AMBIGUITY) + it.dismiss() + } + + onCancelListener = { it.dismiss() } + + }.show() + } private fun sendProductInsights(insightId: String?, annotation: AnnotationAnswer?) { this.insightId = insightId this.annotation = annotation + if (requireActivity().isUserSet()) { processInsight() } else { matomoAnalytics.trackEvent(AnalyticsEvent.RobotoffLoginPrompt) - MaterialDialog.Builder(requireActivity()).run { - title(getString(R.string.sign_in_to_answer)) - positiveText(getString(R.string.sign_in_or_register)) - onPositive { dialog, _ -> + + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(getString(R.string.sign_in_to_answer)) + .setPositiveButton(getString(R.string.sign_in_or_register)) { dialog, _ -> loginThenProcessInsight.launch(Unit) dialog.dismiss() } - neutralText(R.string.dialog_cancel) - onNeutral { dialog, _ -> dialog.dismiss() } - show() - } - + .setNegativeButton(R.string.dialog_cancel) { d, _ -> d.dismiss() } + .show() } } @@ -700,7 +690,7 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { val insightId = this.insightId ?: error("Property 'insightId' not set.") val annotation = this.annotation ?: error("Property 'annotation' not set.") - presenter.annotateInsight(insightId, annotation) + lifecycleScope.launch { presenter.annotateInsight(insightId, annotation) } Log.d(LOG_TAG, "Annotation $annotation received for insight $insightId") binding.productQuestionLayout.visibility = View.GONE @@ -709,74 +699,81 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { override fun showAnnotatedInsightToast(annotationResponse: AnnotationResponse) { if (annotationResponse.status == "updated" && activity != null) { - Snackbar.make(binding.root, R.string.product_question_submit_message, BaseTransientBottomBar.LENGTH_SHORT).show() + Snackbar.make(binding.root, R.string.product_question_submit_message, LENGTH_SHORT).show() } } - override fun showLabels(labelNames: List) { - binding.labelsText.text = SpannableStringBuilder() - .bold { append(getString(R.string.txtLabels)) } - binding.labelsText.isClickable = true - binding.labelsText.movementMethod = LinkMovementMethod.getInstance() - binding.labelsText.append(" ") - labelNames.dropLast(1).forEach { - binding.labelsText.append(getLabelTag(it)) - binding.labelsText.append(", ") + override fun showLabelsState(state: ProductInfoState>) { + requireActivity().runOnUiThread { + when (state) { + is ProductInfoState.Loading -> binding.labelsText.append(getString(R.string.txtLoading)) + is ProductInfoState.Empty -> { + binding.labelsText.visibility = View.GONE + binding.labelsIcon.visibility = View.GONE + } + is ProductInfoState.Data -> { + binding.labelsText.isClickable = true + binding.labelsText.movementMethod = LinkMovementMethod.getInstance() + binding.labelsText.text = SpannableStringBuilder() + .bold { append(getString(R.string.txtLabels)) } + .append(" ") + .append(state.data.joinToString { getLabelTag(it) }) + } + } } - binding.labelsText.append(getLabelTag(labelNames.last())) } - override fun showCategoriesState(state: ProductInfoState) = requireActivity().runOnUiThread { + override fun showCategoriesState(state: ProductInfoState>) = requireActivity().runOnUiThread { when (state) { - ProductInfoState.LOADING -> if (context != null) { + is ProductInfoState.Loading -> if (context != null) { binding.categoriesText.append(getString(R.string.txtLoading)) } - ProductInfoState.EMPTY -> { + is ProductInfoState.Empty -> { binding.categoriesText.visibility = View.GONE binding.categoriesIcon.visibility = View.GONE } - } - } - - override fun showLabelsState(state: ProductInfoState) { - requireActivity().runOnUiThread { - when (state) { - ProductInfoState.LOADING -> binding.labelsText.append(getString(R.string.txtLoading)) - ProductInfoState.EMPTY -> { - binding.labelsText.visibility = View.GONE - binding.labelsIcon.visibility = View.GONE + is ProductInfoState.Data -> { + val categories = state.data + if (categories.isEmpty()) { + binding.categoriesLayout.visibility = View.GONE + return@runOnUiThread } + CategoryProductHelper.showCategories( + this, + binding.categoriesText, + binding.textCategoryAlcoholAlert, + categories, + wikidataClient, + ) } } } - private fun getEmbUrl(embTag: String): String? { - if (mTagDao.queryBuilder().where(TagDao.Properties.Id.eq(embTag)).list().isEmpty()) return null - return mTagDao.queryBuilder().where(TagDao.Properties.Id.eq(embTag)).unique().url + private suspend fun getEmbUrl(embTag: String): String? = withContext(IO) { + if (mTagDao.queryBuilder().where(TagDao.Properties.Id.eq(embTag)).list().isEmpty()) null + else mTagDao.queryBuilder().where(TagDao.Properties.Id.eq(embTag)).unique().url } private fun getEmbCode(embTag: String) = mTagDao.queryBuilder().where(TagDao.Properties.Id.eq(embTag)).unique()?.name ?: embTag private fun getLabelTag(label: LabelName): CharSequence { - val spannableStringBuilder = SpannableStringBuilder() val clickableSpan = object : ClickableSpan() { override fun onClick(view: View) { if (label.isWikiDataIdPresent) { - wikidataClient.doSomeThing(label.wikiDataId).subscribe { result -> + lifecycleScope.launch { + val result = wikidataClient.doSomeThing(label.wikiDataId) val activity = activity if (activity?.isFinishing == false) { showBottomSheet(result, label, activity.supportFragmentManager) } - }.addTo(disp) + } } else { ProductSearchActivity.start(requireContext(), SearchType.LABEL, label.labelTag, label.name) } } } - spannableStringBuilder.append(label.name) - spannableStringBuilder.setSpan(clickableSpan, 0, spannableStringBuilder.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - return spannableStringBuilder + return SpannableStringBuilder().inSpans(clickableSpan) { append(label.name) } } @@ -790,13 +787,13 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { if (requireActivity().isUserSet()) { editProduct() } else { - buildSignInDialog(requireActivity()) - .onPositive { d, _ -> + buildSignInDialog(requireActivity(), + onPositive = { d, _ -> d.dismiss() loginThenEditLauncher.launch(null) - } - .onNegative { d, _ -> d.dismiss() } - .show() + }, + onNegative = { d, _ -> d.dismiss() } + ).show() } } @@ -805,12 +802,14 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { if (requireActivity().isUserSet()) { editProductNutriscore() } else { - buildSignInDialog(requireActivity()) - .onPositive { d, _ -> + buildSignInDialog( + requireActivity(), + onPositive = { d, _ -> d.dismiss() loginThenEditNutrition.launch(null) - } - .onNegative { d, _ -> d.dismiss() } + }, + onNegative = { d, _ -> d.dismiss() } + ).show() } } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/summary/SummaryProductPresenter.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/summary/SummaryProductPresenter.kt index 1be7bbf31b26..25e6aeae3d30 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/summary/SummaryProductPresenter.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/view/summary/SummaryProductPresenter.kt @@ -16,13 +16,14 @@ package openfoodfacts.github.scrachx.openfood.features.product.view.summary import android.util.Log -import io.reactivex.Single -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.rxkotlin.addTo -import io.reactivex.rxkotlin.toObservable -import io.reactivex.schedulers.Schedulers -import openfoodfacts.github.scrachx.openfood.AppFlavors +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.rx2.await +import kotlinx.coroutines.rx2.awaitSingleOrNull +import kotlinx.coroutines.withContext +import openfoodfacts.github.scrachx.openfood.AppFlavors.OBF +import openfoodfacts.github.scrachx.openfood.AppFlavors.OFF +import openfoodfacts.github.scrachx.openfood.AppFlavors.OPFF import openfoodfacts.github.scrachx.openfood.AppFlavors.isFlavors import openfoodfacts.github.scrachx.openfood.models.AnnotationAnswer import openfoodfacts.github.scrachx.openfood.models.Product @@ -35,163 +36,132 @@ class SummaryProductPresenter( private val view: ISummaryProductPresenter.View, private val productRepository: ProductRepository ) : ISummaryProductPresenter.Actions { - private val disp = CompositeDisposable() - override fun loadAdditives() { + override suspend fun loadAdditives() { val additivesTags = product.additivesTags if (additivesTags.isEmpty()) { - view.showAdditivesState(ProductInfoState.EMPTY) + view.showAdditivesState(ProductInfoState.Empty) return } - - additivesTags.toObservable() - .flatMapSingle { tag -> - productRepository.getAdditiveByTagAndLanguageCode(tag, languageCode).map { it to tag } - }.flatMapSingle { (categoryName, tag) -> - if (categoryName.isNotNull) Single.just(categoryName) else productRepository.getAdditiveByTagAndDefaultLanguageCode(tag) - } - .filter { it.isNotNull } - .toList() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnSubscribe { view.showAdditivesState(ProductInfoState.LOADING) } - .doOnError { - Log.e(SummaryProductPresenter::class.simpleName, "loadAdditives", it) - view.showAdditivesState(ProductInfoState.EMPTY) - } - .subscribe { additives -> - if (additives.isEmpty()) view.showAdditivesState(ProductInfoState.EMPTY) else view.showAdditives(additives) - }.addTo(disp) + view.showAdditivesState(ProductInfoState.Loading) + val additives = try { + additivesTags.map { tag -> + val categoryName = productRepository.getAdditiveByTagAndLanguageCode(tag, languageCode).await() + if (categoryName.isNotNull) categoryName + else productRepository.getAdditiveByTagAndDefaultLanguageCode(tag).await() + }.filter { it.isNotNull } + } catch (err: Exception) { + Log.e(SummaryProductPresenter::class.simpleName, "loadAdditives", err) + view.showAdditivesState(ProductInfoState.Empty) + return + } + if (additives.isEmpty()) view.showAdditivesState(ProductInfoState.Empty) + else view.showAdditivesState(ProductInfoState.Data(additives)) } - override fun loadAllergens(runIfError: (() -> Unit)?) { - productRepository.getAllergensByEnabledAndLanguageCode(true, languageCode) - .observeOn(AndroidSchedulers.mainThread()) - .doOnError { - runIfError?.invoke() - Log.e(SummaryProductPresenter::class.simpleName, "LoadAllergens", it) - } - .subscribe { allergens -> view.showAllergens(allergens) } - .addTo(disp) + override suspend fun loadAllergens() { + val allergens = productRepository.getAllergensByEnabledAndLanguageCode(true, languageCode).await() + withContext(Main) { view.showAllergens(allergens) } } - override fun loadCategories() { + override suspend fun loadCategories() { val categoriesTags = product.categoriesTags if (!categoriesTags.isNullOrEmpty()) { - categoriesTags.toObservable() - .flatMapSingle { tag -> - productRepository.getCategoryByTagAndLanguageCode(tag, languageCode).map { it to tag } - } - .flatMapSingle { (categoryName, tag) -> - if (categoryName.isNotNull) Single.just(categoryName) else productRepository.getCategoryByTagAndLanguageCode(tag) - } - .toList() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnSubscribe { view.showCategoriesState(ProductInfoState.LOADING) } - .doOnError { - Log.e(SummaryProductPresenter::class.java.simpleName, "loadCategories", it) - view.showCategoriesState(ProductInfoState.EMPTY) - } - .subscribe { categories -> - if (categories.isEmpty()) { - view.showCategoriesState(ProductInfoState.EMPTY) - } else { - view.showCategories(categories) - } - }.addTo(disp) + view.showCategoriesState(ProductInfoState.Loading) + val categories = try { + categoriesTags.map { tag -> + val categoryName = productRepository.getCategoryByTagAndLanguageCode(tag, languageCode).await() + if (categoryName.isNotNull) categoryName + else productRepository.getCategoryByTagAndLanguageCode(tag).await() + } + } catch (err: Exception) { + Log.e(SummaryProductPresenter::class.java.simpleName, "loadCategories", err) + view.showCategoriesState(ProductInfoState.Empty) + return + } + if (categories.isEmpty()) { + view.showCategoriesState(ProductInfoState.Empty) + } else { + view.showCategoriesState(ProductInfoState.Data(categories)) + } + } else { - view.showCategoriesState(ProductInfoState.EMPTY) + view.showCategoriesState(ProductInfoState.Empty) } } - override fun loadLabels() { + override suspend fun loadLabels() { val labelsTags = product.labelsTags if (labelsTags != null && labelsTags.isNotEmpty()) { - labelsTags.toObservable() - .flatMapSingle { tag -> - productRepository.getLabelByTagAndLanguageCode(tag, languageCode).map { it to tag } - } - .flatMapSingle { (labelName, tag) -> - if (labelName.isNotNull) Single.just(labelName) else productRepository.getLabelByTagAndDefaultLanguageCode(tag) - } - .filter { it.isNotNull } - .toList() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnSubscribe { view.showLabelsState(ProductInfoState.LOADING) } - .doOnError { - Log.e(SummaryProductPresenter::class.java.simpleName, "loadLabels", it) - view.showLabelsState(ProductInfoState.EMPTY) - } - .subscribe { labels -> - if (labels.isEmpty()) view.showLabelsState(ProductInfoState.EMPTY) - else view.showLabels(labels) - }.addTo(disp) + view.showLabelsState(ProductInfoState.Loading) + val labels = try { + labelsTags.map { tag -> + val labelName = productRepository.getLabelByTagAndLanguageCode(tag, languageCode).await() + if (labelName.isNotNull) labelName + else productRepository.getLabelByTagAndDefaultLanguageCode(tag).await() + }.filter { it.isNotNull } + } catch (err: Exception) { + Log.e(SummaryProductPresenter::class.java.simpleName, "loadLabels", err) + view.showLabelsState(ProductInfoState.Empty) + return + } + + if (labels.isEmpty()) view.showLabelsState(ProductInfoState.Empty) + else view.showLabelsState(ProductInfoState.Data(labels)) } else { - view.showLabelsState(ProductInfoState.EMPTY) + view.showLabelsState(ProductInfoState.Empty) } } - override fun loadProductQuestion() { - productRepository.getProductQuestion(product.code, languageCode) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnError { Log.e(this@SummaryProductPresenter::class.simpleName, "loadProductQuestion", it) } - .subscribe { question -> view.showProductQuestion(question) } - .addTo(disp) + override suspend fun loadProductQuestion() { + val question = withContext(IO) { productRepository.getProductQuestion(product.code, languageCode) } + .awaitSingleOrNull() ?: return + withContext(Main) { view.showProductQuestion(question) } } - override fun loadAnalysisTags() { - if (!isFlavors(AppFlavors.OFF, AppFlavors.OBF, AppFlavors.OPFF)) return + override suspend fun loadAnalysisTags() { + if (!isFlavors(OFF, OBF, OPFF)) return val analysisTags = product.ingredientsAnalysisTags + if (analysisTags.isNotEmpty()) { - analysisTags.toObservable() - .flatMapMaybe { productRepository.getAnalysisTagConfigByTagAndLanguageCode(it, languageCode) } - .toList() - .doOnSubscribe { view.showLabelsState(ProductInfoState.LOADING) } - .observeOn(AndroidSchedulers.mainThread()) - .doOnError { - Log.e(SummaryProductPresenter::class.java.simpleName, "loadAnalysisTags", it) - view.showLabelsState(ProductInfoState.EMPTY) - } - .subscribe { analysisTagConfigs -> - if (analysisTagConfigs.isEmpty()) { - view.showLabelsState(ProductInfoState.EMPTY) - } else { - view.showAnalysisTags(analysisTagConfigs) - } - }.addTo(disp) + view.showLabelsState(ProductInfoState.Loading) + val analysisTagConfigs = try { + analysisTags.mapNotNull { + productRepository.getAnalysisTagConfigByTagAndLanguageCode(it, languageCode).awaitSingleOrNull() + } + } catch (err: Exception) { + Log.e(SummaryProductPresenter::class.java.simpleName, "loadAnalysisTags", err) + view.showLabelsState(ProductInfoState.Empty) + return + } + if (analysisTagConfigs.isEmpty()) { + view.showLabelsState(ProductInfoState.Empty) + } else { + view.showAnalysisTags(analysisTagConfigs) + } + } else { - productRepository.getUnknownAnalysisTagConfigsByLanguageCode(languageCode) - .doOnSubscribe { view.showLabelsState(ProductInfoState.LOADING) } - .observeOn(AndroidSchedulers.mainThread()) - .doOnError { - Log.e(SummaryProductPresenter::class.java.simpleName, "loadAnalysisTags", it) - view.showLabelsState(ProductInfoState.EMPTY) - } - .subscribe { analysisTagConfigs -> - if (analysisTagConfigs.isEmpty()) { - view.showLabelsState(ProductInfoState.EMPTY) - } else { - view.showAnalysisTags(analysisTagConfigs) - } - }.addTo(disp) + view.showLabelsState(ProductInfoState.Loading) + val analysisTagConfigs = try { + productRepository.getUnknownAnalysisTagConfigsByLanguageCode(languageCode).await() + } catch (err: Exception) { + Log.e(SummaryProductPresenter::class.java.simpleName, "loadAnalysisTags", err) + view.showLabelsState(ProductInfoState.Empty) + return + } + if (analysisTagConfigs.isEmpty()) { + view.showLabelsState(ProductInfoState.Empty) + } else { + view.showAnalysisTags(analysisTagConfigs) + } + } } - override fun annotateInsight(insightId: String, annotation: AnnotationAnswer) { - productRepository.annotateInsight(insightId, annotation) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnError { Log.e(this@SummaryProductPresenter::class.simpleName, "annotateInsight", it) } - .subscribe { response -> view.showAnnotatedInsightToast(response) } - .addTo(disp) + override suspend fun annotateInsight(insightId: String, annotation: AnnotationAnswer) { + val response = withContext(IO) { productRepository.annotateInsight(insightId, annotation).await() } + withContext(Main) { view.showAnnotatedInsightToast(response) } } - - override fun dispose() = disp.dispose() - - override fun isDisposed() = disp.isDisposed } 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 f2e28d864911..2fb725b1d8a2 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 @@ -53,7 +53,6 @@ import com.squareup.picasso.Picasso import dagger.hilt.android.AndroidEntryPoint import io.reactivex.Completable import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable import kotlinx.coroutines.* import kotlinx.coroutines.rx2.await @@ -132,10 +131,8 @@ class ContinuousScanActivity : BaseActivity(), IProductView { private val bottomSheetCallback by lazy { QuickViewCallback(this) } private val cameraPref by lazy { getSharedPreferences("camera", 0) } - private val settings by lazy { getSharedPreferences("prefs", 0) } - private val commonDisp = CompositeDisposable() private var productDisp: Job? = null private var hintBarcodeDisp: Disposable? = null @@ -190,7 +187,6 @@ class ContinuousScanActivity : BaseActivity(), IProductView { // Dispose the previous call if not ended. productDisp?.cancel() - summaryProductPresenter?.dispose() // First, try to show if we have an offline saved product in the db offlineSavedProduct = offlineProductService.getOfflineProductByBarcode(barcode).also { product -> @@ -284,7 +280,7 @@ class ContinuousScanActivity : BaseActivity(), IProductView { // Set product name, prefer offline if (offlineSavedProduct != null && !offlineSavedProduct?.name.isNullOrEmpty()) { binding.quickViewName.text = offlineSavedProduct!!.name - } else if (product.productName == null || product.productName == "") { + } else if (product.productName == null || product.productName.isEmpty()) { binding.quickViewName.setText(R.string.productNameNull) } else { binding.quickViewName.text = product.productName @@ -389,8 +385,14 @@ class ContinuousScanActivity : BaseActivity(), IProductView { binding.quickViewTags.adapter = adapter } }, productRepository).also { - it.loadAllergens { binding.callToActionImageProgress.visibility = View.GONE } - it.loadAnalysisTags() + lifecycleScope.launch { + try { + it.loadAllergens() + } catch (err: Exception) { + binding.callToActionImageProgress.visibility = View.GONE + } + } + lifecycleScope.launch { it.loadAnalysisTags() } } } @@ -606,12 +608,10 @@ class ContinuousScanActivity : BaseActivity(), IProductView { } override fun onDestroy() { - summaryProductPresenter?.dispose() mlKitView.detach() // Dispose all RxJava disposable hintBarcodeDisp?.dispose() - commonDisp.dispose() // Remove bottom sheet callback as it uses binding quickViewBehavior.removeBottomSheetCallback(bottomSheetCallback) 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 0e98e6c7239b..d5a0a2750386 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 @@ -24,18 +24,33 @@ import androidx.core.content.res.ResourcesCompat import openfoodfacts.github.scrachx.openfood.R import openfoodfacts.github.scrachx.openfood.databinding.DialogProductQuestionBinding -class QuestionDialog(mContext: Context) { - private val binding = DialogProductQuestionBinding.inflate(LayoutInflater.from(mContext)) - private val mDialog = Dialog(mContext, R.style.QuestionDialog).apply { +class QuestionDialog(context: Context) { + private val binding = DialogProductQuestionBinding.inflate(LayoutInflater.from(context)) + private val dialog = Dialog(context, R.style.QuestionDialog).apply { requestWindowFeature(android.view.Window.FEATURE_NO_TITLE) setContentView(binding.root) } - private val mAmbiguityFeedbackIcon = ResourcesCompat.getDrawable(mContext.resources, R.drawable.ic_help_black_24dp, mContext.theme) - private val mAmbiguityFeedbackText = mContext.resources.getString(R.string.product_question_ambiguous_response) - private val mNegativeFeedbackIcon = ResourcesCompat.getDrawable(mContext.resources, R.drawable.ic_cancel_black_24dp, mContext.theme) - private val mNegativeFeedbackText = mContext.resources.getString(R.string.product_question_negative_response) - private val mPositiveFeedbackIcon = ResourcesCompat.getDrawable(mContext.resources, R.drawable.ic_check_circle_black_24dp, mContext.theme) - private val mPositiveFeedbackText = mContext.resources.getString(R.string.product_question_positive_response) + + private val mAmbiguityFeedbackIcon = ResourcesCompat.getDrawable( + context.resources, + R.drawable.ic_help_black_24dp, + context.theme + ) + private val mAmbiguityFeedbackText = context.resources.getString(R.string.product_question_ambiguous_response) + + private val mNegativeFeedbackIcon = ResourcesCompat.getDrawable( + context.resources, + R.drawable.ic_cancel_black_24dp, + context.theme + ) + private val mNegativeFeedbackText = context.resources.getString(R.string.product_question_negative_response) + + private val mPositiveFeedbackIcon = ResourcesCompat.getDrawable( + context.resources, + R.drawable.ic_check_circle_black_24dp, + context.theme + ) + private val mPositiveFeedbackText = context.resources.getString(R.string.product_question_positive_response) @ColorRes @@ -51,19 +66,19 @@ class QuestionDialog(mContext: Context) { init { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - mDialog.window?.setLayout( - (mContext.resources.displayMetrics.widthPixels * 0.90).toInt(), - (mContext.resources.displayMetrics.heightPixels * 0.50).toInt() + dialog.window?.setLayout( + (context.resources.displayMetrics.widthPixels * 0.90).toInt(), + (context.resources.displayMetrics.heightPixels * 0.50).toInt() ) } } private fun initiateListeners() { - binding.postiveFeedbackLayout.setOnClickListener { onPositiveFeedbackClicked() } - binding.negativeFeedbackLayout.setOnClickListener { onNegativeFeedbackClicked() } - binding.ambiguityFeedbackLayout.setOnClickListener { onAmbiguityFeedbackClicked() } - mDialog.setOnCancelListener { onCancelListener?.invoke(this) } + binding.postiveFeedbackLayout.setOnClickListener { onPositiveFeedback?.invoke(this) } + binding.negativeFeedbackLayout.setOnClickListener { onNegativeFeedback?.invoke(this) } + binding.ambiguityFeedbackLayout.setOnClickListener { onAmbiguityFeedback?.invoke(this) } + dialog.setOnCancelListener { onCancelListener?.invoke(this) } } fun show() { @@ -77,23 +92,9 @@ class QuestionDialog(mContext: Context) { binding.ambiguityFeedbackText.text = mAmbiguityFeedbackText binding.ambiguityFeedbackIcon.setImageDrawable(mAmbiguityFeedbackIcon) binding.feedbackBodyLayout.setBackgroundResource(backgroundColor) - mDialog.show() + dialog.show() } - fun dismiss() = mDialog.dismiss() - - private fun onPositiveFeedbackClicked() { - onPositiveFeedback?.let { it(this) } - } - - private fun onNegativeFeedbackClicked() { - onNegativeFeedback?.let { it(this) } - - } - - private fun onAmbiguityFeedbackClicked() { - onAmbiguityFeedback?.let { it(this) } - } - + fun dismiss() = dialog.dismiss() } \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/Question.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/Question.kt index 09922f5f0732..8d063b53f299 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/Question.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/Question.kt @@ -7,14 +7,14 @@ import java.io.Serializable @JsonIgnoreProperties(ignoreUnknown = true) data class Question( - @JsonProperty("barcode") val code: String? = null, - @JsonProperty("type") val type: String? = null, - @JsonProperty("value") val value: String? = null, - @JsonProperty("question") val questionText: String? = null, - @JsonProperty("insight_id") val insightId: String? = null, - @JsonProperty("insight_type") val insightType: String? = null, - @JsonProperty("source_image_url") val sourceImageUrl: String? = null, - @JsonProperty("image_url") val imageUrl: String? = null, + @JsonProperty("barcode") val code: String? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("value") val value: String? = null, + @JsonProperty("question") val questionText: String? = null, + @JsonProperty("insight_id") val insightId: String? = null, + @JsonProperty("insight_type") val insightType: String? = null, + @JsonProperty("source_image_url") val sourceImageUrl: String? = null, + @JsonProperty("image_url") val imageUrl: String? = null, ) : Serializable { @JsonIgnore fun isEmpty() = questionText.isNullOrEmpty() diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/entities/allergen/AllergenHelper.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/entities/allergen/AllergenHelper.kt index c7c44e0386b7..7d282d9f4cf4 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/entities/allergen/AllergenHelper.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/entities/allergen/AllergenHelper.kt @@ -3,7 +3,6 @@ package openfoodfacts.github.scrachx.openfood.models.entities.allergen import openfoodfacts.github.scrachx.openfood.models.Product import openfoodfacts.github.scrachx.openfood.network.ApiFields import org.jetbrains.annotations.Contract -import java.util.* object AllergenHelper { @Contract(" -> new") @@ -15,17 +14,17 @@ object AllergenHelper { if (ApiFields.StateTags.INGREDIENTS_COMPLETED !in product.statesTags) return Data(true, emptyList()) - val productAllergens = HashSet(product.allergensHierarchy) - .also { it += product.tracesTags } + val productAllergens = (product.allergensHierarchy + product.tracesTags).toSet() - val allergenMatch = TreeSet() - userAllergens.filter { productAllergens.contains(it.allergenTag) } - .mapTo(allergenMatch) { it.name } + val matchingAllergens = userAllergens + .filter { it.allergenTag in productAllergens } + .map { it.name } + .toSet() - return Data(false, allergenMatch.toList()) + return Data(false, matchingAllergens.toList()) } - data class Data(val incomplete: Boolean, val allergens: List) { + data class Data(val incomplete: Boolean, val allergens: List) { fun isEmpty() = !incomplete && allergens.isEmpty() } } \ 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 8db7eac7706b..31d3ddd8fbfb 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 @@ -6,14 +6,13 @@ import android.content.Intent import android.util.Log import android.widget.Toast import androidx.core.content.edit -import com.afollestad.materialdialogs.MaterialDialog import com.fasterxml.jackson.databind.JsonNode +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.qualifiers.ApplicationContext -import io.reactivex.Completable import io.reactivex.Single -import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.launch import kotlinx.coroutines.rx2.await import kotlinx.coroutines.rx2.rxCompletable import kotlinx.coroutines.rx2.rxSingle @@ -90,13 +89,11 @@ class OpenFoodAPIClient @Inject constructor( return fieldsSet.joinToString(",") } - private fun productNotFoundDialogBuilder(activity: Activity, barcode: String): MaterialDialog.Builder = - MaterialDialog.Builder(activity) - .title(R.string.txtDialogsTitle) - .content(R.string.txtDialogsContent) - .positiveText(R.string.txtYes) - .negativeText(R.string.txtNo) - .onPositive { _, _ -> + private fun productNotFoundDialogBuilder(activity: Activity, barcode: String): MaterialAlertDialogBuilder = + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.txtDialogsTitle) + .setMessage(R.string.txtDialogsContent) + .setPositiveButton(R.string.txtYes) { _, _ -> activity.startActivity(Intent(activity, ProductEditActivity::class.java).apply { putExtra(KEY_EDIT_PRODUCT, Product().apply { code = barcode @@ -105,6 +102,7 @@ class OpenFoodAPIClient @Inject constructor( }) activity.finish() } + .setNegativeButton(R.string.txtNo) { _, _ -> } /** * Open the product activity if the barcode exist. @@ -116,7 +114,8 @@ class OpenFoodAPIClient @Inject constructor( val fields = Keys.PRODUCT_IMAGES_FIELDS.toMutableSet().also { it += Keys.lcProductNameKey(localeManager.getLanguage()) }.joinToString(",") - return@rxSingle rawApi.getProductByBarcode( + + rawApi.getProductByBarcode( barcode, fields, localeManager.getLanguage(), @@ -150,10 +149,10 @@ class OpenFoodAPIClient @Inject constructor( withContext(Main) { if (state.status == 0L) { productNotFoundDialogBuilder(activity, barcode) - .onNegative { _, _ -> activity.onBackPressed() } + .setNegativeButton(R.string.txtNo) { _, _ -> activity.onBackPressed() } .show() } else { - addToHistory(state.product!!).subscribe() + launch { addToHistory(state.product!!) } startProductViewActivity(activity, state) } } @@ -177,12 +176,13 @@ class OpenFoodAPIClient @Inject constructor( } - fun searchProductsByName(name: String, page: Int) = rxSingle { + fun searchProductsByName(name: String, page: Int) = rxSingle(IO) { rawApi.searchProductByName(name, fieldsToFetchFacets, page) } - fun getProductsByCountry(country: String, page: Int) = + fun getProductsByCountry(country: String, page: Int) = rxSingle(IO) { rawApi.getProductsByCountry(country, page, fieldsToFetchFacets) + } /** * Returns a map for images uploaded for product/ingredients/nutrition/other images @@ -216,11 +216,13 @@ class OpenFoodAPIClient @Inject constructor( return imgMap } - fun getProductsByCategory(category: String, page: Int) = + fun getProductsByCategory(category: String, page: Int) = rxSingle { rawApi.getProductByCategory(category, page, fieldsToFetchFacets) + } - fun getProductsByLabel(label: String, page: Int) = + fun getProductsByLabel(label: String, page: Int) = rxSingle { rawApi.getProductsByLabel(label, page, fieldsToFetchFacets) + } /** * Add a product to ScanHistory asynchronously @@ -229,9 +231,9 @@ class OpenFoodAPIClient @Inject constructor( daoSession.historyProductDao.addToHistory(product, localeManager.getLanguage()) } - fun getProductsByContributor(contributor: String, page: Int) = + fun getProductsByContributor(contributor: String, page: Int) = rxSingle(IO) { rawApi.getProductsByContributor(contributor, page, fieldsToFetchFacets) - .subscribeOn(Schedulers.io()) + } /** * upload images in offline mode @@ -269,11 +271,13 @@ class OpenFoodAPIClient @Inject constructor( } } - fun getProductsByPackaging(packaging: String, page: Int): Single = + fun getProductsByPackaging(packaging: String, page: Int): Single = rxSingle { rawApi.getProductsByPackaging(packaging, page, fieldsToFetchFacets) + } - fun getProductsByStore(store: String, page: Int): Single = + fun getProductsByStore(store: String, page: Int): Single = rxSingle(IO) { rawApi.getProductByStores(store, page, fieldsToFetchFacets) + } /** * Search for products using bran name @@ -281,7 +285,7 @@ class OpenFoodAPIClient @Inject constructor( * @param brand search query for product * @param page page numbers */ - fun getProductsByBrand(brand: String, page: Int): Single = rxSingle(IO) { + fun getProductsByBrand(brand: String, page: Int) = rxSingle(IO) { rawApi.getProductByBrands(brand, page, fieldsToFetchFacets) } @@ -309,16 +313,15 @@ class OpenFoodAPIClient @Inject constructor( } } - private fun setDefaultImageFromServerResponse(body: JsonNode, image: ProductImage): Completable { + private suspend fun setDefaultImageFromServerResponse(body: JsonNode, image: ProductImage) { val queryMap = getUserInfo() + listOf( IMG_ID to body["image"][IMG_ID].asText(), "id" to body["imagefield"].asText() ) - return rawApi.editImage(image.barcode, queryMap).flatMapCompletable { node -> - if (node[Keys.STATUS].asText() != "status ok") throw IOException(node["error"].asText()) - else Completable.complete() - } + val node = rawApi.editImage(image.barcode, queryMap).await() + + if (node[Keys.STATUS].asText() != "status ok") throw IOException(node["error"].asText()) } suspend fun editImage(code: String, imgMap: MutableMap) = withContext(IO) { @@ -332,11 +335,12 @@ class OpenFoodAPIClient @Inject constructor( */ suspend fun unSelectImage(code: String, field: ProductImageField, language: String) = withContext(IO) { val imgMap = getUserInfo() + (IMAGE_STRING_ID to getImageStringKey(field, language)) - return@withContext rawApi.unSelectImage(code, imgMap) + rawApi.unSelectImage(code, imgMap) } - fun getProductsByOrigin(origin: String, page: Int) = + fun getProductsByOrigin(origin: String, page: Int) = rxSingle(IO) { rawApi.getProductsByOrigin(origin, page, fieldsToFetchFacets) + } suspend fun syncOldHistory() = withContext(IO) { val fields = listOf( @@ -382,11 +386,13 @@ class OpenFoodAPIClient @Inject constructor( } } - fun getInfoAddedIncompleteProductsSingle(contributor: String, page: Int) = + fun getInfoAddedIncompleteProductsSingle(contributor: String, page: Int) = rxSingle(IO) { rawApi.getInfoAddedIncompleteProductsSingle(contributor, page) + } - fun getProductsByManufacturingPlace(manufacturingPlace: String, page: Int) = + fun getProductsByManufacturingPlace(manufacturingPlace: String, page: Int) = rxSingle(IO) { rawApi.getProductsByManufacturingPlace(manufacturingPlace, page, fieldsToFetchFacets) + } /** * call API service to return products using Additives @@ -394,29 +400,38 @@ class OpenFoodAPIClient @Inject constructor( * @param additive search query for products * @param page number of pages */ - fun getProductsByAdditive(additive: String, page: Int) = + fun getProductsByAdditive(additive: String, page: Int) = rxSingle(IO) { rawApi.getProductsByAdditive(additive, page, fieldsToFetchFacets) + } - fun getProductsByAllergen(allergen: String, page: Int) = + fun getProductsByAllergen(allergen: String, page: Int) = rxSingle(IO) { rawApi.getProductsByAllergen(allergen, page, fieldsToFetchFacets) + } - fun getToBeCompletedProductsByContributor(contributor: String, page: Int) = + fun getToBeCompletedProductsByContributor(contributor: String, page: Int) = rxSingle(IO) { rawApi.getToBeCompletedProductsByContributor(contributor, page) + } - fun getPicturesContributedProducts(contributor: String, page: Int) = + fun getPicturesContributedProducts(contributor: String, page: Int) = rxSingle(IO) { rawApi.getPicturesContributedProducts(contributor, page) + } - fun getPicturesContributedIncompleteProducts(contributor: String?, page: Int) = + fun getPicturesContributedIncompleteProducts(contributor: String?, page: Int) = rxSingle(IO) { rawApi.getPicturesContributedIncompleteProducts(contributor, page) + } - fun getInfoAddedProducts(contributor: String?, page: Int) = + fun getInfoAddedProducts(contributor: String?, page: Int) = rxSingle(IO) { rawApi.getInfoAddedProducts(contributor, page) + } + - fun getIncompleteProducts(page: Int) = + fun getIncompleteProducts(page: Int) = rxSingle(IO) { rawApi.getIncompleteProducts(page, fieldsToFetchFacets) + } - fun getProductsByStates(state: String?, page: Int) = + fun getProductsByStates(state: String?, page: Int) = rxSingle(IO) { rawApi.getProductsByState(state, page, fieldsToFetchFacets) + } companion object { val MIME_TEXT: MediaType = MediaType.get("text/plain") @@ -474,29 +489,23 @@ class OpenFoodAPIClient @Inject constructor( } /** - * Fill the given [Map] with user info (username, password, comment) - * - * @param imgMap The map to fill - * + * Return a [Map] with user info (username, password, comment) */ - @Deprecated("Use the += operator with getUserInfo()") - private fun addUserInfo(imgMap: MutableMap = mutableMapOf()): Map { - val settings = context.getLoginPreferences() + 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 } - private fun getUserInfo() = addUserInfo() - /** * Uploads comment by users * @@ -527,3 +536,4 @@ class OpenFoodAPIClient @Inject constructor( 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/network/WikiDataApiClient.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/WikiDataApiClient.kt index e09ba416037d..8cd614466713 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/WikiDataApiClient.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/WikiDataApiClient.kt @@ -1,5 +1,6 @@ package openfoodfacts.github.scrachx.openfood.network +import com.fasterxml.jackson.databind.JsonNode import openfoodfacts.github.scrachx.openfood.network.services.WikidataAPI import javax.inject.Inject import javax.inject.Singleton @@ -19,5 +20,5 @@ class WikiDataApiClient @Inject constructor( * * @param code WikiData ID of additive/ingredient/category/label */ - fun doSomeThing(code: String) = wikidataAPI.getWikiCategory(code).map { it["entities"][code] } + suspend fun doSomeThing(code: String): JsonNode = wikidataAPI.getWikiCategory(code)["entities"][code] } \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/services/ProductsAPI.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/services/ProductsAPI.kt index 8929709db0d0..caba9c8b943f 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/services/ProductsAPI.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/services/ProductsAPI.kt @@ -47,10 +47,10 @@ interface ProductsAPI { @GET("$API_P/product/{barcode}.json") fun getProductByBarcode( - @Path("barcode") barcode: String, - @Query("fields") fields: String, - @Query("lc") locale: String, - @Header("User-Agent") header: String + @Path("barcode") barcode: String, + @Query("fields") fields: String, + @Query("lc") locale: String, + @Header("User-Agent") header: String ): Single /** @@ -65,11 +65,11 @@ interface ProductsAPI { @FormUrlEncoded @POST("cgi/product_jqm2.pl") - fun saveProduct( - @Field(ApiFields.Keys.BARCODE) code: String?, - @FieldMap parameters: Map?, - @Field(ApiFields.Keys.USER_COMMENT) comment: String? - ): Single + suspend fun saveProduct( + @Field(ApiFields.Keys.BARCODE) code: String?, + @FieldMap parameters: Map?, + @Field(ApiFields.Keys.USER_COMMENT) comment: String? + ): ProductState @GET("cgi/search.pl?search_simple=1&json=1&action=process") suspend fun searchProductByName( @@ -97,14 +97,14 @@ interface ProductsAPI { @GET("/cgi/product_image_crop.pl") fun editImage( - @Query(ApiFields.Keys.BARCODE) code: String, - @QueryMap fields: Map + @Query(ApiFields.Keys.BARCODE) code: String, + @QueryMap fields: Map ): Single @GET("/cgi/ingredients.pl?process_image=1&ocr_engine=google_cloud_vision") fun performOCR( - @Query(ApiFields.Keys.BARCODE) code: String, - @Query("id") imgId: String + @Query(ApiFields.Keys.BARCODE) code: String, + @Query("id") imgId: String ): Single @GET("cgi/suggest.pl") @@ -128,74 +128,74 @@ interface ProductsAPI { * @param page number of pages */ @GET("additive/{additive}/{page}.json") - fun getProductsByAdditive( - @Path("additive") additive: String, - @Path("page") page: Int, - @Query("fields") fields: String - ): Single + suspend fun getProductsByAdditive( + @Path("additive") additive: String, + @Path("page") page: Int, + @Query("fields") fields: String + ): Search @GET("allergen/{allergen}/{page}.json") - fun getProductsByAllergen( - @Path("allergen") allergen: String, - @Path("page") page: Int, - @Query("fields") fields: String - ): Single + suspend fun getProductsByAllergen( + @Path("allergen") allergen: String, + @Path("page") page: Int, + @Query("fields") fields: String + ): Search @GET("country/{country}/{page}.json") - fun getProductsByCountry( - @Path("country") country: String, - @Path("page") page: Int, - @Query("fields") fields: String - ): Single + suspend fun getProductsByCountry( + @Path("country") country: String, + @Path("page") page: Int, + @Query("fields") fields: String + ): Search @GET("origin/{origin}/{page}.json") - fun getProductsByOrigin( - @Path("origin") origin: String, - @Path("page") page: Int, - @Query("fields") fields: String - ): Single + suspend fun getProductsByOrigin( + @Path("origin") origin: String, + @Path("page") page: Int, + @Query("fields") fields: String + ): Search @GET("manufacturing-place/{manufacturing-place}/{page}.json") - fun getProductsByManufacturingPlace( - @Path("manufacturing-place") manufacturingPlace: String, - @Path("page") page: Int, - @Query("fields") fields: String - ): Single + suspend fun getProductsByManufacturingPlace( + @Path("manufacturing-place") manufacturingPlace: String, + @Path("page") page: Int, + @Query("fields") fields: String + ): Search @GET("store/{store}/{page}.json") - fun getProductByStores( - @Path("store") store: String, - @Path("page") page: Int, - @Query("fields") fields: String - ): Single + suspend fun getProductByStores( + @Path("store") store: String, + @Path("page") page: Int, + @Query("fields") fields: String + ): Search @GET("packaging/{packaging}/{page}.json") - fun getProductsByPackaging( - @Path("packaging") packaging: String, - @Path("page") page: Int, - @Query("fields") fields: String - ): Single + suspend fun getProductsByPackaging( + @Path("packaging") packaging: String, + @Path("page") page: Int, + @Query("fields") fields: String + ): Search @GET("label/{label}/{page}.json") - fun getProductsByLabel( - @Path("label") label: String, - @Path("page") page: Int, - @Query("fields") fields: String - ): Single + suspend fun getProductsByLabel( + @Path("label") label: String, + @Path("page") page: Int, + @Query("fields") fields: String + ): Search @GET("category/{category}/{page}.json") - fun getProductByCategory( - @Path("category") category: String, - @Path("page") page: Int, - @Query("fields") fields: String - ): Single + suspend fun getProductByCategory( + @Path("category") category: String, + @Path("page") page: Int, + @Query("fields") fields: String + ): Search @GET("contributor/{contributor}/{page}.json?nocache=1") - fun getProductsByContributor( - @Path("contributor") contributor: String, - @Path("page") page: Int, - @Query("fields") fields: String - ): Single + suspend fun getProductsByContributor( + @Path("contributor") contributor: String, + @Path("page") page: Int, + @Query("fields") fields: String + ): Search @GET("language/{language}.json") fun getProductsByLanguage(@Path("language") language: String): Single @@ -208,8 +208,8 @@ interface ProductsAPI { @GET("state/{state}.json") fun getProductsByState( - @Path("state") state: String, - @Query("fields") fields: String + @Path("state") state: String, + @Query("fields") fields: String ): Single @GET("packaging/{packaging}.json") @@ -246,37 +246,40 @@ interface ProductsAPI { fun byContributor(@Path("contributor") contributor: String): Single @GET("contributor/{contributor}/state/to-be-completed/{page}.json?nocache=1") - fun getToBeCompletedProductsByContributor( - @Path("contributor") contributor: String, - @Path("page") page: Int - ): Single + suspend fun getToBeCompletedProductsByContributor( + @Path("contributor") contributor: String, + @Path("page") page: Int + ): Search @GET("/photographer/{contributor}/{page}.json?nocache=1") - fun getPicturesContributedProducts( - @Path("contributor") contributor: String, - @Path("page") page: Int - ): Single + suspend fun getPicturesContributedProducts( + @Path("contributor") contributor: String, + @Path("page") page: Int + ): Search @GET("photographer/{Photographer}.json?nocache=1") fun getProductsByPhotographer(@Path("Photographer") photographer: String): Single @GET("photographer/{contributor}/state/to-be-completed/{page}.json?nocache=1") - fun getPicturesContributedIncompleteProducts( - @Path("contributor") contributor: String?, - @Path("page") page: Int - ): Single + suspend fun getPicturesContributedIncompleteProducts( + @Path("contributor") contributor: String?, + @Path("page") page: Int + ): Search @GET("informer/{informer}.json?nocache=1") fun getProductsByInformer(@Path("informer") informer: String?): Single @GET("informer/{contributor}/{page}.json?nocache=1") - fun getInfoAddedProducts(@Path("contributor") contributor: String?, @Path("page") page: Int): Single + suspend fun getInfoAddedProducts( + @Path("contributor") contributor: String?, + @Path("page") page: Int + ): Search @GET("informer/{contributor}/state/to-be-completed/{page}.json?nocache=1") - fun getInfoAddedIncompleteProductsSingle( - @Path("contributor") contributor: String, - @Path("page") page: Int - ): Single + suspend fun getInfoAddedIncompleteProductsSingle( + @Path("contributor") contributor: String, + @Path("page") page: Int + ): Search @GET("last-edit-date/{LastEditDate}.json") fun getProductsByLastEditDate(@Path("LastEditDate") lastEditDate: String): Single @@ -289,19 +292,19 @@ interface ProductsAPI { @GET("additive/{Additive}.json") fun getProductsByAdditive( - @Path("Additive") additive: String?, - @Query("fields") fields: String? + @Path("Additive") additive: String?, + @Query("fields") fields: String? ): Single @GET("code/{Code}.json") fun getProductsByBarcode(@Path("Code") code: String): Single @GET("state/{State}/{page}.json") - fun getProductsByState( - @Path("State") state: String?, - @Path("page") page: Int, - @Query("fields") fields: String? - ): Single + suspend fun getProductsByState( + @Path("State") state: String?, + @Path("page") page: Int, + @Query("fields") fields: String? + ): Search /* * Open Beauty Facts experimental and specific APIs @@ -317,10 +320,10 @@ interface ProductsAPI { * This method gives a list of incomplete products */ @GET("state/to-be-completed/{page}.json?nocache=1") - fun getIncompleteProducts( - @Path("page") page: Int, - @Query("fields") fields: String - ): Single + suspend fun getIncompleteProducts( + @Path("page") page: Int, + @Query("fields") fields: String + ): Search /** * This method is used to get the number of products on Open X Facts @@ -358,8 +361,8 @@ interface ProductsAPI { @Deprecated("") @GET("/cgi/product_image_crop.pl") fun editImagesSingle( - @Query(ApiFields.Keys.BARCODE) code: String, - @QueryMap fields: Map? + @Query(ApiFields.Keys.BARCODE) code: String, + @QueryMap fields: Map? ): Single /** diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/services/WikidataAPI.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/services/WikidataAPI.kt index 45c70ef57947..83b20b8a16e9 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/services/WikidataAPI.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/services/WikidataAPI.kt @@ -1,7 +1,6 @@ package openfoodfacts.github.scrachx.openfood.network.services import com.fasterxml.jackson.databind.node.ObjectNode -import io.reactivex.Single import retrofit2.http.GET import retrofit2.http.Path @@ -11,5 +10,5 @@ import retrofit2.http.Path */ interface WikidataAPI { @GET("{code}.json") - fun getWikiCategory(@Path("code") code: String): Single + suspend fun getWikiCategory(@Path("code") code: String): ObjectNode } \ No newline at end of file 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 5205419581a6..8a972b08529d 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 @@ -23,47 +23,55 @@ import javax.inject.Singleton @Singleton class OfflineProductService @Inject constructor( private val daoSession: DaoSession, - private val productsApi: ProductsAPI, - private val openFoodAPIClient: OpenFoodAPIClient + private val api: ProductsAPI, + private val client: OpenFoodAPIClient ) { /** * @return true if there is still products to upload, false otherwise */ suspend fun uploadAll(includeImages: Boolean) = withContext(Dispatchers.IO) { - getListOfflineProducts().forEach { product -> + for (product in getOfflineProducts()) { if (product.barcode.isEmpty()) { Log.d(LOG_TAG, "Ignore product because empty barcode: $product") - return@forEach + continue } Log.d(LOG_TAG, "Start treating of product $product") - var ok = product.uploadProductIfNeededSync() - if (includeImages) { - ok = ok && product.uploadImageIfNeededSync(FRONT) - ok = ok && product.uploadImageIfNeededSync(INGREDIENTS) - ok = ok && product.uploadImageIfNeededSync(NUTRITION) - if (ok) { - daoSession.offlineSavedProductDao.deleteByKey(product.id) + + val ok = mutableListOf( + uploadProductIfNeededSync(product) + ).apply { + if (includeImages) { + this += uploadImageIfNeededSync(product, FRONT) + this += uploadImageIfNeededSync(product, INGREDIENTS) + this += uploadImageIfNeededSync(product, NUTRITION) } - } + }.all { it } + if (ok) { + daoSession.offlineSavedProductDao.deleteByKey(product.id) + } } - if (includeImages) { - return@withContext getListOfflineProducts().isNotEmpty() - } - return@withContext getListOfflineProductsNotSynced().isNotEmpty() + + if (includeImages) getOfflineProducts().isNotEmpty() + else getOfflineProductsNotSynced().isNotEmpty() } + fun getOfflineProductByBarcode(barcode: String): OfflineSavedProduct? = + daoSession.offlineSavedProductDao.queryBuilder() + .where(OfflineSavedProductDao.Properties.Barcode.eq(barcode)) + .unique() + /** * Performs network call and uploads the product to the server. * Before doing that strip images data from the product map. * */ - private fun OfflineSavedProduct.uploadProductIfNeededSync(): Boolean { - if (isDataUploaded) return true + private suspend fun uploadProductIfNeededSync(product: OfflineSavedProduct): Boolean { + if (product.isDataUploaded) return true // Remove the images from the HashMap before uploading the product details - val productDetails = productDetails.apply { + val productDetails = product.productDetails.apply { // Remove the images from the HashMap before uploading the product details remove(ApiFields.Keys.IMAGE_FRONT) remove(ApiFields.Keys.IMAGE_INGREDIENTS) @@ -75,21 +83,20 @@ class OfflineProductService @Inject constructor( remove(ApiFields.Keys.IMAGE_NUTRITION_UPLOADED) }.filter { !it.value.isNullOrEmpty() } - Log.d(LOG_TAG, "Uploading data for product $barcode: $productDetails") + Log.d(LOG_TAG, "Uploading data for product ${product.barcode}: $productDetails") try { - val productState = productsApi - .saveProduct(barcode, productDetails, openFoodAPIClient.getCommentToUpload()) - .blockingGet() + val productState = api.saveProduct(product.barcode, productDetails, client.getCommentToUpload()) + if (productState.status == 1L) { - isDataUploaded = true - daoSession.offlineSavedProductDao.insertOrReplace(this) - Log.i(LOG_TAG, "Product $barcode uploaded.") + product.isDataUploaded = true + daoSession.offlineSavedProductDao.insertOrReplace(product) + Log.i(LOG_TAG, "Product ${product.barcode} uploaded.") // Refresh product if open - EventBus.getDefault().post(ProductNeedsRefreshEvent(barcode)) + EventBus.getDefault().post(ProductNeedsRefreshEvent(product.barcode)) return true } else { - Log.i(LOG_TAG, "Could not upload product $barcode. Error code: ${productState.status}") + Log.i(LOG_TAG, "Could not upload product ${product.barcode}. Error code: ${productState.status}") } } catch (e: Exception) { Log.e(LOG_TAG, e.message, e) @@ -97,64 +104,70 @@ class OfflineProductService @Inject constructor( return false } - private suspend fun OfflineSavedProduct.uploadImageIfNeededSync(imageField: ProductImageField) = - withContext(Dispatchers.IO) { + private suspend fun uploadImageIfNeededSync( + product: OfflineSavedProduct, + imageField: ProductImageField + ) = withContext(Dispatchers.IO) { - val imageType = imageField.imageType() + val imageType = imageField.imageType() + val imageFilePath = product.productDetails["image_$imageType"] - val imageFilePath = productDetails["image_$imageType"] - if (imageFilePath == null || !needImageUpload(productDetails, imageType)) { - // no need or nothing to upload - Log.d(LOG_TAG, "No need to upload image_$imageType for product $barcode") - return@withContext true - } + if (imageFilePath == null || !isImageUploadNeede(product.productDetails, imageType)) { + // no need or nothing to upload + Log.d(LOG_TAG, "No need to upload image_$imageType for product ${product.barcode}") + return@withContext true + } + + Log.d(LOG_TAG, "Uploading image_$imageType for product ${product.barcode}") + + val imgMap = createRequestBodyMap(product.barcode, product.productDetails, imageField) + val image = ProductImage.createImageRequest(File(imageFilePath)) - Log.d(LOG_TAG, "Uploading image_$imageType for product $barcode") - - val imgMap = createRequestBodyMap(barcode, productDetails, imageField) - val image = ProductImage.createImageRequest(File(imageFilePath)) - - imgMap["""imgupload_$imageType"; filename="${imageType}_$language.png""""] = image - - return@withContext try { - val jsonNode = productsApi.saveImage(imgMap) - val status = jsonNode["status"].asText() - if (status == "status not ok") { - val error = jsonNode["error"].asText() - if (error == "This picture has already been sent.") { - productDetails["image_${imageType}_uploaded"] = "true" - daoSession.offlineSavedProductDao.insertOrReplace(this@uploadImageIfNeededSync) - return@withContext true - } - Log.e(LOG_TAG, "Error uploading $imageType: $error") - return@withContext false + imgMap["""imgupload_$imageType"; filename="${imageType}_${product.language}.png""""] = image + + return@withContext try { + val jsonNode = api.saveImage(imgMap) + val status = jsonNode["status"].asText() + + if (status == "status not ok") { + val error = jsonNode["error"].asText() + if (error == "This picture has already been sent.") { + product.productDetails["image_${imageType}_uploaded"] = "true" + daoSession.offlineSavedProductDao.insertOrReplace(product) + return@withContext true } - productDetails["image_${imageType}_uploaded"] = "true" - daoSession.offlineSavedProductDao.insertOrReplace(this@uploadImageIfNeededSync) - Log.d(LOG_TAG, "Uploaded image_$imageType for product $barcode") - - // Refresh event - EventBus.getDefault().post(ProductNeedsRefreshEvent(barcode)) - true - } catch (e: Exception) { - Log.e(LOG_TAG, e.message, e) - false + + Log.e(LOG_TAG, "Error uploading $imageType: $error") + return@withContext false } - } - fun getOfflineProductByBarcode(barcode: String): OfflineSavedProduct? = - daoSession.offlineSavedProductDao.queryBuilder().where(OfflineSavedProductDao.Properties.Barcode.eq(barcode)).unique() + product.productDetails["image_${imageType}_uploaded"] = "true" + + // Refresh db + daoSession.offlineSavedProductDao.insertOrReplace(product) + Log.d(LOG_TAG, "Uploaded image_$imageType for product ${product.barcode}") + + // Refresh event + EventBus.getDefault().post(ProductNeedsRefreshEvent(product.barcode)) + true + } catch (e: Exception) { + Log.e(LOG_TAG, e.message, e) + false + } + } - private fun getListOfflineProducts() = daoSession.offlineSavedProductDao.queryBuilder() - .where(OfflineSavedProductDao.Properties.Barcode.isNotNull) - .where(OfflineSavedProductDao.Properties.Barcode.notEq("")) - .list() + private fun getOfflineProducts() = + daoSession.offlineSavedProductDao.queryBuilder() + .where(OfflineSavedProductDao.Properties.Barcode.isNotNull) + .where(OfflineSavedProductDao.Properties.Barcode.notEq("")) + .list() - private fun getListOfflineProductsNotSynced() = daoSession.offlineSavedProductDao.queryBuilder() - .where(OfflineSavedProductDao.Properties.Barcode.isNotNull) - .where(OfflineSavedProductDao.Properties.Barcode.notEq("")) - .where(OfflineSavedProductDao.Properties.IsDataUploaded.notEq(true)) - .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 ProductImageField.imageType() = when (this) { FRONT -> "front" @@ -163,17 +176,21 @@ class OfflineProductService @Inject constructor( else -> "other" } - private fun needImageUpload(productDetails: Map, imageType: String): Boolean { + private fun isImageUploadNeede(productDetails: Map, imageType: String): Boolean { val imageUploaded = productDetails["image_${imageType}_uploaded"].toBoolean() val imageFilePath = productDetails["image_$imageType"] return !imageUploaded && !imageFilePath.isNullOrEmpty() } - private fun createRequestBodyMap(code: String, productDetails: Map, front: ProductImageField): MutableMap { + private fun createRequestBodyMap( + code: String, + productDetails: Map, + frontImg: ProductImageField + ): MutableMap { val barcode = RequestBody.create(OpenFoodAPIClient.MIME_TEXT, code) val imageField = RequestBody.create( OpenFoodAPIClient.MIME_TEXT, - "${front}_${productDetails["lang"]}" + "${frontImg}_${productDetails["lang"]}" ) return hashMapOf("code" to barcode, "imagefield" to imageField) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/ProductInfoState.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/ProductInfoState.kt index 425f6f0b041b..79e1ab4d2495 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/ProductInfoState.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/ProductInfoState.kt @@ -18,6 +18,8 @@ package openfoodfacts.github.scrachx.openfood.utils /** * Created by Lobster on 10.03.18. */ -enum class ProductInfoState { - LOADING, EMPTY +sealed class ProductInfoState { + object Loading : ProductInfoState() + object Empty : ProductInfoState() + data class Data(val data: T) : ProductInfoState() } \ 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 15caa786e8ce..c9c042b7e255 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 @@ -18,6 +18,7 @@ package openfoodfacts.github.scrachx.openfood.utils import android.Manifest import android.app.Activity import android.content.Context +import android.content.DialogInterface import android.content.Intent import android.content.pm.PackageManager import android.content.pm.PackageManager.PERMISSION_GRANTED @@ -46,7 +47,6 @@ import androidx.core.net.toUri import androidx.core.text.inSpans import androidx.core.view.children import androidx.work.* -import com.afollestad.materialdialogs.MaterialDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.squareup.picasso.Callback import com.squareup.picasso.RequestCreator @@ -222,10 +222,14 @@ object Utils { fun isAllGranted(grantResults: IntArray) = grantResults.isNotEmpty() && grantResults.none { it != PERMISSION_GRANTED } -fun buildSignInDialog(activity: Activity): MaterialDialog.Builder = MaterialDialog.Builder(activity) - .title(R.string.sign_in_to_edit) - .positiveText(R.string.txtSignIn) - .negativeText(R.string.dialog_cancel) +fun buildSignInDialog( + context: Context, + onPositive: (DialogInterface, Int) -> Unit = { _, _ -> }, + onNegative: (DialogInterface, Int) -> Unit = { _, _ -> } +): MaterialAlertDialogBuilder = MaterialAlertDialogBuilder(context) + .setTitle(R.string.sign_in_to_edit) + .setPositiveButton(R.string.txtSignIn) { d, i -> onPositive(d, i) } + .setNegativeButton(R.string.dialog_cancel) { d, i -> onNegative(d, i) } /** diff --git a/app/src/main/res/layout/calculate_details.xml b/app/src/main/res/layout/calculate_details.xml index 40d56dc264b9..de4d5d9cad0a 100644 --- a/app/src/main/res/layout/calculate_details.xml +++ b/app/src/main/res/layout/calculate_details.xml @@ -45,7 +45,7 @@ android:textSize="15sp" />