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()