• 20 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Ce cours est en vidéo.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

J'ai tout compris !

Mis à jour le 15/04/2019

Créez un fichier sur le stockage externe

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

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... :)

Introduction

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... :)

Sauvegarder et récupérer des données dans un fichier

Créer 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éer 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...).

Représentation de la classe File
Exemple de structure d'un chemin d'accès

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... :) 

Exemple d'utilisation pour SaveMyTrip
Exemple d'utilisation pour SaveMyTrip

Ecrire et lire 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éthode mkdirs(). 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.

Manipuler 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 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 ! :D

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 ? o_O

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

Mise à jour de 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 BaseActivity {

    ...

    // 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);
        this.configureToolbar();
        // 6 - Read from storage when starting
        this.readFromStorage();
    }

    ...

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            ...
            case R.id.action_save:
                // 5 - Save
                this.save();
                return true;
        }
        ...
    }

    // --------------------
    // ACTIONS
    // --------------------

    ...
    public void onClickRadioButton(CompoundButton button, boolean isChecked){
        if (isChecked) {
           ...
        }
        // 7 - Read from storage after user clicked on radio buttons
        this.readFromStorage();
    }

    // 4 - Save after user clicked on button
    private void save(){
        if (this.radioButtonExternalChoice.isChecked()){
            this.writeOnExternalStorage(); //Save on external storage
        } else {
            //TODO: Save on internal storage
        }
    }

    // ----------------------------------
    // UTILS - STORAGE
    // ----------------------------------

    // 2 - Read from storage
    private void readFromStorage(){
        if (this.radioButtonExternalChoice.isChecked()){
            if (StorageUtils.isExternalStorageReadable()){
                // EXTERNAL
                if (radioButtonExternalPublicChoice.isChecked()){
                    // External - Public
                    this.editText.setText(StorageUtils.getTextFromStorage(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS),this, FILENAME, FOLDERNAME));
                } else {
                    // External - Privatex
                    this.editText.setText(StorageUtils.getTextFromStorage(getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), this, FILENAME, FOLDERNAME));
                }
            }
        } else {
            // TODO : READ FROM INTERNAL STORAGE
        }
    }

    // 3 - Write on external storage
    private void writeOnExternalStorage(){
        if (StorageUtils.isExternalStorageWritable()){
            if (radioButtonExternalPublicChoice.isChecked()){
                StorageUtils.setTextInStorage(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), this, FILENAME, FOLDERNAME, this.editText.getText().toString());
            } else {
                StorageUtils.setTextInStorage(getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), this, FILENAME, FOLDERNAME, this.editText.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 ? :soleil:

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.

A ce stade, si vous lancez l'application, celle-ci renverra des messages d'erreurs et l'utilisateur sera incapable d'enregistrer du contenu... :waw: Je vous laisse un peu chercher pourquoi !

Demander  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.

dependencies {
    ...
    //EASY PERMISSIONS
    implementation 'pub.devrel:easypermissions:1.1.1'
}

Explications : On installe ici la librairie EasyPermissions qui nous permettra de faciliter notre demande de permissions sur les versions d'Android supérieures ou égales à 6.

Puis, 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" />

    ...

</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 ? :o

Tout simplement car celle-ci est implicitement approuvée lors de l'approbation de la permission WRITE_EXTERNAL_STORAGE ! :) Modifions maintenant notre activité afin d'y configurer EasyPermissions.

Extrait de TripBookActivity.java :

import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;

public class TripBookActivity extends BaseActivity {

    ...

    // 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);
        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
    }

    ...
    
    // ----------------------------------
    // UTILS - STORAGE
    // ----------------------------------


    @AfterPermissionGranted(RC_STORAGE_WRITE_PERMS)
    private void readFromStorage(){

        // 3 - CHECK PERMISSION
        if (!EasyPermissions.hasPermissions(this, WRITE_EXTERNAL_STORAGE)) {
            EasyPermissions.requestPermissions(this, getString(R.string.title_permission), RC_STORAGE_WRITE_PERMS, WRITE_EXTERNAL_STORAGE);
            return;
        }

        ...
    }
    
    ...
}

Explications : Nous configurons ici EasyPermissions afin qu'il demande à l'utilisateur d'accepter la permission WRITE_EXTERNAL_STORAGE dès que l'activité se lance (et que celle-ci tente de lire le fichier depuis l'espace de stockage). N'hésitez pas à relire le chapitre de ce cours pour plus d'explications sur cette librairie.

Lancez maintenant votre application Android... Vous devriez pouvoir sauvegarder et lire le contenu du fichier sans problème. N'hésitez également pas à utiliser le gestionnaire de fichiers de votre téléphone pour visualiser les fichiers créés. :)

Sauvegarde dans l'espace de stockage externe
Sauvegarde dans l'espace de stockage externe
Exemple de certificat de réussite
Exemple de certificat de réussite