@@ -48,6 +48,8 @@ const (
4848 serverUnavailable = "server-unavailable"
4949 tooManyTopics = "too-many-topics"
5050 unknownError = "unknown-error"
51+
52+ rfc3339Zulu = "2006-01-02T15:04:05.000000000Z"
5153)
5254
5355var (
@@ -176,13 +178,7 @@ type AndroidConfig struct {
176178func (a * AndroidConfig ) MarshalJSON () ([]byte , error ) {
177179 var ttl string
178180 if a .TTL != nil {
179- seconds := int64 (* a .TTL / time .Second )
180- nanos := int64 ((* a .TTL - time .Duration (seconds )* time .Second ) / time .Nanosecond )
181- if nanos > 0 {
182- ttl = fmt .Sprintf ("%d.%09ds" , seconds , nanos )
183- } else {
184- ttl = fmt .Sprintf ("%ds" , seconds )
185- }
181+ ttl = durationToString (* a .TTL )
186182 }
187183
188184 type androidInternal AndroidConfig
@@ -209,42 +205,340 @@ func (a *AndroidConfig) UnmarshalJSON(b []byte) error {
209205 return err
210206 }
211207 if temp .TTL != "" {
212- segments := strings .Split (strings .TrimSuffix (temp .TTL , "s" ), "." )
213- if len (segments ) != 1 && len (segments ) != 2 {
214- return fmt .Errorf ("incorrect number of segments in ttl: %q" , temp .TTL )
215- }
216- seconds , err := strconv .ParseInt (segments [0 ], 10 , 64 )
208+ ttl , err := stringToDuration (temp .TTL )
217209 if err != nil {
218210 return err
219211 }
220- ttl := time .Duration (seconds ) * time .Second
221- if len (segments ) == 2 {
222- nanos , err := strconv .ParseInt (strings .TrimLeft (segments [1 ], "0" ), 10 , 64 )
223- if err != nil {
224- return err
225- }
226- ttl += time .Duration (nanos ) * time .Nanosecond
227- }
228212 a .TTL = & ttl
229213 }
230214 return nil
231215}
232216
233217// AndroidNotification is a notification to send to Android devices.
234218type AndroidNotification struct {
235- Title string `json:"title,omitempty"` // if specified, overrides the Title field of the Notification type
236- Body string `json:"body,omitempty"` // if specified, overrides the Body field of the Notification type
237- Icon string `json:"icon,omitempty"`
238- Color string `json:"color,omitempty"` // notification color in #RRGGBB format
239- Sound string `json:"sound,omitempty"`
240- Tag string `json:"tag,omitempty"`
241- ClickAction string `json:"click_action,omitempty"`
242- BodyLocKey string `json:"body_loc_key,omitempty"`
243- BodyLocArgs []string `json:"body_loc_args,omitempty"`
244- TitleLocKey string `json:"title_loc_key,omitempty"`
245- TitleLocArgs []string `json:"title_loc_args,omitempty"`
246- ChannelID string `json:"channel_id,omitempty"`
247- ImageURL string `json:"image,omitempty"`
219+ Title string `json:"title,omitempty"` // if specified, overrides the Title field of the Notification type
220+ Body string `json:"body,omitempty"` // if specified, overrides the Body field of the Notification type
221+ Icon string `json:"icon,omitempty"`
222+ Color string `json:"color,omitempty"` // notification color in #RRGGBB format
223+ Sound string `json:"sound,omitempty"`
224+ Tag string `json:"tag,omitempty"`
225+ ClickAction string `json:"click_action,omitempty"`
226+ BodyLocKey string `json:"body_loc_key,omitempty"`
227+ BodyLocArgs []string `json:"body_loc_args,omitempty"`
228+ TitleLocKey string `json:"title_loc_key,omitempty"`
229+ TitleLocArgs []string `json:"title_loc_args,omitempty"`
230+ ChannelID string `json:"channel_id,omitempty"`
231+ ImageURL string `json:"image,omitempty"`
232+ Ticker string `json:"ticker,omitempty"`
233+ Sticky bool `json:"sticky,omitempty"`
234+ EventTimestamp * time.Time `json:"-"`
235+ LocalOnly bool `json:"local_only,omitempty"`
236+ Priority AndroidNotificationPriority `json:"-"`
237+ VibrateTimingMillis []int64 `json:"-"`
238+ DefaultVibrateTimings bool `json:"default_vibrate_timings,omitempty"`
239+ DefaultSound bool `json:"default_sound,omitempty"`
240+ LightSettings * LightSettings `json:"light_settings,omitempty"`
241+ DefaultLightSettings bool `json:"default_light_settings,omitempty"`
242+ Visibility AndroidNotificationVisibility `json:"-"`
243+ NotificationCount * int `json:"notification_count,omitempty"`
244+ }
245+
246+ // MarshalJSON marshals an AndroidNotification into JSON (for internal use only).
247+ func (a * AndroidNotification ) MarshalJSON () ([]byte , error ) {
248+ var priority string
249+ if a .Priority != priorityUnspecified {
250+ priorities := map [AndroidNotificationPriority ]string {
251+ PriorityMin : "PRIORITY_MIN" ,
252+ PriorityLow : "PRIORITY_LOW" ,
253+ PriorityDefault : "PRIORITY_DEFAULT" ,
254+ PriorityHigh : "PRIORITY_HIGH" ,
255+ PriorityMax : "PRIORITY_MAX" ,
256+ }
257+ priority , _ = priorities [a .Priority ]
258+ }
259+
260+ var visibility string
261+ if a .Visibility != visibilityUnspecified {
262+ visibilities := map [AndroidNotificationVisibility ]string {
263+ VisibilityPrivate : "PRIVATE" ,
264+ VisibilityPublic : "PUBLIC" ,
265+ VisibilitySecret : "SECRET" ,
266+ }
267+ visibility , _ = visibilities [a .Visibility ]
268+ }
269+
270+ var timestamp string
271+ if a .EventTimestamp != nil {
272+ timestamp = a .EventTimestamp .UTC ().Format (rfc3339Zulu )
273+ }
274+
275+ var vibTimings []string
276+ for _ , t := range a .VibrateTimingMillis {
277+ vibTimings = append (vibTimings , durationToString (time .Duration (t )* time .Millisecond ))
278+ }
279+
280+ type androidInternal AndroidNotification
281+ temp := & struct {
282+ EventTimestamp string `json:"event_time,omitempty"`
283+ Priority string `json:"notification_priority,omitempty"`
284+ Visibility string `json:"visibility,omitempty"`
285+ VibrateTimings []string `json:"vibrate_timings,omitempty"`
286+ * androidInternal
287+ }{
288+ EventTimestamp : timestamp ,
289+ Priority : priority ,
290+ Visibility : visibility ,
291+ VibrateTimings : vibTimings ,
292+ androidInternal : (* androidInternal )(a ),
293+ }
294+ return json .Marshal (temp )
295+ }
296+
297+ // UnmarshalJSON unmarshals a JSON string into an AndroidNotification (for internal use only).
298+ func (a * AndroidNotification ) UnmarshalJSON (b []byte ) error {
299+ type androidInternal AndroidNotification
300+ temp := struct {
301+ EventTimestamp string `json:"event_time,omitempty"`
302+ Priority string `json:"notification_priority,omitempty"`
303+ Visibility string `json:"visibility,omitempty"`
304+ VibrateTimings []string `json:"vibrate_timings,omitempty"`
305+ * androidInternal
306+ }{
307+ androidInternal : (* androidInternal )(a ),
308+ }
309+ if err := json .Unmarshal (b , & temp ); err != nil {
310+ return err
311+ }
312+
313+ if temp .Priority != "" {
314+ priorities := map [string ]AndroidNotificationPriority {
315+ "PRIORITY_MIN" : PriorityMin ,
316+ "PRIORITY_LOW" : PriorityLow ,
317+ "PRIORITY_DEFUALT" : PriorityDefault ,
318+ "PRIORITY_HIGH" : PriorityHigh ,
319+ "PRIORITY_MAX" : PriorityMax ,
320+ }
321+ if prio , ok := priorities [temp .Priority ]; ok {
322+ a .Priority = prio
323+ } else {
324+ return fmt .Errorf ("unknown priority value: %q" , temp .Priority )
325+ }
326+ }
327+
328+ if temp .Visibility != "" {
329+ visibilities := map [string ]AndroidNotificationVisibility {
330+ "PRIVATE" : VisibilityPrivate ,
331+ "PUBLIC" : VisibilityPublic ,
332+ "SECRET" : VisibilitySecret ,
333+ }
334+ if vis , ok := visibilities [temp .Visibility ]; ok {
335+ a .Visibility = vis
336+ } else {
337+ return fmt .Errorf ("unknown visibility value: %q" , temp .Visibility )
338+ }
339+ }
340+
341+ if temp .EventTimestamp != "" {
342+ ts , err := time .Parse (rfc3339Zulu , temp .EventTimestamp )
343+ if err != nil {
344+ return err
345+ }
346+
347+ a .EventTimestamp = & ts
348+ }
349+
350+ var vibTimings []int64
351+ for _ , t := range temp .VibrateTimings {
352+ vibTime , err := stringToDuration (t )
353+ if err != nil {
354+ return err
355+ }
356+
357+ millis := int64 (vibTime / time .Millisecond )
358+ vibTimings = append (vibTimings , millis )
359+ }
360+ a .VibrateTimingMillis = vibTimings
361+ return nil
362+ }
363+
364+ // AndroidNotificationPriority represents the priority levels of a notification.
365+ type AndroidNotificationPriority int
366+
367+ const (
368+ priorityUnspecified AndroidNotificationPriority = iota
369+
370+ // PriorityMin is the lowest notification priority. Notifications with this priority might not
371+ // be shown to the user except under special circumstances, such as detailed notification logs.
372+ PriorityMin
373+
374+ // PriorityLow is a lower notification priority. The UI may choose to show the notifications
375+ // smaller, or at a different position in the list, compared with notifications with PriorityDefault.
376+ PriorityLow
377+
378+ // PriorityDefault is the default notification priority. If the application does not prioritize
379+ // its own notifications, use this value for all notifications.
380+ PriorityDefault
381+
382+ // PriorityHigh is a higher notification priority. Use this for more important
383+ // notifications or alerts. The UI may choose to show these notifications larger, or at a
384+ // different position in the notification lists, compared with notifications with PriorityDefault.
385+ PriorityHigh
386+
387+ // PriorityMax is the highest notification priority. Use this for the application's most
388+ // important items that require the user's prompt attention or input.
389+ PriorityMax
390+ )
391+
392+ // AndroidNotificationVisibility represents the different visibility levels of a notification.
393+ type AndroidNotificationVisibility int
394+
395+ const (
396+ visibilityUnspecified AndroidNotificationVisibility = iota
397+
398+ // VisibilityPrivate shows this notification on all lockscreens, but conceal sensitive or
399+ // private information on secure lockscreens.
400+ VisibilityPrivate
401+
402+ // VisibilityPublic shows this notification in its entirety on all lockscreens.
403+ VisibilityPublic
404+
405+ // VisibilitySecret does not reveal any part of this notification on a secure lockscreen.
406+ VisibilitySecret
407+ )
408+
409+ // LightSettings to control notification LED.
410+ type LightSettings struct {
411+ Color string
412+ LightOnDurationMillis int64
413+ LightOffDurationMillis int64
414+ }
415+
416+ // MarshalJSON marshals an LightSettings into JSON (for internal use only).
417+ func (l * LightSettings ) MarshalJSON () ([]byte , error ) {
418+ clr , err := newColor (l .Color )
419+ if err != nil {
420+ return nil , err
421+ }
422+
423+ temp := struct {
424+ Color * color `json:"color"`
425+ LightOnDuration string `json:"light_on_duration"`
426+ LightOffDuration string `json:"light_off_duration"`
427+ }{
428+ Color : clr ,
429+ LightOnDuration : durationToString (time .Duration (l .LightOnDurationMillis ) * time .Millisecond ),
430+ LightOffDuration : durationToString (time .Duration (l .LightOffDurationMillis ) * time .Millisecond ),
431+ }
432+ return json .Marshal (temp )
433+ }
434+
435+ // UnmarshalJSON unmarshals a JSON string into an LightSettings (for internal use only).
436+ func (l * LightSettings ) UnmarshalJSON (b []byte ) error {
437+ temp := struct {
438+ Color * color `json:"color"`
439+ LightOnDuration string `json:"light_on_duration"`
440+ LightOffDuration string `json:"light_off_duration"`
441+ }{}
442+ if err := json .Unmarshal (b , & temp ); err != nil {
443+ return err
444+ }
445+
446+ on , err := stringToDuration (temp .LightOnDuration )
447+ if err != nil {
448+ return err
449+ }
450+
451+ off , err := stringToDuration (temp .LightOffDuration )
452+ if err != nil {
453+ return err
454+ }
455+
456+ l .Color = temp .Color .toString ()
457+ l .LightOnDurationMillis = int64 (on / time .Millisecond )
458+ l .LightOffDurationMillis = int64 (off / time .Millisecond )
459+ return nil
460+ }
461+
462+ func durationToString (ms time.Duration ) string {
463+ seconds := int64 (ms / time .Second )
464+ nanos := int64 ((ms - time .Duration (seconds )* time .Second ) / time .Nanosecond )
465+ if nanos > 0 {
466+ return fmt .Sprintf ("%d.%09ds" , seconds , nanos )
467+ }
468+ return fmt .Sprintf ("%ds" , seconds )
469+ }
470+
471+ func stringToDuration (s string ) (time.Duration , error ) {
472+ segments := strings .Split (strings .TrimSuffix (s , "s" ), "." )
473+ if len (segments ) != 1 && len (segments ) != 2 {
474+ return 0 , fmt .Errorf ("incorrect number of segments in ttl: %q" , s )
475+ }
476+
477+ seconds , err := strconv .ParseInt (segments [0 ], 10 , 64 )
478+ if err != nil {
479+ return 0 , fmt .Errorf ("failed to parse %s: %v" , s , err )
480+ }
481+
482+ ttl := time .Duration (seconds ) * time .Second
483+ if len (segments ) == 2 {
484+ nanos , err := strconv .ParseInt (strings .TrimLeft (segments [1 ], "0" ), 10 , 64 )
485+ if err != nil {
486+ return 0 , fmt .Errorf ("failed to parse %s: %v" , s , err )
487+ }
488+ ttl += time .Duration (nanos ) * time .Nanosecond
489+ }
490+
491+ return ttl , nil
492+ }
493+
494+ type color struct {
495+ Red float64 `json:"red"`
496+ Green float64 `json:"green"`
497+ Blue float64 `json:"blue"`
498+ Alpha float64 `json:"alpha"`
499+ }
500+
501+ func newColor (clr string ) (* color , error ) {
502+ red , err := strconv .ParseInt (clr [1 :3 ], 16 , 32 )
503+ if err != nil {
504+ return nil , fmt .Errorf ("failed to parse %s: %v" , clr , err )
505+ }
506+
507+ green , err := strconv .ParseInt (clr [3 :5 ], 16 , 32 )
508+ if err != nil {
509+ return nil , fmt .Errorf ("failed to parse %s: %v" , clr , err )
510+ }
511+
512+ blue , err := strconv .ParseInt (clr [5 :7 ], 16 , 32 )
513+ if err != nil {
514+ return nil , fmt .Errorf ("failed to parse %s: %v" , clr , err )
515+ }
516+
517+ alpha := int64 (255 )
518+ if len (clr ) == 9 {
519+ alpha , err = strconv .ParseInt (clr [7 :9 ], 16 , 32 )
520+ if err != nil {
521+ return nil , fmt .Errorf ("failed to parse %s: %v" , clr , err )
522+ }
523+ }
524+
525+ return & color {
526+ Red : float64 (red ) / 255.0 ,
527+ Green : float64 (green ) / 255.0 ,
528+ Blue : float64 (blue ) / 255.0 ,
529+ Alpha : float64 (alpha ) / 255.0 ,
530+ }, nil
531+ }
532+
533+ func (c * color ) toString () string {
534+ red := int (c .Red * 255.0 )
535+ green := int (c .Green * 255.0 )
536+ blue := int (c .Blue * 255.0 )
537+ alpha := int (c .Alpha * 255.0 )
538+ if alpha == 255 {
539+ return fmt .Sprintf ("#%X%X%X" , red , green , blue )
540+ }
541+ return fmt .Sprintf ("#%X%X%X%X" , red , green , blue , alpha )
248542}
249543
250544// AndroidFCMOptions contains additional options for features provided by the FCM Android SDK.
0 commit comments