PlayBook-Software in ActionScript entwickeln

Playing the PlayBook
Kommentare

Iranische Generäle werden die Meldung, dass das PlayBook auf dem gleichen Betriebssystem wie die amerikanischen Kampfpanzer basiert, mit viel Freude vernommen haben. Der Autor kennt zwar keine iranischen Panzer, ist aber sicher, dass diese sich C++ bedienen …

Wer heute eine nativ wirkende Anwendung für das PlayBook genannte Tablet des BlackBerry-Herstellers Research in Motion entwickeln will, muss auf Adobe AIR zurückgreifen. Die ihm zugrundeliegende Sprache ActionScript wurde in dieser Ausgabe ja bereits vorgestellt. Jetzt gehts ans Eingemachte.

SDK beschaffen

Die Adobe-AIR-Entwicklung setzt logischerweise ein AIR SDK voraus. Man kann es unter [1] herunterladen. Der Autor hat es ins Verzeichnis C:AdobeAIRSDK installiert. Es empfiehlt sich, zur Anwendungsentwicklung auf Adobes Flash Builder 4.5 zurückzugreifen. Eine für 60 Tage lauffähige Version kann unter [2] bezogen werden. Wenn diese installiert ist, folgt das unter [3] zu beziehende PlayBook SDK. Dieser Artikel basiert auf Version 0.9.3. Der Autor hat das SDK ins Verzeichnis C:blackpadsdk installiert. Die Flash-Builder-Integration ist erwünscht und sollte bei der Installation des SDKs aktiviert werden. Da das PlayBook bis dato nicht im freien Handel erhältlich ist, bietet Research in Motion einen durch VMWare realisierten Emulator an. Er besteht aus dem unter [4] erhältlichen VMWare Player und dem unter [5] erhältlichen Simulatorimage (am besten auch in C:blackpadsdk installieren). Danach kann in VMWare eine neue VM auf Basis des Images BlackBerryPlayBookSimulator-0.9.3.iso erstellt werden. Als Gastbetriebssystem wird „Other“ festgelegt. Der Speicherausbau darf nicht weniger als 1024 MB betragen. In der Rubrik 3D Graphics muss die Checkbox „Accelerate 3D graphics“ markiert werden.

Erste Schritte

Nach diesen – zugegebenermaßen etwas lästigen – Vorbereitungsschritten ist es nun an der Zeit, eine erste Anwendung zu erstellen. Starten Sie Flash Builder, erledigen Sie die Eclipse-üblichen Vorbereitungen (Workspace festlegen) und erstellen Sie ein neues ActionScript-Mobile-Projekt namens MTPB. Als Zielplattform wird ausschließlich BlackBerry Tablet OS gewählt. Der Rest der Einstellungen bleibt wie vorgegeben. Öffnen Sie die Datei MTPB.as und ersetzen Sie den dort befindlichen Quellcode durch den aus Listing 1.

package 
{
  import flash.display.Sprite;
  import flash.events.MouseEvent;
  import flash.text.TextField;
  import flash.text.TextFormat;
  import qnx.ui.buttons.Button;
  import qnx.ui.buttons.LabelButton;
  
  [SWF(width="1024", height="600", backgroundColor="#cccccc", frameRate="30")]
  public class MTPB extends Sprite
  {
    public function MTPB()
    {
      var helloButton:LabelButton = new LabelButton();
      helloButton.label = "Hello World!";
      helloButton.x = (stage.stageWidth - helloButton.width)/2;
      helloButton.y = (stage.stageHeight - helloButton.height)/2;
      
      var myFormat:TextFormat = new TextFormat();
      myFormat.color = 0xAA0000;   
      myFormat.size = 24;  
      myFormat.italic = true;  
      myFormat.align = "center";
      
      var text:TextField = new TextField();
      text.text = "Close";
      text.setTextFormat(myFormat);
      
      var closeButton:Button = new Button();
      closeButton.addChild(text);    
      closeButton.x = (stage.stageWidth - closeButton.width)/2;
      closeButton.y = helloButton.y - helloButton.height;
      
      addChild(helloButton);
      addChild(closeButton);
      
      stage.nativeWindow.visible = true;
    }
  }
}

