PolyBooks/sdp_polyBooks

View on GitHub
app/src/main/java/com/github/polybooks/activities/FillSaleActivity.kt

Summary

Maintainability
B
4 hrs
Test Coverage
C
71%
package com.github.polybooks.activities

import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.View
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import com.github.polybooks.R
import com.github.polybooks.core.Book
import com.github.polybooks.core.BookCondition
import com.github.polybooks.core.LoggedUser
import com.github.polybooks.core.SaleState
import com.github.polybooks.database.BookDatabase
import com.github.polybooks.database.Database
import com.github.polybooks.database.SaleDatabase
import com.github.polybooks.utils.GlobalVariables.EXTRA_ISBN
import com.github.polybooks.utils.GlobalVariables.EXTRA_PICTURE_FILE
import com.github.polybooks.utils.GlobalVariables.EXTRA_SALE_PRICE
import com.github.polybooks.utils.StringsManip.isbnHasCorrectFormat
import com.github.polybooks.utils.StringsManip.listAuthorsToString
import com.github.polybooks.utils.UIManip.disableButton
import com.github.polybooks.utils.UIManip.enableButton
import com.github.polybooks.utils.setupNavbar
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase
import java.text.DateFormat
import java.util.concurrent.CompletableFuture


/**
 * This activity receives the ISBN, either manually inputted from AddSale or deduced from the scanned barcode,
 * shows the retrieved data, but does not allow modification of it, only confirmation,
 * and offers some additional manual fields such as price, condition, etc.
 */
class FillSaleActivity: AppCompatActivity(), AdapterView.OnItemSelectedListener {

    private lateinit var bookDB: BookDatabase
    private lateinit var salesDB: SaleDatabase

