Chapters

Hide chapters

Android Fundamentals by Tutorials

First Edition · Android 14 · Kotlin 1.9 · Android Studio Hedgehog (2023.1.1)

11. Advanced Storage
Written by Kevin D Moore

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Sometimes, an app needs to store information to the files on a device. If these files are needed just by the app and are not to be used by the user, you can store them in the app’s files and cache directories. Only your app can access these directories, which are relatively secure (a hacker could get access, but not the average user). As the name implies, cache directory files are for cached items, and there’s limited storage for them. The Context object (which an Activity implements) can let you access them through the following calls (from any context):

Files and Directories

Context.cacheDir
Context.filesDir

These File classes let you list, create and delete the directory files. Here’s an example of how to write a file to the directory:

val file = File(context.filesDir, "test.txt")
file.bufferedWriter().use { out ->  out.write("This is a test") }

This creates a file in the context.filesDir directory. If you were to put this in MainActivity and run your app, this file would be generated in the files directory.

Device Explorer

Open the Device Explorer by going to View ▸ Tool Windows ▸ Device Explorer. You’ll find all the device’s app folders in the data/data folder:

lifecycleScope.launch {

  val storageManager = getSystemService(STORAGE_SERVICE) as StorageManager
  Timber.e(
    "Cache Quota: ${
      storageManager.getCacheQuotaBytes(
        storageManager.getUuidForPath(
          context.cacheDir
        )
      )
    }"
  )
}

Cache Files

To create a cache file, you simply call the createTempFile() method on the File class like:

val tempFile = File.createTempFile(fileName, null, context.cacheDir)

External Files

In Android, there are a few well-defined directory names, such as:

val documentFile = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)
val textFile = File(documentFile, "test.txt")
textFile.bufferedWriter().use { out ->  out.write("This is a test") }

Storage Access Framework

The Storage Access Framework lets you use a system picker to have your user pick files for you to open, create or modify. This way, you don’t have to go through the permission system that forces the user to decide whether to grant your app permission to write to the requested directories.

Creating Files

To create a file, use the ACTION_CREATE_DOCUMENT intent. This is an example function you can run to create a text file:

// Request code for creating a Text document.
const val CREATE_FILE = 1

fun createFile(activity: Activity) {
  val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
    addCategory(Intent.CATEGORY_OPENABLE)
    type = "text/plain"
    putExtra(Intent.EXTRA_TITLE, "test.txt")
  }
  activity.startActivityForResult(intent, CREATE_FILE)
}
val contentResolver = applicationContext.contentResolver

val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
    Intent.FLAG_GRANT_WRITE_URI_PERMISSION

contentResolver.takePersistableUriPermission(uri, takeFlags)

Database Explorer

In the previous chapter, you created a SQLite database using Room. Android Studio has a Database Explorer that allows you to view your data easily. To reach the explorer, use the menu View ▸ Tool Windows ▸ App Inspection. This will show a view like:

Security

So far, you’ve learned how to store data in a SQLite database and a Data Store. Remember, Data Store is a newer API for Android’s Shared Preferences. To create a secure Shared Preference file, you must use the Encrypted Preferences package that’s part of the Android Security library. This library hasn’t been converted to the newer Data Store format yet, so you’ll learn how to use it in its Shared Preferences format.

Android Keystore

The Android Keystore system is a container for cryptographic keys and makes it very difficult to extract. A Trusted Execution Environment (TEE) is an area separate from the main operating system. The data is even safer if the phone has secure hardware (Secure Element (SE)) with its own CPU and storage. To check for this feature, use KeyInfo.isInsideSecurityHardware() on API level 28 or lower, or KeyInfo.getSecurityLevel() on API level 29 or higher. These secure hardware components contain the following:

val keystore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply {
  load(null)
}

Encrypted Preferences

The Encrypted Preferences class is actually in the security-crypto library. This library contains both EncryptedSharedPreferences and EncryptedFile (used to create encrypted files). There’s also a convenient MasterKeys class with methods for creating and obtaining master keys from the Android Keystore.

