/ src / bitmessagekivy / mpybit.py
mpybit.py
  1  # pylint: disable=too-many-public-methods, unused-variable, too-many-ancestors
  2  # pylint: disable=too-few-public-methods, unused-argument
  3  # pylint: disable=attribute-defined-outside-init, too-many-instance-attributes
  4  # pylint: disable=broad-exception-caught, no-self-use
  5  
  6  """
  7  Bitmessage android(mobile) interface
  8  """
  9  
 10  import logging
 11  import os
 12  import sys
 13  from functools import partial
 14  
 15  from kivy.clock import Clock
 16  from kivy.core.clipboard import Clipboard
 17  from kivy.core.window import Window
 18  from kivy.lang import Builder
 19  from kivy.uix.boxlayout import BoxLayout
 20  from kivymd.app import MDApp
 21  from kivymd.uix.bottomsheet import MDCustomBottomSheet
 22  from kivymd.uix.button import MDRaisedButton
 23  from kivymd.uix.dialog import MDDialog
 24  from kivymd.uix.filemanager import MDFileManager
 25  from kivymd.uix.label import MDLabel
 26  from kivymd.uix.list import IRightBodyTouch
 27  from PIL import Image as PilImage
 28  
 29  from pybitmessage.bitmessagekivy import identiconGeneration
 30  from pybitmessage.bitmessagekivy.base_navigation import (
 31      BaseContentNavigationDrawer, BaseIdentitySpinner, BaseLanguage,
 32      BaseNavigationDrawerDivider, BaseNavigationDrawerSubheader,
 33      BaseNavigationItem)
 34  from pybitmessage.bitmessagekivy.baseclass.common import (get_identity_list,
 35                                                            load_image_path,
 36                                                            toast)
 37  from pybitmessage.bitmessagekivy.baseclass.popup import (AddAddressPopup,
 38                                                           AddressChangingLoader,
 39                                                           AppClosingPopup)
 40  from pybitmessage.bitmessagekivy.get_platform import platform
 41  from pybitmessage.bitmessagekivy.kivy_state import KivyStateVariables
 42  from pybitmessage.bitmessagekivy.load_kivy_screens_data import load_screen_json
 43  from pybitmessage.bitmessagekivy.uikivysignaler import UIkivySignaler
 44  from pybitmessage.bmconfigparser import config  # noqa: F401
 45  from pybitmessage.mockbm.helper_startup import (
 46      loadConfig, total_encrypted_messages_per_month)
 47  
 48  logger = logging.getLogger('default')
 49  
 50  # Define constants for magic numbers
 51  DIALOG_WIDTH_ANDROID = 0.85
 52  DIALOG_WIDTH_OTHER = 0.8
 53  DIALOG_HEIGHT = 0.23
 54  LOADER_DELAY = 1
 55  IMAGE_SIZE = (300, 300)
 56  MAX_LABEL_LENGTH = 15
 57  TRUNCATE_STRING = '...'
 58  
 59  
 60  class Lang(BaseLanguage):
 61      """UI Language"""
 62  
 63  
 64  class NavigationItem(BaseNavigationItem):
 65      """NavigationItem class for kivy Ui"""
 66  
 67  
 68  class NavigationDrawerDivider(BaseNavigationDrawerDivider):
 69      """
 70      A small full-width divider that can be placed
 71      in the :class:`MDNavigationDrawer`
 72      """
 73  
 74  
 75  class NavigationDrawerSubheader(BaseNavigationDrawerSubheader):
 76      """
 77      A subheader for separating content in :class:`MDNavigationDrawer`
 78  
 79      Works well alongside :class:`NavigationDrawerDivider`
 80      """
 81  
 82  
 83  class ContentNavigationDrawer(BaseContentNavigationDrawer):
 84      """ContentNavigationDrawer class for kivy Uir"""
 85  
 86  
 87  class BadgeText(IRightBodyTouch, MDLabel):
 88      """BadgeText class for kivy Ui"""
 89  
 90  
 91  class IdentitySpinner(BaseIdentitySpinner):
 92      """Identity Dropdown in Side Navigation bar"""
 93  
 94  
 95  class NavigateApp(MDApp):
 96      """Navigation Layout of class"""
 97  
 98      kivy_state = KivyStateVariables()
 99      title = "PyBitmessage"
