Débutons le développement de notre mini-application SaveMyTrip. Si vous ne l'avez pas déjà fait, téléchargez et exécutez cette mini-application que j'ai pré-développée pour vous. :)
Découvrez le programme de cette première partie
Dans cette partie, nous allons configurer l'activité TripBookActivity qui s'occupe de gérer la fonctionnalité "Carnet de voyage".
Le but de celle-ci est de permettre à l'utilisateur d'écrire du texte dans un fichier qui sera mémorisé dans l'espace de stockage externe ou interne de son téléphone, selon le choix qu'il fera via les boutons radios (appelés aussi "cases d'options") :
Dans quel stockage souhaitez-vous écrire ?
Externe
Quel niveau de confidentialité souhaitez-vous ?
Public : Le fichier sera stocké sur l'espace de stockage externe en mode public. Il ne sera donc pas supprimé quand l'utilisateur désinstallera son application.
Privé : Le fichier sera stocké sur l'espace de stockage externe en mode privé. Il sera donc supprimé quand l'utilisateur désinstallera son application.
Interne
Souhaitez-vous stocker le fichier en cache ?
Oui : Le fichier sera stocké sur l'espace de stockage interne dans le répertoire dédié au cache. Il pourra donc être supprimé à n'importe quel moment.
Non : Le fichier sera stocké sur l'espace de stockage interne.
Nous commencerons dans ce chapitre par gérer la sauvegarde de ce fichier sur l'espace de stockage externe : le fichier pourra donc être créé soit en mode privé, soit en mode public.
Ce fichier s'appellera tripBook.txt et sera placé dans un dossier appelé bookTrip/. Nous créerons donc un fichier (ainsi que son dossier) dans chacun des espaces de stockage disponibles, en fonction du choix de notre utilisateur. :)
Créez une classe utilitaire
Avant toute chose, nous allons créer une classe utilitaire que nous appellerons StorageUtils.java et que nous placerons dans le package utils/ de notre application.
Classe utils/StorageUtils.java :
public class StorageUtils {
}
Explications : Cette classe sera responsable de la sauvegarde et de la récupération des données inscrites au sein de notre fichier tripBook.txt. Cela évitera de trop surcharger notre activité TripBookActivity et surtout nous permettra de réutiliser son code dans d'autres activités, si besoin.
Créez un chemin d'accès vers un fichier
Afin d'enregistrer le texte écrit par notre utilisateur, il faut dans un premier temps définir un fichier de destination. Créons ainsi la méthode createOrGetFile()
dans notre classe StorageUtils.java.
Classe utils/StorageUtils.java :
public class StorageUtils {
private static File createOrGetFile(File destination, String fileName, String folderName){
File folder = new File(destination, folderName);
return new File(folder, fileName);
}
}
Explications : Cette méthode sera appelée pour créer ou récupérer un fichier.
La classe File est un peu trompeuse, car on pourrait croire qu'il s'agit d'une représentation d'un fichier, mais ce n'est pas uniquement le cas ! En fait, elle représente plutôt un chemin d'accès vers un fichier, qui sera utilisé par la suite pour y enregistrer ou y récupérer un flux de données (caractères, octets, etc.).
Dans notre cas, nous allons définir un chemin d'accès vers un dossier contenant un fichier (en l'occurrence bookTrip/tripBook.txt), à partir d'une destination racine indiquée en paramètre de la méthode. Ne vous inquiétez pas, vous allez mieux comprendre par la suite... :)
Écrivez et lisez des données depuis un fichier
Maintenant que nous avons vu comment créer et récupérer un fichier à partir d'un chemin d'accès, il serait bien de pouvoir lire et écrire à l'intérieur. Pour cela, rajoutez les lignes suivantes :
Extrait de StorageUtils.java :
public class StorageUtils {
// ----------------------------------
// READ & WRITE ON STORAGE
// ----------------------------------
private static String readOnFile(Context context, File file){
String result = null;
if (file.exists()) {
BufferedReader br;
try {
br = new BufferedReader(new FileReader(file));
try {
StringBuilder sb = new StringBuilder();
String line = br.readLine();
while (line != null) {
sb.append(line);
sb.append("\n");
line = br.readLine();
}
result = sb.toString();
}
finally {
br.close();
}
}
catch (IOException e) {
Toast.makeText(context, context.getString(R.string.error_happened), Toast.LENGTH_LONG).show();
}
}
return result;
}
// ---
private static void writeOnFile(Context context, String text, File file){
try {
file.getParentFile().mkdirs();
FileOutputStream fos = new FileOutputStream(file);
Writer w = new BufferedWriter(new OutputStreamWriter(fos));
try {
w.write(text);
w.flush();
fos.getFD().sync();
} finally {
w.close();
Toast.makeText(context, context.getString(R.string.saved), Toast.LENGTH_LONG).show();
}
} catch (IOException e) {
Toast.makeText(context, context.getString(R.string.error_happened), Toast.LENGTH_LONG).show();
}
}
}
Explications : Nous avons ajouté ici deux méthodes généralistes :readOnFile()
et writeOnFile()
.
La méthode readOnFile()
va nous permettre de lire le contenu d'un fichier passé en paramètre. Cette dernière vérifie que celui-ci existe, puis va utiliser la classe BufferedReader permettant de lire, grâce à sa mémoire tampon, un flux de données de manière efficiente. Ce flux de données sera généré par la classe FileReader à partir du fichier défini en paramètre, puis lu ligne par ligne pour, au final, renvoyer une variable String contenant l'ensemble du texte du fichier.
La méthode writeOnFile()
quant à elle, va nous permettre d'écrire du texte dans un fichier. Cette dernière crée, si ce n'est pas déjà le cas, le(s) dossier(s) nécessaire(s) au chemin d'accès (dans notre cas, bookTrip/ ) grâce à la méthodemkdirs()
. Puis, elle ouvre un flux de données vers notre fichier grâce à la classe FileOutputStream. Une fois ouvert, nous allons pouvoir, grâce aux classes BufferedWriter et OutputStreamWriter, y ajouter notre texte de manière efficiente.
Manipulez les espaces de stockage
Maintenant que nos méthodes d'écriture et de lecture généralistes sont prêtes, nous allons pouvoir manipuler notre espace de stockage externe et interne avec. Pour cela, ajoutez les méthodes suivantes à votre classe StorageUtils.java.
Extrait de StorageUtils.java :
public class StorageUtils {
public static String getTextFromStorage(File rootDestination, Context context, String fileName, String folderName){
File file = createOrGetFile(rootDestination, fileName, folderName);
return readOnFile(context, file);
}
public static void setTextInStorage(File rootDestination, Context context, String fileName, String folderName, String text){
File file = createOrGetFile(rootDestination, fileName, folderName);
writeOnFile(context, text, file);
}
// ----------------------------------
// EXTERNAL STORAGE
// ----------------------------------
public static boolean isExternalStorageWritable() {
String state = Environment.getExternalStorageState();
return (Environment.MEDIA_MOUNTED.equals(state));
}
public static boolean isExternalStorageReadable() {
String state = Environment.getExternalStorageState();
return (Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state));
}
}
Explications : Nous avons ajouté ici plusieurs méthodes. Les deux dernières permettent de vérifier si l'espace de stockage externe est bien disponible et si l'on peut lire ( isExternalStorageReadable
) ou écrire ( isExternalStorageWritable
) dessus.
Puis nous avons créé deux autres méthodes permettant d'écrire et de lire du texte dans un fichier se trouvant dans un espace de stockage défini en paramètre... en réutilisant les méthodes writeOnFile
et createOrGetFile
que nous avons créées précédemment !
Hein, mais je ne comprends pas, cela veut dire que l'on va écrire à chaque fois dans le même fichier au même endroit, non ?
Eh non ! Regardez de plus près ces méthodes. Nous passons un répertoire RACINE de destination (le paramètre rootDestination ) à la méthode createOrGetFile
, ce qui nous permet de créer ou récupérer un fichier (tripBook.txt) et son dossier (bookTrip/) à partir de ce répertoire racine !
Maintenant que tout cela est prêt, appelons toutes ces méthodes dans notre contrôleur, TripBookActivity.
Mettez à jour l'activité
Modifions maintenant notre activité TripBookActivity afin d'appeler ces deux dernières méthodes lors d'une action d'un utilisateur.
Extrait de TripBookActivity.java :
public class TripBookActivity extends AppCompatActivity {
// 1 - FILE PURPOSE
private static final String FILENAME = "tripBook.txt";
private static final String FOLDERNAME = "bookTrip";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 6 - Read from storage when starting
readFromStorage();
}
// -------------------
// UI
// -------------------
private void initView() {
CompoundButton.OnCheckedChangeListener checkedChangeListener = (button, isChecked) -> {
if (isChecked) {
}
readFromStorage();
};
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.action_share) {
return true;
} else if (itemId == R.id.action_save) {
save();
return true;
}
return super.onOptionsItemSelected(item);
}
}
// --------------------
// ACTIONS
// --------------------
// 4 - Save after user clicked on button
private void save() {
if (binding.tripBookActivityRadioExternal.isChecked()) {
this.writeOnExternalStorage(); //Save on external storage
} else {
// TODO : Save on internal storage
}
}
// ----------------------------------
// UTILS - STORAGE
// ----------------------------------
// 2 - Read from storage
private void readFromStorage() {
if (binding.tripBookActivityRadioExternal.isChecked()) {
if (StorageUtils.isExternalStorageReadable()) {
File directory;
// EXTERNAL
if (binding.tripBookActivityRadioPublic.isChecked()) {
// External - Public
directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
} else {
// External - Private
directory = getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
}
binding.tripBookActivityEditText.setText(StorageUtils.getTextFromStorage(directory, this, FILENAME, FOLDERNAME));
}
} else {
// TODO : READ FROM INTERNAL STORAGE
}
}
// 3 - Write on external storage
private void writeOnExternalStorage() {
if (StorageUtils.isExternalStorageWritable()) {
File directory;
if (binding.tripBookActivityRadioPublic.isChecked()) {
directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
} else {
directory = getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
}
StorageUtils.setTextInStorage(directory, this, FILENAME, FOLDERNAME, binding.tripBookActivityEditText.getText().toString());
} else {
Toast.makeText(this, getString(R.string.external_storage_impossible_create_file), Toast.LENGTH_LONG).show();
}
}
}
Explications : Dans cette activité, nous avons appelé les deux méthodes publiques statiques de la classe StorageUtils qui nous permettent d'écrire ou de lire un fichier, à partir d'un répertoire racine.
Et ce fameux répertoire racine changera en fonction du choix de l'utilisateur ?
Tout à fait ! Nous avons, dans un premier temps, déclaré dans des variables statiques (1) le nom du fichier (tripBook.txt) dans lequel nous souhaitons stocker du texte, ainsi que le nom du dossier (bookTrip/) qui contiendra ce fichier.
Puis, nous avons créé une méthode (2), readFromStorage
, permettant de lire le contenu du fichier en fonction des choix de l'utilisateur (via les boutons radios). Nous avons fait la même chose (3) dans la méthode writeOnExternalStorage
, mais cette fois-ci en écriture, bien sûr...
D'ailleurs, peut-être l'aurez-vous remarqué, mais ce sont ces deux méthodes qui appellent les méthodes créées précédemment dans la classe StorageUtils ( getTextFromStorage
et setTextInStorage
), avec bien sûr des répertoires racines différents :
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS): nous permettra de générer un chemin d'accès (File) vers le répertoire "Documents" de l'espace de stockage externe, en mode public ;
getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) : nous permettra de générer un chemin d'accès (File) vers le répertoire "Documents" de l'espace de stockage externe, en mode privé.
Nous avons également créé une méthode (4) save qui sera appelée (5) quand l'utilisateur appuiera sur le bouton "Enregistrer" de la Toolbar. Et enfin, nous appelons la méthode readFromStorage
quand l'activité est lancée (6) ou quand l'utilisateur clique sur les boutons radios (7) afin de mettre à jour le fichier lu.
À ce stade, si vous lancez l'application, celle-ci renverra des messages d'erreurs et l'utilisateur sera incapable d'enregistrer du contenu... Je vous laisse un peu chercher pourquoi !
Demandez l'autorisation
Eh oui ! Comme vous vous en doutez, sauvegarder et lire un fichier sur l'espace de stockage externe du téléphone de vos utilisateurs nécessite des autorisations spéciales. Ainsi, nous devons avant toutes choses déclarer ces autorisations dans notre application.
Pour cela, nous allons déclarer les permissions dans le manifeste de notre application Android (pour les versions égales ou inférieures à 5.1.1) :
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.openclassrooms.savemytrip">
<!-- ENABLE PERMISSIONS ABOUT EXTERNAL STORAGE ACCESS -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
</manifest>
Explications : Nous déclarons les permissions nécessaires pour écrire et lire du contenu sur l'espace de stockage externe d'Android, grâce à la permission WRITE_EXTERNAL_STORAGE.
Tiens, mais pourquoi tu n'as pas rajouté aussi la permission READ_EXTERNAL_STORAGE ?
Tout simplement car celle-ci est implicitement approuvée lors de l'approbation de la permission WRITE_EXTERNAL_STORAGE ! Modifions maintenant notre activité afin de demander la permission à l’utilisateur.
Extrait de TripBookActivity.java :
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
public class TripBookActivity extends AppCompatActivity {
// 1 - PERMISSION PURPOSE
private static final int RC_STORAGE_WRITE_PERMS = 100;
@Override
// 2 - After permission granted or refused
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == RC_STORAGE_WRITE_PERMS) {
if (grantResults.length > 0 &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
readFromStorage();
}
}
}
// ----------------------------------
// UTILS - STORAGE
// ----------------------------------
private boolean checkWriteExternalStoragePermission() {
if (ContextCompat.checkSelfPermission(this, WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{WRITE_EXTERNAL_STORAGE},
RC_STORAGE_WRITE_PERMS);
return true;
}
return false;
}
private void readFromStorage(){
// 3 - CHECK PERMISSION
if (checkWriteExternalStoragePermission()) return;
}
}
Explications : La fonction readFromeStorage
est appelée au démarrage de notre activité. Nous ajoutons donc la demande de permission au début de celle-ci afin de s’assurer que nous avons les droits nécessaires au bon fonctionnement de notre activité.
La fonction checkWriteExternalStoragePermission
vérifie si nous devons demander la permission à l’utilisateur. Elle vérifie d’abord si la permission a déjà été acceptée :
Si c’est le cas, elle renverra le booléen false pour continuer le déroulement de la fonction
readFromStorage
.Si ce n’est pas le cas, elle demandera à l’utilisateur d’accepter la permission et n'exécutera pas la suite de la fonction
readFromStorage
.
Lorsque l’utilisateur accepte ou refuse la permission, la fonction onRequestPermissionsResult
est appelée. Nous pouvons donc récupérer la réponse de l’utilisateur et exécuter à nouveau la fonction readFromStorage
si la permission est acceptée.
Lancez maintenant votre application Android. Vous devriez pouvoir sauvegarder et lire le contenu du fichier sans problème. N'hésitez pas à utiliser également le gestionnaire de fichiers de votre téléphone pour visualiser les fichiers créés.
En résumé
La class StorageUtils contient tous les outils nécessaires pour écrire et lire un fichier de façon simple.
Il est obligatoire de demander la permission à l’utilisateur d’utiliser le stockage externe.
En mode public, le fichier n’est pas supprimé quand l’utilisateur désinstalle l’application.
En mode privé, le fichier est supprimé quand l’utilisateur désinstalle l’application.
Bravo ! Vous savez maintenant comment stocker des données dans le stockage externe du smartphone. Allons plus loin et regardons comment stocker un fichier dans le stockage interne cette fois-ci !