Ghidra und MSVC WIN32 Programme

Für die Entwicklung von ausführbaren Programmen für Windows ist es üblich, Visual Studio zusammen mit zusammen mit dem MSVC Compiler und Linker zu verwenden. Visual Studio erledigt ein paar Dinge unter der Haube, um ein kleines C- oder C++-Programm in eine sichere und optimierte Anwendung zu verwandeln. Um dies zu verstehen, widmen wir uns einigen Dingen sowie WIN32 API-Besonderheiten, um diese erkennen können.

Aufrufkonventionen

Beim Aufruf einer Methode müssen die Argumente an die Methode übergeben werden. Die genauen Details dazu sind im Wikipedia X86-Aufrufkonventionen Artikel nachzulesen, aber die gebräuchlichsten, die man in einer Anwedungsdatei findet, sind:

  • cdecl

    Dies wird üblicherweise von statischen Klassenmethoden verwendet (d.h. alles, was den Zustand der Instanz nicht verändert). Es wird auch von den meisten C-Methoden verwendet sowie sowie Methoden außerhalb einer Klasse.

  • stdcall

    Der stdcall wird an den meisten Stellen der Windows-API verwendet, insbesondere bei Methoden, die aus DLLs importiert werden. Es kann schwierig sein, ihn von thiscall zu unterscheiden, nicht nur für einen selbst, sondern auch für Ghidra. Man erkennt sie am Register ECX und Stack als Eingabe und EAX als Ausgabe.

  • thiscall

    Wie der Name schon sagt, wird er primär auf Klassen angewendet und in den meisten Fällen kann man von einem identischen Verhalten wie bei stdcall ausgehen. Ghidra kümmert sich um die Typverwaltung des this-Zeigers, was viel Arbeit erspart. Der this-Zeiger steht in ECX, die Argumente auf dem Stack und EAX ist die Ausgabe.

Importierte Funktionen ausfüllen

Ghidra kommt bereits mit einer langen Liste von Funktionsdefinitionen. Es deckt einen Großteil der der WIN32-API für verschiedene Versionen von Visual Studio ab. Die offizielle Liste ist unter ghidra-data auf github. Funktionen, die fehlen, aber importiert werden, können Sie einfach zu Ihrem Projekt hinzufügen. Eine gute Referenz ist der WINE-Quellcode der sie in bequemen und lesbaren Header-Dateien anbietet.

Ein gutes Beispiel ist DirectDrawCreate an. Beim Importieren einer Anwendung, die darauf verweist, weiß Ghidra nichts über die Aufrufkonventionen und Parameter, so dass man zwar den Namen, aber sonst nichts sehen kann:

DirectDrawCreate in der ursprünglichen Form

Im nächsten Schritt kann die Funktionsdefinition erstellt werden. Diese wurde aus wine’s ddraw.h entnommen.

DirectDrawCreate nach dem Erstellen der Definition

Jetzt ist es möglich den Funktionsaufruf inkl. der Parameter und auch den entsprechenden Rückgabewert zu sehen. Dabei muss natürlich der Struktur und die Typ-Definition berücksichtigt werden. Während der Arbeit an diesem Beispiel habe ich eine Bibliothek mit den Typen für DirectDraw, Direct3D und Direct3D9 erstellt. Sie finden meine Typ Definitionen auf github.com/egore/ghidra-data.

Aufruf auf DirectDrawCreate nachdem es gefüllt ist

Typisierung von GetProcAddress und LoadLibrary

Unter Windows können Funktionen aus einer Drittanbieter-DLL mittels LoadLibrary und GetProcAddress referenziert werden. Das folgende Beispiel zeigt das Laden und die sofortige Verwendung einer Methode einer DLL. Ohne Definition eines geeigneten Typs für die globalen DAT_XXXXXXXX-Felder, ist der Code kaum lesbar:

MessageBoxA vor der Definition der GetProcAddress-Typen

