1

This is my very first simple and almost-complete Android application.

My Goal

I want to take a picture with the device's built-in camera, display it on the ImageButton in that same Fragment, and have it save to the device locally so that I can reference its file path and add it to my custom SQLite Database to display in my RecyclerView on a different Fragment.

Background Info

My app is built on top of a custom SQLite Database backend containing two tables, users and their associated books that they've added to the app to be displayed in a RecyclerView. The app is built entirely with Kotlin. Navigation is done using Android Jetpack's Navigation with a navigation graph (currently not using SafeArgs), and the entire app is run from a single MainActivity that holds multiple Fragments. I have a Fragment containing a RecyclerView, and this RecyclerView contains an ImageView where I hope to display the thumbnail of the picture that the user took with their device. To display images, I am using the 3rd-party Picasso library. The minimum API that my app is targeting is API 23. I am using the deprecated Camera API because Camera2 and CameraX had pretty much no improvement of my issue, so I fell back on this and it's not helping either.

My manifest contains these bits of code for Write access and Camera access permissions:

<uses-feature android:name="android.hardware.camera"
        android:required="false"
        />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA"/>

My file provider is set up as follows:

<provider
     android:authorities="<my_package_authority>.fileprovider"
     android:name="androidx.core.content.FileProvider"
     android:exported="false"
     android:grantUriPermissions="true">
     <meta-data
         android:name="android.support.FILE_PROVIDER_PATHS"
         android:resource="@xml/file_paths"
         />

</provider>

My file_paths.xml file is here:

<?xml version="1.0" encoding="utf-8" ?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-files-path
        name="my_images"
        path="Android/data/<my_package_authority>/files/Pictures">
    </external-files-path>
    <external-files-path
        name="my_debug_images"
        path="/storage/emulated/0/Android/data/<my_package_authority>/files/Pictures">
    </external-files-path>
    <external-files-path
        name="my_root_images"
        path="/">
    </external-files-path>
</paths>

Here is my MainActivity.kt, which I use basically to store the request codes as well as the userID and bookID of the users and their books to upload to my database (patch-up solution to be better implemented later):

class MainActivity : AppCompatActivity() {

    companion object {
        val REQUEST_CAMERA_PERMISSIONS_CODE = 1
        val REQUEST_CAMERA_USAGE = 2

        var userID: Int? = null
        var bookID: Int? = null
    }


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

    }
}

The .xml is completely unchanged and default.

Below is the .xml and .kt files for CreateBookFragment, the Fragment on which the user is going to add a book to their personal library.

