Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 43 additions & 3 deletions android_notify/internal/android.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from ..config import get_notification_manager, on_android_platform, from_service_file, on_flet_app, \
get_python_activity_context
from .permissions import has_notification_permission
from .java_classes import BuildVersion, Uri, NotificationCompat, NotificationManagerCompat
from .java_classes import autoclass, BuildVersion, Uri, NotificationCompat, NotificationManagerCompat, \
NotificationManager, Context
from .an_types import Importance


Expand Down Expand Up @@ -75,7 +76,7 @@ def show_infinite_progressbar(builder):

if on_android_platform():
builder.setProgress(0, 0, True)

logger.info('Showing infinite progressbar.')


Expand All @@ -97,7 +98,6 @@ def get_sound_uri(res_sound_name):
return Uri.parse(f"android.resource://{package_name}/raw/{res_sound_name}")



def set_sound(builder, res_sound_name):
"""
Sets sound for devices less than android 8 (For 8+ use createChannel)
Expand Down Expand Up @@ -140,3 +140,43 @@ def get_android_importance(importance: Importance):
return value
# side-note 'medium' = NotificationCompat.PRIORITY_LOW and 'low' = NotificationCompat.PRIORITY_MIN # weird but from docs


def do_not_disturb_on():
if not on_android_platform():
return None
try:
nm = get_python_activity_context().getSystemService(Context.NOTIFICATION_SERVICE)
mode = nm.getCurrentInterruptionFilter()
return mode != NotificationManager.INTERRUPTION_FILTER_ALL
except Exception as error_getting_do_not_disturb_state:
logger.exception(error_getting_do_not_disturb_state)


def force_vibrate(repeat=False):
if not on_android_platform():
return None

context = get_python_activity_context()
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE)

if vibrator is None or not vibrator.hasVibrator():
logger.warning("No vibrator available")
return None

if do_not_disturb_on():
logger.warning("Do not disturb is on")

AudioAttributes = autoclass('android.media.AudioAttributes')
AudioAttributesBuilder = autoclass('android.media.AudioAttributes$Builder')
pattern = [0, 500] # vibrate pattern for once trying to replicate regular notification vibration.

if BuildVersion.SDK_INT >= 26:
VibrationEffect = autoclass('android.os.VibrationEffect')
effect = VibrationEffect.createWaveform(pattern, -1 if not repeat else 0)
attributes = AudioAttributesBuilder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).setUsage(
AudioAttributes.USAGE_ALARM).build()
vibrator.vibrate(effect, attributes)
return None
else:
vibrator.vibrate(pattern, -1 if not repeat else 0)
return None
18 changes: 17 additions & 1 deletion android_notify/internal/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from android_notify.internal.java_classes import BuildVersion, NotificationChannel
from android_notify.internal.an_types import Importance
from android_notify.internal.android import get_sound_uri, get_android_importance
from android_notify.internal.logger import logger

def does_channel_exist(channel_id):
"""
Expand All @@ -23,17 +24,23 @@ def does_channel_exist(channel_id):
return False


def create_channel(id__, name: str, description='', importance: Importance = 'urgent', res_sound_name=None):
def create_channel(id__, name: str, description='', importance: Importance = 'urgent', res_sound_name=None,vibrate=False):
"""
Creates a user visible toggle button for specific notifications, Required For Android 8.0+
:param id__: Used to send other notifications later through same channel.
:param name: user-visible channel name.
:param description: user-visible detail about channel (Not required defaults to empty str).
:param importance: ['urgent', 'high', 'medium', 'low', 'none'] defaults to 'urgent' i.e. makes a sound and shows briefly
:param res_sound_name: audio file name (without .wav or .mp3) locate in res/raw/
:param vibrate: if channel notifications should vibrate or not
:return: boolean if channel created
"""
def info_log():
logger.info(
f"Created {name} channel, id: {id__}, description: {description}, res_sound_name: {res_sound_name},vibrate: {vibrate}")

if not on_android_platform():
info_log()
return None

