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