fragment_create_book.xml (I am aware some of the bottom buttons clip out of the screen, depending on screen size. I plan to fix after the camera functionality is implemented)

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/clCreateBookRoot"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/grey"
    tools:context=".CreateBookFragment">

    <TextView
        android:id="@+id/tvCreateBookTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:fontFamily="cursive"
        android:shadowColor="@color/deep_red"
        android:shadowDx="1.5"
        android:shadowDy="1.5"
        android:shadowRadius="1.5"
        android:text="@string/add_book"
        android:textColor="@color/deep_red"
        android:textSize="55sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.024" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="3dp"
        android:orientation="vertical"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tvCreateBookTitle">

        <ImageButton
            android:id="@+id/ibCreateBookImage"
            android:layout_width="75dp"
            android:layout_height="75dp"
            android:layout_gravity="center"
            android:backgroundTint="@color/deep_red"
            android:contentDescription="@string/add_a_picture_to_the_book"
            android:src="@android:drawable/ic_menu_camera" />

        <EditText
            android:id="@+id/etCreateBookTitle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="50dp"
            android:layout_marginTop="3dp"
            android:layout_marginEnd="50dp"
            android:layout_marginBottom="5dp"
            android:ems="10"
            android:hint="@string/title"
            android:importantForAutofill="no"
            android:inputType="textPersonName"
            android:minHeight="48dp"
            android:textColorHint="@color/black"
            tools:ignore="TextContrastCheck" />

        <EditText
            android:id="@+id/etCreateBookAuthor"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="50dp"
            android:layout_marginTop="3dp"
            android:layout_marginEnd="50dp"
            android:layout_marginBottom="5dp"
            android:ems="10"
            android:hint="@string/author"
            android:importantForAutofill="no"
            android:inputType="textPersonName"
            android:minHeight="48dp"
            android:textColorHint="@color/black"
            tools:ignore="TextContrastCheck" />

        <EditText
            android:id="@+id/etCreateBookPages"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="50dp"
            android:layout_marginTop="3dp"
            android:layout_marginEnd="50dp"
            android:layout_marginBottom="5dp"
            android:ems="10"
            android:hint="@string/total_pages"
            android:importantForAutofill="no"
            android:inputType="number"
            android:minHeight="48dp"
            android:textColorHint="@color/black"
            tools:ignore="TextContrastCheck" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginStart="50dp"
            android:layout_marginTop="3dp"
            android:layout_marginEnd="50dp"
            android:layout_marginBottom="5dp"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/tvCreateBookGenre"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginStart="4dp"
                android:layout_marginEnd="10dp"
                android:text="@string/genre"
                android:textColor="@color/black"
                android:textSize="18sp" />

            <Spinner
                android:id="@+id/spinCreateBookGenre"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:ems="10"
                android:hint="@string/genre"
                android:importantForAutofill="no"
                android:inputType="textPersonName"
                android:minHeight="48dp"
                android:textColorHint="@color/black"
                tools:ignore="TextContrastCheck,SpeakableTextPresentCheck" />
        </LinearLayout>

        <EditText
            android:id="@+id/etCreateBookPublisher"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="50dp"
            android:layout_marginTop="3dp"
            android:layout_marginEnd="50dp"
            android:layout_marginBottom="5dp"
            android:ems="10"
            android:hint="@string/publisher"
            android:importantForAutofill="no"
            android:inputType="textPersonName"
            android:minHeight="48dp"
            android:textColorHint="@color/black"
            tools:ignore="TextContrastCheck" />

        <EditText
            android:id="@+id/etCreateBookYearPublished"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="50dp"
            android:layout_marginTop="3dp"
            android:layout_marginEnd="50dp"
            android:layout_marginBottom="5dp"
            android:ems="10"
            android:hint="@string/year_published"
            android:importantForAutofill="no"
            android:inputType="number"
            android:minHeight="48dp"
            android:textColorHint="@color/black"
            tools:ignore="TextContrastCheck" />

        <EditText
            android:id="@+id/etCreateBookISBN"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="50dp"
            android:layout_marginTop="3dp"
            android:layout_marginEnd="50dp"
            android:layout_marginBottom="5dp"
            android:ems="10"
            android:hint="@string/isbn_code"
            android:importantForAutofill="no"
            android:inputType="textPersonName"
            android:minHeight="48dp"
            android:textColorHint="@color/black" />

        <EditText
            android:id="@+id/etCreateBookStarRating"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="50dp"
            android:layout_marginTop="3dp"
            android:layout_marginEnd="50dp"
            android:layout_marginBottom="5dp"
            android:ems="10"
            android:hint="@string/star_rating_1_5"
            android:importantForAutofill="no"
            android:inputType="number"
            android:minHeight="48dp"
            android:textColorHint="@color/black"
            tools:ignore="TextContrastCheck" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="3dp"
            android:gravity="center"
            android:orientation="horizontal">

            <Button
                android:id="@+id/btnCancelCreateBook"
                style="@style/cancel_button_style"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@android:string/cancel" />

            <Button
                android:id="@+id/btnSaveCreateBook"
                style="@style/save_button_style"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/add" />
        </LinearLayout>

    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

CreateBookFragment.kt

import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.navigation.NavController
import androidx.navigation.Navigation
import com.squareup.picasso.MemoryPolicy
import com.squareup.picasso.NetworkPolicy
import com.squareup.picasso.Picasso
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*

class CreateBookFragment : Fragment(), View.OnClickListener, AdapterView.OnItemSelectedListener {

    private var photoFilePath: String? = ""  // This is returning null!
    private var navigationController: NavController? = null

    private lateinit var etCreateBookTitle: EditText
    private lateinit var etCreateBookAuthor: EditText
    private lateinit var etCreateBookPages: EditText
    private lateinit var spinCreateBookGenre: Spinner
    private lateinit var etCreateBookPublisher: EditText
    private lateinit var etCreateBookYearPublished: EditText
    private lateinit var etCreateBookISBN: EditText
    private lateinit var etCreateBookStarRating: EditText
    private lateinit var ibCreateBookImage: ImageButton
    private lateinit var btnCancelCreateBook: Button
    private lateinit var btnSaveCreateBook: Button
    private lateinit var genres: Array<out String>
    private lateinit var spinnerText: String

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment

        return inflater.inflate(R.layout.fragment_create_book, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        navigationController = Navigation.findNavController(view)

        initialiseUIElements(view)

        setUpButtonClickListeners()

        setUpGenreSpinner()

    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == MainActivity.REQUEST_CAMERA_USAGE && resultCode == Activity.RESULT_OK) {
            val photoUri: Uri = Uri.parse(photoFilePath)

            Picasso.with(requireContext())
                .load(photoUri)
                .memoryPolicy(MemoryPolicy.NO_CACHE)
                .networkPolicy(NetworkPolicy.NO_CACHE)
                .error(R.drawable.ic_custom_bookshelf)
                .fit()
                .centerInside()
                .noFade()
                .into(ibCreateBookImage)
        }
    }

    private fun initialiseUIElements(view: View) {
        etCreateBookTitle = view.findViewById(R.id.etCreateBookTitle)
        etCreateBookAuthor = view.findViewById(R.id.etCreateBookAuthor)
        etCreateBookPages = view.findViewById(R.id.etCreateBookPages)
        spinCreateBookGenre = view.findViewById(R.id.spinCreateBookGenre)
        etCreateBookPublisher = view.findViewById(R.id.etCreateBookPublisher)
        etCreateBookYearPublished = view.findViewById(R.id.etCreateBookYearPublished)
        etCreateBookISBN = view.findViewById(R.id.etCreateBookISBN)
        etCreateBookStarRating = view.findViewById(R.id.etCreateBookStarRating)
        ibCreateBookImage = view.findViewById(R.id.ibCreateBookImage)
        btnCancelCreateBook = view.findViewById(R.id.btnCancelCreateBook)
        btnSaveCreateBook = view.findViewById(R.id.btnSaveCreateBook)
    }

    private fun setUpButtonClickListeners() {
        ibCreateBookImage.setOnClickListener(this)
        btnCancelCreateBook.setOnClickListener(this)
        btnSaveCreateBook.setOnClickListener(this)
    }

    private fun setUpGenreSpinner() {
        genres = resources.getStringArray(R.array.book_genres)
        val spinnerAdapter: ArrayAdapter<CharSequence> = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, genres)
        spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        spinCreateBookGenre.adapter = spinnerAdapter
        spinCreateBookGenre.onItemSelectedListener = this
    }

    private fun addBook(view: View) {
        if (checkEmpty(etCreateBookTitle) || checkEmpty(etCreateBookAuthor) ||
            (checkEmpty(etCreateBookPages) || !isNumeric(etCreateBookPages.text.toString().trim())) ||
            spinnerText.isEmpty() || checkEmpty(etCreateBookPublisher) ||
            (checkEmpty(etCreateBookYearPublished) || !isNumeric(etCreateBookYearPublished.text.toString().trim())) ||
            checkEmpty(etCreateBookISBN) ||
            (checkEmpty(etCreateBookStarRating) || !isNumeric(etCreateBookStarRating.text.toString().trim()))) {
            Toast.makeText(requireContext(), "Fields cannot be blank", Toast.LENGTH_SHORT).show()
        }
        else {
            val bookTitle: String = etCreateBookTitle.text.toString().trim()
            val bookAuthor: String = etCreateBookAuthor.text.toString().trim()
            val bookPages: Int = etCreateBookPages.text.toString().trim().toInt()
            val bookGenre: String = spinnerText
            val bookPublisher: String = etCreateBookPublisher.text.toString().trim()
            val bookYearPublished: Int = etCreateBookYearPublished.text.toString().trim().toInt()
            val ISBN: String = etCreateBookISBN.text.toString().trim()
            val bookStarRating: Float = etCreateBookStarRating.text.toString().trim().toFloat()
            val bookImage: String? = if (photoFilePath != "")
                photoFilePath
            else
                null

            val dbHandler: DBHandler = DBHandler(requireContext())

            val status = dbHandler.addBook(BookModelClass(null, bookTitle, bookAuthor, bookPages,
                bookGenre, bookPublisher, bookYearPublished, ISBN, bookStarRating, 0, bookImage, MainActivity.userID!!))

            if (status > -1) {
                Toast.makeText(requireContext(), "Successfully added book to list", Toast.LENGTH_SHORT).show()
                etCreateBookTitle.text.clear()
                etCreateBookAuthor.text.clear()
                etCreateBookPages.text.clear()
                etCreateBookPublisher.text.clear()
                etCreateBookYearPublished.text.clear()
                etCreateBookISBN.text.clear()
                etCreateBookStarRating.text.clear()
            }
        }
    }

    private fun checkEmpty(editText: EditText): Boolean {
        if (editText.text.toString().trim() == "") {
            return true
        }
        return false
    }

    override fun onClick(p0: View?) {
        when (p0) {
            ibCreateBookImage -> {
                if (ContextCompat.checkSelfPermission(requireContext(), android.Manifest.permission.CAMERA)
                == PackageManager.PERMISSION_GRANTED)
                    startCamera()
                else {
                    ActivityCompat.requestPermissions(requireActivity(), arrayOf(android.Manifest.permission.CAMERA),
                        MainActivity.REQUEST_CAMERA_PERMISSIONS_CODE)
                }
            }
            btnCancelCreateBook -> {
                navigationController!!.navigate(R.id.action_createBookFragment_to_bookListFragment)
                Toast.makeText(requireContext(), "Changes discarded", Toast.LENGTH_SHORT).show()
            }
            btnSaveCreateBook -> {
                addBook(p0)

                navigationController!!.navigate(R.id.action_createBookFragment_to_bookListFragment)
            }
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == MainActivity.REQUEST_CAMERA_USAGE) {
            if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)
                startCamera()
            else {
                Toast.makeText(requireContext(), "Oops! Camera permission denied", Toast.LENGTH_SHORT).show()
                return
            }
        }
    }

    private fun startCamera() {
        val cameraIntent: Intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        var photoFile: File? = null
        try {
            photoFile = createPictureFile()
        } catch (exception: IOException) {
            Toast.makeText(requireContext(), "Error: Cannot save photo", Toast.LENGTH_SHORT).show()
            return
        }
        if (photoFile != null) {
            val photoUri = FileProvider.getUriForFile(requireContext(), requireActivity().packageName + ".fileprovider", photoFile)
            cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
            startActivityForResult(cameraIntent, MainActivity.REQUEST_CAMERA_USAGE)
        }
    }

    private fun createPictureFile(): File {
        val timeStamp: String = SimpleDateFormat("ddMMyyyy_HHmmss", Locale.UK).format(Date().time)
        val photoFileName: String = "IMG_" + timeStamp + "_"
        val storageDirectory: File = requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES)!!
        return File.createTempFile(photoFileName, ".jpg", storageDirectory).apply {
            photoFilePath = absolutePath
        }
    }

    private fun isNumeric(string: String): Boolean {
        return string.all { char -> char.isDigit() }
    }

    override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) {
        spinnerText = genres[p2].toString()
    }

    override fun onNothingSelected(p0: AdapterView<*>?) {
        spinnerText = ""
    }
}

