09.10.2018

Android: File Storage  // ...save files like a pro!

  // 1 Motivation

Wozu ist dieses Tutorial gut, wenn es doch bereits einen Haufen Dokumentation zu Android gibt?
z.B. hier: https://developer.android.com/training/data-storage/files?

  1. Die unterschiedlichen Android-Versionen und Geräte verschiedener Hersteller unterscheiden sich teils stark in ihrem Verhalten. Deshalb möchte ich vor allem auf die Unterschiede der APIs 15 bis einschließlich 27 eingehen. Zusätzlich soll die Auswirkungen der Hardware am Beispiel einiger alter und neuer Smartphones getestet werden.
  2. Ziel des Tutorials ist es ein möglichst vollständiges Beispiel zu liefern, welches auf allen vorgestellten Geräten lauffähig ist. Das gesamte Android-Studio-Projekt findest du unter Kapitel 13 Download.

In diesem Tutorial werden zunächst alle relevanten Konzepte zur Datenspeicherung unter Android vorgestellt. Im Text sind viele Codeausschnitte eingebettet, oftmals ist jedoch nicht direkt ersichtlich, von welchem Objekt die beschriebenen Methoden aufgerufen werden, wenn dies zum Verständnis nicht absolut notwendig ist. Damit bleiben die Erklärungen übersichtlicher. Zum Verständnis der "größeren Zusammenhänge" lade dir bitte das Gesamtprojekt aus Kapitel 13 herunter.

  // 2 Internal vs. External Storage

In Android wird zwischen "Internal" und "External" Storage unterschieden. Früher war diese Trennung auch physikalisch, d.h. externer Speicher war eine SD-Karte im Smartphone und der interne Speicher war ein fest verbauter Speicherbaustein. Heutzutage sind die Bezeichnungen intern/extern eher verwirrend, da die beiden Speicherbereiche im Regelfall auf dem selben physikalischen Speicher realisiert sind.

Deshalb merk dir besser folgende Definition:

Ist im Gerät eine SD-Karte verbaut, so sind damit entweder zwei externe Speicher vorhanden oder der fest verbaute Speicher wird mit der SD-Karte von Android zu einem "Laufwerk" zusammengefasst. Bei mehreren externen Speicherbereichen wird der erste Speicherbereich als "Primary External Storage" bezeichnet.

  // 3 Permissions

Zum Zugriff auf den Internal Storage sind keine Berechtigungen erforderlich, hier dürfen Daten ohne Weiteres geschrieben und gelesen werden. Beim External Storage müssen ggf. Berechtigungen angefragt werden:

Bis einschließlich API 18:
Permission muss in manifest.xml definiert sein:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

... oder ...

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

... falls Lesezugriff ausreichend ist.

API 19...22:
Seit Android 4.4 wird unterschieden, ob die App im External Storage nur auf ihr privates Verzeichnis zugreift (getExternalFilesDir(), siehe Kapitel 5) oder Zugriff auf den gesamten Speicher erhalten soll. Wird nur auf das private Verzeichnis der App zugegriffen, so sind keine Berechtigungen nötig.

Wird nur auf das private Verzeichnis der App im External Storage zugegriffen, so kann die Permission WRITE_EXTERNAL_STORAGE in manifest.xml folglich für alle Versionen ab API 19 entzogen werden:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="18" />
                                   

Soll hingegen auch auf andere Verzeichnisse im External Storage zugegriffen werden, so muss die App das entsprechende Attribut in manifest.xml enthalten.

Ab API 23:
Für das private Verzeichnis gilt auch mit Android 6.0 weiterhin, dass keine spezielle Permission notwendig ist. Für Datenzugriff auf alle anderen Verzeichnisse im External Storage muss nun allerdings zusätzlich zum Permission-Tag in manifest.xml die Berechtigung zur Laufzeit angefragt werden.

ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
			

Über den Rückgabewert von checkSelfPermission(...) kann geprüft werden, ob die Permission bereits erteilt wurde (z.B. zu einem früheren Start der App). Ist dieser false, so kann die Permission mittels ...

requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE_EXTERNAL_STORAGE_WRITE);
                                             

... angefragt werden. Daraufhin blendet Android ein Dialogfenster ein, welches der Nutzer beantworten muss:

In der Activity, welche die Methode requestPermissions(...) aufgerufen hat, kann die Antwort des Nutzers abgefragt werden, indem die Methode onRequestPermissionsResult(...) überschrieben wird:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    switch(requestCode){
        case PERMISSION_REQUEST_CODE_EXTERNAL_STORAGE_WRITE:
            if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
                logMessage("onRequestPermissionsResult: EXTERNAL_STORAGE_WRITE permission granted");
            }else{
                logMessage("onRequestPermissionsResult: EXTERNAL_STORAGE_WRITE permission denied");
            }
            break;
        default:
             break;
    }
}
                                             

Falls der User den Permission-Request ablehnt, so ist es sinnvoll ihn über die Notwendigkeit der Permission zu informieren, bevor er erneut nach der Permission gefragt wird. Hierzu kann vor der Permission-Abfrage über die Methode shouldShowRequestPermissionRationale(...) ermittelt werden, ob der User den Request früher bereits abgelehnt hat:

if (shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
    // user denied permission request before -> show info
    AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
    dialogBuilder.setMessage("This app needs permission to save data (demo text files) " +
            "on your device. It will not access your personal data outside of this app. " +
            "Android will ask you to allow this app to access files on your device. " +
            "This app will only work if you grant the access.");
    dialogBuilder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            requestExternalStoragePermission();
        }
    });
    dialogBuilder.setTitle("Storage Permission Info");
    dialogBuilder.setIcon(android.R.drawable.ic_dialog_alert);
    dialogBuilder.show();
} else {
    requestExternalStoragePermission();
}

Achtung! Permissions, welche zur Laufzeit der App vom Nutzer erteilt wurden, kann dieser jederzeit wieder über die Einstellungen der App entziehen:

Die App sollte deshalb vor jeder Aktion, welche eine solche Permission benötigt prüfen, ob die Berechtigung noch vorhanden ist.

Zusammenfassung:

  // 4 External Storage: Verfügbarkeit prüfen

Ob der External Storage zugriffsbereit ist, kann mit der Methode getExternalStorageState() geprüft werden:

String extStorageState = Environment.getExternalStorageState();
 
if(extStorageState.equals(Environment.MEDIA_MOUNTED)){
  // External Storage write- and readable
}else if(extStorageState.equals(Environment.MEDIA_MOUNTED_READ_ONLY)){
  // External Storage only readable
}else{
  // External Storage not available for file access
}
		   
  // 5 Pfad zum Speicherplatz

Bevor Daten in den Speicher von Android geschrieben werden, muss noch der Pfad zum richtigen Verzeichnis bestimmt werden. Im Folgenden werden die relevantesten Methoden dazu kurz vorgestellt:

Verzeichnis des Internal Storage:

String path = this.getFilesDir().getAbsolutePath();
		   

Privates Verzeichnis der App auf dem Primary External Storage:

String path = this.getExternalFilesDir(null).getAbsolutePath();
		   

Private Verzeichnisse der App auf allen External Storages:

File[] extStorageDirectories = this.getExternalFilesDirs(null);
		   

In extStorageDirectories[0].getAbsolutePath() ist der Pfad zum Primary External Storages gespeichert. Falls weitere externe Speicherbereiche vorhanden sind, so enthält das Array extStorageDirectories mehr als ein Element.

Root-Verzeichnis des Primary External Storage:

String path = Environment.getExternalStorageDirectory().getAbsolutePath();
		   

Öffentliches Verzeichnis für Bilder auf dem Primary External Storage:

String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath();
		   

Über den Parameter type von getExternalStoragePublicDirectory(String type) können noch weitere öffentliche Verzeichnisse für typische Datei-Kategorien ausgewählt werden, z.B. DIRECTORY_MUSIC, DIRECTORY_DOWNLOADS, DIRECTORY_DOCUMENTS (erst ab API 19!), ...

  // 6 Unterverzeichnisse erstellen

Zur Erstellung eines Ordners kann die Methode mkdirs() genutzt werden. Im Verzeichnis, welches in Kapitel 4 in der Variable path gespeichert wurde, soll der Ordner "Textfiles" erstellt werden:

path += "/Textfiles";
File directory = new File(path);
boolean internalDirectoriesCreated = directory.mkdirs();
		   

War die Erzeugung des Verzeichnisses erfolgreich, so enthält internalDirectoriesCreated den Wert true.

  // 7 Schreiben in Datei

Im Verzeichnis path soll die Datei "test.txt" erstellt und mit dem Text "I'm alive!" beschrieben werden.

path += "/test.txt";
File txtFile = new File(path);
 
FileOutputStream fileOutputStream;
try {
  fileOutputStream = new FileOutputStream(txtFile, true);
} catch (FileNotFoundException e) {
 
}
 