    private val dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.LONG)

    private lateinit var bookFuture: CompletableFuture<Book?>
    private var stringISBN: String = ""
    private var pictureFileName: String = ""
    private var bookConditionSelected: BookCondition? = null


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_fill_sale)

        bookDB = Database.bookDatabase(applicationContext)
        salesDB = Database.saleDatabase(applicationContext)

        // Get the Intent that started this activity and extract the strings
        val intent = intent
        val extras = intent.extras
        if (extras != null) {
            stringISBN = extras.getString(EXTRA_ISBN) ?: ""
            pictureFileName = extras.getString(EXTRA_PICTURE_FILE) ?: ""
            findViewById<EditText>(R.id.filled_price).setText(extras.getString(EXTRA_SALE_PRICE))
        }


        // Check if ISBN in our database

        // Retrieve book data and display it if possible, else redirect with error toast
        if (stringISBN.isNotEmpty() && isbnHasCorrectFormat(stringISBN)) {
            try {
                bookFuture = bookDB.getBook(stringISBN)
                bookFuture.thenAccept { book ->
                    if (book != null) {
                        fillBookData(book)
                    } else {
                        redirectToAddSaleWithToast(getString(R.string.no_ISBN_match))
                    }
                }
            } catch (e: Exception) {
                redirectToAddSaleWithToast(getString(R.string.error))
            }
        } else {
            redirectToAddSaleWithToast(getString(R.string.missing_ISBN))
        }
        // Drop-down menu for condition
        val spinner: Spinner = findViewById(R.id.filled_condition)
        // Create an ArrayAdapter using the string array and a default spinner layout
        ArrayAdapter.createFromResource(
            this,
            R.array.condition_options_array,
            android.R.layout.simple_spinner_item
        ).also { adapter ->
            // Specify the layout to use when the list of choices appears
            adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
            // Apply the adapter to the spinner
            spinner.adapter = adapter
        }
        spinner.onItemSelectedListener = this

        // Listener on fill-in book price to trigger confirm button
        findViewById<EditText>(R.id.filled_price).addTextChangedListener(object: TextWatcher {
            override fun afterTextChanged(s: Editable?) {
                handleConfirmButton()
            }

            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
            }

            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
            }
        })

        // Disable confirm button until filled
        disableButton(findViewById(R.id.confirm_sale_button), applicationContext)
        setupNavbar(findViewById(R.id.bottom_navigation), this)
    }

    /**
     * Fill the UI with the data about a book
     */
    private fun fillBookData(book: Book) {
        findViewById<TextView>(R.id.filled_authors).apply {
            text = listAuthorsToString(book.authors)
        }
        findViewById<TextView>(R.id.filled_title).apply { text = book.title }
        findViewById<TextView>(R.id.filled_edition).apply { text = book.edition ?: "" }
        // TODO language is ideally not converted to a string but to a flag
        // findViewById<TextView>(R.id.filled_language).apply { text = book.language ?: "" }
        findViewById<TextView>(R.id.filled_publisher).apply { text = book.publisher ?: "" }
        findViewById<TextView>(R.id.filled_publish_date).apply {
            text = dateFormat.format(book.publishDate!!) ?: ""
        }
        // TODO whole lines could be removed from UI (visibility = View.GONE) when argument is null instead of placeholding with default value
        findViewById<TextView>(R.id.filled_format).apply { text = book.format ?: "" }

        if (pictureFileName.isNotEmpty()) {
            Log.w("BookFuture", this.filesDir.path + '/' + pictureFileName)
            val bmImg = BitmapFactory.decodeFile(this.filesDir.path + '/' + pictureFileName)
            //TODO The picture gets rotated 90°, could work on fixing this, but not urgent.
            findViewById<ImageView>(R.id.proof_picture).setImageBitmap(bmImg)
        } else {
            findViewById<ImageView>(R.id.proof_picture).visibility = View.GONE
        }
    }

    /**
     * Called when an issue happen to convert the issue to a nicer UX flow than crashing
     * Showing a message about the error and redirecting to a previous activity
     */
    private fun redirectToAddSaleWithToast(message: String) {
        Log.w("BookFuture", message)
        Toast.makeText(
            this,
            message,
            Toast.LENGTH_LONG
        ).show()

        val intent = Intent(this, AddSaleActivity::class.java)
        startActivity(intent)

    }

    fun takePicture(view: View) {
        val intent = Intent(this, TakeBookPictureActivity::class.java).apply {
            val extras = Bundle()
            extras.putString(EXTRA_ISBN, stringISBN)
            extras.putString(
                EXTRA_SALE_PRICE,
                findViewById<EditText>(R.id.filled_price).text.toString()
            )
            putExtras(extras)
        }
        startActivity(intent)
    }


    /**
     * Create a sale in the database with the relevant data from the activity
     * (i.e. book, condition, user, price, date)
     */
    fun confirmSale(view: View) {
        Firebase.auth.currentUser?.let {
        salesDB.addSale(
            bookISBN = bookFuture.get()!!.isbn,
            seller = LoggedUser(it.uid, it.displayName), //TODO handle real User
            price = findViewById<EditText>(R.id.filled_price).text.toString().toFloat(),
            condition = bookConditionSelected!!,
            state = SaleState.ACTIVE,
            image = null // TODO
        )
        }
        //TODO handle success or failure of the addSale

        // TODO determine to which activity we land, but probably not MainActivity but rather a confirmation page
        val intent = Intent(this, MainActivity::class.java)
        startActivity(intent)
    }

    private fun handleConfirmButton() {
        if (bookConditionSelected != null && findViewById<EditText>(R.id.filled_price).text.toString()
                .isNotEmpty()
        ) {
            enableButton(findViewById(R.id.confirm_sale_button), applicationContext)
        } else {
            disableButton(findViewById(R.id.confirm_sale_button), applicationContext)
        }
    }

    /**
     *
     * Callback method to be invoked when an item in this view has been
     * selected. This callback is invoked only when the newly selected
     * position is different from the previously selected position or if
     * there was no selected item.
     *
     * Implementers can call getItemAtPosition(position) if they need to access the
     * data associated with the selected item.
     *
     * @param parent The AdapterView where the selection happened
     * @param view The view within the AdapterView that was clicked
     * @param position The position of the view in the adapter
     * @param id The row id of the item that is selected
     */
    override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
        when (parent?.getItemAtPosition(position).toString()) {
            "Select" -> bookConditionSelected = null
            "New" -> bookConditionSelected = BookCondition.NEW
            "Good" -> bookConditionSelected = BookCondition.GOOD
            "Worn" -> bookConditionSelected = BookCondition.WORN
            else -> {
                Toast.makeText(
                    applicationContext,
                    "Error in selecting the Book Condition",
                    Toast.LENGTH_LONG
                ).show()
            }
        }
        handleConfirmButton()
    }

    /**
     * Callback method to be invoked when the selection disappears from this
     * view. The selection can disappear for instance when touch is activated
     * or when the adapter becomes empty.
     *
     * @param parent The AdapterView that now contains no selected item.
     */
    override fun onNothingSelected(parent: AdapterView<*>?) {
        disableButton(findViewById(R.id.confirm_sale_button), applicationContext)
    }
}