Allein die Umbenennung der Felder ermöglicht bereits eine bessere Erkennung. Kombiniert mit der Eingabe von den von Ghidra bereitgestellten Funktionsdefinitionen (z.B. “GetActiveWindow*” für GetActiveWindow), macht es den Code leicht lesbar. Man kann schnell sehen, dass dass dies MessageBoxA von user32.dll auf das aktive Fenster oder das letzte aktiven Popup.

MessageBoxA nach der Definition der GetProcAddress-Typen

Jeder, der mit der mit Durch die CRT hinzugefügte Magie vertraut ist, wird diese Funktion erkennen können diese Funktion: es ist der Fehlerpfad, wenn etwas vollständig schief gelaufen ist, um einen Fehlerdialog anzuzeigen, bevor das Programm beendet wird.

C++

Kompiliertes C++ bringt seine eigene Komplexität mit sich, vor allem was in Bezug auf dynamic_cast und virtuelle Methoden passiert. Eine wichtige Ergänzung ist der Ghidra-Cpp-Class-Analyzer, der tolle Arbeit bei der Identifizierung von Klassen und ihren virtuellen Funktionstabellen leistet. Es gibt aber einige händische erkennbare Muster, um Klassen zu identifizieren, die vom Plugin übersehen oder nur unvollständig erstellt wurden.

operator_new

In den meisten Fällen hilft die suche nach einer Methode namens operator_new suchen. Hinter den Kulissen ist es ein Wrapper um malloc (oder __nh_malloc und HeapAlloc wegen der CRT-Magie), und wird in den meisten Fällen von Ghidra erkannt. Sollte dies nicht der Fall sein (z.B. beim Reverse Engineering von Binärdateien aus dem letzten Jahrtausend) kann man es leicht selbst identifizieren. Einmal gefunden haben, wird man eine häufige Verwendung feststellen:

Verwendung von operator_new

Im Beispiel sehen Sie eine Speicherzuweisung von 400 Bytes. Wenn diese Zuweisung erfolgreich durchgeführt wurde, wird sie sofort an eine Funktion übergeben, die sehr wahrscheinlich der Konstruktor ist. Da wir den Zweck dieser Klasse noch nicht kennen, habe ich sie “Unknown_400” genannt, um anzuzeigen, dass sie mir unbekannt ist, aber 400 Bytes hat. Außerdem habe ich die Methode umbenannt, damit sie wie ein Konstruktor aussieht.

Verwendung von operator_new nach dem Erstellen von Klasse und Struktur

Virtuelle Funktionstabellen (vtables, vftables, _vfptr)

Wenn Sie sich den Konstruktor genauer ansehen, werden Sie ein allgemeines Muster erkennen:

  • Superklassenkonstruktoren werden aufgerufen (oder inlined, was häufig passiert)
  • die Tabelle der virtuellen Funktionen wird eingerichtet
  • die Felder der Klasse werden initialisiert

In unserem Unknown_400 Beispiel können wir genau das sehen, was oben beschrieben wurde.

Konstruktor vor dem Aufräumen

Dies verrät, dass PTR_FUN_0043fa00 wahrscheinlich eine Funktionstabelle ist: eine Liste von Funktionen, von denen nur die erste von Ghidra ein Label erhalten hat (d.h. weil sie per Adresse referenziert wird). Eine gängige Praxis ist die vtable durch eine Liste von Zeigern zu ersetzen, welche von in Ghidras Decompiler verwendet wird, und die Anzeige des Rückgabewerts sowie der Argumente nutzbar wird (die Einträge der Liste sollten auf keinen Fall als undefined4 gar int, da der Decompiler sonst die Methoden nicht richtig darstellt).

Definition der Unknown_400 vtable

Durch die Verwendung von “Auto Fill in Class Structure” erhalten wir einen ziemlich anständig aussehenden Konstruktor:

Konstruktor nach dem Aufräumen

Mit Hilfe des Verweises auf die vtable kann man auch den Destruktor finden, der häufig der erste Pointer der vtable ist. Der Destruktor wird das tun was der Konstruktor in umgekehrter Reihenfolge getan hat:

  • die vtable zu unserer eigenen machen (für den Fall, dass sie von einer Kindklasse überschrieben wurde)
  • Aufräumen der Felder
  • den Destruktor der Superklasse aufrufen (oder ihn inlined haben)

