android-notify 1.61.0__tar.gz → 1.61.1.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 → android_notify-1.61.1.dev0}/PKG-INFO +64 -47
  2. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/README.md +63 -45
  3. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/config.py +2 -1
  4. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/core.py +3 -4
  5. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/internal/android.py +59 -9
  6. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/internal/channels.py +19 -6
  7. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/internal/facade.py +53 -0
  8. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/internal/helper.py +4 -1
  9. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/internal/java_classes.py +13 -17
  10. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/internal/permissions.py +9 -22
  11. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/sword.py +6 -4
  12. android_notify-1.61.1.dev0/android_notify/tests/flet/basic/src/core.py +221 -0
  13. android_notify-1.61.1.dev0/android_notify/tests/flet/flet-working/src/core.py +221 -0
  14. android_notify-1.61.1.dev0/android_notify/tests/test_notification_sound.py +67 -0
  15. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/widgets/images.py +2 -1
  16. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/widgets/texts.py +5 -1
  17. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify.egg-info/PKG-INFO +64 -47
  18. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify.egg-info/SOURCES.txt +2 -0
  19. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/pyproject.toml +1 -2
  20. android_notify-1.61.0/android_notify/tests/test_notification_sound.py +0 -24
  21. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/__init__.py +0 -0
  22. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/__main__.py +0 -0
  23. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/base.py +0 -0
  24. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/fallback-icons/flet-appicon.png +0 -0
  25. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/fallback-icons/pydroid3-appicon.png +0 -0
  26. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/internal/an_types.py +0 -0
  27. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/internal/intents.py +0 -0
  28. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/internal/logger.py +0 -0
  29. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/styles.py +0 -0
  30. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/tests/__init__.py +0 -0
  31. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/tests/android_notify_test.py +0 -0
  32. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/tests/base_test.py +0 -0
  33. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/tests/flet/adv/main.py +0 -0
  34. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/tests/flet/adv/tests/__init__.py +0 -0
  35. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/tests/flet/adv/tests/test_android_notify_full.py +0 -0
  36. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/tests/flet/basic/src/main.py +0 -0
  37. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/tests/flet/flet-working/src/main.py +0 -0
  38. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/tests/main.py +0 -0
  39. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/tests/p4a/hook.py +0 -0
  40. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/tests/serivces/wallpaper.py +0 -0
  41. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/tests/test_basic_notifications.py +0 -0
  42. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/tests/test_notification_actions.py +0 -0
  43. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/tests/test_notification_appearance.py +0 -0
  44. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/tests/test_notification_behavior.py +0 -0
  45. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/tests/test_notification_channels.py +0 -0
  46. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/tests/test_notification_clear.py +0 -0
  47. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/tests/test_notification_permission.py +0 -0
  48. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/tests/test_notification_progress.py +0 -0
  49. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify/tests/test_notification_styles.py +0 -0
  50. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify.egg-info/dependency_links.txt +0 -0
  51. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify.egg-info/entry_points.txt +0 -0
  52. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify.egg-info/requires.txt +0 -0
  53. {android_notify-1.61.0 → android_notify-1.61.1.dev0}/android_notify.egg-info/top_level.txt +0 -0
  54. {android_notify-1.61.0 → android_notify-1.61.1.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
3
+ Version: 1.61.1.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,7 +15,6 @@ 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
19
  Requires-Dist: pyjnius>=1.4.2
21
20
  Provides-Extra: dev
@@ -81,14 +80,9 @@ 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
83
+ requirements = python3, kivy, pyjnius, android-notify==1.61.1.dev0
85
84
  # Add permission for notifications
86
85
  android.permissions = POST_NOTIFICATIONS
87
-
88
- # Required dependency (write exactly as shown, no quotation marks)
89
- android.gradle_dependencies = androidx.core:core-ktx:1.15.0
90
- android.enable_androidx = True
91
- android.api = 35
92
86
  ```
93
87
 
94
88
  </details>
@@ -98,13 +92,13 @@ android.api = 35
98
92
  <summary><b>Flet apps:</b></summary>
99
93
  <br/>
100
94
 
101
- In your `pyproject.toml` file, ensure you include the following:
95
+ In your `pyproject.toml` file, ensure you include the following:
102
96
 
103
97
 
104
98
  ```toml
105
99
  [tool.flet.android]
106
100
  dependencies = [
107
- "pyjnius","android-notify==1.61.0.dev0"
101
+ "pyjnius","android-notify==1.61.1.dev0"
108
102
  ]
109
103
 
110
104
  [tool.flet.android.permission]
@@ -114,35 +108,7 @@ dependencies = [
114
108
 
115
109
  </details>
116
110
 
117
- <details>
118
-
119
- <summary><b>Desktop</b></summary>
120
- <br/>
121
-
122
- For IDE IntelliSense Can be installed via `pip install`:
123
-
124
- ```bash
125
- pip install android_notify
126
- android-notify -v
127
- ```
128
-
129
- </details>
130
-
131
- ------
132
- ## Installing without Androidx
133
- How to use without `gradle_dependencies`
134
- Use `android-notify==1.61.0.dev0` to install via `pip`
135
111
 
136
- <details>
137
- <summary><b>In Kivy</b></summary>
138
- <br/>
139
-
140
- ```ini
141
- # buildozer.spec
142
- requirements = python3, kivy, pyjnius, android-notify==1.61.0.dev0
143
- ```
144
-
145
- </details>
146
112
 
147
113
  <details>
148
114
 
@@ -150,10 +116,10 @@ requirements = python3, kivy, pyjnius, android-notify==1.61.0.dev0
150
116
  <br/>
151
117
 
152
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.
153
- - 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.1.dev0`
154
120
  - Minimal working example
155
121
  ```py
156
- # Testing with `android-notify==1.61.0.dev0` on pydroid
122
+ # Testing with `android-notify==1.61.1.dev0` on pydroid
157
123
  from kivy.app import App
158
124
  from kivy.uix.boxlayout import BoxLayout
159
125
  from kivy.uix.button import Button
@@ -180,7 +146,9 @@ class AndroidNotifyDemoApp(App):
180
146
  def send_notification(self, *args):
181
147
  Notification(
182
148
  title="Hello from Android Notify",
183
- message="This is a basic notification."
149
+ message="This is a basic notification.",
150
+ channel_id="android_notify_demo",
151
+ channel_name="Android Notify Demo"
184
152
  ).send()
185
153
 
186
154
 
@@ -191,11 +159,23 @@ if __name__ == "__main__":
191
159
  </details>
192
160
 
193
161
 
162
+ <details>
163
+
164
+ <summary><b>Desktop</b></summary>
165
+ <br/>
166
+
167
+ For IDE IntelliSense Can be installed via `pip install`:
168
+
169
+ ```bash
170
+ pip install android_notify
171
+ android-notify -v
172
+ ```
173
+ </details>
194
174
 
195
175
  ## Documentation
196
- For Dev Version usage
176
+ For Dev Version use
197
177
  ```ini
198
- requirements = python3, kivy, pyjnius, https://github.com/Fector101/android_notify/archive/main.zip
178
+ requirements = python3, kivy, pyjnius, https://github.com/Fector101/android_notify/archive/without-androidx.zip
199
179
  ```
200
180
 
201
181
  <details>
@@ -204,7 +184,7 @@ requirements = python3, kivy, pyjnius, https://github.com/Fector101/android_noti
204
184
  - Make things happen without being in your app
205
185
  ```python
206
186
  from android_notify import Notification
207
- notification = Notification(title="Reciver Notification")
187
+ notification = Notification(title="Receiver Notification")
208
188
  notification.addButton(text="Stop", receiver_name="CarouselReceiver", action="ACTION_STOP")
209
189
  notification.addButton(text="Skip", receiver_name="CarouselReceiver", action="ACTION_SKIP")
210
190
  ```
@@ -228,6 +208,8 @@ n.send()
228
208
  <details>
229
209
  <summary> <b>To use Custom Sounds </b> </summary>
230
210
 
211
+ **Option 1: Audio files bundled in `res/raw`**
212
+
231
213
  - Put audio files in `res/raw` folder,
232
214
  - Then from `buildozer.spec` point to res folder `android.add_resources = res`
233
215
  - and includes it's format `source.include_exts = wav`.
@@ -251,6 +233,37 @@ n=Notification(
251
233
  n.setSound("sneeze")# for android 7 below
252
234
  n.send()
253
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.
254
267
  </details>
255
268
 
256
269
 
@@ -267,7 +280,7 @@ Notification.createChannel(id='shake', name="Shake Passage", vibrate=True)
267
280
 
268
281
  n=Notification(title='Vibrate',channel_id='shake')
269
282
  n.setVibrate() # for less than android 8
270
- n.fVibrate() # To Force Vibrate
283
+ n.fVibrate() # To Force Vibrate (when user turned off in settings)
271
284
  n.send()
272
285
  ```
273
286
 
@@ -305,5 +318,9 @@ from android_notify import Notification, NotificationHandler
305
318
 
306
319
  ## ☕ Support the Project
307
320
 
308
- If you find this project helpful, your support would help me continue working on open-source projects
309
- [donate](https://www.buymeacoffee.com/fector101)
321
+ If you find this project helpful, consider buying me a coffee! 😊
322
+ Or Giving it a star on 🌟 [GitHub](https://github.com/Fector101/android_notify/) Your support helps maintain and improve the project.
323
+
324
+ <a href="https://www.buymeacoffee.com/fector101" target="_blank">
325
+ <img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="60">
326
+ </a>
@@ -58,14 +58,9 @@ 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
61
+ requirements = python3, kivy, pyjnius, android-notify==1.61.1.dev0
62
62
  # Add permission for notifications
63
63
  android.permissions = POST_NOTIFICATIONS
64
-
65
- # Required dependency (write exactly as shown, no quotation marks)
66
- android.gradle_dependencies = androidx.core:core-ktx:1.15.0
67
- android.enable_androidx = True
68
- android.api = 35
69
64
  ```
70
65
 
71
66
  </details>
@@ -75,13 +70,13 @@ android.api = 35
75
70
  <summary><b>Flet apps:</b></summary>
76
71
  <br/>
77
72
 
78
- In your `pyproject.toml` file, ensure you include the following:
73
+ In your `pyproject.toml` file, ensure you include the following:
79
74
 
80
75
 
81
76
  ```toml
82
77
  [tool.flet.android]
83
78
  dependencies = [
84
- "pyjnius","android-notify==1.61.0.dev0"
79
+ "pyjnius","android-notify==1.61.1.dev0"
85
80
  ]
86
81
 
87
82
  [tool.flet.android.permission]
@@ -91,35 +86,7 @@ dependencies = [
91
86
 
92
87
  </details>
93
88
 
94
- <details>
95
-
96
- <summary><b>Desktop</b></summary>
97
- <br/>
98
-
99
- For IDE IntelliSense Can be installed via `pip install`:
100
-
101
- ```bash
102
- pip install android_notify
103
- android-notify -v
104
- ```
105
-
106
- </details>
107
-
108
- ------
109
- ## Installing without Androidx
110
- How to use without `gradle_dependencies`
111
- Use `android-notify==1.61.0.dev0` to install via `pip`
112
89
 
113
- <details>
114
- <summary><b>In Kivy</b></summary>
115
- <br/>
116
-
117
- ```ini
118
- # buildozer.spec
119
- requirements = python3, kivy, pyjnius, android-notify==1.61.0.dev0
120
- ```
121
-
122
- </details>
123
90
 
124
91
  <details>
125
92
 
@@ -127,10 +94,10 @@ requirements = python3, kivy, pyjnius, android-notify==1.61.0.dev0
127
94
  <br/>
128
95
 
129
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.
130
- - 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.1.dev0`
131
98
  - Minimal working example
132
99
  ```py
133
- # Testing with `android-notify==1.61.0.dev0` on pydroid
100
+ # Testing with `android-notify==1.61.1.dev0` on pydroid
134
101
  from kivy.app import App
135
102
  from kivy.uix.boxlayout import BoxLayout
136
103
  from kivy.uix.button import Button
@@ -157,7 +124,9 @@ class AndroidNotifyDemoApp(App):
157
124
  def send_notification(self, *args):
158
125
  Notification(
159
126
  title="Hello from Android Notify",
160
- message="This is a basic notification."
127
+ message="This is a basic notification.",
128
+ channel_id="android_notify_demo",
129
+ channel_name="Android Notify Demo"
161
130
  ).send()
162
131
 
163
132
 
@@ -168,11 +137,23 @@ if __name__ == "__main__":
168
137
  </details>
169
138
 
170
139
 
140
+ <details>
141
+
142
+ <summary><b>Desktop</b></summary>
143
+ <br/>
144
+
145
+ For IDE IntelliSense Can be installed via `pip install`:
146
+
147
+ ```bash
148
+ pip install android_notify
149
+ android-notify -v
150
+ ```
151
+ </details>
171
152
 
172
153
  ## Documentation
173
- For Dev Version usage
154
+ For Dev Version use
174
155
  ```ini
175
- requirements = python3, kivy, pyjnius, https://github.com/Fector101/android_notify/archive/main.zip
156
+ requirements = python3, kivy, pyjnius, https://github.com/Fector101/android_notify/archive/without-androidx.zip
176
157
  ```
177
158
 
178
159
  <details>
@@ -181,7 +162,7 @@ requirements = python3, kivy, pyjnius, https://github.com/Fector101/android_noti
181
162
  - Make things happen without being in your app
182
163
  ```python
183
164
  from android_notify import Notification
184
- notification = Notification(title="Reciver Notification")
165
+ notification = Notification(title="Receiver Notification")
185
166
  notification.addButton(text="Stop", receiver_name="CarouselReceiver", action="ACTION_STOP")
186
167
  notification.addButton(text="Skip", receiver_name="CarouselReceiver", action="ACTION_SKIP")
187
168
  ```
@@ -205,6 +186,8 @@ n.send()
205
186
  <details>
206
187
  <summary> <b>To use Custom Sounds </b> </summary>
207
188
 
189
+ **Option 1: Audio files bundled in `res/raw`**
190
+
208
191
  - Put audio files in `res/raw` folder,
209
192
  - Then from `buildozer.spec` point to res folder `android.add_resources = res`
210
193
  - and includes it's format `source.include_exts = wav`.
@@ -228,6 +211,37 @@ n=Notification(
228
211
  n.setSound("sneeze")# for android 7 below
229
212
  n.send()
230
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.
231
245
  </details>
232
246
 
233
247
 
@@ -244,7 +258,7 @@ Notification.createChannel(id='shake', name="Shake Passage", vibrate=True)
244
258
 
245
259
  n=Notification(title='Vibrate',channel_id='shake')
246
260
  n.setVibrate() # for less than android 8
247
- n.fVibrate() # To Force Vibrate
261
+ n.fVibrate() # To Force Vibrate (when user turned off in settings)
248
262
  n.send()
249
263
  ```
250
264
 
@@ -282,5 +296,9 @@ from android_notify import Notification, NotificationHandler
282
296
 
283
297
  ## ☕ Support the Project
284
298
 
285
- If you find this project helpful, your support would help me continue working on open-source projects
286
- [donate](https://www.buymeacoffee.com/fector101)
299
+ If you find this project helpful, consider buying me a coffee! 😊
300
+ Or Giving it a star on 🌟 [GitHub](https://github.com/Fector101/android_notify/) Your support helps maintain and improve the project.
301
+
302
+ <a href="https://www.buymeacoffee.com/fector101" target="_blank">
303
+ <img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="60">
304
+ </a>
@@ -1,6 +1,7 @@
1
1
  import os
2
2
 
3
- __version__ = "1.61.0"
3
+ __version__ = "1.61.1.dev0"
4
+
4
5
 
5
6
  from .internal.java_classes import autoclass, cast, NotificationManager
6
7
  from .internal.logger import logger
@@ -6,7 +6,7 @@ from android_notify.internal.logger import logger
6
6
  from android_notify.config import get_python_activity, on_android_platform, get_python_activity_context
7
7
  from android_notify.internal.permissions import has_notification_permission, ask_notification_permission
8
8
  from android_notify.internal.java_classes import autoclass, BuildVersion, BitmapFactory, NotificationChannel, NotificationManagerCompat, NotificationCompat, NotificationCompatBuilder, \
9
- NotificationCompatBigTextStyle, NotificationCompatBigPictureStyle, NotificationCompatInboxStyle, IconCompat
9
+ NotificationCompatBigTextStyle, NotificationCompatBigPictureStyle, NotificationCompatInboxStyle
10
10
 
11
11
 
12
12
 
@@ -60,6 +60,7 @@ def get_image_uri(relative_path):
60
60
 
61
61
  def get_icon_object(uri):
62
62
  context = get_python_activity_context()
63
+ IconCompat = autoclass('android.graphics.drawable.Icon')
63
64
  bitmap = BitmapFactory.decodeStream(context.getContentResolver().openInputStream(uri))
64
65
  return IconCompat.createWithBitmap(bitmap)
65
66
 
@@ -114,9 +115,7 @@ def send_notification(
114
115
  return None
115
116
  context = get_python_activity_context()
116
117
 
117
-
118
- asks_permission_if_needed()
119
-
118
+ asks_permission_if_needed(legacy=True)
120
119
  channel_id = channel_name.replace(' ', '_').lower().lower() if not channel_id else channel_id
121
120
  # Get notification manager
122
121
  notification_manager = context.getSystemService(context.NOTIFICATION_SERVICE)
@@ -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
@@ -54,4 +54,7 @@ def execute_callback(callback,arg, from_who="user"):
54
54
  else:
55
55
  callback()
56
56
  except Exception as on_permissions_result_callback_error:
57
- logger.exception(on_permissions_result_callback_error)
57
+ logger.exception(on_permissions_result_callback_error)
58
+
59
+ def on_pydroid_app():
60
+ return "ru.iiec.pydroid3" in os.path.dirname(os.path.abspath(__file__))