@@ -161,11 +161,67 @@ class AndroidNotification(object):
161
161
in ``title_loc_key`` (optional).
162
162
channel_id: channel_id of the notification (optional).
163
163
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
+
164
217
"""
165
218
166
219
def __init__ (self , title = None , body = None , icon = None , color = None , sound = None , tag = None ,
167
220
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 ):
169
225
self .title = title
170
226
self .body = body
171
227
self .icon = icon
@@ -179,6 +235,36 @@ def __init__(self, title=None, body=None, icon=None, color=None, sound=None, tag
179
235
self .title_loc_args = title_loc_args
180
236
self .channel_id = channel_id
181
237
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
182
268
183
269
184
270
class AndroidFCMOptions (object ):
@@ -503,6 +589,18 @@ def check_string_list(cls, label, value):
503
589
raise ValueError ('{0} must not contain non-string values.' .format (label ))
504
590
return value
505
591
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
+
506
604
@classmethod
507
605
def check_analytics_label (cls , label , value ):
508
606
"""Checks if the given value is a valid analytics label."""
@@ -511,6 +609,15 @@ def check_analytics_label(cls, label, value):
511
609
raise ValueError ('Malformed {}.' .format (label ))
512
610
return value
513
611
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
+
514
621
515
622
class MessageEncoder (json .JSONEncoder ):
516
623
"""A custom JSONEncoder implementation for serializing Message instances into JSON."""
@@ -579,6 +686,32 @@ def encode_ttl(cls, ttl):
579
686
return '{0}.{1}s' .format (seconds , str (nanos ).zfill (9 ))
580
687
return '{0}s' .format (seconds )
581
688
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
+
582
715
@classmethod
583
716
def encode_android_notification (cls , notification ):
584
717
"""Encodes an AndroidNotification instance into JSON."""
@@ -613,19 +746,104 @@ def encode_android_notification(cls, notification):
613
746
'channel_id' : _Validators .check_string (
614
747
'AndroidNotification.channel_id' , notification .channel_id ),
615
748
'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 )
618
768
}
619
769
result = cls .remove_null_values (result )
620
770
color = result .get ('color' )
621
771
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.' )
623
774
if result .get ('body_loc_args' ) and not result .get ('body_loc_key' ):
624
775
raise ValueError (
625
776
'AndroidNotification.body_loc_key is required when specifying body_loc_args.' )
626
777
if result .get ('title_loc_args' ) and not result .get ('title_loc_key' ):
627
778
raise ValueError (
628
779
'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 ]}
629
847
return result
630
848
631
849
@classmethod
0 commit comments