Unser erstes Beispiel besteht nur aus einem Konstruktor, der zwei Buttons erstellt und am Bildschirm positioniert. Beim ersten Button handelt es sich um einen LabelButton, der einem von Windows bekannten Command Button entspricht. Der zweite Button entstammt der abstrakteren Button-Klasse. Diese Klasse liefert selbst nur die Eventlogik und ein Rechteck. Will man in ihr Text anzeigen, braucht man zusätzlich ein Label. Labels sind Instanzen der Klasse TextField. Die Formatierung des Texts (Farbe etc.) wird durch ein TextFormat-Objekt bereitgestellt. MTPB selbst ist ein von Sprite abgeleitetes Objekt. Sprite ist eine Besonderheit von Adobe AIR. Ähnlich dem Formular unter Windows verwalten Sie Steuerelemente. Die erstellten Steuerelemente werden deshalb mittels addChild in den Sprite integriert und so zur Anzeige freigegeben.

Die Verwandtschaft zwischen Adobe AIR und Macromedia Flash begründet das Konzept der Display List, die in diesem Zusammenhang wichtig ist. Flash Clips bestehen aus mehreren Ebenen, die – miteinander kombiniert – eine Spielszene ergeben. In Adobe AIR gibt es statt diesen Ebenen die Display List, die Sprites und Steuerelemente kombiniert und in der z-Achse anordnet. Der Kopf dieses Baums wird als Stage bezeichnet.

Aufmacherbild: Miniature toy tank, isolated on white background von Shutterstock / Urheberrecht: yanugkelid

[ header = Debugging ]

Debugging

Die hier besprochenen Konzepte werden klar, wenn unser Programm läuft. Starten Sie deshalb die vorher in VMWare erstellte VM und öffnen Sie das Menü des Programmstarters, indem Sie den schwarzen Bereich über dem Emulator ins Emulatorfenster ziehen. Aktivieren Sie dann die Option Security und klicken Sie auf General. Legen Sie in der Rubrik General Settings ein Gerätepasswort fest und aktivieren Sie den Development Mode über das Toggle. Kehren Sie danach in den Programmstarter zurück. Oben rechts wird ein Hammerpiktogramm angezeigt. Ein Klick darauf zeigt die IP-Adresse des virtuellen PlayBooks. Die Tastenkombination Ctrl+Alt gibt die Maus frei. Im nächsten Schritt wird in Flash Builder eine neue Debuggingkonfiguration vom Typ Mobilanwendung für das Projekt erstellt. Als Startmethode wird On Device gewählt. Die IP-Adresse und das Gerätepasswort des virtuellen PlayBooks werden in die relevanten Felder eingegeben. Nach einem Klick auf Debug wird die Anwendung, wie in Abbildung 1 gezeigt, starten.

Abb. 1: MTPB in Aktion

Event-Handler

Unsere bis dato erstellten Steuerelemente sehen zwar sehr hübsch aus, sind aber funktionslos. Es ist also an der Zeit, sie mit Leben zu füllen. Das erledigt man durch Event-Handler. Wir wollen in unserem Programm auf Button-Klicks reagieren und verdrahten die Knöpfe im Konstruktor daher mit einem Event-Handler:

public function MTPB()
{
  . . .
  closeButton.addEventListener(MouseEvent.CLICK, closeWindow);
  helloButton.addEventListener(MouseEvent.CLICK, helloMe);
  
  addChild(helloButton);
  addChild(closeButton);
  
  . . .
}

Der Event-Handler für den Close-Knopf ist vergleichsweise primitiv:

private function closeWindow(event:MouseEvent):void
{
  stage.nativeWindow.close();
}

Er greift sich das der Stage zugehörige Betriebssystemfenster und schließt es. Da eine GUI-Anwendung ohne GUI nicht sehr sinnvoll ist, wird der Rest der Anwendung daraufhin ebenfalls über den Jordan geschickt. Der Handler für HelloWorld ist etwas länger. Er zeigt im Fall der Aktivierung eine systemmodale Messagebox an:

private function helloMe(event:MouseEvent):void
{
  var myDialog:AlertDialog = new AlertDialog();
  myDialog.title = "Antwort der Welt";
  myDialog.message = "Die Welt bedankt sich...";
  myDialog.addButton("OK");
  myDialog.dialogSize = DialogSize.SIZE_SMALL;
  myDialog.show();
}

