File Storage

Your photo needs more than a place on the screen. Full-size pictures are too large to stick inside a SQLite database, much less an Intent. They will need a place to live on your device’s filesystem.

Luckily, you have a place to stash these files: your private storage. Recall that your database is saved to your app’s private storage. With functions like Context.getFileStreamPath(String) and Context.getFilesDir(), you can do the same thing with regular files, too (which will live in a subfolder adjacent to the databases subfolder your database lives in).

These are the basic file and directory functions in the Context class:

getFilesDir(): File

returns a handle to the directory for private application files

openFileInput(name: String): FileInputStream

opens an existing file in the files directory for input

openFileOutput(name: String, mode: Int): FileOutputStream

opens a file in the files directory for output, possibly creating it

getDir(name: String, mode: Int): File

gets (and possibly creates) a subdirectory within the files directory

fileList(…): Array<String>

gets a list of filenames in the main files directory, such as for use with openFileInput(String)

getCacheDir(): File

returns a handle to a directory you can use specifically for storing cache files; you should take care to keep this directory tidy and use as little space as possible

There is a catch. Because these files are private, only your own application can read or write to them. As long as no other app needs to access those files, these functions are sufficient.

However, they are not sufficient if another application needs to write to your files. This is the case for CriminalIntent, because the external camera app will need to save the picture it takes as a file in your app.

In those cases, the functions above do not go far enough: While there is a Context.MODE_WORLD_READABLE flag you can pass into openFileOutput(String, Int), it is deprecated and not completely reliable in its effects on newer devices. Once upon a time you could also transfer files using publicly accessible external storage, but this has been locked down in recent versions of Android for security reasons.

If you need to share files with or receive files from other apps, you need to expose those files through a ContentProvider. A ContentProvider allows you to expose content URIs to other apps. They can then download from or write to those content URIs. Either way, you are in control and always have the option to deny those reads or writes if you so choose.

Using FileProvider

When all you need to do is receive a file from another application, implementing an entire ContentProvider is overkill. Fortunately, Google has provided a convenience class called FileProvider that takes care of everything except the configuration work.

The first step is to declare FileProvider as a ContentProvider hooked up to a specific authority. Do this by adding a content provider declaration to your Android manifest.

Listing 16.4  Adding a FileProvider declaration (manifests/AndroidManifest.xml)

<activity android:name=".MainActivity">
    ...
</activity>
<provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="com.bignerdranch.android.criminalintent.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
</provider>

The authority is a location – a place that files will be saved to. The string you choose for android:authorities must be unique across the entire system. To help ensure this, the convention is to prepend the authority string with your package name. (We show the package name com.bignerdranch.android.criminalintent above. If your app’s package name is different, use your package name instead.)

By hooking up FileProvider to your authority, you give other apps a target for their requests. By adding the exported="false" attribute, you keep anyone from using your provider except you or anyone you grant permission to. And by adding the grantUriPermissions attribute, you add the ability to grant other apps permission to write to URIs on this authority when you send them out in an intent. (Keep an eye out for this later.)

Now that you have told Android where your FileProvider is, you also need to tell your FileProvider which files it is exposing. This bit of configuration is done with an extra XML resource file. Right-click your app/res folder in the project tool window and select NewAndroid resource file. Enter files for the name, and for Resource type select XML. Click OK and Android Studio will add and open the new resource file.

In the text view of your new res/xml/files.xml, add details about the file path (Listing 16.5).

Listing 16.5  Filling out the paths description (res/xml/files.xml)

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">

</PreferenceScreen>
<paths>
    <files-path name="crime_photos" path="."/>
</paths>

This XML file says, Map the root path of my private storage as crime_photos. You will not use the crime_photos name – FileProvider uses that internally.

Now, hook up files.xml to your FileProvider by adding a meta-data tag in your AndroidManifest.xml.

Listing 16.6  Hooking up the paths description (manifests/AndroidManifest.xml)

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

Designating a picture location

Time to give your pictures a place to live on disk locally. First, add a computed property to Crime to get a well-known filename.

Listing 16.7  Adding the filename property (Crime.kt)

@Entity
data class Crime(@PrimaryKey val id: UUID = UUID.randomUUID(),
                 var title: String = "",
                 var date: Date = Date(),
                 var isSolved: Boolean = false,
                 var suspect: String = "") {

    val photoFileName
        get() = "IMG_$id.jpg"
}

photoFileName does not include the path to the folder the photo will be stored in. However, the filename will be unique, since it is based on the Crime’s ID.

Next, find where the photos should live. CrimeRepository is responsible for everything related to persisting data in CriminalIntent, so it is a natural owner for this idea. Add a getPhotoFile(Crime) function to CrimeRepository that provides a complete local file path for Crime’s image.

Listing 16.8  Finding the photo file location (CrimeRepository.kt)

class CrimeRepository private constructor(context: Context) {
    ...
    private val executor = Executors.newSingleThreadExecutor()
    private val filesDir = context.applicationContext.filesDir

    fun addCrime(crime: Crime) {
        ...
    }

    fun getPhotoFile(crime: Crime): File = File(filesDir, crime.photoFileName)
    ...
}

This code does not create any files on the filesystem. It only returns File objects that point to the right locations. Later on, you will use FileProvider to expose these paths as URIs.

Finally, add a function to CrimeDetailViewModel to expose the file information to CrimeFragment.

Listing 16.9  Exposing a file through CrimeDetailViewModel (CrimeDetailViewModel.kt)

class CrimeDetailViewModel : ViewModel() {
    ...
    fun saveCrime(crime: Crime) {
        crimeRepository.updateCrime(crime)
    }

    fun getPhotoFile(crime: Crime): File {
        return crimeRepository.getPhotoFile(crime)
    }
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.38.125