Smarthome App #9: Anpassung an Systemänderungen & Login-Daten speichern


24.03.2017  |  Tutorial, Smarthome App

In den letzten Beiträgen wurde die Struktur des Smarthome-Systems ein wenig angepasst, um es leichter um neue Geräte-Typen erweitern zu können. Da diese Anpassungen nicht unbedeutend in manche Systemteile eingegriffen haben, muss nun auch die Smarthome-App ein wenig angepasst werden. Es sind jedoch nur drei Klassen der App betroffen, nämlich "RoomFragment", "MainActivity" und "Icons". Nachdem die Klassen angepasst wurden, werden noch die Methoden der Klasse "SaveData" implementiert und der Login-Bildschirm optimiert.

MainActivity anpassen

Als erstes öffnest du die Datei "MainActivity.java" im Java-Verzeichnis des Projektes. Da die Struktur der Datenbank optimiert wurde und die Icons der Räume jetzt in der Datenbank abgespeichert werden, muss in der MainActivity beim Erstellen des Hauptmenüs der abgespeicherte Icon des Raumes übergeben werden. Dazu muss in der Methode createRooms() der Konstruktoraufruf angepasst werden. Der zweite Parameter des Konstruktoraufrufs in Zeile 217 wird zu "Icons.getRoomIcon(o.getString("icon"))".
drawerItemList.add(new DrawerItem(o.getString("name"), Icons.getRoomIcon(o.getString("icon")), o.getString("location"), new RoomFragment()));

Icons bearbeiten

Anschließend wird die Datei "Icons.java" geöffnet. Hier wird nun die Methode getRoomIcon() angelegt, die in der MainActivity aufgerufen wurde. Diese Methode liefert den Icon des Raumes des übergebenen Key's zurück:
/**
 * Gibt anhand des übergebenen Key's den passenden Icon zurück
 * @param key
 * @return
 */
public static int getRoomIcon(String key){
    //Methode wird später genauer implementiert
    switch(key){
        default:
            return R.mipmap.ic_launcher;
    }
}

RoomFragment anpassen

Öffne nun die Klasse "RoomData.java", um auch dort ein paar Anpassungen vorzunehmen. Als erstes wird in der Klasse RoomItem die Variable "deviceType" erstellt (Zeile 4), eine Getter-Methode dafür implementiert (Zeile 59 - 65) und der Konstruktor entsprechend angepasst (Zeile 7 & Zeile 15). In dieser neuen Variable wid abgespeichert, von welchem Gerätetypen das Gerät ist. Damit kann das Smarthome-System leichter um neue Geräte erweitert werden. Die Klasse RoomItem sollte nach den Änderungen so aussehen:
public class RoomItem{
	
	//Hier wurde die Variable "deviceType" hinzugefügt
    private String name, device, icon, type, value, deviceType;

	//Hier wurde der Parameter "deviceType" hinzugefügt
    public RoomItem(String name, String device, String icon, String deviceType, String type, String value){
        this.name = name;
        this.device = device;
        this.icon = icon;
        this.type = type;
        this.value = value;
		
		//Hier wurde eine Wertezuweisung hinzugefügt
        this.deviceType = deviceType;
    }

    /**
     * Gibt den Namen des Items zurück
     * @return
     */
    public String getName(){
        return name;
    }

    /**
     * Gibt den Wert des Items zurück
     * @return
     */
    public String getValue(){
        return value;
    }

    /**
     * Gibt den Typ des Items zurück
     * @return
     */
    public String getType(){
        return type;
    }

    /**
     * Gibt den Icon des Items zurück
     * @return
     */
    public String getIcon(){
        return icon;
    }

    /**
     * Gibt das Device des Items zurück
     * @return
     */
    public String getDevice(){
        return device;
    }

	//Hier wurde eine Getter-Methode für den Gerätetypen hinzugefügt
    /**
     * Gibt den Gerätetypen des Items zurück
     * @return
     */
    public String getDeviceType(){
        return deviceType;
    }
}
Als nächstes wird in der Methode loadRoomData() der Aufruf des RoomItem-Konstruktors an die neuen Parameter angepasst. Füge dazu in Zeile 115 & 116 der Datei zwischen "c.getString("icon")" und "c.getString("type")" den Befehl "c.getString("device_type")" ein, damit der Wert übergeben wird. Der Aufruf sollte nun so aussehen:
roomItems.add(new RoomItem(c.getString("name"), c.getString("device"), c.getString("icon"),
        c.getString("device_type"), c.getString("type"), c.getString("value")));
