Softwareentwickler / Software Developer
App-Menü eines Handys, das in einer Hand gehalten wird

App-Entwicklung | Wie entwickle ich eine Android-App – Teil 2

Wie versprochen folgt nun endlich der zweite Teil der Reihe zur Entwicklung einer Android App. Im ersten Teil haben wir die Oberflächengestaltung der App und die Verbindung zu einem Server über das SSH-Protokoll thematisiert. Heute soll es um vor allem um das Speichern von eingegebenen Hosts und Kommandos gehen. Neben einer Datenbank benötige ich auch Zugriff auf den Speicher von Android.

Schritt 5: Erstellen von Formularen

Da die Daten auf irgendeinen Weg in die App gelangen müssen, werden Formulare benötigt. Diese lassen sich mit Hilfe des Editors in Android Studio sehr schnell per Drag-and-Drop erstellen. Für jedes Formular habe ich jeweils eine neue Aktivität angelegt, also eine neue Klasse, die von der Klasse android.app.Activity erbt. Mithilfe von setContentView() kann ich die Aktivität dann mit der XML-Datei, die das Layout beschreibt, verknüpfen. Im ersten Schritt greife ich dann jeweils auf die eingegebenen Daten zu und führe eine Überprüfung durch.

Für diese Formulare habe ich verschiedene Eingabefelder benötigt. Für Felder, wo ich eine Zeichenkette als Eingabe erwarte, habe ich einen EditText verwendet. Dieses ist sehr simpel, da man die Eingabe über mit getText().toString() ganz einfach in einen String umwandeln kann und nichts weiter gemacht werden muss.

Zusätzlich habe ich noch einen Spinner und eine RadioGroup verwendet. Erst genannter ist dazu da, um Drop-Down-Menüs zu bauen. Dies war notwendig, um einem Host bei Bedarf einen Schlüssel und einem Kommando einen Host zuzuordnen, die vorher jeweils schon existieren mussten. Mit der RadioGroup konnte ich abbilden, dass im Formluar entweder Felder zur Eingabe eines Passwortes ODER zur Authentifizierung mit einem Schlüssel angezeigt wurden. Im Folgenden möchte ich kurz erläutern, wie man diese beiden verwendet.

Spinner

Ein Spinner stellt wie beschrieben eine Liste von Werten bereit, aus denen einer ausgewählt werden kann. Einen Spinner befüllt man entsprechend mit einer Liste. In meiner AddCommandActivity-Klasse, nutze ich den folgenden Code, in dem zusätzlich die verfügbaren Hosts aus der Datenbank gelesen werden.

private void populateSpinner()
{
    List spinnerList = new ArrayList<>();
    list = new ArrayList<>();
    Cursor cursor = databaseManager.runQuery("SELECT * FROM host");

    if (cursor.moveToFirst())
    {
        while(!cursor.isAfterLast())
        {
            spinnerList.add(
                    cursor.getString(cursor.getColumnIndex("username"))
                            + "@"
                            + cursor.getString(cursor.getColumnIndex("host")));
            list.add(cursor.getInt(cursor.getColumnIndex("id")));

            cursor.moveToNext();
        }
    }

    ArrayAdapter adapter = new ArrayAdapter<>(
            this, android.R.layout.simple_spinner_item, spinnerList);

    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    spinner.setAdapter(adapter);
}

Wichtig zu beachten ist dabei, dass eine direkte Verknüpfung nicht möglich ist, sondern noch ein ArrayAdapter dazwischengeklemmt werden muss.

Screenshot eines Spinners in Android Apps
Nach einem Klick auf das Formularelement öffnet sich ein Drop-Down-Menü mit einer Liste von Werten, die man auswählen kann.

RadioGroup

Radio-Buttons sind zusammengehörige Buttons, von denen jeweils nur einer ausgewählt werden kann. Um das Formular entsprechend der Auswahl anzuzeigen, habe ich Listener registriert, die auf das Klicken auf einen der Buttons aus der RadioGroup horchen. Das sieht in der Klasse AddHostActivity folgendermaßen aus.

