Table of Contents

Lab 6. Storage

Objectives

Android storage model

Security of applications' data comes also from the sandboxing mechanism used by the Android system. Using it, not only the executed code is isolated from other apps, but also the data.

The application-specific storage is split into internal storage and external storage. The difference between them is in the access control. A common mistake is to assume that external storage refers to SD cards. It is a space that can include the phones' storage and also an SD card.

The storage accessible only to your app also includes:

For sharing data to external apps, you can use one of the main Android components: ContentProviders. For files, this component is extended by the FileProvider.

Android API Level 29 introduced important changes to how storage is accessed, driven by the need for increased privacy. It provided Scoped Storage (documentation, tutorial). Api Level 30 also introduced some updates related to storage, check this link for some information about them.

Internal Storage

How-to:

Code tutorial

External storage

Security tips:

  • validate the data read from external storage
  • encrypt sensitive data stored in external storage
  • decide whether the data should be persisted after the app is installed or not.
  • ask for the permissions at runtime, preferably showing a custom message informing the user why do you need those permissions.

How-to:

Android provides the MediaStore that provides your app's external files to the other apps. See the next code snippet in which we store an image.

 // Code working on versions < Q (Android10 aka API Level 29)
 
 String path = Environment.getExternalStorageDirectory().toString();
 File file = new File(path, "image.png");
 
 // ... some code that writes stuff to the image ...
 
 MediaStore.Images.Media.insertImage(
                    context.getContentResolver(),
                    file.getAbsolutePath(),
                    file.getName(),
                    file.getName());

From Android 10 you need to use ContentResolver like in these examples: Creating a new file, ContentResolver SO thread

Shared preferences

The Android framework offers an API for storing key-value pairs accessible only to your app. Usually they are used for persisting settings (user preferences).

The key-value pairs are stored in files. You can use one or several files, depending on your app's design. In order to access them, you need to obtain a SharedPreference object using methods provided by the Context class. These files can also be specific to an activity.

// In this example we are in MainActivity
 
SharedPreferences sharedPref = this.getPreferences(MODE_PRIVATE); 
// or 
sharedPref = this.getSharedPreferences("mysettings", Context.MODE_PRIVATE);
// or
sharedPref = PreferenceManager.getDefaultSharedPreferences(context); //!!! recently deprecated in API level 29
SharedPreferences.Editor editor = getPreferences(MODE_PRIVATE).edit();
editor.putString("my_key", "my_value");
editor.putBoolean("my_bool", true);
editor.putInt("my_int", 0);
 
// Must save them!
editor.apply();  //async write to disc
// or use
editor.commit(); //immediately writes to disc, avoid using it from UI thread.
 
//obtain values:
sharedPref.getString("my-key", "default-value");

For securing these preferences through encryption we can implement this mechanism using Android's KeyStore API or use a third-party library like Secure Preferences.

Tasks

Create an app that can download an image from a given location, store it and show it to the user.

Each task is 2p.

Task 0 - Init

Create a new Android application project. You can download the lab archive and use its layout, or start with your own UI design.

Task 1 - Runtime permission check

Add runtime permission checks when writing and reading the files from external storage (internal storage does not require permissions)

Why? These permissions are considered dangerous, and since Android 6 (API23) they need to be requested at runtime for the user to approve them. This restriction has been imposed because such permissions target sensitive user info, and malicious apps requested them from the user at install time, the users just accepted them without reading properly. In this way, if an app wants to access external storage, send sms, use camera etc, it needs to prompt the user to grant the permission. A list of such permissions can be found here.

Task 2 - Download an image

Implement a button listener which triggers an image download from a given URL and stores it on external storage.

Task 3 - Load an image

Implement a button listener which loads the image from external storage and displays it in an ImageView.

Task 4 - Internal storage

Provide the functionality from tasks 1 and 2 using internal storage instead of external.

Task 5 - Shared preferences

Use shared preferences for storing the preferred storage type (external/internal) for the image downloaded in the previous tasks.

Example

Example

    private String getStorageType() {
        SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
        return sharedPref.getString(getString(R.string.storage_type_key), "undefined");
    }
 
    private void storeStorageType(String type)
        SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit();
        editor.putString(getString(R.string.storage_type_key), type);
        editor.apply();
    }
 
    private class StorageTypeSelectedListener implements AdapterView.OnItemSelectedListener {
 
        @Override
        public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
            String selectedItem = adapterView.getItemAtPosition(i).toString();
            Log.d(TAG, "Selected storage type: " +  selectedItem);
            storeStorageType(selectedItem);
        }
 
        @Override
        public void onNothingSelected(AdapterView<?> adapterView) {
        }
    }
 

Image manipulation code snippets:

// to obtain a Bitmap object from an InputStream
BitmapFactory.decodeStream(input);
 
// to save a Bitmap object in an OutputStream
bitmap.compress(compressFormat, compressionRate, outputStream);
 
// to read a Bitmap from a file
Bitmap myBitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
 
// to resize a Bitmap
 
float scaleWidth = ((float) newWidth) / bitmap.width;
float scaleHeight = ((float) newHeight) / bitmap.height;
 
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
Bitmap bitmap2 = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, false);