IUnknown

Die Schnittstelle IUnknown wird in der WIN32-API verwendet, um eigene unbekannte Objekte zu verwenden, bei denen nur die API bekannt ist. Sie unterscheidet sich von einer normalen C++-Klassendefinition dadurch, dass man nichts über Mitgliedsvariablen Variablen wissen darf.

Betrachten wir das Beispiel von IDirectDrawColorControl aus WINE

Verwendung der IUnknown-Schnittstelle durch IDirectDrawColorControl

Es erbt von IUnknown und bringt lediglich zwei Methoden mit. Um dies in Ghidra abzubilden, erstellen wir das notwendige Interface IDirectDrawColorControl sowie eine leere IDirectDrawColorControlVtbl, die von dem Interface referenziert wird.

Struktur für IDirectDrawColorControl

Um die vtable zu füllen, müssen die entsprechenden Funktions-Definitionen erstellen werden (wenn es für das Verständnis notwendig ist, ansonsten werden es nur Zeiger sein). Die ersten drei Methoden werden von IUnknown geerbt, so dass ich einfach die von Ghidra bereitgestellte IUnknownVtl wiederverwende.

Strukturen für IDirectDrawColorControl

Vtable für IDirectDrawColorControl

Das Hinzufügen der Funktionsdefinitionen ist in der Regel sehr zeitaufwändig. Es müssen zusätzliche Strukturen, Typedefs und Schnittstellen erstellen werden. Aber für komplexe Schnittstellen (z.B. IDirectDrawSurface vs. IDirectDrawSurface4 vs. IDirectDrawSurface7) wird eine korrekte Methodendefinition für das Verständnis schnell relevant.

Durch die CRT hinzugefügte Magie

Visual Studio wird dafür sorgen, dass Sie Programmierfehler vermeiden, indem es der C-Laufzeitumgebung (CRT) etwas Magie hinzufügt. Diese Magie ist nicht sichtbar im Code, sondern hinter den Kulissen. Am schnellsten sieht man es bei den dateibezogenen bezogenen APIs. Ich dachte, ich wüsste wie fopen funktioniert. Wenn man sich das Disassembly der dateibezogenen APIs anschaut, wird man etwas anderes feststellen: die FILE, die den Methoden übergeben wird, ist keine einfache Datei, sondern ein __crt_stdio_stream_data. Dies kombiniert ein FILE-Handle mit einem CRITICAL_SECTION zum Sperren von Dateizugriffen. Das ist in der Tat eine gute Idee mit einem akzeptablen Performance-Overhead (E/A ist eher der langsame Teil eines Aufrufs, nicht der Mutex) und es verhindert leicht Programmierfehler durch parallen Zugriff. Leider ist das Innenleben der Methoden notwednig, um diese zu erkennen. Ghidra 10.1 war noch nicht in der Lage, diese zu erkennen und kannte auch __crt_stdio_stream_data nicht.

Wie bereits angedeutet, verhalten sich einige Methoden ebenfalls anders. Zum Beispiel verwendet malloc intern __nh_malloc, um Speicher mit Hilfe der Windows HeapAlloc APIs zu reservieren. Das ist zwar richtig, bringt aber auch seinen eigenen Anteil an Komplexität in ein Binärprogramm.

Ausnahmebehandlung

Besonders in C++ muss man sich der Magie hinter den Kulissen der Ausnahmebehandlung bewusst sein. Unter Windows sind EH oder SEH (strukturierte Ausnahmebehandlung) vertreten, die in den Methoden für einige Unordnung sorgen.

Hinweise zur Ausnahmebehandlung](/posts/ghidra_win32/exception-handling.png)

