AE Android Kochbuch: Notizen App. Reloaded

Unser AE Android Kochbuch Teil 3A: Notizen App Reloaded. Schreiben. Speichern. Laden. Vorlesen lassen.

In Teil 3 präsentierte ich eine kleine Notizbuch App, die aber nur eingeschränkt nutzbar war. Daher gibt es hier Teil 3A – die Notizen App Reloaded! In diesem Teil widme ich mich dem gleichen Thema, aber nun basteln wir die App so, dass man sie auch sinnvoll verwenden kann. Und wer es eilig hat: die App bekommt auch eine Funktion, dass sie euch eure Notizen vorlesen kann!

Unser AE Android Kochbuch für Praktiker. Hier zeige ich euch wie ihr schnell und einfach Android programmieren lernt. Tipps aus der Praxis. Vorweg noch mal der Hinweis: Dieses ist zwar ein Grundkurs, aber ihr solltet schon etwas Grundlagenwissen haben. Zum Beispiel wissen, was eine Variable ist, und dass man Zahlen in Integer und Texte in String Variablen speichert. Wenn euch solche Basics noch fremd sind – ich empfehle die ersten Kapitel vom Buch Java ist auch nur eine Insel oder andere Literatur, die euch in solche grundlegenden Dinge einführt.

Die Ausgangslage

Android Studio aktiv. Ich benutze jetzt die gerade aktuelle Version Giraffe. Neues Java Projekt Writer ist angelegt.

Layout Datei

Wie auch schon in den Teilen davor beginnen wir mit der Layout Gestaltung.

Eingabefenster

Primär verwenden wir ein Element EditText, das wir auf Multiline setzen, damit wir mehrere Zeilen eingeben können. Natürlich vergeben wir auch eine id, damit wir den Inhalt von EditText vom Java Programm aus lesen oder verändern können.

<EditText 
android:id="@+id/editTextText" 
android:layout_width="match_parent" 
android:layout_height="300dp" 
android:background="@drawable/drawback" 
android:gravity="top" 
android:scrollHorizontally="false" 
android:scrollbars="vertical" 
android:inputType="textMultiLine" 
android:text="" />

Buttons für Benutzeraktivitäten

Dem Benutzer spendieren wir sinnvollerweise die Buttons:

-) Button Notizen laden

Der Benutzer soll aus gespeicherten Notizen auswählen und diese laden.

-) Button Notizen speichern

Notizen können als Datei gespeichert werden.

-) Button Notiz vorlesen

Die App soll eine Notiz vorlesen.

-) Notiz Fenster löschen

Der Inhalt im Notiz Fenster soll komplett entfernt werden.

Die ersten drei Buttons basteln wir in der Layout Datei in gewohnter Form direkt unterhalb dem Eingabefenster. Das Ganze kapseln wir in einem horizontalem Layout, damit die Buttons nebeneinander stehen.

<LinearLayout 
android:layout_width="match_parent" 
android:layout_height="wrap_content" 
android:orientation="horizontal"> 

<Button 
android:id="@+id/buttonLaden" 
android:layout_width="120dp" 
android:layout_height="wrap_content" 
android:text="Laden" /> 

<Button 
android:id="@+id/buttonSpeichern" 
android:layout_width="120dp" 
android:layout_height="wrap_content" 
android:layout_marginLeft="10dp" 
android:text="Speichern" /> 

<Button 
android:id="@+id/buttonVorlesen" 
android:layout_width="120dp" 
android:layout_height="wrap_content" 
android:layout_marginLeft="10dp" 
android:text="Vorlesen" /> 
</LinearLayout>

Der Imagebutton zum Löschen

Zum Löschen des Eingabefensters verwenden wir das Element ImageButton, weil ein kleines Piktogramm ausreichen sollte, um die Funktion des Buttons klar zu machen.

Den Imagebutton zieht ihr in der Design Ansicht aus der Palette Buttons einfach wie gewohnt in eure Layout Ansicht.

Damit der Button ein Bild bekommt: Das Bild für den Imagebutton wählen wir in Ressource. In meinem Fall soll das einfach nur das übliche X sein.

