Lab 6. Storage

Objectives

  • Learn about the Android data storage model
  • Store data in several ways: available to other apps or private to your app
  • Use shared preferences

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:

  • SharedPreferences - for persisting key-value pairs
  • Databases - Android provides an SqlLite API, but for an easier development we recommend using libraries such as Room or Realm.

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

  • Accessible only to the app
  • No permissions are needed in order to access it
  • Includes the file cache directory
    • storage space that should be used only for temporary files since the system can delete it without warning if it runs low on storage
  • Security tips:
    • for even better security agains attacks, use KeyStore for storing sensitive information (e.g. keys, pins etc) in an encrypted way. We will discuss it in the next labs.

How-to:

  • Obtain the path using one of the following methods provided by the Context class:
    • getFilesDir - returns the path to the internal directory of your app
    • getCacheDir - returns the directory for temporary cache files. You can also use File.createTempFile to directly create a File with a given name in this directory.
    • getDir - creates a directory with a given name or opens it if it already exists. It is inside the path provided by getFilesDir().
  • Use the language-specific API to write/read to/from files

Code tutorial

External storage

  • Globally accessible (any app can read or write)
  • You can access the files also using the command line
  • Two types, depending on the persistence after the app is uninstalled:
    • public files: will remain after the app is uninstalled (e.g. media recorded using the app)
    • private files: deleted when the app is uninstalled
  • Reading and writing to it requires the app to declare WRITE_EXTERNAL_STORAGE or READ_EXTERNAL_STORAGE permissions (declaring the write permission automatically grants the read permission as well). The next Android version, Q, deprecates these.
    • accessing public external files requires the permissions
    • accessing private external files does not require the permissions from API Level 18 (Android 4.4 - KitKat).

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:

  • Declare the permissions in the manifest (if needed)
  • Obtain the path using one of the following methods provided by the Environment class or Context classes:
  • Use the language-specific API to write/read to/from files

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
  • Context.getSharedPreferences is used when we have multiple shared preferences files and we want to obtain the given name
  • Context.getPreferences if we only use one shared preference file for that activity. We can only provide the MODE_PRIVATE since the other options have been deprecated from API Level 17 for security reasons. They granted read and write access to these files to other apps.
  • PreferenceManager.getDeaultSharedPreferences provides the default file for storing the key-value pairs for the entire app.
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.

  • You can resize the image using the code snippets provided at the end of this Section.

Task 4 - Internal storage

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

  • You can choose to store the file using one of the methods described in the Internal Storage section.
  • Log the path of the file, try to access it from the command line (adb shell).

Task 5 - Shared preferences

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

  • Add a Spinner to the UI (e.g. left of the Load button)
    • three options (declare a string-array in the strings.xml file): External, Internal, Undefined
  • When the app is created, load from shared preferences the storage type and show it in the spinner (use setSelection(int))
  • When the user selects an item from the spinner, store it as a shared preference. Use setOnItemSelectedListener.
  • When the user clicks Download, use an external or internal path, depending on the value stored in the shared preferences. If the value is “Undefined”, show a toast telling the user to choose a type of storage.

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);
smd/laboratoare/06.txt · Last modified: 2021/05/20 21:30 by adriana.draghici
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0