Nach dem Hinzufügen folgender Imports kann die Anwendung abermals im Simulator ausgeführt werden. Klickt man nun auf einen der Buttons, wird die passende Aktion ausgelöst:

import qnx.dialog.AlertDialog;
import qnx.dialog.DialogSize;

[ header = Layouts und Menüs ]

Layouts und Menüs

Doch damit genug des Beispiels 1. Erstellen Sie ein neues Beispiel namens MTPB2 und definieren Sie seinen Konstruktor. Das Hauptfenster soll lediglich aus einem leeren weißen Formular bestehen:

public function MTPB2()
{
  super();
  
  stage.nativeWindow.visible = true;  
}

Strokes aus dem oberen Bereich aktivieren das Menü. Soweit, so klar – leider bieten weder Flash noch die QNX-UI-Widgets eine fertige Menüklasse. Der Autor realisiert Menüs in seinen Anwendungen als Gruppe von Buttons. Doch bevor wir ein Menü bauen, müssen wir den Stroke abfangen. Dazu fügen wir folgenden Code in den Konstruktor ein:

QNXApplication.qnxApplication.addEventListener(QNXApplicationEvent.SWIPE_DOWN, swipeDown);

Das Stroke-Event ist ein Event, das nur in QNX bekannt ist. Daher wird es von der QNXApplication-Klasse emittiert. Der Aufruf QNXApplication.qnxApplication schafft einen Verweis auf die Instanz dieser Singleton-Klasse. Der Event-Handler wird mit dem Code aus Listing 2 versehen:

private function swipeDown(event:QNXApplicationEvent):void
{
  clearWindow();
  
  //Container
  var myContainer=new Container;
  myContainer.flow=ContainerFlow.VERTICAL;
  
  var AboutButton:LabelButton=new LabelButton();
  AboutButton.label="Load";
  myContainer.addChild(AboutButton);
  
  var BackButton:LabelButton=new LabelButton();
  BackButton.label="Save";
  myContainer.addChild(BackButton);
  
  myContainer.setSize(stage.stageWidth,stage.stageHeight*0.7);
  addChild(myContainer);  
}

Diese Funktion erstellt einen Container. Container sind am ehesten mit den aus Qt bekannten Layouts zu vergleichen. Sie enthalten andere Steuerelemente und ordnen diese in einer vertikalen oder horizontalen Reihe an. Daraufhin werden zwei Knöpfe erstellt und zum Container gefügt. Am Ende wird die Breite des Containers festgelegt und der Container wird dem Sprite angegliedert. Ab nun sieht der Benutzer zwei Buttons – Load und Save –, die allerdings wegen der fehlenden Eventhandler nicht sonderlich viel tun. Übrigens: Die Funktion clearWindow ist nicht Teil des offiziellen SDKs. Sie wird folgendermaßen definiert und in das Programm eingebunden:

private function clearWindow():void
{
  while(numChildren > 0)
  {
    removeChildAt(0);
  }
}

Die Funktion ist simpel: Sie stellt fest, ob der sie enthaltende Sprite Kinder hat. Wenn ja, wird das erste Kind aus der Liste entfernt. Wenn alle Kinder entfernt sind, ist logischerweise (Displayliste) kein Steuerelement mehr am Sprite zu sehen. Danach kann man neue Steuerelemente hinzufügen und so ein neues „Formular“ erstellen.

Mehr Steuerelemente

Bis dato haben wir uns ausschließlich mit Buttons beschäftigt. Diese machen zweifellos Freude, reichen aber zum Erstellen einer produktiv nutzbaren Anwendung nicht aus. Im nächsten Schritt werden wir uns mit Checkboxen, Slidern und einer Textbox befassen. Der Konstruktor unseres Beispiels wird um das Codesegment aus Listing 3 ergänzt.

var myContainer=new Container;
myContainer.flow=ContainerFlow.VERTICAL;

var checkBox:CheckBox=new CheckBox();
checkBox.label="Markieren?";
myContainer.addChild(checkBox);

var sliderLabel:Label=new Label();
sliderLabel.text="Schiebewert: ";
var slider:Slider=new Slider();
myContainer.addChild(sliderLabel);
myContainer.addChild(slider);

var textField:TextInput=new TextInput();
myContainer.addChild(textField);

addChild(myContainer);

