• 8 hours
  • Hard

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 4/27/23

Create a file on external storage

Let’s start developing SaveMyTrip. If you haven’t already done so, download and run this premade mini-application. 😀 

Discover the program for this first part

First, we will configure the TripBookActivity activity which is responsible for managing the travel book feature.

The goal of the travel book is to allow the user to write text in a file that will be saved in the external or internal storage space of their telephone.  The storage space will depend on the choice they make via some radio buttons (also called option boxes): 

Which storage do you want to write to?

  • External

    • What level of privacy do you want?

      • Public. 
        The file will be stored on the external storage space in public mode. Therefore, it will not be deleted when the user uninstalls the app.

      • Private.

        The file will be stored on the internal storage space in private mode. Therefore, it will be deleted when the user uninstalls the app.

  • Internal

    • Do you want to store the file in the cache?

      • Yes.
        The file will be stored on the internal storage in the cache-dedicated directory. It may be deleted at any time.

      • No.
        The file will be stored on the internal storage space 

We'll now manage how we want to save this file in the external storage space. The file can, therefore, be created either in private mode or in public mode.

This file will be called tripBook.txt, and placed in a folder called bookTrip/. We will create a file (and folder) in both available storage spaces, depending on what our user decides. 🙂

Create a utility class

First things first, we’ll create a singleton object that we’ll call StorageUtils, and place in the package utils/ of our application.

Class utils/StorageUtils.kt:

object StorageUtils {

}

Explanation: This object will be responsible for saving and retrieving data entered into our file tripBook.txt. This will avoid overloading our TripBookActivity activity and most importantly, will allow us to reuse its code in other activities if need be.

Create an access path to the file

To save the text written by our user, a destination file must first be defined. So let’s create the method  createOrGetFile()  in our class StorageUtils.java.

Class utils/StorageUtils.kt:

private fun createOrGetFile(
       destination: File,      // e.g., /storage/emulated/0/Android/data/
       fileName: String,       // e.g., tripBook.txt
       folderName: String)     // e.g., bookTrip
: File {
   val folder = File(destination, folderName)
   // file path = /storage/emulated/0/Android/data/bookTrip/tripBook.txt
   return File(folder, fileName)
}

Explanation: This method will be called to create or retrieve a file.

The class File is a little misleading because you might think it’s a representation of a file, but it's not! It actually represents a path to a file, which will be used later to save or retrieve a data stream (characters, bytes, etc...).

Example path structure
Example path structure

Let's define a path to a folder containing a file (bookTrip/tripBook.txt) from a root destination specified in a parameter of the method. Don’t worry, we'll go through that in a little bit! For now, just concentrate on defining the path. 😀

Example use for SaveMyTrip
Example use for SaveMyTrip

Read and write data from a file

Now that you’ve seen how to create and retrieve a file from a path, it would be good to be able to read and write inside it. To do this, add the following lines:

Excerpt from StorageUtils.kt:
object StorageUtils {
…

   // ----------------------------------
   // READ & WRITE STORAGE
   // ----------------------------------
   private fun readFile(context: Context, file: File): String {
 
        val sb = StringBuilder()
 
        if (file.exists()) {
            try {
                val bufferedReader = file.bufferedReader();
                bufferedReader.useLines { lines ->
                    lines.forEach {
                        sb.append(it)
                        sb.append("\n")
                    }
                }
            } catch (e: IOException) {
                Toast.makeText(context, context.getString(R.string.error_happened), Toast.LENGTH_LONG).show()
            }
        }
        return sb.toString()
    }
    // ---
    private fun writeFile(context: Context, text: String, file: File) {
 
        try {
          file.parentFile.mkdirs()
          file.bufferedWriter().use {
             out -> out.write(text)
          }
        } catch (e: IOException) {
            Toast.makeText(context, context.getString(R.string.error_happened), Toast.LENGTH_LONG).show()
            return
        }
 
        Toast.makeText(context, context.getString(R.string.saved), Toast.LENGTH_LONG).show()
   }
}

Explanation: Here we’ve added two general methods: readFile()  and  writeFile().

The method readFile()  will allow you to read the contents of a file passed as a parameter. The latter verifies that it exists, then uses the BufferedReader class that can use its memory buffer to efficiently read a data stream. This data stream will be generated by the FileReader class from the file defined in the parameter, then read line by line to finally return a String variable containing the whole text of the file.

The  writeOnFile()  method will enable you to write text to a file. If they do not already exist, this creates the folder(s) necessary for the access path (in our case, bookTrip/ ) using the mkdirs() method. Then, it opens a data flow to our file using the FileOutputStream class. Once opened, we will be able to use the BufferedWriter and OutputStreamWriter classes to efficiently add our text.