Man kann in einer dekompilierten Methode in_FS_OFFSET, local_c, puStack8 und local_4 ignorieren, und sich auf den Positivfall konzentrieren, d.h. der Durchführung ohne dass das Programm Ausnahme auslöst. Das Thema Ausnahmebehandlung wird noch nicht durch den Ghidra-Decompiler unterstützt, um diese lesbar zu machen (siehe Ghidra issue tracker 2477).

/GS Puffer Sicherheitsüberprüfung

Wenn ein Programm mit der Compiler-Option /GS kompiliert wird, findet sich ein kleines bisschen Magie in jeder Methode, die den Stack benutzt (d.h. eigentlich überall). Man kann diese an ___security_init_cookie erkennen, die eine globale Variable einrichtet (gewöhnlich SECURITY_COOKIE genannt). In den Methoden gibt es dann einen zusätzlichen Prolog sehen:

/GS prolog

Und Epilog:

/GS epilog

Magie durch DLLs von Drittanbietern hinzugefügt

Manchmal kommt es vor, dass sich einige Codeteile sich ganz anders verhalten als andere. Das liegt in der Regel daran, dass sie von jemand anderem verfasst wurden, der andere Standards und Muster verwendet. So kommt es vor, dass manche Methoden die OutputDebugString protokollieren, während andere dies mit fprintf tun.

In manchen obskuren Codepfaden werden Sie über ein Verhalten stolpern, das schwer zu erklären ist. Ein wichtiger Aspekt ist, dass C und C++ auf Präprozessoren basieren. Damit sind verrückte Dinge möglich, wie z.B. einen eigenen Allokator zu schreiben und Folgendes damit zu tun

#undef new
#define new MY_WAY_BETTER_ALLOCATOR

Je nach Definition wird diese Definition nicht nur den eigenen Code verändern, sondern auch den von anderen.

hashCode()/equals() mit Lambdas

Korrekte hashCode() und equals() Implementierungen sind sehr wichtig, aber es macht keinen Spaß, sie zu schreiben. Glücklicherweise generieren moderne IDEs die Methoden automatisch, also muss man nicht zu viele Gedanken daran verschwenden. Leider hat dieser Ansatz zwei Nachteile:

  1. in eigentlich sehr einfachen Klassen (e.g. DTOs or JPA entities) entstehen mindestens 20 Zeilen komplex wirkenden Codes, der zum Einen die Lesbarkeit reduziert und bei statischer Code Analyse negativ auffallen kann
  2. jede IDE generiert die Implementierung etwas anders, daher unterscheidet sich der Code z.B. Eclipse und IntelliJ

Lambdas als Alternative

Eine Alternative, um das Generieren der hashCode() und equals() Methoden zu vereinfachen, sind die mit Java 8 eingeführten Lambdas. Ich habe mir also folgenden Ansatz überlegt:

public class HashCodeEqualsBuilder {

    public static  int buildHashCode(T t, Function<? super T, ?>... functions) {
        final int prime = 31;
        int result = 1;
        for (Function<? super T, ?> f : functions) {
            Object o = f.apply(t);
            result = prime * result + ((o == null) ? 0 : o.hashCode());
        }
        return result;
    }

    public static  boolean isEqual(T t, Object obj, Function<? super T, ?>... functions) {
        if (t == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (t.getClass() != obj.getClass()) {
            return false;
        }
        T other = (T) obj;
        for (Function<? super T, ?> f : functions) {
            Object own = f.apply(t);
            Object foreign = f.apply(other);
            if (own == null) {
                if (foreign != null) {
                    return false;
                }
            } else if (!own.equals(foreign)) {
                return false;
            }
        }
        return true;
    }
}

Diese Hilfklasse kann wie folgt bei der Implementierung einer Klasse (z.B. ein DTO) verwendet werden:

@Override
public int hashCode() {
    return HashCodeEqualsBuilder.buildHashCode(this, MyClass::getA, MyClass::getB);
}

@Override
public boolean equals(Object obj) {
    return HashCodeEqualsBuilder.isEqual(this, obj, MyClass::getA, MyClass::getB);
}

Basierend auf einigen Microbenchmarks, unterscheidet sich dieser Ansatz nicht messbar zu einer autogenerierten Implementation (z.B. von Eclipse). Ich werde zukünftig diesen Ansatz verwenden und kann ihn auch nur wärmstens empfehlen::

