Added sorting, freshness precentage, fullscreen view for products on shelf

This commit is contained in:
leca 2024-10-17 03:01:58 +03:00
parent fd7feb72eb
commit 28da5408a1
10 changed files with 251 additions and 73 deletions

View File

@ -15,6 +15,9 @@ import java.io.File
import java.io.FileOutputStream
import java.net.URLEncoder
import java.security.MessageDigest
import java.time.Duration
import java.time.LocalDate
import java.time.format.DateTimeFormatter
fun getImageUri(activity: Activity, imageFile: File): Uri? {
return FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + "." + activity.localClassName + ".provider", imageFile)
@ -69,4 +72,18 @@ fun getActivity(context: Context?): Activity? {
}
return null
}
@RequiresApi(Build.VERSION_CODES.O)
fun calculateProductFreshness(dateOfProduction: String, dateOfExpiry: String): Double {
val dateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("d.M.yyyy")
val fresh = LocalDate.parse(dateOfProduction, dateFormatter)
val expired = LocalDate.parse(dateOfExpiry, dateFormatter)
val shelfLife = Duration.between(fresh.atStartOfDay(), expired.atStartOfDay()).toDays()
val today = LocalDate.parse(LocalDate.now().format(dateFormatter), dateFormatter)
val daysBeforeExpiry = Duration.between(today.atStartOfDay(), expired.atStartOfDay()).toDays()
return daysBeforeExpiry / shelfLife.toDouble()
}

View File