Manipulate storage spaces

Now that the general methods of writing and reading are ready, we will be able to manipulate the external and internal storage with them. To do so, add the following methods to your StorageUtils.java class.

Excerpt from StorageUtils.kt:

object StorageUtils {

   fun getTextFromStorage(rootDestination: File,
                         context: Context,
                         fileName: String,
                         folderName: String): String? {
       val file = createOrGetFile(rootDestination, fileName, folderName)
       return readFile(context, file)
    }
    
    fun setTextInStorage(rootDestination: File,
                         context: Context,
                         fileName: String,
                         folderName: String,
                         text: String) {
       val file = createOrGetFile(rootDestination, fileName, folderName)
       writeFile(context, text, file)
    }
    
   // ----------------------------------
   // EXTERNAL STORAGE
   // ----------------------------------
   
   fun isExternalStorageWritable(): Boolean {
      val state = Environment.getExternalStorageState()
      return Environment.MEDIA_MOUNTED == state
   }
   
   fun isExternalStorageReadable(): Boolean {
      val state = Environment.getExternalStorageState()
      return Environment.MEDIA_MOUNTED == state ||
             Environment.MEDIA_MOUNTED_READ_ONLY == state
   }

 Explanation: We’ve added several methods: The last two check if the external storage is available, and if it can be read (isExternalStorageReadable) or written to (isExternalStorageWritable).

Then we created two other methods to write and read text in a file located in a storage space defined in a parameter by reusing the methods writeFile and  createOrGetFile we created earlier! 😀

Does that means that we will write to the same file in the same place each time? 😲

Nope! Take a closer look at these methods. We pass a destination root directory (the parameter rootDestination ) to the method  createOrGetFile, which allows you to create or retrieve a file (tripBook.txt) and its folder (bookTrip/) from that root directory!

Now that everything’s ready, we call all of these methods in our controller, TripBookActivity. 

Updating your activity

Now let's edit our TripBookActivity activity to call these last two methods when a user takes an action.

Excerpt from TripBookActivity.kt:

class TripBookActivity : BaseActivity() {

    // 1 - FILE MANAGEMENT
    private val FILENAME = "tripBook.txt"
    private val FOLDERNAME = "bookTrip"
   ...
 
   override fun onCreate(savedInstanceState: Bundle?) {
   
      super.onCreate(savedInstanceState)
      this.configureToolbar()
      ...
     // 2 - Read from storage when starting
      this.readFromStorage()

   }

    // ----------------------------------
    // ACTIONS
    // ----------------------------------
 
    fun onClickRadioButton(button: CompoundButton, isChecked: Boolean) {

        if (isChecked) {
          ...
        }

       // 3 - Read from storage after user clicks on radio buttons
       this.readFromStorage()
    }

   override fun onOptionsItemSelected(item: MenuItem): Boolean {

       when (item.itemId) {
          ...
          R.id.action_save -> {
              // 4 – Save
              this.save()
              return true
          }
       }
       ...
   }
 
    // 5 - Save after user clicked on button
    private fun save() {

        if (radioButtonExternal.isChecked) {
            writeExternalStorage() //Save on external storage
        } else {
            //TODO: Save on internal storage
        }
    }

   // ----------------------------------
   // UTILS - STORAGE
   // ----------------------------------

   // 6 - Read from storage

 
    private fun readFromStorage() {

       if (radioButtonExternal.isChecked) {
          if (StorageUtils.isExternalStorageReadable()) {
              // EXTERNAL
              if (radioButtonExternalPublic.isChecked) {
                // External - Public
                editText.setText(StorageUtils.getTextFromStorage(

                 Environment.getExternalStoragePublicDirectory(
                 Environment.DIRECTORY_DOCUMENTS), this, FILENAME, FOLDERNAME))
            } else {
                // External - Privatex
                editText.setText(StorageUtils.getTextFromStorage(

                 getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)!!, this, FILENAME, FOLDERNAME))
            }
        }
    } else {
        // TODO : READ FROM INTERNAL STORAGE
    }

   // 7 - Write external storage
   private fun writeExternalStorage() {

        if (StorageUtils.isExternalStorageWritable()) {
            if (radioButtonExternalPublic.isChecked) {
                StorageUtils.setTextInStorage(               
                 Environment.getExternalStoragePublicDirectory(
                 Environment.DIRECTORY_DOCUMENTS),
                   this,
                   FILENAME,
                   FOLDERNAME,
                   this.editText.text.toString())
            } else {
                StorageUtils.setTextInStorage(
                 getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS),
                   this,
                   FILENAME,
                   FOLDERNAME,
                   this.editText.text.toString())
            }
        } else {
            Toast.makeText(this, getString(R.string.external_storage_impossible_create_file),
                          Toast.LENGTH_LONG).show()
        }
    }
}

