android-notify 1.58__py3-none-any.whl → 1.59.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of android-notify might be problematic. Click here for more details.

android_notify/sword.py CHANGED
@@ -2,34 +2,41 @@
2
2
  import traceback
3
3
  import os,re
4
4
  import threading
5
- # ,time
5
+ from .an_types import Importance
6
+ from .an_utils import can_accept_arguments
6
7
  from .styles import NotificationStyles
7
8
  from .base import BaseNotification
8
9
  DEV=0
9
10
  ON_ANDROID = False
10
11
 
12
+ # noinspection PyBroadException
11
13
  try:
12
14
  # Android Imports
13
- from jnius import autoclass,cast # Needs Java to be installed pylint: disable=W0611, C0114
14
- from android import activity # pylint: disable=import-error
15
- from android.config import ACTIVITY_CLASS_NAME # pylint: disable=import-error
16
- from android.runnable import run_on_ui_thread # pylint: disable=import-error
15
+ from jnius import autoclass,cast
16
+ from android import activity
17
+ from android.config import ACTIVITY_CLASS_NAME
18
+ from android.runnable import run_on_ui_thread
17
19
 
18
- # Get the required Java classes
20
+ # Get the required Java classes needs to on android to import
19
21
  Bundle = autoclass('android.os.Bundle')
20
22
  PythonActivity = autoclass('org.kivy.android.PythonActivity')
21
23
  String = autoclass('java.lang.String')
22
24
  Intent = autoclass('android.content.Intent')
23
25
  PendingIntent = autoclass('android.app.PendingIntent')
24
- context = PythonActivity.mActivity # Get the app's context
26
+ context = PythonActivity.mActivity
25
27
  BitmapFactory = autoclass('android.graphics.BitmapFactory')
26
28
  BuildVersion = autoclass('android.os.Build$VERSION')
29
+ VersionCodes = autoclass('android.os.Build$VERSION_CODES')
27
30
  NotificationManager = autoclass('android.app.NotificationManager')
28
31
  NotificationChannel = autoclass('android.app.NotificationChannel')
29
32
  IconCompat = autoclass('androidx.core.graphics.drawable.IconCompat')
30
33
  ON_ANDROID = True
31
- except Exception as e:# pylint: disable=W0718
32
- MESSAGE='This Package Only Runs on Android !!! ---> Check "https://github.com/Fector101/android_notify/" to see design patterns and more info.' # pylint: disable=C0301
34
+ except Exception as e:
35
+ if e.name != 'android':
36
+ print('Exception: ',e)
37
+ print(traceback.format_exc())
38
+
39
+ MESSAGE='This Package Only Runs on Android !!! ---> Check "https://github.com/Fector101/android_notify/" to see design patterns and more info.'
33
40
  # from .types_idea import *
34
41
  # print(MESSAGE) Already Printing in core.py
35
42
 
@@ -43,18 +50,18 @@ except Exception as e:# pylint: disable=W0718
43
50
 
44
51
  if ON_ANDROID:
45
52
  try:
46
- from android.permissions import request_permissions, Permission,check_permission # pylint: disable=E0401
47
- from android.storage import app_storage_path # pylint: disable=E0401
53
+ from android.permissions import request_permissions, Permission,check_permission
54
+ from android.storage import app_storage_path
48
55
 
49
56
  NotificationManagerCompat = autoclass('androidx.core.app.NotificationManagerCompat')
50
57
  NotificationCompat = autoclass('androidx.core.app.NotificationCompat')
51
58
 
52
59
  # Notification Design
53
- NotificationCompatBuilder = autoclass('androidx.core.app.NotificationCompat$Builder') # pylint: disable=C0301
54
- NotificationCompatBigTextStyle = autoclass('androidx.core.app.NotificationCompat$BigTextStyle') # pylint: disable=C0301
55
- NotificationCompatBigPictureStyle = autoclass('androidx.core.app.NotificationCompat$BigPictureStyle') # pylint: disable=C0301
60
+ NotificationCompatBuilder = autoclass('androidx.core.app.NotificationCompat$Builder')
61
+ NotificationCompatBigTextStyle = autoclass('androidx.core.app.NotificationCompat$BigTextStyle')
62
+ NotificationCompatBigPictureStyle = autoclass('androidx.core.app.NotificationCompat$BigPictureStyle')
56
63
  NotificationCompatInboxStyle = autoclass('androidx.core.app.NotificationCompat$InboxStyle')
57
- except Exception as e:# pylint: disable=W0718
64
+ except Exception as e:
58
65
  print(e)
