Skip to content

Commit b1985cd

Browse files
committed
sync passed basic test
1 parent 549a425 commit b1985cd

8 files changed

Lines changed: 199 additions & 75 deletions

File tree

android_notify/internal/android.py

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
"""
22
Android related logic
33
"""
4-
import time
4+
import time, os
55

66
from .logger import logger
77
from ..config import get_notification_manager, on_android_platform, from_service_file, on_flet_app, \
88
get_python_activity_context
99
from .permissions import has_notification_permission
1010
from .java_classes import autoclass, BuildVersion, Uri, NotificationCompat, NotificationManagerCompat, \
11-
NotificationManager, Context
11+
NotificationManager, Context, File
1212
from .an_types import Importance
1313

1414

@@ -98,22 +98,72 @@ def get_sound_uri(res_sound_name):
9898
return Uri.parse(f"android.resource://{package_name}/raw/{res_sound_name}")
9999

100100

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):
102144
"""
103145
Sets sound for devices less than android 8 (For 8+ use createChannel)
104146
:param builder: builder instance
105147
: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
106149
"""
107150

108151
if not on_android_platform():
109152
return None
110153

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)
117167
return None
118168

119169

android_notify/internal/channels.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55

66
from android_notify.config import get_notification_manager, on_android_platform
77

8-
from android_notify.internal.java_classes import BuildVersion, NotificationChannel
8+
from android_notify.internal.java_classes import BuildVersion, NotificationChannel, AudioAttributes, AudioAttributesBuilder
99
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
1111
from android_notify.internal.logger import logger
1212

1313
def does_channel_exist(channel_id):
@@ -24,35 +24,48 @@ def does_channel_exist(channel_id):
2424
return False
2525

2626

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):
2828
"""
2929
Creates a user visible toggle button for specific notifications, Required For Android 8.0+
3030
:param id__: Used to send other notifications later through same channel.
3131
:param name: user-visible channel name.
3232
:param description: user-visible detail about channel (Not required defaults to empty str).
3333
:param importance: ['urgent', 'high', 'medium', 'low', 'none'] defaults to 'urgent' i.e. makes a sound and shows briefly
3434
: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
3536
:param vibrate: if channel notifications should vibrate or not
3637
:return: boolean if channel created
3738
"""
3839
def info_log():
3940
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}")
4142

4243
if not on_android_platform():
4344
info_log()
4445
return None
4546

4647
notification_manager = get_notification_manager()
4748
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)
4954

5055
if not does_channel_exist(id__):
5156
channel = NotificationChannel(id__, name, android_importance_value)
5257
if description:
5358
channel.setDescription(description)
5459
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)
5669
if vibrate:
5770
# channel.setVibrationPattern([0, 500, 200, 500]) # Using Phone's default pattern
5871
# Android 15 ignored long patterns, didn't vibrate when not in silent and

android_notify/internal/facade.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,9 @@ def getAbsolutePath(self):
149149
logger.debug(f"[MOCK] File.getAbsolutePath called, returning {self.path}")
150150
return self.path
151151

152-
153152
class NotificationManager:
154153
pass
155154

156-
157155
class NotificationChannel:
158156
def __init__(self, channel_id, channel_name, importance):
159157
self.description = None

