• 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 6/12/19

Create a file on external storage

Log in or subscribe for free to enjoy all this course has to offer!

Let’s start developing SaveMyTrip. If you haven’t already done so, download and run this premade mini-application. 😀 First, we will configure the TripBookActivity activity which is responsible for managing the travel book feature.

Choose the right storage

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). We'll show the user the following questions and answer choices (minus the italics - those explanations are for you! 😉): 

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.

Save and retrieve data in a file

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 {
}

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)
}

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()
}
}

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. It first creates a StringBuilder to accumulate the contents of the file as it is read. Then, it verifies that the file exists before obtaining a BufferedReader from the File to read the data stream efficiently using its memory buffer. Within the useLines() block, the contents of the file are read line by line appending each line to the StringBuilder. Upon completion, the method returns a string that contains the entire contents of the file.

The method  writeFile() allows you to write the text into a file and creates any folder(s) needed in the path that don’t already exist (in our case, bookTrip/) using the method  mkdirs(). Next, it obtains a BufferedWriter to open a data stream to the file and writes the contents of the file to it in the  use()  block.

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
}

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()
}
}
}

 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.

At the end of our onCreate method, we called the method  readFromStorage, so that every time the activity is launched, (2) we get read the latest file contents.

All of the radio buttons have click listeners that call the  onClickRadioButton method, which now has a call to readFromStorage (3) as well.

In the onOptionsItemSelected method, (4) we respond when the user presses the Save button on the toolbar by calling the save method (5).

Next, we created a method (6),  readFromStorage, to read the contents of the file depending on what the user chooses (via the radio buttons). We did the same thing (7) in the method   writeExternalStorage, but this time by writing of course. 

Additionally, as you may have noticed, these are the same two methods that call the methods created previously in the class StorageUtils (getTextFromStorage and setTextInStorage), with of course, different root directories. First, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)  makes  it possible to generate a path (File) to the Documents directory of the external storage space in public mode. Next, getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) makes it possible to generate a path (File) to the Documents directory of the external storage space in private mode.

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.

dependencies {
...
//EASY PERMISSIONS
implementation 'pub.devrel:easypermissions:1.1.1'
}

The library EasyPermissions is installed, which will enable us to facilitate our request for permissions on Android versions 6 and above.

Next, we will declare the permissions in the manifest of our Android app (for versions at or below 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>

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 edit our activity to configure EasyPermissions.

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
}
...
}

We've configured  EasyPermissions  here so that it prompts the user to accept the WRITE_EXTERNAL_STORAGE permission as soon as the activity launches (and tries to read the file from the storage space). Now run the app. You should be able to save and read the contents of the file without any problem.

Saving to external storage
Saving to external storage

Take the time to read and reread this code to familiarize yourself with all the concepts it refers to. And don't forget the golden rule of any good developer: practice makes perfect! Good luck.😇

Let's recap!

We focused on saving files in external storage spaces.  Before moving on to the next chapter, make sure you remember the steps for:

  • Saving and retrieving data in a file (including setting up a path structure).

  • Updating the activity to manipulate the storage space.

  • Adding permissions for your user to approve. 

Once you feel confident about this, move on the tackling internal storage in the next chapter!

Example of certificate of achievement
Example of certificate of achievement