59
66
  print("""
60
67
  Dependency Error: Add the following in buildozer.spec:
@@ -74,11 +81,12 @@ class Notification(BaseNotification):
74
81
  both_imgs == using lager icon and big picture
75
82
  :param big_picture_path: Relative Path to the image resource.
76
83
  :param large_icon_path: Relative Path to the image resource.
77
- :param progress_current_value: interger To set progress bar current value.
78
- :param progress_max_value: interger To set Max range for progress bar.
79
- :param body: large text For `big_Text` style, while `message` acts as sub-title.
84
+ :param progress_current_value: integer To set progress bar current value.
85
+ :param progress_max_value: integer To set Max range for progress bar.
86
+ :param body: large text For `big_Text` style, while `message` acts as subtitle.
80
87
  ---
81
88
  (Advance Options)
89
+ :param id: Pass in Old 'id' to use old instance
82
90
  :param callback: Function for notification Click.
83
91
  :param channel_name: - str Defaults to "Default Channel"
84
92
  :param channel_id: - str Defaults to "default_channel"
@@ -86,17 +94,19 @@ class Notification(BaseNotification):
86
94
  (Options during Dev On PC)
87
95
  :param logs: - Bool Defaults to True
88
96
  """
89
- notification_ids=[0]
97
+
98
+ notification_ids = [0]
90
99
  button_ids=[0]
91
100
  btns_box={}
92
101
  main_functions={}
93
102
 
94
103
  # During Development (When running on PC)
95
104
  BaseNotification.logs=not ON_ANDROID
96
- def __init__(self,**kwargs): #pylint: disable=W0231 #@dataclass already does work
105
+ def __init__(self,**kwargs): #@dataclass already does work
97
106
  super().__init__(**kwargs)
98
107
 
99
- self.__id = self.__getUniqueID()
108
+ self.__id = self.id or self.__get_unique_id() # Different use from self.name all notifications require `integers` id's not `strings`
109
+ self.id = self.__id # To use same Notification in different instances
100
110
 
101
111
  # To Track progressbar last update (According to Android Docs Don't update bar to often, I also faced so issues when doing that)
102
112
  self.__update_timer = None
@@ -104,19 +114,155 @@ class Notification(BaseNotification):
104
114
  self.__progress_bar_title = ''
105
115
  self.__cooldown = 0
106
116
 
107
- self.__formatChannel(kwargs)
117
+ self.__built_parameter_filled=False
118
+ self.__using_set_priority_method=False
119
+
120
+ self.__format_channel(self.channel_name, self.channel_id)
108
121
  if not ON_ANDROID:
109
122
  return
110
- # TODO make send method wait for __asks_permission_if_needed method
111
- self.__asks_permission_if_needed()
112
- self.notification_manager = context.getSystemService(context.NOTIFICATION_SERVICE)
113
- self.__builder=NotificationCompatBuilder(context, self.channel_id)# pylint: disable=E0606
123
+
124
+ NotificationHandler.asks_permission()
125
+ notification_service = context.getSystemService(context.NOTIFICATION_SERVICE)
126
+ self.notification_manager = cast(NotificationManager, notification_service)
127
+ self.__builder = NotificationCompatBuilder(context, self.channel_id)
128
+
129
+ def cancel(self,_id=0):
130
+ """
131
+ Removes a Notification instance from tray
132
+ :param _id: not required uses Notification instance id as default
133
+ """
134
+ if ON_ANDROID:
135
+ self.notification_manager.cancel(_id or self.__id)
136
+ if self.logs:
137
+ print('Removed Notification.')
138
+
139
+ @classmethod
140
+ def cancelAll(cls):
141
+ """
142
+ Removes all app Notifications from tray
143
+ """
144
+ if ON_ANDROID:
145
+ cls.__return_notification_manger().cancelAll()
146
+ if cls.logs:
147
+ print('Removed All Notifications.')
148
+
149
+ @classmethod
150
+ def createChannel(cls, id, name:str, description='',importance:Importance='urgent'):
151
+ """
152
+ Creates a user visible toggle button for specific notifications, Required For Android 8.0+
153
+ :param id: Used to send other notifications later through same channel.
154
+ :param name: user-visible channel name.
155
+ :param description: user-visible detail about channel (Not required defaults to empty str).
156
+ :param importance: ['urgent', 'high', 'medium', 'low', 'none'] defaults to 'urgent' i.e. makes a sound and shows briefly
157
+ :return: boolean if channel created
158
+ """
159
+
160
+ if not ON_ANDROID:
161
+ return False
162
+
163
+ notification_manager=cls.__return_notification_manger()
164
+ android_importance_value = cls.__get_android_importance(importance)
165
+
166
+ if BuildVersion.SDK_INT >= 26 and notification_manager.getNotificationChannel(id) is None:
167
+ channel = NotificationChannel(id, name, android_importance_value)
168
+ if description:
169
+ channel.setDescription(description)
170
+ notification_manager.createNotificationChannel(channel)
171
+ return True
172
+ return False
173
+
174
+ @classmethod
175
+ def deleteChannel(cls, channel_id):
176
+ """Delete a Channel Matching channel_id"""
177
+ if not ON_ANDROID:
178
+ return None
179
+
180
+ cls.__return_notification_manger().deleteNotificationChannel(channel_id)
181
+
182
+ @classmethod
183
+ def deleteAllChannel(cls):
184
+ """Deletes all notification channel
185
+ :returns amount deleted
186
+ """
187
+
188
+ amount = 0
189
+ if not ON_ANDROID:
190
+ return amount
191
+
192
+ notification_manager = cls.__return_notification_manger()
193
+ channels = cls.getChannels()
194
+ for index in range(channels.size()):
195
+ amount += 1
196
+ channel = channels.get(index)
197
+ channel_id = channel.getId()
198
+ notification_manager.deleteNotificationChannel(channel_id)
199
+ return amount
200
+
201
+ def refresh(self):
202
+ """TO apply new components on notification"""
203
+ if self.__built_parameter_filled:
204
+ # Don't dispatch before filling required values `self.__create_basic_notification`
205
+ # We generally shouldn't dispatch till user call .send()
206
+ self.__dispatch_notification()
207
+
208
+ def setBigPicture(self,path):
209
+ """
210
+ set a Big Picture at the bottom
211
+ :param path: can be `Relative Path` or `URL`
212
+ :return:
213
+ """
214
+ if ON_ANDROID:
215
+ self.__build_img(path, NotificationStyles.BIG_PICTURE)
216
+ elif self.logs:
217
+ # When on android there are other logs
218
+ print('Done setting big picture')
219
+
220
+ def setSmallIcon(self,path):
221
+ """
222
+ sets small icon to the top left
223
+ :param path: can be `Relative Path` or `URL`
224
+ :return:
225
+ """
226
+ if ON_ANDROID:
227
+ self.__insert_app_icon(path)
228
+ elif self.logs:
229
+ # When on android there are other logs
230
+ print('Done setting small icon')
231
+
232
+ def setLargeIcon(self,path):
233
+ """
234
+ sets Large icon to the right
235
+ :param path: can be `Relative Path` or `URL`
236
+ :return:
237
+ """
238
+ if ON_ANDROID:
239
+ self.__build_img(path, NotificationStyles.LARGE_ICON)
240
+ elif self.logs:
241
+ #When on android there are other logs
242
+ print('Done setting large icon')
243
+
244
+ def setBigText(self,body):
245
+ """Sets a big text for when drop down button is pressed
246
+
247
+ :param body: The big text that will be displayed
248
+ """
249
+ if ON_ANDROID:
250
+ big_text_style = NotificationCompatBigTextStyle()
251
+ big_text_style.bigText(str(body))
252
+ self.__builder.setStyle(big_text_style)
253
+ elif self.logs:
254
+ # When on android there are other logs
255
+ print('Done setting big text')
256
+
114
257
  def showInfiniteProgressBar(self):
115
258
  """Displays an (Infinite) progress Bar in Notification, that continues loading indefinitely.
116
259
  Can be Removed By `removeProgressBar` Method
117
260
  """
118
- self.__builder.setProgress(0,0, True)
119
- self.__dispatchNotification()
261
+ if self.logs:
262
+ print('Showing infinite progressbar')
263
+ if ON_ANDROID:
264
+ self.__builder.setProgress(0,0, True)
265
+ self.refresh()
120
266
 
121
267
  def updateTitle(self,new_title):
122
268
  """Changes Old Title
@@ -129,7 +275,7 @@ class Notification(BaseNotification):
129
275
  print(f'new notification title: {self.title}')
130
276
  if ON_ANDROID:
131
277
  self.__builder.setContentTitle(String(self.title))
132
- self.__dispatchNotification()
278
+ self.refresh()
133
279
 
134
280
  def updateMessage(self,new_message):
135
281
  """Changes Old Message
@@ -142,7 +288,7 @@ class Notification(BaseNotification):
142
288
  print(f'new notification message: {self.message}')
143
289
  if ON_ANDROID:
144
290
  self.__builder.setContentText(String(self.message))
145
- self.__dispatchNotification()
291
+ self.refresh()
146
292
 
147
293
  def updateProgressBar(self,current_value:int,message:str='',title:str='',cooldown=0.5):
148
294
  """Updates progress bar current value
@@ -151,7 +297,7 @@ class Notification(BaseNotification):
151
297
  current_value (int): the value from progressbar current progress
152
298
  message (str): defaults to last message
153
299
  title (str): defaults to last title
154
- cooldown (float, optional): Avoid Updating progressbar value too frequently Defaults to 0.5secs
300
+ cooldown (float, optional): Little Time to Wait before change actually reflects, to avoid android Ignoring Change, Defaults to 0.5secs
155
301
 
156
302
  NOTE: There is a 0.5sec delay for value change, if updating title,msg with progressbar frequently pass them in too to avoid update issues
157
303
  """
@@ -187,7 +333,7 @@ class Notification(BaseNotification):
187
333
  if self.__progress_bar_title:
188
334
  self.updateTitle(self.__progress_bar_title)
189
335
 
190
- self.__dispatchNotification()
336
+ self.refresh()
191
337
  self.__update_timer = None
192
338
 
193
339
 
@@ -204,7 +350,7 @@ class Notification(BaseNotification):
204
350
  message (str, optional): notification message. Defaults to 'last message'.
205
351
  show_on_update (bool, optional): To show notification briefly when progressbar removed. Defaults to True.
206
352
  title (str, optional): notification title. Defaults to 'last title'.
207
- cooldown (float, optional): Avoid Updating progressbar value too frequently Defaults to 0.5secs
353
+ cooldown (float, optional): Little Time to Wait before change actually reflects, to avoid android Ignoring Change, Defaults to 0.5secs
208
354
 
209
355
  In-Built Delay of 0.5sec According to Android Docs Don't Update Progressbar too Frequently
210
356
  """
@@ -220,7 +366,6 @@ class Notification(BaseNotification):
220
366
  self.__update_timer = None
221
367
 
222
368
 
223
-
224
369
  def delayed_update():
225
370
  if self.logs:
226
371
  msg = message or self.message
@@ -236,11 +381,23 @@ class Notification(BaseNotification):
236
381
  self.updateTitle(title)
237
382
  self.__builder.setOnlyAlertOnce(not show_on_update)
238
383
  self.__builder.setProgress(0, 0, False)
239
- self.__dispatchNotification()
384
+ self.refresh()
240
385
 
241
386
  # Incase `self.updateProgressBar delayed_update` is called right before this method, so android doesn't bounce update
242
387
  threading.Timer(cooldown, delayed_update).start()
243
388
 
389
+ def setPriority(self,importance:Importance):
390
+ """
391
+ For devices less than android 8
392
+ :param importance: ['urgent', 'high', 'medium', 'low', 'none'] defaults to 'urgent' i.e. makes a sound and shows briefly
393
+ :return:
394
+ """
395
+ self.__using_set_priority_method=True
396
+ if ON_ANDROID:
397
+ android_importance_value = self.__get_android_importance(importance)
398
+ if not isinstance(android_importance_value, str): # Can be an empty str if importance='none'
399
+ self.__builder.setPriority(android_importance_value)
400
+
244
401
  def send(self,silent:bool=False,persistent=False,close_on_click=True):
245
402
  """Sends notification
246
403
 
@@ -249,26 +406,29 @@ class Notification(BaseNotification):
249
406
  persistent (bool): True To not remove Notification When User hits clears All notifications button
250
407
  close_on_click (bool): True if you want Notification to be removed when clicked
251
408
  """
252
- self.silent=self.silent or silent
409
+ self.silent= silent or self.silent
253
410
  if ON_ANDROID:
254
- self.__startNotificationBuild(persistent,close_on_click)
255
- self.__dispatchNotification()
411
+ self.__start_notification_build(persistent, close_on_click)
412
+ self.__dispatch_notification()
413
+
256
414
  if self.logs:
257
415
  string_to_display=''
258
416
  print("\n Sent Notification!!!")
259
417
  for name,value in vars(self).items():
260
- if value and name in ["title", "message", "style", "body", "large_icon_path", "big_picture_path", "progress_current_value", "progress_max_value", "channel_name"]:
418
+ if value and name in ["title", "message", "style", "body", "large_icon_path", "big_picture_path", "progress_current_value", "progress_max_value", "channel_name",'body','name']:
261
419
  if name == "progress_max_value":
262
420
  if self.style == NotificationStyles.PROGRESS:
263
421
  string_to_display += f'\n {name}: {value}'
422
+ elif name == "body":
423
+ if self.style == NotificationStyles.BIG_TEXT:
424
+ string_to_display += f'\n {name}: {value}'
264
425
  else:
265
426
  string_to_display += f'\n {name}: {value}'
266
427
 
267
428
  string_to_display +="\n (Won't Print Logs When Complied,except if selected `Notification.logs=True`)"
268
429
  print(string_to_display)
269
430
  if DEV:
270
- print(f'channel_name: {self.channel_name}, Channel ID: {self.channel_id}, id: {self.__id}')
271
- print('Can\'t Send Package Only Runs on Android !!! ---> Check "https://github.com/Fector101/android_notify/" for Documentation.\n' if DEV else '\n') # pylint: disable=C0301
431
+ print(f'channel_name: {self.channel_name}, Channel ID: {self.channel_id}, __id: {self.__id}')
272
432
 
273
433
  def addButton(self, text:str,on_release):
274
434
  """For adding action buttons
@@ -283,7 +443,7 @@ class Notification(BaseNotification):
283
443
  if not ON_ANDROID:
284
444
  return
285
445
 
286
- btn_id= self.__getIDForButton()
446
+ btn_id= self.__get_id_for_button()
287
447
  action = f"BTN_ACTION_{btn_id}"
288
448
 
289
449
  action_intent = Intent(context, PythonActivity)
@@ -325,9 +485,9 @@ class Notification(BaseNotification):
325
485
  """
326
486
  if ON_ANDROID:
327
487
  self.__builder.mActions.clear()
328
- self.__dispatchNotification()
488
+ self.refresh()
329
489
  if self.logs:
330
- print('Removed Notication Buttons')
490
+ print('Removed Notification Buttons')
331
491
 
332
492
  @run_on_ui_thread
333
493
  def addNotificationStyle(self,style:str,already_sent=False):
@@ -340,108 +500,108 @@ class Notification(BaseNotification):
340
500
  """
341
501
 
342
502
  if not ON_ANDROID:
343
- # TODO for logs when not on android and style related to imgs etraxct app path from buildozer.spec and print
503
+ # TODO for logs when not on android and style related to imgs extract app path from buildozer.spec and print
344
504
  return False
345
505
 
346
506
  if style == NotificationStyles.BIG_TEXT:
347
- big_text_style = NotificationCompatBigTextStyle() # pylint: disable=E0606
348
- big_text_style.bigText(str(self.body))
349
- self.__builder.setStyle(big_text_style)
507
+ self.setBigText(self.body)
350
508
 
351
509
  elif style == NotificationStyles.INBOX:
352
- inbox_style = NotificationCompatInboxStyle() # pylint: disable=E0606
510
+ inbox_style = NotificationCompatInboxStyle()
353
511
  for line in self.message.split("\n"):
354
512
  inbox_style.addLine(str(line))
355
513
  self.__builder.setStyle(inbox_style)
356
514
 
357
515
  elif (style == NotificationStyles.LARGE_ICON and self.large_icon_path) or (style == NotificationStyles.BIG_PICTURE and self.big_picture_path):
358
516
  img = self.large_icon_path if style == NotificationStyles.LARGE_ICON else self.big_picture_path
359
- self.__buildImg(img, style)
517
+ self.__build_img(img, style)
360
518
 
361
519
  elif style == NotificationStyles.BOTH_IMGS and (self.big_picture_path or self.large_icon_path):
362
520
  if self.big_picture_path:
363
- self.__buildImg(self.big_picture_path, NotificationStyles.BIG_PICTURE)
521
+ self.setBigPicture(self.big_picture_path)
364
522
  if self.large_icon_path:
365
- self.__buildImg(self.large_icon_path, NotificationStyles.LARGE_ICON)
523
+ self.setLargeIcon(self.large_icon_path)
366
524
 
367
525
  elif style == NotificationStyles.PROGRESS:
368
526
  self.__builder.setProgress(self.progress_max_value, self.progress_current_value, False)
369
527
 
370
528
  if already_sent:
371
- self.__dispatchNotification()
529
+ self.refresh()
372
530
 
373
531
  return True
374
- # elif style == 'custom':
375
- # self.__builder = self.__doCustomStyle()
376
532
 
377
- def __dispatchNotification(self):
378
- self.notification_manager.notify(self.__id, self.__builder.build())
379
- def __startNotificationBuild(self,persistent,close_on_click):
380
- self.__createBasicNotification(persistent,close_on_click)
533
+ def __dispatch_notification(self):
534
+ if NotificationHandler.has_permission():
535
+ self.notification_manager.notify(self.__id, self.__builder.build())
536
+ else:
537
+ print('Permission not granted to send notifications')
538
+ # Not asking for permission too frequently, This makes dialog popup to stop showing
539
+ # NotificationHandler.asks_permission()
540
+
541
+ def __start_notification_build(self, persistent, close_on_click):
542
+ self.__create_basic_notification(persistent, close_on_click)
381
543
  if self.style not in ['simple','']:
382
544
  self.addNotificationStyle(self.style)
383
545
 
384
- def __createBasicNotification(self,persistent,close_on_click):
385
- # Notification Channel (Required for Android 8.0+)
386
- # print("THis is cchannel is ",self.channel_id) #"
546
+ def __create_basic_notification(self, persistent, close_on_click):
387
547
  if BuildVersion.SDK_INT >= 26 and self.notification_manager.getNotificationChannel(self.channel_id) is None:
388
- importance=NotificationManagerCompat.IMPORTANCE_DEFAULT if self.silent else NotificationManagerCompat.IMPORTANCE_HIGH # pylint: disable=possibly-used-before-assignment
389
- # importance = 3 or 4
390
- channel = NotificationChannel(
391
- self.channel_id,
392
- self.channel_name,
393
- importance
394
- )
395
- self.notification_manager.createNotificationChannel(channel)
396
-
548
+ self.createChannel(self.channel_id, self.channel_name)
549
+ elif not self.__using_set_priority_method:
550
+ self.setPriority('medium' if self.silent else 'urgent')
397
551
  # Build the notification
398
552
  # str() This is to prevent Error When user does Notification.title='blah' instead of Notification(title='blah'
399
- # TODO fix this by creating a on_Title method in other versions
553
+ # TODO fix this by creating a on_title method in other versions
400
554
  self.__builder.setContentTitle(str(self.title))
401
555
  self.__builder.setContentText(str(self.message))
402
- self.__insertAppIcon()
403
- self.__builder.setDefaults(NotificationCompat.DEFAULT_ALL) # pylint: disable=E0606
404
- self.__builder.setPriority(NotificationCompat.PRIORITY_DEFAULT if self.silent else NotificationCompat.PRIORITY_HIGH)
556
+ self.__insert_app_icon()
557
+ self.__builder.setDefaults(NotificationCompat.DEFAULT_ALL)
405
558
  self.__builder.setOnlyAlertOnce(True)
406
559
  self.__builder.setOngoing(persistent)
407
560
  self.__builder.setAutoCancel(close_on_click)
408
- self.__addIntentToOpenApp()
409
-
410
- # def __doCustomStyle(self):
411
- # # TODO Will implement when needed
412
- # return self.__builder
413
- def __insertAppIcon(self):
414
- if self.app_icon not in ['','Defaults to package app icon']:
415
- self.__setIconFromBitmap(self.app_icon)
561
+ self.__add_intent_to_open_app()
562
+ self.__built_parameter_filled = True
563
+
564
+ def __insert_app_icon(self,path=''):
565
+ if path or self.app_icon not in ['','Defaults to package app icon']:
566
+ if self.logs:
567
+ print('getting custom icon...')
568
+ self.__set_icon_from_bitmap(path or self.app_icon)
416
569
  else:
570
+ if self.logs:
571
+ print('using default icon...')
417
572
  self.__builder.setSmallIcon(context.getApplicationInfo().icon)
418
573
 
419
- def __buildImg(self, user_img,img_style):
574
+ def __build_img(self, user_img, img_style):
420
575
  if user_img.startswith('http://') or user_img.startswith('https://'):
421
- def callback(bitmap):
422
- self.__applyNotificationImage(bitmap,img_style)
576
+ def callback(bitmap_):
577
+ self.__apply_notification_image(bitmap_,img_style)
423
578
  thread = threading.Thread(
424
- target=self.__getBitmapFromURL,
579
+ target=self.__get_bitmap_from_url,
425
580
  args=[user_img,callback]
426
581
  )
427
582
  thread.start()
428
583
  else:
429
- bitmap = self.__getImgFromPath(user_img)
584
+ bitmap = self.__get_img_from_path(user_img)
430
585
  if bitmap:
431
- self.__applyNotificationImage(bitmap,img_style)
586
+ self.__apply_notification_image(bitmap, img_style)
432
587
 
433
- def __setIconFromBitmap(self,img_path):
434
- """Path can be link or relative path"""
588
+ def __set_icon_from_bitmap(self, img_path):
589
+ """Path can be a link or relative path"""
435
590
  if img_path.startswith('http://') or img_path.startswith('https://'):
436
- def callback(bitmap):
437
- icon = IconCompat.createWithBitmap(bitmap)
438
- self.__builder.setSmallIcon(icon)
591
+ def callback(bitmap_):
592
+ if bitmap_:
593
+ icon_ = IconCompat.createWithBitmap(bitmap_)
594
+ self.__builder.setSmallIcon(icon_)
595
+ else:
596
+ if self.logs:
597
+ print('Using Default Icon as fallback......')
598
+ self.__builder.setSmallIcon(context.getApplicationInfo().icon)
439
599
  threading.Thread(
440
- target=self.__getBitmapFromURL,
441
- args=[img_path,callback]
442
- ).start()
600
+ target=self.__get_bitmap_from_url,
601
+ args=[img_path,callback]
602
+ ).start()
443
603
  else:
444
- bitmap = self.__getImgFromPath(img_path)
604
+ bitmap = self.__get_img_from_path(img_path)
445
605
  if bitmap:
446
606
  icon = IconCompat.createWithBitmap(bitmap)
447
607
  self.__builder.setSmallIcon(icon)
@@ -450,8 +610,9 @@ class Notification(BaseNotification):
450
610
  print('Failed getting img for custom notification icon defaulting to app icon')
451
611
  self.__builder.setSmallIcon(context.getApplicationInfo().icon)
452
612
 
453
- def __getImgFromPath(self, relative_path):
454
- app_folder=os.path.join(app_storage_path(),'app') # pylint: disable=possibly-used-before-assignment
613
+ @staticmethod
614
+ def __get_img_from_path(relative_path):
615
+ app_folder=os.path.join(app_storage_path(),'app')
455
616
  output_path = os.path.join(app_folder, relative_path)
456
617
  if not os.path.exists(output_path):
457
618
  print(f"\nImage not found at path: {output_path}, (Local images gotten from App Path)")
@@ -463,7 +624,7 @@ class Notification(BaseNotification):
463
624
  uri = Uri.parse(f"file://{output_path}")
464
625
  return BitmapFactory.decodeStream(context.getContentResolver().openInputStream(uri))
465
626
 
466
- def __getBitmapFromURL(self,url,callback):
627
+ def __get_bitmap_from_url(self, url, callback):
467
628
  """Gets Bitmap from url
468
629
 
469
630
  Args:
@@ -483,52 +644,36 @@ class Notification(BaseNotification):
483
644
  if bitmap:
484
645
  callback(bitmap)
485
646
  else:
486
- print('Error No Bitmap ------------')
487
- except Exception as e:
488
- # TODO get all types of JAVA Error
489
- print('Error Type ',e)
647
+ print('Error No Bitmap for small icon ------------')
648
+ except Exception as extracting_bitmap_frm_URL_error:
649
+ callback(None)
650
+ # TODO get all types of JAVA Error that can fail here
651
+ print('Error Type ',extracting_bitmap_frm_URL_error)
490
652
  print('Failed to get Bitmap from URL ',traceback.format_exc())
491
653
 
492
654
  @run_on_ui_thread
493
- def __applyNotificationImage(self,bitmap,img_style):
494
- if self.logs:
495
- print('appying notification image-------')
655
+ def __apply_notification_image(self, bitmap, img_style):
496
656
  try:
497
- if img_style == NotificationStyles.BIG_PICTURE:
498
- big_picture_style = NotificationCompatBigPictureStyle().bigPicture(bitmap) # pylint: disable=E0606
657
+ if img_style == NotificationStyles.BIG_PICTURE and bitmap:
658
+ big_picture_style = NotificationCompatBigPictureStyle().bigPicture(bitmap)
499
659
  self.__builder.setStyle(big_picture_style)
500
- elif img_style == NotificationStyles.LARGE_ICON:
660
+ elif img_style == NotificationStyles.LARGE_ICON and bitmap:
501
661
  self.__builder.setLargeIcon(bitmap)
502
- self.__dispatchNotification()
662
+ # LargeIcon requires smallIcon to be already set
663
+ # 'setLarge, setBigPic' tries to dispatch before filling required values `self.__create_basic_notification`
664
+ self.refresh()
503
665
  if self.logs:
504
666
  print('Done adding image to notification-------')
505
- except Exception as e:
667
+ except Exception as notification_image_error:
506
668
  img = self.large_icon_path if img_style == NotificationStyles.LARGE_ICON else self.big_picture_path
507
- print(f'Failed adding Image of style: {img_style} || From path: {img}, Exception {e}')
508
- print('could stop get Img from URL ',traceback.format_exc())
509
-
510
- def __getUniqueID(self):
511
- notification_id = self.notification_ids[-1] + 1
512
- self.notification_ids.append(notification_id)
513
- return notification_id
669
+ print(f'Failed adding Image of style: {img_style} || From path: {img}, Exception {notification_image_error}')
670
+ print('could not get Img traceback: ',traceback.format_exc())
514
671
 
515
- def __asks_permission_if_needed(self):
516
- """
517
- Ask for permission to send notifications if needed.
518
- """
519
- def on_permissions_result(permissions, grant): # pylint: disable=unused-argument
520
- if self.logs:
521
- print("Permission Grant State: ",grant)
522
-
523
- permissions=[Permission.POST_NOTIFICATIONS] # pylint: disable=E0606
524
- if not all(check_permission(p) for p in permissions):
525
- request_permissions(permissions,on_permissions_result) # pylint: disable=E0606
526
-
527
- def __addIntentToOpenApp(self):
672
+ def __add_intent_to_open_app(self):
528
673
  intent = Intent(context, PythonActivity)
529
- action = str(self.identifer) or f"ACTION_{self.__id}"
674
+ action = str(self.name or self.__id)
530
675
  intent.setAction(action)
531
- self.__addDataToIntent(intent)
676
+ self.__add_data_to_intent(intent)
532
677
  self.main_functions[action]=self.callback
533
678
  intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
534
679
 
@@ -538,32 +683,38 @@ class Notification(BaseNotification):
538
683
  )
539
684
  self.__builder.setContentIntent(pending_intent)
540
685
 
541
- def __addDataToIntent(self,intent):
542
- """Persit Some data to notification object for later use"""
686
+ def __add_data_to_intent(self, intent):
687
+ """Persist Some data to notification object for later use"""
543
688
  bundle = Bundle()
544
689
  bundle.putString("title", self.title or 'Title Placeholder')
545
- bundle.putInt("notify_id", self.__id)
690
+ # bundle.putInt("notify_id", self.__id)
691
+ bundle.putInt("notify_id", 101)
546
692
  intent.putExtras(bundle)
547
693
 
548
- def __getIDForButton(self):
694
+ def __get_id_for_button(self):
549
695
  btn_id = self.button_ids[-1] + 1
550
696
  self.button_ids.append(btn_id)
551
697
  return btn_id
552
698
 
553
- def __formatChannel(self, inputted_kwargs):
554
- if 'channel_name' in inputted_kwargs:
555
- cleaned_name = inputted_kwargs['channel_name'].strip()
699
+ def __format_channel(self, channel_name:str='Default Channel',channel_id:str='default_channel'):
700
+ """
701
+ Formats and sets self.channel_name and self.channel_id to formatted version
702
+ :param channel_name:
703
+ :param channel_id:
704
+ :return:
705
+ """
706
+ # Shorten channel name # android docs as at most 40 chars
707
+ if channel_name != 'Default Channel':
708
+ cleaned_name = channel_name.strip()
556
709
  self.channel_name = cleaned_name[:40] if cleaned_name else 'Default Channel'
557
710
 
558
- if 'channel_id' in inputted_kwargs:
559
- cleaned_id = inputted_kwargs['channel_id'].strip()
560
- self.channel_id = self.__generate_channel_id(cleaned_id) if cleaned_id else 'default_channel'
561
- elif 'channel_name' in inputted_kwargs:
562
- # Generate channel_id from channel_name if only channel_name is provided
563
- generated_id = self.__generate_channel_id(inputted_kwargs['channel_name'])
564
- self.channel_id = generated_id
711
+ # If no channel_id then generating channel_id from passed in channel_name
712
+ if channel_id == 'default_channel':
713
+ generated_id = self.__generate_channel_id(channel_name)
714
+ self.channel_id = generated_id
565
715
 
566
- def __generate_channel_id(self,channel_name: str) -> str:
716
+ @staticmethod
717
+ def __generate_channel_id(channel_name: str) -> str:
567
718
  """
568
719
  Generate a readable and consistent channel ID from a channel name.
569
720
 
@@ -581,40 +732,84 @@ class Notification(BaseNotification):
581
732
  channel_id = channel_id.strip('_')
582
733
  return channel_id[:50]
583
734
 
735
+ def __get_unique_id(self):
736
+ notification_id = self.notification_ids[-1] + 1
737
+ self.notification_ids.append(notification_id)
738
+ return notification_id
739
+
740
+ @staticmethod
741
+ def __return_notification_manger():
742
+ notification_service = context.getSystemService(context.NOTIFICATION_SERVICE)
743
+ return cast(NotificationManager, notification_service)
744
+
745
+ @classmethod
746
+ def getChannels(cls):
747
+ """Return all existing channels"""
748
+ if not ON_ANDROID:
749
+ return []
750
+
751
+ return cls.__return_notification_manger().getNotificationChannels()
752
+
753
+ @staticmethod
754
+ def __get_android_importance(importance:Importance):
755
+ """
756
+ Returns Android Importance Values
757
+ :param importance: ['urgent','high','medium','low','none']
758
+ :return: Android equivalent int or empty str
759
+ """
760
+ value=''
761
+ if importance == 'urgent':
762
+ value = NotificationCompat.PRIORITY_HIGH if BuildVersion.SDK_INT <= 25 else NotificationManagerCompat.IMPORTANCE_HIGH
763
+ elif importance == 'high':
764
+ value = NotificationCompat.PRIORITY_DEFAULT if BuildVersion.SDK_INT <= 25 else NotificationManagerCompat.IMPORTANCE_DEFAULT
765
+ elif importance == 'medium':
766
+ value = NotificationCompat.PRIORITY_LOW if BuildVersion.SDK_INT <= 25 else NotificationManagerCompat.IMPORTANCE_LOW
767
+ elif importance == 'low':
768
+ value = NotificationCompat.PRIORITY_MIN if BuildVersion.SDK_INT <= 25 else NotificationManagerCompat.IMPORTANCE_MIN
769
+ elif importance == 'none':
770
+ value = '' if BuildVersion.SDK_INT <= 25 else NotificationManagerCompat.IMPORTANCE_NONE
771
+
772
+ return value
773
+ # side-note 'medium' = NotificationCompat.PRIORITY_LOW and 'low' = NotificationCompat.PRIORITY_MIN # weird but from docs
774
+ # TODO method to create channel groups
775
+
776
+
584
777
  class NotificationHandler:
585
778
  """For Notification Operations """
586
- __identifer = None
779
+ __name = None
587
780
  __bound = False
588
-
781
+ __requesting_permission=False
589
782
  @classmethod
590
- def getIdentifer(cls):
591
- """Returns identifer for Clicked Notification."""
783
+ def get_name(cls):
784
+ """Returns name or id str for Clicked Notification."""
592
785
  if not cls.is_on_android():
593
786
  return "Not on Android"
594
787
 
595
- saved_intent = cls.__identifer
596
- if not saved_intent or (isinstance(saved_intent, str) and saved_intent.startswith("android.intent")):
788
+ saved_intent = cls.__name
789
+ cls.__name = None # so value won't be set when opening app not from notification
790
+ # print('saved_intent ',saved_intent)
791
+ # if not saved_intent or (isinstance(saved_intent, str) and saved_intent.startswith("android.intent")):
597
792
  # All other notifications are not None after First notification opens app
598
793
  # NOTE these notifications are also from Last time app was opened and they Still Give Value after first one opens App
599
794
  # TODO Find a way to get intent when App if Swiped From recents
600
- __PythonActivity = autoclass(ACTIVITY_CLASS_NAME)
601
- __mactivity = __PythonActivity.mActivity
602
- __context = cast('android.content.Context', __mactivity)
603
- __Intent = autoclass('android.content.Intent')
604
- __intent = __Intent(__context, __PythonActivity)
605
- action = __intent.getAction()
606
- print('Start up Intent ----', action)
607
- print('start Up Title --->',__intent.getStringExtra("title"))
795
+ # Below action is always None
796
+ # __PythonActivity = autoclass(ACTIVITY_CLASS_NAME)
797
+ # __mactivity = __PythonActivity.mActivity
798
+ # __context = cast('android.content.Context', __mactivity)
799
+ # __Intent = autoclass('android.content.Intent')
800
+ # __intent = __Intent(__context, __PythonActivity)
801
+ # action = __intent.getAction()
802
+ # print('Start up Intent ----', action)
803
+ # print('start Up Title --->',__intent.getStringExtra("title"))
608
804
 
609
805
  return saved_intent
610
806
 
611
807
  @classmethod
612
- def __notificationHandler(cls,intent):
808
+ def __notification_handler(cls, intent):
613
809
  """Calls Function Attached to notification on click.
614
810
  Don't Call this function manual, it's Already Attach to Notification.
615
811
 
616
- Returns:
617
- str: The Identiter of Nofication that was clicked.
812
+ Sets self.__name #action of Notification that was clicked from Notification.name or Notification.id
618
813
  """
619
814
  if not cls.is_on_android():
620
815
  return "Not on Android"
@@ -623,13 +818,13 @@ class NotificationHandler:
623
818
  if DEV:
624
819
  print("notifty_functions ",notifty_functions)
625
820
  print("buttons_object", buttons_object)
626
- action = None
627
821
  try:
628
822
  action = intent.getAction()
629
- cls.__identifer = action
823
+ cls.__name = action
630
824
 
631
- print("The Action --> ",action)
825
+ # print("The Action --> ",action)
632
826
  if action == "android.intent.action.MAIN": # Not Open From Notification
827
+ cls.__name = None
633
828
  return 'Not notification'
634
829
 
635
830
  print(intent.getStringExtra("title"))
@@ -638,12 +833,11 @@ class NotificationHandler:
638
833
  notifty_functions[action]()
639
834
  elif action in buttons_object:
640
835
  buttons_object[action]()
641
- except Exception as e: # pylint: disable=broad-exception-caught
836
+ except Exception as notification_handler_function_error:
837
+ print("Error Type ",notification_handler_function_error)
642
838
  print('Failed to run function: ', traceback.format_exc())
643
- print("Error Type ",e)
644
- except Exception as e: # pylint: disable=broad-exception-caught
645
- print('Notify Hanlder Failed ',e)
646
- return action
839
+ except Exception as extracting_notification_props_error:
840
+ print('Notify Handler Failed ',extracting_notification_props_error)
647
841
 
648
842
  @classmethod
649
843
  def bindNotifyListener(cls):
@@ -652,15 +846,16 @@ class NotificationHandler:
652
846
  return "Not on Android"
653
847
  #TODO keep trying BroadcastReceiver
654
848
  if cls.__bound:
655
- print("bounding done already ")
849
+ print("binding done already ")
656
850
  return True
657
851
  try:
658
- activity.bind(on_new_intent=cls.__notificationHandler)
852
+ activity.bind(on_new_intent=cls.__notification_handler)
659
853
  cls.__bound = True
660
854
  return True
661
- except Exception as e: # pylint: disable=broad-exception-caught
662
- print('Failed to bin notitfications listener',e)
855
+ except Exception as binding_listener_error:
856
+ print('Failed to bin notifications listener',binding_listener_error)
663
857
  return False
858
+
664
859
  @classmethod
665
860
  def unbindNotifyListener(cls):
666
861
  """Removes Listener for Notifications Click"""
@@ -669,10 +864,10 @@ class NotificationHandler:
669
864
 
670
865
  #Beta TODO use BroadcastReceiver
671
866
  try:
672
- activity.unbind(on_new_intent=cls.__notificationHandler)
867
+ activity.unbind(on_new_intent=cls.__notification_handler)
673
868
  return True
674
- except Exception as e: # pylint: disable=broad-exception-caught
675
- print("Failed to unbind notifications listener: ",e)
869
+ except Exception as unbinding_listener_error:
870
+ print("Failed to unbind notifications listener: ",unbinding_listener_error)
676
871
  return False
677
872
 
678
873
  @staticmethod
@@ -680,4 +875,49 @@ class NotificationHandler:
680
875
  """Utility to check if the app is running on Android."""
681
876
  return ON_ANDROID
682
877
 
878
+ @staticmethod
879
+ def has_permission():
880
+ """
881
+ Checks if device has permission to send notifications
882
+ returns True if device has permission
883
+ """
884
+ if not ON_ANDROID:
885
+ return True
886
+ return check_permission(Permission.POST_NOTIFICATIONS)
887
+
888
+ @classmethod
889
+ @run_on_ui_thread
890
+ def asks_permission(cls,callback=None):
891
+ """
892
+ Ask for permission to send notifications if needed.
893
+ Passes True to callback if access granted
894
+ """
895
+ if cls.__requesting_permission or not ON_ANDROID:
896
+ return True
897
+
898
+ def on_permissions_result(permissions, grants):
899
+ try:
900
+ if callback:
901
+ if can_accept_arguments(callback, True):
902
+ callback(grants[0])
903
+ else:
904
+ callback()
905
+ except Exception as request_permission_error:
906
+ print('Exception: ',request_permission_error)
907
+ print('Permission response callback error: ',traceback.format_exc())
908
+ finally:
909
+ cls.__requesting_permission = False
910
+
911
+ if not cls.has_permission():
912
+ cls.__requesting_permission = True
913
+ request_permissions([Permission.POST_NOTIFICATIONS],on_permissions_result)
914
+ else:
915
+ cls.__requesting_permission = False
916
+ if callback:
917
+ if can_accept_arguments(callback,True):
918
+ callback(True)
919
+ else:
920
+ callback()
921
+
922
+
683
923
  NotificationHandler.bindNotifyListener()