Start Magazin Alternative Texteingabe mit Dasher ? Teil 2, Implementation

Alternative Texteingabe mit Dasher ? Teil 2, Implementation

Im ersten Artikel haben wir die konkrete Umsetzung des Dasher-Sprachmodells vorgestellt. In diesem zweiten Artikel beschreiben wir eine Reihe kniffliger Probleme, die wir bei der Portierung von Dasher auf Android lösen mussten.

Im ersten Artikel dieser Serie haben wir erklärt, wie das PPM-4 Sprachmodell für Dasher in einer SQLite-Datenbank gespeichert wird. Dies wird mit einer DasherCorePPM genannten Java-Bibliothek realisiert, welche alle Dasher-Klassen beinhaltet, die für die Wahrscheinlichkeitsberechnung sowie für das Herstellen und Speichern von Sprachmodellen zuständig sind. In diesem Artikel konzentrieren wir uns nun gänzlich auf die Benutzer-Oberfläche des Dasher, die wir für Android entworfen und implementiert haben.

Tipp

Teil 1 dieses Workshops aus Android User 01/2012 finden Sie auf der Heft-CD im PDF-Format.

Übersicht

Dasher ist als Android-Texteingabe-Service (InputMethodService) implementiert. Der Service "Dasher for Android IME muss vor der ersten Anwendung beim Betriebssystem registriert werden, wie Abbildung 1 zeigt. Der Benutzer kann anschließend im Kontextmenü des Editors als Eingabemethode Dasher auswählen (Abbildung 2).

Abbildung 1: Im Menü Settings muss im Language & Keyboards Untermenü Dasher for Android IME aktiviert werden.
Abbildung 1: Im Menü Settings muss im Language & Keyboards Untermenü Dasher for Android IME aktiviert werden.

Abbildung 2: Im Menü Select input method muss Dasher for Android IME ausgewählt werden.
Abbildung 2: Im Menü Select input method muss Dasher for Android IME ausgewählt werden.

Architektur

Die Klasse DasherInputManager bildet das eigentliche Rückgrat unserer Applikation (Abbildung 3). Diese erweitert die Android Klasse InputMethodService und erlaubt die Kommunikation zwischen einem Texteditor-Feld (EditText) einer beliebigen Applikation und der Dasher Input-Methode. Die Klasse DasherInputManager orchestriert den gesamten Prozess auf dem Mobilgerät. Sie ist als Service beim Android System angemeldet und erhält callbacks, wenn wichtige Events geschehen (Android Klasse InputMethodService). Um die Dasher UI zu zeichnen, wird die Klasse DasherInputView verwendet. Diese hat verschiedene Hilfsklassen (FrameManager, DasherViewPort und SurfaceRunLoop), welche Teilaufgaben der View übernehmen. Die Klassen SQLiteLanguageModel und SQLiteModelFactory ermöglichen den Zugriff auf die SQLite Datenbank.

Die Text-Eingabe wird in Form einer soft Input View, DasherInputView genannt, auf dem Bildschirm dargestellt. Mit Hilfe dieser Klasse werden vom bereits eingetippten Text ausgehend die wahrscheinlichsten folgenden Buchstaben als Quadrate verschiedener Farben gezeichnet, wobei die Grösse der Quadrate der relativen Probabilität der jeweiligen Symbole entspricht (Abbildung 4). Der Benutzer hat auf der Abbildung bis dato den SMS-Text This prob mit Hilfe von Fingerbewegungen eingegeben; der PPM-Algorithmus hat bereits die zwei wahrscheinlichsten Pfade vorausberechnet: This probably/e und This problem(s). Man könnte aber auch einen anderen Pfad einschlagen: zum Beispiel könnte man Dasher in Richtung der Silbe cess lenken, um das Wort process zu schreiben; dadurch würde das bereits geschriebene b gelöscht. Die Quadrate verschiedener Farben stellen verschiedene Buchstaben dar; zwei aufeinanderfolgende Buchstaben gleicher Farbe zeichnet die App mit leicht verschiedenen Farbabstufungen. Der zuletzt geschriebene Buchstabe (in Abbildung 4 "b") erscheint rot.