notification_manager = get_notification_manager()
Expand All @@ -46,8 +53,17 @@ def create_channel(id__, name: str, description='', importance: Importance = 'ur
channel.setDescription(description)
if sound_uri:
channel.setSound(sound_uri, None)
if vibrate:
# channel.setVibrationPattern([0, 500, 200, 500]) # Using Phone's default pattern
# Android 15 ignored long patterns, didn't vibrate when not in silent and
# conflicting channel names got the same vibrate state even with different ids
# IMPORTANCE_LOW didn't vibrate but didn't show heads-up
channel.enableVibration(bool(vibrate))
notification_manager.createNotificationChannel(channel)
info_log()
return True
else:
logger.debug(f"{id__} channel already exists")
return False


Expand Down
9 changes: 8 additions & 1 deletion android_notify/internal/facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,13 @@ def getId(self):
return self.channel_id

def setSound(self, sound_uri, _):
pass
logger.debug(f"[MOCK] NotificationChannel.setSound called, sound_uri={sound_uri}")

def enableVibration(self,state):
logger.debug(f"[MOCK] NotificationChannel.enableVibration called, state={state}")

def setVibrationPattern(self,list_of_numbers):
logger.debug(f"[MOCK] NotificationChannel.setVibrationPattern called, list_of_numbers={list_of_numbers}")
class IconCompat:
@classmethod
def createWithBitmap(cls, bitmap):
Expand Down Expand Up @@ -252,6 +256,9 @@ def setSubText(self, text):
def setColor(self, color: Color) -> None:
logger.debug(f"[MOCK] setColor called with color={color}")

def setVibrate(self, state) -> None:
logger.debug(f"[MOCK] setVibrate called with state={state}")


class NotificationCompatBigTextStyle:
def bigText(self, body):
Expand Down
33 changes: 14 additions & 19 deletions android_notify/internal/intents.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,12 @@ def add_intent_to_open_app(builder, action_name, notification_title, notificatio
# intent.addCategory(Intent.CATEGORY_LAUNCHER) # Adds the launcher category so Android treats it as a launcher app intent and properly manages the task/back stack.

add_data_to_intent(intent, notification_title, notification_id, str(action_name))
logger.debug(f'data for intent: {notification_title}, id: {notification_id}, name: {action_name}')
pending_intent = PendingIntent.getActivity(
context, notification_id,
intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
)
builder.setContentIntent(pending_intent)
logger.debug(f'data for opening app- notification_title: {notification_title}, notification_id: {notification_id}, notification_name: {action_name}')



Expand All @@ -88,25 +88,25 @@ def get_intent_used_to_open_app():
# intent = activity.getIntent()
# try:
# extras = intent.getExtras()
# rint(extras, 11)
# drint(extras, 11)
# if extras:
# for key in extras.keySet().toArray():
# value = extras.get(key)
# rint(key, value)
# rint('start Up Title --->', intent.getStringExtra("notification_title"))
# drint(key, value)
# drint('start Up Title --->', intent.getStringExtra("notification_title"))
# except Exception as error_in_loop:
# rint(error_in_loop)
# drint(error_in_loop)
#
#
# try:
# action = intent.getAction()
# rint('Start up Intent ----', action)
# drint('Start up Intent ----', action)
# except Exception as error_getting_action:
# rint("error_getting_action",error_getting_action)
# drint("error_getting_action",error_getting_action)
#
#
# except Exception as error_getting_notify_name:
# rint("Error getting xxxxx name:", error_getting_notify_name)
# drint("Error getting name1:", error_getting_notify_name)

# TODO action Doesn't change even not opened from notification
try:
Expand All @@ -115,17 +115,12 @@ def get_intent_used_to_open_app():
extras = intent.getExtras()
if extras:
name = extras.getString("notification_name")
logger.debug(f"fallback notification_name: {name}")
#
# rint("notification_id:", extras.getInt("notification_id"))
# for key in extras.keySet().toArray():
# value = extras.get(key)
# logger.debug(f"key: {key}, value: {value}")
else:
logger.warning(f"Did not find notification_name no extras in intent, Using action value")
name = intent.getAction()

logger.debug(f"fallback action: {intent.getAction()}")
# logger.debug(f"fallback notification_name: {name}")
if not name:
action = name = intent.getAction()
logger.warning(f"Did not find notification name no extras in intent, Using action value: {action}")

# logger.debug(f"fallback action: {intent.getAction()}")
except Exception as error_getting_notification_name:
logger.exception(error_getting_notification_name)

Expand Down
1 change: 1 addition & 0 deletions android_notify/internal/java_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def on_android_platform():
Uri = autoclass("android.net.Uri")
Manifest = autoclass('android.Manifest$permission')
Color = autoclass('android.graphics.Color')
Context = autoclass('android.content.Context')
except Exception as e:
from .facade import *
logger.exception("Didn't get Basic Java Classes")
Expand Down
4 changes: 2 additions & 2 deletions android_notify/internal/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ def format(self, record):


logger = logging.getLogger("android_notify")
logger.setLevel(logging.NOTSET)
# logger.setLevel(logging.NOTSET) # this override app logger level

handler = logging.StreamHandler(sys.stdout)
formatter = KivyColorFormatter()
handler.setFormatter(formatter)
handler.setLevel(logging.WARNING) # respect logger level
# handler.setLevel(logging.WARNING) # this override app logger level

# Avoid duplicate logs if root logger is configured
logger.propagate = False
Expand Down
48 changes: 42 additions & 6 deletions android_notify/sword.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from .config import from_service_file, get_notification_manager, on_flet_app, get_package_name, run_on_ui_thread, \
get_python_activity_context, on_android_platform
from .internal.android import cancel_all_notifications, cancel_notifications, dispatch_notification, \
set_when, show_infinite_progressbar, remove_buttons, set_sound, get_android_importance
set_when, show_infinite_progressbar, remove_buttons, set_sound, get_android_importance, force_vibrate

# Types
from .internal.an_types import Importance
Expand Down Expand Up @@ -131,17 +131,18 @@ def channelExists(cls, channel_id):
return does_channel_exist(channel_id=channel_id)

@classmethod
def createChannel(cls, id, name: str, description='', importance: Importance = 'urgent', res_sound_name=None):
def createChannel(cls, id, name: str, description='', importance: Importance = 'urgent', res_sound_name=None, vibrate=False):
"""
Creates a user visible toggle button for specific notifications, Required For Android 8.0+
:param id: Used to send other notifications later through same channel.
:param name: user-visible channel name.
:param description: user-visible detail about channel (Not required defaults to empty str).
:param importance: ['urgent', 'high', 'medium', 'low', 'none'] defaults to 'urgent' i.e. makes a sound and shows briefly
:param res_sound_name: audio file name (without .wav or .mp3) locate in res/raw/
:param vibrate: if channel notifications should vibrate or not
:return: boolean if channel created
"""
return create_channel(id, name, description, importance, res_sound_name)
return create_channel(id__=id, name=name, description=description, importance=importance, res_sound_name=res_sound_name, vibrate=vibrate)

@classmethod
def deleteChannel(cls, channel_id):
Expand Down Expand Up @@ -413,6 +414,41 @@ def send_(self, silent: bool = False, persistent=False, close_on_click=True):
self.passed_check = True
self.send(silent, persistent, close_on_click)

def setVibrate(self, pattern=None):
"""
Set the vibration pattern for the notification (Android API < 26 only).

On devices running Android versions prior to 8.0 (Oreo),
vibration is configured directly on the notification builder.
This method is ignored on API 26+ where NotificationChannel
controls vibration behavior.

Args:
pattern (list[int] | None, optional):
A vibration pattern in milliseconds formatted as:
[delay, vibrate, pause, vibrate, ...].

If not provided, the default pattern
[0, 500] is used.

Example:
>>> self.setVibrate()
>>> self.setVibrate([0, 500, 200, 500])
"""
if on_android_platform() and BuildVersion < 26:
pattern = pattern or [0, 500]
self.builder.setVibrate(pattern)
if not on_android_platform() or BuildVersion < 26:
logger.info(f"Vibration pattern set to {pattern}")

@staticmethod
def fVibrate():
"""
Some Android devices have a setting to only vibrate on silent, If Vibration is a MUST called this.
:return:
"""
force_vibrate()

def __send_logs(self):
if not self.logs:
return
Expand Down Expand Up @@ -560,7 +596,7 @@ def __applyNewLinesIfAny(self):

def __create_basic_notification(self, persistent, close_on_click):
if not self.channelExists(self.channel_id):
self.createChannel(self.channel_id, self.channel_name)
self.createChannel(id=self.channel_id, name=self.channel_name)
elif not self.__using_set_priority_method:
self.setPriority('medium' if self.silent else 'urgent')

Expand Down Expand Up @@ -790,12 +826,12 @@ def bindNotifyListener(cls):
return None

if on_flet_app():
logger.warning('On Flet App, Didnt Binding Notification Listener')
logger.warning("On Flet App, Didn't Binding Notification Listener")
return None

if from_service_file():
# In Service File error 'NoneType' object has no attribute 'registerNewIntentListener'
logger.warning("In service file, Didnt Binding Notification Listener")
logger.warning("In service file, Didn't Binding Notification Listener")
return None

# TODO use BroadcastReceiver For Whole notification Click Not Just Buttons
Expand Down
6 changes: 2 additions & 4 deletions android_notify/widgets/texts.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ def set_title(builder, title, using_layout=False):
:param title: New Notification Title
:param using_layout: Whether to use layout or not
"""
title = String(title)

if not on_android_platform():
return None
Expand All @@ -57,7 +56,7 @@ def set_title(builder, title, using_layout=False):
pass
# self.__apply_basic_custom_style()
else:
builder.setContentTitle(title)
builder.setContentTitle(String(title))

logger.info(f'new notification title: {title}')
return None
Expand All @@ -70,7 +69,6 @@ def set_message(builder, message, using_layout=False):
:param message: New Notification message
:param using_layout: Whether to use layout or not
"""
message = String(message)

if not on_android_platform():
return None
Expand All @@ -79,7 +77,7 @@ def set_message(builder, message, using_layout=False):
pass
# self.__apply_basic_custom_style()
else:
builder.setContentText(message)
builder.setContentText(String(message))

logger.info(f'new notification message: {message}')
return None
Expand Down