Explanation: Here, we've called the two public methods of object  StorageUtils, which allows us to write or read a file from a root directory.

And this root directory will change depending on what the user chooses? 😎

Indeed! Let's walk through it, shall we? First, we declared some variables (1).  FILENAME is the name of the file (tripBook.txt) in which we wish to store text, and  FOLDERNAME is the name of the folder (bookTrip/) that will contain this file.

Then we created a method (2),    readFromStorage  ,  for reading the content of the file according to the user's choices (via the radio buttons). We did the same thing (3) in the method   writeOnExternalStorage, but this time for writing, of course...  

By the way, you may have noticed that these two methods call the methods created previously in the StorageUtils class (   getTextFromStorage  and    setTextInStorage  ), with of course different root directories

  • Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS): will enable us to generate a path (File) to the "Documents" directory of the external storage space, in public mode;

  • getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS): will enable us to generate a path (File) to the "Documents" directory of the external storage space, in private mode.

We have also created a save method (4), which will be called (5) when the user presses the "Save" button of the Toolbar. And finally, we call the method   readFromStorage when the activity is launched (6) or when the user clicks on the radio buttons (7) in order to update the file that is read.

But at this stage, if you launch the application, it will return error messages and the user will be unable to save any content. Can you figure out why? (Hint: the answer is in the next section 😉)

Ask permission to use external storage space

As you might have guessed from the title of this section, saving and reading a file on the external storage space of your user's phone requires special permissions. To avoid those error messages, you need to ask permission from the user! For this reason, we must first declare these permissions in our application.

To do this, we will declare the permissions in the manifest of our Android application (for versions equal to or lower than 5.1.1):

<manifest

   xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.openclassrooms.savemytrip">
   <!-- ENABLE PERMISSIONS ABOUT EXTERNAL STORAGE ACCESS -->
   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
   ...

</manifest>

Explanation: Declare the permissions needed to write and read the content into Android’s external storage using WRITE_EXTERNAL_STORAGE.

Okay, but why didn’t you also add the permission READ_EXTERNAL_STORAGE?

Simply because it is implicitly approved at the same time the WRITE_EXTERNAL_STORAGE permission is!  :) Now let's modify our activity to ask the user for permission.

Excerpt from TripBookActivity.kt:

import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE:

// 1 – PERMISSIONS MANAGEMENT

private const val RC_STORAGE_WRITE_PERMS = 100

class TripBookActivity : BaseActivity() {

...

   // 2 - After permission granted or refused
  override fun onRequestPermissionsResult(requestCode: Int,
                 permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this)
    } 
   ...
 
   // ----------------------------------
   // UTILS - STORAGE
   // ----------------------------------

   @AfterPermissionGranted(RC_STORAGE_WRITE_PERMS)

   private fun readFromStorage() {

    // CHECK PERMISSION
    if (!EasyPermissions.hasPermissions(this, WRITE_EXTERNAL_STORAGE)) {
        EasyPermissions.requestPermissions(this, getString(R.string.title_permission), RC_STORAGE_WRITE_PERMS, WRITE_EXTERNAL_STORAGE)
        return
    }
   ...
}

Explanation:  The  readFromStorage  function is called at the start of our activity. We therefore add the permission request at the beginning of this to ensure that we have the rights necessary for our activity to function properly.

The  checkWriteExternalStoragePermission  function checks if we need to ask the user for permission. It first checks if the permission has already been accepted: 

  • If it is the case, it will return the boolean false to continue the execution of the function   readFromStorage  . 

  • If not, it will ask the user to accept the permission and will not execute the rest of the   readFromStorage  function.

When the user accepts or denies the permission, the   onRequestPermissionsResult  function is called. So we can retrieve the user's response and run the   readFromStorage  function again if the permission is accepted.

Now launch your Android application. You should be able to save and read the contents of the file without any problems. Also, feel free to use your phone's file manager to view the files created.

Saving to external storageSaving to external storage

Let's recap!

  • The StorageUtils class contains all the necessary tools to easily write and read a file.

  • It is mandatory to ask the user's permission to use the external storage.

  • In public mode, the file is not deleted when the user uninstalls the application.

  • In private mode, the file is deleted when the user uninstalls the application.

Bravo! Now you know how to store data in the external storage of the smartphone. Let's go further and look at how to store a file in the internal storage this time!

Example of certificate of achievement
Example of certificate of achievement