Abbildung 3: Das Klassendiagramm des Android Clients von Dasher.
Abbildung 3: Das Klassendiagramm des Android Clients von Dasher.

Abbildung 4: Dasher Input-Feld auf einem Android Bildschirm in horizontaler Lage. Grau markiert sind Leerzeichen. Der Button ^ stellt von Klein- auf Großbuchstaben um.
Abbildung 4: Dasher Input-Feld auf einem Android Bildschirm in horizontaler Lage. Grau markiert sind Leerzeichen. Der Button ^ stellt von Klein- auf Großbuchstaben um.

Zentral für jede von InputMethodService abgeleitete Klasse ist die sogenannte Input View. Diese stellt eine Soft Input View dar, in welcher der Benutzer den Eingabetext generiert. Die Klasse DasherInputManager steuert somit einerseits den Zustand und das aktuelle Sichtbarkeitsfenster von Dasher und ermöglicht andererseits die Verbindung zwischen dem Dasher User Interface und dem eigentlichen Dasher Prediction Modell. Dazu gehört die Übertragung der entsprechenden Änderungen an das ursprüngliche Texteditor-Feld.

Die von SurfaceView abgeleitete Klasse DasherInputView zeichnet das Dasher User Interface auf den Bildschirm (Abbildung 4). Sie legt die Buchstabenquadrate aus und reagiert auf die Touch Events des Benutzers, um Dasher in Richtung des nächsten Buchstabens zu leiten, das heißt: im Dasher-Raum zu zoomen. Sobald das Quadrat eines Symbols das zentrale Kreuz bedeckt, wird es als gewählt betrachtet, und entsprechend ins Textfenster geschrieben. Im Fall, dass ein Quadrat das zentrale Kreuz wieder verlässt, wird hingegen das entsprechende Symbol aus dem Textfenster gelöscht.

Das Dasher User Interface muss laufend den Fingerbewegungen des Benutzers innerhalb der Texteingabefläche angepasst werden. Aus Geschwindigkeitsgründen zeichnen wir die Symbolquadrate mit Hilfe einer eigenen Canvas direkt auf die Surface (Zeichenoberfläche), anstatt den normalen Zeichnungsprozess der gesamten View-Hierarchie des Systems abzuwarten. Auf diese Weise rufen wir direkt die onDraw(Canvas)-Methode auf und haben somit die totale Kontrolle über das Navigieren im Dasher-Raum. Damit die App das Dasher UI flott aktualisiert, zeichnet sie die verschiedenen Dasher Wahrscheinlichkeitsquadrate in einem sekundären Thread (unabhängig vom demjenigen der UI Activity). Auf diese Weise kann Dasher, so schnell wie es der sekundäre Thread vermag, aufgefrischt werden.

Die Basisklasse von DasherInputView, SurfaceView, ist eine spezielle View-Klasse, die diese Art Darstellung unterstützt. Aus diesem Grund enthält unsere DasherInputView die von der Java Klasse Thread abgeleitete Klasse SurfaceRunLoop. Diese Klasse orchestriert das Layout und das Zeichnen der Symbolquadrate. Jede von SurfaceView abgeleitete Klasse muss das Interface SurfaceHolder.Callback implementieren, welches den Zugang zu der darunterliegenden Surface erlaubt und diese kontrolliert.

Die callback-Methoden surfaceCreated(SurfaceHolder), surfaceDestroyed(SurfaceHolder) und surfaceChanged(SurfaceHolder, int, int, int) informieren darüber, wenn die darunterliegende Surface kreiert, zerstört oder geändert wird. Wir haben die erste Methode so überschrieben, dass hier die SurfaceRunLoop kreiert und gestartet wird; in der zweiten Methode wird diese gestoppt. Die dritte Methode definiert, wie Dasher dargestellt werden soll, wenn die Dimensionen der DasherInputView geändert wurden. Dies ist zum Beispiel der Fall, wenn das Handygerät gedreht wurde. Dabei müssen die Hilfsklassen-Objekte, DasherViewPort und FrameManager, die neuen Masse der Eingabefläche erhalten, damit sie Dasher entsprechend anpassen können.

