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 3524e27fb885..0abe4f03b9aa 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 @@ -26,10 +26,10 @@ import openfoodfacts.github.scrachx.openfood.utils.ProductInfoState open class AbstractSummaryProductPresenter : ISummaryProductPresenter.View { override fun showAllergens(allergens: List) = Unit - override fun showProductQuestion(question: Question) = Unit + override suspend 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 + override suspend fun showAnalysisTags(state: ProductInfoState>) = 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 2afd14f53913..db6539cc486e 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 @@ -40,7 +40,7 @@ interface ISummaryProductPresenter { } interface View { - fun showProductQuestion(question: Question) + suspend fun showProductQuestion(question: Question) fun showAnnotatedInsightToast(annotationResponse: AnnotationResponse) fun showAllergens(allergens: List) @@ -49,6 +49,6 @@ interface ISummaryProductPresenter { fun showLabelsState(state: ProductInfoState>) fun showAdditivesState(state: ProductInfoState>) - fun showAnalysisTags(analysisTags: List) + suspend fun showAnalysisTags(state: ProductInfoState>) } } \ 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 573eeeef03a0..dbfa16345605 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 @@ -47,6 +47,7 @@ import com.google.android.material.snackbar.Snackbar import com.squareup.picasso.Picasso import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -585,31 +586,52 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { } } - override fun showAnalysisTags(analysisTags: List) { - requireActivity().runOnUiThread { - binding.analysisContainer.visibility = View.VISIBLE - val adapter = IngredientAnalysisTagsAdapter(requireContext(), analysisTags, picasso, sharedPreferences) - adapter.setOnItemClickListener { view, _ -> - val fragment = IngredientsWithTagDialogFragment - .newInstance(product, view.getTag(R.id.analysis_tag_config) as AnalysisTagConfig) - fragment.show(childFragmentManager, "fragment_ingredients_with_tag") - fragment.onDismissListener = { adapter.filterVisibleTags() } + override suspend fun showAnalysisTags(state: ProductInfoState>) { + withContext(Main) { + when (state) { + is ProductInfoState.Data -> { + val analysisTags = state.data + + binding.analysisContainer.visibility = View.VISIBLE + + binding.analysisTags.adapter = IngredientAnalysisTagsAdapter( + requireContext(), + analysisTags, + picasso, + sharedPreferences + ).apply adapter@{ + setOnItemClickListener { view, _ -> + IngredientsWithTagDialogFragment.newInstance( + product, + view.getTag(R.id.analysis_tag_config) as AnalysisTagConfig + ).run { + onDismissListener = { filterVisibleTags() } + show(childFragmentManager, "fragment_ingredients_with_tag") + } + } + } + } + ProductInfoState.Empty -> { + // TODO: + } + ProductInfoState.Loading -> { + // TODO: + } } - binding.analysisTags.adapter = adapter + } } override fun showAllergens(allergens: List) { val data = AllergenHelper.computeUserAllergen(product, allergens) - if (data.isEmpty()) { - return - } + if (data.isEmpty()) return + if (data.incomplete) { binding.productAllergenAlertText.setText(R.string.product_incomplete_message) binding.productAllergenAlertLayout.visibility = View.VISIBLE return } - binding.productAllergenAlertText.text = StringBuilder(resources.getString(R.string.product_allergen_prompt)) + binding.productAllergenAlertText.text = SpannableStringBuilder(getString(R.string.product_allergen_prompt)) .append("\n") .append(data.allergens.joinToString(", ")) @@ -617,8 +639,7 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { } - override fun showProductQuestion(question: Question) { - if (isRemoving) return + override suspend fun showProductQuestion(question: Question) = withContext(Main) { if (!question.isEmpty()) { productQuestion = question @@ -706,45 +727,60 @@ class SummaryProductFragment : BaseFragment(), ISummaryProductPresenter.View { 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) }) + .apply { + state.data.map(::getLabelTag).forEachIndexed { i, el -> + append(el) + if (i == state.data.size) append(", ") + } + } + } + is ProductInfoState.Loading -> { + binding.labelsText.text = SpannableStringBuilder() + .bold { append(getString(R.string.txtLabels)) } + .append(getString(R.string.txtLoading)) + } + + is ProductInfoState.Empty -> { + binding.labelsText.visibility = View.GONE + binding.labelsIcon.visibility = View.GONE } } } } - override fun showCategoriesState(state: ProductInfoState>) = requireActivity().runOnUiThread { - when (state) { - is ProductInfoState.Loading -> if (context != null) { - binding.categoriesText.append(getString(R.string.txtLoading)) - } - is ProductInfoState.Empty -> { - binding.categoriesText.visibility = View.GONE - binding.categoriesIcon.visibility = View.GONE - } - is ProductInfoState.Data -> { - val categories = state.data - if (categories.isEmpty()) { - binding.categoriesLayout.visibility = View.GONE - return@runOnUiThread + override fun showCategoriesState(state: ProductInfoState>) { + requireActivity().runOnUiThread { + when (state) { + is ProductInfoState.Loading -> { + binding.categoriesText.text = SpannableStringBuilder() + .bold { append(getString(R.string.txtCategories)) } + .append(getString(R.string.txtLoading)) + } + is ProductInfoState.Empty -> { + binding.categoriesText.visibility = View.GONE + binding.categoriesIcon.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, + ) } - CategoryProductHelper.showCategories( - this, - binding.categoriesText, - binding.textCategoryAlcoholAlert, - categories, - wikidataClient, - ) } } } 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 25e6aeae3d30..793a48d9982e 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 @@ -123,39 +123,35 @@ class SummaryProductPresenter( override suspend fun loadAnalysisTags() { if (!isFlavors(OFF, OBF, OPFF)) return - val analysisTags = product.ingredientsAnalysisTags + val knownTags = product.ingredientsAnalysisTags - if (analysisTags.isNotEmpty()) { - view.showLabelsState(ProductInfoState.Loading) - val analysisTagConfigs = try { - analysisTags.mapNotNull { - productRepository.getAnalysisTagConfigByTagAndLanguageCode(it, languageCode).awaitSingleOrNull() + view.showAnalysisTags(ProductInfoState.Loading) + + if (knownTags.isNotEmpty()) { + val configs = try { + knownTags.mapNotNull { + productRepository.getAnalysisTagConfigByTagAndLanguageCode(it, languageCode) } } catch (err: Exception) { - Log.e(SummaryProductPresenter::class.java.simpleName, "loadAnalysisTags", err) - view.showLabelsState(ProductInfoState.Empty) + Log.e(SummaryProductPresenter::class.simpleName, "loadAnalysisTags", err) + view.showAnalysisTags(ProductInfoState.Empty) return } - if (analysisTagConfigs.isEmpty()) { - view.showLabelsState(ProductInfoState.Empty) - } else { - view.showAnalysisTags(analysisTagConfigs) - } + + if (configs.isEmpty()) view.showAnalysisTags(ProductInfoState.Empty) + else view.showAnalysisTags(ProductInfoState.Data(configs)) } else { - view.showLabelsState(ProductInfoState.Loading) - val analysisTagConfigs = try { + val configs = try { productRepository.getUnknownAnalysisTagConfigsByLanguageCode(languageCode).await() } catch (err: Exception) { - Log.e(SummaryProductPresenter::class.java.simpleName, "loadAnalysisTags", err) + Log.e(SummaryProductPresenter::class.simpleName, "loadAnalysisTags", err) view.showLabelsState(ProductInfoState.Empty) return } - if (analysisTagConfigs.isEmpty()) { - view.showLabelsState(ProductInfoState.Empty) - } else { - view.showAnalysisTags(analysisTagConfigs) - } + + if (configs.isEmpty()) view.showAnalysisTags(ProductInfoState.Empty) + else view.showAnalysisTags(ProductInfoState.Data(configs)) } } 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 2fb725b1d8a2..7233ee88e875 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 @@ -55,7 +55,6 @@ import io.reactivex.Completable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import kotlinx.coroutines.* -import kotlinx.coroutines.rx2.await import openfoodfacts.github.scrachx.openfood.AppFlavors.OFF import openfoodfacts.github.scrachx.openfood.AppFlavors.isFlavors import openfoodfacts.github.scrachx.openfood.BuildConfig @@ -242,7 +241,7 @@ class ContinuousScanActivity : BaseActivity(), IProductView { this@ContinuousScanActivity.product = product // Add product to scan history - productDisp = lifecycleScope.launch { client.addToHistory(product).await() } + productDisp = lifecycleScope.launch { client.addToHistory(product) } // If we're here from comparison -> add product, return to comparison activity if (intent.getBooleanExtra(ProductCompareActivity.KEY_COMPARE_PRODUCT, false)) { @@ -364,25 +363,39 @@ class ContinuousScanActivity : BaseActivity(), IProductView { } } - override fun showAnalysisTags(analysisTags: List) { - super.showAnalysisTags(analysisTags) - if (analysisTags.isEmpty()) { - binding.quickViewTags.visibility = View.GONE - analysisTagsEmpty = true - return - } - binding.quickViewTags.visibility = View.VISIBLE - analysisTagsEmpty = false - val adapter = IngredientAnalysisTagsAdapter(this@ContinuousScanActivity, analysisTags, picasso, sharedPreferences) - adapter.setOnItemClickListener { view: View?, _ -> - if (view == null) return@setOnItemClickListener - IngredientsWithTagDialogFragment.newInstance(product, view.getTag(R.id.analysis_tag_config) as AnalysisTagConfig).run { - show(supportFragmentManager, "fragment_ingredients_with_tag") - onDismissListener = { adapter.filterVisibleTags() } + override suspend fun showAnalysisTags(state: ProductInfoState>) = withContext(Dispatchers.Main) { + when (state) { + is ProductInfoState.Data -> { + binding.quickViewTags.visibility = View.VISIBLE + analysisTagsEmpty = false + val adapter = IngredientAnalysisTagsAdapter( + this@ContinuousScanActivity, + state.data, + picasso, + sharedPreferences + ).apply adapter@{ + setOnItemClickListener { view: View, _ -> + IngredientsWithTagDialogFragment.newInstance( + product, + view.getTag(R.id.analysis_tag_config) as AnalysisTagConfig + ).run { + onDismissListener = { this@adapter.filterVisibleTags() } + show(supportFragmentManager, "fragment_ingredients_with_tag") + } + } + } + + binding.quickViewTags.adapter = adapter + } + is ProductInfoState.Empty -> { + binding.quickViewTags.visibility = View.GONE + analysisTagsEmpty = true + } + ProductInfoState.Loading -> { + // TODO } } - binding.quickViewTags.adapter = adapter } }, productRepository).also { lifecycleScope.launch { 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 6295d5576dda..fa92c5df0119 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 @@ -12,7 +12,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext import io.reactivex.Single 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 @@ -152,7 +151,9 @@ class OpenFoodAPIClient @Inject constructor( .setNegativeButton(R.string.txtNo) { _, _ -> activity.onBackPressed() } .show() } else { - launch { addToHistory(state.product!!) } + addToHistory(state.product!!) + + // After this the lifecycleScope is cleared because we're switching activities startProductViewActivity(activity, state) } } @@ -226,7 +227,7 @@ class OpenFoodAPIClient @Inject constructor( /** * Add a product to ScanHistory asynchronously */ - fun addToHistory(product: Product) = rxCompletable(IO) { + suspend fun addToHistory(product: Product) = withContext(IO) { daoSession.historyProductDao.addToHistory(product, localeManager.getLanguage()) } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/repositories/ProductRepository.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/repositories/ProductRepository.kt index 79d72c451b8e..b0e94e09ed8e 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/repositories/ProductRepository.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/repositories/ProductRepository.kt @@ -24,7 +24,7 @@ import io.reactivex.Completable import io.reactivex.Maybe import io.reactivex.Single import io.reactivex.schedulers.Schedulers -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.rx2.await import kotlinx.coroutines.rx2.rxMaybe import kotlinx.coroutines.rx2.rxSingle @@ -190,7 +190,7 @@ class ProductRepository @Inject constructor( * * @return The list of allergens. */ - suspend fun getEnabledAllergens(): List = withContext(Dispatchers.IO) { + suspend fun getEnabledAllergens(): List = withContext(IO) { daoSession.allergenDao.queryBuilder() .where(AllergenDao.Properties.Enabled.eq("true")) .list() @@ -434,14 +434,12 @@ class ProductRepository @Inject constructor( } /** - * TODO to be improved by loading only if required and only in the user language * Ingredients saving to local database + * Ingredient and IngredientName has One-To-Many relationship, therefore we need to save them separately. * * @param ingredients The list of ingredients to be saved. - * - * - * Ingredient and IngredientName has One-To-Many relationship, therefore we need to save them separately. */ + // TODO to be improved by loading only if required and only in the user language private fun saveIngredients(ingredients: List) { daoSession.database.beginTransaction() try { @@ -676,7 +674,7 @@ class ProductRepository @Inject constructor( * @param languageCode is a 2-digit language code * @return The list of translated allergen names */ - suspend fun getAllergensByLanguageCode(languageCode: String?): List = withContext(Dispatchers.IO) { + suspend fun getAllergensByLanguageCode(languageCode: String?): List = withContext(IO) { daoSession.allergenNameDao.queryBuilder() .where(AllergenNameDao.Properties.LanguageCode.eq(languageCode)) .list() @@ -689,7 +687,7 @@ class ProductRepository @Inject constructor( * @param languageCode is a 2-digit language code * @return The translated allergen name */ - suspend fun getAllergenByTagAndLanguageCode(allergenTag: String?, languageCode: String?): AllergenName = withContext(Dispatchers.IO) { + suspend fun getAllergenByTagAndLanguageCode(allergenTag: String?, languageCode: String?): AllergenName = withContext(IO) { daoSession.allergenNameDao.queryBuilder().where( AllergenNameDao.Properties.AllergenTag.eq(allergenTag), AllergenNameDao.Properties.LanguageCode.eq(languageCode) @@ -717,7 +715,7 @@ class ProductRepository @Inject constructor( * @return The translated states name */ suspend fun getStatesByTagAndLanguageCode(statesTag: String, languageCode: String?): StatesName { - return withContext(Dispatchers.IO) { + return withContext(IO) { daoSession.statesNameDao.queryBuilder().where( StatesNameDao.Properties.StatesTag.eq(statesTag), StatesNameDao.Properties.LanguageCode.eq(languageCode) @@ -873,11 +871,12 @@ class ProductRepository @Inject constructor( * @param languageCode * @return [Maybe.empty] if no analysis tag found */ - fun getAnalysisTagConfigByTagAndLanguageCode(analysisTag: String?, languageCode: String) = Maybe.fromCallable { + suspend fun getAnalysisTagConfigByTagAndLanguageCode(analysisTag: String?, languageCode: String): AnalysisTagConfig? = withContext(IO) { daoSession.analysisTagConfigDao.queryBuilder() .where(AnalysisTagConfigDao.Properties.AnalysisTag.eq(analysisTag)) - .unique().also { updateAnalysisTagConfig(it, languageCode) } - }.subscribeOn(Schedulers.io()) + .unique() + .also { updateAnalysisTagConfig(it, languageCode) } + } fun getUnknownAnalysisTagConfigsByLanguageCode(languageCode: String) = Single.fromCallable { daoSession.analysisTagConfigDao.queryBuilder()