android_notify/internal/helper.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""Collection of useful functions"""
22

33
import inspect, os, re
4-
from .logger import logger
5-
4+
from .logger import logger, on_android_platform
5+
from android_notify.config import get_package_name
66

77
def can_accept_arguments(func, *args, **kwargs):
88
try:
@@ -57,4 +57,21 @@ def execute_callback(callback,arg, from_who="user"):
5757
logger.exception(on_permissions_result_callback_error)
5858

5959
def on_pydroid_app():
60-
return "ru.iiec.pydroid3" in os.path.dirname(os.path.abspath(__file__))
60+
package_name = "ru.iiec.pydroid3"
61+
if package_name in os.environ.get("PYTHONHOME",""):
62+
return True
63+
elif package_name in os.path.dirname(os.path.abspath(__file__)):
64+
return True
65+
elif on_android_platform():
66+
return package_name == get_package_name()
67+
return False
68+
69+
70+
def has_androidx_dependency():
71+
"""Check if androidx dependencies are available"""
72+
try:
73+
from jnius import autoclass
74+
autoclass('androidx.core.app.NotificationCompat')
75+
return True
76+
except Exception:
77+
return False

android_notify/internal/java_classes.py

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22

3+
from .helper import has_androidx_dependency, on_pydroid_app
34
from .logger import logger
45

56
def on_android_platform():
@@ -12,14 +13,16 @@ def on_android_platform():
1213
return True
1314
return os.getenv("MAIN_ACTIVITY_HOST_CLASS_NAME")
1415

16+
def on_flet_app():
17+
return os.getenv("MAIN_ACTIVITY_HOST_CLASS_NAME")
1518

1619
if on_android_platform():
1720
try:
1821
from jnius import cast, autoclass
1922
except ModuleNotFoundError:
2023
cast = lambda x, y: x
2124
autoclass = lambda x: None
22-
logger.exception("run pip install pyjnius")
25+
logger.exception("add pyjnius to dependencies list")
2326

2427
# Leaving this as a broad Exception for unforeseen case so apps don't crash
2528
# noinspection PyBroadException
@@ -38,32 +41,55 @@ def on_android_platform():
3841
Uri = autoclass("android.net.Uri")
3942
Manifest = autoclass('android.Manifest$permission')
4043
Context = autoclass('android.content.Context')
44+
PackageManager = autoclass("android.content.pm.PackageManager")
45+
AudioAttributes = autoclass('android.media.AudioAttributes')
46+
AudioAttributesBuilder = autoclass('android.media.AudioAttributes$Builder')
47+
File = autoclass('java.io.File')
4148
Color = autoclass('android.graphics.Color')
42-
IconClass = autoclass('androidx.core.graphics.drawable.IconCompat')
4349
except Exception as e:
4450
from .facade import *
4551
logger.exception("Didn't get Basic Java Classes")
4652

47-
# noinspection PyBroadException
48-
try:
49-
NotificationManagerCompat = autoclass('androidx.core.app.NotificationManagerCompat')
50-
NotificationCompat = autoclass('androidx.core.app.NotificationCompat')
51-
NotificationCompatBuilder = autoclass('androidx.core.app.NotificationCompat$Builder')
53+
if on_flet_app() or on_pydroid_app() or not has_androidx_dependency():
54+
# Leaving this as a broad Exception for unforeseen case so apps don't crash
55+
# noinspection PyBroadException
56+
try:
57+
IconClass = autoclass('android.graphics.drawable.Icon')
58+
NotificationCompat = autoclass("android.app.Notification")
59+
NotificationManagerCompat = autoclass('android.app.NotificationManager')
60+
NotificationCompatBuilder = autoclass('android.app.Notification$Builder')
5261

53-
# Notification Design
54-
NotificationCompatBigTextStyle = autoclass('androidx.core.app.NotificationCompat$BigTextStyle')
55-
NotificationCompatBigPictureStyle = autoclass('androidx.core.app.NotificationCompat$BigPictureStyle')
56-
NotificationCompatInboxStyle = autoclass('androidx.core.app.NotificationCompat$InboxStyle')
57-
NotificationCompatDecoratedCustomViewStyle = autoclass('androidx.core.app.NotificationCompat$DecoratedCustomViewStyle')
62+
NotificationCompatBigTextStyle = autoclass('android.app.Notification$BigTextStyle')
63+
NotificationCompatBigPictureStyle = autoclass('android.app.Notification$BigPictureStyle')
64+
NotificationCompatInboxStyle = autoclass('android.app.Notification$InboxStyle')
65+
NotificationCompatDecoratedCustomViewStyle = autoclass('android.app.Notification$DecoratedCustomViewStyle')
66+
except Exception as styles_import_error:
67+
logger.exception(styles_import_error)
68+
from .facade import *
69+
elif has_androidx_dependency():
5870

59-
except Exception as dependencies_import_error:
60-
logger.exception("""
61-
Dependency Error: Add the following in buildozer.spec:
62-
* android.gradle_dependencies = androidx.core:core:1.12.0
63-
* android.enable_androidx = True
64-
""")
71+
# Leaving this as a broad Exception for unforeseen case so apps don't crash
72+
# noinspection PyBroadException
73+
try:
74+
IconClass = autoclass('androidx.core.graphics.drawable.IconCompat')
75+
NotificationCompat = autoclass('androidx.core.app.NotificationCompat')
76+
NotificationManagerCompat = autoclass('androidx.core.app.NotificationManagerCompat')
77+
NotificationCompatBuilder = autoclass('androidx.core.app.NotificationCompat$Builder')
6578

66-
from .facade import *
79+
# Notification Design
80+
NotificationCompatBigTextStyle = autoclass('androidx.core.app.NotificationCompat$BigTextStyle')
81+
NotificationCompatBigPictureStyle = autoclass('androidx.core.app.NotificationCompat$BigPictureStyle')
82+
NotificationCompatInboxStyle = autoclass('androidx.core.app.NotificationCompat$InboxStyle')
83+
NotificationCompatDecoratedCustomViewStyle = autoclass('androidx.core.app.NotificationCompat$DecoratedCustomViewStyle')
84+
85+
except Exception as dependencies_import_error:
86+
logger.exception("""
87+
Dependency Error: Add the following in buildozer.spec:
88+
* android.gradle_dependencies = androidx.core:core:1.12.0
89+
* android.enable_androidx = True
90+
""")
91+
92+
from .facade import *
6793
else:
6894
cast = lambda x, y: x
6995
autoclass = lambda x: None

0 commit comments

Comments
 (0)