Um das Bild für den Imagebutton zu wählen: einfach den Cursor im Projektfenster auf RES platzieren und mit der rechten Maustaste bekommt ihr ein Auswahlmenü NEW um dann ein Bild als Vector Asset auszuwählen.

Klickt dann in der Maske einfach in das Feld Clip Art und ihr erhalten eine Übersicht über die verfügbaren Clip Arts aus denen ihr dann auswählen könnt. Ihr könnt nun durch die Cllip Arts scrollen oder im Suchfeld Clear angeben. Je naachdem, wie ihr vorgehen woll. Am Ende sollte es dann mal so aussehen und ihr könnt das Bild als Ressource übernehmen.

Last not least die Ressource in der XML Datei beim Image Button einbauen:

<ImageButton 
android:id="@+id/imageButton_clrText" 
android:layout_width="40dp" 
android:layout_height="40dp" 
app:srcCompat="@drawable/baseline_clear_24" />

Layout komplett

Somit erhalten wir die Layout Datei:

<?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
xmlns:app="http://schemas.android.com/apk/res-auto" 
xmlns:tools="http://schemas.android.com/tools" 
android:layout_width="match_parent" 
android:layout_height="match_parent" 
android:orientation="vertical" 
tools:context=".MainActivity"> 

<TextView 
android:layout_width="wrap_content" 
android:layout_height="wrap_content" 
android:text="Willkommen im AE Mini Writer" /> 

<ImageButton 
android:id="@+id/imageButton_clrText" 
android:layout_width="40dp" 
android:layout_height="40dp" 
app:srcCompat="@drawable/baseline_clear_24" /> 

<EditText 
android:id="@+id/editTextText" 
android:layout_width="match_parent" 
android:layout_height="300dp" 
android:background="@drawable/drawback" 
android:gravity="top" 
android:scrollHorizontally="false" 
android:scrollbars="vertical" 
android:inputType="textMultiLine" 
android:text="" /> 

<LinearLayout 
android:layout_width="match_parent" 
android:layout_height="wrap_content" 
android:orientation="horizontal"> 

<Button 
android:id="@+id/buttonLaden" 
android:layout_width="120dp" 
android:layout_height="wrap_content" 
android:text="Laden" /> 

<Button 
android:id="@+id/buttonSpeichern" 
android:layout_width="120dp" 
android:layout_height="wrap_content" 
android:layout_marginLeft="10dp" 
android:text="Speichern" /> 

<Button 
android:id="@+id/buttonVorlesen" 
android:layout_width="120dp" 
android:layout_height="wrap_content" 
android:layout_marginLeft="10dp" 
android:text="Vorlesen" /> 

</LinearLayout> 

</LinearLayout>

Der Java Code

Wechseln wir jetzt in die Datei MainAcitivity.java und füllen die App mit Leben.

private static EditText etEingabe1;

//----------------------------------------------------------------------
 @Override 
protected void onCreate(Bundle savedInstanceState) { 
  super.onCreate(savedInstanceState); 
  setContentView(R.layout.activity_main); 

  //EditText Variable init - weil wir sie ueberall nutzen
  etEingabe1 = findViewById(R.id.editTextText); 

  //Listener fuer Buttons registrieren
  findViewById(R.id.imageButton_clrText).setOnClickListener(this); 
  findViewById(R.id.buttonLaden).setOnClickListener(this); 
  findViewById(R.id.buttonSpeichern).setOnClickListener(this); 
  findViewById(R.id.buttonVorlesen).setOnClickListener(this); 

  //am Start: Cursor in das Textfeld setzen
  etEingabe1.requestFocus(); 
}

Das Eingabefenster als editText Element wird der EditText Variable etEingabe1 zugewiesen. Die Variable wird außerhalb der Methode deklariert, damit wir sie in der ganzen Klasse nutzen können, ohne sie jedes Mal neu zuweisen zu müssen. Die Zuweisung hingegen findet in der Methode onCreate statt. Last not least stellen wir den Cursor ins Eingabefenster, damit der Benutzer gleich tippen kann.

