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/__init__.py
CHANGED
android_notify/__main__.py
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from .config import __version__
|
|
3
|
+
|
|
4
|
+
def print_version():
|
|
5
|
+
text = f"android_notify: v{__version__}"
|
|
6
|
+
border = '+'+'-'*(len(text) + 2)+'+'
|
|
7
|
+
print(border)
|
|
8
|
+
print(f'| {text} |')
|
|
9
|
+
print(border)
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
parser = argparse.ArgumentParser(description="Android Notify CLI")
|
|
13
|
+
parser.add_argument('-v','--version', action='store_true', help="Show the version of android_notify")
|
|
14
|
+
args = parser.parse_args()
|
|
15
|
+
|
|
16
|
+
if args.version:
|
|
17
|
+
print_version()
|
|
18
|
+
# # Placeholder for the main functionality
|
|
19
|
+
# print("Android Notify CLI is running...")
|
|
20
|
+
# DEV: pip install -e ., when edit and test project locally
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
main()
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""For autocomplete Storing Reference to Available Methods"""
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
Importance = Literal['urgent', 'high', 'medium', 'low', 'none']
|
|
5
|
+
"""
|
|
6
|
+
:argument urgent - Makes a sound and appears as a heads-up notification.
|
|
7
|
+
|
|
8
|
+
:argument high - Makes a sound.
|
|
9
|
+
|
|
10
|
+
:argument urgent - Makes no sound.
|
|
11
|
+
|
|
12
|
+
:argument urgent - Makes no sound and doesn't appear in the status bar.
|
|
13
|
+
|
|
14
|
+
:argument urgent - Makes no sound and doesn't in the status bar or shade.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# For Dev
|
|
19
|
+
# Idea for typing autocompletion and reference
|
|
20
|
+
class Bundle:
|
|
21
|
+
def putString(self, key, value):
|
|
22
|
+
print(f"[MOCK] Bundle.putString called with key={key}, value={value}")
|
|
23
|
+
|
|
24
|
+
def putInt(self, key, value):
|
|
25
|
+
print(f"[MOCK] Bundle.putInt called with key={key}, value={value}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class String(str):
|
|
29
|
+
def __new__(cls, value):
|
|
30
|
+
print(f"[MOCK] String created with value={value}")
|
|
31
|
+
return str.__new__(cls, value)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Intent:
|
|
35
|
+
FLAG_ACTIVITY_NEW_TASK = 'FACADE_FLAG_ACTIVITY_NEW_TASK'
|
|
36
|
+
CATEGORY_DEFAULT = 'FACADE_FLAG_CATEGORY_DEFAULT'
|
|
37
|
+
|
|
38
|
+
def __init__(self, context='', activity=''):
|
|
39
|
+
self.obj = {}
|
|
40
|
+
print(f"[MOCK] Intent initialized with context={context}, activity={activity}")
|
|
41
|
+
|
|
42
|
+
def setAction(self, action):
|
|
43
|
+
print(f"[MOCK] Intent.setAction called with: {action}")
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def addFlags(self, *flags):
|
|
47
|
+
print(f"[MOCK] Intent.addFlags called with: {flags}")
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
def setData(self, uri):
|
|
51
|
+
print(f"[MOCK] Intent.setData called with: {uri}")
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def setFlags(self, intent_flag):
|
|
55
|
+
print(f"[MOCK] Intent.setFlags called with: {intent_flag}")
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def addCategory(self, intent_category):
|
|
59
|
+
print(f"[MOCK] Intent.addCategory called with: {intent_category}")
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def getAction(self):
|
|
63
|
+
print("[MOCK] Intent.getAction called")
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def getStringExtra(self, key):
|
|
67
|
+
print(f"[MOCK] Intent.getStringExtra called with key={key}")
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def putExtra(self, key, value):
|
|
71
|
+
self.obj[key] = value
|
|
72
|
+
print(f"[MOCK] Intent.putExtra called with key={key}, value={value}")
|
|
73
|
+
|
|
74
|
+
def putExtras(self, bundle: Bundle):
|
|
75
|
+
self.obj['bundle'] = bundle
|
|
76
|
+
print(f"[MOCK] Intent.putExtras called with bundle={bundle}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class PendingIntent:
|
|
80
|
+
FLAG_IMMUTABLE = ''
|
|
81
|
+
FLAG_UPDATE_CURRENT = ''
|
|
82
|
+
|
|
83
|
+
def getActivity(self, context, value, action_intent, pending_intent_type):
|
|
84
|
+
print(
|
|
85
|
+
f"[MOCK] PendingIntent.getActivity called with context={context}, value={value}, action_intent={action_intent}, type={pending_intent_type}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class BitmapFactory:
|
|
89
|
+
def decodeStream(self, stream):
|
|
90
|
+
print(f"[MOCK] BitmapFactory.decodeStream called with stream={stream}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class BuildVersion:
|
|
94
|
+
SDK_INT = 0
|
|
95
|
+
|
|
96
|
+
class Manifest:
|
|
97
|
+
POST_NOTIFICATIONS = 'FACADE_IMPORT'
|
|
98
|
+
|
|
99
|
+
class Settings:
|
|
100
|
+
ACTION_APP_NOTIFICATION_SETTINGS = 'FACADE_IMPORT_ACTION_APP_NOTIFICATION_SETTINGS'
|
|
101
|
+
EXTRA_APP_PACKAGE = 'FACADE_IMPORT_EXTRA_APP_PACKAGE'
|
|
102
|
+
ACTION_APPLICATION_DETAILS_SETTINGS = 'FACADE_IMPORT_ACTION_APPLICATION_DETAILS_SETTINGS'
|
|
103
|
+
|
|
104
|
+
class Uri:
|
|
105
|
+
def __init__(self,package_name):
|
|
106
|
+
print("FACADE_URI")
|
|
107
|
+
|
|
108
|
+
class NotificationManager:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
class NotificationManagerClass:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class NotificationChannel:
|
|
116
|
+
def __init__(self, channel_id, channel_name, importance):
|
|
117
|
+
self.description = None
|
|
118
|
+
self.channel_id = channel_id
|
|
119
|
+
self.channel = None
|
|
120
|
+
print(
|
|
121
|
+
f"[MOCK] NotificationChannel initialized with id={channel_id}, name={channel_name}, importance={importance}")
|
|
122
|
+
|
|
123
|
+
def createNotificationChannel(self, channel):
|
|
124
|
+
self.channel = channel
|
|
125
|
+
print(f"[MOCK] NotificationChannel.createNotificationChannel called with channel={channel}")
|
|
126
|
+
|
|
127
|
+
def getNotificationChannel(self, channel_id):
|
|
128
|
+
self.channel_id = channel_id
|
|
129
|
+
print(f"[MOCK] NotificationChannel.getNotificationChannel called with id={channel_id}")
|
|
130
|
+
|
|
131
|
+
def setDescription(self, description):
|
|
132
|
+
self.description = description
|
|
133
|
+
print(f"[MOCK] NotificationChannel.setDescription called with description={description}")
|
|
134
|
+
|
|
135
|
+
def getId(self):
|
|
136
|
+
print(f"[MOCK] NotificationChannel.getId called, returning {self.channel_id}")
|
|
137
|
+
return self.channel_id
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class IconCompat:
|
|
141
|
+
def createWithBitmap(self, bitmap):
|
|
142
|
+
print(f"[MOCK] IconCompat.createWithBitmap called with bitmap={bitmap}")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class Color:
|
|
146
|
+
def __init__(self):
|
|
147
|
+
print("[MOCK] Color initialized")
|
|
148
|
+
|
|
149
|
+
def parseColor(self, color: str):
|
|
150
|
+
print(f"[MOCK] Color.parseColor called with color={color}")
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class RemoteViews:
|
|
155
|
+
def __init__(self, package_name, small_layout_id):
|
|
156
|
+
print(f"[MOCK] RemoteViews initialized with package_name={package_name}, layout_id={small_layout_id}")
|
|
157
|
+
|
|
158
|
+
def createWithBitmap(self, bitmap):
|
|
159
|
+
print(f"[MOCK] RemoteViews.createWithBitmap called with bitmap={bitmap}")
|
|
160
|
+
|
|
161
|
+
def setTextViewText(self, id, text):
|
|
162
|
+
print(f"[MOCK] RemoteViews.setTextViewText called with id={id}, text={text}")
|
|
163
|
+
|
|
164
|
+
def setTextColor(self, id, color: Color):
|
|
165
|
+
print(f"[MOCK] RemoteViews.setTextColor called with id={id}, color={color}")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class NotificationManagerCompat:
|
|
169
|
+
IMPORTANCE_HIGH = 4
|
|
170
|
+
IMPORTANCE_DEFAULT = 3
|
|
171
|
+
IMPORTANCE_LOW = ''
|
|
172
|
+
IMPORTANCE_MIN = ''
|
|
173
|
+
IMPORTANCE_NONE = ''
|
|
174
|
+
|
|
175
|
+
class AndroidNotification:
|
|
176
|
+
DEFAULT_ALL = 3
|
|
177
|
+
PRIORITY_HIGH = 4
|
|
178
|
+
PRIORITY_DEFAULT = ''
|
|
179
|
+
PRIORITY_LOW = ''
|
|
180
|
+
PRIORITY_MIN = ''
|
|
181
|
+
|
|
182
|
+
class NotificationCompat:
|
|
183
|
+
DEFAULT_ALL = 3
|
|
184
|
+
PRIORITY_HIGH = 4
|
|
185
|
+
PRIORITY_DEFAULT = ''
|
|
186
|
+
PRIORITY_LOW = ''
|
|
187
|
+
PRIORITY_MIN = ''
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class MActions:
|
|
191
|
+
def clear(self):
|
|
192
|
+
"""This Removes all buttons"""
|
|
193
|
+
print('[MOCK] MActions.clear called')
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class NotificationCompatBuilder:
|
|
197
|
+
def __init__(self, context, channel_id):
|
|
198
|
+
self.mActions = MActions()
|
|
199
|
+
print(f"[MOCK] NotificationCompatBuilder initialized with context={context}, channel_id={channel_id}")
|
|
200
|
+
|
|
201
|
+
def setProgress(self, max_value, current_value, endless):
|
|
202
|
+
print(f"[MOCK] setProgress called with max={max_value}, current={current_value}, endless={endless}")
|
|
203
|
+
|
|
204
|
+
def setStyle(self, style):
|
|
205
|
+
print(f"[MOCK] setStyle called with style={style}")
|
|
206
|
+
|
|
207
|
+
def setContentTitle(self, title):
|
|
208
|
+
print(f"[MOCK] setContentTitle called with title={title}")
|
|
209
|
+
|
|
210
|
+
def setContentText(self, text):
|
|
211
|
+
print(f"[MOCK] setContentText called with text={text}")
|
|
212
|
+
|
|
213
|
+
def setSmallIcon(self, icon):
|
|
214
|
+
print(f"[MOCK] setSmallIcon called with icon={icon}")
|
|
215
|
+
|
|
216
|
+
def setLargeIcon(self, icon):
|
|
217
|
+
print(f"[MOCK] setLargeIcon called with icon={icon}")
|
|
218
|
+
|
|
219
|
+
def setAutoCancel(self, auto_cancel: bool):
|
|
220
|
+
print(f"[MOCK] setAutoCancel called with auto_cancel={auto_cancel}")
|
|
221
|
+
|
|
222
|
+
def setPriority(self, priority: Importance):
|
|
223
|
+
print(f"[MOCK] setPriority called with priority={priority}")
|
|
224
|
+
|
|
225
|
+
def setDefaults(self, defaults):
|
|
226
|
+
print(f"[MOCK] setDefaults called with defaults={defaults}")
|
|
227
|
+
|
|
228
|
+
def setOngoing(self, persistent: bool):
|
|
229
|
+
print(f"[MOCK] setOngoing called with persistent={persistent}")
|
|
230
|
+
|
|
231
|
+
def setOnlyAlertOnce(self, state):
|
|
232
|
+
print(f"[MOCK] setOnlyAlertOnce called with state={state}")
|
|
233
|
+
|
|
234
|
+
def build(self):
|
|
235
|
+
print("[MOCK] build called")
|
|
236
|
+
|
|
237
|
+
def setContentIntent(self, pending_action_intent: PendingIntent):
|
|
238
|
+
print(f"[MOCK] setContentIntent called with {pending_action_intent}")
|
|
239
|
+
|
|
240
|
+
def addAction(self, icon_int, action_text, pending_action_intent):
|
|
241
|
+
print(f"[MOCK] addAction called with icon={icon_int}, text={action_text}, intent={pending_action_intent}")
|
|
242
|
+
|
|
243
|
+
def setShowWhen(self, state):
|
|
244
|
+
print(f"[MOCK] setShowWhen called with state={state}")
|
|
245
|
+
|
|
246
|
+
def setWhen(self, time_ms):
|
|
247
|
+
print(f"[MOCK] setWhen called with time_ms={time_ms}")
|
|
248
|
+
|
|
249
|
+
def setCustomContentView(self, layout):
|
|
250
|
+
print(f"[MOCK] setCustomContentView called with layout={layout}")
|
|
251
|
+
|
|
252
|
+
def setCustomBigContentView(self, layout):
|
|
253
|
+
print(f"[MOCK] setCustomBigContentView called with layout={layout}")
|
|
254
|
+
|
|
255
|
+
def setSubText(self, text):
|
|
256
|
+
print(f"[MOCK] setSubText called with text={text}")
|
|
257
|
+
|
|
258
|
+
def setColor(self, color: Color) -> None:
|
|
259
|
+
print(f"[MOCK] setColor called with color={color}")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class NotificationCompatBigTextStyle:
|
|
263
|
+
def bigText(self, body):
|
|
264
|
+
print(f"[MOCK] NotificationCompatBigTextStyle.bigText called with body={body}")
|
|
265
|
+
return self
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class NotificationCompatBigPictureStyle:
|
|
269
|
+
def bigPicture(self, bitmap):
|
|
270
|
+
print(f"[MOCK] NotificationCompatBigPictureStyle.bigPicture called with bitmap={bitmap}")
|
|
271
|
+
return self
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class NotificationCompatInboxStyle:
|
|
275
|
+
def addLine(self, line):
|
|
276
|
+
print(f"[MOCK] NotificationCompatInboxStyle.addLine called with line={line}")
|
|
277
|
+
return self
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class NotificationCompatDecoratedCustomViewStyle:
|
|
281
|
+
def __init__(self):
|
|
282
|
+
print("[MOCK] NotificationCompatDecoratedCustomViewStyle initialized")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class Permission:
|
|
286
|
+
POST_NOTIFICATIONS = ''
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def check_permission(permission: Permission.POST_NOTIFICATIONS):
|
|
290
|
+
print(f"[MOCK] check_permission called with {permission}")
|
|
291
|
+
print(permission)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def request_permissions(_list: [], _callback):
|
|
295
|
+
print(f"[MOCK] request_permissions called with {_list}")
|
|
296
|
+
_callback()
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class AndroidActivity:
|
|
300
|
+
def bind(self, on_new_intent):
|
|
301
|
+
print(f"[MOCK] AndroidActivity.bind called with {on_new_intent}")
|
|
302
|
+
|
|
303
|
+
def unbind(self, on_new_intent):
|
|
304
|
+
print(f"[MOCK] AndroidActivity.unbind called with {on_new_intent}")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class PythonActivity:
|
|
308
|
+
mActivity = "[MOCK] mActivity used"
|
|
309
|
+
def __init__(self):
|
|
310
|
+
print("[MOCK] PythonActivity initialized")
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class DummyIcon:
|
|
314
|
+
icon = 101
|
|
315
|
+
|
|
316
|
+
def __init__(self):
|
|
317
|
+
print("[MOCK] DummyIcon initialized")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class Context:
|
|
321
|
+
def __init__(self):
|
|
322
|
+
print("[MOCK] Context initialized")
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
@staticmethod
|
|
326
|
+
def getApplicationInfo():
|
|
327
|
+
print("[MOCK] Context.getApplicationInfo called")
|
|
328
|
+
return DummyIcon
|
|
329
|
+
|
|
330
|
+
@staticmethod
|
|
331
|
+
def getResources():
|
|
332
|
+
print("[MOCK] Context.getResources called")
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
@staticmethod
|
|
336
|
+
def getPackageName():
|
|
337
|
+
print("[MOCK] Context.getPackageName called")
|
|
338
|
+
return None # TODO get package name from buildozer.spec file
|
|
339
|
+
|
|
340
|
+
# Now writing Knowledge from errors
|
|
341
|
+
# notify.(int, Builder.build()) # must be int
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Collection of useful functions"""
|
|
2
|
+
|
|
3
|
+
import inspect, os, re, traceback
|
|
4
|
+
from .config import autoclass
|
|
5
|
+
from .an_types import Importance
|
|
6
|
+
from .config import (
|
|
7
|
+
get_python_activity_context, app_storage_path, ON_ANDROID,
|
|
8
|
+
BitmapFactory, BuildVersion, Bundle,
|
|
9
|
+
NotificationManagerClass, AndroidNotification, Intent, Settings, Uri, String, Manifest
|
|
10
|
+
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
if ON_ANDROID:
|
|
14
|
+
Color = autoclass('android.graphics.Color')
|
|
15
|
+
else:
|
|
16
|
+
from .an_types import Color
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def can_accept_arguments(func, *args, **kwargs):
|
|
20
|
+
try:
|
|
21
|
+
sig = inspect.signature(func)
|
|
22
|
+
sig.bind(*args, **kwargs)
|
|
23
|
+
return True
|
|
24
|
+
except TypeError:
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if ON_ANDROID:
|
|
29
|
+
context = get_python_activity_context()
|
|
30
|
+
else:
|
|
31
|
+
context = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_android_importance(importance: Importance):
|
|
35
|
+
"""
|
|
36
|
+
Returns Android Importance Values
|
|
37
|
+
:param importance: ['urgent','high','medium','low','none']
|
|
38
|
+
:return: Android equivalent int or empty str
|
|
39
|
+
"""
|
|
40
|
+
if not ON_ANDROID:
|
|
41
|
+
return None
|
|
42
|
+
value = ''
|
|
43
|
+
if importance == 'urgent':
|
|
44
|
+
value = AndroidNotification.PRIORITY_HIGH if BuildVersion.SDK_INT <= 25 else NotificationManagerClass.IMPORTANCE_HIGH
|
|
45
|
+
elif importance == 'high':
|
|
46
|
+
value = AndroidNotification.PRIORITY_DEFAULT if BuildVersion.SDK_INT <= 25 else NotificationManagerClass.IMPORTANCE_DEFAULT
|
|
47
|
+
elif importance == 'medium':
|
|
48
|
+
value = AndroidNotification.PRIORITY_LOW if BuildVersion.SDK_INT <= 25 else NotificationManagerClass.IMPORTANCE_LOW
|
|
49
|
+
elif importance == 'low':
|
|
50
|
+
value = AndroidNotification.PRIORITY_MIN if BuildVersion.SDK_INT <= 25 else NotificationManagerClass.IMPORTANCE_MIN
|
|
51
|
+
elif importance == 'none':
|
|
52
|
+
value = '' if BuildVersion.SDK_INT <= 25 else NotificationManagerClass.IMPORTANCE_NONE
|
|
53
|
+
|
|
54
|
+
return value
|
|
55
|
+
# side-note 'medium' = NotificationCompat.PRIORITY_LOW and 'low' = NotificationCompat.PRIORITY_MIN # weird but from docs
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def generate_channel_id(channel_name: str) -> str:
|
|
59
|
+
"""
|
|
60
|
+
Generate a readable and consistent channel ID from a channel name.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
channel_name (str): The name of the notification channel.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
str: A sanitized channel ID.
|
|
67
|
+
"""
|
|
68
|
+
# Normalize the channel name
|
|
69
|
+
channel_id = channel_name.strip().lower()
|
|
70
|
+
# Replace spaces and special characters with underscores
|
|
71
|
+
channel_id = re.sub(r'[^a-z0-9]+', '_', channel_id)
|
|
72
|
+
# Remove leading/trailing underscores
|
|
73
|
+
channel_id = channel_id.strip('_')
|
|
74
|
+
return channel_id[:50]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_img_from_path(relative_path):
|
|
78
|
+
app_folder = os.path.join(app_storage_path(), 'app')
|
|
79
|
+
img_full_path = os.path.join(app_folder, relative_path)
|
|
80
|
+
if not os.path.exists(img_full_path):
|
|
81
|
+
print(f'\nImage: "{img_full_path}" Not Found, (Local images gotten from App Path)')
|
|
82
|
+
try:
|
|
83
|
+
print("- These are the existing files in your app Folder:")
|
|
84
|
+
print('[' + ', '.join(os.listdir(app_folder)) + ']\n')
|
|
85
|
+
except Exception as could_not_get_files_in_path_error:
|
|
86
|
+
print('Exception: ', could_not_get_files_in_path_error)
|
|
87
|
+
print("Couldn't get Files in App Folder")
|
|
88
|
+
return None
|
|
89
|
+
return get_bitmap_from_path(img_full_path)
|
|
90
|
+
# TODO test with a badly written Image and catch error
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def setLayoutText(layout, id, text, color):
|
|
94
|
+
# checked if self.title_color available before entering method
|
|
95
|
+
if id and text:
|
|
96
|
+
layout.setTextViewText(id, text)
|
|
97
|
+
if color:
|
|
98
|
+
layout.setTextColor(id, Color.parseColor(color))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_bitmap_from_url(url, callback, logs):
|
|
102
|
+
"""Gets Bitmap from url
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
:param url: img url
|
|
106
|
+
:param callback: function to be called after thread done, callback receives bitmap data as argument
|
|
107
|
+
:param logs:
|
|
108
|
+
"""
|
|
109
|
+
if logs:
|
|
110
|
+
print("getting Bitmap from URL---")
|
|
111
|
+
try:
|
|
112
|
+
URL = autoclass('java.net.URL')
|
|
113
|
+
url = URL(url)
|
|
114
|
+
connection = url.openConnection()
|
|
115
|
+
connection.connect()
|
|
116
|
+
input_stream = connection.getInputStream()
|
|
117
|
+
bitmap = BitmapFactory.decodeStream(input_stream)
|
|
118
|
+
input_stream.close()
|
|
119
|
+
if bitmap:
|
|
120
|
+
callback(bitmap)
|
|
121
|
+
else:
|
|
122
|
+
print('Error No Bitmap for small icon ------------')
|
|
123
|
+
except Exception as extracting_bitmap_frm_URL_error:
|
|
124
|
+
callback(None)
|
|
125
|
+
# TODO get all types of JAVA Error that can fail here
|
|
126
|
+
print('Error Type ', extracting_bitmap_frm_URL_error)
|
|
127
|
+
print('Failed to get Bitmap from URL ', traceback.format_exc())
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def add_data_to_intent(intent, title):
|
|
131
|
+
"""Persist Some data to notification object for later use"""
|
|
132
|
+
bundle = Bundle()
|
|
133
|
+
bundle.putString("title", title or 'Title Placeholder')
|
|
134
|
+
# bundle.putInt("notify_id", self.__id)
|
|
135
|
+
bundle.putInt("notify_id", 101)
|
|
136
|
+
intent.putExtras(bundle)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_sound_uri(res_sound_name):
|
|
140
|
+
if not res_sound_name: # Incase it's None
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
package_name = context.getPackageName()
|
|
144
|
+
return Uri.parse(f"android.resource://{package_name}/raw/{res_sound_name}")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def get_package_path():
|
|
148
|
+
"""
|
|
149
|
+
Returns the directory path of this Python package.
|
|
150
|
+
Works on Android, Windows, Linux, macOS.
|
|
151
|
+
"""
|
|
152
|
+
return os.path.dirname(os.path.abspath(__file__))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_bitmap_from_path(img_full_path):
|
|
156
|
+
uri = Uri.parse(f"file://{img_full_path}")
|
|
157
|
+
return BitmapFactory.decodeStream(context.getContentResolver().openInputStream(uri))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def icon_finder(icon_name):
|
|
161
|
+
"""Get the full path to an icon file."""
|
|
162
|
+
try:
|
|
163
|
+
import pkg_resources
|
|
164
|
+
return pkg_resources.resource_filename(__name__, f"fallback-icons/{icon_name}")
|
|
165
|
+
except Exception:
|
|
166
|
+
# Fallback if pkg_resources not available
|
|
167
|
+
package_dir = get_package_path()
|
|
168
|
+
return os.path.join(package_dir, "fallback-icons", icon_name)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def can_show_permission_request_popup():
|
|
172
|
+
"""
|
|
173
|
+
Check if we can show permission request popup for POST_NOTIFICATIONS
|
|
174
|
+
:return: bool
|
|
175
|
+
"""
|
|
176
|
+
if not ON_ANDROID:
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
if BuildVersion.SDK_INT < 33:
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
return context.shouldShowRequestPermissionRationale(Manifest.POST_NOTIFICATIONS)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def open_settings_screen():
|
|
186
|
+
if not context:
|
|
187
|
+
print("android_notify - Can't open settings screen, No context [not On Android]")
|
|
188
|
+
return None
|
|
189
|
+
intent = Intent()
|
|
190
|
+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
191
|
+
package_name = String(context.getPackageName()) # String() is very important else fails silently with a toast
|
|
192
|
+
# saying "The app wasn't found in the list of installed apps" - Xiaomi or "unable to find application to perform this action" - Samsung and Techno
|
|
193
|
+
|
|
194
|
+
if BuildVersion.SDK_INT >= 26: # Android 8.0 - android.os.Build.VERSION_CODES.O
|
|
195
|
+
intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
|
|
196
|
+
intent.putExtra(Settings.EXTRA_APP_PACKAGE, package_name)
|
|
197
|
+
elif BuildVersion.SDK_INT >= 22: # Android 5.0 - Build.VERSION_CODES.LOLLIPOP
|
|
198
|
+
intent.setAction("android.settings.APP_NOTIFICATION_SETTINGS")
|
|
199
|
+
intent.putExtra("app_package", package_name)
|
|
200
|
+
intent.putExtra("app_uid", context.getApplicationInfo().uid)
|
|
201
|
+
else: # Last Retort is to open App Settings Screen
|
|
202
|
+
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
|
203
|
+
intent.addCategory(Intent.CATEGORY_DEFAULT)
|
|
204
|
+
intent.setData(Uri.parse("package:" + package_name))
|
|
205
|
+
|
|
206
|
+
context.startActivity(intent)
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
# https://stackoverflow.com/a/45192258/19961621
|
android_notify/base.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Assists Notification Class with Args keeps subclass cleaner"""
|
|
2
|
+
from dataclasses import dataclass, fields
|
|
3
|
+
import difflib
|
|
4
|
+
from .styles import NotificationStyles
|
|
5
|
+
# For Dev when creating new attr use have to set type for validate_args to work
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class BaseNotification:
|
|
9
|
+
"""Encapsulation"""
|
|
10
|
+
|
|
11
|
+
# Basic options
|
|
12
|
+
title: str = ''
|
|
13
|
+
message: str = ''
|
|
14
|
+
style: str = 'simple'
|
|
15
|
+
|
|
16
|
+
# Style-specific attributes
|
|
17
|
+
big_picture_path: str = ''
|
|
18
|
+
large_icon_path: str = ''
|
|
19
|
+
progress_max_value: int = 0
|
|
20
|
+
progress_current_value: float = 0.0 # Also Takes in Ints
|
|
21
|
+
body: str = ''
|
|
22
|
+
lines_txt: str = ''
|
|
23
|
+
|
|
24
|
+
# Notification Functions
|
|
25
|
+
name: str = ''
|
|
26
|
+
callback: object = None
|
|
27
|
+
|
|
28
|
+
# Advanced Options
|
|
29
|
+
id: int = 0
|
|
30
|
+
app_icon: str = 'Defaults to package app icon'
|
|
31
|
+
sub_text: str=''
|
|
32
|
+
|
|
33
|
+
# Channel related
|
|
34
|
+
channel_name: str = 'Default Channel'
|
|
35
|
+
"""User visible channel name"""
|
|
36
|
+
channel_id: str = 'default_channel'
|
|
37
|
+
"""Used to reference notification channel"""
|
|
38
|
+
|
|
39
|
+
silent: bool = False
|
|
40
|
+
logs: bool = False
|
|
41
|
+
|
|
42
|
+
# Custom Notification Attrs
|
|
43
|
+
title_color: str = ''
|
|
44
|
+
message_color: str = ''
|
|
45
|
+
|
|
46
|
+
def __init__(self, **kwargs):
|
|
47
|
+
"""Custom init to handle validation before dataclass assigns values"""
|
|
48
|
+
|
|
49
|
+
# Validate provided arguments
|
|
50
|
+
self.validate_args(kwargs)
|
|
51
|
+
|
|
52
|
+
# Assign validated values using the normal dataclass behavior
|
|
53
|
+
for field_ in fields(self):
|
|
54
|
+
field_name = field_.name
|
|
55
|
+
setattr(self, field_name, kwargs.get(field_name, getattr(self, field_name)))
|
|
56
|
+
|
|
57
|
+
def validate_args(self, inputted_kwargs):
|
|
58
|
+
"""Check for unexpected arguments and suggest corrections before Python validation"""
|
|
59
|
+
default_fields = {field.name : field.type for field in fields(self)} #{'title': <class 'str'>, 'message': <class 'str'>,...
|
|
60
|
+
allowed_fields_keys = set(default_fields.keys())
|
|
61
|
+
|
|
62
|
+
# Identify invalid arguments
|
|
63
|
+
invalid_args = set(inputted_kwargs) - allowed_fields_keys
|
|
64
|
+
if invalid_args:
|
|
65
|
+
suggestions = []
|
|
66
|
+
for arg in invalid_args:
|
|
67
|
+
closest_match = difflib.get_close_matches(arg, allowed_fields_keys, n=1, cutoff=0.6)
|
|
68
|
+
if closest_match:
|
|
69
|
+
suggestions.append(f"* '{arg}' is invalid -> Did you mean '{closest_match[0]}'?")
|
|
70
|
+
else:
|
|
71
|
+
suggestions.append(f"* '{arg}' is not a valid argument.")
|
|
72
|
+
|
|
73
|
+
suggestion_text = '\n'.join(suggestions)
|
|
74
|
+
raise ValueError(f"Invalid arguments provided:\n{suggestion_text}")
|
|
75
|
+
|
|
76
|
+
# Validating types
|
|
77
|
+
for each_arg in inputted_kwargs.keys():
|
|
78
|
+
expected_type = default_fields[each_arg]
|
|
79
|
+
actual_value = inputted_kwargs[each_arg]
|
|
80
|
+
|
|
81
|
+
# Allow both int and float for progress_current_value
|
|
82
|
+
if each_arg == "progress_current_value":
|
|
83
|
+
if not isinstance(actual_value, (int, float)):
|
|
84
|
+
raise TypeError(f"Expected '{each_arg}' to be int or float, got {type(actual_value)} instead.")
|
|
85
|
+
else:
|
|
86
|
+
if not isinstance(actual_value, expected_type):
|
|
87
|
+
raise TypeError(f"Expected '{each_arg}' to be {expected_type}, got {type(actual_value)} instead.")
|
|
88
|
+
|
|
89
|
+
# Validate `style` values
|
|
90
|
+
style_values = [value for key, value in vars(NotificationStyles).items() if not key.startswith("__")]
|
|
91
|
+
if 'style' in inputted_kwargs and inputted_kwargs['style'] not in ['',*style_values]:
|
|
92
|
+
inputted_style=inputted_kwargs['style']
|
|
93
|
+
allowed_styles=', '.join(style_values)
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"Invalid style '{inputted_style}'. Allowed styles: {allowed_styles}"
|
|
96
|
+
)
|
|
97
|
+
|