Adding the Security Library

If you’re following along with your app from the previous chapters, open it and keep using it with this chapter. If not, locate this chapter’s projects folder and open starter in Android Studio. Open gradle/libs.versions.toml. At the end of the versions section, add:

security = "1.0.0"
# Security
security = { module = "androidx.security:security-crypto", version.ref = "security" }
implementation(libs.security)

Secure Preferences

You’ll use the Encrypted Preferences class to save data in an encrypted format.

import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys

class SecurePrefs(context: Context) {
  private val prefs: SharedPreferences
  // TODO: Add init
  // TODO: Add get/save methods
}
init {
  // 1
  val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)

  // 2
  prefs = EncryptedSharedPreferences.create(
    // 3
    "encrypted_preferences", // fileName
    masterKeyAlias, // masterKeyAlias
    context, // context
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, // prefKeyEncryptionScheme
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM // prefvalueEncryptionScheme
  )
}
fun saveString(key: String, value: String) {
  prefs.edit().putString(key, value).apply()
}

fun getString(key: String): String? {
  return prefs.getString(key, null)
}

fun saveInt(key: String, value: Int) {
  prefs.edit().putInt(key, value).apply()
}

fun getInt(key: String): Int {
  return prefs.getInt(key, 0)
}

fun hasKey(key: String): Boolean {
  return prefs.contains(key)
}
val LocalPrefsProvider =
  compositionLocalOf<Prefs> { error("No prefs provided") }
val LocalPrefsProvider =
  compositionLocalOf<SecurePrefs> { error("No prefs provided") }
lateinit var prefs: Prefs
lateinit var securePrefs: SecurePrefs
prefs = Prefs(this)
prefs = SecurePrefs(this)

Encrypted Room

Now that you’ve encrypted your preferences, it’s time to encrypt your database. Room doesn’t have an encryption library, but underneath it is just a SQLite database. You can encrypt this using the SQLCipher library, which wraps itself around SQLite to encrypt the data.

Adding the SQLCipher Library

Open gradle/libs.versions.toml. At the end of the versions section, add:

sqlcipher = "4.4.0"
# Secure Room
sqlcipher = { module = "net.zetetic:android-database-sqlcipher", version.ref = "sqlcipher" }
implementation(libs.sqlcipher)
import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SupportFactory
import kotlin.random.Random
const val PASSCODE_KEY = "PASSCODE_KEY"
fun getPassCode(stringSize: Int): String {
  val randomString = StringBuilder()
  val random = Random.Default
  for (i in 0 until stringSize) {
    randomString.append('a' + random.nextInt(26))
  }
  return randomString.toString()
}
fun getInstance(context: Context, passCode: CharArray): RecipeDatabase {
val supportFactory = SupportFactory(SQLiteDatabase.getBytes(passCode))
instance = Room.databaseBuilder(
  context.applicationContext,
  RecipeDatabase::class.java,
  "recipe_database"
)
  .openHelperFactory(supportFactory)
  .fallbackToDestructiveMigration()
  .build()
repository = RecipeRepository(
  Room.databaseBuilder(
    this,
    RecipeDatabase::class.java,
    "Recipes"
  ).build()
)
val randomPassCode: String
// 1
if (!securePrefs.hasKey(PASSCODE_KEY)) {
  // 2
  randomPassCode = RecipeDatabase.getPassCode(15)
  // 3
  securePrefs.saveString(PASSCODE_KEY, randomPassCode)
} else {
  // 4
  randomPassCode = securePrefs.getString(PASSCODE_KEY)!!
}
// 5
repository = RecipeRepository(
  RecipeDatabase.getInstance(this, randomPassCode.toCharArray())
)

Key Points

  • Encryption is key to securely storing your data.
  • You can encrypt both preferences and databases.
  • The security library provides access to the Encrypted Preferences class.
  • SQLCipher provides encryption for SQLite databases.

Where to Go From Here?

In this chapter, you learned how to store encrypted data in preferences and a database.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now