@ -1,7 +1,10 @@
package org.foxarmy.barcodescannerforemployees.dataclasses
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import androidx.annotation.RequiresApi
import org.foxarmy.barcodescannerforemployees.calculateProductFreshness
class Product() : Parcelable {
var id = 0
@ -9,13 +12,16 @@ class Product() : Parcelable {
var amount = 0
var dateOfProduction = "01.01.1970"
var dateOfExpiry = "01.01.1970"
var freshness: Double = 0.0
@RequiresApi(Build.VERSION_CODES.O)
constructor(id: Int, abstractProductId: Int, amount: Int, dateOfProduction: String, dateOfExpiry: String) : this() {
this.id = id
this.abstractProductId = abstractProductId
this.amount = amount
this.dateOfProduction = dateOfProduction
this.dateOfExpiry = dateOfExpiry
this.freshness = calculateProductFreshness(dateOfProduction, dateOfExpiry)
}
constructor(parcel: Parcel) : this() {
@ -24,6 +30,7 @@ class Product() : Parcelable {
amount = parcel.readInt()
dateOfProduction = parcel.readString()!!
dateOfExpiry = parcel.readString()!!
freshness = parcel.readDouble()
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
@ -32,6 +39,7 @@ class Product() : Parcelable {
parcel.writeInt(amount)
parcel.writeString(dateOfProduction)
parcel.writeString(dateOfExpiry)
parcel.writeDouble(freshness)
}
override fun describeContents(): Int {

View File

@ -12,10 +12,10 @@ import androidx.core.content.ContextCompat
import androidx.core.view.children
import androidx.fragment.app.Fragment
import org.foxarmy.barcodescannerforemployees.CategoriesContract
import org.foxarmy.barcodescannerforemployees.dataclasses.Category
import org.foxarmy.barcodescannerforemployees.DBStorageController
import org.foxarmy.barcodescannerforemployees.R
import org.foxarmy.barcodescannerforemployees.activities.AddCategoryActivity
import org.foxarmy.barcodescannerforemployees.dataclasses.Category
import org.foxarmy.barcodescannerforemployees.views.CategoryView
class CategoriesFragment : Fragment() {
@ -61,8 +61,6 @@ class CategoriesFragment : Fragment() {
ContextCompat.startActivity(context!!, addCategoryIntent, extras)
}
}
}
fun updateContent() {

View File

@ -5,31 +5,57 @@ import android.provider.BaseColumns
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.Toast
import androidx.core.view.children
import androidx.fragment.app.Fragment
import androidx.gridlayout.widget.GridLayout
import org.foxarmy.barcodescannerforemployees.AbstractProductContract
import org.foxarmy.barcodescannerforemployees.DBStorageController
import org.foxarmy.barcodescannerforemployees.ProductContract
import org.foxarmy.barcodescannerforemployees.R
import org.foxarmy.barcodescannerforemployees.databinding.FragmentShelfBinding
import org.foxarmy.barcodescannerforemployees.dataclasses.Product
import org.foxarmy.barcodescannerforemployees.views.ProductView
import kotlin.concurrent.thread
class ShelfFragment : Fragment() {
private lateinit var binding: FragmentShelfBinding
private var updateInProgress = false
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
): View {
binding = FragmentShelfBinding.inflate(layoutInflater)
return inflater.inflate(R.layout.fragment_shelf, container, false)
}
fillUpSortBySpinner()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) {
updateContent()
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
updateContent()
}
}
updateContent()
return binding.root
}
private fun fillUpSortBySpinner() {
val sorts = mutableListOf("Name", "Category", "Freshness", "Date of production", "Date of expiry")
val arrayAdapter =
ArrayAdapter(requireContext(), androidx.appcompat.R.layout.support_simple_spinner_dropdown_item, sorts)
arrayAdapter.setDropDownViewResource(androidx.appcompat.R.layout.support_simple_spinner_dropdown_item)
binding.spinner.adapter = arrayAdapter
}
override fun onResume() {
@ -40,6 +66,8 @@ class ShelfFragment : Fragment() {
fun updateContent() {
thread {
if (updateInProgress) return@thread
updateInProgress = true
val grv = view?.findViewById<GridLayout>(R.id.contentGridLayout)
activity!!.runOnUiThread {
grv?.removeAllViews()
@ -54,7 +82,37 @@ class ShelfFragment : Fragment() {
ProductContract.ProductEntry.EXPIRY_DATE,
)
val cursor = db.query(ProductContract.ProductEntry.TABLE_NAME, projection, null, null, null, null, null)
var orderBy: String = ""
when (binding.spinner.selectedItem) {
"Name" -> {
orderBy =
"(SELECT ${AbstractProductContract.AbstractProductEntry.PRODUCT_NAME} FROM ${AbstractProductContract.AbstractProductEntry.TABLE_NAME} WHERE ${BaseColumns._ID} = ${ProductContract.ProductEntry.ABSTRACT_PRODUCT_ID}) ASC"
}
"Category" -> {
orderBy =
"(SELECT ${AbstractProductContract.AbstractProductEntry.CATEGORY} FROM ${AbstractProductContract.AbstractProductEntry.TABLE_NAME} WHERE ${BaseColumns._ID} = ${ProductContract.ProductEntry.ABSTRACT_PRODUCT_ID}) ASC"
}
// Wow, I wrote this on first try but unfortunately SQLite can't do that :(
// I'll leave it here as a memory.
// "Freshness" -> {
// orderBy =
// "(SELECT ( (julianday(${ProductContract.ProductEntry.EXPIRY_DATE}) - julianday('now', 'localtime')) / (julianday(${ProductContract.ProductEntry.EXPIRY_DATE}) - julianday(${ProductContract.ProductEntry.DATE_OF_PRODUCTION})) )) ASC"
// }
"Date of production" -> {
orderBy = "${ProductContract.ProductEntry.DATE_OF_PRODUCTION} ASC"
}
"Date of expiry" -> {
orderBy = "${ProductContract.ProductEntry.EXPIRY_DATE} ASC"
}
}
val cursor = db.query(ProductContract.ProductEntry.TABLE_NAME, projection, null, null, null, null, orderBy)
val products = mutableListOf<Product>()
with(cursor) {
while (moveToNext()) {
@ -68,16 +126,34 @@ class ShelfFragment : Fragment() {
val product = Product(productId, abstractProductId, amount, dateOfProduction, dateOfExpiry)
val productView = ProductView(
requireActivity(),
requireContext(),
product
)
activity!!.runOnUiThread {
grv?.addView(productView)
if (binding.spinner.selectedItem == "Freshness") {
products.add(product)
} else {
val productView = ProductView(
requireActivity(),
requireContext(),
product
)
activity!!.runOnUiThread {
grv?.addView(productView)
}
}
}
}
products.sortWith(compareByDescending { it.freshness })
for (product in products.iterator()) {
val productView = ProductView(
requireActivity(),
requireContext(),
product
)
activity!!.runOnUiThread {
grv?.addView(productView)
}
}
updateInProgress = false
}
}
@ -92,7 +168,6 @@ class ShelfFragment : Fragment() {
view.findViewById<ImageView>(R.id.productPicture).setImageURI(null)
}
if (view.isProductSelected) {
// db.eraseAbstractProduct(db.writableDatabase, view.abstractProduct.id, requireContext())
db.eraseProduct(db.writableDatabase, view.product.id)
deleted = true
}

View File

@ -3,19 +3,22 @@ package org.foxarmy.barcodescannerforemployees.fragments
import android.content.Intent
import android.os.Bundle
import android.provider.BaseColumns
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.view.children
import androidx.fragment.app.Fragment
import androidx.gridlayout.widget.GridLayout
import org.foxarmy.barcodescannerforemployees.AbstractProductContract
import org.foxarmy.barcodescannerforemployees.DBStorageController
import org.foxarmy.barcodescannerforemployees.R
import org.foxarmy.barcodescannerforemployees.activities.AddAbstractProductActivity
import org.foxarmy.barcodescannerforemployees.databinding.FragmentStorageBinding
import org.foxarmy.barcodescannerforemployees.dataclasses.AbstractProduct
import org.foxarmy.barcodescannerforemployees.generateThumbnailForImage
import org.foxarmy.barcodescannerforemployees.views.AbstractProductView
@ -23,11 +26,27 @@ import kotlin.concurrent.thread
class StorageFragment : Fragment() {
private lateinit var binding: FragmentStorageBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_storage, container, false)
): View {
binding = FragmentStorageBinding.inflate(inflater)
fillUpSortBySpinner()
binding.spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) {
updateContent()
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
updateContent()
}
}
return binding.root
}
override fun onResume() {
@ -36,20 +55,21 @@ class StorageFragment : Fragment() {
updateContent()
}
override fun onStop() {
super.onStop()
private fun fillUpSortBySpinner() {
val sorts = mutableListOf("Name", "Category")
val grv = view?.findViewById<GridLayout>(R.id.contentGridLayout)
grv?.removeAllViews()
val arrayAdapter = ArrayAdapter(requireContext(), androidx.appcompat.R.layout.support_simple_spinner_dropdown_item, sorts)
arrayAdapter.setDropDownViewResource(androidx.appcompat.R.layout.support_simple_spinner_dropdown_item)
binding.spinner.adapter = arrayAdapter
}
fun removeSelected() {
thread {
val grv = view?.findViewById<GridLayout>(R.id.contentGridLayout)
val grv = binding.contentGridLayout
val db = DBStorageController(requireContext())
var deleted = false
for (view: AbstractProductView in grv?.children!!.iterator() as Iterator<AbstractProductView>) {
for (view: AbstractProductView in grv.children.iterator() as Iterator<AbstractProductView>) {
activity!!.runOnUiThread {
view.findViewById<ImageView>(R.id.productPicture).setImageURI(null)
}
@ -69,9 +89,9 @@ class StorageFragment : Fragment() {
}
fun updateSelected() {
val grv = view?.findViewById<GridLayout>(R.id.contentGridLayout)
val grv = binding.contentGridLayout
for (view: AbstractProductView in grv?.children!!.iterator() as Iterator<AbstractProductView>) {
for (view: AbstractProductView in grv.children.iterator() as Iterator<AbstractProductView>) {
if (view.isProductSelected) {
val addProductIntent = Intent(requireContext(), AddAbstractProductActivity::class.java)
val extras = Bundle()
@ -84,9 +104,9 @@ class StorageFragment : Fragment() {
fun updateContent() {
thread {
val grv:GridLayout? = view?.findViewById(R.id.contentGridLayout)
val grv = binding.contentGridLayout
activity!!.runOnUiThread{
grv?.removeAllViews()
grv.removeAllViews()
}
val db = DBStorageController(requireContext()).readableDatabase
@ -99,7 +119,18 @@ class StorageFragment : Fragment() {
AbstractProductContract.AbstractProductEntry.CATEGORY
)
val cursor = db.query(AbstractProductContract.AbstractProductEntry.TABLE_NAME, projection, null, null, null, null, null)
var orderBy: String = ""
Log.d("QWERTYUIOP", binding.spinner.selectedItem.toString())
when(binding.spinner.selectedItem) {
"Name" -> {
orderBy = "${AbstractProductContract.AbstractProductEntry.PRODUCT_NAME} ASC"
}
"Category" -> {
orderBy = "${AbstractProductContract.AbstractProductEntry.CATEGORY} ASC"
}
}
val cursor = db.query(AbstractProductContract.AbstractProductEntry.TABLE_NAME, projection, null, null, null, null, orderBy)
with (cursor) {
while(moveToNext()) {
@ -121,7 +152,7 @@ class StorageFragment : Fragment() {
)
activity!!.runOnUiThread{
grv?.addView(abstractProduct)
grv.addView(abstractProduct)
}
}
}

View File

@ -2,11 +2,12 @@ package org.foxarmy.barcodescannerforemployees.views
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.os.Bundle
import android.util.AttributeSet
import android.util.Log
import android.view.LayoutInflater
import android.widget.ImageView
import android.widget.LinearLayout
@ -14,20 +15,17 @@ import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.startActivity
import androidx.core.graphics.blue
import androidx.core.graphics.green
import androidx.core.graphics.red
import androidx.core.math.MathUtils.clamp
import org.foxarmy.barcodescannerforemployees.DBStorageController
import org.foxarmy.barcodescannerforemployees.R
import org.foxarmy.barcodescannerforemployees.*
import org.foxarmy.barcodescannerforemployees.activities.FullscreenActivity
import org.foxarmy.barcodescannerforemployees.dataclasses.AbstractProduct
import org.foxarmy.barcodescannerforemployees.dataclasses.Product
import org.foxarmy.barcodescannerforemployees.getActivity
import org.foxarmy.barcodescannerforemployees.getImageUri
import java.io.File
import java.time.Duration
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.text.DecimalFormat
import kotlin.concurrent.thread
class ProductView: LinearLayout {
@ -41,6 +39,8 @@ class ProductView: LinearLayout {
private lateinit var productAmountTextView: TextView
private lateinit var productCategoryView: TextView
private lateinit var productLifeSpan: TextView
private lateinit var productFreshnessTextView: TextView
private var backgroundColor: Int = 0xffffff
private var strokeColor: Int = 0x000000
private lateinit var outline: GradientDrawable
@ -64,6 +64,7 @@ class ProductView: LinearLayout {
productAmountTextView = findViewById(R.id.amountView)
productCategoryView = findViewById(R.id.categoryView)
productLifeSpan = findViewById(R.id.dateSpan)
productFreshnessTextView = findViewById(R.id.freshnessPercentTextView)
findViewById<ConstraintLayout>(R.id.productLayout).setOnLongClickListener {
isProductSelected = !isProductSelected
@ -71,6 +72,15 @@ class ProductView: LinearLayout {
true
}
productImageView.setOnClickListener {
val fullscreenIntent = Intent(activity, FullscreenActivity::class.java)
val extras = Bundle()
val abstractProduct = DBStorageController(context).findAbstractProductById(DBStorageController(context).readableDatabase, product.abstractProductId)
extras.putString("imagehash", abstractProduct!!.imageHash)
fullscreenIntent.putExtras(extras)
startActivity(context, fullscreenIntent, extras)
}
update()
}
@ -90,7 +100,12 @@ class ProductView: LinearLayout {
productAmountTextView.text = product.amount.toString()
productCategoryView.text = DBStorageController(activity).getCategoryNameById(DBStorageController(activity).readableDatabase, linkedAbstractProduct.category)
productLifeSpan.text = "${product.dateOfProduction}-${product.dateOfExpiry}"
productFreshnessTextView.text =
if (product.freshness == Double.NEGATIVE_INFINITY || product.freshness == Double.POSITIVE_INFINITY) {
"Expired"
} else {
"${DecimalFormat("#.#").format(product.freshness*100)}%"
}
updateStroke()
}
@ -118,17 +133,7 @@ class ProductView: LinearLayout {
@RequiresApi(Build.VERSION_CODES.O)
fun evaluateColor(): Int {
val dateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("d.M.yyyy")
val fresh = LocalDate.parse(product.dateOfProduction, dateFormatter)
val expired = LocalDate.parse(product.dateOfExpiry, dateFormatter)
val shelfLife = Duration.between(fresh.atStartOfDay(), expired.atStartOfDay()).toDays()
val today = LocalDate.parse(LocalDate.now().format(dateFormatter), dateFormatter)
val daysBeforeExpiry = Duration.between(today.atStartOfDay(), expired.atStartOfDay()).toDays()
val freshnessPercentage: Double = daysBeforeExpiry / shelfLife.toDouble()
val freshnessPercentage = calculateProductFreshness(product.dateOfProduction, product.dateOfExpiry)
return calculateFreshnessGradient(freshnessPercentage)
}
@ -153,9 +158,6 @@ class ProductView: LinearLayout {
val colorString = "#$redHex$greenHex$blueHex"
Log.d("QWERTYUIOP", "$red:$green:$blue")
Log.d("QWERTYUIOP", "$redHex-$greenHex-$blueHex")
Log.d("QWERTYUIOP", colorString)
return Color.parseColor(colorString)
}
@ -166,8 +168,6 @@ class ProductView: LinearLayout {
val red = c.red() * (1 - darkPercent)
val green = c.green() * (1 - darkPercent)
val blue = c.blue() * (1 - darkPercent)
Log.d("QWERTYUIOP", "....A:${c.red()}, ${c.green()}, ${c.blue()}")
Log.d("QWERTYUIOP", "....B:${red.toFloat()}, ${green.toFloat()}, ${blue.toFloat()}")
return Color.rgb(red.toFloat(), green.toFloat(), blue.toFloat())
}

View File

@ -1,7 +1,7 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="200dp"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="190dp"
android:layout_height="wrap_content" app:barrierMargin="1dp" android:clickable="false"
android:outlineProvider="background" android:background="@drawable/outline">

View File

@ -3,14 +3,36 @@
xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.gridlayout.widget.GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/contentGridLayout" app:columnCount="2"
app:rowCount="100" android:nestedScrollingEnabled="true">
</androidx.gridlayout.widget.GridLayout>
</ScrollView>
<TextView
android:text="Sort by"
android:layout_width="wrap_content"
android:layout_height="0dp" android:id="@+id/sortByTextView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/scrollView" android:textAlignment="center"
/>
<Spinner
android:layout_width="0dp"
android:layout_height="32dp" android:id="@+id/spinner"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@+id/sortByTextView"
app:layout_constraintEnd_toEndOf="parent" android:layout_marginStart="16dp"
/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent" app:layout_constraintTop_toBottomOf="@+id/spinner"
android:layout_marginTop="32dp" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" android:id="@+id/scrollView">
<androidx.gridlayout.widget.GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/contentGridLayout" app:columnCount="2"
app:rowCount="100" android:nestedScrollingEnabled="true">
</androidx.gridlayout.widget.GridLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@ -5,14 +5,35 @@
android:layout_height="match_parent"
android:id="@+id/fragment_storage"
tools:context=".fragments.StorageFragment">
<ScrollView
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.gridlayout.widget.GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/contentGridLayout" app:columnCount="2"
app:rowCount="100" android:nestedScrollingEnabled="true">
</androidx.gridlayout.widget.GridLayout>
</ScrollView>
<TextView
android:text="Sort by"
android:layout_width="wrap_content"
android:layout_height="0dp" android:id="@+id/sortByTextView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:textAlignment="center"
app:layout_constraintBottom_toBottomOf="@+id/scrollView2"/>
<Spinner
android:layout_width="0dp"
android:layout_height="32dp" android:id="@+id/spinner"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@+id/sortByTextView"
app:layout_constraintEnd_toEndOf="parent" android:layout_marginStart="16dp"
/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@+id/spinner"
android:id="@+id/scrollView2">
<androidx.gridlayout.widget.GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/contentGridLayout" app:columnCount="2"
app:rowCount="100" android:nestedScrollingEnabled="true">
</androidx.gridlayout.widget.GridLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical"
android:layout_width="200dp"
android:layout_width="190dp"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
@ -14,6 +14,12 @@
android:id="@+id/productPicture"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:text="0%"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:id="@+id/freshnessPercentTextView"
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="10dp"
app:layout_constraintTop_toTopOf="parent" android:layout_marginTop="5dp"/>
<TextView
android:text="@string/sample_product_name"
android:layout_width="match_parent"