Stockage de données persistantes
-
Stockage de fichiers
- Sur le système de fichier principal de la mémoire flash interne
- Sur une carte SD ou un périphérique de stockage USB connecté
- En ligne sur une machine distante
-
Stockage de données structurées
- Dans une base de données SQLite3
-
Stockage de préférences (couples de clé-valeur)
- En utilisant l'interface SharedPreferences
Manipulation de fichiers
Fichiers internes
- Chaque application dispose d'un répertoire réservé pour stocker ses fichiers (noms de fichier en UTF-8) récupérable avec File Context.getFilesDir() (ce répertoire est détruit lors de la désinstallation de l'application)
- Le système de fichiers interne peut être chiffré à l'aide du mot de passe de déverrouillage
-
Opérations (chemins relatifs au répertoire de l'application) sur instance de Context :
- FileInputStream openFileInput(String name)
- FileOutputStream openFileOutput(String name, int mode)
- File getDir(String name, int mode) : ouverture (création si nécessaire) d'un répertoire
- File deleteFile(String name)
- String[] fileList() : liste des fichiers privés sauvés par l'application
-
Modes de création de fichier et répertoire (combinables par ou binaire) :
- MODE_PRIVATE : accessibilité réservée à l'application (ou d'autres applications avec le même user ID)
- MODE_APPEND : ajout en fin de fichier (par défaut écrasement du fichier)
- MODE_WORLD_{READABLE, WRITABLE} : accessibilité en lecture, écriture pour toutes les applications. À bannir : si des données doivent être lisibles ou écrites depuis d'autres applications, elles doivent l'être depuis une API publique
Fichiers externes
- Les fichiers sur support externes sont toujours publics et possiblement non chiffrés
-
Obtention de répertoires racines externes :
- File Context.getExternalFilesDir(String type) : répertoire racine réservé à l'application (détruit à la désinstallation), par exemple /sdcard/Android/data/fr.upemlv.HelloWorld/files/ ; type peut être null
- File Environment.getExternalStorageDirectory() : répertoire racine externe global
- File Environment.getExternalStoragePublicDirectory(String type) : répertoire racine externe global pour un type de fichier spécifié
-
Les fichiers de mêmes types doivent être regroupés dans des sous-répertoires :
- DIRECTORY_MUSIC
- DIRECTORY_PODCASTS
- DIRECTORY_RINGTONES
- DIRECTORY_ALARMS
- DIRECTORY_NOTIFICATIONS
- DIRECTORY_PICTURES
- DIRECTORY_MOVIES
- DIRECTORY_DOWNLOADS
- DIRECTORY_DCIM
-
Permissions nécessaires pour lire/écrire sur un support externe
- READ_EXTERNAL_STORAGE et WRITE_EXTERNAL_STORAGE pour Android ≤ 10
- Pas de permissions nécessaires pour Android ≥ 10 à condition d'utiliser le scoped storage (confinement des fichiers pour chaque application)
Répertoires cache
-
Obtention des chemins vers les répertoires cache spécifiques à l'application courante :
- File getCacheDir()
- File getExternalCacheDir() (retourne null si le stockage externe n'est pas disponible)
- Utile pour y stocker des données temporaires (issues de calculs, de récupération de données sur Internet...)
-
Les données stockées peuvent être effacées par le système :
- En cas de désinstallation de l'application
- En cas de pénurie de mémoire de stockage
- Nécessité pour chaque application d'être raisonnable pour l'espace utilisé par le cache (pas de quota, partage par toutes les applications)
Storage Access Framework (SAF)
- SAF : exposition de systèmes de fichiers cherchables fournis par des DocumentsProvider
- Pas de permssion préalable spécifique pour l'utilisation de SAF : octroi explicite de la permission fichier par fichier par sélection de l'utilisateur
-
Utilisation d'Intent pour accéder aux fichiers (avec startActivityForResult pour obtenir une URI)
- ACTION_GET_CONTENT pour lire un fichier
-
ACTION_OPEN_DOCUMENT pour avoir un accès persistant à un fichier en lecture/écriture/effacement (jusqu'au prochain redémarrage)
- Possibilité d'avoir une permission permanente avec getContentResolver().takePersistableUriPermission(uri, takeFlags)
- ACTION_CREATE_DOCUMENT pour créer un nouveau document en indiquant son type MIME et un nom de fichier
- Métadonnées consultables sur les fichiers sélectionnés avec ``Cursor cursor = getActivity().getContentResolver()
- .query(uri, null, null, null, null, null);``
- Document supprimable (si droit de suppression) avec DocumentsContract.deleteDocument(getContentResolver(), uri)
- Concept de fichiers virtuels depuis Android 7 non éditables mais prévisualiables en utilisant certains types MIME
- Possibilité d'écrire son propre DocumentsProvider montrant une base de fichiers locales ou stockée à distance
Exemple : une activité qui propose de choisir un fichier parmi tous les fichiers d'un type MIME donné (e.g. image/jpeg) et qui envoie l'URI de ce fichier vers une autre activité
- Utilisation de SAF pour démarrer l'activité de choix de fichier pour un type MIME indiqué en cochant un bouton radio
- Récupération de toutes les activités installées supportant ACTION_SEND
- Affichage de ces activités sous la forme de boutons radio
- Envoi vers l'activité cochée de l'URI du fichier
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.filechooser import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Bundle import android.util.Log import android.widget.RadioButton import androidx.appcompat.app.AppCompatActivity import fr.upem.coursand.R import kotlinx.android.synthetic.main.activity_file_chooser.* class FileChooserActivity : AppCompatActivity() { /** Return the MIME type for the selected radio button */ private val selectedFileType: String get() { return when (fileTypeRadioGroup.checkedRadioButtonId) { R.id.imageRadioButton -> "image/*" R.id.videoRadioButton -> "video/*" R.id.AudioRadioButton -> "audio/*" R.id.textRadioButton -> "text/*" else -> "*/*" } } private val OPEN_REQUEST_CODE = 42 private var selectedUri: Uri? = null private var activityMap: Map<String, Intent>? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_file_chooser) // When we click on the button we open activity to choose a file matching the type selected with the radio button // using the Storage Access Framework chooserButton.setOnClickListener { Intent(Intent.ACTION_OPEN_DOCUMENT) .addCategory(Intent.CATEGORY_OPENABLE) .setType(selectedFileType) .apply { startActivityForResult(this, OPEN_REQUEST_CODE) } } // we must write the onActivityResult callback } /** This methid is called back when we have the result of the file chosen with the SAF */ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == OPEN_REQUEST_CODE && data != null && resultCode == Activity.RESULT_OK) { selectedUri = data.data chosenFileView.text = "${data.data}" // build the share Intent val shareIntent = Intent(Intent.ACTION_SEND) .setType(contentResolver.getType(selectedUri!!)) .putExtra(Intent.EXTRA_STREAM, selectedUri) // get all the intents for all the activities that can send this file activityMap = shareIntent.resolveIntent(this) Log.i(javaClass.name, "activity map $activityMap") if (selectedUri != null) { activityRadioGroup.removeAllViews() // remove all the radio buttons activityMap?.map { val rb = RadioButton(this); rb.text = it.key; rb.tag = it.key; rb } ?.forEachIndexed { i, b -> b.id = i; activityRadioGroup.addView(b) } // configure the button to start the selectedActivity sendToActivityButton.apply { isEnabled = true setOnClickListener { // find the selected radio button val rb = activityRadioGroup.findViewById<RadioButton>(activityRadioGroup.checkedRadioButtonId) // start the activity for the selected intent activityMap?.get(rb.tag?: "")?.also { startActivity(it) } } } } } } }
Media Store
-
Media Store : base de données de fichiers multimédia (image, audio, vidéo)
- Fichiers partagés entre toutes les applications
- Fichiers conservés lors de la suppression de l'application créatrice
-
Accès en lecture avec récupération de métadonnees et du contenu des fichiers
- Nécessite la permission READ_EXTERNAL_STORAGE pour accéder à des fichiers média créés par d'autres applications
- Nécessite la permission ACCESS_MEDIA_LOCATION pour Android ≥ 10 pour accéder aux métadonnées de géolocalisation
-
Accès en écriture
- Nécessite la permission WRITE_EXTERNAL_STORAGE
Exemple : une fonction en Kotlin créeant un enregistrement dans le MediaStore et retournant l'URI du fichier créé
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.screenrecorder import android.content.ContentValues import android.content.Context import android.net.Uri import android.provider.MediaStore import java.io.File enum class MediaType { IMAGE, AUDIO, VIDEO } fun Context.saveInMediaStore(type: MediaType, mimeType: String, name: String, file: File, additionalMetadata: Map<String, String> = emptyMap()): Uri? { val contentValues = ContentValues() contentValues.put(MediaStore.MediaColumns.TITLE, name) contentValues.put(MediaStore.MediaColumns.DATE_ADDED, (System.currentTimeMillis() / 1000).toInt()) contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType) contentValues.put(MediaStore.MediaColumns.DATA, file.absolutePath) additionalMetadata.forEach { contentValues.put(it.key, it.value) } val resolver = contentResolver val contentUri = when (type) { MediaType.IMAGE -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI MediaType.AUDIO -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI MediaType.VIDEO -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI } return resolver.insert(contentUri, contentValues) } fun Context.openUri(uri: Uri) = contentResolver.openOutputStream(uri)
Sauvegarde des fichiers d'application
-
Propriétés de l'application
- android:allowBackup
- android:backupAgent : classe de backup
-
Implantation d'une classe dérivée de BackupAgent :
- void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) : réalise une sauvegarde incrémentale depuis oldState vers newState en écrivant des données binaires dans data
- void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) : restaure une sauvegarde incrémentale ; appVersionCode est la version de l'application ayant réalisée le backup
- Il existe des BackupAgentHelper pour aider à la sauvegarde/restauration de données courantes (fichiers, préférences...)
- Attention à ne pas modifier des données concurremment à leur sauvegarde (utiliser un verrou)
- Lorsque des données utilisateur sont modifiées, l'application peut demander une sauvegarde incrémentale avec BackupManager.dataChanged()
- L'implantation du transport de backup dépend de la distribution Android (par exemple un système de backup utilisant le nuage de Google)
Préférences d'application
-
API de préférences pour stocker des données persistantes au redémarrage sous la forme d'entrées clé/valeur
- Utile pour des valeurs de petite taille
- Fichiers plus adaptés pour des valeurs volumineuses
-
Obtention des préférences de l'application : SharedPreferences Context.getSharedPreferences(String name, int mode)
- PreferenceManager.getDefaultSharedPreferences() permet de récupérer simplement les préférences par défaut de l'application
- Context.getPreferences(int mode) récupère les préférences du nom de l'activité courante (ou service)
- Plusieurs applications peuvent accéder aux mêmes préférences si mode = MODE_WORLD_READABLE ou MODE_WORLD_WRITABLE
- Récupération d'une valeur avec get{Boolean, Float, Int, Long, String}(String key, X defaultValue)
-
Modification transactionnelle d'entrées :
- en obtenant l'éditeur (edit()) sur lequel on réalise des opérations putX(String key, X value)
- en validant atomiquement les changements avec commit()
- Possibilité d'ajouter un listener appelé lors de la modification d'une entrée : registerOnSharedPreferenceChangeListener()
- Mise en place facilitée d'une activité d'édition de préférences avec PreferenceActivity couplée avec un fichier ressource XML décrivant hiérarchiquement l'écran de préférence (PreferenceScreen avec PreferenceCategory, ListPreference, EditTextPreference...)
Exemple : utilisation des préférences pour mémoriser des informations d'authentification (nom d'utilisateur et mot de passe)
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.login; import android.content.Intent; import android.content.SharedPreferences; import android.preference.PreferenceManager; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.widget.CheckBox; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import fr.upem.coursand.R; /** An activity that asks the user to supply her login information. * The username and password are then sent back to the calling activity. * It illustrates bidirectional communication between activities. */ public class LoginActivity extends AppCompatActivity { private SharedPreferences prefs; private String service; // the service for which we login private EditText usernameView; private EditText passwordView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); usernameView = findViewById(R.id.usernameView); passwordView = findViewById(R.id.passwordView); // read the calling intent service = getIntent().getStringExtra("service"); TextView serviceView = findViewById(R.id.serviceView); serviceView.setText(service); // display the service name // maybe we have already saved into the preferences of the activity the login info for this service // in this case we prefill the input fields with the stored data prefs = PreferenceManager.getDefaultSharedPreferences(this); String username0 = prefs.getString(service + ".username", null); String password0 = prefs.getString(service + ".password", null); if (username0 != null) usernameView.setText(username0); if (password0 != null) passwordView.setText(password0); // set the login info findViewById(R.id.loginButton).setOnClickListener( v -> { String username = usernameView.getText().toString(); String password = passwordView.getText().toString(); if (username.isEmpty() || password.isEmpty()) { Toast.makeText(this, "Missing information", Toast.LENGTH_LONG).show(); } else { CheckBox rememberCheckBox = findViewById(R.id.rememberCheckBox); SharedPreferences.Editor editor = prefs.edit(); // open a transaction for the preferences if (rememberCheckBox.isChecked()) { // we store the login info into the prefs editor.putString(service + ".username", username); editor.putString(service + ".password", password); editor.putLong(service + ".timestamp", System.currentTimeMillis()); } else { // we must forget the information already (or not) stored editor.remove(service + ".username"); editor.remove(service + ".password"); editor.remove(service + ".timestamp"); } // finally we commit the prefs transaction editor.commit(); // create a result intent and send it back to the calling activity Intent resultIntent = new Intent(); resultIntent.putExtra("service", service); resultIntent.putExtra("username", username); resultIntent.putExtra("password", password); setResult(RESULT_OK, resultIntent); // the job is done, we quit the activity finish(); } }); } }