Skip to content

Commit

Permalink
[3162] Add history refreshing
Browse files Browse the repository at this point in the history
  • Loading branch information
naivekook committed Mar 14, 2021
1 parent dc26edf commit 865a790
Show file tree
Hide file tree
Showing 14 changed files with 603 additions and 309 deletions.
5 changes: 5 additions & 0 deletions app/build.gradle.kts
Expand Up @@ -84,6 +84,11 @@ dependencies {
implementation("io.reactivex.rxjava2:rxkotlin:2.4.0")
implementation("io.reactivex.rxjava2:rxjava:2.2.20")
implementation("io.reactivex.rxjava2:rxandroid:2.1.1")
implementation("com.jakewharton.rxrelay2:rxrelay:2.1.1")

//Rx optional
implementation("com.gojuno.koptional:koptional:1.7.0")
implementation("com.gojuno.koptional:koptional-rxjava2-extensions:1.7.0")

//Networking
implementation("com.squareup.retrofit2:retrofit:2.6.4")
Expand Down
Expand Up @@ -371,19 +371,8 @@ class ProductListActivity : BaseActivity(), SwipeController.Actions {
return notificationManager
}

fun getProductBrandsQuantityDetails(brands: String?, quantity: String?): String {
val builder = StringBuilder()
if (!brands.isNullOrEmpty()) {
builder.append(brands.split(",")[0].trim { it <= ' ' }.capitalize(Locale.ROOT))
}
if (!quantity.isNullOrEmpty()) {
builder.append(" - ").append(quantity)
}
return builder.toString()
}

const val KEY_LIST_ID = "listId"
const val KEY_LIST_NAME = "listName"
const val KEY_PRODUCT_TO_ADD = "product"
}
}
}

Large diffs are not rendered by default.

@@ -1,110 +1,101 @@
package openfoodfacts.github.scrachx.openfood.features.scanhistory

import android.app.Activity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.squareup.picasso.Callback
import openfoodfacts.github.scrachx.openfood.AppFlavors.OFF
import openfoodfacts.github.scrachx.openfood.AppFlavors.isFlavors
import openfoodfacts.github.scrachx.openfood.R
import openfoodfacts.github.scrachx.openfood.databinding.HistoryListItemBinding
import openfoodfacts.github.scrachx.openfood.features.productlist.ProductListActivity.Companion.getProductBrandsQuantityDetails
import openfoodfacts.github.scrachx.openfood.models.HistoryProduct
import openfoodfacts.github.scrachx.openfood.network.OpenFoodAPIClient
import openfoodfacts.github.scrachx.openfood.models.getProductBrandsQuantityDetails
import openfoodfacts.github.scrachx.openfood.utils.*
import openfoodfacts.github.scrachx.openfood.utils.Utils.picassoBuilder
import openfoodfacts.github.scrachx.openfood.utils.getEcoscoreResource
import openfoodfacts.github.scrachx.openfood.utils.getNovaGroupResource
import openfoodfacts.github.scrachx.openfood.utils.getNutriScoreResource
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.properties.Delegates