  1. es werden keine unnötigen Code-Zeilen zum Projekt hinzugefügt, die evtl. noch negativ in der Code Coverage durch Unit Tests bewertet werden
  2. eine Änderung an der Implementierung (falls je notwendig) muss an exakt einer Stelle durchgeführt werden

DAO Unit Tests mit JUnit und Mockito

If it’s not tested, it’s broken. – Bruce Eckel

Stabile software ist mir sehr wichtig. Als Teil der Softwareindustrie möchte ich nur software herstellen, die auch möglichst zuverlässig funktioniert. Sollte eine Software abstürzen (oder noch schlimmer: Daten verlieren), erzeugt das beim Anwender ein so großes Misstrauen, dass er die Software nicht mehr verwendet. Wenn man sich selbst die Frage stellt: Würde ich eine Software einsetzen wollen, die abstürzt? Ich nicht. Daher ist die Qualitätssicherung und insbesondere der Softwaretest, um Fehler vor dem Endnutzer zu finden, der wichtigste Schritt im Softwareentwicklungsprozess.

Nun stellt sich die Frage: Wie testet man Software? Wie immer gibt es hier auf Wikipedia detaillierte Beschreibungen zu verschiedenen Arten von Tests. Auch gibt es diverse Blogs, Tutorials und Ähnliches überall im Internet. Es wird also schnell deutlich, dass es verschiedenste Arten gibt, die Qualität der Software sicherzustellen. In diesem Artikel beziehen wir uns auf den Teil der Softwareentwicklung, die Hauptsächlich durch Entwickler durchgeführt wird: Unit Tests. Wie der Name suggeriert, wird dabei eine Unit, also eine Einheit der Software geprüft. Wie genau eine Einheit definiert ist, kann von Anwendungsfall, Art der Software und sogar der Programmiersprache abhängig sein. In diesem Beispiel verwenden wir Java Klassen und ihre Methodem als Einheit. Wir starten also mit folgender trivialen Klasse, dem SomeDao:

package de.egore911.test;

import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.Id;

public class SomeDao {
    public static class Some {
        @Id public Integer id;
    }

    @Inject private EntityManager em;

    protected EntityManager getEntityManager() { return em; }

    public Some comeGetSome(Integer id) {
        return getEntityManager().find(Some.class, id);
    }
}

Wir haben ein DAO (Data Access Object) das eine Entität des Typs *Some anhand ihrer ID aus der Datenbank laden kann. Die Klasse selbst verwendet CDI um eine EntityManager Instanz zu beziehen. Dieses Muster ist typisch für diverse Webanwendungen und es wird in verschiedsten Projekten so verwendet. Aber wie testet am so eine Einheit? Auf den ersten Blick brauchen wir also ein Injection Framework, das wiederum eine Implementierung der EntityManager-JPA-Spezifiktion benötigt und diese wiederum benötig eine Datenbank, gefüllt mit Testdaten. Diese Abhängigkeiten könnten selbst Fehler erzeugen (z.B. wenn die Datenbank nicht verfügbar ist), und dürfen daher nicht Teil eines Unit-Tests sein. Eine Einheit muss abgeschlossen sein und externe Abhängigkeiten gemockt. Ein Mock ist dabei eine Blackbox, die zu definierten Eingaben immer die gleichen Ausgaben liefert. Wir benötigen also einen Mock des EntityManager, der selbst nicht durch ein Framework injeziert wird und selbst gar nicht auf eine Datenbank zugreifen muss. Dafür gibt es Frameworks wie Mockito um einen solchen zu erstellen.

import javax.persistence.EntityManager;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

public class SomeDaoTest {