100      identity_list = get_identity_list()
101      image_path = load_image_path()
102      app_platform = platform
103      encrypted_messages_per_month = total_encrypted_messages_per_month()
104      tr = Lang("en")  # for changing in franch replace en with fr
105  
106      def __init__(self):
107          super(NavigateApp, self).__init__()
108          # workaround for relative imports
109          sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
110          self.data_screens, self.all_data, self.data_screen_dict, response = load_screen_json()
111          self.kivy_state_obj = KivyStateVariables()
112          self.image_dir = load_image_path()
113          self.kivy_state_obj.screen_density = Window.size
114          self.window_size = self.kivy_state_obj.screen_density
115  
116      def build(self):
117          """Method builds the widget"""
118          for kv in self.data_screens:
119              Builder.load_file(
120                  os.path.join(
121                      os.path.dirname(__file__),
122                      'kv',
123                      '{0}.kv'.format(self.all_data[kv]["kv_string"]),
124                  )
125              )
126          Window.bind(on_request_close=self.on_request_close)
127          return Builder.load_file(os.path.join(os.path.dirname(__file__), 'main.kv'))
128  
129      def set_screen(self, screen_name):
130          """Set the screen name when navigate to other screens"""
131          self.root.ids.scr_mngr.current = screen_name
132  
133      def run(self):
134          """Running the widgets"""
135          loadConfig()
136          kivysignalthread = UIkivySignaler()
137          kivysignalthread.daemon = True
138          kivysignalthread.start()
139          self.kivy_state_obj.kivyui_ready.set()
140          super(NavigateApp, self).run()
141  
142      def addingtoaddressbook(self):
143          """Dialog for saving address"""
144          width = DIALOG_WIDTH_ANDROID if platform == 'android' else DIALOG_WIDTH_OTHER
145          self.add_popup = MDDialog(
146              title='Add contact',
147              type="custom",
148              size_hint=(width, DIALOG_HEIGHT),
149              content_cls=AddAddressPopup(),
150              buttons=[
151                  MDRaisedButton(
152                      text="Save",
153                      on_release=self.savecontact,
154                  ),
155                  MDRaisedButton(
156                      text="Cancel",
157                      on_release=self.close_pop,
158                  ),
159                  MDRaisedButton(
160                      text="Scan QR code",
161                      on_release=self.scan_qr_code,
162                  ),
163              ],
164          )
165          self.add_popup.auto_dismiss = False
166          self.add_popup.open()
167  
168      def scan_qr_code(self, instance):
169          """this method is used for showing QR code scanner"""
170          if self.is_camara_attached():
171              self.add_popup.dismiss()
172              self.root.ids.id_scanscreen.get_screen(self.root.ids.scr_mngr.current, self.add_popup)
173              self.root.ids.scr_mngr.current = 'scanscreen'
174          else:
175              alert_text = (
176                  'Currently this feature is not available!'
177                  if platform == 'android'
178                  else 'Camera is not available!')
179              self.add_popup.dismiss()
180              toast(alert_text)
181  
182      def is_camara_attached(self):
183          """This method is for checking the camera is available or not"""
184          self.root.ids.id_scanscreen.check_camera()
185          is_available = self.root.ids.id_scanscreen.camera_available
186          return is_available
187  
188      def savecontact(self, instance):
189          """Method is used for saving contacts"""
190          popup_obj = self.add_popup.content_cls
191          label = popup_obj.ids.label.text.strip()
192          address = popup_obj.ids.address.text.strip()
193          popup_obj.ids.label.focus = not label
194          # default focus on address field
195          popup_obj.ids.address.focus = label or not address
196  
197      def close_pop(self, instance):
198          """Close the popup"""
199          self.add_popup.dismiss()
200          toast('Canceled')
201  
202      def load_my_address_screen(self, action):
203          """load_my_address_screen method spin the loader"""
204          if len(self.root.ids.id_myaddress.children) <= 2:
205              self.root.ids.id_myaddress.children[0].active = action
206          else:
207              self.root.ids.id_myaddress.children[1].active = action
208  
209      def load_screen(self, instance):
210          """This method is used for loading screen on every click"""
211          if instance.text == 'Inbox':
212              self.root.ids.scr_mngr.current = 'inbox'
213              self.root.ids.id_inbox.children[1].active = True
214          elif instance.text == 'Trash':
215              self.root.ids.scr_mngr.current = 'trash'
216              try:
217                  self.root.ids.id_trash.children[1].active = True
218              except Exception as e:
219                  self.root.ids.id_trash.children[0].children[1].active = True
220          Clock.schedule_once(partial(self.load_screen_callback, instance), LOADER_DELAY)
221  
222      def load_screen_callback(self, instance, dt=0):
223          """This method is rotating loader for few seconds"""
224          if instance.text == 'Inbox':
225              self.root.ids.id_inbox.ids.ml.clear_widgets()
226              self.root.ids.id_inbox.loadMessagelist(self.kivy_state_obj.selected_address)
227              self.root.ids.id_inbox.children[1].active = False
228          elif instance.text == 'Trash':
229              self.root.ids.id_trash.clear_widgets()
230              self.root.ids.id_trash.add_widget(self.data_screen_dict['Trash'].Trash())
231              try:
232                  self.root.ids.id_trash.children[1].active = False
233              except Exception as e:
234                  self.root.ids.id_trash.children[0].children[1].active = False
235  
236      @staticmethod
237      def get_enabled_addresses():
238          """Getting list of all the enabled addresses"""
239          addresses = [addr for addr in config.addresses()
240                       if config.getboolean(str(addr), 'enabled')]
241          return addresses
242  
243      @staticmethod
244      def format_address(address):
245          """Formatting address"""
246          return " ({0})".format(address)
247  
248      @staticmethod
249      def format_label(label):
250          """Formatting label"""
251          if label:
252              f_name = label.split()
253              formatted_label = f_name[0][:MAX_LABEL_LENGTH - 1].capitalize() + TRUNCATE_STRING if len(
254                  f_name[0]) > MAX_LABEL_LENGTH else f_name[0].capitalize()
255              return formatted_label
256          return ''
257  
258      @staticmethod
259      def format_address_and_label(address=None):
260          """Getting formatted address information"""
261          if not address:
262              try:
263                  address = NavigateApp.get_enabled_addresses()[0]
264              except IndexError:
265                  return ''
266          return "{0}{1}".format(
267              NavigateApp.format_label(config.get(address, "label")),
268              NavigateApp.format_address(address)
269          )
270  
271      def get_default_account_data(self, instance):
272          """Getting Default Account Data"""
273          if self.identity_list:
274              self.kivy_state_obj.selected_address = first_addr = self.identity_list[0]
275              return first_addr
276          return 'Select Address'
277  
278      def get_current_account_data(self, text):
279          """Get Current Address Account Data"""
280          if text != '':
281              if os.path.exists(os.path.join(
282                      self.image_dir, 'default_identicon', '{}.png'.format(text))
283              ):
284                  self.load_selected_image(text)
285              else:
286                  self.set_identicon(text)
287                  self.root.ids.content_drawer.ids.reset_image.opacity = 0
288                  self.root.ids.content_drawer.ids.reset_image.disabled = True
289              address_label = self.format_address_and_label(text)
290              self.root_window.children[1].ids.toolbar.title = address_label
291              self.kivy_state_obj.selected_address = text
292              AddressChangingLoader().open()
293              for nav_obj in self.root.ids.content_drawer.children[
294                      0].children[0].children[0].children:
295                  nav_obj.active = True if nav_obj.text == 'Inbox' else False
296              self.file_manager_setting()
297              Clock.schedule_once(self.set_current_account_data, 0.5)
298  
299      def set_current_account_data(self, dt=0):
300          """This method set the current accout data on all the screens"""
301          self.root.ids.id_inbox.ids.ml.clear_widgets()
302          self.root.ids.id_inbox.loadMessagelist(self.kivy_state_obj.selected_address)
303  
304          self.root.ids.id_sent.ids.ml.clear_widgets()
305          self.root.ids.id_sent.children[2].children[2].ids.search_field.text = ''
306          self.root.ids.id_sent.loadSent(self.kivy_state_obj.selected_address)
307  
308      def file_manager_setting(self):
309          """This method is for file manager setting"""
310          if not self.root.ids.content_drawer.ids.file_manager.opacity and \
311                  self.root.ids.content_drawer.ids.file_manager.disabled:
312              self.root.ids.content_drawer.ids.file_manager.opacity = 1
313              self.root.ids.content_drawer.ids.file_manager.disabled = False
314  
315      def on_request_close(self, *args):
316          """This method is for app closing request"""
317          AppClosingPopup().open()
318          return True
319  
320      def clear_composer(self):
321          """If slow down, the new composer edit screen"""
322          self.set_navbar_for_composer()
323          composer_obj = self.root.ids.id_create.children[1].ids
324          composer_obj.ti.text = ''
325          composer_obj.composer_dropdown.text = 'Select'
326          composer_obj.txt_input.text = ''
327          composer_obj.subject.text = ''
328          composer_obj.body.text = ''
329          self.kivy_state_obj.in_composer = True
330          self.kivy_state_obj = False
331  
332      def set_navbar_for_composer(self):
333          """Clearing toolbar data when composer open"""
334          self.root.ids.toolbar.left_action_items = [
335              ['arrow-left', lambda x: self.back_press()]]
336          self.root.ids.toolbar.right_action_items = [
337              ['refresh',
338               lambda x: self.root.ids.id_create.children[1].reset_composer()],
339              ['send',
340               lambda x: self.root.ids.id_create.children[1].send(self)]]
341  
342      def set_identicon(self, text):
343          """Show identicon in address spinner"""
344          img = identiconGeneration.generate(text)
345          self.root.ids.content_drawer.ids.top_box.children[0].texture = img.texture
346  
347      # pylint: disable=import-outside-toplevel
348      def file_manager_open(self):
349          """This method open the file manager of local system"""
350          if not self.kivy_state_obj.file_manager:
351              self.file_manager = MDFileManager(
352                  exit_manager=self.exit_manager,
353                  select_path=self.select_path,
354                  ext=['.png', '.jpg']
355              )
356          self.file_manager.previous = False
357          self.file_manager.current_path = '/'
358          if platform == 'android':
359              # pylint: disable=import-error
360              from android.permissions import (Permission, check_permission,
361                                               request_permissions)
362              if check_permission(Permission.WRITE_EXTERNAL_STORAGE) and \
363                      check_permission(Permission.READ_EXTERNAL_STORAGE):
364                  self.file_manager.show(os.getenv('EXTERNAL_STORAGE'))
365                  self.kivy_state_obj.manager_open = True
366              else:
367                  request_permissions([
368                      Permission.WRITE_EXTERNAL_STORAGE, Permission.READ_EXTERNAL_STORAGE
369                  ])
370          else:
371              self.file_manager.show(os.environ["HOME"])
372              self.kivy_state_obj.manager_open = True
373  
374      def select_path(self, path):
375          """This method is used to set the select image"""
376          try:
377              new_image = PilImage.open(path).resize(IMAGE_SIZE)
378              if platform == 'android':
379                  android_path = os.path.join(
380                      os.path.join(os.environ['ANDROID_PRIVATE'], 'app', 'images', 'kivy')
381                  )
382                  if not os.path.exists(os.path.join(android_path, 'default_identicon')):
383                      os.makedirs(os.path.join(android_path, 'default_identicon'))
384                  new_image.save(os.path.join(android_path, 'default_identicon', '{}.png'.format(
385                      self.kivy_state_obj.selected_address))
386                  )
387              else:
388                  if not os.path.exists(os.path.join(self.image_dir, 'default_identicon')):
389                      os.makedirs(os.path.join(self.image_dir, 'default_identicon'))
390                  new_image.save(os.path.join(self.image_dir, 'default_identicon', '{0}.png'.format(
391                      self.kivy_state_obj.selected_address))
392                  )
393              self.load_selected_image(self.kivy_state_obj.selected_address)
394              toast('Image changed')
395          except Exception:
396              toast('Exit')
397          self.exit_manager()
398  
399      def exit_manager(self, *args):
400          """Called when the user reaches the root of the directory tree."""
401          self.kivy_state_obj.manager_open = False
402          self.file_manager.close()
403  
404      def load_selected_image(self, curerent_addr):
405          """This method load the selected image on screen"""
406          top_box_obj = self.root.ids.content_drawer.ids.top_box.children[0]
407          top_box_obj.source = os.path.join(
408              self.image_dir,
409              'default_identicon',
410              '{0}.png'.format(curerent_addr)
411          )
412          self.root.ids.content_drawer.ids.reset_image.opacity = 1
413          self.root.ids.content_drawer.ids.reset_image.disabled = False
414          top_box_obj.reload()
415  
416      def rest_default_avatar_img(self):
417          """set default avatar generated image"""
418          self.set_identicon(self.kivy_state_obj.selected_address)
419          img_path = os.path.join(
420              self.image_dir, 'default_identicon',
421              '{}.png'.format(self.kivy_state_obj.selected_address)
422          )
423          if os.path.exists(img_path):
424              os.remove(img_path)
425          self.root.ids.content_drawer.ids.reset_image.opacity = 0
426          self.root.ids.content_drawer.ids.reset_image.disabled = True
427          toast('Avatar reset')
428  
429      def get_default_logo(self, instance):
430          """Getting default logo image"""
431          if self.identity_list:
432              first_addr = self.identity_list[0]
433              if config.getboolean(str(first_addr), 'enabled'):
434                  if os.path.exists(
435                      os.path.join(
436                          self.image_dir, 'default_identicon', '{}.png'.format(first_addr)
437                      )
438                  ):
439                      return os.path.join(
440                          self.image_dir, 'default_identicon', '{}.png'.format(first_addr)
441                      )
442                  else:
443                      img = identiconGeneration.generate(first_addr)
444                      instance.texture = img.texture
445                      return None
446          return os.path.join(self.image_dir, 'drawer_logo1.png')
447  
448      @staticmethod
449      def have_any_address():
450          """Checking existance of any address"""
451          if config.addresses():
452              return True
453          return False
454  
455      def reset_login_screen(self):
456          """This method is used for clearing the widgets of random screen"""
457          if self.root.ids.id_newidentity.ids.add_random_bx.children:
458              self.root.ids.id_newidentity.ids.add_random_bx.clear_widgets()
459  
460      def reset(self, *args):
461          """Set transition direction"""
462          self.root.ids.scr_mngr.transition.direction = 'left'
463          self.root.ids.scr_mngr.transition.unbind(on_complete=self.reset)
464  
465      def back_press(self):
466          """Method for, reverting composer to previous page"""
467          if self.root.ids.scr_mngr.current == 'showqrcode':
468              self.set_common_header()
469              self.root.ids.scr_mngr.current = 'myaddress'
470          self.root.ids.scr_mngr.transition.bind(on_complete=self.reset)
471          self.kivy_state.in_composer = False
472  
473      def set_toolbar_for_qr_code(self):
474          """This method is use for setting Qr code toolbar."""
475          self.root.ids.toolbar.left_action_items = [
476              ['arrow-left', lambda x: self.back_press()]]
477          self.root.ids.toolbar.right_action_items = []
478  
479      def set_common_header(self):
480          """Common header for all the Screens"""
481          self.root.ids.toolbar.right_action_items = [
482              ['account-plus', lambda x: self.addingtoaddressbook()]]
483          self.root.ids.toolbar.left_action_items = [
484              ['menu', lambda x: self.root.ids.nav_drawer.set_state("toggle")]]
485          return
486  
487      def open_payment_layout(self, sku):
488          """It basically open up a payment layout for kivy UI"""
489          pml = PaymentMethodLayout()
490          self.product_id = sku
491          self.custom_sheet = MDCustomBottomSheet(screen=pml)
492          self.custom_sheet.open()
493  
494      def initiate_purchase(self, method_name):
495          """initiate_purchase module"""
496          logger.debug("Purchasing %s through %s", self.product_id, method_name)
497  
498      def copy_composer_text(self, text):
499          """Copy text to clipboard"""
500          Clipboard.copy(text)
501  
502  
503  class PaymentMethodLayout(BoxLayout):
504      """PaymentMethodLayout class for kivy Ui"""
505  
506  
507  if __name__ == '__main__':
508      NavigateApp().run()