android-notify 1.61.0.dev0__tar.gz → 1.61.2.dev0__tar.gz

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.
Files changed (54) hide show
  1. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/PKG-INFO +39 -7
  2. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/README.md +37 -4
  3. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/config.py +1 -1
  4. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/internal/android.py +59 -9
  5. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/internal/channels.py +19 -6
  6. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/internal/facade.py +53 -0
  7. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/internal/java_classes.py +3 -0
  8. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/sword.py +6 -4
  9. android_notify-1.61.2.dev0/android_notify/tests/test_notification_sound.py +67 -0
  10. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify.egg-info/PKG-INFO +39 -7
  11. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify.egg-info/requires.txt +1 -1
  12. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/pyproject.toml +2 -3
  13. android_notify-1.61.0.dev0/android_notify/tests/test_notification_sound.py +0 -24
  14. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/__init__.py +0 -0
  15. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/__main__.py +0 -0
  16. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/base.py +0 -0
  17. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/core.py +0 -0
  18. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/fallback-icons/flet-appicon.png +0 -0
  19. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/fallback-icons/pydroid3-appicon.png +0 -0
  20. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/internal/an_types.py +0 -0
  21. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/internal/helper.py +0 -0
  22. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/internal/intents.py +0 -0
  23. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/internal/logger.py +0 -0
  24. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/internal/permissions.py +0 -0
  25. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/styles.py +0 -0
  26. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/__init__.py +0 -0
  27. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/android_notify_test.py +0 -0
  28. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/base_test.py +0 -0
  29. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/flet/adv/main.py +0 -0
  30. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/flet/adv/tests/__init__.py +0 -0
  31. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/flet/adv/tests/test_android_notify_full.py +0 -0
  32. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/flet/basic/src/core.py +0 -0
  33. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/flet/basic/src/main.py +0 -0
  34. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/flet/flet-working/src/core.py +0 -0
  35. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/flet/flet-working/src/main.py +0 -0
  36. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/main.py +0 -0
  37. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/p4a/hook.py +0 -0
  38. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/serivces/wallpaper.py +0 -0
  39. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/test_basic_notifications.py +0 -0
  40. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/test_notification_actions.py +0 -0
  41. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/test_notification_appearance.py +0 -0
  42. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/test_notification_behavior.py +0 -0
  43. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/test_notification_channels.py +0 -0
  44. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/test_notification_clear.py +0 -0
  45. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/test_notification_permission.py +0 -0
  46. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/test_notification_progress.py +0 -0
  47. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/tests/test_notification_styles.py +0 -0
  48. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/widgets/images.py +0 -0
  49. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify/widgets/texts.py +0 -0
  50. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify.egg-info/SOURCES.txt +0 -0
  51. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify.egg-info/dependency_links.txt +0 -0
  52. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify.egg-info/entry_points.txt +0 -0
  53. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/android_notify.egg-info/top_level.txt +0 -0
  54. {android_notify-1.61.0.dev0 → android_notify-1.61.2.dev0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: android-notify
3
- Version: 1.61.0.dev0
3
+ Version: 1.61.2.dev0
4
4
  Summary: A Python package that simplifies creating Android notifications in Kivy and Flet apps.
5
5
  Author-email: Fabian <fector101@yahoo.com>
6
6
  License-Expression: MIT
@@ -15,9 +15,8 @@ Classifier: Operating System :: Android
15
15
  Classifier: Development Status :: 5 - Production/Stable
16
16
  Classifier: Intended Audience :: Developers
17
17
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
- Requires-Python: >=3.6
19
18
  Description-Content-Type: text/markdown
20
- Requires-Dist: pyjnius>=1.4.2
19
+ Requires-Dist: pyjnius
21
20
  Provides-Extra: dev
22
21
  Requires-Dist: kivy>=2.0.0; extra == "dev"
23
22
 
@@ -81,7 +80,7 @@ In your **`buildozer.spec`** file, ensure you include the following:
81
80
 
82
81
  ```ini
83
82
  # Add pyjnius so ensure it's packaged with the build
84
- requirements = python3, kivy, pyjnius, android-notify==1.61.0.dev0
83
+ requirements = python3, kivy, pyjnius, android-notify==1.61.2.dev0
85
84
  # Add permission for notifications
86
85
  android.permissions = POST_NOTIFICATIONS
87
86
  ```
@@ -99,7 +98,7 @@ In your `pyproject.toml` file, ensure you include the following:
99
98
  ```toml
100
99
  [tool.flet.android]
101
100
  dependencies = [
102
- "pyjnius","android-notify==1.61.0.dev0"
101
+ "pyjnius","android-notify==1.61.2.dev0"
103
102
  ]
104
103
 
105
104
  [tool.flet.android.permission]
@@ -117,10 +116,10 @@ dependencies = [
117
116
  <br/>
118
117
 
119
118
  On the [pydroid 3](https://play.google.com/store/apps/details?id=ru.iiec.pydroid3) mobile app for running python code you can test some features.
120
- - In pip section where you're asked to insert `Libary name` paste `android-notify==1.61.0.dev0`
119
+ - In pip section where you're asked to insert `Libary name` paste `android-notify==1.61.2.dev0`
121
120
  - Minimal working example
122
121
  ```py
123
- # Testing with `android-notify==1.61.0.dev0` on pydroid
122
+ # Testing with `android-notify==1.61.2.dev0` on pydroid
124
123
  from kivy.app import App
125
124
  from kivy.uix.boxlayout import BoxLayout
126
125
  from kivy.uix.button import Button
@@ -209,6 +208,8 @@ n.send()
209
208
  <details>
210
209
  <summary> <b>To use Custom Sounds </b> </summary>
211
210
 
211
+ **Option 1: Audio files bundled in `res/raw`**
212
+
212
213
  - Put audio files in `res/raw` folder,
213
214
  - Then from `buildozer.spec` point to res folder `android.add_resources = res`
214
215
  - and includes it's format `source.include_exts = wav`.
@@ -232,6 +233,37 @@ n=Notification(
232
233
  n.setSound("sneeze")# for android 7 below
233
234
  n.send()
234
235
  ```
236
+
237
+ **Option 2: Local file path or URI (`sound_path`)**
238
+
239
+ You can use a local audio file, a `content://`, `file://`, or `android.resource://` URI directly:
240
+
241
+ ```py
242
+ # Using a local file path
243
+ Notification.createChannel(
244
+ id="local_sound",
245
+ name="Local Sound",
246
+ sound_path="/storage/emulated/0/Download/alert.mp3"
247
+ )
248
+
249
+ # Using a content URI (e.g., from media store)
250
+ Notification.createChannel(
251
+ id="uri_sound",
252
+ name="URI Sound",
253
+ sound_path="content://media/external/audio/media/123"
254
+ )
255
+
256
+ # Send notification with custom sound path
257
+ n = Notification(
258
+ title="Custom Sound",
259
+ message="Playing from local path",
260
+ channel_id="local_sound"
261
+ )
262
+ n.setSound(sound_path="/storage/emulated/0/Download/alert.mp3")
263
+ n.send()
264
+ ```
265
+
266
+ Private files (e.g., in app's `data/` directory) are automatically copied to external storage before playing.
235
267
  </details>
236
268
 
237
269
 
@@ -58,7 +58,7 @@ In your **`buildozer.spec`** file, ensure you include the following:
58
58
 
59
59
  ```ini
60
60
  # Add pyjnius so ensure it's packaged with the build
61
- requirements = python3, kivy, pyjnius, android-notify==1.61.0.dev0
61
+ requirements = python3, kivy, pyjnius, android-notify==1.61.2.dev0
62
62
  # Add permission for notifications
63
63
  android.permissions = POST_NOTIFICATIONS
64
64
  ```
@@ -76,7 +76,7 @@ In your `pyproject.toml` file, ensure you include the following:
76
76
  ```toml
77
77
  [tool.flet.android]
78
78
  dependencies = [
79
- "pyjnius","android-notify==1.61.0.dev0"
79
+ "pyjnius","android-notify==1.61.2.dev0"
80
80
  ]
81
81
 
82
82
  [tool.flet.android.permission]
@@ -94,10 +94,10 @@ dependencies = [
94
94
  <br/>
95
95
 
96
96
  On the [pydroid 3](https://play.google.com/store/apps/details?id=ru.iiec.pydroid3) mobile app for running python code you can test some features.
97
- - In pip section where you're asked to insert `Libary name` paste `android-notify==1.61.0.dev0`
97
+ - In pip section where you're asked to insert `Libary name` paste `android-notify==1.61.2.dev0`
98
98
  - Minimal working example
99
99
  ```py
100
- # Testing with `android-notify==1.61.0.dev0` on pydroid
100
+ # Testing with `android-notify==1.61.2.dev0` on pydroid
101
101
  from kivy.app import App
102
102
  from kivy.uix.boxlayout import BoxLayout
103
103
  from kivy.uix.button import Button
@@ -186,6 +186,8 @@ n.send()
186
186
  <details>
187
187
  <summary> <b>To use Custom Sounds </b> </summary>
188
188
 
189
+ **Option 1: Audio files bundled in `res/raw`**
190
+
189
191
  - Put audio files in `res/raw` folder,
190
192
  - Then from `buildozer.spec` point to res folder `android.add_resources = res`
191
193
  - and includes it's format `source.include_exts = wav`.
@@ -209,6 +211,37 @@ n=Notification(
209
211
  n.setSound("sneeze")# for android 7 below
210
212
  n.send()
211
213
  ```
214
+
215
+ **Option 2: Local file path or URI (`sound_path`)**
216
+
217
+ You can use a local audio file, a `content://`, `file://`, or `android.resource://` URI directly:
218
+
219
+ ```py
220
+ # Using a local file path
221
+ Notification.createChannel(
222
+ id="local_sound",
223
+ name="Local Sound",
224
+ sound_path="/storage/emulated/0/Download/alert.mp3"
225
+ )
226
+
227
+ # Using a content URI (e.g., from media store)
228
+ Notification.createChannel(
229
+ id="uri_sound",
230
+ name="URI Sound",
231
+ sound_path="content://media/external/audio/media/123"
232
+ )
233
+
234
+ # Send notification with custom sound path
235
+ n = Notification(
236
+ title="Custom Sound",
237
+ message="Playing from local path",
238
+ channel_id="local_sound"
239
+ )
240
+ n.setSound(sound_path="/storage/emulated/0/Download/alert.mp3")
241
+ n.send()
242
+ ```
243
+
244
+ Private files (e.g., in app's `data/` directory) are automatically copied to external storage before playing.
212
245
  </details>
213
246
 
214
247
 
@@ -1,6 +1,6 @@
1
1
  import os
2
2
 
3
- __version__ = "1.61.0.dev0"
3
+ __version__ = "1.61.2.dev0"
4
4
 
5
5
 
6
6
  from .internal.java_classes import autoclass, cast, NotificationManager
@@ -1,14 +1,14 @@
1
1
  """
2
2
  Android related logic
3
3
  """
4
- import time
4
+ import time, os
5
5
 
6
6
  from .logger import logger
7
7
  from ..config import get_notification_manager, on_android_platform, from_service_file, on_flet_app, \
8
8
  get_python_activity_context
9
9
  from .permissions import has_notification_permission
10
10
  from .java_classes import autoclass, BuildVersion, Uri, NotificationCompat, NotificationManagerCompat, \
11
- NotificationManager, Context
11
+ NotificationManager, Context, File
12
12
  from .an_types import Importance
13
13
 
14
14
 
@@ -98,22 +98,72 @@ def get_sound_uri(res_sound_name):
98
98
  return Uri.parse(f"android.resource://{package_name}/raw/{res_sound_name}")
99
99
 
100
100
 
101
- def set_sound(builder, res_sound_name):
101
+ def get_sound_uri_from_path(sound_path):
102
+ if not on_android_platform() or not sound_path:
103
+ return None
104
+
105
+ if sound_path.startswith("content://") or sound_path.startswith("file://") or sound_path.startswith("android.resource://"):
106
+ try:
107
+ return Uri.parse(sound_path)
108
+ except Exception as e:
109
+ logger.exception(f"Error parsing sound URI: {sound_path}")
110
+ return None
111
+
112
+ # Resolve relative paths
113
+ abs_path = os.path.abspath(sound_path)
114
+ if not os.path.exists(abs_path):
115
+ logger.warning(f"Sound file does not exist: {sound_path} (resolved to {abs_path})")
116
+ return None
117
+
118
+ # Check if the file is in private storage (contains /data/)
119
+ if "/data/" in abs_path or not (abs_path.startswith("/storage") or abs_path.startswith("/sdcard")):
120
+ try:
121
+ context = get_python_activity_context()
122
+ ext_files = context.getExternalFilesDir("Notifications")
123
+ if ext_files:
124
+ ext_files_path = ext_files.getAbsolutePath()
125
+ dest_file = os.path.join(ext_files_path, os.path.basename(abs_path))
126
+
127
+ # Copy if destination doesn't exist or is older than the source
128
+ if not os.path.exists(dest_file) or os.path.getmtime(abs_path) > os.path.getmtime(dest_file):
129
+ import shutil
130
+ shutil.copy2(abs_path, dest_file)
131
+ abs_path = dest_file
132
+ except Exception as copy_error:
133
+ logger.exception(f"Failed to copy private sound file {abs_path} to external files: {copy_error}")
134
+
135
+ try:
136
+ java_file = File(abs_path)
137
+ return Uri.fromFile(java_file)
138
+ except Exception as e:
139
+ logger.exception(f"Error generating Uri from file path: {abs_path}")
140
+ return None
141
+
142
+
143
+ def set_sound(builder, res_sound_name=None, sound_path=None):
102
144
  """
103
145
  Sets sound for devices less than android 8 (For 8+ use createChannel)
104
146
  :param builder: builder instance
105
147
  :param res_sound_name: audio file name (without .wav or .mp3) locate in res/raw/
148
+ :param sound_path: local file path or uri string
106
149
  """
107
150
 
108
151
  if not on_android_platform():
109
152
  return None
110
153
 
111
- if res_sound_name and BuildVersion.SDK_INT < 26:
112
- try:
113
- builder.setSound(get_sound_uri(res_sound_name))
114
- return True
115
- except Exception as failed_adding_sound_for_devices_below_android8:
116
- logger.exception(failed_adding_sound_for_devices_below_android8)
154
+ if BuildVersion.SDK_INT < 26:
155
+ sound_uri = None
156
+ if sound_path:
157
+ sound_uri = get_sound_uri_from_path(sound_path)
158
+ elif res_sound_name:
159
+ sound_uri = get_sound_uri(res_sound_name)
160
+
161
+ if sound_uri:
162
+ try:
163
+ builder.setSound(sound_uri)
164
+ return True
165
+ except Exception as failed_adding_sound_for_devices_below_android8:
166
+ logger.exception(failed_adding_sound_for_devices_below_android8)
117
167
  return None
118
168
 
119
169
 
@@ -5,9 +5,9 @@ from typing import Any
5
5
 
6
6
  from android_notify.config import get_notification_manager, on_android_platform
7
7
 
8
- from android_notify.internal.java_classes import BuildVersion, NotificationChannel
8
+ from android_notify.internal.java_classes import BuildVersion, NotificationChannel, AudioAttributes, AudioAttributesBuilder
9
9
  from android_notify.internal.an_types import Importance
10
- from android_notify.internal.android import get_sound_uri, get_android_importance
10
+ from android_notify.internal.android import get_sound_uri, get_sound_uri_from_path, get_android_importance
11
11
  from android_notify.internal.logger import logger
12
12
 
13
13
  def does_channel_exist(channel_id):
@@ -24,7 +24,7 @@ def does_channel_exist(channel_id):
24
24
  return False
25
25
 
26
26
 
27
- def create_channel(id__, name: str, description='', importance: Importance = 'urgent', res_sound_name=None,vibrate=False):
27
+ def create_channel(id__, name: str, description='', importance: Importance = 'urgent', res_sound_name=None, sound_path=None, vibrate=False):
28
28
  """
29
29
  Creates a user visible toggle button for specific notifications, Required For Android 8.0+
30
30
  :param id__: Used to send other notifications later through same channel.
@@ -32,12 +32,13 @@ def create_channel(id__, name: str, description='', importance: Importance = 'ur
32
32
  :param description: user-visible detail about channel (Not required defaults to empty str).
33
33
  :param importance: ['urgent', 'high', 'medium', 'low', 'none'] defaults to 'urgent' i.e. makes a sound and shows briefly
34
34
  :param res_sound_name: audio file name (without .wav or .mp3) locate in res/raw/
35
+ :param sound_path: local file path or uri string
35
36
  :param vibrate: if channel notifications should vibrate or not
36
37
  :return: boolean if channel created
37
38
  """
38
39
  def info_log():
39
40
  logger.info(
40
- f"Created {name} channel, id: {id__}, description: {description}, res_sound_name: {res_sound_name},vibrate: {vibrate}")
41
+ f"Created {name} channel, id: {id__}, description: {description}, res_sound_name: {res_sound_name}, sound_path: {sound_path}, vibrate: {vibrate}")
41
42
 
42
43
  if not on_android_platform():
43
44
  info_log()
@@ -45,14 +46,26 @@ def create_channel(id__, name: str, description='', importance: Importance = 'ur
45
46
 
46
47
  notification_manager = get_notification_manager()
47
48
  android_importance_value = get_android_importance(importance)
48
- sound_uri = get_sound_uri(res_sound_name)
49
+
50
+ if sound_path:
51
+ sound_uri = get_sound_uri_from_path(sound_path)
52
+ else:
53
+ sound_uri = get_sound_uri(res_sound_name)
49
54
 
50
55
  if not does_channel_exist(id__):
51
56
  channel = NotificationChannel(id__, name, android_importance_value)
52
57
  if description:
53
58
  channel.setDescription(description)
54
59
  if sound_uri:
55
- channel.setSound(sound_uri, None)
60
+ try:
61
+ aa_builder = AudioAttributesBuilder()
62
+ aa_builder.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
63
+ aa_builder.setUsage(AudioAttributes.USAGE_NOTIFICATION)
64
+ audio_attributes = aa_builder.build()
65
+ channel.setSound(sound_uri, audio_attributes)
66
+ except Exception as sound_attributes_error:
67
+ logger.warning(f"Could not build AudioAttributes, falling back to None: {sound_attributes_error}")
68
+ channel.setSound(sound_uri, None)
56
69
  if vibrate:
57
70
  # channel.setVibrationPattern([0, 500, 200, 500]) # Using Phone's default pattern
58
71
  # Android 15 ignored long patterns, didn't vibrate when not in silent and
@@ -106,6 +106,49 @@ class Uri:
106
106
  def __init__(self, package_name):
107
107
  logger.debug("FACADE_URI")
108
108
 
109
+ @classmethod
110
+ def parse(cls, uri_string):
111
+ logger.debug(f"[MOCK] Uri.parse called with uri_string={uri_string}")
112
+ return cls
113
+
114
+ @classmethod
115
+ def fromFile(cls, java_file):
116
+ logger.debug(f"[MOCK] Uri.fromFile called with file={java_file}")
117
+ return cls
118
+
119
+
120
+ class AudioAttributes:
121
+ CONTENT_TYPE_SONIFICATION = 4
122
+ USAGE_NOTIFICATION = 5
123
+ USAGE_ALARM = 4
124
+
125
+
126
+ class AudioAttributesBuilder:
127
+ def __init__(self):
128
+ logger.debug("[MOCK] AudioAttributesBuilder initialized")
129
+
130
+ def setContentType(self, content_type):
131
+ logger.debug(f"[MOCK] AudioAttributesBuilder.setContentType called with content_type={content_type}")
132
+ return self
133
+
134
+ def setUsage(self, usage):
135
+ logger.debug(f"[MOCK] AudioAttributesBuilder.setUsage called with usage={usage}")
136
+ return self
137
+
138
+ def build(self):
139
+ logger.debug("[MOCK] AudioAttributesBuilder.build called")
140
+ return AudioAttributes()
141
+
142
+
143
+ class File:
144
+ def __init__(self, path):
145
+ self.path = path
146
+ logger.debug(f"[MOCK] File initialized with path={path}")
147
+
148
+ def getAbsolutePath(self):
149
+ logger.debug(f"[MOCK] File.getAbsolutePath called, returning {self.path}")
150
+ return self.path
151
+
109
152
 
110
153
  class NotificationManager:
111
154
  pass
@@ -402,6 +445,16 @@ class Context:
402
445
  logger.debug("[MOCK] Context.getPackageName called")
403
446
  return None # TODO get package name from buildozer.spec file
404
447
 
448
+ @staticmethod
449
+ def getExternalFilesDir(directory_type):
450
+ logger.debug(f"[MOCK] Context.getExternalFilesDir called with type={directory_type}")
451
+ return File("mock_external_files_dir")
452
+
453
+ @staticmethod
454
+ def getExternalCacheDir():
455
+ logger.debug("[MOCK] Context.getExternalCacheDir called")
456
+ return File("mock_external_cache_dir")
457
+
405
458
 
406
459
  class PackageManager:
407
460
  @property
@@ -40,6 +40,9 @@ if on_android_platform():
40
40
  Color = autoclass('android.graphics.Color')
41
41
  Context = autoclass('android.content.Context')
42
42
  PackageManager = autoclass("android.content.pm.PackageManager")
43
+ AudioAttributes = autoclass('android.media.AudioAttributes')
44
+ AudioAttributesBuilder = autoclass('android.media.AudioAttributes$Builder')
45
+ File = autoclass('java.io.File')
43
46
  except Exception as e:
44
47
  from .facade import *
45
48
  logger.exception("Didn't get Basic Java Classes")
@@ -149,7 +149,7 @@ class Notification(BaseNotification):
149
149
  return does_channel_exist(channel_id=channel_id)
150
150
 
151
151
  @classmethod
152
- def createChannel(cls, id, name: str, description='', importance: Importance = 'urgent', res_sound_name=None, vibrate=False):
152
+ def createChannel(cls, id, name: str, description='', importance: Importance = 'urgent', res_sound_name=None, sound_path=None, vibrate=False):
153
153
  """
154
154
  Creates a user visible toggle button for specific notifications, Required For Android 8.0+
155
155
  :param id: Used to send other notifications later through same channel.
@@ -157,10 +157,11 @@ class Notification(BaseNotification):
157
157
  :param description: user-visible detail about channel (Not required defaults to empty str).
158
158
  :param importance: ['urgent', 'high', 'medium', 'low', 'none'] defaults to 'urgent' i.e. makes a sound and shows briefly
159
159
  :param res_sound_name: audio file name (without .wav or .mp3) locate in res/raw/
160
+ :param sound_path: local file path or uri string
160
161
  :param vibrate: if channel notifications should vibrate or not
161
162
  :return: boolean if channel created
162
163
  """
163
- return create_channel(id__=id, name=name, description=description, importance=importance, res_sound_name=res_sound_name, vibrate=vibrate)
164
+ return create_channel(id__=id, name=name, description=description, importance=importance, res_sound_name=res_sound_name, sound_path=sound_path, vibrate=vibrate)
164
165
 
165
166
  @classmethod
166
167
  def deleteChannel(cls, channel_id):
@@ -558,13 +559,14 @@ class Notification(BaseNotification):
558
559
  """Pass in a list of strings to be used for lines"""
559
560
  set_lines(builder=self.builder, lines=lines)
560
561
 
561
- def setSound(self, res_sound_name):
562
+ def setSound(self, res_sound_name=None, sound_path=None):
562
563
  """
563
564
  Sets sound for devices less than android 8 (For 8+ use createChannel)
564
565
  :param res_sound_name: audio file name (without .wav or .mp3) locate in res/raw/
566
+ :param sound_path: local file path or uri string
565
567
  """
566
568
 
567
- return set_sound(self.builder, res_sound_name)
569
+ return set_sound(self.builder, res_sound_name=res_sound_name, sound_path=sound_path)
568
570
 
569
571
  def fill_args(self, silent: bool = False, persistent=False, close_on_click=True):
570
572
  """Name Makes More sense than start_building
@@ -0,0 +1,67 @@
1
+ from android_notify import Notification
2
+ from .base_test import AndroidNotifyBaseTest, secs5
3
+ import time
4
+
5
+
6
+ class TestNotificationSound(AndroidNotifyBaseTest):
7
+
8
+ def test_set_sound(self):
9
+ try:
10
+ time.sleep(secs5)
11
+ Notification.createChannel(
12
+ id="sound_test",
13
+ name="Sound Test",
14
+ res_sound_name="sneeze"
15
+ )
16
+ n = Notification(
17
+ title="Sound Test",
18
+ message="Testing custom sound",
19
+ channel_id="sound_test"
20
+ )
21
+ n.setSound("sneeze")
22
+ n.send()
23
+ except Exception as e:
24
+ self.fail(f"Sound failed: {e}")
25
+
26
+ def test_set_sound_path(self):
27
+ import os
28
+ mock_file = "test_mock_sound.mp3"
29
+ with open(mock_file, "w") as f:
30
+ f.write("mock audio data")
31
+
32
+ try:
33
+ Notification.createChannel(
34
+ id="sound_path_test",
35
+ name="Sound Path Test",
36
+ sound_path=mock_file
37
+ )
38
+ n = Notification(
39
+ title="Sound Path Test",
40
+ message="Testing custom sound path",
41
+ channel_id="sound_path_test"
42
+ )
43
+ n.setSound(sound_path=mock_file)
44
+ n.send()
45
+ except Exception as e:
46
+ self.fail(f"Sound path failed: {e}")
47
+ finally:
48
+ if os.path.exists(mock_file):
49
+ os.remove(mock_file)
50
+
51
+ def test_set_sound_content_uri(self):
52
+ try:
53
+ content_uri = "content://media/external/audio/media/123"
54
+ Notification.createChannel(
55
+ id="sound_uri_test",
56
+ name="Sound Uri Test",
57
+ sound_path=content_uri
58
+ )
59
+ n = Notification(
60
+ title="Sound Uri Test",
61
+ message="Testing content URI",
62
+ channel_id="sound_uri_test"
63
+ )
64
+ n.setSound(sound_path=content_uri)
65
+ n.send()
66
+ except Exception as e:
67
+ self.fail(f"Sound content URI failed: {e}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: android-notify
3
- Version: 1.61.0.dev0
3
+ Version: 1.61.2.dev0
4
4
  Summary: A Python package that simplifies creating Android notifications in Kivy and Flet apps.
5
5
  Author-email: Fabian <fector101@yahoo.com>
6
6
  License-Expression: MIT
@@ -15,9 +15,8 @@ Classifier: Operating System :: Android
15
15
  Classifier: Development Status :: 5 - Production/Stable
16
16
  Classifier: Intended Audience :: Developers
17
17
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
- Requires-Python: >=3.6
19
18
  Description-Content-Type: text/markdown
20
- Requires-Dist: pyjnius>=1.4.2
19
+ Requires-Dist: pyjnius
21
20
  Provides-Extra: dev
22
21
  Requires-Dist: kivy>=2.0.0; extra == "dev"
23
22
 
@@ -81,7 +80,7 @@ In your **`buildozer.spec`** file, ensure you include the following:
81
80
 
82
81
  ```ini
83
82
  # Add pyjnius so ensure it's packaged with the build
84
- requirements = python3, kivy, pyjnius, android-notify==1.61.0.dev0
83
+ requirements = python3, kivy, pyjnius, android-notify==1.61.2.dev0
85
84
  # Add permission for notifications
86
85
  android.permissions = POST_NOTIFICATIONS
87
86
  ```
@@ -99,7 +98,7 @@ In your `pyproject.toml` file, ensure you include the following:
99
98
  ```toml
100
99
  [tool.flet.android]
101
100
  dependencies = [
102
- "pyjnius","android-notify==1.61.0.dev0"
101
+ "pyjnius","android-notify==1.61.2.dev0"
103
102
  ]
104
103
 
105
104
  [tool.flet.android.permission]
@@ -117,10 +116,10 @@ dependencies = [
117
116
  <br/>
118
117
 
119
118
  On the [pydroid 3](https://play.google.com/store/apps/details?id=ru.iiec.pydroid3) mobile app for running python code you can test some features.
120
- - In pip section where you're asked to insert `Libary name` paste `android-notify==1.61.0.dev0`
119
+ - In pip section where you're asked to insert `Libary name` paste `android-notify==1.61.2.dev0`
121
120
  - Minimal working example
122
121
  ```py
123
- # Testing with `android-notify==1.61.0.dev0` on pydroid
122
+ # Testing with `android-notify==1.61.2.dev0` on pydroid
124
123
  from kivy.app import App
125
124
  from kivy.uix.boxlayout import BoxLayout
126
125
  from kivy.uix.button import Button
@@ -209,6 +208,8 @@ n.send()
209
208
  <details>
210
209
  <summary> <b>To use Custom Sounds </b> </summary>
211
210
 
211
+ **Option 1: Audio files bundled in `res/raw`**
212
+
212
213
  - Put audio files in `res/raw` folder,
213
214
  - Then from `buildozer.spec` point to res folder `android.add_resources = res`
214
215
  - and includes it's format `source.include_exts = wav`.
@@ -232,6 +233,37 @@ n=Notification(
232
233
  n.setSound("sneeze")# for android 7 below
233
234
  n.send()
234
235
  ```
236
+
237
+ **Option 2: Local file path or URI (`sound_path`)**
238
+
239
+ You can use a local audio file, a `content://`, `file://`, or `android.resource://` URI directly:
240
+
241
+ ```py
242
+ # Using a local file path
243
+ Notification.createChannel(
244
+ id="local_sound",
245
+ name="Local Sound",
246
+ sound_path="/storage/emulated/0/Download/alert.mp3"
247
+ )
248
+
249
+ # Using a content URI (e.g., from media store)
250
+ Notification.createChannel(
251
+ id="uri_sound",
252
+ name="URI Sound",
253
+ sound_path="content://media/external/audio/media/123"
254
+ )
255
+
256
+ # Send notification with custom sound path
257
+ n = Notification(
258
+ title="Custom Sound",
259
+ message="Playing from local path",
260
+ channel_id="local_sound"
261
+ )
262
+ n.setSound(sound_path="/storage/emulated/0/Download/alert.mp3")
263
+ n.send()
264
+ ```
265
+
266
+ Private files (e.g., in app's `data/` directory) are automatically copied to external storage before playing.
235
267
  </details>
236
268
 
237
269
 
@@ -1,4 +1,4 @@
1
- pyjnius>=1.4.2
1
+ pyjnius
2
2
 
3
3
  [dev]
4
4
  kivy>=2.0.0
@@ -4,16 +4,15 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "android-notify"
7
- version = "1.61.0.dev0"
7
+ version = "1.61.2.dev0"
8
8
  description = "A Python package that simplifies creating Android notifications in Kivy and Flet apps."
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  authors = [
11
11
  { name = "Fabian", email = "fector101@yahoo.com" }
12
12
  ]
13
13
  license = "MIT"
14
- requires-python = ">=3.6"
15
14
  dependencies = [
16
- "pyjnius>=1.4.2"
15
+ "pyjnius"
17
16
  ]
18
17
  keywords = [
19
18
  "android",
@@ -1,24 +0,0 @@
1
- from android_notify import Notification
2
- from .base_test import AndroidNotifyBaseTest, secs5
3
- import time
4
-
5
-
6
- class TestNotificationSound(AndroidNotifyBaseTest):
7
-
8
- def test_set_sound(self):
9
- try:
10
- time.sleep(secs5)
11
- Notification.createChannel(
12
- id="sound_test",
13
- name="Sound Test",
14
- res_sound_name="sneeze"
15
- )
16
- n = Notification(
17
- title="Sound Test",
18
- message="Testing custom sound",
19
- channel_id="sound_test"
20
- )
21
- n.setSound("sneeze")
22
- n.send()
23
- except Exception as e:
24
- self.fail(f"Sound failed: {e}")