Die Klasse FrameManager ist für das Zoomen verantwortlich, wobei der momentane Ausschnitt des gesamten Dasher-Raums anhand der gegenwärtigen Fingerposition laufend verändert wird. Dasher kann aber auch mit Hilfe von pinch-Gesten bewegt werden. Zusätzlich ist sie auch für die Anpassung der Dasher-Laufgeschwindigkeit zuständig.

Die Klasse DasherViewPort berechnet anhand der vom FrameManager gelieferten Information die Position und Grösse der verschiedenen Symbolquadrate, die auf dem Bildschirm dargestellt werden. Des Weiteren testet sie, ob ein Quadrat so gross geworden ist, dass es das Zentralkreuz bedeckt.

Die Klassen SQLiteLanguageModel und SQLiteModelFactory erlauben den Zugriff auf die SQLite Datenbank.

Immer im Vollbild

Damit Dasher mehr Platz zum Zeichnen erhält, stellen wir dieTexteingabemethode immer im full-screeen Modus dar, was einige Anpassungen der

  • Damit Dasher den ganzen Bildschirm einnimmt, muss die Methode DasherInputManager.onEvaluateFullscreenMode() derart überschrieben werden, dass sie immer den boolean Wert true zurückgibt.
  • Die Android full-screen InputMethodService-Implementation bewirkt, dass der gewählte Input Method Editor (IME) den ganzen Bildschirm ausfüllt und somit die eigentliche Applikation vollständig zudeckt. Glücklicherweise generiert Android automatisch ein extra Editierfeld (vom Typ ExtractEditText) am oberen Bildschirmrand (Abbildung 4). Das neue Editierfeld ist eine Art Kopie des eigentlichen Texteditor-Feldes, das sich in der darunterliegenden Applikation befindet und in welches geschrieben wird. Ärger bereitet die Tatsache, dass dieses extra Editierfeld nur dann sichtbar wird, wenn die Input View genügend Platz dafür frei lässt. Dies stört vor allem im Landscape-Modus (Querformat). Deshalb mussten wir in der DasherInputView Klasse die Methode onMeasure() so überschreiben, dass die Höhe der Input View auf 5/8 der Höhe des Handydisplays beschränkt wird. (Dieser Bruchteil wurde empirisch bestimmt.)

Wichtige Attribute

Zusätzlich zum extra Editierfeld wird automatisch rechts daneben ein Aktion-Knopf angezeigt. Dieser wird mit dem android:imeOptions Attribut des darunterliegenden Texteditors definiert, und ermöglicht dem Benutzer einen schnellen Zugang zu den gebräuchlichen Funktionen wie zum Beispiel Go, Search, Send, Next und Done, ohne in die ursprüngliche Applikation zurückkehren zu müssen. Ein zweites wichtiges Attribut des Texteditors, android:inputType, informiert die Inputmethode, welche Art von Text zu erwarten ist. Die Input Methode erhält beim Starten jeder neuen Texteingabe diese Informationen in Form eines EditorInfo-Objektes. Je nach Textart könnte somit eine spezielle Textvorhersage-Datenbank angewendet werden. Daher ist es ratsam, diese Attribute im Hinblick auf verschiedene mögliche Texteingabe-Methoden genau zu definieren.

Die Eingabemethode

Damit Dasher jeder Text-Änderung angepasst werden kann, muss die Eingabemethode sowohl den aktuellen Text als auch die Cursor-Position im Editor-Feld der Applikation ermitteln können. Das InputConnection Interface des InputMethodService dient als Kommunikationskanal zwischen dem Texteditor und der Eingabemethode. Da eine Applikation mehrere Texteditorfelder enthalten kann, muss der aktuelle Kanal mit Hilfe von getCurrentInputConnection() immer neu bestimmt werden. Listing 1 zeigt als Beispiel, wie der Dasher-Service die aktuelle Cursor-Position innerhalb des Editor-Textes eruiert.

Listing 1

Cursorposition ermitteln

