comparison libervia/backend/plugins/plugin_misc_android.py @ 4270:0d7bb4df2343

Reformatted code base using black.
author Goffi <goffi@goffi.org>
date Wed, 19 Jun 2024 18:44:57 +0200
parents 4b842c1fb686
children
comparison
equal deleted inserted replaced
4269:64a85ce8be70 4270:0d7bb4df2343
65 from jnius import autoclass 65 from jnius import autoclass
66 from android.broadcast import BroadcastReceiver 66 from android.broadcast import BroadcastReceiver
67 from android import python_act 67 from android import python_act
68 68
69 69
70 Context = autoclass('android.content.Context') 70 Context = autoclass("android.content.Context")
71 ConnectivityManager = autoclass('android.net.ConnectivityManager') 71 ConnectivityManager = autoclass("android.net.ConnectivityManager")
72 MediaPlayer = autoclass('android.media.MediaPlayer') 72 MediaPlayer = autoclass("android.media.MediaPlayer")
73 AudioManager = autoclass('android.media.AudioManager') 73 AudioManager = autoclass("android.media.AudioManager")
74 74
75 # notifications 75 # notifications
76 AndroidString = autoclass('java.lang.String') 76 AndroidString = autoclass("java.lang.String")
77 PendingIntent = autoclass('android.app.PendingIntent') 77 PendingIntent = autoclass("android.app.PendingIntent")
78 Intent = autoclass('android.content.Intent') 78 Intent = autoclass("android.content.Intent")
79 79
80 # DNS 80 # DNS
81 # regex to find dns server prop with "getprop" 81 # regex to find dns server prop with "getprop"
82 RE_DNS = re.compile(r"^\[net\.[a-z0-9]+\.dns[0-4]\]: \[(.*)\]$", re.MULTILINE) 82 RE_DNS = re.compile(r"^\[net\.[a-z0-9]+\.dns[0-4]\]: \[(.*)\]$", re.MULTILINE)
83 SystemProperties = autoclass('android.os.SystemProperties') 83 SystemProperties = autoclass("android.os.SystemProperties")
84 84
85 #: delay between a pause event and sending the inactive indication to server, in seconds 85 #: delay between a pause event and sending the inactive indication to server, in seconds
86 #: we don't send the indication immediately because user can be just checking something 86 #: we don't send the indication immediately because user can be just checking something
87 #: quickly on an other app. 87 #: quickly on an other app.
88 CSI_DELAY = 30 88 CSI_DELAY = 30
146 notification_intent.add_category(Intent.CATEGORY_LAUNCHER) 146 notification_intent.add_category(Intent.CATEGORY_LAUNCHER)
147 if sat_action is not None: 147 if sat_action is not None:
148 action_data = AndroidString(json.dumps(sat_action).encode()) 148 action_data = AndroidString(json.dumps(sat_action).encode())
149 log.debug(f"adding extra {INTENT_EXTRA_ACTION} ==> {action_data}") 149 log.debug(f"adding extra {INTENT_EXTRA_ACTION} ==> {action_data}")
150 notification_intent = notification_intent.putExtra( 150 notification_intent = notification_intent.putExtra(
151 INTENT_EXTRA_ACTION, action_data) 151 INTENT_EXTRA_ACTION, action_data
152 )
152 153
153 # we use PendingIntent.FLAG_UPDATE_CURRENT here, otherwise extra won't be set 154 # we use PendingIntent.FLAG_UPDATE_CURRENT here, otherwise extra won't be set
154 # in the new intent (the old ACTION_MAIN intent will be reused). This differs 155 # in the new intent (the old ACTION_MAIN intent will be reused). This differs
155 # from plyers original behaviour which set no flag here 156 # from plyers original behaviour which set no flag here
156 pending_intent = PendingIntent.getActivity( 157 pending_intent = PendingIntent.getActivity(
166 # we also set, where suitable, default values to empty string instead of 167 # we also set, where suitable, default values to empty string instead of
167 # original None, as a string is expected (in plyer the empty string is used 168 # original None, as a string is expected (in plyer the empty string is used
168 # in the generic "notify" method). 169 # in the generic "notify" method).
169 sat_action = kwargs.pop("sat_action", None) 170 sat_action = kwargs.pop("sat_action", None)
170 noti = None 171 noti = None
171 message = kwargs.get('message', '').encode('utf-8') 172 message = kwargs.get("message", "").encode("utf-8")
172 ticker = kwargs.get('ticker', '').encode('utf-8') 173 ticker = kwargs.get("ticker", "").encode("utf-8")
173 title = AndroidString( 174 title = AndroidString(kwargs.get("title", "").encode("utf-8"))
174 kwargs.get('title', '').encode('utf-8') 175 icon = kwargs.get("app_icon", "")
175 ) 176
176 icon = kwargs.get('app_icon', '') 177 if kwargs.get("toast", False):
177
178 if kwargs.get('toast', False):
179 self._toast(message) 178 self._toast(message)
180 return 179 return
181 else: 180 else:
182 noti = self._build_notification(title) 181 noti = self._build_notification(title)
183 182
208 def __init__(self, android_plugin): 207 def __init__(self, android_plugin):
209 self.android_plugin = android_plugin 208 self.android_plugin = android_plugin
210 209
211 def buildProtocol(self, addr): 210 def buildProtocol(self, addr):
212 return FrontendStateProtocol(self.android_plugin) 211 return FrontendStateProtocol(self.android_plugin)
213
214 212
215 213
216 class AndroidPlugin(object): 214 class AndroidPlugin(object):
217 215
218 params = """ 216 params = """
241 239
242 def __init__(self, host): 240 def __init__(self, host):
243 log.info(_("plugin Android initialization")) 241 log.info(_("plugin Android initialization"))
244 log.info(f"using Android API {api_version}") 242 log.info(f"using Android API {api_version}")
245 self.host = host 243 self.host = host
246 self._csi = host.plugins.get('XEP-0352') 244 self._csi = host.plugins.get("XEP-0352")
247 self._csi_timer = None 245 self._csi_timer = None
248 host.memory.update_params(self.params) 246 host.memory.update_params(self.params)
249 try: 247 try:
250 os.mkdir(SOCKET_DIR, 0o700) 248 os.mkdir(SOCKET_DIR, 0o700)
251 except OSError as e: 249 except OSError as e:
266 reactor.listenUNIX(socket_path, factory) 264 reactor.listenUNIX(socket_path, factory)
267 else: 265 else:
268 raise e 266 raise e
269 # we set a low priority because we want the notification to be sent after all 267 # we set a low priority because we want the notification to be sent after all
270 # plugins have done their job 268 # plugins have done their job
271 host.trigger.add("message_received", self.message_received_trigger, priority=-1000) 269 host.trigger.add(
270 "message_received", self.message_received_trigger, priority=-1000
271 )
272 272
273 # profiles autoconnection 273 # profiles autoconnection
274 host.bridge.add_method( 274 host.bridge.add_method(
275 "profile_autoconnect_get", 275 "profile_autoconnect_get",
276 ".plugin", 276 ".plugin",
310 # https://github.com/kivy/pyjnius/issues/59) 310 # https://github.com/kivy/pyjnius/issues/59)
311 self.br = BroadcastReceiver( 311 self.br = BroadcastReceiver(
312 callback=lambda *args, **kwargs: reactor.callFromThread( 312 callback=lambda *args, **kwargs: reactor.callFromThread(
313 self.on_connectivity_change 313 self.on_connectivity_change
314 ), 314 ),
315 actions=["android.net.conn.CONNECTIVITY_CHANGE"] 315 actions=["android.net.conn.CONNECTIVITY_CHANGE"],
316 ) 316 )
317 self.br.start() 317 self.br.start()
318 318
319 @property 319 @property
320 def state(self): 320 def state(self):
353 353
354 notification is sent if: 354 notification is sent if:
355 - there is a message and it is not a groupchat 355 - there is a message and it is not a groupchat
356 - message is not coming from ourself 356 - message is not coming from ourself
357 """ 357 """
358 if (mess_data["message"] and mess_data["type"] != C.MESS_TYPE_GROUPCHAT 358 if (
359 and not mess_data["from"].userhostJID() == client.jid.userhostJID()): 359 mess_data["message"]
360 and mess_data["type"] != C.MESS_TYPE_GROUPCHAT
361 and not mess_data["from"].userhostJID() == client.jid.userhostJID()
362 ):
360 message = next(iter(mess_data["message"].values())) 363 message = next(iter(mess_data["message"].values()))
361 try: 364 try:
362 subject = next(iter(mess_data["subject"].values())) 365 subject = next(iter(mess_data["subject"].values()))
363 except StopIteration: 366 except StopIteration:
364 subject = D_("new message from {contact}").format( 367 subject = D_("new message from {contact}").format(
365 contact = mess_data['from']) 368 contact=mess_data["from"]
369 )
366 370
367 notification = Notification() 371 notification = Notification()
368 notification._notify( 372 notification._notify(
369 title=subject, 373 title=subject,
370 message=message, 374 message=message,
377 381
378 ringer_mode = self.am.getRingerMode() 382 ringer_mode = self.am.getRingerMode()
379 vibrate_mode = ringer_mode == AudioManager.RINGER_MODE_VIBRATE 383 vibrate_mode = ringer_mode == AudioManager.RINGER_MODE_VIBRATE
380 384
381 ring_setting = self.host.memory.param_get_a( 385 ring_setting = self.host.memory.param_get_a(
382 PARAM_RING_NAME, 386 PARAM_RING_NAME, PARAM_RING_CATEGORY, profile_key=client.profile
383 PARAM_RING_CATEGORY, 387 )
384 profile_key=client.profile 388
385 ) 389 if ring_setting != "never" and ringer_mode == AudioManager.RINGER_MODE_NORMAL:
386
387 if ring_setting != 'never' and ringer_mode == AudioManager.RINGER_MODE_NORMAL:
388 self.notif_player.start() 390 self.notif_player.start()
389 391
390 vibration_setting = self.host.memory.param_get_a( 392 vibration_setting = self.host.memory.param_get_a(
391 PARAM_VIBRATE_NAME, 393 PARAM_VIBRATE_NAME, PARAM_VIBRATE_CATEGORY, profile_key=client.profile
392 PARAM_VIBRATE_CATEGORY, 394 )
393 profile_key=client.profile 395 if (
394 ) 396 vibration_setting == "always"
395 if (vibration_setting == 'always' 397 or vibration_setting == "vibrate"
396 or vibration_setting == 'vibrate' and vibrate_mode): 398 and vibrate_mode
397 try: 399 ):
398 vibrator.vibrate() 400 try:
399 except Exception as e: 401 vibrator.vibrate()
400 log.warning("Can't use vibrator: {e}".format(e=e)) 402 except Exception as e:
403 log.warning("Can't use vibrator: {e}".format(e=e))
401 return mess_data 404 return mess_data
402 405
403 def message_received_trigger(self, client, message_elt, post_treat): 406 def message_received_trigger(self, client, message_elt, post_treat):
404 if not self.cagou_active: 407 if not self.cagou_active:
405 # we only send notification is the frontend is not displayed 408 # we only send notification is the frontend is not displayed
412 def _profile_autoconnect_get(self): 415 def _profile_autoconnect_get(self):
413 return defer.ensureDeferred(self.profile_autoconnect_get()) 416 return defer.ensureDeferred(self.profile_autoconnect_get())
414 417
415 async def _get_profiles_autoconnect(self): 418 async def _get_profiles_autoconnect(self):
416 autoconnect_dict = await self.host.memory.storage.get_ind_param_values( 419 autoconnect_dict = await self.host.memory.storage.get_ind_param_values(
417 category='Connection', name='autoconnect_backend', 420 category="Connection",
421 name="autoconnect_backend",
418 ) 422 )
419 return [p for p, v in autoconnect_dict.items() if C.bool(v)] 423 return [p for p, v in autoconnect_dict.items() if C.bool(v)]
420 424
421 async def profile_autoconnect_get(self): 425 async def profile_autoconnect_get(self):
422 """Return profile to connect automatically by frontend, if any""" 426 """Return profile to connect automatically by frontend, if any"""
424 if not profiles_autoconnect: 428 if not profiles_autoconnect:
425 return None 429 return None
426 if len(profiles_autoconnect) > 1: 430 if len(profiles_autoconnect) > 1:
427 log.warning( 431 log.warning(
428 f"More that one profiles with backend autoconnection set found, picking " 432 f"More that one profiles with backend autoconnection set found, picking "
429 f"up first one (full list: {profiles_autoconnect!r})") 433 f"up first one (full list: {profiles_autoconnect!r})"
434 )
430 return profiles_autoconnect[0] 435 return profiles_autoconnect[0]
431 436
432 # CSI 437 # CSI
433 438
434 def _set_inactive(self): 439 def _set_inactive(self):
501 elif net_type == NET_TYPE_WIFI: 506 elif net_type == NET_TYPE_WIFI:
502 log.info("WIFI activated") 507 log.info("WIFI activated")
503 elif net_type == NET_TYPE_MOBILE: 508 elif net_type == NET_TYPE_MOBILE:
504 log.info("mobile data activated") 509 log.info("mobile data activated")
505 else: 510 else:
506 log.info("network activated (type={net_type_android})" 511 log.info(
507 .format(net_type_android=net_type_android)) 512 "network activated (type={net_type_android})".format(
513 net_type_android=net_type_android
514 )
515 )
508 else: 516 else:
509 log.debug("_check_connectivity called without network change ({net_type})" 517 log.debug(
510 .format(net_type = net_type)) 518 "_check_connectivity called without network change ({net_type})".format(
519 net_type=net_type
520 )
521 )
511 522
512 # we always call _handle_network_change even if there is not connectivity change 523 # we always call _handle_network_change even if there is not connectivity change
513 # to be sure to reconnect when necessary 524 # to be sure to reconnect when necessary
514 await self._handle_network_change(net_type) 525 await self._handle_network_change(net_type)
515
516 526
517 def on_connectivity_change(self): 527 def on_connectivity_change(self):
518 log.debug("on_connectivity_change called") 528 log.debug("on_connectivity_change called")
519 d = defer.ensureDeferred(self._check_connectivity()) 529 d = defer.ensureDeferred(self._check_connectivity())
520 d.addErrback(self.host.log_errback) 530 d.addErrback(self.host.log_errback)
525 # updating Twisted's resolver accordingly 535 # updating Twisted's resolver accordingly
526 dns_servers = await self.get_dns_servers() 536 dns_servers = await self.get_dns_servers()
527 537
528 log.info( 538 log.info(
529 "Patching Twisted to use Android DNS resolver ({dns_servers})".format( 539 "Patching Twisted to use Android DNS resolver ({dns_servers})".format(
530 dns_servers=', '.join([s[0] for s in dns_servers])) 540 dns_servers=", ".join([s[0] for s in dns_servers])
541 )
531 ) 542 )
532 dns_client.theResolver = dns_client.createResolver(servers=dns_servers) 543 dns_client.theResolver = dns_client.createResolver(servers=dns_servers)
533 544
534 async def get_dns_servers(self): 545 async def get_dns_servers(self):
535 servers = [] 546 servers = []
536 547
537 if api_version < 26: 548 if api_version < 26:
538 # thanks to A-IV at https://stackoverflow.com/a/11362271 for the way to go 549 # thanks to A-IV at https://stackoverflow.com/a/11362271 for the way to go
539 log.debug("Old API, using SystemProperties to find DNS") 550 log.debug("Old API, using SystemProperties to find DNS")
540 for idx in range(1, 5): 551 for idx in range(1, 5):
541 addr = SystemProperties.get(f'net.dns{idx}') 552 addr = SystemProperties.get(f"net.dns{idx}")
542 if abstract.isIPAddress(addr): 553 if abstract.isIPAddress(addr):
543 servers.append((addr, 53)) 554 servers.append((addr, 53))
544 else: 555 else:
545 log.debug(f"API {api_version} >= 26, using getprop to find DNS") 556 log.debug(f"API {api_version} >= 26, using getprop to find DNS")
546 # use of getprop inspired by various solutions at 557 # use of getprop inspired by various solutions at
547 # https://stackoverflow.com/q/3070144 558 # https://stackoverflow.com/q/3070144
548 # it's the most simple option, and it fit wells with async_process 559 # it's the most simple option, and it fit wells with async_process
549 getprop_paths = which('getprop') 560 getprop_paths = which("getprop")
550 if getprop_paths: 561 if getprop_paths:
551 try: 562 try:
552 getprop_path = getprop_paths[0] 563 getprop_path = getprop_paths[0]
553 props = await async_process.run(getprop_path) 564 props = await async_process.run(getprop_path)
554 servers = [(ip, 53) for ip in RE_DNS.findall(props.decode()) 565 servers = [
555 if abstract.isIPAddress(ip)] 566 (ip, 53)
567 for ip in RE_DNS.findall(props.decode())
568 if abstract.isIPAddress(ip)
569 ]
556 except Exception as e: 570 except Exception as e:
557 log.warning(f"Can't use \"getprop\" to find DNS server: {e}") 571 log.warning(f'Can\'t use "getprop" to find DNS server: {e}')
558 if not servers: 572 if not servers:
559 # FIXME: Cloudflare's 1.1.1.1 seems to have a better privacy policy, to be 573 # FIXME: Cloudflare's 1.1.1.1 seems to have a better privacy policy, to be
560 # checked. 574 # checked.
561 log.warning( 575 log.warning(
562 "no server found, we have to use factory Google DNS, this is not ideal " 576 "no server found, we have to use factory Google DNS, this is not ideal "
563 "for privacy" 577 "for privacy"
564 ) 578 )
565 servers.append(('8.8.8.8', 53), ('8.8.4.4', 53)) 579 servers.append(("8.8.8.8", 53), ("8.8.4.4", 53))
566 return servers 580 return servers