Da ein Gerät nun nichtmehr nur durch seine ID sondern durch seine ID in Verbindung mit seinem Gerätetypen identifiziert wird, müssen die Methodenaufrufe an diese Struktur angepasst werden. In Zeile 231 wird dazu zusätzlich der Wert "ri.getDeviceType()" an die Methode showOverview() übergeben:
showOverview(ri.getDeviceType(), ri.getDevice());
Der Aufruf der Methode setModes() in Zeile 213 wird ebenfalls angepasst. Der Methode wird jetzt statt der ID des Gerätes das ganze RoomItem übergeben. ENtferne dazu einfach ".getDevice()" vom ersten Parameter, damit der Aufruf so aussieht:
setModes(ri, !switchViewHolder.switchView.isChecked());
Anschließend wird in Zeile 161 der Methode showOverview() ein neuer Parameter hinzugefügt, der den Gerätetypen des Gerätes übergeben haben möchte. Füge dazu "String type" zur Parameterliste hinzu:
public void showOverview(String type, String sensor){}
Auch die Methode setModes() in Zeile 153 wird an die neue Struktur angepasst. Statt einem String mit der ID des Gerätes, wird ab jetzt das RoomItem selbst an die Methode übergeben. Außerdem wird der Parameter final gemacht:
public void setModes(final RoomItem item, boolean mode){}

SaveData implementieren

Im nächsten Schritt werden die Methoden der Klasse SaveData implementiert, damit die Login-Daten der Nutzer gespeichert werden können. Dazu wird die Datei "SaveData.java" geöffnet. Hier werden am Anfang der Datei sechs neue Variablen angelegt. Die ersten drei speichern den Nutzernamen, das Passwort und die Server-IP. Die anderen drei Variablen enthalten den Pfad zu den Speicherorten der Dateien für Nutzername, Passwort und Server-IP. Alle Variablen sind private statische Strings.
private static String username = null;
private static String password = null;
private static String serverIp = null;

private static String usernameFile = "uname.ini";
private static String passwordFile = "psw.ini";
private static String serverFile = "serverip.ini";
Die Getter-Methoden zum Abfragen der Nutzerdaten getUsername(), getPassword() und getServerIp() folgen alle dem selben Muster. Als erstes wird readFromFile() mit dem Kontext und dem Pfad der entsprechenden Datei (usernameFile für den Nutzernamen, passwordFile für das Password und serverFile für die Server-IP) aufgerufen. Das Ergebnis dieser Methode wird einem neuen String zugewiesen. Als nächstes wird geprüft, ob dieser String null ist. Wenn nicht, so wird der Wert des Strings der entsprechenden Variable (username für Nutzername, password für Passwort und serverIP für die Server-IP) zugewiesen. Anschließend wird diese Variable zurückgegeben:
/**
 * Gibt den Nutzernamen zurück
 * @param context Kontext der App
 * @return der Nutzername
 */
public static String getUsername(Context context){
    String savedUsername = readFromFile(context, usernameFile);

    if(savedUsername != null){
        username = savedUsername;
    }

    return username;
}

/**
 * Gibt das Password zurück
 * @param context Kontext der App
 * @return das Passwort
 */
public static String getPassword(Context context){
    String savedPassword = readFromFile(context, passwordFile);

    if(savedPassword != null){
        password = savedPassword;
    }

    return password;
}

/**
 * Gibt die IP zurück
 * @param context Kontext der App
 * @return die IP
 */
public static String getServerIp(Context context){
    String savedIp = readFromFile(context, serverFile);

    if(savedIp != null){
        serverIp = savedIp;
    }

    return serverIp;
}
Mit der Methode setLoginData() werden der Nutzername und das Passwort des Nutzers gespeichert. Hier wurde der boolean-Parameter "saveData" hinzugefügt, der angibt, ob die Nutzerdaten über diese Sitzung hinaus gespeichert werden sollen. Damit wird die Methode setSaveLoginData() überflüssig und kann entfernt werden. Im Inneren der wird writeToFile() für den Nutzernamen und das Passwort aufgerufen, falls "saveData" True ist. Anschließend wird in jedem Fall der Variable "username" der Wert des Parameters "uname" und der Variable "password" der Wert des Parameters "pw" zugewiesen:
/**
 * Setzt die Login-Daten des Nutzers
 * @param context Kontext der App
 * @param uname Nutzername
 * @param pw Passwort
 * @param saveData true, wenn Daten gespeichert werden sollen
 *                 false, wenn nicht
 */