/**
* @param isLowBatteryMode determine if image should be loaded or not
*/
class ScanHistoryAdapter(
private val activity: Activity,
private val isLowBatteryMode: Boolean,
val products: MutableList<HistoryProduct>,
) : RecyclerView.Adapter<ScanHistoryHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ScanHistoryHolder {
val view = HistoryListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ScanHistoryHolder(view)
}
private val onItemClicked: (HistoryProduct) -> Unit
) : RecyclerView.Adapter<ScanHistoryAdapter.ViewHolder>(), AutoUpdatableAdapter {

private fun HistoryProduct.getProductBrandsQuantityDetails() = getProductBrandsQuantityDetails(brands, quantity)

override fun onBindViewHolder(holder: ScanHistoryHolder, position: Int) {

val hProduct = products[position]
val productBrandsQuantityDetails = hProduct.getProductBrandsQuantityDetails()

// Use the provided View Holder on the onCreateViewHolder method to populate the current row on the RecyclerView
holder.binding.productName.text = hProduct.title
holder.binding.barcode.text = hProduct.barcode
holder.binding.productDetails.text = productBrandsQuantityDetails
holder.binding.imgProgress.visibility = if (hProduct.url == null) View.GONE else View.VISIBLE

// Load Image if isBatteryLoad is false
if (!isLowBatteryMode) {
picassoBuilder(activity)
.load(hProduct.url)
.placeholder(R.drawable.placeholder_thumb)
.error(R.drawable.ic_no_red_24dp)
.fit()
.centerCrop()
.into(holder.binding.productImage, object : Callback {
override fun onSuccess() {
holder.binding.imgProgress.visibility = View.GONE
}

override fun onError(ex: Exception) {
holder.binding.imgProgress.visibility = View.GONE
}
})
} else {
holder.binding.productImage.background = ContextCompat.getDrawable(activity, R.drawable.placeholder_thumb)
holder.binding.imgProgress.visibility = View.INVISIBLE
}
holder.binding.lastScan.text = calcTime(hProduct.lastSeen)
var products: List<HistoryProduct> by Delegates.observable(emptyList()) { _, oldList, newList ->
autoNotify(oldList, newList) { o, n -> o.barcode == n.barcode }
}

if (isFlavors(OFF)) {
holder.binding.nutriscore.setImageResource(hProduct.getNutriScoreResource())
holder.binding.ecoscore.setImageResource(hProduct.getEcoscoreResource())
holder.binding.novaGroup.setImageResource(hProduct.getNovaGroupResource())
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = HistoryListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(view, isLowBatteryMode)
}

holder.binding.root.setOnClickListener {
OpenFoodAPIClient(activity).openProduct(hProduct.barcode, activity)
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
val historyProduct = products[position]
viewHolder.bind(historyProduct) {
onItemClicked(it)
}
}

override fun getItemCount() = products.size

// Insert a new item to the RecyclerView on a predefined position
fun insert(position: Int, data: HistoryProduct) {
products.add(position, data)
notifyItemInserted(position)
override fun onViewRecycled(viewHolder: ViewHolder) {
viewHolder.unbind()
super.onViewRecycled(viewHolder)
}

// Remove a RecyclerView item containing a specified Data object
fun removeAndNotify(data: HistoryProduct) {
val position = products.indexOf(data)
products.removeAt(position)
notifyItemRemoved(position)
}
class ViewHolder(
val binding: HistoryListItemBinding,
val isLowBatteryMode: Boolean
) : RecyclerView.ViewHolder(binding.root) {

private val context = binding.root.context

private fun calcTime(date: Date): String {
val duration = Date().time - date.time
val seconds = TimeUnit.MILLISECONDS.toSeconds(duration)
val minutes = TimeUnit.MILLISECONDS.toMinutes(duration)
val hours = TimeUnit.MILLISECONDS.toHours(duration)
val days = TimeUnit.MILLISECONDS.toDays(duration)
return when {
seconds < 60 -> activity.resources.getQuantityString(R.plurals.seconds, seconds.toInt(), seconds.toInt())
minutes < 60 -> activity.resources.getQuantityString(R.plurals.minutes, minutes.toInt(), minutes.toInt())
hours < 24 -> activity.resources.getQuantityString(R.plurals.hours, hours.toInt(), hours.toInt())
else -> activity.resources.getQuantityString(R.plurals.days, days.toInt(), days.toInt())
fun bind(product: HistoryProduct, onClicked: (HistoryProduct) -> Unit) {
binding.productName.text = product.title
binding.barcode.text = product.barcode
binding.productDetails.text = product.getProductBrandsQuantityDetails()

// Load Image if isBatteryLoad is false
if (!isLowBatteryMode && product.url != null) {
binding.imgProgress.isVisible = true
picassoBuilder(context)
.load(product.url)
.placeholder(R.drawable.placeholder_thumb)
.error(R.drawable.ic_no_red_24dp)
.fit()
.centerCrop()
.into(binding.productImage, object : Callback {
override fun onSuccess() {
binding.imgProgress.isVisible = false
}

override fun onError(ex: Exception) {
binding.imgProgress.isVisible = false
}
})
} else {
binding.productImage.setImageResource(R.drawable.placeholder_thumb)
binding.imgProgress.isVisible = false
}
binding.lastScan.text = product.lastSeen.timeFormatted(context)

if (isFlavors(OFF)) {
binding.nutriscore.setImageResource(product.getNutriScoreResource())
binding.ecoscore.setImageResource(product.getEcoscoreResource())
binding.novaGroup.setImageResource(product.getNovaGroupResource())
}

binding.root.setOnClickListener { onClicked(product) }
}

fun unbind() {
binding.root.setOnClickListener(null)
}

}

}

}

This file was deleted.

@@ -0,0 +1,153 @@
package openfoodfacts.github.scrachx.openfood.features.scanhistory

import android.content.Context
import com.gojuno.koptional.rxjava2.filterSome
import com.gojuno.koptional.toOptional
import com.jakewharton.rxrelay2.BehaviorRelay
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.addTo
import io.reactivex.schedulers.Schedulers
import openfoodfacts.github.scrachx.openfood.R
import openfoodfacts.github.scrachx.openfood.models.HistoryProduct
import openfoodfacts.github.scrachx.openfood.models.HistoryProductDao
import openfoodfacts.github.scrachx.openfood.network.OpenFoodAPIClient
import openfoodfacts.github.scrachx.openfood.utils.LocaleHelper
import openfoodfacts.github.scrachx.openfood.utils.SortType
import openfoodfacts.github.scrachx.openfood.utils.Utils

class ScanHistoryViewModel(private val context: Context) {

private val compositeDisposable = CompositeDisposable()
private val fetchProductsStateRelay = BehaviorRelay.createDefault<FetchProductsState>(FetchProductsState.Loading)
private val client = OpenFoodAPIClient(context)
private var sortType = SortType.TIME

fun attach() {
refreshItems()
}

fun detach() {
compositeDisposable.clear()
}

fun observeFetchProductState(): Observable<FetchProductsState> = fetchProductsStateRelay

fun refreshItems() {
fetchProductsStateRelay.accept(FetchProductsState.Loading)
Observable
.fromCallable {
Utils.daoSession.historyProductDao.queryBuilder().list()
}
.flatMapIterable { it }
.flatMap {
client.getProductStateFull(it.barcode)
.toObservable()
.map { it.product.toOptional() }
}
.filterSome()
.flatMap { product ->
Observable.fromCallable {
val historyProduct = Utils.daoSession.historyProductDao.queryBuilder()
.where(HistoryProductDao.Properties.Barcode.eq(product.code))
.build()
.unique()
product.productName?.let { historyProduct.title = it }
product.brands?.let { historyProduct.brands = it }
product.getImageSmallUrl(LocaleHelper.getLanguage(context))?.let { historyProduct.url = it }
product.quantity?.let { historyProduct.quantity = it }
product.nutritionGradeFr?.let { historyProduct.nutritionGrade = it }
product.ecoscore?.let { historyProduct.ecoscore = it }
product.novaGroups?.let { historyProduct.novaGroup = it }
Utils.daoSession.historyProductDao.update(historyProduct)
}
}
.toList()
.flatMap {
Single.fromCallable {
Utils.daoSession.historyProductDao.queryBuilder().list()
}
}
.map { it.customSort() }
.subscribeOn(Schedulers.io())
.subscribe(
{ items -> fetchProductsStateRelay.accept(FetchProductsState.Data(items)) },
{ fetchProductsStateRelay.accept(FetchProductsState.Error) }
)
.addTo(compositeDisposable)
}

fun clearHistory() {
fetchProductsStateRelay.accept(FetchProductsState.Loading)
Observable
.fromCallable {
Utils.daoSession.historyProductDao.deleteAll()
}
.subscribeOn(Schedulers.io())
.subscribe(
{ fetchProductsStateRelay.accept(FetchProductsState.Data(emptyList())) },
{ fetchProductsStateRelay.accept(FetchProductsState.Error) }
)
.addTo(compositeDisposable)
}

fun removeProductFromHistory(product: HistoryProduct) {
Observable
.fromCallable {
Utils.daoSession.historyProductDao.delete(product)
Utils.daoSession.historyProductDao.queryBuilder().list()
}
.map { it.customSort() }
.subscribeOn(Schedulers.io())
.subscribe(
{ products -> fetchProductsStateRelay.accept(FetchProductsState.Data(products)) },
{ fetchProductsStateRelay.accept(FetchProductsState.Error) }
)
.addTo(compositeDisposable)

}

fun updateSortType(type: SortType) {
sortType = type
fetchProductsStateRelay
.take(1)
.map {
(it as? FetchProductsState.Data)?.items?.customSort() ?: emptyList()
}
.filter { it.isNotEmpty() }
.subscribeOn(Schedulers.io())
.subscribe(
{ products -> fetchProductsStateRelay.accept(FetchProductsState.Data(products)) },
{ fetchProductsStateRelay.accept(FetchProductsState.Error) }
)
.addTo(compositeDisposable)
}

/**
* Function to compare history items based on title, brand, barcode, time and nutrition grade
*/
private fun List<HistoryProduct>.customSort() = when (sortType) {
SortType.TITLE -> sortedWith { item1, item2 ->
if (item1.title.isNullOrEmpty()) item1.title = context.getString(R.string.no_title)
if (item2.title.isNullOrEmpty()) item2.title = context.getString(R.string.no_title)
item1.title.compareTo(item2.title, true)
}

SortType.BRAND -> sortedWith { item1, item2 ->
if (item1.brands.isNullOrEmpty()) item1.brands = context.getString(R.string.no_brand)
if (item2.brands.isNullOrEmpty()) item2.brands = context.getString(R.string.no_brand)
item1.brands!!.compareTo(item2.brands!!, true)
}
SortType.BARCODE -> sortedBy { it.barcode }
SortType.GRADE -> sortedBy { it.nutritionGrade }
SortType.TIME -> sortedByDescending { it.lastSeen }
SortType.NONE -> this
}

sealed class FetchProductsState {
object Loading : FetchProductsState()
data class Data(val items: List<HistoryProduct>) : FetchProductsState()
object Error : FetchProductsState()
}
}

0 comments on commit 865a790

Please sign in to comment.