The Issue

In reality, when trying to save the picture to the device, Android Studio registers the picture/path as "null", and I used to get a NullPointerException when trying to load the pictures on the RecyclerView before I started using Picasso, even when I can see the picture was indeed saved to local storage on the emulator (API 30) and on a physical device (API 24) with the path set to: "/storage/emulated/0/Android/data/<my_package_authority>/files/Pictures/IMG_01062022_164449_8511882897552656984.jpg" for the emulator and a similar path for the physical device (file_paths.xml included below). I got this path by using an application called DBBrowser for Sqlite, which I used to confirm that my database is indeed getting the path.

Picasso is only displaying the error image that I have set for the thumbnail, despite trying so many different sources such as the official Android docs and some other forums, videos and other StackOverflow questions/answers of a similar topic. When the user tries to add a picture to their book when creating it at first, it doesn't even actually show the picture after they've taken a camera picture. What appears to happen is that the file is never "officially" created, but the file does exist at the specified location on device memory. I am currently using the deprecated startActivityForResult() function for testing purposes and trying to set things in onActivityResult() inside the Fragment, but it's not working. I am using the deprecated function because the newer functions that replace it aren't helping either, so it was kind of a last-resort thing. An answer on StackOverflow suggested that the startActivityForResult() function is actually sending the data to the MainActivity instead of localising that data in the Fragment, which has pretty much caused the biggest headaches. It's been days, if not an entire week with this issue, send help...

I do not need any help with trying to display images or even trying to get the image path from my database, I only need help figuring out how to stop the file path from returning null so that Picasso can display the image preview when the user first adds the image on CreateBookFragment. If the file path can stop returning null, I can get this entire functionality going. I debated putting my database here with the two model classes, but it's like 1 000 lines of code and StackOverflow doesn't allow more than 40 000 characters.

A Few Sources (definitely not all of them)

https://developer.android.com/training/camera/photobasics

onActivityResult is not being called in Fragment

One of the MANY Youtube videos I've watched: https://www.google.com/search?q=how+to+save+an+image+from+camera+onto+device+android+studio&rlz=1C1CHBF_enZA979ZA979&oq=how+to+save+an+image+from+camera+onto+device+android+studio&aqs=chrome..69i57j0i22i30l2j0i390.10856j0j7&sourceid=chrome&ie=UTF-8#kpvalbx=_fW2YYqCeCJq6gAbewp2gCA22

Edit: As a quick and dirty solution, I ended up saving the captured images as blobs in my SQLite database and converting them to bitmaps when it came time to displaying them on my ImageViews. I am still looking for a more efficient way to do this, as storing images as blobs in SQLite database isn't best practice.

Chùnky
  • 77
  • 7

0 Answers0