In der Methode onCreate registrieren wir die vier Buttons jeweils mit einem onClickListener. Ob ImageButton oder normaler Button spielt dabei keine Rolle. Alle Button Elemente werden identisch registriert. Wir erhalten dann den onClick Listener, bei dem wir die Aktionen für jeden Button eintragen.

//----------------------------------------------------------------------
 @Override 
public void onClick(View v) { 
  //Unser Eingabe Listener
  switch (v.getId()) {
    //hier tragen wir die Aktionen für die Buttons ein
 }
}

Image Button Löschen

Mit dem kleinen Imagebutton kann ich das Eingabefenster jederzeit löschen. Grafik und Design des Buttons finden sich in der XML Layout Datei. Im Java Code lege ich die Aktionen fest, wenn der Button geklickt wird. Konkret geschieht das in der switch Schleife in der onClick Methode. Dort hinterlege ich die Aktionen, was passiert, wenn der Benutzer den Image Button betätigt.

//----------------------------------------------------------------------
 @Override 
public void onClick(View v) { 
  //Unser Eingabe Listener
  switch (v.getId()) { 
    case R.id.imageButton_clrText: 
    //Button CLEAR
    etEingabe1.setText(""); 
    etEingabe1.requestFocus(); 
    break;
  }
}

Daten speichern und laden von öffentlichen Orten. Intents

Bevor wir uns jetzt mit dem Buttons Speichern und Laden beschäftigen, müssen wir über Intents reden. Intents sind die Google Antwort, damit wir per Software auf alle möglichen Ressourcen des Smartphone zugreifen und trotzdem die von Google erwünschten Sicherheitsstandards einhalten. Hierbei gilt: Eine Anwendung darf nicht ohne Erlaubnis auf Ressourcen des Smartphone zugreifen, sondern benötigt immer eine Erlaubnis oder Aktion des Benutzers.

In unserem Fall werden wir Intents verwenden, um Daten zu speichern und Daten zu lesen. Dateiname der Datei sowie Speicherort kann der Benutzer jeweils individuell vergeben. Wegen der unterschiedlichen Rechte auf verschiedenen Verzeichnissen empfehle ich, das /Downloads Verzeichnis zu verwenden. Wer mag, kann sich hier ein weiteres Unterverzeichnis anlegen.

In Intent besteht aus Sicht des Programms immer aus folgenden Aktionen:

1) Request Code für verschiedene Intents definieren

2) Ein Intent wird aufgerufen

3) Ein Intent wurde aktiviert und muss ausgewertet werden

Request Code für Intent definierern

Damit verschiedene Intents voneinander trennen können, bekommen sie beim Aufruf einen beliebigen Request Code mitgeteilt, den ihr dann später in der Auswertung abfragen könnt. Zu diesem Zweck vergebe ich am Anfang der Klasse für verschiedene Intents unterschiedliche Id Werte, z.B:

//----------------------------------------------------------------------
 public class MainActivity extends AppCompatActivity implements View.OnClickListener { 

private static final int REQUESTCODE_SAVE = 123; //Requestcode f. Export / Save
private static final int REQUESTCODE_LOAD = 125; //Requestcode f. Import / Laden
private static final int REQUESTCODE_CAMERA = 160; //Requestcode f. Camera / Scanner

Die ID Werte sind rein willkürlich gewählt. Wichtig ist nur, dass ihr sie unterscheiden können. Also wenn Save und Camera den gleichen Wert aufweisen, wird es ein Problem geben.

Intent aufrufen

Den Aufruf eines Intentes können ihr in eurem Java Code dort unterbringen, wo ihr es gerne habt. Also z.b. in der onClick Methode, wenn ein Button betätigt wird. Abhängig von dem, was der Benutzer machen soll, bekommen die Intents unterschiedliche Start Werte. Nachstehendes Beispiel zeigt wie man das Intent zum Anlegen und Speichern einer reinen Text Datei aufruft:

Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); 
intent.addCategory(Intent.CATEGORY_DEFAULT); 
intent.setType("text/plain"); 
intent.putExtra(Intent.EXTRA_TITLE, ""); 
startActivityForResult(intent, REQUESTCODE_SAVE);