private void registerListeners()
{
	radioButtonPassword.setOnClickListener(new View.OnClickListener() {
		@Override
		public void onClick(View v)
		{
			//Mache Felder für Schlüssel unsichtbar mit .setVisibility(View.INVISIBLE)
			//Mache Felder für Passwort sichtbar mit .setVisibility(View.VISIBLE)
		}
	});

	radioButtonKey.setOnClickListener(new View.OnClickListener() {
		@Override
		public void onClick(View v)
		{
			//Mache Felder für Schlüssel sichtbar mit .setVisibility(View.VISIBLE)
			//Mache Felder für Passwort unsichtbar mit .setVisibility(View.INVISIBLE)
		}
	});
}
Animation zur Veranschaulichung der Funktionsweise einer Radio-Group
Je nachdem, welcher der Buttons aus der Radio-Group aktiviert ist, ändern sich die angezeigten Eingabefelder

Zugriff auf das Dateisystem

Ein Problem, was mich einige Zeit beschäftigt hat, war der Zugriff auf den Android-Speicher. Der Grund, warum ich diese Funktionalität brauche, ist, dass in einem Formular der private Schlüssel eingegeben werden soll, der dann im Speicher des Gerätes gespeichert wird.

Zuerst einmal benötigt die App dafür die Rechte. Dazu muss in der AndroidManifest.xml folgendes ergänzt werden:

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

In der onCreate()-Methode der Aktivität, die beim Öffnen der App gestartet wird, muss seit Android 6 zusätzlich nach dieser Berechtigung gefragt werden. Die dazu notwendige Methode kann so ausschauen:

private void getPermissionToWriteToExternalStorage()
{
    if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
    {         
        ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, MY_PERMISSIONS_WRITE_EXTERNAL_STORAGE);
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)
{
    switch(requestCode)
    {
        case MY_PERMISSIONS_WRITE_EXTERNAL_STORAGE:
        {
            if (grantResults.length > 0
               && grantResults[0] == PackageManager.PERMISSION_GRANTED)
            {                 
                 Toast.makeText(getApplicationContext(), "Permission granted to write on external storage", Toast.LENGTH_LONG).show();

            }
        }
    }
}

Sobald die Berechtigung gewährt wurde, wird die Methode onRequestPermissionResult() ausgeführt und zeigt ein Popup, in dem dieser Vorgang noch einmal bestätigt wird. Nun folgt der Code, mit dem ich einen als String eingegebenen Schlüssel in einer Datei speichere.

private boolean saveKey(String filename, String privateKey)
{
    if (!isExternalStorageWritable())
    {
        showMessage("Error while saving key: can't write to external storage");
        return false;
    }

    File file = new File(getPublicStorageDirectory(), filename);

    try
    {
        OutputStream os = new FileOutputStream(file);
             os.write(privateKey.getBytes(StandardCharsets.UTF_8));
        os.close();

        MediaScannerConnection.scanFile(
                    this,
                    new String[]{file.toString()},
                    null,
                    new MediaScannerConnection.OnScanCompletedListener()
                    {
                        @Override
                        public void onScanCompleted(String path, Uri uri)
                        {
                            Log.i("ExternalStorage", "Scanned " + path + ":");
                            Log.i("ExternalStorage", "-> uri=" + uri);
                        }
                    });
        return true;
    }
    catch (IOException e)
    {
        showMessage("Error while saving key: " + e.getMessage());
        return false;
    }
}

private File getPublicStorageDirectory()
{
    File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "command-sender");
    file.mkdirs();
    return file;
}

private boolean isExternalStorageWritable()
{
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state))
    {
        return true;
    }
    return false;
}

Das ist jetzt eine Menge Code auf einmal, aber natürlich folgt auch eine Erklärung. Zuerst einmal prüfen wir, ob wir in den externen Speicher schreiben können. Dazu wird in isExternalStorageWritable() überprüft, ob überhaupt ein externer Speicher vorhanden bzw. gemountet ist. Danach wird eine neue Datei erstellt. Das File-Objekt erhalten wird als Rückgabe von getPublicStorageDirectory(). Diese Methode greift auf einen Pfad zu, der über eine statische Methode der Klasse Environment bereitgestellt wird. Dort erstellen wird einen Ordner mit dem Namen command-sender. In diesen Ordner wird dann über einen OutputStream eine neue Datei erzeugt. Der MediaScanner ist abschließend dafür zuständig, damit erkannt wird, dass es nun eine neue Datei gibt und liest Metadaten aus.