private int getCursorPosition(InputConnection connection) {
  ExtractedTextRequest req = new ExtractedTextRequest();
  req.flags = InputConnection.GET_TEXT_WITH_STYLES;
  req.hintMaxLines = 10;
  req.hintMaxChars = 10000;
  ExtractedText extracted = connection.getExtractedText(req, 0);
  if (extracted == null) {
    return -1;
  }
  return extracted.startOffset + extracted.selectionStart;
}

Wie bereits betont stellt das extra Editierfeld, das im full-screeen Modus erscheint, eine Art Kopie des darunterliegenden, aktiven Textfeldes dar. Die beiden kommunizieren aber miteinander: Wird dem Applikationstextfeld neuer Text via die InputConnection geschickt, so wird dieser im extra Editierfeld sofort aufgefrischt. Des Weiteren wird das Erstere benachrichtigt, sobald der Text im extra Editierfeld angeklickt oder über die Standard-Funktionen cut oder paste geändert wurde. Damit neue Symbole nicht nur am Ende, sondern auch innerhalb des aktuellen Textes eingefügt werden können, hält das System standardmässig jede Änderung der Cursorposition fest. Der neue Text wird dann mit Hilfe von setComposingText(<symbol>,1) an der entsprechenden Stelle angefügt (Listing 2).

Listing 2

Neuen Text einfügen

public void pushSymbol(Symbol symbol, boolean alt) {
 mComposing.append((alt <I> symbol.getVariant() : symbol.getLabel()));
         ...
 InputConnection icx = getCurrentInputConnection();
 if (icx != null)
  icx.setComposingText(mComposing, 1);
}

Die angefügten Zeichen müssen aber wieder gelöscht werden können; entweder sobald man im Dasher eine neue Richtung einschlägt und das Wahrscheinlichkeits-Quadrat des bereits gesetzten Symbols das zentrale Kreuz wieder verlässt, oder aber wenn man mit Dasher rückwärts navigiert. Um das zuletzt gesetzte Zeichen aus dem Textfeld zu löschen, werden die folgenden zwei rohen Tastenereignisse (raw key events) via die InputConnection an die Applikation geschickt (Listing 3).

Listing 3

Text löschen

icx.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
icx.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));

Alle Ereignisse, die durch eine Selektionsänderung im extra Editierfeld verursacht werden, müssen über die Methode onUpdateSelection(...) abgefangen werden. Hier muss die Methode der super Klasse aufgerufen werden, damit das Default-Verhalten bei solchen Ereignissen gewährleistet ist. Dasher wird entsprechend der Änderung der Cursorposition aufgefrischt: Das Quadrat des letzten Buchstabens vor dem Cursor wird in der Mitte des Bildschirms platziert, als ob es gerade das Zentralkreuz passiert hätte. Wichtig ist auch der Aufruf von finishComposingText(), damit die neue Position des Cursors auch im Applikation-Textfeld beibehalten bleibt.

Man sollte Dasher immer in einem geordneten Zustand verlassen, sobald man eine andere Eingabemethode im Kontextmenü des extra Editierfeldes auswählt. Aus diesem Grund haben wir die Methode onExtractTextContextMenuItem(int) überschrieben. Hier wird die volle Standard-Funktionalität des Menüs (wie z.B. cut und paste) von der Methode der super-Klasse gewährleistet; mit der Ausnahme, dass die Sprachmodell-Datenbank beim Verlassen von Dasher geschlossen wird, nachdem Dasher (falls erwünscht) mit dem bereits eingegebenen Text trainiert wurde.

Zoomen und Navigieren

Die Klasse SurfaceRunLoop ist der Motor unserer Applikation, deren Aufgabe darin besteht, nach vorherigem Verlauf und nach der aktuellen Fingerposition einen Ausschnitt des Dasher-Raums stets neu auf den Bildschirm zu zeichnen. Die Klasse wird instanziert und die Schleife gestartet, sobald die Surface zum ersten Mal erstellt wird. Solange die Surface nicht zerstört wird, bleibt die Schleife aktiv. Um auf der Surface zeichnen zu können, muss ein Objekt von Typ Canvas vom SurfaceHolder mit lockCanvas(Rect) herangeholt werden. Dasher wird darauf gezeichnet, und dann mit Hilfe von unlockCanvasAndPost(Canvas) auf der Surface dargestellt. Die Methode run() dieser Klasse ist in Listing 4 gezeigt.