Es versteht sich vermutlich, dass das Intent für die eingebaute Camera etwas anders aussehen wird. Ihr müsst euch also jedes Mal über das Intent informieren, wenn ihr eins verwenden wollt.

Intent auswerten

Das Abfragen von Intents geschieht hingegen in der Methode onAcitivityResult. Diese Methode ist für alle Intents da, d.h. ihr müsst selbst abfragen, welches Intent gerade durchgeführt wurde. Ob z.B. der Benutzer die Camera verwendet hat, um Barcode zu lesen oder ob eine Datei gespeichert werden soll. Beispiel für die Methode, wobei ich bereits auf die Intents Datei speichern und Datei laden abfrage:

//----------------------------------------------------------------------
 public void onActivityResult(int requestCode, int resultCode, Intent resultData) { 
  //Intent auswerten

  super.onActivityResult(requestCode, resultCode, resultData); 

  //Intent: Datei speichern
  if (requestCode == REQUESTCODE_SAVE && resultCode == Activity.RESULT_OK) { 
    //hier kommt der Code zum Speichern rein 
  } 

  //Intent: Datei laden
  if (requestCode == REQUESTCODE_LOAD && resultCode == Activity.RESULT_OK) {
    //hier kommt der Code zum Laden rein 
  } 
}

Button Notiz speichern

Genauso wie oben geht es auch mit dem Button Notiz. Hier soll eine Speicherroutine aktiviert werden, bei der Benutzer Dateinamen und Speicherort auswählen kann, wenn er seine Notiz speichert. Die Aktionen für den Button werden daher auch hier wieder in der switch Schleife in der Methode onClick eingetragen. Da ich über Intents speichern will – wie oben angeführt:

//----------------------------------------------------------------------
 @Override 
public void onClick(View v) { 
  //Unser Eingabe Listener

  Intent intent = new Intent(); 

  switch (v.getId()) { 
... 

    case R.id.buttonSpeichern: 
      intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); 
      intent.addCategory(Intent.CATEGORY_DEFAULT); 
      intent.setType("text/plain"); 
      intent.putExtra(Intent.EXTRA_TITLE, ""); 
      startActivityForResult(intent, REQUESTCODE_SAVE); 
      break;

  }
}

Zum Abfragen des Intents nutzen wir unsere onActivityResult Methode.

//----------------------------------------------------------------------
 public void onActivityResult(int requestCode, int resultCode, Intent resultData) { 
  //Intent auswerten

  super.onActivityResult(requestCode, resultCode, resultData); 

  //Intent: Datei speichern
  if (requestCode == REQUESTCODE_SAVE && resultCode == Activity.RESULT_OK) { 
    Uri uri = null; 
    if (resultData != null) { 
      uri = resultData.getData(); 
      doSaveData2Uri (etEingabe1, uri); 
    } 
  } 
}

Intents mit Datei Operationen liefern uns eine Uri. Eine URI ist im Prinzip auch ein Verweis auf eine Datei, aber etwas spezieller als nur Datenpfad und Dateiname. Info zur URI hier! Für mich bedeutet dass, ich muss eine Speichermethode doSaveData2Uri so bauen, dass ich über eine URI speichern kann. Als Parameter gebe ich der Methode das Element EditText und und die Uri mit, wo die Dateien hin sollen.

Fehlt nur noch die Methode zum Speichern einer Datei, die über eine URI definiert wird.

//----------------------------------------------------------------------
 private void doSaveData2Uri (EditText etQuelle, Uri uri) { 
  //Save Date von EditText in Datei via URI

  String myData = etQuelle.getText().toString(); 

  try { 
    OutputStream stream = getContentResolver().openOutputStream(uri, "wt"); 
    PrintWriter writer = new PrintWriter(stream); 
    writer.write(myData); 
    writer.flush(); 
    stream.close(); 
    Toast.makeText(this, "Datei wurde gespeichert!", Toast.LENGTH_LONG).show(); 
  } catch (Exception e) { 
    Log.e(getLocalClassName(), "caught IOException", e); 
    Log.d ("ERROR", e.toString()); 
  } 
}

