New port numbers
[electrum-nvc.git] / gui / android.py
1 #!/usr/bin/env python
2 #
3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2011 thomasv@gitorious
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19
20
21
22 from __future__ import absolute_import
23 import android
24
25 from electrum_nvc import SimpleConfig, Wallet, WalletStorage, format_satoshis, mnemonic_encode, mnemonic_decode
26 from electrum_nvc.bitcoin import is_valid
27 from electrum_nvc import util
28 from decimal import Decimal
29 import datetime, re
30
31
32
33 def modal_dialog(title, msg = None):
34     droid.dialogCreateAlert(title,msg)
35     droid.dialogSetPositiveButtonText('OK')
36     droid.dialogShow()
37     droid.dialogGetResponse()
38     droid.dialogDismiss()
39
40 def modal_input(title, msg, value = None, etype=None):
41     droid.dialogCreateInput(title, msg, value, etype)
42     droid.dialogSetPositiveButtonText('OK')
43     droid.dialogSetNegativeButtonText('Cancel')
44     droid.dialogShow()
45     response = droid.dialogGetResponse()
46     result = response.result
47     droid.dialogDismiss()
48
49     if result is None:
50         print "modal input: result is none"
51         return modal_input(title, msg, value, etype)
52
53     if result.get('which') == 'positive':
54         return result.get('value')
55
56 def modal_question(q, msg, pos_text = 'OK', neg_text = 'Cancel'):
57     droid.dialogCreateAlert(q, msg)
58     droid.dialogSetPositiveButtonText(pos_text)
59     droid.dialogSetNegativeButtonText(neg_text)
60     droid.dialogShow()
61     response = droid.dialogGetResponse()
62     result = response.result
63     droid.dialogDismiss()
64
65     if result is None:
66         print "modal question: result is none"
67         return modal_question(q,msg, pos_text, neg_text)
68
69     return result.get('which') == 'positive'
70
71 def edit_label(addr):
72     v = modal_input('Edit label',None,wallet.labels.get(addr))
73     if v is not None:
74         if v:
75             wallet.labels[addr] = v
76         else:
77             if addr in wallet.labels.keys():
78                 wallet.labels.pop(addr)
79         wallet.update_tx_history()
80         wallet.save()
81         droid.fullSetProperty("labelTextView", "text", v)
82
83 def select_from_contacts():
84     title = 'Contacts:'
85     droid.dialogCreateAlert(title)
86     l = []
87     for i in range(len(wallet.addressbook)):
88         addr = wallet.addressbook[i]
89         label = wallet.labels.get(addr,addr)
90         l.append( label )
91     droid.dialogSetItems(l)
92     droid.dialogSetPositiveButtonText('New contact')
93     droid.dialogShow()
94     response = droid.dialogGetResponse().result
95     droid.dialogDismiss()
96
97     if response.get('which') == 'positive':
98         return 'newcontact'
99
100     result = response.get('item')
101     print result
102     if result is not None:
103         addr = wallet.addressbook[result]
104         return addr
105
106
107 def select_from_addresses():
108     droid.dialogCreateAlert("Addresses:")
109     l = []
110     addresses = wallet.addresses()
111     for i in range(len(addresses)):
112         addr = addresses[i]
113         label = wallet.labels.get(addr,addr)
114         l.append( label )
115     droid.dialogSetItems(l)
116     droid.dialogShow()
117     response = droid.dialogGetResponse()
118     result = response.result.get('item')
119     droid.dialogDismiss()
120     if result is not None:
121         addr = addresses[result]
122         return addr
123
124
125 def protocol_name(p):
126     if p == 't': return 'TCP'
127     if p == 'h': return 'HTTP'
128     if p == 's': return 'SSL'
129     if p == 'g': return 'HTTPS'
130
131
132 def protocol_dialog(host, protocol, z):
133     droid.dialogCreateAlert('Protocol',host)
134     if z:
135         protocols = z.keys()
136     else:
137         protocols = 'thsg'
138     l = []
139     current = protocols.index(protocol)
140     for p in protocols:
141         l.append(protocol_name(p))
142     droid.dialogSetSingleChoiceItems(l, current)
143     droid.dialogSetPositiveButtonText('OK')
144     droid.dialogSetNegativeButtonText('Cancel')
145     droid.dialogShow()
146     response = droid.dialogGetResponse().result
147     selected_item = droid.dialogGetSelectedItems().result
148     droid.dialogDismiss()
149
150     if not response: return
151     if not selected_item: return
152     if response.get('which') == 'positive':
153         return protocols[selected_item[0]]
154
155
156
157
158 def make_layout(s, scrollable = False):
159     content = """
160
161       <LinearLayout 
162         android:id="@+id/zz"
163         android:layout_width="match_parent"
164         android:layout_height="wrap_content" 
165         android:background="#ff222222">
166
167         <TextView
168           android:id="@+id/textElectrum"
169           android:text="Electrum-NVC"
170           android:textSize="7pt"
171           android:textColor="#ff4444ff"
172           android:gravity="left"
173           android:layout_height="wrap_content"
174           android:layout_width="match_parent"
175         />
176       </LinearLayout>
177
178         %s   """%s
179
180     if scrollable:
181         content = """
182       <ScrollView 
183         android:id="@+id/scrollview"
184         android:layout_width="match_parent"
185         android:layout_height="match_parent" >
186
187       <LinearLayout
188         android:orientation="vertical" 
189         android:layout_width="match_parent"
190         android:layout_height="wrap_content" >
191
192       %s
193
194       </LinearLayout>
195       </ScrollView>
196       """%content
197
198
199     return """<?xml version="1.0" encoding="utf-8"?>
200       <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
201         android:id="@+id/background"
202         android:orientation="vertical" 
203         android:layout_width="match_parent"
204         android:layout_height="match_parent" 
205         android:background="#ff000022">
206
207       %s 
208       </LinearLayout>"""%content
209
210
211
212
213 def main_layout():
214     return make_layout("""
215         <TextView android:id="@+id/balanceTextView" 
216                 android:layout_width="match_parent"
217                 android:text=""
218                 android:textColor="#ffffffff"
219                 android:textAppearance="?android:attr/textAppearanceLarge" 
220                 android:padding="7dip"
221                 android:textSize="8pt"
222                 android:gravity="center_vertical|center_horizontal|left">
223         </TextView>
224
225         <TextView android:id="@+id/historyTextView" 
226                 android:layout_width="match_parent"
227                 android:layout_height="wrap_content" 
228                 android:text="Recent transactions"
229                 android:textAppearance="?android:attr/textAppearanceLarge" 
230                 android:gravity="center_vertical|center_horizontal|center">
231         </TextView>
232
233         %s """%get_history_layout(15),True)
234
235
236
237 def qr_layout(addr):
238     return make_layout("""
239
240      <TextView android:id="@+id/addrTextView" 
241                 android:layout_width="match_parent"
242                 android:layout_height="50" 
243                 android:text="%s"
244                 android:textAppearance="?android:attr/textAppearanceLarge" 
245                 android:gravity="center_vertical|center_horizontal|center">
246      </TextView>
247
248      <ImageView
249         android:id="@+id/qrView"
250         android:gravity="center"
251         android:layout_width="match_parent"
252         android:layout_height="350"
253         android:antialias="false"
254         android:src="file:///sdcard/sl4a/qrcode.bmp" /> 
255
256      <TextView android:id="@+id/labelTextView" 
257                 android:layout_width="match_parent"
258                 android:layout_height="50" 
259                 android:text="%s"
260                 android:textAppearance="?android:attr/textAppearanceLarge" 
261                 android:gravity="center_vertical|center_horizontal|center">
262      </TextView>
263
264      """%(addr,wallet.labels.get(addr,'')), True)
265
266 payto_layout = make_layout("""
267
268         <TextView android:id="@+id/recipientTextView" 
269                 android:layout_width="match_parent"
270                 android:layout_height="wrap_content" 
271                 android:text="Pay to:"
272                 android:textAppearance="?android:attr/textAppearanceLarge" 
273                 android:gravity="left">
274         </TextView>
275
276
277         <EditText android:id="@+id/recipient"
278                 android:layout_width="match_parent"
279                 android:layout_height="wrap_content" 
280                 android:tag="Tag Me" android:inputType="text">
281         </EditText>
282
283         <LinearLayout android:id="@+id/linearLayout1"
284                 android:layout_width="match_parent"
285                 android:layout_height="wrap_content">
286                 <Button android:id="@+id/buttonQR" android:layout_width="wrap_content"
287                         android:layout_height="wrap_content" android:text="From QR code"></Button>
288                 <Button android:id="@+id/buttonContacts" android:layout_width="wrap_content"
289                         android:layout_height="wrap_content" android:text="From Contacts"></Button>
290         </LinearLayout>
291
292
293         <TextView android:id="@+id/labelTextView" 
294                 android:layout_width="match_parent"
295                 android:layout_height="wrap_content" 
296                 android:text="Description:"
297                 android:textAppearance="?android:attr/textAppearanceLarge" 
298                 android:gravity="left">
299         </TextView>
300
301         <EditText android:id="@+id/label"
302                 android:layout_width="match_parent"
303                 android:layout_height="wrap_content" 
304                 android:tag="Tag Me" android:inputType="text">
305         </EditText>
306
307         <TextView android:id="@+id/amountLabelTextView" 
308                 android:layout_width="match_parent"
309                 android:layout_height="wrap_content" 
310                 android:text="Amount:"
311                 android:textAppearance="?android:attr/textAppearanceLarge" 
312                 android:gravity="left">
313         </TextView>
314
315         <EditText android:id="@+id/amount"
316                 android:layout_width="match_parent"
317                 android:layout_height="wrap_content" 
318                 android:tag="Tag Me" android:inputType="numberDecimal">
319         </EditText>
320
321         <LinearLayout android:layout_width="match_parent"
322                 android:layout_height="wrap_content" android:id="@+id/linearLayout1">
323                 <Button android:id="@+id/buttonPay" android:layout_width="wrap_content"
324                         android:layout_height="wrap_content" android:text="Send"></Button>
325         </LinearLayout>""",False)
326
327
328
329 settings_layout = make_layout(""" <ListView
330            android:id="@+id/myListView" 
331            android:layout_width="match_parent"
332            android:layout_height="wrap_content" />""")
333
334
335
336 def get_history_values(n):
337     values = []
338     h = wallet.get_tx_history()
339     length = min(n, len(h))
340     for i in range(length):
341         tx_hash, conf, is_mine, value, fee, balance, timestamp = h[-i-1]
342         try:
343             dt = datetime.datetime.fromtimestamp( timestamp )
344             if dt.date() == dt.today().date():
345                 time_str = str( dt.time() )
346             else:
347                 time_str = str( dt.date() )
348         except Exception:
349             time_str = 'pending'
350
351         conf_str = 'v' if conf else 'o'
352         label, is_default_label = wallet.get_label(tx_hash)
353         values.append((conf_str, '  ' + time_str, '  ' + format_satoshis(value,True), '  ' + label ))
354
355     return values
356
357
358 def get_history_layout(n):
359     rows = ""
360     i = 0
361     values = get_history_values(n)
362     for v in values:
363         a,b,c,d = v
364         color = "#ff00ff00" if a == 'v' else "#ffff0000"
365         rows += """
366         <TableRow>
367           <TextView
368             android:id="@+id/hl_%d_col1" 
369             android:layout_column="0"
370             android:text="%s"
371             android:textColor="%s"
372             android:padding="3" />
373           <TextView
374             android:id="@+id/hl_%d_col2" 
375             android:layout_column="1"
376             android:text="%s"
377             android:padding="3" />
378           <TextView
379             android:id="@+id/hl_%d_col3" 
380             android:layout_column="2"
381             android:text="%s"
382             android:padding="3" />
383           <TextView
384             android:id="@+id/hl_%d_col4" 
385             android:layout_column="3"
386             android:text="%s"
387             android:padding="4" />
388         </TableRow>"""%(i,a,color,i,b,i,c,i,d)
389         i += 1
390
391     output = """
392 <TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
393     android:layout_width="fill_parent"
394     android:layout_height="wrap_content"
395     android:stretchColumns="0,1,2,3">
396     %s
397 </TableLayout>"""% rows
398     return output
399
400
401 def set_history_layout(n):
402     values = get_history_values(n)
403     i = 0
404     for v in values:
405         a,b,c,d = v
406         droid.fullSetProperty("hl_%d_col1"%i,"text", a)
407
408         if a == 'v':
409             droid.fullSetProperty("hl_%d_col1"%i, "textColor","#ff00ff00")
410         else:
411             droid.fullSetProperty("hl_%d_col1"%i, "textColor","#ffff0000")
412
413         droid.fullSetProperty("hl_%d_col2"%i,"text", b)
414         droid.fullSetProperty("hl_%d_col3"%i,"text", c)
415         droid.fullSetProperty("hl_%d_col4"%i,"text", d)
416         i += 1
417
418
419
420
421 status_text = ''
422 def update_layout():
423     global status_text
424     if not network.is_connected():
425         text = "Not connected..."
426     elif not wallet.up_to_date:
427         text = "Synchronizing..."
428     else:
429         c, u = wallet.get_balance()
430         text = "Balance:"+format_satoshis(c) 
431         if u : text += '   [' + format_satoshis(u,True).strip() + ']'
432
433
434     # vibrate if status changed
435     if text != status_text:
436         if status_text and network.is_connected() and wallet.up_to_date:
437             droid.vibrate()
438         status_text = text
439
440     droid.fullSetProperty("balanceTextView", "text", status_text)
441
442     if wallet.up_to_date:
443         set_history_layout(15)
444
445
446
447
448 def pay_to(recipient, amount, fee, label):
449
450     if wallet.use_encryption:
451         password  = droid.dialogGetPassword('Password').result
452         if not password: return
453     else:
454         password = None
455
456     droid.dialogCreateSpinnerProgress("Electrum", "signing transaction...")
457     droid.dialogShow()
458
459     try:
460         tx = wallet.mktx( [(recipient, amount)], password, fee)
461     except Exception as e:
462         modal_dialog('error', e.message)
463         droid.dialogDismiss()
464         return
465
466     if label: 
467         wallet.labels[tx.hash()] = label
468
469     droid.dialogDismiss()
470
471     r, h = wallet.sendtx( tx )
472     if r:
473         modal_dialog('Payment sent', h)
474         return True
475     else:
476         modal_dialog('Error', h)
477
478
479
480
481
482
483
484 def make_new_contact():
485     code = droid.scanBarcode()
486     r = code.result
487     if r:
488         data = r['extras']['SCAN_RESULT']
489         if data:
490             if re.match('^novacoin:', data):
491                 address, _, _, _, _ = util.parse_URI(data)
492             elif is_valid(data):
493                 address = data
494             else:
495                 address = None
496             if address:
497                 if modal_question('Add to contacts?', address):
498                     wallet.add_contact(address)
499         else:
500             modal_dialog('Invalid address', data)
501
502
503 do_refresh = False
504
505 def update_callback():
506     global do_refresh
507     print "gui callback", network.is_connected()
508     do_refresh = True
509     droid.eventPost("refresh",'z')
510
511 def main_loop():
512     global do_refresh
513
514     update_layout()
515     out = None
516     quitting = False
517     while out is None:
518
519         event = droid.eventWait(1000).result
520         if event is None:
521             if do_refresh: 
522                 update_layout()
523                 do_refresh = False
524             continue
525
526         print "got event in main loop", repr(event)
527         if event == 'OK': continue
528         if event is None: continue
529         if not event.get("name"): continue
530
531         # request 2 taps before we exit
532         if event["name"]=="key":
533             if event["data"]["key"] == '4':
534                 if quitting:
535                     out = 'quit'
536                 else: 
537                     quitting = True
538         else: quitting = False
539
540         if event["name"]=="click":
541             id=event["data"]["id"]
542
543         elif event["name"]=="settings":
544             out = 'settings'
545
546         elif event["name"] in menu_commands:
547             out = event["name"]
548
549             if out == 'contacts':
550                 global contact_addr
551                 contact_addr = select_from_contacts()
552                 if contact_addr == 'newcontact':
553                     make_new_contact()
554                     contact_addr = None
555                 if not contact_addr:
556                     out = None
557
558             elif out == "receive":
559                 global receive_addr
560                 receive_addr = select_from_addresses()
561                 if receive_addr:
562                     amount = modal_input('Amount', 'Amount you want receive. ', '', "numberDecimal")
563                     if amount:
564                         receive_addr = 'bitcoin:%s?amount=%s'%(receive_addr, amount)
565
566                 if not receive_addr:
567                     out = None
568
569
570     return out
571                     
572
573 def payto_loop():
574     global recipient
575     if recipient:
576         droid.fullSetProperty("recipient","text",recipient)
577         recipient = None
578
579     out = None
580     while out is None:
581         event = droid.eventWait().result
582         if not event: continue
583         print "got event in payto loop", event
584         if event == 'OK': continue
585         if not event.get("name"): continue
586
587         if event["name"] == "click":
588             id = event["data"]["id"]
589
590             if id=="buttonPay":
591
592                 droid.fullQuery()
593                 recipient = droid.fullQueryDetail("recipient").result.get('text')
594                 label  = droid.fullQueryDetail("label").result.get('text')
595                 amount = droid.fullQueryDetail('amount').result.get('text')
596
597                 if not is_valid(recipient):
598                     modal_dialog('Error','Invalid Novacoin address')
599                     continue
600
601                 try:
602                     amount = int( 1000000 * Decimal(amount) )
603                 except Exception:
604                     modal_dialog('Error','Invalid amount')
605                     continue
606
607                 result = pay_to(recipient, amount, wallet.fee, label)
608                 if result:
609                     out = 'main'
610
611             elif id=="buttonContacts":
612                 addr = select_from_contacts()
613                 droid.fullSetProperty("recipient","text",addr)
614
615             elif id=="buttonQR":
616                 code = droid.scanBarcode()
617                 r = code.result
618                 if r:
619                     data = r['extras']['SCAN_RESULT']
620                     if data:
621                         if re.match('^novacoin:', data):
622                             payto, amount, label, _, _ = util.parse_URI(data)
623                             droid.fullSetProperty("recipient", "text",payto)
624                             droid.fullSetProperty("amount", "text", amount)
625                             droid.fullSetProperty("label", "text", label)
626                         else:
627                             droid.fullSetProperty("recipient", "text", data)
628
629                     
630         elif event["name"] in menu_commands:
631             out = event["name"]
632
633         elif event["name"]=="key":
634             if event["data"]["key"] == '4':
635                 out = 'main'
636
637         #elif event["name"]=="screen":
638         #    if event["data"]=="destroy":
639         #        out = 'main'
640
641     return out
642
643
644 receive_addr = ''
645 contact_addr = ''
646 recipient = ''
647
648 def receive_loop():
649     out = None
650     while out is None:
651         event = droid.eventWait().result
652         print "got event", event
653         if event["name"]=="key":
654             if event["data"]["key"] == '4':
655                 out = 'main'
656
657         elif event["name"]=="clipboard":
658             droid.setClipboard(receive_addr)
659             modal_dialog('Address copied to clipboard',receive_addr)
660
661         elif event["name"]=="edit":
662             edit_label(receive_addr)
663
664     return out
665
666 def contacts_loop():
667     global recipient
668     out = None
669     while out is None:
670         event = droid.eventWait().result
671         print "got event", event
672         if event["name"]=="key":
673             if event["data"]["key"] == '4':
674                 out = 'main'
675
676         elif event["name"]=="clipboard":
677             droid.setClipboard(contact_addr)
678             modal_dialog('Address copied to clipboard',contact_addr)
679
680         elif event["name"]=="edit":
681             edit_label(contact_addr)
682
683         elif event["name"]=="paytocontact":
684             recipient = contact_addr
685             out = 'send'
686
687         elif event["name"]=="deletecontact":
688             if modal_question('delete contact', contact_addr):
689                 out = 'main'
690
691     return out
692
693
694 def server_dialog(servers):
695     droid.dialogCreateAlert("Public servers")
696     droid.dialogSetItems( servers.keys() )
697     droid.dialogSetPositiveButtonText('Private server')
698     droid.dialogShow()
699     response = droid.dialogGetResponse().result
700     droid.dialogDismiss()
701     if not response: return
702
703     if response.get('which') == 'positive':
704         return modal_input('Private server', None)
705
706     i = response.get('item')
707     if i is not None:
708         response = servers.keys()[i]
709         return response
710
711
712 def show_seed():
713     if wallet.use_encryption:
714         password  = droid.dialogGetPassword('Seed').result
715         if not password: return
716     else:
717         password = None
718     
719     try:
720         seed = wallet.get_seed(password)
721     except Exception:
722         modal_dialog('error','incorrect password')
723         return
724
725     modal_dialog('Your seed is',seed)
726     modal_dialog('Mnemonic code:', ' '.join(mnemonic_encode(seed)) )
727
728 def change_password_dialog():
729     if wallet.use_encryption:
730         password  = droid.dialogGetPassword('Your wallet is encrypted').result
731         if password is None: return
732     else:
733         password = None
734
735     try:
736         wallet.get_seed(password)
737     except Exception:
738         modal_dialog('error','incorrect password')
739         return
740
741     new_password  = droid.dialogGetPassword('Choose a password').result
742     if new_password == None:
743         return
744
745     if new_password != '':
746         password2  = droid.dialogGetPassword('Confirm new password').result
747         if new_password != password2:
748             modal_dialog('error','passwords do not match')
749             return
750
751     wallet.update_password(password, new_password)
752     if new_password:
753         modal_dialog('Password updated','your wallet is encrypted')
754     else:
755         modal_dialog('No password','your wallet is not encrypted')
756     return True
757
758
759 def settings_loop():
760
761
762     def set_listview():
763         host, port, p = network.default_server.split(':')
764         fee = str( Decimal( wallet.fee)/1000000 )
765         is_encrypted = 'yes' if wallet.use_encryption else 'no'
766         protocol = protocol_name(p)
767         droid.fullShow(settings_layout)
768         droid.fullSetList("myListView",['Server: ' + host, 'Protocol: '+ protocol, 'Port: '+port, 'Transaction fee: '+fee, 'Password: '+is_encrypted, 'Seed'])
769
770     set_listview()
771
772     out = None
773     while out is None:
774         event = droid.eventWait()
775         event = event.result
776         print "got event", event
777         if event == 'OK': continue
778         if not event: continue
779
780         servers = network.get_servers()
781         name = event.get("name")
782         if not name: continue
783
784         if name == "itemclick":
785             pos = event["data"]["position"]
786             host, port, protocol = network.default_server.split(':')
787             network_changed = False
788
789             if pos == "0": #server
790                 host = server_dialog(servers)
791                 if host:
792                     p = servers[host]
793                     port = p[protocol]
794                     network_changed = True
795
796             elif pos == "1": #protocol
797                 if host in servers:
798                     protocol = protocol_dialog(host, protocol, servers[host])
799                     z = servers[host]
800                     port = z[protocol]
801                     network_changed = True
802
803             elif pos == "2": #port
804                 a_port = modal_input('Port number', 'If you use a public server, this field is set automatically when you set the protocol', port, "number")
805                 if a_port != port:
806                     port = a_port
807                     network_changed = True
808
809             elif pos == "3": #fee
810                 fee = modal_input('Transaction fee', 'The fee will be this amount multiplied by the number of inputs in your transaction. ', str( Decimal( wallet.fee)/1000000 ), "numberDecimal")
811                 if fee:
812                     try:
813                         fee = int( 1000000 * Decimal(fee) )
814                     except Exception:
815                         modal_dialog('error','invalid fee value')
816                     wallet.set_fee(fee)
817                     set_listview()
818
819             elif pos == "4":
820                 if change_password_dialog():
821                     set_listview()
822
823             elif pos == "5":
824                 show_seed()
825
826             if network_changed:
827                 proxy = None
828                 auto_connect = False
829                 try:
830                     network.set_parameters(host, port, protocol, proxy, auto_connect)
831                 except Exception:
832                     modal_dialog('error','invalid server')
833                 set_listview()
834
835         elif name in menu_commands:
836             out = event["name"]
837
838         elif name == 'cancel':
839             out = 'main'
840
841         elif name == "key":
842             if event["data"]["key"] == '4':
843                 out = 'main'
844
845     return out
846
847 def add_menu(s):
848     droid.clearOptionsMenu()
849     if s == 'main':
850         droid.addOptionsMenuItem("Send","send",None,"")
851         droid.addOptionsMenuItem("Receive","receive",None,"")
852         droid.addOptionsMenuItem("Contacts","contacts",None,"")
853         droid.addOptionsMenuItem("Settings","settings",None,"")
854     elif s == 'receive':
855         droid.addOptionsMenuItem("Copy","clipboard",None,"")
856         droid.addOptionsMenuItem("Label","edit",None,"")
857     elif s == 'contacts':
858         droid.addOptionsMenuItem("Copy","clipboard",None,"")
859         droid.addOptionsMenuItem("Label","edit",None,"")
860         droid.addOptionsMenuItem("Pay to","paytocontact",None,"")
861         #droid.addOptionsMenuItem("Delete","deletecontact",None,"")
862
863
864 def make_bitmap(addr):
865     # fixme: this is highly inefficient
866     droid.dialogCreateSpinnerProgress("please wait")
867     droid.dialogShow()
868     try:
869         import pyqrnative, bmp
870         qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
871         qr.addData(addr)
872         qr.make()
873         k = qr.getModuleCount()
874         assert k == 33
875         bmp.save_qrcode(qr,"/sdcard/sl4a/qrcode.bmp")
876     finally:
877         droid.dialogDismiss()
878
879         
880
881
882 droid = android.Android()
883 menu_commands = ["send", "receive", "settings", "contacts", "main"]
884 wallet = None
885 network = None
886
887 class ElectrumGui:
888
889     def __init__(self, config, _network):
890         global wallet, network
891         network = _network
892         network.register_callback('updated', update_callback)
893         network.register_callback('connected', update_callback)
894         network.register_callback('disconnected', update_callback)
895         network.register_callback('disconnecting', update_callback)
896         
897         storage = WalletStorage(config)
898         if not storage.file_exists:
899             action = self.restore_or_create()
900             if not action: exit()
901
902             wallet = Wallet(storage)
903             if action == 'create':
904                 wallet.init_seed(None)
905                 self.show_seed()
906                 wallet.save_seed(None)
907                 wallet.synchronize()  # generate first addresses offline
908                 
909             elif action == 'restore':
910                 seed = self.seed_dialog()
911                 if not seed:
912                     exit()
913                 wallet.init_seed(str(seed))
914                 wallet.save_seed(None)
915             else:
916                 exit()
917
918             wallet.start_threads(network)
919
920             if action == 'restore':
921                 if not self.restore_wallet():
922                     exit()
923
924             self.password_dialog()
925
926         else:
927             wallet = Wallet(storage)
928             wallet.start_threads(network)
929
930
931     def main(self, url):
932         s = 'main'
933         while True:
934             add_menu(s)
935             if s == 'main':
936                 droid.fullShow(main_layout())
937                 s = main_loop()
938
939             elif s == 'send':
940                 droid.fullShow(payto_layout)
941                 s = payto_loop()
942
943             elif s == 'receive':
944                 make_bitmap(receive_addr)
945                 droid.fullShow(qr_layout(receive_addr))
946                 s = receive_loop()
947
948             elif s == 'contacts':
949                 make_bitmap(contact_addr)
950                 droid.fullShow(qr_layout(contact_addr))
951                 s = contacts_loop()
952
953             elif s == 'settings':
954                 s = settings_loop()
955
956             else:
957                 break
958
959         droid.makeToast("Bye!")
960
961
962     def restore_or_create(self):
963         droid.dialogCreateAlert("Wallet not found","Do you want to create a new wallet, or restore an existing one?")
964         droid.dialogSetPositiveButtonText('Create')
965         droid.dialogSetNeutralButtonText('Restore')
966         droid.dialogSetNegativeButtonText('Cancel')
967         droid.dialogShow()
968         response = droid.dialogGetResponse().result
969         droid.dialogDismiss()
970         if not response: return
971         if response.get('which') == 'negative':
972             return
973
974         return 'restore' if response.get('which') == 'neutral' else 'create'
975
976
977     def seed_dialog(self):
978         if modal_question("Enter your seed","Input method",'QR Code', 'mnemonic'):
979             code = droid.scanBarcode()
980             r = code.result
981             if r:
982                 seed = r['extras']['SCAN_RESULT']
983             else:
984                 return
985         else:
986             m = modal_input('Mnemonic','please enter your code')
987             try:
988                 seed = mnemonic_decode(m.split(' '))
989             except Exception:
990                 modal_dialog('error: could not decode this seed')
991                 return
992
993         return str(seed)
994
995
996     def network_dialog(self):
997         return True
998
999         
1000     def show_seed(self):
1001         modal_dialog('Your seed is:', wallet.seed)
1002         modal_dialog('Mnemonic code:', ' '.join(mnemonic_encode(wallet.seed)) )
1003
1004
1005     def password_dialog(self):
1006         change_password_dialog()
1007
1008
1009     def restore_wallet(self):
1010
1011         msg = "recovering wallet..."
1012         droid.dialogCreateSpinnerProgress("Electrum", msg)
1013         droid.dialogShow()
1014
1015         wallet.restore(lambda x: None)
1016
1017         droid.dialogDismiss()
1018         droid.vibrate()
1019
1020         if wallet.is_found():
1021             wallet.fill_addressbook()
1022             modal_dialog("recovery successful")
1023         else:
1024             if not modal_question("no transactions found for this seed","do you want to keep this wallet?"):
1025                 return False
1026
1027         return True
1028