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();
}
});
}
}