Für das Speichern definiere ich mir mittels getCotentResolver einen Output Stream mit der angegebenen Uri und dem Paramter „wt“. Das wt ist wichtig. Würde ich den Paramter weglassen, werden neue Daten zwar in die Datei gespeichert, wenn die Datei aber vorher länger war, bleibt der alte Dateninhalt noch erhalten. Mittels t wird ein truncate erzwungen, d.h. die Datei wird nach dem Speichern der Informationen abgeschnitten, so dass keine alten Daten mehr enthalten sind.

Der eigentliche Speichervorgang findet dann mit writer.write statt. Hier wird der Inhalt des Elements EditText einfach als Stringwert gespeichert und die Datei geschlossen. Am Schluss gebe ich mir mittels Toast Message noch eine kleine Meldung aus, dass der Speichervorgang vollzogen wurde.

Button Notiz laden

Der Lade Button läuft fast ähnlich wie der Speicher Button – nur anders herum! Wieder wird der switch in der onClick Methode erweitert. Dieses Mal rufe ich ein Intent zum Öffnen und Lesen einer TEXT Datei auf und verwenden den Requestcode LOAD.

//----------------------------------------------------------------------
 @Override 
public void onClick(View v) { 
  //Unser Eingabe Listener

  Intent intent = new Intent(); 

  switch (v.getId()) { 
  ... 
    case R.id.buttonLaden: 
      intent = new Intent (Intent.ACTION_OPEN_DOCUMENT); 
      intent.addCategory(Intent.CATEGORY_DEFAULT); 
      intent.setType("text/plain"); 
      intent.putExtra(Intent.EXTRA_TITLE, ""); 
      startActivityForResult(intent, REQUESTCODE_LOAD); 
      break; 
  } 
}

Analog dazu in der Methode onActivityResult die Abfrage ob das Intent zum Öffnen und Lader einer Text Datei ausgewählt wurde und ich bastele mir eine Methode, bei der eine Datei unter Angabe einer URI geladen wird:

//----------------------------------------------------------------------
 public void onActivityResult(int requestCode, int resultCode, Intent resultData) { 
  //Intent auswerten
 ... 
  //Intent: Datei laden
  if (requestCode == REQUESTCODE_LOAD && resultCode == Activity.RESULT_OK) { 
    Uri uri = null; 
    if (resultData != null) { 
      uri = resultData.getData(); 
      doLoadDataFromUri (uri, etEingabe1); 
    } 
  } 
}

Auch hier nehme ich wieder eine eigene Methode, die ich doLoadDataFromUri nenne. Als Parameter gebe ich die die vom Benutzer ausgewählte Uri und das EditText Elemtn mit, in dem die Daten angezeigt werden sollen. Die Lademethode dann im Detail:

//----------------------------------------------------------------------
 private void doLoadDataFromUri (Uri myUri, EditText etZiel) { 
//Text Datei laden von URI. Dann in EditText anzeigen

try { 
  InputStreamReader inputStreamReader = new InputStreamReader(getContentResolver().openInputStream(myUri)); 
  BufferedReader bufferedReader = new BufferedReader(inputStreamReader); 
  StringBuilder sb = new StringBuilder(); 
  String s = ""; 

  //Datei lesen, String aufbauen
  while ((s = bufferedReader.readLine()) != null) { 
    Log.d ("Akt gelesen", s); 
    sb.append(s); 
  } 

  //Wenn fertig mit lesen: String nach EditText anzeigen
  String fileContent = sb.toString(); 
  bufferedReader.close(); 
  etZiel.setText(fileContent); 

  } catch (IOException e) { 
    //Fehlerbehandlung bei Ausnahme
    Log.e(getLocalClassName(), "caught IOException", e); 
  } 
}

Dieses Mal hole ich mir einen Stream Reader mit dem ich die Daten lese, mir mittels String Builder einen großen String aufbauen und diesen am Schluss in die als Parameter übergebene EditText anzeige.

Button Vorlesen