Listing 4

Run-Methode

        @Override
        public void run() {
     Canvas canvas;
          try {
            while (running) {
          Rect rect = handler.beforeDrawing();
              canvas = null;
              try {
               canvas = surfaceHolder.lockCanvas(rect);
                   synchronized (contextLock) {
                    if (!running) { break; }
                    handler.onDraw(canvas); }
              } finally {
                if (canvas != null) {
                    surfaceHolder.unlockCanvasAndPost(canvas); }
              }
              if (!running) { break; }
               handler.afterDrawing(); }
           } finally { ... }
     }

Bevor man zeichnen kann, sollte Dasher in die gewünschte Richtung bewegt werden. Die Methode onTouchEvent(MotionEvent) berechnet ununterbrochen die aktuelle Position des Fingers. In Abhängigkeit dieser Position vergrössert die Methode beforeDrawing() den angepeilten Dasher-Raum-Ausschnitt, was den Eindruck des Zoomens zwischen den Symbolen hervorruft. Solange der Bildschirm nicht berührt wird, bleibt Dasher still. Ansonsten wird in dieser Methode die nächste Momentaufnahme des Bildschirms vorbereitet. Diese wird unter der Annahme berechnet, dass der momentane Berührungspunkt nach einer festgelegten Anzahl Schritte genau im zentralen Kreuz ankommen würde; dazu werden ganz einfache geometrische Regeln verwendet [1]. Nachdem der neue Dasher-Raum-Ausschnitt festgelegt worden ist, werden die Position und die Grösse der Symbolquadrate ihrer Auftrittswahrscheinlichkeit entsprechend neu berechnet. Die oben genannte Methode testet, ob das Symbolquadrat, welches das zuletzt geschriebene Zeichen darstellt, immer noch das Kreuz bedeckt. Ist dies nicht mehr der Fall, so wird das Symbol vom geschriebenen Text entfernt. Am Ende der Schleife wird in der Methode afterDrawing() geprüft, ob eines der unmittelbar nachfolgenden Symbolquadrate so gross geworden ist, dass es nun das Kreuz bedeckt. Trifft dies zu, wird der entsprechende Buchstabe in das EditText-Fenster geschrieben. Mit Hilfe dieser Schleife erreichen wir etwa 50-60 Updates der DasherInputView pro Sekunde.

Lebenszyklus von Dasher Service

Um einen zuverlässigen InputMethodService auf die Beine zu stellen, müssen wir sorgfältig wählen, welche der von Android zur Verfügung gestellten callback-Methoden wir implementieren sollen. Vor allem muss man bedenken, dass der InputMethodService immer aktiv ist, sobald das Android-Gerät eingeschaltet und Dasher das erste Mal gestartet wird. In unserem Fall ist noch das Zusammenspiel zwischen dem Service und der SurfaceView von Bedeutung. Deswegen ist der Lebenzyklus eines Android-InputMethodService für Dasher so wichtig. Detaillierte Informationen findet man in [2].

Beim erstmaligen Starten des Dasher Service wird die Methode onCreateInputView() aufgerufen. Hier konstruieren wir die LinearLayout View Komponente programmatisch, da sie nicht nur die eigentliche DasherInputView beinhaltet, sondern auch den Knopf, der die Umschaltung von kleinen zu grossen Buchstaben (und umgekehrt) ermöglicht. Solange der Service aktiv ist, wird diese Methode beim nächsten Öffnen einer Target-Applikation nicht mehr aufgerufen, sondern nur noch die Methode onStartInputView(EditorInfo, boolean). In Letzterer wird der Dasher-Raum-Auschnitt, der am Bildschirm gezeigt werden soll, dem gegenwärtigen Text und der Cursor-Position im aktuell-angeklickten EditText Feld angepasst und gezeichnet.

