android-notify 1.57.1__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
@@ -1,36 +1,41 @@
1
1
  """This Module Contain Class for creating Notification With Java"""
2
- import difflib
3
2
  import traceback
4
- import os
3
+ import os,re
5
4
  import threading
6
- import re
5
+ from .an_types import Importance
7
6
  from .styles import NotificationStyles
8
7
  from .base import BaseNotification
9
8
  DEV=0
10
9
  ON_ANDROID = False
11
10
 
11
+ # noinspection PyBroadException
12
12
  try:
13
13
  # Android Imports
14
- from jnius import autoclass,cast # Needs Java to be installed pylint: disable=W0611, C0114
15
- from android import activity # pylint: disable=import-error
16
- from android.config import ACTIVITY_CLASS_NAME # pylint: disable=import-error
17
- 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
18
18
 
19
- # Get the required Java classes
19
+ # Get the required Java classes needs to on android to import
20
20
  Bundle = autoclass('android.os.Bundle')
21
21
  PythonActivity = autoclass('org.kivy.android.PythonActivity')
22
22
  String = autoclass('java.lang.String')
23
23
  Intent = autoclass('android.content.Intent')
24
24
  PendingIntent = autoclass('android.app.PendingIntent')
25
- context = PythonActivity.mActivity # Get the app's context
25
+ context = PythonActivity.mActivity
26
26
  BitmapFactory = autoclass('android.graphics.BitmapFactory')
27
27
  BuildVersion = autoclass('android.os.Build$VERSION')
28
+ VersionCodes = autoclass('android.os.Build$VERSION_CODES')
28
29
  NotificationManager = autoclass('android.app.NotificationManager')
29
30
  NotificationChannel = autoclass('android.app.NotificationChannel')
30
31
  IconCompat = autoclass('androidx.core.graphics.drawable.IconCompat')
31
32
  ON_ANDROID = True
32
- except Exception as e:# pylint: disable=W0718
33
- 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.'
34
39
  # from .types_idea import *
35
40
  # print(MESSAGE) Already Printing in core.py
36
41
 
@@ -44,18 +49,18 @@ except Exception as e:# pylint: disable=W0718
44
49
 
45
50
  if ON_ANDROID:
46
51
  try:
47
- from android.permissions import request_permissions, Permission,check_permission # pylint: disable=E0401
48
- 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
49
54
 
50
55
  NotificationManagerCompat = autoclass('androidx.core.app.NotificationManagerCompat')
51
56
  NotificationCompat = autoclass('androidx.core.app.NotificationCompat')
52
57
 
53
58
  # Notification Design
54
- NotificationCompatBuilder = autoclass('androidx.core.app.NotificationCompat$Builder') # pylint: disable=C0301
55
- NotificationCompatBigTextStyle = autoclass('androidx.core.app.NotificationCompat$BigTextStyle') # pylint: disable=C0301
56
- 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')
57
62
  NotificationCompatInboxStyle = autoclass('androidx.core.app.NotificationCompat$InboxStyle')
58
- except Exception as e:# pylint: disable=W0718
63
+ except Exception as e:
59
64
  print(e)
60
65
  print("""
61
66
  Dependency Error: Add the following in buildozer.spec:
@@ -75,11 +80,12 @@ class Notification(BaseNotification):
75
80
  both_imgs == using lager icon and big picture
76
81
  :param big_picture_path: Relative Path to the image resource.
77
82
  :param large_icon_path: Relative Path to the image resource.
78
- :param progress_current_value: interger To set progress bar current value.
79
- :param progress_max_value: interger To set Max range for progress bar.
80
- :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.
81
86
  ---
82
87
  (Advance Options)
88
+ :param id: Pass in Old 'id' to use old instance
83
89
  :param callback: Function for notification Click.
84
90
  :param channel_name: - str Defaults to "Default Channel"
85
91
  :param channel_id: - str Defaults to "default_channel"
@@ -87,32 +93,172 @@ class Notification(BaseNotification):
87
93
  (Options during Dev On PC)
88
94
  :param logs: - Bool Defaults to True
89
95
  """
90
- notification_ids=[0]
96
+
97
+ notification_ids = [0]
91
98
  button_ids=[0]
92
99
  btns_box={}
93
100
  main_functions={}
94
101
 
95
102
  # During Development (When running on PC)
96
103
  BaseNotification.logs=not ON_ANDROID
97
- def __init__(self,**kwargs): #pylint: disable=W0231 #@dataclass already does work
104
+ def __init__(self,**kwargs): #@dataclass already does work
98
105
  super().__init__(**kwargs)
99
106
 
100
- self.__id = self.__getUniqueID()
101
- self.__update_timer = None # To Track progressbar last update (According to Android Docs Don't update bar to often, I also faced so issues when doing that)
102
- self.__formatChannel(kwargs)
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
109
+
110
+ # To Track progressbar last update (According to Android Docs Don't update bar to often, I also faced so issues when doing that)
111
+ self.__update_timer = None
112
+ self.__progress_bar_msg = ''
113
+ self.__progress_bar_title = ''
114
+ self.__cooldown = 0
115
+
116
+ self.__built_parameter_filled=False
117
+ self.__using_set_priority_method=False
118
+
119
+ self.__format_channel(self.channel_name, self.channel_id)
103
120
  if not ON_ANDROID:
104
121
  return
105
- # TODO make send method wait for __asks_permission_if_needed method
106
- self.__asks_permission_if_needed()
107
- self.notification_manager = context.getSystemService(context.NOTIFICATION_SERVICE)
108
- 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
+
109
252
  def showInfiniteProgressBar(self):
110
253
  """Displays an (Infinite) progress Bar in Notification, that continues loading indefinitely.
111
254
  Can be Removed By `removeProgressBar` Method
112
255
  """
113
- self.__builder.setProgress(0,0, True)
114
- self.__dispatchNotification()
115
-
256
+ if self.logs:
257
+ print('Showing infinite progressbar')
258
+ if ON_ANDROID:
259
+ self.__builder.setProgress(0,0, True)
260
+ self.refresh()
261
+
116
262
  def updateTitle(self,new_title):
117
263
  """Changes Old Title
118
264
 
@@ -124,7 +270,7 @@ class Notification(BaseNotification):
124
270
  print(f'new notification title: {self.title}')
125
271
  if ON_ANDROID:
126
272
  self.__builder.setContentTitle(String(self.title))
127
- self.__dispatchNotification()
273
+ self.refresh()
128
274
 
129
275
  def updateMessage(self,new_message):
130
276
  """Changes Old Message
@@ -137,111 +283,154 @@ class Notification(BaseNotification):
137
283
  print(f'new notification message: {self.message}')
138
284
  if ON_ANDROID:
139
285
  self.__builder.setContentText(String(self.message))
140
- self.__dispatchNotification()
286
+ self.refresh()
141
287
 
142
- def updateProgressBar(self,current_value:int,message:str='',title:str='',buffer=0.5):
288
+ def updateProgressBar(self,current_value:int,message:str='',title:str='',cooldown=0.5):
143
289
  """Updates progress bar current value
144
-
290
+
145
291
  Args:
146
292
  current_value (int): the value from progressbar current progress
147
293
  message (str): defaults to last message
148
294
  title (str): defaults to last title
149
- buffer (float, optional): Avoid Updating progressbar value too frequently Defaults to 0.5secs
150
- NOTE: There is a 0.5sec delay for value change, if updating title,msg with progressbar frequenlty pass them in too to avoid update issues
295
+ cooldown (float, optional): Little Time to Wait before change actually reflects, to avoid android Ignoring Change, Defaults to 0.5secs
296
+
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
151
298
  """
152
- # replacing new value for when timer is called
153
- self.progress_current_value = current_value
154
299
 
155
- if self.__update_timer:
156
- if self.logs:
157
- print('Progressbar update too soon Doing bounce 0.5sec')
300
+ # replacing new values for when timer is called
301
+ self.progress_current_value = current_value
302
+ self.__progress_bar_msg = message
303
+ self.__progress_bar_title = title
304
+
305
+ if self.__update_timer and self.__update_timer.is_alive():
306
+ # Make Logs too Dirty
307
+ # if self.logs:
308
+ # remaining = self.__cooldown - (time.time() - self.__timer_start_time)
309
+ # print(f'Progressbar update too soon, waiting for cooldown ({max(0, remaining):.2f}s)')
158
310
  return
159
311
 
160
312
  def delayed_update():
161
- if self.__update_timer is None:
162
- # Ensure we are not executing an old timer
313
+ if self.__update_timer is None: # Ensure we are not executing an old timer
314
+ if self.logs:
315
+ print('ProgressBar update skipped: bar has been removed.')
163
316
  return
164
317
  if self.logs:
165
318
  print(f'Progress Bar Update value: {self.progress_current_value}')
166
319
 
167
320
  if not ON_ANDROID:
321
+ self.__update_timer = None
168
322
  return
323
+
169
324
  self.__builder.setProgress(self.progress_max_value, self.progress_current_value, False)
170
- if message:
171
- self.updateMessage(message)
172
- if title:
173
- self.updateTitle(title)
174
- self.__dispatchNotification()
325
+
326
+ if self.__progress_bar_msg:
327
+ self.updateMessage(self.__progress_bar_msg)
328
+ if self.__progress_bar_title:
329
+ self.updateTitle(self.__progress_bar_title)
330
+
331
+ self.refresh()
175
332
  self.__update_timer = None
176
333
 
177
334
 
178
335
  # Start a new timer that runs after 0.5 seconds
179
- self.__update_timer = threading.Timer(buffer, delayed_update)
336
+ # self.__timer_start_time = time.time() # for logs
337
+ self.__cooldown = cooldown
338
+ self.__update_timer = threading.Timer(cooldown, delayed_update)
180
339
  self.__update_timer.start()
181
340
 
182
- def removeProgressBar(self,message='',show_on_update=True, title:str='') -> bool:
341
+ def removeProgressBar(self,message='',show_on_update=True, title:str='',cooldown=0.5):
183
342
  """Removes Progress Bar from Notification
184
343
 
185
344
  Args:
186
345
  message (str, optional): notification message. Defaults to 'last message'.
187
- show_on_update (bool, optional): To show notification brifely when progressbar removed. Defaults to True.
346
+ show_on_update (bool, optional): To show notification briefly when progressbar removed. Defaults to True.
188
347
  title (str, optional): notification title. Defaults to 'last title'.
348
+ cooldown (float, optional): Little Time to Wait before change actually reflects, to avoid android Ignoring Change, Defaults to 0.5secs
349
+
350
+ In-Built Delay of 0.5sec According to Android Docs Don't Update Progressbar too Frequently
189
351
  """
352
+
353
+ # To Cancel any queued timer from `updateProgressBar` method and to avoid race effect incase it somehow gets called while in this method
354
+ # Avoiding Running `updateProgressBar.delayed_update` at all
355
+ # so didn't just set `self.__progress_bar_title` and `self.progress_current_value` to 0
190
356
  if self.__update_timer:
357
+ # Make Logs too Dirty
358
+ # if self.logs:
359
+ # print('cancelled progressbar stream update because about to remove',self.progress_current_value)
191
360
  self.__update_timer.cancel()
192
361
  self.__update_timer = None
193
-
194
- if self.logs:
195
- msg = message or self.message
196
- title_=title or self.title
197
- print(f'removed progress bar with message: {msg} and title: {title_}')
198
362
 
199
- if not ON_ANDROID:
200
- return False
201
363
 
202
- self.__builder.setOnlyAlertOnce(not show_on_update)
203
- if message:
204
- self.updateMessage(message)
205
- if title:
206
- self.updateTitle(title)
207
- self.__builder.setProgress(0, 0, False)
208
- self.__dispatchNotification()
209
- return True
364
+ def delayed_update():
365
+ if self.logs:
366
+ msg = message or self.message
367
+ title_=title or self.title
368
+ print(f'removed progress bar with message: {msg} and title: {title_}')
369
+
370
+ if not ON_ANDROID:
371
+ return
372
+
373
+ if message:
374
+ self.updateMessage(message)
375
+ if title:
376
+ self.updateTitle(title)
377
+ self.__builder.setOnlyAlertOnce(not show_on_update)
378
+ self.__builder.setProgress(0, 0, False)
379
+ self.refresh()
380
+
381
+ # Incase `self.updateProgressBar delayed_update` is called right before this method, so android doesn't bounce update
382
+ threading.Timer(cooldown, delayed_update).start()
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)
210
395
 
211
396
  def send(self,silent:bool=False,persistent=False,close_on_click=True):
212
397
  """Sends notification
213
-
398
+
214
399
  Args:
215
400
  silent (bool): True if you don't want to show briefly on screen
216
401
  persistent (bool): True To not remove Notification When User hits clears All notifications button
217
402
  close_on_click (bool): True if you want Notification to be removed when clicked
218
403
  """
219
- self.silent=self.silent or silent
404
+ self.silent= silent or self.silent
220
405
  if ON_ANDROID:
221
- self.__startNotificationBuild(persistent,close_on_click)
222
- self.__dispatchNotification()
406
+ self.__start_notification_build(persistent, close_on_click)
407
+ self.__dispatch_notification()
408
+
223
409
  if self.logs:
224
410
  string_to_display=''
225
411
  print("\n Sent Notification!!!")
226
412
  for name,value in vars(self).items():
227
- 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']:
228
414
  if name == "progress_max_value":
229
415
  if self.style == NotificationStyles.PROGRESS:
230
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}'
231
420
  else:
232
421
  string_to_display += f'\n {name}: {value}'
233
422
 
234
423
  string_to_display +="\n (Won't Print Logs When Complied,except if selected `Notification.logs=True`)"
235
424
  print(string_to_display)
236
425
  if DEV:
237
- print(f'channel_name: {self.channel_name}, Channel ID: {self.channel_id}, id: {self.__id}')
238
- 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}')
239
427
 
240
428
  def addButton(self, text:str,on_release):
241
429
  """For adding action buttons
242
430
 
243
431
  Args:
244
432
  text (str): Text For Button
433
+ on_release: function to be called when button is clicked
245
434
  """
246
435
  if self.logs:
247
436
  print('Added Button: ', text)
@@ -249,7 +438,7 @@ class Notification(BaseNotification):
249
438
  if not ON_ANDROID:
250
439
  return
251
440
 
252
- btn_id= self.__getIDForButton()
441
+ btn_id= self.__get_id_for_button()
253
442
  action = f"BTN_ACTION_{btn_id}"
254
443
 
255
444
  action_intent = Intent(context, PythonActivity)
@@ -291,9 +480,9 @@ class Notification(BaseNotification):
291
480
  """
292
481
  if ON_ANDROID:
293
482
  self.__builder.mActions.clear()
294
- self.__dispatchNotification()
483
+ self.refresh()
295
484
  if self.logs:
296
- print('Removed Notication Buttons')
485
+ print('Removed Notification Buttons')
297
486
 
298
487
  @run_on_ui_thread
299
488
  def addNotificationStyle(self,style:str,already_sent=False):
@@ -306,108 +495,103 @@ class Notification(BaseNotification):
306
495
  """
307
496
 
308
497
  if not ON_ANDROID:
309
- # 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
310
499
  return False
311
500
 
312
501
  if style == NotificationStyles.BIG_TEXT:
313
- big_text_style = NotificationCompatBigTextStyle() # pylint: disable=E0606
314
- big_text_style.bigText(str(self.body))
315
- self.__builder.setStyle(big_text_style)
502
+ self.setBigText(self.body)
316
503
 
317
504
  elif style == NotificationStyles.INBOX:
318
- inbox_style = NotificationCompatInboxStyle() # pylint: disable=E0606
505
+ inbox_style = NotificationCompatInboxStyle()
319
506
  for line in self.message.split("\n"):
320
507
  inbox_style.addLine(str(line))
321
508
  self.__builder.setStyle(inbox_style)
322
509
 
323
510
  elif (style == NotificationStyles.LARGE_ICON and self.large_icon_path) or (style == NotificationStyles.BIG_PICTURE and self.big_picture_path):
324
511
  img = self.large_icon_path if style == NotificationStyles.LARGE_ICON else self.big_picture_path
325
- self.__buildImg(img, style)
512
+ self.__build_img(img, style)
326
513
 
327
514
  elif style == NotificationStyles.BOTH_IMGS and (self.big_picture_path or self.large_icon_path):
328
515
  if self.big_picture_path:
329
- self.__buildImg(self.big_picture_path, NotificationStyles.BIG_PICTURE)
516
+ self.setBigPicture(self.big_picture_path)
330
517
  if self.large_icon_path:
331
- self.__buildImg(self.large_icon_path, NotificationStyles.LARGE_ICON)
518
+ self.setLargeIcon(self.large_icon_path)
332
519
 
333
520
  elif style == NotificationStyles.PROGRESS:
334
521
  self.__builder.setProgress(self.progress_max_value, self.progress_current_value, False)
335
522
 
336
523
  if already_sent:
337
- self.__dispatchNotification()
524
+ self.refresh()
338
525
 
339
526
  return True
340
- # elif style == 'custom':
341
- # self.__builder = self.__doCustomStyle()
342
527
 
343
- def __dispatchNotification(self):
528
+ def __dispatch_notification(self):
344
529
  self.notification_manager.notify(self.__id, self.__builder.build())
345
- def __startNotificationBuild(self,persistent,close_on_click):
346
- 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)
347
533
  if self.style not in ['simple','']:
348
534
  self.addNotificationStyle(self.style)
349
535
 
350
- def __createBasicNotification(self,persistent,close_on_click):
351
- # Notification Channel (Required for Android 8.0+)
352
- # print("THis is cchannel is ",self.channel_id) #"
536
+ def __create_basic_notification(self, persistent, close_on_click):
353
537
  if BuildVersion.SDK_INT >= 26 and self.notification_manager.getNotificationChannel(self.channel_id) is None:
354
- importance=NotificationManagerCompat.IMPORTANCE_DEFAULT if self.silent else NotificationManagerCompat.IMPORTANCE_HIGH # pylint: disable=possibly-used-before-assignment
355
- # importance = 3 or 4
356
- channel = NotificationChannel(
357
- self.channel_id,
358
- self.channel_name,
359
- importance
360
- )
361
- self.notification_manager.createNotificationChannel(channel)
362
-
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')
363
541
  # Build the notification
364
542
  # str() This is to prevent Error When user does Notification.title='blah' instead of Notification(title='blah'
365
- # 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
366
544
  self.__builder.setContentTitle(str(self.title))
367
545
  self.__builder.setContentText(str(self.message))
368
- self.__insertAppIcon()
369
- self.__builder.setDefaults(NotificationCompat.DEFAULT_ALL) # pylint: disable=E0606
370
- 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)
371
548
  self.__builder.setOnlyAlertOnce(True)
372
549
  self.__builder.setOngoing(persistent)
373
550
  self.__builder.setAutoCancel(close_on_click)
374
- self.__addIntentToOpenApp()
375
-
376
- # def __doCustomStyle(self):
377
- # # TODO Will implement when needed
378
- # return self.__builder
379
- def __insertAppIcon(self):
380
- if self.app_icon not in ['','Defaults to package app icon']:
381
- 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)
382
559
  else:
560
+ if self.logs:
561
+ print('using default icon...')
383
562
  self.__builder.setSmallIcon(context.getApplicationInfo().icon)
384
563
 
385
- def __buildImg(self, user_img,img_style):
564
+ def __build_img(self, user_img, img_style):
386
565
  if user_img.startswith('http://') or user_img.startswith('https://'):
387
- def callback(bitmap):
388
- self.__applyNotificationImage(bitmap,img_style)
566
+ def callback(bitmap_):
567
+ self.__apply_notification_image(bitmap_,img_style)
389
568
  thread = threading.Thread(
390
- target=self.__getBitmapFromURL,
569
+ target=self.__get_bitmap_from_url,
391
570
  args=[user_img,callback]
392
571
  )
393
572
  thread.start()
394
573
  else:
395
- bitmap = self.__getImgFromPath(user_img)
574
+ bitmap = self.__get_img_from_path(user_img)
396
575
  if bitmap:
397
- self.__applyNotificationImage(bitmap,img_style)
576
+ self.__apply_notification_image(bitmap, img_style)
398
577
 
399
- def __setIconFromBitmap(self,img_path):
400
- """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"""
401
580
  if img_path.startswith('http://') or img_path.startswith('https://'):
402
- def callback(bitmap):
403
- icon = IconCompat.createWithBitmap(bitmap)
404
- 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)
405
589
  threading.Thread(
406
- target=self.__getBitmapFromURL,
407
- args=[img_path,callback]
408
- ).start()
590
+ target=self.__get_bitmap_from_url,
591
+ args=[img_path,callback]
592
+ ).start()
409
593
  else:
410
- bitmap = self.__getImgFromPath(img_path)
594
+ bitmap = self.__get_img_from_path(img_path)
411
595
  if bitmap:
412
596
  icon = IconCompat.createWithBitmap(bitmap)
413
597
  self.__builder.setSmallIcon(icon)
@@ -416,8 +600,9 @@ class Notification(BaseNotification):
416
600
  print('Failed getting img for custom notification icon defaulting to app icon')
417
601
  self.__builder.setSmallIcon(context.getApplicationInfo().icon)
418
602
 
419
- def __getImgFromPath(self, relative_path):
420
- 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')
421
606
  output_path = os.path.join(app_folder, relative_path)
422
607
  if not os.path.exists(output_path):
423
608
  print(f"\nImage not found at path: {output_path}, (Local images gotten from App Path)")
@@ -429,7 +614,7 @@ class Notification(BaseNotification):
429
614
  uri = Uri.parse(f"file://{output_path}")
430
615
  return BitmapFactory.decodeStream(context.getContentResolver().openInputStream(uri))
431
616
 
432
- def __getBitmapFromURL(self,url,callback):
617
+ def __get_bitmap_from_url(self, url, callback):
433
618
  """Gets Bitmap from url
434
619
 
435
620
  Args:
@@ -449,52 +634,36 @@ class Notification(BaseNotification):
449
634
  if bitmap:
450
635
  callback(bitmap)
451
636
  else:
452
- print('Error No Bitmap ------------')
453
- except Exception as e:
454
- # TODO get all types of JAVA Error
455
- 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)
456
642
  print('Failed to get Bitmap from URL ',traceback.format_exc())
457
643
 
458
644
  @run_on_ui_thread
459
- def __applyNotificationImage(self,bitmap,img_style):
460
- if self.logs:
461
- print('appying notification image-------')
645
+ def __apply_notification_image(self, bitmap, img_style):
462
646
  try:
463
- if img_style == NotificationStyles.BIG_PICTURE:
464
- 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)
465
649
  self.__builder.setStyle(big_picture_style)
466
- elif img_style == NotificationStyles.LARGE_ICON:
650
+ elif img_style == NotificationStyles.LARGE_ICON and bitmap:
467
651
  self.__builder.setLargeIcon(bitmap)
468
- 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()
469
655
  if self.logs:
470
656
  print('Done adding image to notification-------')
471
- except Exception as e:
657
+ except Exception as notification_image_error:
472
658
  img = self.large_icon_path if img_style == NotificationStyles.LARGE_ICON else self.big_picture_path
473
- print(f'Failed adding Image of style: {img_style} || From path: {img}, Exception {e}')
474
- print('could stop get Img from URL ',traceback.format_exc())
475
-
476
- def __getUniqueID(self):
477
- notification_id = self.notification_ids[-1] + 1
478
- self.notification_ids.append(notification_id)
479
- return notification_id
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())
480
661
 
481
- def __asks_permission_if_needed(self):
482
- """
483
- Ask for permission to send notifications if needed.
484
- """
485
- def on_permissions_result(permissions, grant): # pylint: disable=unused-argument
486
- if self.logs:
487
- print("Permission Grant State: ",grant)
488
-
489
- permissions=[Permission.POST_NOTIFICATIONS] # pylint: disable=E0606
490
- if not all(check_permission(p) for p in permissions):
491
- request_permissions(permissions,on_permissions_result) # pylint: disable=E0606
492
-
493
- def __addIntentToOpenApp(self):
662
+ def __add_intent_to_open_app(self):
494
663
  intent = Intent(context, PythonActivity)
495
- action = str(self.identifer) or f"ACTION_{self.__id}"
664
+ action = str(self.name or self.__id)
496
665
  intent.setAction(action)
497
- self.__addDataToIntent(intent)
666
+ self.__add_data_to_intent(intent)
498
667
  self.main_functions[action]=self.callback
499
668
  intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
500
669
 
@@ -504,32 +673,38 @@ class Notification(BaseNotification):
504
673
  )
505
674
  self.__builder.setContentIntent(pending_intent)
506
675
 
507
- def __addDataToIntent(self,intent):
508
- """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"""
509
678
  bundle = Bundle()
510
679
  bundle.putString("title", self.title or 'Title Placeholder')
511
- bundle.putInt("notify_id", self.__id)
680
+ # bundle.putInt("notify_id", self.__id)
681
+ bundle.putInt("notify_id", 101)
512
682
  intent.putExtras(bundle)
513
683
 
514
- def __getIDForButton(self):
684
+ def __get_id_for_button(self):
515
685
  btn_id = self.button_ids[-1] + 1
516
686
  self.button_ids.append(btn_id)
517
687
  return btn_id
518
688
 
519
- def __formatChannel(self, inputted_kwargs):
520
- if 'channel_name' in inputted_kwargs:
521
- 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()
522
699
  self.channel_name = cleaned_name[:40] if cleaned_name else 'Default Channel'
523
700
 
524
- if 'channel_id' in inputted_kwargs:
525
- cleaned_id = inputted_kwargs['channel_id'].strip()
526
- self.channel_id = self.__generate_channel_id(cleaned_id) if cleaned_id else 'default_channel'
527
- elif 'channel_name' in inputted_kwargs:
528
- # Generate channel_id from channel_name if only channel_name is provided
529
- generated_id = self.__generate_channel_id(inputted_kwargs['channel_name'])
530
- 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
531
705
 
532
- def __generate_channel_id(self,channel_name: str) -> str:
706
+ @staticmethod
707
+ def __generate_channel_id(channel_name: str) -> str:
533
708
  """
534
709
  Generate a readable and consistent channel ID from a channel name.
535
710
 
@@ -547,40 +722,84 @@ class Notification(BaseNotification):
547
722
  channel_id = channel_id.strip('_')
548
723
  return channel_id[:50]
549
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
+
550
767
  class NotificationHandler:
551
768
  """For Notification Operations """
552
- __identifer = None
769
+ __name = None
553
770
  __bound = False
554
771
 
555
772
  @classmethod
556
- def getIdentifer(cls):
557
- """Returns identifer for Clicked Notification."""
773
+ def get_name(cls):
774
+ """Returns name or id str for Clicked Notification."""
558
775
  if not cls.is_on_android():
559
776
  return "Not on Android"
560
777
 
561
- saved_intent = cls.__identifer
562
- 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")):
563
782
  # All other notifications are not None after First notification opens app
564
783
  # NOTE these notifications are also from Last time app was opened and they Still Give Value after first one opens App
565
784
  # TODO Find a way to get intent when App if Swiped From recents
566
- __PythonActivity = autoclass(ACTIVITY_CLASS_NAME)
567
- __mactivity = __PythonActivity.mActivity
568
- __context = cast('android.content.Context', __mactivity)
569
- __Intent = autoclass('android.content.Intent')
570
- __intent = __Intent(__context, __PythonActivity)
571
- action = __intent.getAction()
572
- print('Start up Intent ----', action)
573
- 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"))
574
794
 
575
795
  return saved_intent
576
796
 
577
797
  @classmethod
578
- def __notificationHandler(cls,intent):
798
+ def __notification_handler(cls, intent):
579
799
  """Calls Function Attached to notification on click.
580
800
  Don't Call this function manual, it's Already Attach to Notification.
581
801
 
582
- Returns:
583
- str: The Identiter of Nofication that was clicked.
802
+ Sets self.__name #action of Notification that was clicked from Notification.name or Notification.id
584
803
  """
585
804
  if not cls.is_on_android():
586
805
  return "Not on Android"
@@ -589,13 +808,13 @@ class NotificationHandler:
589
808
  if DEV:
590
809
  print("notifty_functions ",notifty_functions)
591
810
  print("buttons_object", buttons_object)
592
- action = None
593
811
  try:
594
812
  action = intent.getAction()
595
- cls.__identifer = action
813
+ cls.__name = action
596
814
 
597
- print("The Action --> ",action)
815
+ # print("The Action --> ",action)
598
816
  if action == "android.intent.action.MAIN": # Not Open From Notification
817
+ cls.__name = None
599
818
  return 'Not notification'
600
819
 
601
820
  print(intent.getStringExtra("title"))
@@ -604,12 +823,11 @@ class NotificationHandler:
604
823
  notifty_functions[action]()
605
824
  elif action in buttons_object:
606
825
  buttons_object[action]()
607
- 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)
608
828
  print('Failed to run function: ', traceback.format_exc())
609
- print("Error Type ",e)
610
- except Exception as e: # pylint: disable=broad-exception-caught
611
- print('Notify Hanlder Failed ',e)
612
- return action
829
+ except Exception as extracting_notification_props_error:
830
+ print('Notify Handler Failed ',extracting_notification_props_error)
613
831
 
614
832
  @classmethod
615
833
  def bindNotifyListener(cls):
@@ -618,15 +836,16 @@ class NotificationHandler:
618
836
  return "Not on Android"
619
837
  #TODO keep trying BroadcastReceiver
620
838
  if cls.__bound:
621
- print("bounding done already ")
839
+ print("binding done already ")
622
840
  return True
623
841
  try:
624
- activity.bind(on_new_intent=cls.__notificationHandler)
842
+ activity.bind(on_new_intent=cls.__notification_handler)
625
843
  cls.__bound = True
626
844
  return True
627
- except Exception as e: # pylint: disable=broad-exception-caught
628
- print('Failed to bin notitfications listener',e)
845
+ except Exception as binding_listener_error:
846
+ print('Failed to bin notifications listener',binding_listener_error)
629
847
  return False
848
+
630
849
  @classmethod
631
850
  def unbindNotifyListener(cls):
632
851
  """Removes Listener for Notifications Click"""
@@ -635,10 +854,10 @@ class NotificationHandler:
635
854
 
636
855
  #Beta TODO use BroadcastReceiver
637
856
  try:
638
- activity.unbind(on_new_intent=cls.__notificationHandler)
857
+ activity.unbind(on_new_intent=cls.__notification_handler)
639
858
  return True
640
- except Exception as e: # pylint: disable=broad-exception-caught
641
- print("Failed to unbind notifications listener: ",e)
859
+ except Exception as unbinding_listener_error:
860
+ print("Failed to unbind notifications listener: ",unbinding_listener_error)
642
861
  return False
643
862
 
644
863
  @staticmethod
@@ -646,4 +865,33 @@ class NotificationHandler:
646
865
  """Utility to check if the app is running on Android."""
647
866
  return ON_ANDROID
648
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
+
649
897
  NotificationHandler.bindNotifyListener()