Schritt 6: Speichern der Daten in eine Datenbank

Um die Daten abzuspeichern, nutze ich eine SQLite-Datenbank. Android bringt hier schon viele Funktionen mit, die das Arbeiten mit der Persistenzschicht sehr einfach machen. Für die Arbeit mit der Datenbank habe ich die Klasse DatenbankManager erstellt, die folgendermaßen aussieht.

public class DatabaseManager
{
    private static final String DATABASE_NAME = "SSH";

    private SQLiteDatabase database;

    public DatabaseManager(Context context)
    {
        database = context.openOrCreateDatabase(DATABASE_NAME, Context.MODE_PRIVATE, null);
    }

    public void executeSql(String sql)
    {
        Log.e("", sql);
        database.execSQL(sql);
    }

    public Cursor runQuery(String query)
    {
        Log.e("", query);
        return database.rawQuery(query, null);
    }

    public int findNextId(String database, String idColumn)
    {
        Cursor cursor = runQuery("SELECT MAX(" + idColumn + ") AS id FROM " + database + ";");

        if (cursor.moveToFirst())
        {
            return cursor.getInt(cursor.getColumnIndex("id")) + 1;
        }

        return 0;
    }
}

Um auf eine Datenbank mit einem bestimmten Namen zugreifen zu können und diese gegebenenfalls vorher erst noch anzulegen, muss man lediglich an dem aktuellen Context die Methode openOrCreateDatabase aufrufen. Anschließend kann man an dem Objekt der Klasse SQLiteDatabase ganz normale SQL-Abfragen ausführen.

An jeder Stelle, wo ich nun eine Datenbank-Operation ausführen möchte, kann ich nun ein Objekt dieser Klasse instanziieren und arbeite immer gegen die gleiche Datenbank, da wir uns innerhalb der App im selben Kontext bewegen.

Was bisher noch fehlt, sind die Tabellen in der Datenbank. Diese erstelle ich, imdem ich in der onCreate-Methode der Start-Aktivität folgende Methode aufrufe.

private void createOrOpenDatabase()
{
    databaseManager = new DatabaseManager(getApplicationContext());
    
    databaseManager.executeSql("CREATE TABLE IF NOT EXISTS host (id INT(4) NOT NULL PRIMARY KEY, username VARCHAR NOT NULL, host VARCHAR NOT NULL, sshPort INT(5) NOT NULL, privateKeyPath VARCHAR, keyPassphrase VARCHAR, password VARCHAR)");    
    databaseManager.executeSql("CREATE TABLE IF NOT EXISTS command (id INT(4) NOT NULL PRIMARY KEY, command VARCHAR NOT NULL, name VARCHAR NOT NULL, hostconfiguration INT(4) NOT NULL, FOREIGN KEY(hostconfiguration) REFERENCES host(id))");
}

Fazit

Natürlich musste ich mich ein wenig mehr programmieren, als ich hier beschrieben habe. Den kompletten Code zu erklären, würde aber den Rahmen sprengen. Ich hoffe aber, dass diese beiden Teile schon Mal einen hilfreichen Einblick gegeben haben.

Es hat einige Zeit gedauert, erst einmal zu verstehen, wie eine Android App überhaupt aufgebaut ist und auch in Android Studio musste ich mich zurecht finden. Zudem haben sich im Laufe des Projektes noch einige weitere Probleme herauskristallisiert, die ich in diesen beiden Teilen größtenteils zumindest angeschnitten habe, damit du nicht auf dieselben Hindernisse triffst. Google war immer eine gute Hilfe 🙂 Natürlich ist die App noch lange nicht perfekt und der Sicherheitsaspekt wurde auch noch nicht berücksichtigt, aber sie ist auf jeden Fall funktionstüchtig und es hat viel Spaß gemacht, zu erlernen, wie man eine App für Android entwickelt.

Zu finden ist das Projekt auf meinem Github-Profil: Command-Sender App

Bonus: Screenshot von der App

Screenshot der Startseite der Command-Sender App

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

2 Gedanken zu “App-Entwicklung | Wie entwickle ich eine Android-App – Teil 2”