Ich bin bereits seit geraumer Zeit ein riesiger Fan des kleinen Mini-Computers Raspberry Pi und der enormen Möglichkeiten, die dieses kleine Wunderwerk eröffnet.
Selber habe ich meinen eigenen Raspberry Pi 2 erst seit etwa einem halben Jahr, doch bastel ich seit dem ersten Tag an munter an ihm herum.
Da ich nun im Eifer des Gefechts auf die Idee kam meinen Raspberry Pi Steckdosen ein- und ausschalten zu lassen, bin ich im Internet auf dieses Tutorial von tutorials-raspberrypi.de gestoßen.
Natürlich habe ich dieses Projekt direkt umgesetzt.
Zurück zum Thema:
Nun denn, genug um den heißen Brei herum geredet.
Eines verregneten Tages hatte ich dann die Idee meinen Raspberry Pi per Handy fernsteuern zu wollen.
Steckdosen ein, Steckdosen aus.
Es gibt im Google PlayStore bereits Apps, die es uns erlauben eine SSH Verbindung zu einem Linux Gerät aufzubauen. Leider sind diese Apps nicht darauf ausgelegt per einfachem Knopfdruck einen längeren Befehl auf dem Gerät auszuführen.
Wir wollen dies nun ändern!
Vorwort:
Wir werden in diesem Artikel ebenfalls SSH für unsere Verbindung zum Raspberry Pi nutzen, allerdings auf Tastendruck nur eine Verbindung aufbauen, um einen Befehl zu senden und die Verbindung anschließend wieder schließen.
Das Protokoll von SSH ist nicht sonderlich einfach zu programmieren, weshalb wir auf eine bereits bestehende Bibliothek (JSch) zurückgreifen werden.
Bitte versteht, dass diese Anleitung zwar so einfach wie möglich gehalten ist, aber tiefgreifendere Programmierung darstellt. Vor allem aber wird dieser Artikel mehr als nur ein paar kleine Worte (nach dem fertigen Schreiben dieses Artikels sind es ganze 4561 Worte) umfassen und meines Erachtens nach recht mächtig werden. Ich hoffe ihr habt etwas Geduld mit gebracht.
Vorbereitung:
Bevor wir anfangen können zu programmieren ist es zwingend von Nöten die bereits angesprochene Bibliothek in Android zu importieren.
Diese Bibliothek wird uns viel Arbeit abnehmen.
Als erstes müssen wir JSch herunterladen.
http://sourceforge.net/projects/jsch/files/jsch.jar/0.1.53/jsch-0.1.53.jar/download
Ist dies geschehen verschieben wir die Datei in den Ordner app -> libs.
Abschließend muss noch “Sync Project with Gradle Files” in der oberen Leiste von Android Studio gewählt werden.
Programmierung:
Die Vorarbeit ist geleistet. Fangen wir an zu programmieren!
Als Erstes benötigen wir ein paar wenige Imports.
Diese fügen wir direkt unterhalb unseres Packagepfades, in die zu benutzende Java Datei, ein:
import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Properties;
Wir erstellen uns nun eine eigene Funktion. Wir nennen diese “sshBefehl”:
public static ArrayList sshBefehl(String username, String passwort, String hostname, int port, String befehl) throws Exception {
//Hier den Code einfügen.
}
Dieser Funktion können wir dann später die entsprechenden Nutzer- und Verbindungsdaten und den Befehl übergeben.
Sie soll unseren Befehl per SSH ausführen, die Ausgabe der Konsole zurückliefern und bei einem Fehler eine Meldung ausgeben (“throws Exception”).
Verbindung aufbauen und Befehl senden:
Unsere Aufgabe gestaltet sich nun etwas komplexer.
Als Erstes müssen wir versuchen eine Verbindung zu unserem Raspberry Pi aufzubauen, anschließend müssen wir noch einen Channel öffnen, indem wir den Befehl senden. Erst innerhalb unseres Channels können wir Ausgaben der Konsole protokollieren.
Wir benötigen nun ein Objekt aus der Library “JSch”, um eine Verbindung aufbauen zu können:
// Erstelle das benötigte Objekt, um eine SSH-Verbindung aufbauen zu können. JSch jsch = new JSch();
Nun können wir dieses Objekt mit den Verbindungs- und Nutzerdaten füttern und eine Verbindung aufbauen. Der Einfachheit halber verzichten wir auf die Verifizierung des Keys. Damit prüfen wir nicht mehr auf Man-In-Middle Angriffe, aber haben es deutlich einfacher eine Verbindung zu öffnen. Im heimischen Netzwerk ist die Gefahr eines solchen Angriffes ohnehin sehr gering.
// Füttere das Objekt mit allen nötigen Informationen, um eine Verbindung aufbauen zu können. Session session = jsch.getSession(username, hostname, port); session.setPassword(passwort); // Umgehe das Abgleichen nach dem richtigen Key. (Es wird nun eine Man-In-Middle Attacke nicht mehr abgefangen) Properties prop = new Properties(); prop.put("StrictHostKeyChecking", "no"); session.setConfig(prop); // Stelle eine Verbindung her. session.connect();
Da wir idealerweise bereits erfolgreich eine Verbindung zu unserem Raspberry geöffnet haben können wir mit den erhaltenen Daten einen Channel öffnen:
// Erstelle ein neues Objekt, für einen neuen SSH Channel. ChannelExec channelssh = (ChannelExec) session.openChannel("exec"); // Füttere den Channel mit dem Befehl und schicke den Befehl ab. channelssh.setCommand( befehl ); channelssh.connect();
Eigentlich sind wir bereits fertig.
Doch der Sicherheit und der Performance zu Liebe sollten wir den bestehenden Channel und die bestehende Verbindung noch schließen.
// Beende alle Verbindungen. channelssh.disconnect(); session.disconnect();
Aber Achtung!
Wir überprüfen bei der Verbindung über SSH nicht, ob die Keys stimmen.
Wir können so also keinen Man-in-the-Middle Angriff abfangen.
Des Weiteren sollte unser Raspberry Pi am besten eine statische IP haben.
Konsolenausgabe protokollieren:
Zwar ist unsere Funktion vom Prinzip her fertig, doch haben wir oben definiert, dass wir ein Array zurückgeben wollen.
Dieses Array soll alle Zeilen der Konsolenausgabe der Reihe nach enthalten.
An diesem Punkt wird es wieder etwas Tricky.
Wir müssen nämlich während der Protokollierung auf mehrere Fehler gleichzeitig prüfen.
Ist die Zeile leer? Wurde die Zeile bereits protokolliert? Wurden wir ausgeloggt? Ist die Verbindung abgebrochen? Ist der Befehl beendet?
Ich werde hier an dieser Stelle nicht großartig auf den Code eingehen, sondern nur zur Verfügung stellen. Er ist mit Kommentaren versehen.
Unterhalb von “JSch jsch = new JSch();” einfügen:
// Bereite ein paar Variablen vor, um Ausgaben der Konsole auslesen zu können. byte[] buffer = new byte[1024]; ArrayListlines = new ArrayList<>();
Zwischen “channelssh.connect();” und “channelssh.disconnect();” einfügen:
// Fange an die Ausgaben der Konsole auszulesen. try { InputStream in = channelssh.getInputStream(); String line = ""; // Lese alle Ausgaben aus, bis der Befehl beendet wurde oder die Verbindung abbricht. while (true) { // Schreibe jede Zeile der Konsolenausgabe in unser Array. while (in.available() > 0) { int i = in.read(buffer, 0, 1024); // Brich die Protokollierung der Ausgabe, für diese Zeile, ab, wenn die Ausgabe leer sein sollte. if (i < 0) { break; } line = new String(buffer, 0, i); lines.add(line); } // Wir wurden ausgeloggt. if (line.contains("logout")) { break; } // Befehl beendet oder Verbindung abgebrochen. if (channelssh.isClosed()) { break; } // Warte einen kleinen Augenblick mit der nächsten Zeile. try { Thread.sleep(1000); } catch (Exception ee) {} } }catch (Exception e){}
Unterhalb von “session.disconnect();” einfügen:
// Gib die Ausgabe zurück return lines;
Funktion starten:
Eine Kleinigkeit fehlt uns noch zu vollkommenem Glück.
Wie auch die Programmierung der eigentlichen Funktion etwas Tricky war, ist auch die Programmierung des Funktionsstarts etwas umständlicher.
Ich sollte etwas weiter ausholen.
Wir bewegen uns hier in einer Programmierung die Zeit benötigt.
Je nach Internetverbindung benötigen wir Zeit eine Verbindung aufzubauen.
Wir benötigen zusätzlich Zeit einen Channel zu öffnen.
Wie sollte es anders sein, benötigen wir wahrscheinlich noch Sekunden dafür die Verbindungen wieder zu schließen.
Ganz zu schweigen von der Performance des Raspberry Pi und der darauf ausgeführten Befehle.
Führen wir die Funktion “sshBefehl” auf unserem Smartphone ganz normal aus, so müssen wir davon ausgehen, dass es Sekunden bis Minuten dauern wird bis es wieder reagiert.
Was können wir nun tun?
Ganz einfach!
Wir führen unsere Funktion in einem BackgroundThread aus.
Wir erstellen also eine Hintergrundaufgabe, um das Einfrieren unseres Smartphones zu verhindern.
// Hintergrundaufgabe erstellen new AsyncTask() { @Override protected Void doInBackground(Integer... params) { try { // Funktion ausführen und Konsolenausgabe in "lines" speichern. ArrayList lines = sshBefehl("Nutzername", "Passwort", "IP-Adresse", Port, "Befehl"); // Alle Zeilen der Konsolenausgabe in den Android Logs ausgeben. while(!lines.isEmpty()){ Log.e("Rückgabe", lines.get(0)); lines.remove(0); } } catch (Exception e) { e.printStackTrace(); } return null; } }.execute(1);
Wie geht es weiter ?
Wir haben es ja bereits geschafft eine komplette Funktion zu schreiben, welche es uns erlaubt längere Befehle auf unserem SSH Server auszuführen.
Bisher ist die Funktion aber eher etwas zusammenhangslos gestaltet und ohne zugehörige App.
Auf den nächsten Seiten dieses Artikels werden wir zusammen die zugehörige App dazu schreiben.
Eine fertige App werde ich ebenfalls im Google PlayStore veröffentlichen.
Ich hoffe ihr lasst euch nicht entmutigen.
Wir haben bis dato die Grundlegenden Funktionen programmiert, bisher fehlt es unserer App noch an allem Anderen.
Wir wollen nun alle weitere Elemente zu unserer App hinzufügen.
Klappt alles nach Plan, dann werden wir am Ende der Artikelreihe eine fertige SSH-Fernsteuerung vorliegen haben.
Vorerst sollte ich vielleicht erklären wie ich mir das Ganze gedacht habe.
- Wir sollten in unserer App Verbingungsdaten zu unserem Raspberry Pi eingeben können.
- Wir sollten beliebig viele Befehle speichern können.
- Für jeden gespeicherten Befehl sollte ein Knopf angezeigt werden, der bei Druck den Befehl auf dem Raspberry Pi ausführt.
Unsere Vorgehensweise als erstes die Hauptfunktion zu schreiben ist vielleicht nicht ganz konventionell, aber jetzt können wir uns ganz in Ruhe der App drumherum widmen. Folgende Komponenten müssen Programmiert werden:
- Anzeige der Liste der gespeicherten Befehle.
- Einstellungen/Verbindungsdaten ändern.
- Neue Befehle hinzufügen.
- Datenbank, um Befehle speichern zu können.
Datenbank:
Vielleicht sollten wir mit der Datenbank beginnen.
Wir benötigen sie, um Befehle unterspeichern und wieder anzeigen lassen zu können. Relevant sind lediglich 3 Spalten der Datenbank. Eine ID, einen Titel für den Befehl und den eigentlichen Befehl in Textform.
//Erstes Konstrukt der Datenbank: public class DB_Befehle extends SQLiteOpenHelper { //Version und Datenbanksname definieren (Sobald hier etwas geändert wird, wird die Datenbank in der App neu erstellt.) public static final int DB_VERSION = 1; public static final String DB_NAME = "befehle.db"; //Vorschrift, wie die Tabelle dieser Datenbank auszusehen hat private static final String DB_TABELLE = "CREATE TABLE Befehle (Row integer primary key, ID INTEGER, Befehl TEXT, Titel TEXT)"; //Konstruktor public DB_Befehle(Context context) { super(context, DB_NAME, null, DB_VERSION); } //Erstelle die neue Datenbank, beim Start der App public void onCreate(SQLiteDatabase db) { db.execSQL(DB_TABELLE); } //Lösche die alte Datenbank und erstelle eine Neue, falls sich die Version ändert public void onUpgrade(SQLiteDatabase db, int alteVersion, int neueVersion) { db.execSQL("DROP TABLE IF EXISTS Befehle"); onCreate(db); } //Hier sinnlos, sonst die Wiederherstellung einer älteren Datenbanksversion public void onDowngrade(SQLiteDatabase db, int alteVersion, int neueVersion) { onUpgrade(db, alteVersion, neueVersion); } //Neuen Datenbankeintrag speichern public void addBefehl(int ID, String titel, String befehl){ //Öffne die Datenbank, als beschreibbar SQLiteDatabase db = this.getWritableDatabase(); //Erstelle einen Eintrag und befülle ihn mit den Daten ContentValues eintrag = new ContentValues(); eintrag.put("ID", ID); eintrag.put("Titel", titel); eintrag.put("Befehl", befehl); //Füge den Eintrag zur Datenbank hinzu db.insert("Befehle", null, eintrag); } //Alle Daten der Datenbank auslesen, indem man eine Liste an Objekten des Typs Befehl erstellt public ArrayListgetAlle(){ //Erstelle eine Liste und ein Objekt b, welches wir später in die Liste einfügen Befehl b; ArrayList array_list = new ArrayList<>(); //Öffne lesbar die Datenbank SQLiteDatabase db = this.getReadableDatabase(); //Lese alle Datenbankeinträge, sortiert nach der ID absteigend Cursor res = db.rawQuery( "select * from Befehle order by ID asc", null ); //Gehe zum ersten Datenbankeintrag res.moveToFirst(); while(!res.isAfterLast()){ //Erstelle a neu, um es zu beschreiben und anschließend eine Kopie von a in die Liste zu kopieren b = new Befehl(); //Fütter das Objekt a mit den nötigen Informationen b.titel = res.getString(res.getColumnIndex("Titel")); b.befehl = res.getString(res.getColumnIndex("Befehl")); b.ID = res.getInt(res.getColumnIndex("ID")); //Kopiere b in die Liste und gehe einen Datenbankeintrag weiter array_list.add(b); res.moveToNext(); } //Gib die Liste mit den verschiedenen Kopien von b zurück res.close(); return array_list; } }
Wir haben hiermit jetzt eine komplette Klasse DB_Befehle.java, für die Speicherung der Befehle.
Über die Funktionen “addBefehl” und “getAlle” können wir neue Befehle einspeichern oder auslesen.
Die Funktion “getAlle” gibt uns eine Liste der Klasse “Befehl” wieder. In dieser Klasse sind alle 3 Spalten der Datenbank, für jeweils eine Zeile, enthalten.
Wir müssen nun noch eine Klasse “Befehl.java” erstellen, damit diese die Informationen auch speichern kann.
class Befehl{ public int ID; public String titel; public String befehl; }
Super! Nun gibt es auch eine kleine “Datenbank” im Hintergrund, mit Hilfe derer wir alle Befehle speichern können. Auf der nächsten Seite werden wir uns so langsam aber sicher an das Design der App wagen.
Wir haben bereits unsere Hauptfunktion geschrieben und eine Datenbank im Hintergrund besitzen wir ebenfalls.
Zeit sich um das Aussehen der App zu kümmern.
Auf dieser Seite werden wir uns mit der Startseite unserer App beschäftigen.
Wir werden alles Nötige vorbereiten und Programmieren, damit wir eine Liste von gespeicherten Befehlen angezeigt bekommen.
Design:
Bevor wir beginnen können eine Funktion zu schreiben, welche es es uns erlaubt alle gespeicherten Befehle anzuzeigen, ist es von Nöten ein Design zu erstellen, welches alle Befehle anzeigen kann. Klingt logisch. Erst einmal benötigen wir also eine Liste, im Anschluss können wir beginnen diese Liste zu füllen.
Wir öffnen die Datei “MainActivity.xml” und ersetzen die TextView “Hello World!” durch eine ListView mit der ID “befehlsliste”:
<!--?xml version="1.0" encoding="utf-8"?-->
<!--
~ Copyright 2017 www.staticfloat.de
~
~ Licensed under the Apache License, Version 2.0 (the „License“);
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an „AS IS“ BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<relativelayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingbottom="@dimen/activity_vertical_margin" android:paddingleft="@dimen/activity_horizontal_margin" android:paddingright="@dimen/activity_horizontal_margin" android:paddingtop="@dimen/activity_vertical_margin" tools:context="de.staticfloat.quickssh.MainActivity">
<listview android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/befehlsliste"></listview>
</relativelayout>
Anschließend gilt es ein Design für jeden einzelnen Eintrag zu gestalten.
Jeder Eintrag soll einen Namen tragen und einen Knopf, zum Ausführen des Befehls, zeigen.
Wir erstellen eine neue Datei in res -> layout und nennen sie “befehlsliste_eintrag.xml”.
Der Inhalt sähe so oder so ähnlich aus. Euch ist hier jegliche Freiheit gegeben:
<!--?xml version="1.0" encoding="utf-8"?-->
<!--
~ Copyright 2017 www.staticfloat.de
~
~ Licensed under the Apache License, Version 2.0 (the „License“);
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an „AS IS“ BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<linearlayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
<textview android:layout_width="wrap_content" android:layout_height="wrap_content" android:textappearance="?android:attr/textAppearanceMedium" android:text="Titel des Befehls" android:id="@+id/befehlsTitel"></textview>
<button style="?android:attr/buttonStyleSmall" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Befehl ausführen" android:id="@+id/befehlsButton"></button>
</linearlayout>
Liste füllen:
Nun haben wir bereits ein Design erstellt.
Befüllen wir es mit Inhalt!
Zum Füllen der eben erstellten Liste (ListeView) benötigen wir 3 Dinge:
- Einen Adapter, der die Anzeige der einzelnen Einträge übernimmt.
- Daten, die wir in die Liste “stopfen” wollen.
- Eine Funktion, die den Adapter auf das erstellte ListView aufschaltet und die Daten an den Adapter übergibt.
Das klingt im ersten Moment vielleicht etwas konfus. Ich versuche im weiteren Verlauf des Artikels aber Klarheit zu schaffen.
Nun denn, auf gehts mit dem Adapter!
Hier kann ich eigentlich relativ wenig zu erzählen.
Die Grundform des Adapters immer ähnlich aussieht.
Wir erstellen uns eine Klasse “BefehlsAdapter”:
public class BefehlsAdapter extends BaseAdapter { private ArrayListbefehle; public BefehlsAdapter(Context c, ArrayList befehle){ // Die Liste der übergebenen Befehle in einer Variable speichern this.befehle = befehle; // Hier werden Berechnungen durchgeführt und Variablen vorbereitet, die nur beim Setzen des Adapters von Nöten sind. } @Override public int getCount() { return befehle.size(); } @Override public Object getItem(int arg0) { return null; } @Override public long getItemId(int arg0) { return arg0; } @Override public View getView(int arg0, View arg1, ViewGroup arg2) { // Hier behandeln wir die Anzeige der einzelnen Einträge. } }
Und schon steht unser Grundgerüst für den Adapter.
Erfüllen wir die Klasse mit Leben!
Unter “private ArrayList
private LayoutInflater eintragInflater; private String nutzername; private String passwort; private String ip; private int port; private SharedPreferences pref; private Context cxt;
Anschließend befüllen wir die variablen mit ihrem benötigten Inhalt. Wir fügen an die Stelle “// Hier werden Berechnungen durchgeführt und Variablen vorbereitet, die nur beim Setzen des Adapters von Nöten sind.” ein:
// Bekomme das ListView übergeben, in dass die Einträge geschreiben werden sollen eintragInflater = LayoutInflater.from(c); // Den Context benötigen wir für diverse Funktionsaufrufe. cxt = c; //shared data vorbereiten pref = c.getSharedPreferences("QuickSSH", 0); //Verbindungsdaten laden nutzername = pref.getString("nutzername", ""); passwort = pref.getString("passwort", ""); ip = pref.getString("ip", ""); port = pref.getInt("port", 22);
Alles ist vorbereitet, um die Klasse “BefehlsAdapter” die Befehle anzeigen zu lassen.
Wir ersetzen “// Hier behandeln wir die Anzeige der einzelnen Einträge.” durch:
// Lese das richtige Design der ListeView Einträge aus. LinearLayout befehlsLayout = (LinearLayout) eintragInflater.inflate(R.layout.befehlsliste_eintrag, arg2, false); // Lese die zu verändernden Elemente eines Eintrages aus. TextView befehlsTitel = (TextView) befehlsLayout.findViewById(R.id.befehlsTitel); TextView befehlsButton = (Button) befehlsLayout.findViewById(R.id.befehlsButton); // Lese den Befehl an der Stelle "arg0" aus. final Befehl momentanerBefehl = befehle.get(arg0); // Beschreibe das Element des Titels mit dem Titel des momentanen Befehls. befehlsTitel.setText(momentanerBefehl.titel); // Reagiere auf einen Klick und führe unsere hauptfunktion aus, welche in der Klasse "MainActivity" steht. befehlsButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(!nutzername.equals("") && !passwort.equals("") && !ip.equals("")) MainActivity.executeBefehl(nutzername, passwort, ip, port, momentanerBefehl.befehl); else Toast.makeText(cxt, "Es wurden keine Verbindungsdaten angegeben.", Toast.LENGTH_SHORT).show(); } }); // Vollende die Anzeige befehlsLayout.setTag(arg0); return befehlsLayout;
Supi, fast haben wir es geschafft.
Unser Adapter ist vollendet und auf unsere Datenbank sollten wir uns auch verlassen können. Nun wechseln wir zurück ind die Klasse “MainActivity” und füge die Funktion hinzu, welche es uns erlaubt den Adapter einer ListView zuzuordnen und den Adapter mit den Daten der Datenbank füttert.
Wir schreiben in die Funktion “onCreate”:
// Datenbank initialisieren. DB_Befehle DB_b = new DB_Befehle(this); // Alle Befehle aus der Datenbak auslesen und in eine Variable schreiben. ArrayListbefehlsListe = DB_b.getAlle(); // Adapter mit allen gefundenen Befehlen der Datenbank füttern. BefehlsAdapter befehlsAdapter = new BefehlsAdapter(this, befehlsListe); // Adapter auf das ListView aufschalten. ListView liedListView = (ListView)findViewById(R.id.befehlsliste); liedListView.setAdapter(befehlsAdapter);
Weiter möchte ich gerne einen weiteren Teilbereich der App ansprechen.
Dem Hinzufügen von Befehle in die Datenbank und dem Bereich der Eingabe.
Wir haben unsere App bereits in sofern fertiggestellt, als dass es uns nun möglich ist Befehle in der Datenbank zu speichern, unsere Befehle auf der Startseite anzuzeigen und bei einem Klick den Befehl auszuführen. Leider fehlt bis dato noch eine Möglichkeit innerhalb der App neue Befehle in die Datenbank ein zu pflegen.
Speicherung der Befehle:
Beginnen wir mit einer einfachen Funktion, welche wir aufrufen, um die Befehle zu speichern.
Wenn wir uns an die zweite Seite zurück erinnern, dann wissen wir, dass unsere Datenbank drei Dinge speichert. Die ID, den Titel und den eigentlichen Befehl.
Wir sollten also eine Funktion erstellen, der wir zwei dieser drei Dinge übergeben können und die anschließend die Speicherung in der Datenbank übernimmt. Wir sollten den Titel und den Befehl übergeben. Die ID können wir innerhalb der Funktion dynamisch generieren.
public void speichereBefehl(String titel, String befehl){ // Übernehme hier die Speicherung des Befehls }
Die ID des Befehls generieren wir aus der momentanen Zeit in Sekunden.
Es ist relativ unwahrscheinlich, bis unmöglich, mehrere Befehle zur gleichen Sekunde in die Datenbank zu schreiben, so dass zwei Befehle die gleiche ID zugewiesen bekommen.
// Lese die Zeit in Millisekunden aus und teile durch 1000, um auf Sekunden zu gelangen. int id = (int) (System.currentTimeMillis() / 1000);
Anschließend gilt es innerhalb der “speichereBefehl” Funktion zu prüfen, ob die Eingabefelder leer waren, und anschließend die Eingaben zu speichern. Falls eines der Eingabefelder leer war, geben wir eine Fehlermeldung aus.
if(!titel.equals("") && !befehl.equals("")){ DB_Befehle DB_e = new DB_Befehle(MainActivity.this); DB_e.addBefehl(id, titel, befehl); // Gehe bei erfolgreicher Speicherung zur Startseite zurück zeigeStart(); }else{ // Fehlermeldung ausgeben, falls eines der Felder frei war. Toast.makeText(MainActivity.this, "Es scheinen noch Felder frei zu sein.", Toast.LENGTH_SHORT).show(); }
Design:
Supi, wir haben unsere Funktion, zum Speichern der Befehle.
Es fehl nun noch eine Funktion zum Aufrufen der Eingabeseite, das Aussehen der Seite und eine Funktion, um auf den “Speichern”-Knopf zu reagieren. Zunächst müssen wir jedoch eine Design erstellen, welches wir dann anschließend anzeigen lassen können.
Wir erstellen im Ordner res -> layout eine neue Datei mit dem Namen “activity_neuer_befehl.xml” und fügen ein:
<!--?xml version="1.0" encoding="utf-8"?-->
<!--
~ Copyright 2017 www.staticfloat.de
~
~ Licensed under the Apache License, Version 2.0 (the „License“);
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an „AS IS“ BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<linearlayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
<textview android:layout_width="wrap_content" android:layout_height="wrap_content" android:textappearance="?android:attr/textAppearanceSmall" android:text="Neuen Befehl hinzufügen:" android:id="@+id/textView2" android:layout_margin="10dp"></textview>
<edittext android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/edittext_titel" android:layout_marginright="10dp" android:layout_marginleft="10dp" android:hint="Titel:"></edittext>
<edittext android:layout_width="match_parent" android:layout_height="wrap_content" android:inputtype="textMultiLine" android:ems="10" android:id="@+id/edittext_befehl" android:layout_marginleft="10dp" android:layout_marginright="10dp" android:lines="5" android:hint="Befehl:"></edittext>
<button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hinzufügen" android:id="@+id/button_hinzufuegen" android:layout_gravity="right" android:layout_marginright="16dp" android:background="@color/colorPrimary" android:textcolor="@android:color/white" android:padding="10dp"></button>
</linearlayout>
Anzeige & Eingabe der Befehle:
Weiter gehts!
Unser Design steht.
Machen wir uns daran die Eingabeseite auch anzeigen zu können und auf den Klick des “Speichern”-Knopfes zu reagieren.
Dazu wechseln wir in die Datei “MainActivity.java” und erstellen eine neue Funktion “zeigeNeuerBefehl()”, als void.
public void zeigeNeuerBefehl(){ // Zeige unser eben erstelles layout an. setContentView(R.layout.acitivity_neuer_befehl); // Reagiere auf einen Klick auf den "Speichern"-Knopf. Button hinzufuegen_button = (Button) findViewById(R.id.button_hinzufuegen); if(hinzufuegen_button != null){ hinzufuegen_button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Erstelle Variablen, um die Textfeldeingabe zwischenzuspeichern. String titel = ""; String befehl = ""; // Lese den Titel aus und speichere ihn zwischen. EditText edittext_titel = (EditText) findViewById(R.id.edittext_titel); if(edittext_titel != null){ titel = edittext_titel.getText().toString(); } // Lese den befehl aus und speichere ihn zwischen. EditText edittext_befehl = (EditText) findViewById(R.id.edittext_befehl); if(edittext_befehl != null){ befehl = edittext_befehl.getText().toString(); } // Rufe die Funktion auf, welche die Speicherung des befehl übernimmt. speichereBefehl(titel, befehl); } }); } }
Übrigens:
Damit nach dem Hinzufügen eines neuen Befehles die Startseite wieder angezeigt wird sollten wir den Teil, den wir im zweiten Artikel in die Funktion “onCreate” geschrieben haben in eine neue Funktion “zeigeStart” verschoben werden. Zusätzlich muss an der alten Stelle des Codes in “onCreate” nun “zeigeStart();” stehen.
Sie sieht etwas so aus:
@Override protected void onCreate(Bundle savedInstanceState) { [...] zeigeStart(); } // Zeige die Startseite an. public void zeigeStart(){ setContentView(R.layout.activity_main); // Datenbank initialisieren. DB_Befehle DB_b = new DB_Befehle(this); // Alle Befehle aus der Datenbak auslesen und in eine Variable schreiben. ArrayListbefehlsListe = DB_b.getAlle(); // Adapter mit allen gefundenen Befehlen der Datenbank füttern. BefehlsAdapter befehlsAdapter = new BefehlsAdapter(this, befehlsListe); // Adapter auf das ListView aufschalten. ListView liedListView = (ListView)findViewById(R.id.befehlsliste); liedListView.setAdapter(befehlsAdapter); }
Im 5. Teil des Artikels klären wir den Bereich der Eingabe der Verbindungsdaten, denn an eben diesem letzten Bereich fehlt es unserer App noch.
Bereits in Teil 3 haben wir bisher unbeschriebene Verbindungsdaten ausgelesen und einen Eintrag im Speicher für die verbindungsdaten erstellt. In Teil 1, verwenden wir diese Daten ebenfalls, indem wir sie an die Funktion “sshBefehl” weitergeben.
Schön und gut, doch können wir sie bisher nicht ändern, geschweige denn definieren.
Design:
Wieder einmal steht das Erstellen eines neuen Designs an erster Stelle, bei unserem Artikel.
Wir erstellen die Datei namens “activity_einstellungen.xml” im Ordner res -> layout.
In dieser Datei definieren wir das Layout für unsere Einstellungsseite. Diese Seite soll 4 Textfelder und einen Knopf zum speichern enthalten. Als Eingaben benötigen die IP-Adresse unseres Raspberry’s, den Port, unseren Nutzernamen und das Passwort eben dieses Accounts.
Dementsprechend wäre es hilfreich die Textfelder nach diesen Eingaben zu benennen.
<!--?xml version="1.0" encoding="utf-8"?-->
<!--
~ Copyright 2017 www.staticfloat.de
~
~ Licensed under the Apache License, Version 2.0 (the „License“);
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an „AS IS“ BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<linearlayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
<textview android:layout_width="wrap_content" android:layout_height="wrap_content" android:textappearance="?android:attr/textAppearanceSmall" android:text="Verbindungsdaten:" android:id="@+id/textView" android:layout_margin="10dp"></textview>
<edittext android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/edittext_ip" android:hint="IP-Adresse:" android:layout_marginleft="10dp" android:layout_marginright="10dp"></edittext>
<edittext android:layout_width="match_parent" android:layout_height="wrap_content" android:inputtype="number" android:ems="10" android:id="@+id/edittext_port" android:hint="Port: (Normalerweise 22)" android:layout_marginleft="10dp" android:layout_marginright="10dp"></edittext>
<edittext android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/edittext_nutzername" android:hint="Nutzername:" android:layout_marginleft="10dp" android:layout_marginright="10dp"></edittext>
<edittext android:layout_width="match_parent" android:layout_height="wrap_content" android:inputtype="textPassword" android:ems="10" android:id="@+id/edittext_passwort" android:hint="Passwort:" android:layout_marginright="10dp" android:layout_marginleft="10dp"></edittext>
<button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Speichern" android:id="@+id/button_speichern" android:layout_gravity="right" android:layout_marginright="16dp" android:background="@color/colorPrimary" android:textcolor="@android:color/white" android:padding="10dp"></button>
</linearlayout>
Ich war so fei und habe direkt Hints zu unseren Textfeldern hinzugefügt.
Diese Hints sind dazu da zu beschreiben, für was das Textfeld genutzt wird. Hints verschwinden automatisch, sobald der Nutzer eine erste Eingabe gemacht hat.
Speicherung:
Da wir unser Design nun definiert haben können wir beginnen eben dieses anzeigen zu lassen und beim Klick auf “Speichern” die Eingaben auch zu protokollieren/speichern.
Ich lagere die Anzeige in eine externe Funktion “zeigeEinstellungen” aus:
// Zeigt die Einstellungsseite an. public void zeigeEinstellungen(){ setContentView(R.layout.activity_einstellungen); [ ... ] }
Nachfolgend müssen wir die Textfelder und den Button in Java registrieren und in eine variable speichern.
Eine kleine Fehlerabfrage, ob die Elemente auch existieren, wäre auch ganz schön.
// Den Textfeldern Namen geben, um den Inhalt schreiben und auslesen zu können. final EditText edittext_nutzername = (EditText) findViewById(R.id.edittext_nutzername); final EditText edittext_ip = (EditText) findViewById(R.id.edittext_ip); final EditText edittext_passwort = (EditText) findViewById(R.id.edittext_passwort); final EditText edittext_port = (EditText) findViewById(R.id.edittext_port); Button speichern_button = (Button) findViewById(R.id.button_speichern); // Fehlerabfrage if(edittext_ip != null && edittext_nutzername != null && edittext_passwort != null && edittext_port != null && speichern_button != null) { [...] }
Der Nutzerfreundlichkeit halber könne wir nun noch versuchen die alten Einstellungen und Verbindungsdaten in die noch leeren Textfelder zu schreiben.
// Textfelder mit bereits bestehenden Einstellungen befüllen. edittext_ip.setText(pref.getString("ip", "")); edittext_nutzername.setText(pref.getString("nutzername", "")); edittext_passwort.setText(pref.getString("passwort", "")); edittext_port.setText(pref.getInt("port", 22) + ""); [...]
Wir können nun auf den Speichern-Knopf reagieren, die Eingaben auf Fehler überprüfen und abspeichern.
// Auf den Klick, auf den Knopf "Speichern" reagieren. speichern_button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Variablen vorbereiten, um den Inhalt der Textfelder unterspeichern zu können. String nutzername = ""; String passwort = ""; String ip = ""; String port = ""; // Inhalt der Textfelder in Variablen schreiben. nutzername = edittext_nutzername.getText().toString(); ip = edittext_ip.getText().toString(); passwort = edittext_passwort.getText().toString(); port = edittext_port.getText().toString(); if(!port.equals("") && !ip.equals("") && !nutzername.equals("") && !passwort.equals("")){ // Verbindungsdaten laden editor.putString("nutzername", nutzername); editor.putString("passwort", passwort); editor.putString("ip", ip); editor.putInt("port", Integer.parseInt(port)); // Einstellungen speichern editor.commit(); // Gehe bei erfolgreicher Speicherung zur Startseite zurück zeigeStart(); }else{ // Fehlermeldung ausgeben, falls eines der Felder frei war. Toast.makeText(MainActivity.this, "Es scheinen noch Felder frei zu sein.", Toast.LENGTH_SHORT).show(); } } });
Fast haben wir es geschafft! Wirklich!
Das war jetzt aber auch ein Haufen Arbeit, aber ich bin mir sicher, dass es sich lohnen wird!
Wollen wir uns nun auf der letzten Seite der Erstellung einer fertigen Raspberry Pi Fernsteuerung’s App widmen und damit die App komplettieren.
Bisher haben wir alle dringend nötigen Funktionen, Bereiche und Speichermöglichkeiten implementiert.
Was allerdings noch an unserem Vorhanden fehlt ist das Zusammenfügen der einzelnen Bereiche und Funktionen zu einem Ganzen.
Wir benötigen nun also die Navigation, um zwischen den einzelnen Bereichen wechseln zu können.
Dazu verwenden wir ein OptionMenü. Auf gut Deutsch bedeutet dies, dass wir oben rechts drei Punkte angezeigt bekommen, über die wir die einzelnen Bereiche ansteuern können. Existiert eine Menütaste an dem Smartphone, dann wird die Navigation darüber geöffnet.
Hilfreich wäre es vielleicht auch, wenn wir eine Nachricht angezeigt bekommen, wenn noch keine Verbindungsdaten angegeben sind.
OptionMenü:
Vorerst sollten wir mit dem größeren Vorhaben beginnen.
Wir benötigen ein paar Einträge in unserer Strings.xml Datei, welche die Texte unserer App enthält. Sie liegt im Ordner res -> values.
<!-- Texte für unser Menü -->
<string name="menu_start">Startseite</string>
<string name="menu_ueber">Über</string>
<string name="menu_einstellungen">Einstellungen</string>
<string name="menu_neuer_befehl">Neuer Befehl</string>
Nun, da wir unsere Texte definiert haben, erstellen wir eine neue Datei im Ordner res -> menu, mit dem Namen “actionbar_menu.xml”.
Existiert dieser Ordner wiedererwartend nicht, dann erstellen wir auch diesen.
In dieser Datei werden wir definieren, wie unser OptionMenü aufgebaut ist und die eben erstellten Texte wiederverwenden.
<!--?xml version="1.0" encoding="utf-8"?-->
<!--
~ Copyright 2017 www.staticfloat.de
~
~ Licensed under the Apache License, Version 2.0 (the „License“);
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an „AS IS“ BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- Menüpunkt, der angezeigt wird, wenn auf die drei Punkte in der Actionbar geklickt wird. -->
<item android:id="@+id/menu_start" android:title="@string/menu_start" app:showasaction="never"></item>
<item android:id="@+id/menu_neuer_befehl" android:title="@string/menu_neuer_befehl" app:showasaction="never"></item>
<item android:id="@+id/menu_einstellungen" android:title="@string/menu_einstellungen" app:showasaction="never"></item>
<item android:id="@+id/menu_ueber" android:title="@string/menu_ueber" app:showasaction="never"></item>
</menu>
Anschließend wechseln wir wieder in die Hauptklasse “MainActivity”.
Hier werden wir der App nun angeben, dass es ein OptionMenü gibt, wie dieses aussehen soll und welche Aktionen beim Klick auf einen der Menüeinträge geschehen soll.
// Definiert, dass es ein OptionMenü gibt und wie dieses aussehen soll. (Nach Vorlage der Datei "actionbar_manu.xml") @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.actionbar_menu, menu); return true; }
Was nun in letztem Schritt noch fehlt ist die Funktion, die auf den Klick eines Menüeintrages reagiert und uns dann schließlich die entsprechenden Bereiche anzeigt.
@Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_start: // Dieser Teil (bis zum "return") wird ausgeführt, wenn wir auf den Menüpunkt mit der ID "menu_start" klicken. zeigeStart(); return true; case R.id.menu_ueber: // Dieser Teil (bis zum "return") wird ausgeführt, wenn wir auf den Menüpunkt mit der ID "menu_ueber" klicken. return true; case R.id.menu_einstellungen: // Dieser Teil (bis zum "return") wird ausgeführt, wenn wir auf den Menüpunkt mit der ID "menu_einstellungen" klicken. zeigeEinstellungen(); return true; case R.id.menu_neuer_befehl: // Dieser Teil (bis zum "return") wird ausgeführt, wenn wir auf den Menüpunkt mit der ID "menu_neuer_befehl" klicken. zeigeNeuerBefehl(); return true; default: // Standardfunktion. Sollte bestehen bleiben. return super.onOptionsItemSelected(item); } }
Für den Menüpunkt “Über” habe ich mir etwas ganz ausgefallenes einfallen lassen.
Das Ganze nennt sich DialogFragment und zeigt uns eine Art Pop-Up mit dem vorher eingestellten Text und den eingestellten Knöpfen an.
//Kleine Klasse zum Erstellen eines Dialogs public static class UeberDialog extends DialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { //Dialog erstellen AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); //Befülle Dialog mit dem Text "ueber_text" aus der Datei res -> strings.xml builder.setMessage(R.string.ueber_text) //Erstelle einen Button mit dem Text "ok" aus der Datei res -> strings.xml .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { //Schließe das Dialogfeld, beim Klick auf den erstellten Button dismiss(); } }); //Gib das erstellte Dialogfeld an die aufrufende Klasse zurück, um es anzeigen zu können. return builder.create(); } }
In die Funktion onOptionMenuSelected schreiben wir unter den Fall “case R.id.menu_ueber”:
DialogFragment newFragment = new UeberDialog(); newFragment.show(this.getFragmentManager(), "Über");
Damit wird das Pop-Up geöffnet. Der Titel ist das “Über”.
Benachrichtigung:
Wie bereits oben angesprochen haben wir ja den Plan eine Benachrichtigung anzeigen zu lassen, falls noch keine Verbindungsdaten angegeben sind.
Um dies zu realisieren gehen wir ans Ende der “onCreate” Funktion und fügen den Funktionsaufruf von “benachrichtige();” ein.
Damit wir nicht in einen Fehler laufen, müssen wir diese Funktion selbstverständlich auch definieren.
// Zeigt eine Benachrichtigung an, falls zu dem Zeitpunkt des Starts noch keine Verbindungsdaten angegeben wurde. public void benachrichtige(){ // Prüft, ob eines der Felder für die Verbindungsdaten unvollständig ist. if(pref.getInt("port", 0) == 0 || pref.getString("ip", "").equals("") || pref.getString("passwort", "").equals("") || pref.getString("nutzername", "").equals("")) { Toast.makeText(MainActivity.this, "Verbindungsdaten unvollständig. Menü -> Einstellungen", Toast.LENGTH_LONG).show(); } }
Nachwort:
Puhh, da haben wir ja bereits eine gehörige Portion Arbeit hinter uns.
Unsere App sieht vielleicht nicht super schön aus und 1 bis 2 zusätzliche Funktionen sind für die Marktreifen Gebrauch auch noch nötig, doch für den Hausgebrauch ist diese App vollkommen ausreichend.
Ich hoffe ich konntet so Einiges in dieser Artikelreihe lernen und euch ist mit der Funktionsweise geholfen.
Ich wünsche selbstverständlich frohes Programmieren und viel Glück bei dem Raspberry Pi Projekt.
Bitte beachtet die Lizenzen von JSch.
Hallo Marvin!
Ich möchte Dein Projekt gerne übernehmen! Leider wird der Inhalt der xml-Dateien nicht dargestellt. Kannst Du mir da bitte helfen ?
Hallo maksimilian,
vielen lieben Dank für deinen Hinweis. Die entsprechenden Stellen wurden aktualisiert. Ich wünsche viel Spaß beim Coden und Steuern.
Gruß
Marvin