Im Vergleich zum vorigen Beispiel hat sich hier nicht viel geändert. Wie immer werden die Steuerelementinstanzen erstellt und dem Container hinzugefügt. Wird das Programm ausgeführt, präsentiert sich ein Bild des Grauens (Abbildung 2):

Abb. 2: Die Eigenintelligenz von PlayBook-Steuerelementen ist nicht besonders ausgeprägt …

Es gilt allgemein, dass PlayBook-Steuerelemente nicht wirklich in der Lage sind, ihre Größe selbst zu erfassen. Nach dem manuellen Zuweisen passender Parameter ist der Konstruktor um einiges länger (Listing 4).

var checkBox:CheckBox=new CheckBox();
checkBox.label="Markieren?";
checkBox.setSize(200,checkBox.height);
myContainer.addChild(checkBox);
      
var sliderLabel:Label=new Label();
sliderLabel.text="Schiebewert: ";
sliderLabel.setSize(200,sliderLabel.height);
var slider:Slider=new Slider();
slider.setSize(600,slider.height);
myContainer.addChild(sliderLabel);
myContainer.addChild(slider);

Allerdings ist das Ergebnis zweifellos brauchbarer, wie in Abbildung 3 zu sehen ist.

Abb. 3: Das Berechnen der Steuerelementgrößen von Hand lohnt sich…

Leider kleben die Steuerelemente eng zusammen. Die Lösung dafür ist der Spacer – ein unsichtbares Steuerelement, das sich so weit wie möglich ausbreitet. Der finale Konstruktor sieht dann aus wie in Listing 5 gezeigt.

public function MTPB2()
{
  QNXApplication.qnxApplication.addEventListener(QNXApplicationEvent.SWIPE_DOWN, swipeDown);

  var myContainer=new Container;
  myContainer.flow=ContainerFlow.VERTICAL;
  
  myContainer.addChild(new Spacer());
  
  var checkBox:CheckBox=new CheckBox();
  checkBox.label="Markieren?";
  checkBox.setSize(200,checkBox.height);
  myContainer.addChild(checkBox);
  myContainer.addChild(new Spacer());
      
  var sliderLabel:Label=new Label();
  sliderLabel.text="Schiebewert: ";
  sliderLabel.setSize(200,sliderLabel.height);
  var slider:Slider=new Slider();
  slider.setSize(600,slider.height);
  myContainer.addChild(sliderLabel);
  myContainer.addChild(slider);
  myContainer.addChild(new Spacer());
      
  var textField:TextInput=new TextInput();
  myContainer.addChild(textField);
  myContainer.addChild(new Spacer());
      
  addChild(myContainer);
      
      myContainer.setSize(stage.stageWidth,stage.stageHeight*0.7);
  stage.nativeWindow.visible = true;  
}

Das Endergebnis lässt sich sehen (Abbildung 4):

Abb. 4: Mit Spacern kombiniert, sieht unser Formular doch gleich mehr als brauchbar aus…

[ header = Daten remanent speichern ]

Daten remanent speichern

Anwendungen werden erst durch die Möglichkeit der lokalen Datenspeicherung wirklich nützlich. Daher werden wir die vorher erwähnten Load- und Save-Buttons des Menüs mit Logik versehen, welche die Werte lädt bzw. speichert. Doch dabei stehen wir einem gravierenden Problem gegenüber: Jeder Aufruf von clearWindow zerstört die auf dem Bildschirm befindlichen Steuerelemente. Auch muss nach dem Schließen des Menüs (wieder clearWindow) das Formular wiederhergestellt werden. Deshalb definieren wir im ersten Schritt eine neue Funktion – generateForm -, die den Sprite mittels clearWindow leert und das im vorigen Abschnitt entwickelte Formular danach neu erstellt. Diese Funktion wird aus dem Konstruktor aufgerufen. Die Werte der Steuerelemente werden wie die Steuerelemente selbst als Mitglieder der Klasse deklariert (Listing 6):

public var sliderVal:int=0;
public var checkBoxVal:Boolean=false;
public var textFieldVal:String=new String();
public var slider:Slider;
public var checkBox:CheckBox;
public var textField:TextInput;

public function MTPB2()
{
  QNXApplication.qnxApplication.addEventListener(QNXApplicationEvent.SWIPE_DOWN, swipeDown);
  restoreForm();
  stage.nativeWindow.visible = true;  
}