Sobald Dasher nicht mehr in Fokus ist, zum Beispiel wenn der Knopf Back gedrückt wird, wird die Methode onFinishInputView(boolean finishingInput) aktiv. Hier schliessen wir die Dasher Datenbank. Wir rufen hier auch die Methode der super Klasse auf, damit die aktuelle Cursor Position beim Verlassen des Editors erhalten bleibt. Das Android-Framework garantiert, dass das aktuelle Bild beim Drehen des Telefons korrekt dargestellt wird, indem das InputView Objekt anhand der neuen Bildschirmmasse neu rekonstruiert wird. In unserem Fall haben wir die surfaceChanged(...) Methode so implementiert, dass in der Längs- ein grösserer Dasher-Raum Auschnitt als in der Porträt-Konfiguration sichtbar ist.

Dasher Service laden

Ein InputMethodService ist bekanntlich keine Aktivität. Deshalb bedienen wir uns der PrefenceActivity als Startklasse unseres Dasher Service. Darin werden einige Dasher Optionen zur Wahl angeboten; vor allem wird aber die Dasher SQLite Datenbank aufs Gerät geladen. Listing 5 zeigt einen Ausschnitt der Datei AndroidManifest.xml, der Dasher als ein InputMethodService deklariert.

Listing 5

Dasher als Input-Service

<service android:name=".input.DasherInputManager"
  android:label="Dasher for Android IME"
  android:permission="android.permission.BIND_INPUT_METHOD">
  <intent-filter>
    <action android:name="android.view.InputMethod" />
  </intent-filter>
  <meta-data android:name="android.view.im" android:resource="@xml/method" />
</service>

Viele Android-Geräte beinhalten eine SD-Karte, die zusätzlichen Flash-Speicher zur Verfügung stellt. Diese Karte enthält viel mehr Speicherplatz als das im Handy eingebaute RAM, und ist darum ideal zum Speichern von Datenbanken. Unser PPM-4 basiertes prediction Modell, das mit einem für die englische Sprache repräsentativen Text trainiert wurde, beansprucht in der SQLite Datenbank etwa 4.86 MB. Damit das Trainieren mit beliebigen Texten auf dem Handy möglich wird, muss Dasher neue Datenbankeinträge auf der SD-Karte speichern können. Dafür sollte ab Android 1.6 in der AndroidManifest-Datei die folgende Erlaubnis stehen: <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>. Sowohl die Datenbank, als auch die binäre Datei LatinAlphabet.dac solten aufs Handy geladen werden. Die Letztere enthält sowohl eine Liste der von Dasher erkannten Buchstaben, und die entsprechenden Zeichen für Klein- und Grossbuchstaben, als auch die für das jewilige Symbolquadrat verwendete Farbe. Da XML Parsing auf Android umständlich und langsam ist, wurde diese Datei zwar zuerst in XML geschrieben, aber mit Hilfe des Java-Hilfsprogramms in ein binäres Format umgewandelt.

Fazit

Unser Dasher ist nahtlos im Android Programmierungsmodell integriert, was man nicht von der Version von Snowton [3] behaupten kann. Da die Sprachmodell-Datenbank auf der SD-Karte gespeichert wird, beansprucht unsere Dasher Implementation ungefähr 9 MByte weniger Speicherplatz auf dem Heap als die Referenz-Implementation [3]. Unsere Dasher Applikation wurde mit Android 2.3.3 und auf einem Google S Nexus getestet. Der gesamte Quellcode ist unter einer GPL-Lizenz frei erhältlich [4].

Infos

  1. Adaptive Computer Interfaces, David J. Ward, PhD Thesis, 2001: http://www.inference.phy.cam.ac.uk/djw30/papers/thesis.pdf
  2. Lebenszyklusschema eines Input Method Service: http://android-developers.blogspot.com/2009/04/creating-input-method.html
  3. Das Dasher Projekt: http://www.inference.phy.cam.ac.uk/dasher/
  4. Quellcode für die FHNW-Implentation von Dasher: http://www.fhnw.ch/personen/carlo-nicola/projekte
  5. Teil 1 zum Workshop finden Sie auf der Heft-CD, in Android User 01/2012 und online unter http://www.android-user.de/Magazin/Archiv/2012/01/Dasher-fuer-Android-implementieren/

Kommentiere den Artikel

Please enter your comment!
Please enter your name here