Symbian OS
Python / PyS60
1 Andreas Jakl, 2009 v1.2 – 01 March 2009 PyS60 – Examples
● ShakerRacer
www.youtube.com/watch?v=EMjAYdF13cU
2 Andreas Jakl, 2009 PyS60 – Examples
● PyWuzzler (Benjamin Gmeiner, Yen-Chia Lin)
3 Andreas Jakl, 2009 Controlling Super Mario PyS60 – Examples by movements (jumping, walking, ...)
● NiiMe
Playing drums
Controlling flight / racing games with tilting
http://www.niime.com/
4 Andreas Jakl, 2009 PyS60 – Examples Time-lapse Photography http://blog.foozia.com/blog/2007/jan/21/python-s60-time-lapse-photography-using-nokia-n80/
aspyplayer Last.fm-Player for PyS60 http://code.google.com/p/aspyplayer/
pyPoziomica Use your phone as a level tool http://www.symbian-freak.com/news/007/12/pypoziomica_freeware_level_tool.htm PyED Edit Python source code on the phone http://sourceforge.net/projects/pyed/
5 Andreas Jakl, 2009 What is it all about? Python
6 Andreas Jakl, 2009 Python
● Older than you might think: 1989 – 1991 by Guido van Rossum (National Research Institute for Mathematics and Computer Science, Netherlands) Named after Monty Python’s Flying Circus
● Today: Microcontrollers Mobile Phones Web servers
7 Andreas Jakl, 2009 Scalable
● Modular architecture
● Easy code reuse
● Huge Python standard library
● Extension with own C/C++ modules
Shell scripts
Huge projects
8 Andreas Jakl, 2009 Mature
Helpful Object Interpreted error oriented, Very high & byte- messages, memory level compiled exception manager handling
9 Andreas Jakl, 2009 Fast
● Rapid Application Prototyping
● Easy to learn – can you read this code?
import inbox, audio
box = inbox.Inbox() msg_id = box.sms_messages()[0] msg_txt = u"Message: " + box.content(msg_id)
audio.say(msg_txt)
● What would this be like in C++ or Java ME? ...
10 Andreas Jakl, 2009 Symbian OS + Runtimes
Java ME Python .net Basic Perl
Widgets Apache / Silverlight (Web Flash Lite Ruby PHP / MySQL (soon) Runtime)
S60 (C++)
Symbian OS
11 Andreas Jakl, 2009 UI Platforms: S60
● Unified UI platform based on S60 Official UI platform of Symbian Foundation Former name: Series 60 th Nokia N97 ● Touchscreen support with S60 5 Edition
12 Andreas Jakl, 2009 UI Platforms: S60 www.s60.com
Business High-End Multimedia Mass Market
Nokia N96
Nokia E66 Samsung Omnia HD Nokia 6121 Classic Nokia 5800 XPressMusic
Nokia E71
Nokia N85 SE Idou Nokia 5500 Sport Nokia E90 Samsung INNOV8
13 Andreas Jakl, 2009 Nokia 6210 Navigator PyS60
● Python port to S60
● Web Allows easy access to: Flash Python Accelerometer Managed code Camera Java
Text-to-speech P.I.P.S. Location Ease of development of Ease Symbian Web services Native code C++
Messaging Functionality and performance Bluetooth UI
14 Andreas Jakl, 2009 Setup – Phone PyS60 1.4.x: based on Python 2.3 PyS60 1.9.2+: based on Python 2.5.1, supports new sensor framework ● Install the latest Nokia PC Suite: of S60 3rd Ed., FP2+ http://europe.nokia.com/A4144903
● Download and install PyS60: 1.4.x: http://sourceforge.net/projects/pys60/ 1.9.x+: https://garage.maemo.org/projects/pys60/
● Phone: Phone software: PythonForS60_1_x_x_3rdEd.SIS Phone script shell: PythonScriptShell_1_x_x_3rdEd.SIS
15 Andreas Jakl, 2009 Setup – PC
● Extract SDK plug-in to the S60 SDK: PythonForS60_1_x_x_SDK_3rdEd.zip
16 Andreas Jakl, 2009 IDEs
● IDLE – comes with Python SDK C:\Program Files\Python\Lib\idlelib\idle.bat
● PythonWin + Win32 Extensions http://sourceforge.net/projects/pywi n32/
● PyDev Eclipse/Carbide.c++ Plug-in http://pydev.sf.net/
● SPE http://pythonide.stani.be/
17 Andreas Jakl, 2009 Hello World
● Create a file hello.py: hello.py print “Hello World”
● Connect your phone to the PC (“PC Suite” connection mode)
● Transfer the script to E:\Python\ (memory card) using the Nokia PC suite file manager
● Run the script:
18 Andreas Jakl, 2009 Hello World – Emulator
● Copy hello.py to:
● Start the emulator from:
19 Andreas Jakl, 2009 PyDev + Emulator
● Either create the project / workspace directly in the python-dir of the emulator
● Or link the project files to source files in the dir:
20 Andreas Jakl, 2009 Starting with the UI PyS60 – User Interface
21 Andreas Jakl, 2009 Module
● Collection of related functions and data grouped together
● Has to be imported at the beginning: import appuifw
● Addressing a function of the module: appuifw.query(label, name)
● Import multiple modules in a single statement: import appuifw, e32
22 Andreas Jakl, 2009 import appuifw
Query appuifw.query(u"Type a word:", "text", u"Hello") appuifw.query(u"Type a number:", "number", 7) appuifw.query(u"Type a date:", "date") appuifw.query(u"Type a time:", "time") appuifw.query(u"Type a password:", "code") appuifw.query(u"Do you like PyS60?", "query") Syntax: appuifw.query(label, type[, initial value])
number / text float date time code query
23 Andreas Jakl, 2009 import appuifw
Note Dialog appuifw.note(u"Hello") appuifw.note(u"File not found", "error") appuifw.note(u"Upload finished", "conf")
Syntax: appuifw.note(text[, type[, global] ] )
info / default error conf
24 Andreas Jakl, 2009 Variables
● Variables not declared ahead of time
● Implicit typing
● Automated memory management Reference counting, garbage collection
● Variable names can be “recycled”
● del statement allows explicit de-allocation
25 Andreas Jakl, 2009 Variables – Example age = 5 name = u"Andreas" # u in front of the string: unicode name += age # Doesn't work, the type isn’t converted automatically name += str(age) # name == Andreas5 name = age # name now points to the same object as age; name == 5 foo = "xyz" foo “xyz” bar = foo # bar points to the same object as foo bar foo = 123 # a new object is created for foo, bar still points to "xyz“ foo “xyz”
bar 123
26 Andreas Jakl, 2009 Multi-Query Dialog
● Syntax: appuifw.multi_query(label1, label2)
import appuifw
pwd = u"secret"
info = appuifw.multi_query(u"Username:", u"Password:") if info: // returns a tuple with the info login_id, login_pwd = info if login_pwd == pwd: appuifw.note(u"Login successful", "conf") else: appuifw.note(u"Wrong password", "error") else: // returns None – special type appuifw.note(u"Cancelled")
27 Andreas Jakl, 2009 if-statement
● Works like in other languages
● Blocks are defined by indentation – avoids dangling else
● if expression1: expr1_true_suite elif expression2: expr2_true_suite else: none_of_the_above_suite
28 Andreas Jakl, 2009 Lists, Tuples and Dictionaries
● Generic “arrays” for arbitrary number of arbitrary objects
● Ordered and accessed via index offsets
● List – created using [ ] myList = [1, 2, 3, 4] # [1, 2, 3, 4] print myList[0] # 1 # Subsets: sequence[starting_index:ending_index] print myList[1:3] # [2, 3] print myList[2:] # [3, 4] print myList[:3] # [1, 2, 3] myList[1] = 5 # [1, 5, 3, 4] myList[2] = ["bla", (-2.3+4j)] # [1, 5, ["bla", (-2.3+4j)], 4] print myList[2][1] # -2.3+4j print 4 in myList # True 29 Andreas Jakl, 2009 Lists, Tuples and Dictionaries
● Tuple – created using ( ) Immutable – can therefore be used as dictionary keys Also useful when you don’t want a function to be able to change your data
myTuple1 = ('python', 's60', 27) print myTuple1[0] # python myTuple[1] = 'no' # tuples are immutable – exception! myTuple2 = ('symbian', 'uiq') myTuple3 = myTuple1 + myTuple2 print myTuple3 # ('python', 's60', 27, 'symbian', 'uiq')
30 Andreas Jakl, 2009 Lists, Tuples and Dictionaries
● Dictionary – created using { } Mapping type – like associative arrays or hashes in Perl Key-value pairs
– Keys: almost any Python type, usually numbers or stings
– Values: arbitrary Python object
myDict = {'planet': 'earth'} myDict['port'] = 80 print myDict # {'planet': 'earth', 'port': 80} print myDict.keys() # ['planet', 'port'] print myDict['planet'] # earth for key in myDict: print "key=%s, value=%s" % (key, myDict[key]) # key=planet, value=earth # key=port, value=80
31 Andreas Jakl, 2009 Popup Menu
● Syntax: appuifw.popup_menu(list[, label ])
import appuifw
items = [u"The Journey", u"RealReplay", u"ShakerRacer"] index = appuifw.popup_menu(items, u"Buy:")
if index == 0: appuifw.note(u"Great choice") elif index == 1: appuifw.note(u"Cool") elif index == 2: appuifw.note(u"I like that") elif index == None: appuifw.note(u"Purchase cancelled")
32 Andreas Jakl, 2009 Selection List Search field appears after pressing a letter key
● Syntax: appuifw.selection_list(choices[, search_field=0])
import appuifw
names = [u"Michael", u"Devon", u"Bonnie", u"April", u"RC3"] index = appuifw.selection_list(names, 1)
if index == 2: print "I love you!" else: print "You're great!"
33 Andreas Jakl, 2009 Multi-Selection List
● Syntax: appuifw.multi_selection_list( choices[, style=„checkbox‟, search_field=0])
import appuifw
names = [u"Michael", u"Devon", u"Bonnie", u"April", u"RC3"] selections = appuifw.multi_selection_list(names, 'checkbox', 1) print selections
Style: checkmark Select multiple items with the pen key Not really important
34 Andreas Jakl, 2009 for loop
● for iter_var in iterable: suite_to_repeat
● With each loop, iter_var set to current element of iterable Similar to foreach in other languages
35 Andreas Jakl, 2009 for Loop – Examples
# Iterating over a string current letter: T for eachLetter in "Text": current letter: e print "current letter:", eachLetter current letter: x current letter: t # Iterating by sequence item Charles nameList = ["Mike", "Sarah", "Charles"] for eachName in sorted(nameList): Mike print eachName Sarah
# Iterating by sequence index Mike for nameIndex in range(len(nameList)): Sarah print nameList[nameIndex] Charles # Iterate over a range value: 0 for eachVal in range(3): print "value: ", eachVal value: 1 value: 2 # Extended syntax: # range(start, end, step = 1) value: 2 for eachVal in range(2, 10, 3): print "value: ", eachVal value: 5 value: 8
36 Andreas Jakl, 2009 while loop
● while expression: suite_to_repeat
● Nice addition: else is executed if loop was not abandoned by break
def showMaxFactor(num): count = num / 2 while count > 1: if num % count == 0: print "Largest factor of %d is %d" % (num, count) break count -= 1 else: print num, "is prime"
for eachNum in range(10, 21): showMaxFactor(eachNum)
37 Andreas Jakl, 2009 Example – System Info SMS import appuifw, messaging, sysinfo
# You could use a dictionary as well, but here this is more straightforward later on infoNames = [u"Profile", u"Battery", u"Signal DBM"] infoCalls = ["sysinfo.active_profile()", "sysinfo.battery()", "sysinfo.signal_dbm()"]
# Let the user choose the information he wants to send choices = appuifw.multi_selection_list(infoNames, "checkbox", 0) infoSms = "" for idx in choices: # Execute the statement(s) stored in the infoCalls-list through the eval-statement, # convert the result to a string and append it to the sms text infoSms += infoNames[idx] + ": " + str(eval(infoCalls[idx])) + "; "
# Query the telephone number smsNum = appuifw.query(u"Number:", "text", u"+15550135") if smsNum: # Send the SMS if the user didn’t cancel messaging.sms_send(smsNum, infoSms) appuifw.note(u"Info sent", "conf")
38 Andreas Jakl, 2009 Procedural and Object Oriented Development Python – Function and Classes
39 Andreas Jakl, 2009 Functions – Basics
● def function_name(arguments): [“function_documentation_string”] function_body_suite
● Supports: def foo(): "foo() -- does't do anything special." Default arguments print "in foo"
Variable length arguments # Execute the function foo() Multiple return values # Print the help text of the function print foo.__doc__ Inner functions # foo() -- does't do anything special. ...
40 Andreas Jakl, 2009 Functions – Default Arguments
● Default arguments: def func(posargs, defarg1=dval1, defarg2=dval2, ...):
def calcTax(amount, rate=0.0275): return amount + (amount * rate)
print calcTax(100) # 102.75
print calcTax(100, 0.05) # 105.0
41 Andreas Jakl, 2009 Functions – Variable Length Arguments
● Variable length arguments (unknown number): Non-keyword variable arguments (tuple): def func([formal_args,] *vargs_tuple): Argument with * will hold all remaining arguments once all formal parameters have been exhausted Keyword variable arguments (dictionary): def func([formal_args,][*vargst,] **vargsd):
def tupleVarArgs(arg1, arg2='defaultB', *theRest): tupleVarArgs('abc') print "formal arg 1: ", arg1 # formal arg 1: abc print "formal arg 2: ", arg2 # formal arg 2: defaultB for eachXtraArg in theRest: print 'another arg: ', eachXtraArg tupleVarArgs('abc', 123, 'xyz', 123.456) # formal arg 1: abc # formal arg 2: 123 # another arg: xyz # another arg: 123.456
42 Andreas Jakl, 2009 Variable Scope global_str = "foo" def foo(): local_str = "bar"
local scope local return global_str + local_str ● Global scope Declared outside a function print foo() Lifespan lasts as long as script is running
● Local scope Live temporarily as long as function they are defined in is active
● Searching for identifiers First local, then global Possible to override global variables by creating a local one
43 Andreas Jakl, 2009 Variable Scope
● Writing to global variables in functions: Creates a new local variable (pushes global variable out of scope) global statement specifically references a named global variable
def foo(): def foo(): localscope bar = 200 global bar print "in foo(), bar is", bar bar = 200
local scope local print "in foo(), bar is", bar bar = 100 print "in __main__, bar is", bar bar = 100 foo() print "in __main__, bar is", bar print "in __main__, bar still is", bar foo() in __main__, bar is 100 print "in __main__, bar is now", bar in foo(), bar is 200 in __main__, bar is 100 in __main__, bar still is 100 in foo(), bar is 200 in __main__, bar is now 200 44 Andreas Jakl, 2009 Classes
● class MyObject(bases): “Documentation text” class_suite
● New style classes should be derived from any other class or from object
● Simplest use: container object for instance attributes Not defined in class definition class MyData(object): pass # code is required syntactically, Only valid for this instance # but no operation is desired mathObj = MyData() mathObj.x = 4 mathObj.y = 5 45 Andreas Jakl, 2009 print mathObj.x * mathObj.y # 20 Classes – Subclasses, Methods
class LibraryEntry(object): ● __init__ def __init__(self, id, title): self.id = id similar to constructor self.title = title def updateId(self, newId): ● self-parameter self.id = newId
Passes reference to class LibraryBook(LibraryEntry): def __init__(self, id, title, author): current instance LibraryEntry.__init__(self, id, title) self.author = author Not needed for static or class methods def updateAuthor(self, newAuthor): self.author = newAuthor Python wants to be libBook = LibraryBook(1, "PyS60", "Andreas Jakl") explicitly clear libBook.updateId(2) libBook.updateAuthor("Nokia")
46 Andreas Jakl, 2009 Classes – Attributes
● Instance attributes are set “on-the-fly” by using them Constructor is the first place to set instance attributes Use default arguments for default instance setup
● Class variables / static data have to be defined class Letter(object): class C(object): def __init__(self, text, author="Andreas Jakl", category=“love"): foo = 100 # Static data self.text = text self.author = author print C.foo # 100 self.category = category C.foo = C.foo + 1 def printLetter(self): print C.foo # 101 print self.text, self.author, self.category
['__class__', '__delattr__', '__dict__', '__doc__', loveLetter = Letter("I love you") '__getattribute__', '__hash__', '__init__', print dir(loveLetter) # Print all methods & attributes of this class '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', 47 Andreas Jakl, 2009 '__str__', '__weakref__', 'author', 'category', 'printLetter', 'text'] How to organize your application PyS60 – Application Structure
48 Andreas Jakl, 2009 Application Structure
Title appuifw.app.title Navigation pane appuifw.app.set_tabs() Body appuifw.app.body
Dialog appuifw.
Menu Exit appuifw.app. appuifw.app.menu exit_key_handler
49 Andreas Jakl, 2009 UI App – Example
import appuifw, e32
def quit(): print "Exit key pressed" app_lock.signal()
appuifw.app.exit_key_handler = quit appuifw.app.title = u"UI App"
print "App is now running"
app_lock = e32.Ao_lock() app_lock.wait()
print "Application exits"
50 Andreas Jakl, 2009 Callback Function
● No difference to normal functions
● Associating function with event: binding Use function name without () to get the function object Compare to function pointers in C
def quit(): pass # function is empty (instead of {} in C++ / Java)
appuifw.app.exit_key_handler = quit
51 Andreas Jakl, 2009 Wait for the User
● Previously: app. exited after executing all lines
● Event based UI-app: wait for user input
... app_lock = e32.Ao_lock() # Create an instance of an Ao_lock object app_lock.wait() Refer to the Symbian OS course for more information about Waiting for quit() call-back handler Active Objects events app_lock.signal()
52 Andreas Jakl, 2009 Application Body
appuifw.app.screen = “normal" appuifw.app.screen = “large" appuifw.app.screen = "full"
53 Andreas Jakl, 2009 Application Body
● You can assign several objects to the body Canvas: provides drawable screen area + support for handling raw key events Form: complex forms with various input fields Listbox: shows a list of items (single- or double-line-item) Text: free-form text input
54 Andreas Jakl, 2009 Application Menu
import appuifw, e32 ● Defined using tuples: def play(): Text print "Play file" def volume_up(): Call-back function print "Volume up" def volume_down(): – Specify another print "Volume down"
tuple instead to def quit(): print "Exit key pressed" define a submenu app_lock.signal()
appuifw.app.exit_key_handler = quit appuifw.app.title = u"mp3 Player" appuifw.app.menu = [(u"Play", play), (u"Volume", ( (u"Up", volume_up), (u"Down", volume_down) ) )] submenu print "App is now running" app_lock = e32.Ao_lock() app_lock.wait() Example based on [1] 55 Andreas Jakl, 2009 String Manipulation txt = "I like Python" url = " http://www.mopius.com " print txt[2:6] url = url.strip() print txt.find("like") if url.startswith("http://"): if txt.find("love") == -1: print url, "is a valid URL" print "What's wrong with you?" txt.replace("like", "love") webServiceInput = " 1, 2, 3, 4" print webServiceInput.replace(" ", "") print txt.upper() print "Length", len(txt) txt = "one;two;three" print txt.split(";") txt2 = "" http://www.mopius.com is a valid URL if txt2: 1,2,3,4 print "txt2 contains characters" ['one', 'two', 'three'] else: print "txt2 doesn't contain characters" like 2 What's wrong with you? I LIKE PYTHON Length 13 txt2 doesn't contain characters
56 Andreas Jakl, 2009 String Formatting
● String slicing like for lists: [start:end]
● Assemble string based on other variables:
print "Host: %s\tPort: %d" % ("Earth", 80) print "DD.MM.YYYY = %02d.%02d.%d" % (12, 3, 82) list = [3, 2, 1, "go"] # A list has to be converted to a tuple first print "Counting: %d, %d, %d, %s" % tuple(list) Symbol Conversion Host: EarthPort: 80 %s String conversion via str() prior DD.MM.YYYY = 12.03.82 to formatting Counting: 3, 2, 1, go %d Signed decimal integer %f Floating point real number ......
57 Andreas Jakl, 2009 Example – Inbox Search
import inbox, appuifw
# Create an instance of the Inbox object box = inbox.Inbox() # Query search phrase query = appuifw.query(u"Search for:", "text").lower() hits = [] ids = [] # sms_messages() returns message IDs for all messages in the SMS inbox for sms_id in box.sms_messages(): # Retrieve the full message text and convert it to lowercase msg_text = box.content(sms_id).lower() if msg_text.find(query) != -1: # If the text was found, store a preview hits.append(msg_text[:25]) ids.append(sms_id)
# Display all results in a list index = appuifw.selection_list(hits, 1) if index >= 0: # Show the full text of the selected message appuifw.note(box.content(ids[index]))
58 Andreas Jakl, 2009 Event Loop
● e32.Ao_lock waits for events, but stops execution
● Game: App. has to be active all the time But still needs respond to events (keys, ...)
Initialize event call-backs Sleep (+ execute waiting active objects): while
59 Andreas Jakl, 2009 The Python way of Exception Handling
60 Andreas Jakl, 2009 Exceptions
● Example: Exception “NameError” raised by the interpreter:
>>> print foo Traceback (most recent call last): File “
61 Andreas Jakl, 2009 Exceptions
● try: try_suite # watch for exceptions here except Exception[, reason]: except_suite # exception-handling code
● Different objects derived from Exception
● Exception arguments / reasons: May be passed along Not just a string, but contains more information
62 Andreas Jakl, 2009 Exceptions – Information
try: f = file(u"c:\\python\\test.txt", "w+") print >> f, "Welcome to Python" f.seek(0) print "File contents:", f.read() f.close() except IOError, reason: [Errno 2] No such file or print reason directory: u'c:\\python\\test.txt'
print reason.filename c:\python\test.txt print reason.errno 2 print reason.strerror No such file or directory
63 Andreas Jakl, 2009 Multiple Exceptions
● Catching multiple exceptions: Multiple except-statements Multiple exceptions in one except statement Catch the base class Exception (no good coding style – you might be silently dropping errors)
64 Andreas Jakl, 2009 Code Examples
● Code snippets for: Camera Text to Speech Sound recording / playing Bluetooth Networking http://www.mobilenin.com/pys60/menu.htm
Graphics, UI Additional modules: 3D (Open GL ES) http://cyke64.googlepages.com/ Camera Sensor ...
65 Andreas Jakl, 2009 Bonus – Acceleration Sensor (3rd Ed (FP1))
import appuifw,e32,sensor def get_sensor_data(status): "Callback function for regular accelerometer status" print "x: %d, y: %d, z: %d" % (status['data_1'], status['data_2'], status['data_3']) def exit_key_handler(): # Disconnect from the sensor and exit acc_sensor.disconnect() app_lock.signal() appuifw.app.exit_key_handler = exit_key_handler
# Retrieve the acceleration sensor sensor_type = sensor.sensors()['AccSensor'] # Create an acceleration sensor object acc_sensor = sensor.Sensor(sensor_type['id'],sensor_type['category']) # Connect to the sensor acc_sensor.connect(get_sensor_data)
# Wait for sensor data and the exit event app_lock = e32.Ao_lock() app_lock.wait()
66 Andreas Jakl, 2009 Bonus – Acceleration Sensor (3rd Ed FP2+) from sensor import * import e32, time, appuifw class DemoApp(): def __init__(self): self.accelerometer = AccelerometerXYZAxisData(data_filter=LowPassFilter()) self.accelerometer.set_callback(data_callback=self.my_callback) self.counter = 0
def my_callback(self): # For stream sensor data the callback is hit 35 times per sec (On 5800). # The device cannot handle resource hungry operations like print in the callback function # for such high frequencies. A workaround is to sample the data as demonstrated below. if self.counter % 5 == 0: print "X:%s, Y:%s, Z:%s" % (self.accelerometer.x, self.accelerometer.y, self.accelerometer.z) self.counter = self.counter + 1
def run(self): self.accelerometer.start_listening() def exit_key_handler(): # Disconnect from the sensor and exit global d d.accelerometer.stop_listening() print "Exiting Accelorometer" app_lock.signal() if __name__ == '__main__': appuifw.app.exit_key_handler = exit_key_handler d = DemoApp() d.run() app_lock67 = e32.Ao_lock() Andreas Jakl, 2009 app_lock.wait() Literature – Recommended
Mobile Python Jürgen Scheible, Ville Tuulos Complete overview of Python development for PyS60, many small code samples. Status: Symbian OS 9, PyS60 1.4, 2007 Free code samples: http://www.mobilenin.com/pys60/menu.htm
Core Python Programming (Second Edition) Wesley J. Chun Python for developers who already know other languages. Comprehensive short overview, in later chapters a detailed overview of the individual components. Status: 2007
68 Andreas Jakl, 2009 That’s it! Thanks for your attention
69 Andreas Jakl, 2009