    @Test
    public void testComeGetSome() {
        // Gegeben: Ein Mock des EntityManager der unseren Dummy zurückgibt
        Some dummy = new Some();
        EntityManager em = Mockito.mock(EntityManager.class);
        Mockito.when(em.find(Some.class, 1234)).thenReturn(dummy);

        // Ebenfalls gegeben: Unser gemocktes SomeDao mit dem gemockten
        // EntityManager, dass nur bestimmte Aufrufe auf comeGetSome beantwortet
        // allowing only the call to comeGetSome
        SomeDao someDao = Mockito.mock(SomeDao.class);
        Mockito.when(someDao.comeGetSome(1234)).thenCallRealMethod();
        Mockito.when(someDao.getEntityManager()).thenReturn(em);

        // Der eigentliche Test-Code
        Assertions.assertSame(dummy, someDao.comeGetSome(1234));
        Assertions.assertNull(someDao.comeGetSome(4321));
    }
}

Das Beispiel ist bewusst extrem einfach gehalten und der eigentliche Test-Code ergibt daher wenig Sinn: wir testen nur unseren Mock (weil unser DAO selbst keine Logik besitzt). Sobald das DAO allerdings Logik benötigt (z.B. das Validieren von Parametern oder die Verwendung der Criteria API), kann diese Art des Tests einen Mehrwert bringen. Zuerst mocken wir den EntityManager, der das Dummy-Objekt zurückgibt, wenn seine EntityManager.find()-Method aufgerufen wird. Anschließend sorgen wir dafür, dass der gemockte EntityManager von SomeDao.getEntityManager() zurückgegeben wird, was im Fall von der echten SomeDao.comeGetSome()-Methode der Fall ist is invoked. Selbstverständlich könnten wir das alles mit Reflection selbst machen, aber Mockito übernimmt hier die harte Arbeit für uns.

Das obige Bespiel kan sogar noch vereinfacht werden, wenn man Mockito die Dependency Injection überlässt. Das folgende Beispiel zeigt eine vergleichbare Implementation auf Basis der InjectMocks-Annotation.

import javax.persistence.EntityManager;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
public class SomeDaoTest {

    @Mock
    private EntityManager em;

    @InjectMocks
    SomeDao someDao;

    @Test
    public void testComeGetSome() {
        // Given: Tell the mocked entitymanager to return our dummy element
        Some dummy = new Some();
        Mockito.when(em.find(Some.class, 1234)).thenReturn(dummy);
        Mockito.when(em.find(Some.class, Mockito.any(Integer.class))).thenReturn(null);

        // Perform the actual test
        Assertions.assertSame(dummy, someDao.comeGetSome(1234));
        Assertions.assertNull(someDao.comeGetSome(4321));
    }
}

Wie man einen Dateimanager für Android 15 minutes erstellt

Mit Android ist es möglich, Apps sehr schnell zu erstellen. Das klingt wie ein Werbeversprechen, oder? Wir lassen es auf einen Versuch ankommen und prfen, ob das wirklich stimmt! Wir werden einen Android-Dateimanager erstellen.

Beginnen wir mit der Erstellung einer Standard-“Hello World”-App, die aus einer Layout-Datei und einer passenden Aktivität bestehen wird. Da wir bereits wissen, was wir erstellen, nennen wir das Layout activity_list_files.xml und die Activity ListFileActivity. Wenn man mit Android Studio entwickelt, erscheint der Standard-Layout-Editor und zeigt die “Hello World”-App an. Jetzt ersetzen wir das Default-Layouts durch den folgenden Code:

<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
  android:layout_width="match_parent" android:layout_height="match_parent">
  <ListView android:id="@android:id/list" android:layout_width="match_parent"
    android:layout_height="wrap_content" />
</LinearLayout>

Jetzt haben wir ein sehr einfaches Layout, das nur aus einem einzigen Element besteht, einer ListView. Die ListView wird die Komponente sein, die unsere Dateien anzeigt. Mehr brauchen wir auch nicht. Um die View mit Daten zu befüllen kommt unsere Aktivität ins Spiel.

public class ListFileActivity extends ListActivity {

  private String path;

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

    // Use the current directory as title
    path = "/";
    if (getIntent().hasExtra("path")) {
      path = getIntent().getStringExtra("path");
    }
    setTitle(path);