public static void setLoginData(Context context, String uname, String pw, boolean saveData){
    if(saveData){
        writeToFile(context, usernameFile, uname);
        writeToFile(context, passwordFile, pw);
    }

    username = uname;
    password = pw;
}

public static void setServerIp(Context context, String serverIp){}
Die Methode getSaveLoginData() kann geprüft werden, ob die Login-Daten des Nutzers gespeichert wurden. Sie erhält als Parameter ein Kontext-Objekt und liefert einen boolean-Wert zurück. In der Methode wird geprüft, ob die Ergebnisse der Methoden getUsername() und getPassword() null sind. Das Ergebnis dieser Prüfung wird zurückgegeben:
/**
 * Gibt zurück, ob die Nutzerdaten gespeichert wurden
 * @param context Kontext der App
 * @return
 */
public static boolean getSaveLoginData(Context context){
    boolean saveLoginData = (getUsername(context) != null && getPassword(context) != null);

    return saveLoginData;
}
Mit der Methode writeToFile() greifen die oben genannten Methoden schreibend auf den Speicherort des entsprechenden Wertes zu und schreiben mit einem FileOutputStream den übergebenen Text in die Datei. Falls eine Exception geworfen wird, wird lediglich der StackTrace ausgegeben. Als Parameter erhält die Methode ein Kontext-Objekt, den Pfad der zu beschreibenden Datei und den zu schreibenden Text:
/**
 * Schreibt den übergebenen Text in die angegebene Datei
 * @param context Kontext der App
 * @param filePath Pfad der Datei
 * @param text der zu schreibende Text
 */
private static void writeToFile(Context context, String filePath, String text){
    try{
        FileOutputStream fos = context.openFileOutput(filePath, Context.MODE_PRIVATE);
        fos.write(text.getBytes());
        fos.close();
    }catch(IOException e){
        e.printStackTrace();
    }
}
Damit gespeicherte Werte auch wieder geladen werden können, gibt es die Methode readFromFile, die als Parameter ein Kontext-Objekt und den Pfad der zu lesenden Datei erhält. Der Rückgabetyp ist String. Am Anfang der Methode wird die Variable "text" erstellt und mit einem leeren String initialisiert. In der Methode wird ein FileInputStream erstellt und mit einem InputStreamReader, der in einen BufferedReader gepackt wurde, gelesen. Dazu wird die Datei mit einer while-Schleife Zeile für Zeile gelesen und mit einem StringBuilder zusammengesetzt. Dieser Wert wird der Variable "text" zugewiesen. Anschließend wird null zurückgegeben, wenn der gelesene Text leer ist. Falls eine Exception geworfen wird, wird der StackTrace ausgegeben und die Variable "text" auf null gesetzt. Am Ende der Methode wird die Variable "text" zurückgegeben:
/**
 * Gibt den Text der ausgegebenen Datei aus
 * @param context Kontext der App
 * @param filePath Pfad der zu lesenden Datei
 * @return String mit dem Inhalt der Datei oder null, falls Datei leer ist oder es einen Fehler gab
 */
private static String readFromFile(Context context, String filePath){
    String text = "";

    try{
        FileInputStream fis = context.openFileInput(filePath);
        InputStreamReader isr = new InputStreamReader(fis);
        BufferedReader bufferedReader = new BufferedReader(isr);
        StringBuilder sb = new StringBuilder();
        String line;
        while((line = bufferedReader.readLine()) != null){
            sb.append(line);
        }
        text = ""+sb;

        if(text.equals("")){
            return null;
        }
    }catch(Exception e){
        e.printStackTrace();
        text = null;
    }

    return text;
}
Die letzte Methode wird beim AUsloggen des Nutzers verwendet und löscht alle gespeicherten Nutzerdaten. Dazu wird die Methode writeToFile() ein mal für jeden Speicherort aufgerufen und jedes mal ein leerer String übergeben. Anschließend werden die drei Variablen für die Nutzerdaten auf null gesetzt:
/**
 * Löscht alle gespeicherten Nutzerdaten
 * @param context Kontext der App
 */