if(fileOutputStream != null){
  OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream);
 
  if(outputStreamWriter != null){
    try{
      outputStreamWriter.append("I'm alive!");
    }catch (IOException e) {
 
    }
 
    try{
      outputStreamWriter.close();
      fileOutputStream.flush();
      fileOutputStream.close();
    }catch (IOException e) {
 
    }
  }
}
		   

  // 8 Lesen von Datei

Die Datei "test.txt" im Verzeichnis path soll in die Variable msgBuffer gelesen werden.

path += "/test.txt";
File txtFile = new File(path);
 
String msgBuffer = null;
 
FileInputStream fileInputStream;
try {
    fileInputStream = new FileInputStream(file);
} catch (FileNotFoundException e) {
 
}
 
if(fileInputStream != null){
  InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream);
 
  if(inputStreamReader != null){
    BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
    if(bufferedReader != null){
      String line;
      do{
        try {
          line = bufferedReader.readLine();
        } catch (IOException e) {
 
        }
 
        if(line != null){
          if(msgBuffer == null){
            msgBuffer = line + "\n";
          }else {
            msgBuffer += line + "\n";
          }
        }
      }while(line != null);
    }
 
    try
    {
      bufferedReader.close();
      inputStreamReader.close();
      fileInputStream.close();
    }
    catch (IOException e) {
 
    }
  }
}
		   

  // 9 Löschen von Datei

Die Datei "test.txt" im Verzeichnis path soll gelöscht werden.

path += "/test.txt";
File txtFile = new File(path);
boolean txtFileDeleted = txtFile.delete();
		  		   

Wurde die Datei erfolgreich gelöscht, so wird txtFileDeleted der Wert true zugewiesen.

  // 10 Deinstallieren der App und Löschen der App-Daten durch Nutzer

Wird die App vom Android-Gerät deinstalliert, so werden die Daten in folgenden Verzeichnissen gelöscht:

Die Daten welche die App in anderen Verzeichnissen auf dem External Storage abgelegt hat, bleiben auch nach der Deinstallation erhalten.

Alternativ kann der Nutzer die Daten der App über das Android System Löschen, z.B. Android 7.0: Einstellungen -> Apps -> FileStorage_Tests -> Speicher -> Daten löschen

Bei diesem Vorgang werden ebenfalls alle Daten der App auf dem Internal Storage und dem privaten Verzeichnis auf dem External Storage gelöscht.

  // 11 USB-Verbindung zum PC

Nach dem Verbinden des Android-Geräts mit einem PC, ist dieses üblicherweise zunächst im Lademodus. Deshalb muss die Option "Dateien übertragen" (oder ähnlich) ausgewählt werden:

Achtung! Der External Storage steht nun im Konflikt zwischen dem angeschlossenen PC und den Apps auf Android. Dieser Konflikt ist auf den meisten neuen Geräten derart gelöst, dass der PC stehts nur eine "Momentaufnahme" des External Storages sieht. Diese ist ggf. veraltet, d.h. wenn eine Android-App neue Dateien angelegt oder gelöscht hat, so sind diese aus Sicht des PCs noch nicht vorhanden oder bereits durch Android gelöschte Dateien sind weiterhin sichtbar. Um diese "Momentaufnahme" zu aktualisieren, kann das Android-Gerät neu gestartet werden. Wenn du eine schneller Methode kennst, schreib mir bitte kurz an isencelessnfo@wordsdillblablaer-techgo_awaynologdumb_botsies.get out!de, dann stelle ich diese gerne hier vor. ;)

Beim Sony Xperia arc S wird der Konflikt so gelöst, dass der External Storage entweder nur vom PC oder nur von den Apps in Android zugreifbar ist. D.h. Apps können nicht auf den External Storage zugreifen, wenn die SD-Karte mit dem PC verbunden ist.

  // 12 Tests

Das Beispielsprojekt wurde mit folgenden Android-Geräten erfolgreich getestet:

Das Sony Xperia arc S ist das einzige getestete Smartphone, welches Internal und External Storage auch physikalisch trennt. Dies bedeutet, dass der External Storage nicht zur Verfügung steht, wenn keine SD-Karte eingesetzt ist. Bei allen anderen Smartphones ist der External Storage auf dem fest verbauten Speicher verfügbar und wird optional durch eine zusätzliche SD-Karte erweitert.

Auf allen getesteten Geräten werden die Daten im External Storage auf folgenden Verzeichnissen gespeichert:

"Root" ist hierbei das Root-Verzeichnis aus Sicht des angeschlossenen PCs, nicht aus Sicht des Android-Geräts.

  // 13 Download