Skip to content

Commit a965841

Browse files
committed
feat(fcm): Add 12 new Android Notification Parameters Support
1 parent c6080e4 commit a965841

File tree

4 files changed

+444
-8
lines changed

4 files changed

+444
-8
lines changed

firebase_admin/_messaging_utils.py

Lines changed: 222 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,67 @@ class AndroidNotification(object):
161161
in ``title_loc_key`` (optional).
162162
channel_id: channel_id of the notification (optional).
163163
image: Image url of the notification (optional).
164+
ticker: Sets the "ticker" text, which is sent to accessibility services. Prior to API
165+
level 21 (Lollipop), sets the text that is displayed in the status bar when the
166+
notification first arrives (optional).
167+
sticky: When set to ``false`` or unset, the notification is automatically dismissed when the
168+
user clicks it in the panel. When set to ``True``, the notification persists even when
169+
the user clicks it (optional).
170+
event_timestamp: For notifications that inform users about events with an absolute time
171+
reference, sets the time that the event in the notification occurred. Notifications
172+
in the panel are sorted by this time (optional).
173+
local_only: Set whether or not this notification is relevant only to the current device.
174+
Some notifications can be bridged to other devices for remote display, such as a Wear OS
175+
watch. This hint can be set to recommend this notification not be bridged (optional).
176+
See Wear OS guides:
177+
https://developer.android.com/training/wearables/notifications/bridger#existing-method-of-preventing-bridging
178+
priority: Set the relative priority for this notification. Low-priority notifications may be
179+
hidden from the user in certain situations. Note this priority differs from
180+
``AndroidMessagePriority``. This priority is processed by the client after the message
181+
has been delivered. Whereas ``AndroidMessagePriority`` is an FCM concept that controls
182+
when the message is delivered (optional). Must be one of ``default``, ``min``, ``low``,
183+
``high``, ``max`` or ``normal``.
184+
vibrate_timings_millis: Set the vibration pattern to use. Pass in an array of seconds
185+
to turn the vibrator on or off (optional). The first value indicates the duration to
186+
wait before turning the vibrator on. The next value indicates the duration to keep the
187+
vibrator on. Subsequent values alternate between duration to turn the vibrator off and
188+
to turn the vibrator on. If ``vibrate_timings_millis`` is set and
189+
``default_vibrate_timings`` is set to ``True``, the default value is used instead of the
190+
user-specified ``vibrate_timings_millis``.
191+
default_vibrate_timings: If set to ``True``, use the Android framework's default vibrate
192+
pattern for the notification (optional). Default values are specified in ``config.xml``
193+
https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml.
194+
If ``default_vibrate_timings`` is set to ``True`` and ``vibrate_timings`` is also set,
195+
the default value is used instead of the user-specified ``vibrate_timings``.
196+
default_sound: If set to ``True``, use the Android framework's default sound for the
197+
notification (optional). Default values are specified in ``config.xml``
198+
https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml
199+
light_settings: Settings to control the notification's LED blinking rate and color if LED is
200+
available on the device. The total blinking time is controlled by the OS (optional).
201+
default_light_settings: If set to ``True``, use the Android framework's default LED light
202+
settings for the notification. Default values are specified in ``config.xml``
203+
https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml.
204+
If ``default_light_settings`` is set to ``True`` and ``light_settings`` is also set, the
205+
user-specified ``light_settings`` is used instead of the default value.
206+
visibility: Set the visibility of the notification. Must be either ``private``, ``public``,
207+
or ``secret``. If unspecified, default to ``private``.
208+
notification_count: Sets the number of items this notification represents. May be displayed
209+
as a badge count for Launchers that support badging. See ``NotificationBadge``
210+
https://developer.android.com/training/notify-user/badges. For example, this might be
211+
useful if you're using just one notification to represent multiple new messages but you
212+
want the count here to represent the number of total new messages. If zero or
213+
unspecified, systems that support badging use the default, which is to increment a
214+
number displayed on the long-press menu each time a new notification arrives.
215+
216+
164217
"""
165218

166219
def __init__(self, title=None, body=None, icon=None, color=None, sound=None, tag=None,
167220
click_action=None, body_loc_key=None, body_loc_args=None, title_loc_key=None,
168-
title_loc_args=None, channel_id=None, image=None):
221+
title_loc_args=None, channel_id=None, image=None, ticker=None, sticky=None,
222+
event_timestamp=None, local_only=None, priority=None, vibrate_timings_millis=None,
223+
default_vibrate_timings=None, default_sound=None, light_settings=None,
224+
default_light_settings=None, visibility=None, notification_count=None):
169225
self.title = title
170226
self.body = body
171227
self.icon = icon
@@ -179,6 +235,36 @@ def __init__(self, title=None, body=None, icon=None, color=None, sound=None, tag
179235
self.title_loc_args = title_loc_args
180236
self.channel_id = channel_id
181237
self.image = image
238+
self.ticker = ticker
239+
self.sticky = sticky
240+
self.event_timestamp = event_timestamp
241+
self.local_only = local_only
242+
self.priority = priority
243+
self.vibrate_timings_millis = vibrate_timings_millis
244+
self.default_vibrate_timings = default_vibrate_timings
245+
self.default_sound = default_sound
246+
self.light_settings = light_settings
247+
self.default_light_settings = default_light_settings
248+
self.visibility = visibility
249+
self.notification_count = notification_count
250+
251+
252+
class LightSettings(object):
253+
"""Represents settings to control notification LED that can be included in a
254+
``messaging.AndroidNotification``.
255+
256+
Args:
257+
color: Set color of the LED in ``#rrggbb`` or ``#rrggbbaa`` format (required).
258+
light_on_duration_millis: Along with ``light_off_duration``, define the blink rate of LED
259+
flashes (required).
260+
light_off_duration_millis: Along with ``light_on_duration``, define the blink rate of LED
261+
flashes (required).
262+
"""
263+
def __init__(self, color=None, light_on_duration_millis=None,
264+
light_off_duration_millis=None):
265+
self.color = color
266+
self.light_on_duration_millis = light_on_duration_millis
267+
self.light_off_duration_millis = light_off_duration_millis
182268

183269

184270
class AndroidFCMOptions(object):
@@ -503,6 +589,18 @@ def check_string_list(cls, label, value):
503589
raise ValueError('{0} must not contain non-string values.'.format(label))
504590
return value
505591

592+
@classmethod
593+
def check_number_list(cls, label, value):
594+
"""Checks if the given value is a list comprised only of numbers."""
595+
if value is None or value == []:
596+
return None
597+
if not isinstance(value, list):
598+
raise ValueError('{0} must be a list of numbers.'.format(label))
599+
non_number = [k for k in value if not isinstance(k, numbers.Number)]
600+
if non_number:
601+
raise ValueError('{0} must not contain non-number values.'.format(label))
602+
return value
603+
506604
@classmethod
507605
def check_analytics_label(cls, label, value):
508606
"""Checks if the given value is a valid analytics label."""
@@ -511,6 +609,15 @@ def check_analytics_label(cls, label, value):
511609
raise ValueError('Malformed {}.'.format(label))
512610
return value
513611

612+
@classmethod
613+
def check_datetime(cls, label, value):
614+
"""Checks if the given value is a datetime."""
615+
if value is None:
616+
return None
617+
if not isinstance(value, datetime.datetime):
618+
raise ValueError('{0} must be a datetime.'.format(label))
619+
return value
620+
514621

515622
class MessageEncoder(json.JSONEncoder):
516623
"""A custom JSONEncoder implementation for serializing Message instances into JSON."""
@@ -579,6 +686,32 @@ def encode_ttl(cls, ttl):
579686
return '{0}.{1}s'.format(seconds, str(nanos).zfill(9))
580687
return '{0}s'.format(seconds)
581688

689+
@classmethod
690+
def encode_milliseconds(cls, label, msec):
691+
"""Encodes a duration in milliseconds into a string."""
692+
if msec is None:
693+
return None
694+
if isinstance(msec, numbers.Number):
695+
msec = datetime.timedelta(milliseconds=msec)
696+
if not isinstance(msec, datetime.timedelta):
697+
raise ValueError('{0} must be a duration in milliseconds or an instance of '
698+
'datetime.timedelta.'.format(label))
699+
total_seconds = msec.total_seconds()
700+
if total_seconds < 0:
701+
raise ValueError('{0} must not be negative.'.format(label))
702+
seconds = int(math.floor(total_seconds))
703+
nanos = int((total_seconds - seconds) * 1e9)
704+
if nanos:
705+
return '{0}.{1}s'.format(seconds, str(nanos).zfill(9))
706+
return '{0}s'.format(seconds)
707+
708+
@classmethod
709+
def encode_boolean(cls, value):
710+
"""Encodes a boolean into JSON."""
711+
if value is None:
712+
return None
713+
return 1 if value else 0
714+
582715
@classmethod
583716
def encode_android_notification(cls, notification):
584717
"""Encodes an AndroidNotification instance into JSON."""
@@ -613,19 +746,104 @@ def encode_android_notification(cls, notification):
613746
'channel_id': _Validators.check_string(
614747
'AndroidNotification.channel_id', notification.channel_id),
615748
'image': _Validators.check_string(
616-
'image', notification.image
617-
)
749+
'image', notification.image),
750+
'ticker': _Validators.check_string(
751+
'AndroidNotification.ticker', notification.ticker),
752+
'sticky': cls.encode_boolean(notification.sticky),
753+
'event_time': _Validators.check_datetime(
754+
'AndroidNotification.event_timestamp', notification.event_timestamp),
755+
'local_only': cls.encode_boolean(notification.local_only),
756+
'notification_priority': _Validators.check_string(
757+
'AndroidNotification.priority', notification.priority, non_empty=True),
758+
'vibrate_timings': _Validators.check_number_list(
759+
'AndroidNotification.vibrate_timings_millis', notification.vibrate_timings_millis),
760+
'default_vibrate_timings': cls.encode_boolean(notification.default_vibrate_timings),
761+
'default_sound': cls.encode_boolean(notification.default_sound),
762+
'default_light_settings': cls.encode_boolean(notification.default_light_settings),
763+
'light_settings': cls.encode_light_settings(notification.light_settings),
764+
'visibility': _Validators.check_string(
765+
'AndroidNotification.visibility', notification.visibility, non_empty=True),
766+
'notification_count': _Validators.check_number(
767+
'AndroidNotification.notification_count', notification.notification_count)
618768
}
619769
result = cls.remove_null_values(result)
620770
color = result.get('color')
621771
if color and not re.match(r'^#[0-9a-fA-F]{6}$', color):
622-
raise ValueError('AndroidNotification.color must be in the form #RRGGBB.')
772+
raise ValueError(
773+
'AndroidNotification.color must be in the form #RRGGBB.')
623774
if result.get('body_loc_args') and not result.get('body_loc_key'):
624775
raise ValueError(
625776
'AndroidNotification.body_loc_key is required when specifying body_loc_args.')
626777
if result.get('title_loc_args') and not result.get('title_loc_key'):
627778
raise ValueError(
628779
'AndroidNotification.title_loc_key is required when specifying title_loc_args.')
780+
781+
event_time = result.get('event_time')
782+
if event_time:
783+
result['event_time'] = str(event_time.isoformat()) + 'Z'
784+
785+
priority = result.get('notification_priority')
786+
if priority and priority not in ('min', 'low', 'default', 'high', 'max'):
787+
raise ValueError('AndroidNotification.priority must be "default", "min", "low", "high" '
788+
'or "max".')
789+
if priority:
790+
result['notification_priority'] = 'PRIORITY_' + priority.upper()
791+
792+
visibility = result.get('visibility')
793+
if visibility and visibility not in ('private', 'public', 'secret'):
794+
raise ValueError(
795+
'AndroidNotification.visibility must be "private", "public" or "secret".')
796+
if visibility:
797+
result['visibility'] = visibility.upper()
798+
799+
vibrate_timings_millis = result.get('vibrate_timings')
800+
if vibrate_timings_millis:
801+
vibrate_timings_secs = []
802+
for msec in vibrate_timings_millis:
803+
formated_string = cls.encode_milliseconds(
804+
'AndroidNotification.vibrate_timings_millis', msec)
805+
vibrate_timings_secs.append(formated_string)
806+
result['vibrate_timings'] = vibrate_timings_secs
807+
return result
808+
809+
@classmethod
810+
def encode_light_settings(cls, light_settings):
811+
"""Encodes a LightSettings instance into JSON."""
812+
if light_settings is None:
813+
return None
814+
if not isinstance(light_settings, LightSettings):
815+
raise ValueError(
816+
'AndroidNotification.light_settings must be an instance of LightSettings class.')
817+
result = {
818+
'color': _Validators.check_string(
819+
'LightSettings.color', light_settings.color, non_empty=True),
820+
'light_on_duration': cls.encode_milliseconds(
821+
'LightSettings.light_on_duration_millis', light_settings.light_on_duration_millis),
822+
'light_off_duration': cls.encode_milliseconds(
823+
'LightSettings.light_off_duration_millis',
824+
light_settings.light_off_duration_millis),
825+
}
826+
result = cls.remove_null_values(result)
827+
color = result.get('color')
828+
if not color:
829+
raise ValueError('LightSettings.color is required.')
830+
light_on_duration = result.get('light_on_duration')
831+
if not light_on_duration:
832+
raise ValueError(
833+
'LightSettings.light_on_duration_millis is required.')
834+
light_off_duration = result.get('light_off_duration')
835+
if not light_off_duration:
836+
raise ValueError(
837+
'LightSettings.light_off_duration_millis is required.')
838+
839+
if not re.match(r'^#[0-9a-fA-F]{6}$', color) and not re.match(r'^#[0-9a-fA-F]{8}$', color):
840+
raise ValueError(
841+
'LightSettings.color must be in the form #aabbcc or aabbccdd.')
842+
if len(color) == 7:
843+
color = (color+'FF')
844+
rgba = [int(color[i:i + 2], 16) / 255. for i in (1, 3, 5, 7)]
845+
result['color'] = {'red': rgba[0], 'green': rgba[1],
846+
'blue': rgba[2], 'alpha': rgba[3]}
629847
return result
630848

631849
@classmethod

firebase_admin/messaging.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
'CriticalSound',
4545
'ErrorInfo',
4646
'FCMOptions',
47+
'LightSettings',
4748
'Message',
4849
'MulticastMessage',
4950
'Notification',
@@ -76,6 +77,7 @@
7677
ApsAlert = _messaging_utils.ApsAlert
7778
CriticalSound = _messaging_utils.CriticalSound
7879
FCMOptions = _messaging_utils.FCMOptions
80+
LightSettings = _messaging_utils.LightSettings
7981
Message = _messaging_utils.Message
8082
MulticastMessage = _messaging_utils.MulticastMessage
8183
Notification = _messaging_utils.Notification

integration/test_messaging.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Integration tests for firebase_admin.messaging module."""
1616

1717
import re
18+
from datetime import datetime
1819

1920
import pytest
2021

@@ -39,7 +40,15 @@ def test_send():
3940
title='android-title',
4041
body='android-body',
4142
image='https://images.unsplash.com/'
42-
'photo-1494438639946-1ebd1d20bf85?fit=crop&w=900&q=60'
43+
'photo-1494438639946-1ebd1d20bf85?fit=crop&w=900&q=60',
44+
event_timestamp=datetime.now(),
45+
priority='high', vibrate_timings_millis=[100, 200, 300, 400],
46+
visibility='public', light_settings=messaging.LightSettings(
47+
color='#aabbcc',
48+
light_off_duration_millis=200,
49+
light_on_duration_millis=300
50+
),
51+
notification_count=1
4352
)
4453
),
4554
apns=messaging.APNSConfig(payload=messaging.APNSPayload(

0 commit comments

Comments
 (0)