Das machen wir uns einfach und verwenden die Text To Speach Routinen, die uns Google anbietet. Eine private Variable TextToSpeech deklairieren und in der onInit Funktion auf das Textfenster setzen. Dazu kann ich noch den Dialekt spezfizieren – ich hätte es gerne halbwegs verständlich – also Deutsch / Deutsch! Im onClick Listener aktivere ich dann die Vorlesefunktion und fertig. Sieht dann konkret so aus:

//----------------------------------------------------------------------
 public class MainActivity extends AppCompatActivity implements View.OnClickListener, TextToSpeech.OnInitListener { 

...

private TextToSpeech tts;

….

//----------------------------------------------------------------------
 @Override 
public void onClick(View v) { 
  //Unser Eingabe Listener
…. 
  switch (v.getId()) { 
…. 
    case R.id.buttonVorlesen: 
      tts = new TextToSpeech(this, this); 
      tts.shutdown(); 
      break; 
  } 
}

….

//----------------------------------------------------------------------
 @Override 
public void onInit(int i) { 
  String S1 = etEingabe1.getText().toString(); 
  tts.setLanguage(Locale.GERMAN); 
  tts.speak(S1, TextToSpeech.QUEUE_FLUSH, null); 
}

Das war es schon.

Java Code komplett

Hinweis: Leider ist die Formatierung etwas beschädigt. Die Klammern und Einrückungen sind umgefallen!

//----------------------------------------------------------------------
 public class MainActivity extends AppCompatActivity implements View.OnClickListener, TextToSpeech.OnInitListener { 

private static EditText etEingabe1; 

private static final int REQUESTCODE_SAVE = 123; //Requestcode f. Export / Save
private static final int REQUESTCODE_LOAD = 125; //Requestcode f. Import / Laden
private static final int REQUESTCODE_CAMERA = 160; //Requestcode f. Camera / Scanner

private TextToSpeech tts; 

//----------------------------------------------------------------------
@Override 
protected void onCreate(Bundle savedInstanceState) { 
  super.onCreate(savedInstanceState); 
  setContentView(R.layout.activity_main); 

  //EditText Variable init - weil wir sie ueberall nutzen
  etEingabe1 = findViewById(R.id.editTextText); 

  //Listener fuer Buttons registrieren
  findViewById(R.id.imageButton_clrText).setOnClickListener(this); 
  findViewById(R.id.buttonLaden).setOnClickListener(this); 
  findViewById(R.id.buttonSpeichern).setOnClickListener(this); 
  findViewById(R.id.buttonVorlesen).setOnClickListener(this); 

  //am Start: Cursor in das Textfeld setzen
  etEingabe1.requestFocus(); 
} 

//----------------------------------------------------------------------
@Override 
public void onClick(View v) { 
  //Unser Eingabe Listener

  Intent intent = new Intent(); 

  switch (v.getId()) { 
    case R.id.imageButton_clrText: 
      //Button CLEAR
      etEingabe1.setText(""); 
      etEingabe1.requestFocus(); 
      break; 

    case R.id.buttonSpeichern: 
      intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); 
      intent.addCategory(Intent.CATEGORY_DEFAULT); 
      intent.setType("text/plain"); 
      intent.putExtra(Intent.EXTRA_TITLE, ""); 
      startActivityForResult(intent, REQUESTCODE_SAVE); 
      break; 

    case R.id.buttonLaden: 
      intent = new Intent (Intent.ACTION_OPEN_DOCUMENT); 
      intent.addCategory(Intent.CATEGORY_DEFAULT); 
      intent.setType("text/plain"); 
      intent.putExtra(Intent.EXTRA_TITLE, ""); 
      startActivityForResult(intent, REQUESTCODE_LOAD); 
      break; 

    case R.id.buttonVorlesen: 
      tts = new TextToSpeech(this, this); 
      tts.shutdown(); 
      break; 
  } 
} 

//----------------------------------------------------------------------
@Override 
public void onInit(int i) { 
  String S1 = etEingabe1.getText().toString(); 
  tts.setLanguage(Locale.GERMAN); 
  tts.speak(S1, TextToSpeech.QUEUE_FLUSH, null); 
} 

//----------------------------------------------------------------------
public void onActivityResult(int requestCode, int resultCode, Intent resultData) { 
  //Intent auswerten

  super.onActivityResult(requestCode, resultCode, resultData); 

  //Intent: Datei speichern
  if (requestCode == REQUESTCODE_SAVE && resultCode == Activity.RESULT_OK) { 
    Uri uri = null; 
    if (resultData != null) { 
      uri = resultData.getData(); 
      doSaveData2Uri (etEingabe1, uri); 
    } 
  } 

  //Intent: Datei laden
  if (requestCode == REQUESTCODE_LOAD && resultCode == Activity.RESULT_OK) { 
    Uri uri = null; 
    if (resultData != null) { 
      uri = resultData.getData(); 
      doLoadDataFromUri (uri, etEingabe1); 
    } 
  } 

  //Andere RequestCodes hier einbauen
} 

//----------------------------------------------------------------------
private void doSaveData2Uri (EditText etQuelle, Uri uri) { 
  //Save Date von EditText in Datei via URI

  String myData = etQuelle.getText().toString(); 

  try { 
    OutputStream stream = getContentResolver().openOutputStream(uri, "wt"); 
    PrintWriter writer = new PrintWriter(stream); 
    writer.write(myData); 
    writer.flush(); 
    stream.close(); 
    Toast.makeText(this, "Datei wurde gespeichert!", Toast.LENGTH_LONG).show();
  } catch (Exception e) { 
    Log.e(getLocalClassName(), "caught IOException", e); 
    Log.d ("ERROR", e.toString()); 
  } 
} 

//----------------------------------------------------------------------
private void doLoadDataFromUri (Uri myUri, EditText etZiel) { 
  //Text Datei laden von URI. Dann in EditText anzeigen

  try { 
    InputStreamReader inputStreamReader = new InputStreamReader(getContentResolver().openInputStream(myUri)); 
    BufferedReader bufferedReader = new BufferedReader(inputStreamReader); 
    StringBuilder sb = new StringBuilder(); 
    String s = ""; 

    //Datei lesen, String aufbauen
    while ((s = bufferedReader.readLine()) != null) { 
      Log.d ("Akt gelesen", s); 
      sb.append(s); 
    } 

    //Wenn fertig mit lesen: String nach EditText anzeigen
    String fileContent = sb.toString(); 
    bufferedReader.close(); 
    etZiel.setText(fileContent); 

  } catch (IOException e) { 
    //Fehlerbehandlung bei Ausnahme
    Log.e(getLocalClassName(), "caught IOException", e); 
  } 
} 

} //end class

Der Testlauf

In den vorherigen Teilen habe ich es ausführlich behandelt. Um die App nun im Emulator zu starten: Device Emulator mit einem ausgewählten Gerät starten, App starten. Das Ergebnis sieht dann erwartungsgemäß so aus:

Spielen wir mit allen Funktionen herum, um uns einen Eindruck von der kleinen App zu verschaffen. Wir können Texte tippen, speichern, laden und vorlese lassen. Tipp: wenn ihr beim Vorlesen nichts hört – Lautstärke im Device Emulator hochdrehen. Gibt extra Buttons für die Lautstärke auf dem Smartphone!

Fehlersuche / Optimierung

Programmierung ist Handwerk. Viele verschiedene Wege führen zum Ergebnis und manchmal geschehen unerwartete Sachen. Daher müssen wir Programmierer unser Werk testen. Möglichst bevor Kunden oder echte Einsatzfälle es tun.

Code abzutippen ist heute einfach geworden. Einfach aus dem Internet ein paar Codeschnipsel kopieren, fertig ist ein Programm. Doch wie gut ein Programm ist, entscheidet der Alltag. Ich verbringe z.B. genauso viel Zeit mit dem Testen der Software wie mit dem Programmieren. Und trotzdem passieren im Alltag Sachen mit denen ich nicht rechnete.

Im Fall unserer Anwendung stellt man recht schnell fest, dass etwas nur suboptimal läuft. Wird Beispielsweise der Text unten gespeichert und anschließend wieder geladen, sieht er verändert aus.

Wenn ich das jetzt speichere und mir im Device Explorer die Datei ansehe sieht sie so aus:

Wenn ich die Datei aber dann wieder lade schaut sie so aus.

Offensichtlich hat sie ihre Zeilenumbrüche verloren und alles wird einfach hintereinander angezeigt. Um zu begreifen, was passiert ist, mache ich folgendes:

Ich gebe den Text neu ein und starte den Device Explorer. Dort navigiere ich in das Verzeichnis /Download und übertragen die Datei an meinen PC. Auf dem PC schaue ich sie mir in einem Editor in verschiedenen Darstellungen, zuerst in der ASCII Text und anschließend in der HEX Darstellung.

Anschließend wiederhole ich den Vorgang mit der Datei, die nach dem Laden keine Zeilenumbrüche mehr hat.

Beim Vergleichen der beiden Anzeigen vorher / nachher fällt relativ schnell auf: Der Zeilenumbruch nach dem Punkt (Hex 2E) wird als LF (Line Feed) mit Hex 0A gespeichert. Nach dem Laden ist das LF jedoch nicht mehr da und man kann in der Hex Darstellung nur noch den Punkt 2E Hex finden. (Das war übrigens der Grund, warum ich einen Punkt als Satzende eingeben, bevor ich die ENTER Taste drückte. So lassen sich die Daten in der Hexdarstellung leichter finden. Man muss nur den Punkt suchen!)

Problem also: meine Laderoutine doLoadDataFromUri filtert irgendwie den Zeilenumbruch heraus und muss optimiert werden, damit Zeilenumbrüche richtig dargestellt werden.

Bisher wurden die Daten satzweise gelesen und an einen Stringbuffer angehängt:

//Datei lesen, String aufbauen
  while ((s = bufferedReader.readLine()) != null) { 
    Log.d ("Akt gelesen", s); 
    sb.append(s); 
  }

Bei dieser Logik scheint nun aber das LF 0A Hex verloren zu gehen. Ich habe jetzt folgende Möglichkeiten:

1) Ich kann jetzt die Dokumentationen von Google oder Java zu Rate ziehen und versuchen herauszufinden, ob es eine Lösung gibt, wie auch das Stringende 0A Hex in den Ziel-String übernommen werden kann

oder

2) ich füge einfach selbst CF LF (Hex 0D 0A) am jeweiligen Stringende ein. Theoretisch reicht LF Hex 0A, aber es gibt Editoren, die erwarten CR LF als Zeilenende. Wenn ich also beides einfüge, bin ich auf der garantiert sicheren Seite und muss mir keine Gedanken machen, wenn der Benutzer die Datei wo-auch-immer noch verarbeiten will.

Aufgrund der einfachen Lösung wähle ich Lösung 2 und bastele einfach die Schreibroutine etwas um:

//Datei lesen, String aufbauen
while ((s = bufferedReader.readLine()) != null) { 
  Log.d ("Akt gelesen", s);
  s = s + "\r\n"; 
  sb.append(s) ; 
}

In der Praxis würde ich es allerdings so darstellen:

//Datei lesen, String aufbauen
while ((s = bufferedReader.readLine()) != null) { 
  Log.d ("Akt gelesen", s); 
  sb.append(s + "\r\n"); 
}

Anschließend natürlich wieder testen. Nun stellt sich heraus: Texte werden mit Umbrüchen gespeichert und haben keine Veränderung mehr!


Das Video zu dieser App ist zweigeteilt:

Teil A: XML Layout

Teil B: Der Java Code


Text und Entwurf. (c) AE SYSTEME Testcenter, Hans-J. Walter
Hans-J. Walter ist Programmierer für Windows DOT.NET / C# und Android und als eingetragener, unabhängiger Journalist verantwortlich für Fachberichte und Schulungstexte über Technik u. Entwicklung. hjw@terminal-systems.de

Für diese und alle nachfolgenden Seiten gilt ebenso der obligatorische Hinweis: Alle Angaben ohne Gewähr. Bilder und Codes zeigen Beispiele. Diese Beschreibung bezieht sich auf unsere Installation und stellt keine Bewertung der verwendeten Techniken da. Fehler und Irrtümer vorbehalten!

Schreibe einen Kommentar