public static void deleteAllUserData(Context context){
    writeToFile(context, usernameFile, "");
    writeToFile(context, passwordFile, "");
    writeToFile(context, serverFile, "");

    username = null;
    password = null;
    serverIp = null;
}

Login-Bildschirm optimieren

Jetzt wird der Login-Bildschirm an die neuen Methoden der Klasse SaveData angepasst. Öffne dazu die Klasse "LoginActivity.java". Als erstes werden die Aufrufe auf die eben gelöschte Methode setSaveLoginData() entfernt. Dazu löscht du den setSaveLoginData()-Aufruf in Zeile 61. Anschließend wird der OnCheckedChangedListener in Zeile 72 - 76 entfernt, da er nicht mehr benötigt wird. Der Methode login() wird ein weiterer Parameter namens "saveData" vom Typ boolean hinzugefügt, mit dem angegeben wird, ob die Daten gespeichert werden sollen:
public void login(final String username, final String password, final String serverIp, final boolean saveData){
	//...
}
In der Methode login() werden die Nutzerdaten gespeichert, wenn der Login-Vorgang erfolgreich war, und der Haken in der CheckBox zum Speichern der Daten gesetzt wurde. Hier wird der Aufruf der Methode setLoginData() um den Parameter "saveData" erweitert. Auch wird der if-Block um die Zeilen 116 & 117 entfernt, da die Speicher-Methoden in jedem Fall aufgerufen werden sollen:
SaveData.setLoginData(getApplicationContext(), username, password, saveData);
SaveData.setServerIp(getApplicationContext(), serverIp);
Nun werden die Aufrufe der login-Methode an den neuen Methodenkopf angepasst. Dazu fügst du den Aufrufen in den Zeilen 61 & 83 den Parameter "saveLogin.isChecked()" hinzu:
login(username, password, serverIp, saveLogin.isChecked());
Außerdem soll sich der Login-Bildschirm schließen, wenn der Login-Vorgang erfolgreich abgeschlossen wurde, damit der Nutzer mit dem Zurück-Button nicht wieder zum Login-Bildschirm gelangt, sondern die App verlässt. Dazu wird in der Methode login() nach dem Starten des Intents (Zeile 120 - 123) die Methode finish() aufgerufen, die die aktuelle Activity beendet.
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
intent.putExtra(MainActivity.EXTRA_ROOMS, result);
startActivity(intent);
finish();

Logout-Button erstellen

Um einen Logout-Button zu erstellen, öffnest du zuerst die Datei "main.xml" im Verzeichnis "/res/menu" und fügst dort einen neuen Item-Block hinzu, der den Titel "Ausloggen", die ID "action_logout" und bei showAsAction "never" hat. Damit wird der Button immer als Text angezeigt.
<item
    android:id="@+id/action_logout"
    android:orderInCategory="100"
    android:title="Ausloggen"
    app:showAsAction="never" />

Logout-Button in MainActivity einfügen

Damit die App auch reagiert, wenn der Logout-Button geklickt wird, muss die Methode onOptionsItemSelected() ein wenig erweitert werden. Dazu wechselst du wieder zur Datei "MainActivity.java" und erstellst in dem vorhandenen if-Block einen "else if"-Block für den Buttonklick, der die gespeicherten Nutzerdaten löscht, die Login-Activity startet und die MainActivity beendet:
else if(post_id == R.id.action_logout){
    SaveData.deleteAllUserData(getApplicationContext());

    Intent intent = new Intent(MainActivity.this, LoginActivity.class);
    startActivity(intent);
    finish();
}
Bei Fragen oder Problemen kannst du mir gerne einen Kommentar hinterlassen.

Über den Autor


Sascha Huber

Hallo, ich bin Sascha, der Gründer von Smarthome Blogger.

Mit einer Leidenschaft für Technologie und einem Hintergrund als Software Engineer habe ich 2016 Smarthome Blogger gegründet. Mein Ziel war es schon immer, innovative Lösungen zu entdecken, die unser Leben einfacher und intelligenter gestalten können. In meinem beruflichen Leben arbeite ich täglich mit Software und Technik, aber auch in meiner Freizeit bin ich stets auf der Suche nach neuen technischen Spielereien und Möglichkeiten, mein Zuhause zu automatisieren und zu verbessern.

Auf Smarthome Blogger teile ich mein Wissen, meine Erfahrungen und meine Begeisterung für alles rund um das Thema Smarthome.



Dieser Beitrag hat dir gefallen?

Dann abonniere doch unseren Newsletter!