android-notify 1.3__py3-none-any.whl → 1.60.6.dev0__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
@@ -1,375 +1,1192 @@
1
1
  """This Module Contain Class for creating Notification With Java"""
2
- import difflib
3
- import random
4
- import os
5
- import re
6
- from jnius import autoclass,cast # pylint: disable=W0611, C0114
7
-
8
- DEV=0
9
- ON_ANDROID = False
10
-
11
- try:
12
- # Get the required Java classes
13
- PythonActivity = autoclass('org.kivy.android.PythonActivity')
14
- String = autoclass('java.lang.String')
15
- Intent = autoclass('android.content.Intent')
16
- PendingIntent = autoclass('android.app.PendingIntent')
17
- context = PythonActivity.mActivity # Get the app's context
18
- BitmapFactory = autoclass('android.graphics.BitmapFactory')
19
- BuildVersion = autoclass('android.os.Build$VERSION')
20
- NotificationManager = autoclass('android.app.NotificationManager')
21
- NotificationChannel = autoclass('android.app.NotificationChannel')
22
- ON_ANDROID = True
23
- except Exception as e:# pylint: disable=W0718
24
- MESSAGE='This Package Only Runs on Android !!! ---> Check "https://github.com/Fector101/android_notify/" to see design patterns and more info.' # pylint: disable=C0301
25
- print(MESSAGE if DEV else '')
26
-
27
- if ON_ANDROID:
28
- try:
29
- from android.permissions import request_permissions, Permission,check_permission # pylint: disable=E0401
30
- from android.storage import app_storage_path # pylint: disable=E0401
31
-
32
- NotificationManagerCompat = autoclass('androidx.core.app.NotificationManagerCompat')
33
- NotificationCompat = autoclass('androidx.core.app.NotificationCompat')
34
-
35
- # Notification Design
36
- NotificationCompatBuilder = autoclass('androidx.core.app.NotificationCompat$Builder') # pylint: disable=C0301
37
- NotificationCompatBigTextStyle = autoclass('androidx.core.app.NotificationCompat$BigTextStyle') # pylint: disable=C0301
38
- NotificationCompatBigPictureStyle = autoclass('androidx.core.app.NotificationCompat$BigPictureStyle') # pylint: disable=C0301
39
- NotificationCompatInboxStyle = autoclass('androidx.core.app.NotificationCompat$InboxStyle')
40
- except Exception as e:# pylint: disable=W0718
41
- print(e if DEV else '','Import Fector101')
42
- # print(e if DEV else '')
43
- print("""
44
- Dependency Error: Add the following in buildozer.spec:
45
- * android.gradle_dependencies = androidx.core:core-ktx:1.15.0, androidx.core:core:1.6.0
46
- * android.enable_androidx = True
47
- * android.permissions = POST_NOTIFICATIONS
48
- """)
49
-
50
- class Notification:
2
+ import os, time, threading, traceback
3
+ from typing import Any, Callable
4
+ from .config import cast, autoclass
5
+
6
+ from .an_types import Importance
7
+ from .an_utils import can_accept_arguments, get_python_activity_context, \
8
+ get_android_importance, generate_channel_id, get_img_from_path, setLayoutText, \
9
+ get_bitmap_from_url, add_data_to_intent, get_sound_uri, icon_finder, get_bitmap_from_path, \
10
+ can_show_permission_request_popup, open_settings_screen
11
+
12
+ from .config import from_service_file, get_python_activity, get_notification_manager, ON_ANDROID, on_flet_app, get_package_name
13
+ from .config import (Bundle, String, BuildVersion,
14
+ Intent, PendingIntent,
15
+ app_storage_path,
16
+ NotificationChannel, RemoteViews,
17
+ run_on_ui_thread,
18
+ )
19
+ from .config import (AndroidNotification, NotificationCompatBuilder,
20
+ NotificationCompatBigTextStyle, NotificationCompatBigPictureStyle,
21
+ NotificationCompatInboxStyle,
22
+ Color, Manifest
23
+ )
24
+ from .styles import NotificationStyles
25
+ from .base import BaseNotification
26
+
27
+ DEV = 0
28
+ PythonActivity = get_python_activity()
29
+ context = get_python_activity_context()
30
+
31
+
32
+ class Notification(BaseNotification):
51
33
  """
52
34
  Send a notification on Android.
53
35
 
54
36
  :param title: Title of the notification.
55
37
  :param message: Message body.
56
- :param style: Style of the notification
57
- ('simple', 'progress', 'big_text', 'inbox', 'big_picture', 'large_icon', 'both_imgs').
58
- both_imgs == using lager icon and big picture
59
- :param big_picture_path: Path to the image resource.
60
- :param large_icon_path: Path to the image resource.
38
+ ---
39
+ (Style Options)
40
+ :param style: Style of the notification ('simple', 'progress', 'big_text', 'inbox', 'big_picture', 'large_icon', 'both_imgs'). both_imgs == using lager icon and big picture
41
+ :param big_picture_path: Relative Path to the image resource.
42
+ :param large_icon_path: Relative Path to the image resource.
43
+ :param progress_current_value: Integer To set progress bar current value.
44
+ :param progress_max_value: Integer To set Max range for progress bar.
45
+ :param body: Large text For `big_Text` style, while `message` acts as subtitle.
46
+ :param lines_txt: text separated by newLine symbol For `inbox` style `use addLine method instead`
61
47
  ---
62
48
  (Advance Options)
63
- :param channel_name: Defaults to "Default Channel"
64
- :param channel_id: Defaults to "default_channel"
49
+ :param sub_text: str for additional information next to title
50
+ :param id: Pass in Old 'id' to use old instance
51
+ :param callback: Function for notification Click.
52
+ :param channel_name: - str Defaults to "Default Channel"
53
+ :param channel_id: - str Defaults to "default_channel"
65
54
  ---
66
55
  (Options during Dev On PC)
67
- :param logs: Defaults to True
56
+ :param logs: - Bool Defaults to True
57
+ ---
58
+ (Custom Style Options)
59
+ :param title_color: title color str (to be safe use hex code)
60
+ :param message_color: message color str (to be safe use hex code)
61
+
68
62
  """
69
- notification_ids=[]
70
- style_values=[
71
- '','simple',
72
- 'progress','big_text',
73
- 'inbox', 'big_picture',
74
- 'large_icon','both_imgs',
75
- 'custom'
76
- ] # TODO make pattern for non-android Notifications
77
- defaults={
78
- 'title':'Default Title',
79
- 'message':'Default Message', # TODO Might change message para to list if style set to inbox
80
- 'style':'simple',
81
- 'big_picture_path':'',
82
- 'large_icon_path':'',
83
- 'progress_max_value': 100,
84
- 'progress_current_value': 0,
85
- 'channel_name':'Default Channel',
86
- 'channel_id':'default_channel',
87
- 'logs':True,
88
- }
63
+
64
+ notification_ids = [0]
65
+ btns_box = {}
66
+ main_functions = {}
67
+ passed_check = False
68
+
89
69
  # During Development (When running on PC)
90
- logs=not ON_ANDROID
91
- def __init__(self,**kwargs):
92
- self.__validateArgs(kwargs)
93
- # Basic options
94
- self.title=''
95
- self.message=''
96
- self.style=''
97
- self.large_icon_path=''
98
- self.big_picture_path=''
99
- self.progress_current_value=0
100
- self.progress_max_value=100
101
- # Advance Options
102
- self.channel_name=''
103
- self.channel_id=''
104
- self.silent=False
105
- # During Dev on PC
106
- self.logs=self.logs
107
- # Private (Don't Touch)
108
- self.__id = self.__getUniqueID()
109
- self.__setArgs(kwargs)
110
- self.__builder=None
70
+ BaseNotification.logs = not ON_ANDROID
71
+
72
+ def __init__(self, **kwargs): # @dataclass already does work
73
+ super().__init__(**kwargs)
74
+
75
+ self.__id = self.id or self.__get_unique_id() # Different use from self.name all notifications require `integers` id's not `strings`
76
+ self.id = self.__id # To use same Notification in different instances
77
+
78
+ # To Track progressbar last update (According to Android Docs Don't update bar to often, I also faced so issues when doing that)
79
+ self.__update_timer = None
80
+ self.__progress_bar_msg = ''
81
+ self.__progress_bar_title = ''
82
+ self.__cooldown = 0
83
+
84
+ self.__built_parameter_filled = False
85
+ self.__using_set_priority_method = False
86
+
87
+ # For components
88
+ self.__lines = []
89
+ self.__has_small_icon = False # important notification can't send without
90
+ self.__using_custom = self.message_color or self.title_color
91
+ self.__format_channel(self.channel_name, self.channel_id)
92
+ self.__builder = None # available through getter `self.builder`
93
+ self.__no_of_buttons = 0
94
+ self.notification_manager = None
95
+
111
96
  if not ON_ANDROID:
112
97
  return
113
- # TODO make send method wait for __asks_permission_if_needed method
114
- self.__asks_permission_if_needed()
115
- self.notification_manager = context.getSystemService(context.NOTIFICATION_SERVICE)
116
98
 
117
- def updateTitle(self,new_title):
99
+ if not from_service_file() and not NotificationHandler.has_permission():
100
+ NotificationHandler.asks_permission()
101
+
102
+ self.notification_manager = get_notification_manager()
103
+ self.__builder = NotificationCompatBuilder(context, self.channel_id)
104
+
105
+ def addLine(self, text: str):
106
+ self.__lines.append(text)
107
+
108
+ def cancel(self, _id=0):
109
+ """
110
+ Removes a Notification instance from tray
111
+ :param _id: not required uses Notification instance id as default
112
+ """
113
+ if ON_ANDROID:
114
+ self.notification_manager.cancel(_id or self.__id)
115
+ if self.logs:
116
+ print('Removed Notification.')
117
+
118
+ @classmethod
119
+ def cancelAll(cls):
120
+ """
121
+ Removes all app Notifications from tray
122
+ """
123
+ if ON_ANDROID:
124
+ get_notification_manager().cancelAll()
125
+ if cls.logs:
126
+ print('Removed All Notifications.')
127
+
128
+ @classmethod
129
+ def channelExists(cls, channel_id):
130
+ """
131
+ Checks if a notification channel exists
132
+ """
133
+ if not ON_ANDROID:
134
+ return False
135
+ notification_manager = get_notification_manager()
136
+ if BuildVersion.SDK_INT >= 26 and notification_manager.getNotificationChannel(channel_id):
137
+ return True
138
+ return False
139
+
140
+ @classmethod
141
+ def createChannel(cls, id, name: str, description='', importance: Importance = 'urgent', res_sound_name=None):
142
+ """
143
+ Creates a user visible toggle button for specific notifications, Required For Android 8.0+
144
+ :param id: Used to send other notifications later through same channel.
145
+ :param name: user-visible channel name.
146
+ :param description: user-visible detail about channel (Not required defaults to empty str).
147
+ :param importance: ['urgent', 'high', 'medium', 'low', 'none'] defaults to 'urgent' i.e. makes a sound and shows briefly
148
+ :param res_sound_name: audio file file name (without .wav or .mp3) locate in res/raw/
149
+ :return: boolean if channel created
150
+ """
151
+
152
+ if not ON_ANDROID:
153
+ return False
154
+
155
+ notification_manager = get_notification_manager()
156
+ android_importance_value = get_android_importance(importance)
157
+ sound_uri = get_sound_uri(res_sound_name)
158
+
159
+ if not cls.channelExists(id):
160
+ channel = NotificationChannel(id, name, android_importance_value)
161
+ if description:
162
+ channel.setDescription(description)
163
+ if sound_uri:
164
+ channel.setSound(sound_uri, None)
165
+ notification_manager.createNotificationChannel(channel)
166
+ return True
167
+ return False
168
+
169
+ @classmethod
170
+ def deleteChannel(cls, channel_id):
171
+ """Delete a Channel Matching channel_id"""
172
+ if not ON_ANDROID:
173
+ return None
174
+
175
+ if cls.channelExists(channel_id):
176
+ get_notification_manager().deleteNotificationChannel(channel_id)
177
+
178
+ @classmethod
179
+ def deleteAllChannel(cls):
180
+ """Deletes all notification channel
181
+ :returns amount deleted
182
+ """
183
+
184
+ amount = 0
185
+ if not ON_ANDROID:
186
+ return amount
187
+
188
+ notification_manager = get_notification_manager()
189
+ channels = cls.getChannels()
190
+ for index in range(channels.size()):
191
+ amount += 1
192
+ channel = channels.get(index)
193
+ channel_id = channel.getId()
194
+ notification_manager.deleteNotificationChannel(channel_id)
195
+ return amount
196
+
197
+ @classmethod
198
+ def doChannelsExist(cls, ids):
199
+ """Uses list of IDs to check if channel exists
200
+ returns list of channels that don't exist
201
+ """
202
+ if not ON_ANDROID:
203
+ return ids # Assume none exist on non-Android environments
204
+ missing_channels = []
205
+ notification_manager = get_notification_manager()
206
+ for channel_id in ids:
207
+ exists = (
208
+ BuildVersion.SDK_INT >= 26 and
209
+ notification_manager.getNotificationChannel(channel_id)
210
+ )
211
+ if not exists:
212
+ missing_channels.append(channel_id)
213
+ return missing_channels
214
+
215
+ def refresh(self):
216
+ """TO apply new components on notification"""
217
+ if self.__built_parameter_filled:
218
+ # Don't dispatch before filling required values `self.__create_basic_notification`
219
+ # We generally shouldn't dispatch till user call .send()
220
+ self.__applyNewLinesIfAny()
221
+ self.__dispatch_notification()
222
+
223
+ def setBigPicture(self, path):
224
+ """
225
+ set a Big Picture at the bottom
226
+ :param path: can be `Relative Path` or `URL`
227
+ :return:
228
+ """
229
+ if ON_ANDROID:
230
+ self.__build_img(path, NotificationStyles.BIG_PICTURE)
231
+ elif self.logs:
232
+ # When on android there are other logs
233
+ print('Done setting big picture')
234
+
235
+ def setSmallIcon(self, path):
236
+ """
237
+ sets small icon to the top left
238
+ :param path: can be `Relative Path` or `URL`
239
+ :return:
240
+ """
241
+ if ON_ANDROID:
242
+ self.app_icon = path
243
+ self.__insert_app_icon(path)
244
+ if self.logs:
245
+ # When on android there are other logs
246
+ print('Done setting small icon')
247
+
248
+ def setLargeIcon(self, path):
249
+ """
250
+ sets Large icon to the right
251
+ :param path: can be `Relative Path` or `URL`
252
+ :return:
253
+ """
254
+ if ON_ANDROID:
255
+ self.__build_img(path, NotificationStyles.LARGE_ICON)
256
+ elif self.logs:
257
+ # When on android there are other logs
258
+ print('Done setting large icon')
259
+
260
+ def setBigText(self, body, title="", summary=""):
261
+ """Sets a big text for when drop down button is pressed
262
+
263
+ :param body: The big text that will be displayed
264
+ :param title: The big text title
265
+ :param summary: The big text summary
266
+ """
267
+ if ON_ANDROID:
268
+ big_text_style = NotificationCompatBigTextStyle()
269
+ if title:
270
+ big_text_style.setBigContentTitle(str(title))
271
+ if summary:
272
+ big_text_style.setSummaryText(str(summary))
273
+
274
+ big_text_style.bigText(str(body))
275
+ self.__builder.setStyle(big_text_style)
276
+ elif self.logs:
277
+ # When on android, there are other logs
278
+ print('Done setting big text')
279
+
280
+ def setSubText(self, text):
281
+ """
282
+ In android version 7+ text displays in header next to title,
283
+ While in lesser versions displays in third line of text, where progress-bar occupies
284
+ :param text: str for subtext
285
+
286
+ """
287
+ self.sub_text = str(text)
288
+ if self.logs:
289
+ print(f'new notification sub text: {self.sub_text}')
290
+ if ON_ANDROID:
291
+ self.__builder.setSubText(self.sub_text)
292
+
293
+ def setColor(self, color: str):
294
+ """
295
+ Sets Notification accent color, visible change in SmallIcon color
296
+ :param color: str - red,pink,... (to be safe use hex code)
297
+ """
298
+ if self.logs:
299
+ print(f'new notification icon color: {color}')
300
+ if ON_ANDROID:
301
+ self.__builder.setColor(Color.parseColor(color))
302
+
303
+ def setWhen(self, secs_ago):
304
+ """
305
+ Sets the notification's timestamp to a specified number of seconds in the past.
306
+
307
+ Parameters
308
+ ----------
309
+ secs_ago : int or float
310
+ The number of seconds ago the notification should appear to have been posted.
311
+ For example, `60` means "1 minute ago", `3600` means "1 hour ago".
312
+
313
+ Notes
314
+ -----
315
+ - Android expects the `when` timestamp in **milliseconds** since the Unix epoch.
316
+ """
317
+
318
+ if ON_ANDROID:
319
+ ms = int((time.time() - secs_ago) * 1000)
320
+ self.__builder.setWhen(ms)
321
+ self.__builder.setShowWhen(True)
322
+ if self.logs:
323
+ print(f"Done setting secs ago {secs_ago}")
324
+
325
+ def showInfiniteProgressBar(self):
326
+ """Displays an (Infinite) progress Bar in Notification, that continues loading indefinitely.
327
+ Can be Removed By `removeProgressBar` Method
328
+ """
329
+ if self.logs:
330
+ print('Showing infinite progressbar')
331
+ if ON_ANDROID:
332
+ self.__builder.setProgress(0, 0, True)
333
+ self.refresh()
334
+
335
+ def updateTitle(self, new_title):
118
336
  """Changes Old Title
119
337
 
120
338
  Args:
121
339
  new_title (str): New Notification Title
122
340
  """
123
- self.title=new_title
341
+ self.title = str(new_title)
342
+ if self.logs:
343
+ print(f'new notification title: {self.title}')
124
344
  if ON_ANDROID:
125
- self.__builder.setContentTitle(new_title)
345
+ if self.isUsingCustom():
346
+ self.__apply_basic_custom_style()
347
+ else:
348
+ self.__builder.setContentTitle(String(self.title))
349
+ self.refresh()
126
350
 
127
- def updateMessage(self,new_message):
351
+ def updateMessage(self, new_message):
128
352
  """Changes Old Message
129
353
 
130
354
  Args:
131
355
  new_message (str): New Notification Message
132
356
  """
133
- self.message=new_message
357
+ self.message = str(new_message)
358
+ if self.logs:
359
+ print(f'new notification message: {self.message}')
360
+ if ON_ANDROID:
361
+ if self.isUsingCustom():
362
+ self.__apply_basic_custom_style()
363
+ else:
364
+ self.__builder.setContentText(String(self.message))
365
+ self.refresh()
366
+
367
+ def updateProgressBar(self, current_value: int, message: str = '', title: str = '', cooldown=0.5,
368
+ _callback: Callable = None):
369
+ """Updates progress bar current value
370
+
371
+ Args:
372
+ current_value (int): the value from progressbar current progress
373
+ message (str): defaults to last message
374
+ title (str): defaults to last title
375
+ cooldown (float, optional): Little Time to Wait before change actually reflects, to avoid android Ignoring Change, Defaults to 0.5secs
376
+ _callback (object): function for when change actual happens
377
+
378
+ NOTE: There is a 0.5 sec delay for value change, if updating title,msg with progressbar frequently pass them in too to avoid update issues
379
+ """
380
+
381
+ # replacing new values for when timer is called
382
+ self.progress_current_value = current_value
383
+ self.__progress_bar_msg = message or self.message
384
+ self.__progress_bar_title = title or self.title
385
+
386
+ if self.__update_timer and self.__update_timer.is_alive():
387
+ # Make Logs too Dirty
388
+ # if self.logs:
389
+ # remaining = self.__cooldown - (time.time() - self.__timer_start_time)
390
+ # print(f'Progressbar update too soon, waiting for cooldown ({max(0, remaining):.2f}s)')
391
+ return
392
+
393
+ def delayed_update():
394
+ if self.__update_timer is None: # Ensure we are not executing an old timer
395
+ if self.logs:
396
+ print('ProgressBar update skipped: bar has been removed.')
397
+ return
398
+ if self.logs:
399
+ print(f'Progress Bar Update value: {self.progress_current_value}')
400
+
401
+ if _callback:
402
+ try:
403
+ _callback()
404
+ except Exception as passed_in_callback_error:
405
+ print('Exception passed_in_callback_error:', passed_in_callback_error)
406
+ traceback.print_exc()
407
+
408
+ if not ON_ANDROID:
409
+ self.__update_timer = None
410
+ return
411
+
412
+ self.__builder.setProgress(self.progress_max_value, self.progress_current_value, False)
413
+
414
+ if self.__progress_bar_msg:
415
+ self.updateMessage(self.__progress_bar_msg)
416
+ if self.__progress_bar_title:
417
+ self.updateTitle(self.__progress_bar_title)
418
+
419
+ self.refresh()
420
+ self.__update_timer = None
421
+
422
+ # Start a new timer that runs after 0.5 seconds
423
+ # self.__timer_start_time = time.time() # for logs
424
+ self.__cooldown = cooldown
425
+ self.__update_timer = threading.Timer(cooldown, delayed_update)
426
+ self.__update_timer.start()
427
+
428
+ def removeProgressBar(self, message='', show_on_update=True, title: str = '', cooldown=0.5,
429
+ _callback: Callable = None):
430
+ """Removes Progress Bar from Notification
431
+
432
+ Args:
433
+ message (str, optional): notification message. Defaults to 'last message'.
434
+ show_on_update (bool, optional): To show notification briefly when progressbar removed. Defaults to True.
435
+ title (str, optional): notification title. Defaults to 'last title'.
436
+ cooldown (float, optional): Little Time to Wait before change actually reflects, to avoid android Ignoring Change, Defaults to 0.5secs
437
+ _callback (object): function for when change actual happens
438
+
439
+ In-Built Delay of 0.5 sec According to Android Docs Don't Update Progressbar too Frequently
440
+ """
441
+
442
+ # To Cancel any queued timer from `updateProgressBar` method and to avoid race effect incase it somehow gets called while in this method
443
+ # Avoiding Running `updateProgressBar.delayed_update` at all
444
+ # so didn't just set `self.__progress_bar_title` and `self.progress_current_value` to 0
445
+ if self.__update_timer:
446
+ # Make Logs too Dirty
447
+ # if self.logs:
448
+ # print('cancelled progressbar stream update because about to remove',self.progress_current_value)
449
+ self.__update_timer.cancel()
450
+ self.__update_timer = None
451
+
452
+ def delayed_update():
453
+ if self.logs:
454
+ msg = message or self.message
455
+ title_ = title or self.title
456
+ print(f'removed progress bar with message: {msg} and title: {title_}')
457
+
458
+ if _callback:
459
+ try:
460
+ _callback()
461
+ except Exception as passed_in_callback_error:
462
+ print('Exception passed_in_callback_error:', passed_in_callback_error)
463
+ traceback.print_exc()
464
+
465
+ if not ON_ANDROID:
466
+ return
467
+
468
+ if message:
469
+ self.updateMessage(message)
470
+ if title:
471
+ self.updateTitle(title)
472
+ self.__builder.setOnlyAlertOnce(not show_on_update)
473
+ self.__builder.setProgress(0, 0, False)
474
+ self.refresh()
475
+
476
+ # Incase `self.updateProgressBar delayed_update` is called right before this method, so android doesn't bounce update
477
+ threading.Timer(cooldown, delayed_update).start()
478
+
479
+ def setPriority(self, importance: Importance):
480
+ """
481
+ For devices less than android 8
482
+ :param importance: ['urgent', 'high', 'medium', 'low', 'none'] defaults to 'urgent' i.e. makes a sound and shows briefly
483
+ :return:
484
+ """
485
+ self.__using_set_priority_method = True
134
486
  if ON_ANDROID:
135
- self.__builder.setContentText(new_message)
136
-
137
- def updateProgressBar(self,current_value,message:str=''):
138
- """message defaults to last message"""
139
- self.__builder.setProgress(self.progress_max_value, current_value, False)
140
- if message:
141
- self.__builder.setContentText(String(message))
142
- self.notification_manager.notify(self.__id, self.__builder.build())
143
-
144
- def removeProgressBar(self,message=''):
145
- """message defaults to last message"""
146
- if message:
147
- self.__builder.setContentText(String(message))
148
- self.__builder.setProgress(0, 0, False)
149
- self.notification_manager.notify(self.__id, self.__builder.build())
150
-
151
- def send(self,silent:bool=False):
487
+ android_importance_value = get_android_importance(importance)
488
+ if not isinstance(android_importance_value, str): # Can be an empty str if importance='none'
489
+ self.__builder.setPriority(android_importance_value)
490
+
491
+ def send(self, silent: bool = False, persistent=False, close_on_click=True):
152
492
  """Sends notification
153
-
493
+
154
494
  Args:
155
495
  silent (bool): True if you don't want to show briefly on screen
496
+ persistent (bool): True To not remove Notification When User hits clears All notifications button
497
+ close_on_click (bool): True if you want Notification to be removed when clicked
156
498
  """
157
- self.silent=self.silent or silent
499
+ self.silent = silent or self.silent
158
500
  if ON_ANDROID:
159
- self.__startNotificationBuild()
160
- self.notification_manager.notify(self.__id, self.__builder.build())
161
- elif self.logs:
162
- print(f"""
163
- Dev Notification Properties:
164
- title: '{self.title}'
165
- message: '{self.message}'
166
- large_icon_path: '{self.large_icon_path}'
167
- big_picture_path: '{self.big_picture_path}'
168
- style: '{self.style}'
169
- Silent: '{self.silent}'
170
- (Won't Print Logs When Complied,except if selected `Notification.logs=True`
171
- """)
172
- if DEV:
173
- print(f'channel_name: {self.channel_name}, Channel ID: {self.channel_id}, id: {self.__id}')
174
- 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
175
-
176
- def __validateArgs(self,inputted_kwargs):
177
-
178
- def checkInReference(inputted_keywords,accepteable_inputs,input_type):
179
- def singularForm(plural_form):
180
- return plural_form[:-1]
181
- invalid_args= set(inputted_keywords) - set(accepteable_inputs)
182
- if invalid_args:
183
- suggestions=[]
184
- for arg in invalid_args:
185
- closest_match = difflib.get_close_matches(arg,accepteable_inputs,n=2,cutoff=0.6)
186
- if closest_match:
187
- suggestions.append(f"* '{arg}' Invalid -> Did you mean '{closest_match[0]}'? ") # pylint: disable=C0301
188
- else:
189
- suggestions.append(f"* {arg} is not a valid {singularForm(input_type)}.")
190
- suggestion_text='\n'.join(suggestions)
191
- hint_msg=singularForm(input_type) if len(invalid_args) < 2 else input_type
192
-
193
- raise ValueError(f"Invalid {hint_msg} provided: \n\t{suggestion_text}\n\t* list of valid {input_type}: [{', '.join(accepteable_inputs)}]")
194
-
195
- allowed_keywords=self.defaults.keys()
196
- inputted_keywords_=inputted_kwargs.keys()
197
- checkInReference(inputted_keywords_,allowed_keywords,'arguments')
198
-
199
- # Validate style values
200
- if 'style' in inputted_keywords_ and inputted_kwargs['style'] not in self.style_values:
201
- checkInReference([inputted_kwargs['style']],self.style_values,'values')
202
-
203
- def __setArgs(self,options_dict:dict):
204
- for key,value in options_dict.items():
205
- if key == 'channel_name':
206
- setattr(self,key, value[:40] if value else self.defaults[key])
207
- elif key == 'channel_id' and value:
208
- setattr(self,key, self.__generate_channel_id(value) if value else self.defaults[key])
209
-
210
- setattr(self,key, value if value else self.defaults[key])
211
-
212
- if "channel_id" not in options_dict and 'channel_name' in options_dict:
213
- setattr(self,'channel_id', self.__generate_channel_id(options_dict['channel_name']))
214
-
215
- def __startNotificationBuild(self):
216
- self.__createBasicNotification()
217
- if self.style not in ['simple','']:
218
- self.__addNotificationStyle()
219
-
220
- def __createBasicNotification(self):
221
- # Notification Channel (Required for Android 8.0+)
222
- if BuildVersion.SDK_INT >= 26 and self.notification_manager.getNotificationChannel(self.channel_id) is None:
223
- importance=NotificationManagerCompat.IMPORTANCE_DEFAULT if self.silent else NotificationManagerCompat.IMPORTANCE_HIGH # pylint: disable=possibly-used-before-assignment
224
- channel = NotificationChannel(
225
- self.channel_id,
226
- self.channel_name,
227
- importance
501
+ self.start_building(persistent, close_on_click)
502
+ self.__dispatch_notification()
503
+
504
+ self.__send_logs()
505
+
506
+ def send_(self, silent: bool = False, persistent=False, close_on_click=True):
507
+ """Sends notification without checking for additional notification permission
508
+
509
+ Args:
510
+ silent (bool): True if you don't want to show briefly on screen
511
+ persistent (bool): True To not remove Notification When User hits clears All notifications button
512
+ close_on_click (bool): True if you want Notification to be removed when clicked
513
+ """
514
+ self.passed_check = True
515
+ self.send(silent, persistent, close_on_click)
516
+
517
+ def __send_logs(self):
518
+ if not self.logs:
519
+ return
520
+ string_to_display = ''
521
+ print("\n Sent Notification!!!")
522
+ displayed_args = [
523
+ "title", "message",
524
+ "style", "body", "large_icon_path", "big_picture_path",
525
+ "progress_max_value",
526
+ 'name', "channel_name",
527
+ ]
528
+ is_progress_not_default = isinstance(self.progress_current_value, int) or (isinstance(self.progress_current_value, float) and self.progress_current_value != 0.0)
529
+ for name,value in vars(self).items():
530
+ if value and name in displayed_args:
531
+ if name == "progress_max_value":
532
+ if is_progress_not_default:
533
+ string_to_display += f'\n progress_current_value: {self.progress_current_value}, {name}: {value}'
534
+ elif name == "channel_name":
535
+ string_to_display += f'\n {name}: {value}, channel_id: {self.channel_id}'
536
+ else:
537
+ string_to_display += f'\n {name}: {value}'
538
+
539
+ string_to_display += "\n (Won't Print Logs When Complied,except if selected `Notification.logs=True`)"
540
+ print(string_to_display)
541
+
542
+ @property
543
+ def builder(self):
544
+ return self.__builder
545
+
546
+ def addButton(self, text: str, on_release=None, receiver_name=None, action=None):
547
+ """For adding action buttons
548
+
549
+ :param text: Text For Button
550
+ :param on_release: function to be called when button is clicked
551
+ :param receiver_name: receiver class name
552
+ :param action: action for receiver
553
+ """
554
+
555
+ if not ON_ANDROID:
556
+ self.__no_of_buttons += 1
557
+ return
558
+
559
+ # Convert text to CharSequence
560
+ action_text = cast('java.lang.CharSequence', String(text))
561
+ action = action or f"{text}_{self.id}" # tagging with id so it can found notification handle object
562
+
563
+ def set_action(action_intent__):
564
+ action_intent__.setAction(action)
565
+ action_intent__.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
566
+ bundle = Bundle()
567
+ bundle.putString("title", self.title or 'Title Placeholder')
568
+ bundle.putInt("key_int", self.__no_of_buttons or 1)
569
+ action_intent__.putExtras(bundle)
570
+ action_intent__.putExtra("button_id", action)
571
+
572
+ def set_default_action_intent():
573
+ action_intent__ = Intent(context, PythonActivity)
574
+ set_action(action_intent__)
575
+ pending_action_intent__ = PendingIntent.getActivity(
576
+ context, self.__no_of_buttons or 1, action_intent__,
577
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
228
578
  )
229
- self.notification_manager.createNotificationChannel(channel)
579
+ return pending_action_intent__
230
580
 
231
- # Build the notification
232
- self.__builder = NotificationCompatBuilder(context, self.channel_id)# pylint: disable=E0606
233
- self.__builder.setContentTitle(self.title)
234
- self.__builder.setContentText(self.message)
235
- self.__builder.setSmallIcon(context.getApplicationInfo().icon)
236
- self.__builder.setDefaults(NotificationCompat.DEFAULT_ALL) # pylint: disable=E0606
237
- if not self.silent:
238
- self.__builder.setPriority(NotificationCompat.PRIORITY_DEFAULT if self.silent else NotificationCompat.PRIORITY_HIGH)
239
-
240
- def __addNotificationStyle(self):
241
- # pylint: disable=trailing-whitespace
242
-
243
- large_icon_javapath=None
244
- if self.large_icon_path:
581
+ if receiver_name:
245
582
  try:
246
- large_icon_javapath = self.__get_image_uri(self.large_icon_path)
247
- except FileNotFoundError as e:
248
- print('Failed Adding Big Picture Bitmap: ',e)
249
-
250
- big_pic_javapath=None
251
- if self.big_picture_path:
252
- try:
253
- big_pic_javapath = self.__get_image_uri(self.big_picture_path)
254
- except FileNotFoundError as e:
255
- print('Failed Adding Lagre Icon Bitmap: ',e)
256
-
257
-
258
- if self.style == "big_text":
259
- big_text_style = NotificationCompatBigTextStyle() # pylint: disable=E0606
260
- big_text_style.bigText(self.message)
261
- self.__builder.setStyle(big_text_style)
262
-
263
- elif self.style == "inbox":
264
- inbox_style = NotificationCompatInboxStyle() # pylint: disable=E0606
265
- for line in self.message.split("\n"):
266
- inbox_style.addLine(line)
583
+ receiverClass = autoclass(f"{get_package_name()}.{receiver_name}")
584
+ action_intent = Intent(context, receiverClass)
585
+ set_action(action_intent)
586
+ pending_action_intent = PendingIntent.getBroadcast(
587
+ context, self.__no_of_buttons or 1, action_intent,
588
+ PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE
589
+ )
590
+
591
+ except Exception as error_getting_broadcast_receiver:
592
+ print("android_notify- error_getting_broadcast_receiver:", error_getting_broadcast_receiver)
593
+ pending_action_intent = set_default_action_intent()
594
+
595
+ else:
596
+ pending_action_intent = set_default_action_intent()
597
+
598
+
599
+ self.__builder.addAction(int(context.getApplicationInfo().icon), action_text, pending_action_intent)
600
+ self.__builder.setContentIntent(pending_action_intent) # Set content intent for notification tap
601
+
602
+ self.btns_box[action] = on_release
603
+ self.__no_of_buttons += 1
604
+ # action_intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP)
605
+
606
+ if self.logs:
607
+ print('Added Button: ', text)
608
+ print('Button action: ', action)
609
+
610
+ def removeButtons(self):
611
+ """Removes all notification buttons
612
+ """
613
+ if ON_ANDROID:
614
+ self.__builder.mActions.clear()
615
+ self.refresh()
616
+ if self.logs:
617
+ print('Removed Notification Buttons')
618
+
619
+ @run_on_ui_thread
620
+ def addNotificationStyle(self, style: str, already_sent=False):
621
+ """Adds Style to Notification
622
+
623
+ Note: This method has Deprecated Use - (setLargeIcon, setBigPicture, setBigText and setLines) Instead
624
+
625
+ Args:
626
+ style (str): required style
627
+ already_sent (bool,False): If notification was already sent
628
+ """
629
+
630
+ if not ON_ANDROID:
631
+ # TODO for logs when not on android and style related to imgs extract app path from buildozer.spec and print
632
+ return False
633
+
634
+ if self.body:
635
+ self.setBigText(self.body)
636
+
637
+ elif self.lines_txt:
638
+ lines = self.lines_txt.split("\n")
639
+ self.setLines(lines)
640
+
641
+ elif self.big_picture_path or self.large_icon_path:
642
+ if self.big_picture_path:
643
+ self.setBigPicture(self.big_picture_path)
644
+ if self.large_icon_path:
645
+ self.setLargeIcon(self.large_icon_path)
646
+
647
+ elif self.progress_max_value or self.progress_current_value:
648
+ self.__builder.setProgress(self.progress_max_value, self.progress_current_value or 0.1, False)
649
+
650
+ if already_sent:
651
+ self.refresh()
652
+
653
+ return True
654
+
655
+ def setLines(self, lines: list):
656
+ """Pass in a list of strings to be used for lines"""
657
+ if not lines:
658
+ return
659
+ if ON_ANDROID:
660
+ inbox_style = NotificationCompatInboxStyle()
661
+ for line in lines:
662
+ inbox_style.addLine(str(line))
267
663
  self.__builder.setStyle(inbox_style)
268
-
269
- elif self.style == "big_picture" and big_pic_javapath:
270
- big_pic_bitmap = self.__getBitmap(big_pic_javapath)
271
- big_picture_style = NotificationCompatBigPictureStyle().bigPicture(big_pic_bitmap) # pylint: disable=E0606
272
- self.__builder.setStyle(big_picture_style)
273
-
274
- elif self.style == "large_icon" and large_icon_javapath:
275
- large_icon_bitmap = self.__getBitmap(large_icon_javapath)
276
- self.__builder.setLargeIcon(large_icon_bitmap)
277
-
278
- elif self.style == 'both_imgs' and (large_icon_javapath or big_pic_javapath):
279
- if big_pic_javapath:
280
- big_pic_bitmap = self.__getBitmap(big_pic_javapath)
281
- big_picture_style = NotificationCompatBigPictureStyle().bigPicture(big_pic_bitmap)
664
+ print('Set Lines: ', lines)
665
+
666
+ if self.logs:
667
+ print('Added Lines: ', lines)
668
+
669
+ def setSound(self, res_sound_name):
670
+ """
671
+ Sets sound for devices less than android 8 (For 8+ use createChannel)
672
+ :param res_sound_name: audio file file name (without .wav or .mp3) locate in res/raw/
673
+ """
674
+
675
+ if not ON_ANDROID:
676
+ return
677
+
678
+ if res_sound_name and BuildVersion.SDK_INT < 26:
679
+ try:
680
+ self.__builder.setSound(get_sound_uri(res_sound_name))
681
+ except Exception as failed_adding_sound_device_below_android8:
682
+ print("failed_adding_sound_device_below_android8:", failed_adding_sound_device_below_android8)
683
+ traceback.print_exc()
684
+
685
+ def __dispatch_notification(self):
686
+ # self.passed_check is for self.send_() some devices don't return true when checking for permission when it's actually True in settingsAdd commentMore actions
687
+ # And so users can do Notification.passed_check = True at top of their file and use regular .send()
688
+
689
+ if from_service_file(): # android has_permission has some internal error when checking from service
690
+ try:
691
+ self.notification_manager.notify(self.__id, self.__builder.build())
692
+ except Exception as sending_notification_from_service_error:
693
+ print('error sending notification from service:', sending_notification_from_service_error)
694
+ elif on_flet_app() or self.passed_check or NotificationHandler.has_permission():
695
+ try:
696
+ self.notification_manager.notify(self.__id, self.__builder.build())
697
+ except Exception as notify_error:
698
+ print('Exception:', notify_error)
699
+ print('Failed to send traceback:', traceback.format_exc())
700
+ else:
701
+ print('Permission not granted to send notifications')
702
+ # TODO find way to open app notification settings and not ask only through POP-UP
703
+ # Not asking for permission too frequently, This makes dialog popup to stop showing
704
+ # NotificationHandler.asks_permission()
705
+
706
+ def start_building(self, persistent=False, close_on_click=True, silent: bool = False):
707
+ # Main use is for foreground service, bypassing .notify in .send method to let service.startForeground(...) send it
708
+ self.silent = silent or self.silent
709
+ if not ON_ANDROID:
710
+ return NotificationCompatBuilder # this is just a facade
711
+ self.__create_basic_notification(persistent, close_on_click)
712
+ if self.style not in ['simple', '']:
713
+ self.addNotificationStyle(self.style)
714
+ self.__applyNewLinesIfAny()
715
+
716
+ return self.__builder
717
+
718
+ def __applyNewLinesIfAny(self):
719
+ if self.__lines:
720
+ self.setLines(self.__lines)
721
+ self.__lines = [] # for refresh method to known when new lines added
722
+
723
+ def __create_basic_notification(self, persistent, close_on_click):
724
+ if not self.channelExists(self.channel_id):
725
+ self.createChannel(self.channel_id, self.channel_name)
726
+ elif not self.__using_set_priority_method:
727
+ self.setPriority('medium' if self.silent else 'urgent')
728
+
729
+ # Build the notification
730
+ if self.isUsingCustom():
731
+ self.__apply_basic_custom_style()
732
+ else:
733
+ self.__builder.setContentTitle(str(self.title))
734
+ self.__builder.setContentText(str(self.message))
735
+ self.__insert_app_icon()
736
+ self.__builder.setDefaults(AndroidNotification.DEFAULT_ALL)
737
+ self.__builder.setOnlyAlertOnce(True)
738
+ self.__builder.setOngoing(persistent)
739
+ self.__builder.setAutoCancel(close_on_click)
740
+
741
+ try:
742
+ self.__add_intent_to_open_app()
743
+ except Exception as failed_to_add_intent_to_open_app:
744
+ print('failed_to_add_intent_to_open_app Error: ', failed_to_add_intent_to_open_app)
745
+ traceback.print_exc()
746
+
747
+ self.__built_parameter_filled = True
748
+
749
+ def __insert_app_icon(self, path=''):
750
+ if BuildVersion.SDK_INT >= 23 and (path or self.app_icon not in ['', 'Defaults to package app icon']):
751
+ # Bitmap Insert as Icon Not available below Android 6
752
+ if self.logs:
753
+ print('getting custom icon...')
754
+ self.__set_icon_from_bitmap(path or self.app_icon)
755
+ else:
756
+ def set_default_icon():
757
+ if self.logs:
758
+ print('using default icon...')
759
+ self.__builder.setSmallIcon(context.getApplicationInfo().icon)
760
+
761
+ fallback_icon_path = None
762
+ if on_flet_app():
763
+ fallback_icon_path = icon_finder("flet-appicon.png")
764
+ elif "ru.iiec.pydroid3" in os.path.dirname(os.path.abspath(__file__)):
765
+ fallback_icon_path = icon_finder("pydroid3-appicon.png")
766
+ else:
767
+ set_default_icon()
768
+
769
+ if fallback_icon_path:
770
+ success = self.__set_smallicon_with_bitmap_from_path(fallback_icon_path)
771
+ if not success:
772
+ print("error_using_fallback_appicon")
773
+ set_default_icon()
774
+
775
+ self.__has_small_icon = True
776
+
777
+ def __set_smallicon_with_bitmap_from_path(self, fullpath):
778
+ try:
779
+ bitmap = get_bitmap_from_path(fullpath)
780
+ if bitmap:
781
+ self.__set_builder_icon_with_bitmap(bitmap)
782
+ return True
783
+ except Exception as error_using_bitmap_for_appicon:
784
+ print("error_using_bitmap_for_appicon :", error_using_bitmap_for_appicon)
785
+ traceback.print_exc()
786
+ return False
787
+
788
+ def __build_img(self, user_img, img_style):
789
+ if user_img.startswith('http://') or user_img.startswith('https://'):
790
+ def callback(bitmap_):
791
+ self.__apply_notification_image(bitmap_, img_style)
792
+
793
+ thread = threading.Thread(
794
+ target=get_bitmap_from_url,
795
+ args=[user_img, callback, self.logs]
796
+ )
797
+ thread.start()
798
+ else:
799
+ bitmap = get_img_from_path(user_img)
800
+ if bitmap:
801
+ self.__apply_notification_image(bitmap, img_style)
802
+
803
+ def __set_icon_from_bitmap(self, img_path):
804
+ """Path can be a link or relative path"""
805
+
806
+ if img_path.startswith('http://') or img_path.startswith('https://'):
807
+ def callback(bitmap_):
808
+ if bitmap_:
809
+ self.__set_builder_icon_with_bitmap(bitmap_)
810
+ else:
811
+ if self.logs:
812
+ print('Using Default Icon as fallback......')
813
+ self.__builder.setSmallIcon(context.getApplicationInfo().icon)
814
+ self.__has_small_icon = True
815
+
816
+ threading.Thread(
817
+ target=get_bitmap_from_url,
818
+ args=[img_path, callback, self.logs]
819
+ ).start()
820
+ else:
821
+ bitmap = get_img_from_path(
822
+ img_path) # get_img_from_path is different from get_bitmap_from_path because it those some logging for user
823
+ if bitmap:
824
+ self.__set_builder_icon_with_bitmap(bitmap)
825
+ else:
826
+ if self.logs:
827
+ app_folder = os.path.join(app_storage_path(), 'app')
828
+ img_absolute_path = os.path.join(app_folder, img_path)
829
+ print(
830
+ f'Failed getting bitmap for custom notification icon defaulting to app icon\n absolute path {img_absolute_path}')
831
+ self.__builder.setSmallIcon(context.getApplicationInfo().icon)
832
+ self.__has_small_icon = True
833
+
834
+ def __set_builder_icon_with_bitmap(self, bitmap):
835
+ try:
836
+ Icon = autoclass('android.graphics.drawable.Icon')
837
+ except Exception as autoclass_icon_error:
838
+ print("Couldn't find class to set custom icon:", autoclass_icon_error)
839
+ self.__builder.setSmallIcon(context.getApplicationInfo().icon)
840
+ self.__has_small_icon = True
841
+ return
842
+
843
+ Icon = autoclass('android.graphics.drawable.Icon')
844
+ icon = Icon.createWithBitmap(bitmap)
845
+ self.__builder.setSmallIcon(icon)
846
+
847
+ @run_on_ui_thread
848
+ def __apply_notification_image(self, bitmap, img_style):
849
+ try:
850
+ if img_style == NotificationStyles.BIG_PICTURE and bitmap:
851
+ big_picture_style = NotificationCompatBigPictureStyle().bigPicture(bitmap)
282
852
  self.__builder.setStyle(big_picture_style)
283
- elif large_icon_javapath:
284
- large_icon_bitmap = self.__getBitmap(large_icon_javapath)
285
- self.__builder.setLargeIcon(large_icon_bitmap)
286
- elif self.style == 'progress':
287
- self.__builder.setContentTitle(String(self.title))
288
- self.__builder.setContentText(String(self.message))
289
- self.__builder.setProgress(self.progress_max_value, self.progress_current_value, False)
290
- # elif self.style == 'custom':
291
- # self.__builder = self.__doCustomStyle()
292
-
293
- # def __doCustomStyle(self):
294
- # # TODO Will implement when needed
295
- # return self.__builder
296
-
297
- def __getUniqueID(self):
298
- reasonable_amount_of_notifications=101
299
- notification_id = random.randint(1, reasonable_amount_of_notifications)
300
- while notification_id in self.notification_ids:
301
- notification_id = random.randint(1, 100)
853
+ elif img_style == NotificationStyles.LARGE_ICON and bitmap:
854
+ self.__builder.setLargeIcon(bitmap)
855
+ # LargeIcon requires smallIcon to be already set
856
+ # 'setLarge, setBigPic' tries to dispatch before filling required values `self.__create_basic_notification`
857
+ self.refresh()
858
+ if self.logs:
859
+ print('Done adding image to notification-------')
860
+ except Exception as notification_image_error:
861
+ img = self.large_icon_path if img_style == NotificationStyles.LARGE_ICON else self.big_picture_path
862
+ print(
863
+ f'Failed adding Image of style: {img_style} || From path: {img}, Exception {notification_image_error}')
864
+ print('could not get Img traceback: ', traceback.format_exc())
865
+
866
+ def __add_intent_to_open_app(self):
867
+ intent = Intent(context, PythonActivity)
868
+ intent.setFlags(
869
+ Intent.FLAG_ACTIVITY_CLEAR_TOP | # Makes Sure tapping notification always brings the existing instance of app forward.
870
+ Intent.FLAG_ACTIVITY_SINGLE_TOP | # If the activity is already at the top, reuse it instead of creating a new instance.
871
+ Intent.FLAG_ACTIVITY_NEW_TASK
872
+ # Required when starting an Activity from a Service; ignored when starting from another Activity.
873
+ )
874
+ action = str(self.name or self.__id)
875
+ intent.setAction(action)
876
+ add_data_to_intent(intent, self.title)
877
+ self.main_functions[action] = self.callback
878
+
879
+ #intent.setAction(Intent.ACTION_MAIN) # Marks this intent as the main entry point of the app, like launching from the home screen.
880
+ #intent.addCategory(Intent.CATEGORY_LAUNCHER) # Adds the launcher category so Android treats it as a launcher app intent and properly manages the task/back stack.
881
+
882
+ pending_intent = PendingIntent.getActivity(
883
+ context, 0,
884
+ intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
885
+ )
886
+ self.__builder.setContentIntent(pending_intent)
887
+
888
+ def __format_channel(self, channel_name: str = 'Default Channel', channel_id: str = 'default_channel'):
889
+ """
890
+ Formats and sets self.channel_name and self.channel_id to a formatted version
891
+ :param channel_name:
892
+ :param channel_id:
893
+ :return:
894
+ """
895
+ # Shorten channel name # android docs as at most 40 chars
896
+ if channel_name != 'Default Channel':
897
+ cleaned_name = channel_name.strip()
898
+ self.channel_name = cleaned_name[:40] if cleaned_name else 'Default Channel'
899
+
900
+ # If no channel_id then generating channel_id from passed in channel_name
901
+ if channel_id == 'default_channel':
902
+ generated_id = generate_channel_id(channel_name)
903
+ self.channel_id = generated_id
904
+
905
+ def __get_unique_id(self):
906
+ if from_service_file():
907
+ max_int = 2_147_483_647
908
+ return int(time.time() * 1000) % max_int
909
+
910
+ notification_id = self.notification_ids[-1] + 1
302
911
  self.notification_ids.append(notification_id)
303
912
  return notification_id
304
913
 
305
- def __asks_permission_if_needed(self):
306
- """
307
- Ask for permission to send notifications if needed.
914
+ @classmethod
915
+ def getChannels(cls) -> list[Any] | Any:
916
+ """Return all existing channels"""
917
+ if not ON_ANDROID:
918
+ return []
919
+
920
+ return get_notification_manager().getNotificationChannels()
921
+
922
+ def __apply_basic_custom_style(self):
923
+ NotificationCompatDecoratedCustomViewStyle = autoclass(
924
+ 'androidx.core.app.NotificationCompat$DecoratedCustomViewStyle')
925
+
926
+ # Load layout
927
+ resources = context.getResources()
928
+ package_name = context.getPackageName()
929
+
930
+ # ids
931
+ small_layout_id = resources.getIdentifier("an_colored_basic_small", "layout", package_name)
932
+ large_layout_id = resources.getIdentifier("an_colored_basic_large", "layout", package_name)
933
+ title_id = resources.getIdentifier("title", "id", package_name)
934
+ message_id = resources.getIdentifier("message", "id", package_name)
935
+
936
+ # Layout
937
+ notificationLayout = RemoteViews(package_name, small_layout_id)
938
+ notificationLayoutExpanded = RemoteViews(package_name, large_layout_id)
939
+
940
+ if DEV:
941
+ print('small: ', small_layout_id, 'notificationLayout: ', notificationLayout)
942
+
943
+ # Notification Content
944
+ setLayoutText(
945
+ layout=notificationLayout, id=title_id,
946
+ text=self.title, color=self.title_color
947
+ )
948
+ setLayoutText(
949
+ layout=notificationLayoutExpanded, id=title_id,
950
+ text=self.title, color=self.title_color
951
+ )
952
+ setLayoutText(
953
+ layout=notificationLayoutExpanded, id=message_id,
954
+ text=self.message, color=self.message_color
955
+ )
956
+ # self.__setLayoutText(
957
+ # layout=notificationLayout, id=message_id,
958
+ # text=self.message, color=self.message_color
959
+ # )
960
+ if not self.__built_parameter_filled:
961
+ current_time_mills = int(time.time() * 1000)
962
+ self.__builder.setWhen(current_time_mills)
963
+ self.__builder.setShowWhen(True)
964
+ self.__builder.setStyle(NotificationCompatDecoratedCustomViewStyle())
965
+ self.__builder.setCustomContentView(notificationLayout)
966
+ self.__builder.setCustomBigContentView(notificationLayoutExpanded)
967
+
968
+ def isUsingCustom(self):
969
+ self.__using_custom = self.title_color or self.message_color
970
+ return bool(self.__using_custom)
971
+ # TODO method to create channel groups
972
+
973
+
974
+ class NotificationHandler:
975
+ """For Notification Operations """
976
+ __name = None
977
+ __bound = False
978
+ __requesting_permission = False
979
+ android_activity = None
980
+ if ON_ANDROID and not on_flet_app():
981
+ from android import activity
982
+ android_activity = activity
983
+
984
+ @classmethod
985
+ def get_name(cls):
986
+ """Returns name or id str for Clicked Notification."""
987
+ if not cls.is_on_android():
988
+ return "Not on Android"
989
+
990
+ saved_intent = cls.__name
991
+ cls.__name = None # so value won't be set when opening app not from notification
992
+ # print('saved_intent ',saved_intent)
993
+ # if not saved_intent or (isinstance(saved_intent, str) and saved_intent.startswith("android.intent")):
994
+ # All other notifications are not None after First notification opens app
995
+ # NOTE these notifications are also from Last time app was opened and they Still Give Value after first one opens App
996
+ # TODO Find a way to get intent when App if Swiped From recents
997
+ # Below action is always None
998
+ # __PythonActivity = autoclass(ACTIVITY_CLASS_NAME)
999
+ # __mactivity = __PythonActivity.mActivity
1000
+ # __context = cast('android.content.Context', __mactivity)
1001
+ # __Intent = autoclass('android.content.Intent')
1002
+ # __intent = __Intent(__context, __PythonActivity)
1003
+ # action = __intent.getAction()
1004
+ # print('Start up Intent ----', action)
1005
+ # print('start Up Title --->',__intent.getStringExtra("title"))
1006
+
1007
+ return saved_intent
1008
+
1009
+ @classmethod
1010
+ def __notification_handler(cls, intent):
1011
+ """Calls Function Attached to notification on click.
1012
+ Don't Call this function manual, it's Already Attach to Notification.
1013
+
1014
+ Sets self.__name #action of Notification that was clicked from Notification.name or Notification.id
308
1015
  """
309
- def on_permissions_result(permissions, grant): # pylint: disable=unused-argument
310
- if self.logs:
311
- print("Permission Grant State: ",grant)
1016
+ if not cls.is_on_android():
1017
+ return "Not on Android"
1018
+ #print('intent.getStringExtra("title")',intent.getStringExtra("title"))
1019
+ buttons_object = Notification.btns_box
1020
+ notifty_functions = Notification.main_functions
1021
+ if DEV:
1022
+ print("notify_functions ", notifty_functions)
1023
+ print("buttons_object", buttons_object)
1024
+ try:
1025
+ action = intent.getAction()
1026
+ cls.__name = action
1027
+
1028
+ # print("The Action --> ",action)
1029
+ if action == "android.intent.action.MAIN": # Not Open From Notification
1030
+ cls.__name = None
1031
+ return 'Not notification'
312
1032
 
313
- permissions=[Permission.POST_NOTIFICATIONS] # pylint: disable=E0606
314
- if not all(check_permission(p) for p in permissions):
315
- request_permissions(permissions,on_permissions_result) # pylint: disable=E0606
1033
+ # print(intent.getStringExtra("title"))
1034
+ try:
1035
+ if action in notifty_functions and notifty_functions[action]:
1036
+ notifty_functions[action]()
1037
+ elif action in buttons_object:
1038
+ if buttons_object[action]:
1039
+ buttons_object[action]()
1040
+ else:
1041
+ print("android_notify- Notification button function not found got:", buttons_object[action])
1042
+ except Exception as notification_handler_function_error:
1043
+ print("Error Type ", notification_handler_function_error)
1044
+ print('Failed to run function: ', traceback.format_exc())
1045
+ except Exception as extracting_notification_props_error:
1046
+ print('Notify Handler Failed ', extracting_notification_props_error)
316
1047
 
317
- def __get_image_uri(self,relative_path):
1048
+ @classmethod
1049
+ def bindNotifyListener(cls):
1050
+ """This Creates a Listener for All Notification Clicks and Functions"""
1051
+ if on_flet_app():
1052
+ return False
1053
+
1054
+ if not cls.is_on_android():
1055
+ return "Not on Android"
1056
+ # TODO keep trying BroadcastReceiver
1057
+ if cls.__bound:
1058
+ print("binding done already ")
1059
+ return True
1060
+ try:
1061
+ cls.android_activity.bind(on_new_intent=cls.__notification_handler)
1062
+ cls.__bound = True
1063
+ return True
1064
+ except Exception as binding_listener_error:
1065
+ print('Failed to bin notifications listener', binding_listener_error)
1066
+ return False
1067
+
1068
+ @classmethod
1069
+ def unbindNotifyListener(cls):
1070
+ """Removes Listener for Notifications Click"""
1071
+ if not cls.is_on_android():
1072
+ return "Not on Android"
1073
+
1074
+ # Beta TODO use BroadcastReceiver
1075
+ if on_flet_app() or from_service_file():
1076
+ return False # error 'NoneType' object has no attribute 'registerNewIntentListener'
1077
+ try:
1078
+ cls.android_activity.unbind(on_new_intent=cls.__notification_handler)
1079
+ return True
1080
+ except Exception as unbinding_listener_error:
1081
+ print("Failed to unbind notifications listener: ", unbinding_listener_error)
1082
+ return False
1083
+
1084
+ @staticmethod
1085
+ def is_on_android():
1086
+ """Utility to check if the app is running on Android."""
1087
+ return ON_ANDROID
1088
+
1089
+ @staticmethod
1090
+ def has_permission():
318
1091
  """
319
- Get the absolute URI for an image in the assets folder.
320
- :param relative_path: The relative path to the image (e.g., 'assets/imgs/icon.png').
321
- :return: Absolute URI java Object (e.g., 'file:///path/to/file.png').
1092
+ Checks if device has permission to send notifications
1093
+ returns True if device has permission
322
1094
  """
1095
+ if not ON_ANDROID:
1096
+ return True
323
1097
 
324
- output_path = os.path.join(app_storage_path(),'app', relative_path) # pylint: disable=possibly-used-before-assignment
325
- # print(output_path) # /data/user/0/(package.domain+package.name)/files/app/assets/imgs/icon.png | pylint: disable=:line-too-long
1098
+ if BuildVersion.SDK_INT < 33: # Android 12 below
1099
+ print("android_notify- On android 12 or less don't need permission")
1100
+ return True
326
1101
 
327
- if not os.path.exists(output_path):
328
- # TODO Use images From Any where even Web
329
- raise FileNotFoundError(f"Image not found at path: {output_path}, (Can Only Use Images in App Path)")
330
- Uri = autoclass('android.net.Uri')
331
- return Uri.parse(f"file://{output_path}")
332
- def __getBitmap(self,img_path):
333
- return BitmapFactory.decodeStream(context.getContentResolver().openInputStream(img_path))
1102
+ if on_flet_app():
1103
+ Manifest = autoclass('android.Manifest$permission')
1104
+ VERSION_CODES = autoclass('android.os.Build$VERSION_CODES')
1105
+ PackageManager = autoclass("android.content.pm.PackageManager")
1106
+ permission = Manifest.POST_NOTIFICATIONS
1107
+ return PackageManager.PERMISSION_GRANTED == context.checkSelfPermission(permission)
1108
+ else:
1109
+ from android.permissions import Permission, check_permission # type: ignore
1110
+ return check_permission(Permission.POST_NOTIFICATIONS)
334
1111
 
335
- def __generate_channel_id(self,channel_name: str) -> str:
1112
+ @classmethod
1113
+ @run_on_ui_thread
1114
+ def asks_permission(cls, callback=None):
336
1115
  """
337
- Generate a readable and consistent channel ID from a channel name.
338
-
339
- Args:
340
- channel_name (str): The name of the notification channel.
341
-
342
- Returns:
343
- str: A sanitized channel ID.
344
- """
345
- # Normalize the channel name
346
- channel_id = channel_name.strip().lower()
347
- # Replace spaces and special characters with underscores
348
- channel_id = re.sub(r'[^a-z0-9]+', '_', channel_id)
349
- # Remove leading/trailing underscores
350
- channel_id = channel_id.strip('_')
351
- return channel_id[:50]
352
-
353
- # try:
354
- # notify=Notification(titl='My Title',channel_name='Go')#,logs=False)
355
- # # notify.channel_name='Downloads'
356
- # notify.message="Blah"
357
- # notify.send()
358
- # notify.updateTitle('New Title')
359
- # notify.updateMessage('New Message')
360
- # notify.send(True)
361
- # except Exception as e:
362
- # print(e)
363
-
364
- # notify=Notification(title='My Title1')
365
- # # notify.updateTitle('New Title1')
366
- # notify.send()
367
-
368
-
369
- # Notification.logs=False # Add in Readme
370
- # notify=Notification(style='large_icon',title='My Title',channel_name='Some thing about a thing ')#,logs=False)
371
- # # notify.channel_name='Downloads'
372
- # notify.message="Blah"
373
- # notify.send()
374
- # notify.updateTitle('New Title')
375
- # notify.updateMessage('New Message')
1116
+ Ask for permission to send notifications if needed.
1117
+ Passes True to callback if access granted
1118
+ """
1119
+ if not ON_ANDROID:
1120
+ print("android_notify- Can't ask permission when not on android")
1121
+ return None
1122
+
1123
+ if cls.__requesting_permission:
1124
+ print("android_notify- still requesting permission ")
1125
+ return True
1126
+
1127
+ if BuildVersion.SDK_INT < 33: # Android 12 below
1128
+ print("android_notify- On android 12 or less don't need permission")
1129
+
1130
+ if not ON_ANDROID or BuildVersion.SDK_INT < 33: # Android 12 below:
1131
+ try:
1132
+ if callback:
1133
+ if can_accept_arguments(callback, True):
1134
+ callback(True)
1135
+ else:
1136
+ callback()
1137
+ except Exception as request_permission_error:
1138
+ print('Exception: ', request_permission_error)
1139
+ print('Permission response callback error: ', traceback.format_exc())
1140
+
1141
+ return
1142
+
1143
+ if not can_show_permission_request_popup():
1144
+ print("""android_notify- Permission to send notifications has been denied permanently.
1145
+ This happens when the user denies permission twice from the popup.
1146
+ Opening notification settings...""")
1147
+ open_settings_screen()
1148
+ return None
1149
+
1150
+ def on_permissions_result(permissions, grants):
1151
+ try:
1152
+ if callback:
1153
+ if can_accept_arguments(callback, True):
1154
+ callback(grants[0])
1155
+ else:
1156
+ callback()
1157
+ except Exception as request_permission_error:
1158
+ print('Exception: ', request_permission_error)
1159
+ print('Permission response callback error: ', traceback.format_exc())
1160
+ finally:
1161
+ cls.__requesting_permission = False
1162
+
1163
+ if not cls.has_permission():
1164
+ if on_flet_app():
1165
+ Manifest = autoclass('android.Manifest$permission')
1166
+ permission = Manifest.POST_NOTIFICATIONS
1167
+ context.requestPermissions([permission], 101)
1168
+ # TODO Callback when user answers request question
1169
+ else:
1170
+ from android.permissions import request_permissions, Permission # type: ignore
1171
+ cls.__requesting_permission = True
1172
+ request_permissions([Permission.POST_NOTIFICATIONS], on_permissions_result)
1173
+ return None
1174
+ else:
1175
+ cls.__requesting_permission = False
1176
+ if callback:
1177
+ if can_accept_arguments(callback, True):
1178
+ callback(True)
1179
+ else:
1180
+ callback()
1181
+ return None
1182
+
1183
+
1184
+ if not on_flet_app() and from_service_file():
1185
+ print("didn't bind listener, In service file")
1186
+ elif ON_ANDROID:
1187
+ try:
1188
+ NotificationHandler.bindNotifyListener()
1189
+ except Exception as bind_error:
1190
+ # error 'NoneType' object has no attribute 'registerNewIntentListener'
1191
+ print("notification listener bind error:", bind_error)
1192
+ traceback.print_exc()