    // Read all files sorted into the values-array
    List values = new ArrayList();
    File dir = new File(path);
    if (!dir.canRead()) {
      setTitle(getTitle() + " (inaccessible)");
    }
    String[] list = dir.list();
    if (list != null) {
      for (String file : list) {
        if (!file.startsWith(".")) {
          values.add(file);
        }
      }
    }
    Collections.sort(values);

    // Put the data into the list
    ArrayAdapter adapter = new ArrayAdapter(this,
        android.R.layout.simple_list_item_2, android.R.id.text1, values);
    setListAdapter(adapter);
  }

  @Override
  protected void onListItemClick(ListView l, View v, int position, long id) {
    String filename = (String) getListAdapter().getItem(position);
    if (path.endsWith(File.separator)) {
      filename = path + filename;
    } else {
      filename = path + File.separator + filename;
    }
    if (new File(filename).isDirectory()) {
      Intent intent = new Intent(this, ListFileActivity.class);
      intent.putExtra("path", filename);
      startActivity(intent);
    } else {
      Toast.makeText(this, filename + " is not a directory", Toast.LENGTH_LONG).show();
    }
  }
}

Schauen wir uns die Aktivität einmal genauer an. Zunächst einmal zum Kerngedanken, der hinter dieser Aktivität steht: Die Aktivität zeigt alle Dateien innerhalb eines bestimmten Pfades an. Wenn wir den Pfad wechseln, wechseln wir zu einer neuen Aktivität, die alle Dateien innerhalb des gewählten Pfades anzeigt. Einfach, oder? Um das zu erreichen, erweitern wir die ListActivity, die uns Methoden wie getListAdapter(), setListAdapter() und onListItemClick() zur Verfügung stellt, um mit der Liste in unserem Layout zu interagieren. Wenn also unsere Activity von Android gestartet wird und onCreate aufgerufen wird, ist das Erste, was wir tun (nach dem Setzen des Layouts): Den Pfad als Titel unserer Activity setzen. Im nächsten Schritt werden wir alle Dateien aus dem angegebenen Pfad lesen, aber es gibt einen Haken: Android sperrt einige Pfade für den Zugriff durch Apps. Das ist gewollt und notwendig, um zu verhindern, dass sich Apps gegenseitig ausspionieren (der “/data”-Ordner zum Beispiel enthält alle Datenbanken der Apps, einschließlich sensiblen Daten wie gespeicherten Anmeldedaten). Aus diesem Grund fügen wir dem Titel einen Hinweis hinzu und müssen überprüfen, ob File.list() tatsächlich ein Array zurückgibt. Die Javadoc besagt, daß nur Null zurückgegeben wird, wenn die Methode auf eine Datei aufgerufen wird, aber das gilt auch für unzugängliche Verzeichnisse.

Der untere Teil von onCreate ist ebenfalls relevant. Dort wird unser Datenadapter erstellt? Wir benutzen Androids ArrayAdapter, der es uns erlaubt, eine Liste zu übergeben und ihm mitzuteilen, wie er sie darstellen soll. Wirklich einfach und daher für viele Zwecke geeignet. Schließlich wird der onListItemClick eine neue Instanz unserer ListFileActivity starten, um einen weiteren Ordner anzuzeigen. Wenn man den Zurück-Knopf drückt, gelangt man zum vorherigen Ordner zurück, also der vorherigen Aktivität.

Die Erstellung eines sehr einfachen Dateibrowsers ist in sehr kurzer Zeit und mit nur wenigen Zeilen Code möglich. Natürlich lässt sich die App noch verbessern (z.B. wie ein wirkliches UX Konzept), aber es gibt bereits sehr gute Dateibrowser im Google Play Store (und es würde weit mehr als 15 Minuten dauern, deren Funktionen nachzuahmen).

Vielen Dank an Simeon M. und Laurent T. für den Hinweis auf kleinere Tippfehler!

Copyright © christophbrill.de, 2002-2022.