private function restoreForm():void
{
. . .

Den Steuerelementen werden schließlich von restoreForm die Werte der Variablen zugewiesen:

slider.value=sliderVal;
checkBox.selected=checkBoxVal;
textField.text=textFieldVal;

Im nächsten Schritt werden die Knöpfe des Menüs mit Listenern verdrahtet:

AboutButton.addEventListener(MouseEvent.CLICK,loadClicked);
BackButton.addEventListener(MouseEvent.CLICK,saveClicked);

Das Erstellen des Menüs würde allerdings die Werte unserer Steuerelemente vernichten. Deshalb sichern wir sie vor dem Aufruf von clearWindow() wie in Listing 7:

private function swipeDown(event:QNXApplicationEvent):void
{
  sliderVal=slider.value;
  checkBoxVal=checkBox.selected;
  textFieldVal=textField.text;
  
  clearWindow();
  
  //Container

Der Speichern-Listener sieht dann so aus wie in Listing 8.

private function saveClicked(event:MouseEvent):void
{
  var dataFile:File = File.applicationStorageDirectory.resolvePath("settings.txt");
  var stream:FileStream;
  stream = new FileStream();
  stream.open(dataFile, FileMode.WRITE);
  stream.writeInt(sliderVal);
  stream.writeBoolean(checkBoxVal);
  stream.writeUTF(textFieldVal);
  stream.close();
  clearWindow();
  restoreForm();
}

Im ersten Schritt wird dabei eine File-Instanz erstellt, die auf eine Datei namens settings.txt im Ordner der eigenen Anwendung zeigt. Im nächsten Schritt wird um diese Instanz ein FileStream geschaffen, in den die Werte der einzelnen Steuerelemente nacheinander geschrieben werden.
Zuletzt wird das Menü durch Aufruf von clearWindow abgetragen und das Formular durch restoreForm wiederhergestellt. Das Laden erfolgt nicht unähnlich (Listing 9).

private function loadClicked(event:MouseEvent):void
{
  var dataFile:File = File.applicationStorageDirectory.resolvePath("settings.txt");
  if(dataFile.exists)
  {
    var stream:FileStream;
    stream = new FileStream();
    stream.open(dataFile, FileMode.READ);
    sliderVal=stream.readInt();
    checkBoxVal=stream.readBoolean();
    textFieldVal=stream.readUTF();
    stream.close();
  }
  clearWindow();  
  restoreForm();
}

Der Aufruf von dataFile.exists() prüft, ob die angegebene Datei existiert. Wenn ja, wird abermals ein Strom geöffnet. Die Werte werden nacheinander ausgelesen und in die Steuerelemente zurückgeschrieben. Der Strom wird danach geschlossen. In der Debug-Konfiguration darf beim Testen derartiger Anwendungen die Einstellung Anwendungsdateien löschen in keinem Fall aktiviert werden. Ansonsten würde die IDE unsere Datendatei nach jedem Neu-Deployment der Anwendung selbsttätig löschen.

[ header = Von Icons und XML-Files ]

Von Icons und XML-Files

Research in Motion hat bis zum Zeitpunkt der Drucklegung dieser Ausgabe keine signierten Anwendungen freigegeben. Die hier wiedergegebenen Schritte basieren auf der Dokumentation und den im Unternehmen des Autors verwendeten Prozessen. Im ersten Schritt werden unter [6] Keys für das PlayBook angefordert. Diese werden innerhalb von vier bis fünf Arbeitstagen per E-Mail als .csj-Datei ausgeliefert und sind für die folgenden Schritte zwingend erforderlich. Ebenso zwingend erforderlich ist ein Icon im PNG-Format, das im Idealfall 72×72 Pixel groß ist. Das Icon wird in den /src/-Ordner des Projekts kopiert. Danach wird die beschreibende .xml-Datei (Appname-app.xml) rechts angeklickt und mittels ÖFFNEN MIT | TEXTEDITOR zur Bearbeitung geöffnet. Das Feld Copyright ist nach der Anwendungserstellung auskommentiert:

<!-- Copyright information. Optional -->
  <!-- <copyright></copyright> -->

Es wird durch das Entfernen des Kommentarmarkers aktiviert und mit dem unter [6] ins Feld Company Name eingegebenen Wert versehen:

<!-- Copyright information. Optional -->
  <copyright>Tamoggemon Limited</copyright>

Im nächsten Schritt wird der icon-Block gesucht, von unnötigen Einträgen befreit und durch das Icon ergänzt:

<icon>
    <image72x72>icon-bicatab.png</image72x72>
  </icon>

Im letzten Schritt kann die Versionsnummer angepasst werden. Das Programmicon ist dann einsatzbereit (und wird nach dem nächsten Debuggen im Simulatore angezeigt). Das PlayBook verlangt zusätzlich eine Datei namens blackberry-tablet.xml, die sich im selben Ordner wie die Anwendungs-XML befinden muss. Ihr Inhalt folgt dem Schema unten:

<qnx>
  <initialWindow>
    <systemChrome>none</systemChrome>
    <transparent>true</transparent>
  </initialWindow>
  <publisher>Tamoggemon Limited</publisher>
  <category>core.internet</category>
    <icon>
    <image>icon-bicatab.png</image>
  </icon>
</qnx>

Signaturen und andere Albträume

Bevor eine Anwendung in die App World hochgeladen werden kann, muss sie signiert werden. Die Signierung muss durch ein von RIM freigegebenes Terminal erfolgen. Der erste Schritt ist daher, das eigene Terminal bei den Kanadiern anzumelden. Dazu fügt man den Pfad zum /bin/-Verzeichnis des SDKs zur Path-Variable des Betriebssystems hinzu, öffnet eine Eingabeaufforderung und navigiert in ebendieses Verzeichnis. Dort erstellt man mittels blackberry-signer eine CSK-Umgebung:

blackberry-signer -csksetup -cskpass <eigenesPasswort>

Danach kopiert man die per E-Mail erhaltene .csj-Datei in den /bin/-Ordner und registriert sein Terminal mit folgendem Befehl bei RIM:

blackberry-signer -register -csjpin <PIN aus Formular 6>
-cskpass <eigenesPasswort> <Name der CSJ-Datei>

Die in den letzten Schritten erstellte Information wird nun mittels blackberry-keytool in eine .p12-Datei namens output.p12 gepackt:

blackberry-keytool -genkeypair -keystore output.p12
-storepass <eigenesPasswort > -dname "cn=<Firmenname aus Formular 6>" -alias author

Damit ist das eigene Terminal signierbereit. Die zu signierende Anwendung muss im so genannten Produktionsmodus kompiliert werden. Aufgrund eines bekannten Fehlers in Flash Builder ist das nicht ohne Weiteres möglich. Daher klickt man rechts auf das Projekt, wählt Eigenschaften | ActionScript-Compiler und ergänzt das Feld Zusätzliche Compiler-Argumente um folgenden String (Trennung von anderen Parametern durch Leerzeichen):

-debug=false

Nach einer Neuerstellung des Projekts durch Projekt | Bereinigen, ist die im Ordner /bin-debug/ befindliche .swf-Datei um einige Prozent geschrumpft. Sie wird zusammen mit der blackberry-tablet.xml, der Appname-app.xml und dem Icon in den /bin/-Ordner des SDKs kopiert und mit folgendem Befehl in eine .bar-Datei verwandelt:

blackberry-airpackager -package <ausgabebarname.bar> <project_name-app.xml> <project_name.swf> blackberry-tablet.xml <iconname>

Die .bar-Datei wird mit nachstehenden Kommandos signiert. Eine Internetverbindung ist dabei zwangsweise erforderlich:

blackberry-signer -verbose -cskpass <eigenesPasswort> -keystore output.p12 -storepass <eigenesPasswort> <BAR_file.bar> RDK

blackberry-signer -keystore output.p12 -storepass <eigenesPasswort> <BAR_file.bar> author

Damit ist die .bar-Datei fertig und kann in die App World hochgeladen werden.

Fazit

Obwohl insbesondere im Bereich Packaging und native Entwicklung noch einige Fragen offen stehen, ist es schon jetzt möglich, Anwendungen für das PlayBook ohne allzu großen Aufwand zu entwickeln. Allerdings: Die Attraktivität einer Plattform hängt nicht wirklich von den Entwicklungswerkzeugen ab. Viel wichtiger ist es, möglichst viele Kunden erreichen zu können. Das PlayBook hat in diesem Bereich zweifellos gravierenden Nachholbedarf. Aber – wie heißt es doch so schön? No risk, no fun…

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -