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/__init__.py +2 -1
- android_notify/__main__.py +24 -0
- android_notify/an_types.py +341 -0
- android_notify/an_utils.py +209 -0
- android_notify/base.py +97 -0
- android_notify/config.py +167 -0
- android_notify/core.py +171 -76
- android_notify/fallback-icons/flet-appicon.png +0 -0
- android_notify/fallback-icons/pydroid3-appicon.png +0 -0
- android_notify/styles.py +17 -7
- android_notify/sword.py +1142 -325
- android_notify-1.60.6.dev0.dist-info/METADATA +171 -0
- android_notify-1.60.6.dev0.dist-info/RECORD +24 -0
- {android_notify-1.3.dist-info → android_notify-1.60.6.dev0.dist-info}/WHEEL +1 -1
- android_notify-1.60.6.dev0.dist-info/entry_points.txt +2 -0
- {android_notify-1.3.dist-info → android_notify-1.60.6.dev0.dist-info}/top_level.txt +1 -0
- docs/examples/flet-working/src/core.py +221 -0
- docs/examples/flet-working/src/main.py +68 -0
- docs/tests/flet/adv/main.py +97 -0
- docs/tests/flet/adv/tests/__init__.py +0 -0
- docs/tests/flet/adv/tests/test_android_notify_full.py +199 -0
- docs/tests/flet/basic/src/core.py +221 -0
- docs/tests/flet/basic/src/main.py +112 -0
- docs/website/src/pages/data/laner_Sent.py +24 -0
- android_notify-1.3.dist-info/METADATA +0 -350
- android_notify-1.3.dist-info/RECORD +0 -9
android_notify/sword.py
CHANGED
|
@@ -1,375 +1,1192 @@
|
|
|
1
1
|
"""This Module Contain Class for creating Notification With Java"""
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
from
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
57
|
-
(
|
|
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
|
|
64
|
-
:param
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
self.
|
|
96
|
-
self.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
self.
|
|
100
|
-
self.
|
|
101
|
-
|
|
102
|
-
self.
|
|
103
|
-
|
|
104
|
-
self.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
#
|
|
108
|
-
self.
|
|
109
|
-
self.
|
|
110
|
-
self.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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=
|
|
499
|
+
self.silent = silent or self.silent
|
|
158
500
|
if ON_ANDROID:
|
|
159
|
-
self.
|
|
160
|
-
self.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
579
|
+
return pending_action_intent__
|
|
230
580
|
|
|
231
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
except
|
|
255
|
-
print(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
self.
|
|
288
|
-
self.
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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
|
-
|
|
320
|
-
|
|
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
|
-
|
|
325
|
-
|
|
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
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
1112
|
+
@classmethod
|
|
1113
|
+
@run_on_ui_thread
|
|
1114
|
+
def asks_permission(cls, callback=None):
|
|
336
1115
|
"""
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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()
|