Skip to content

Commit 3ca24dd

Browse files
authored
feat(fcm): Introduced a series of new parameters to AndroidNotification type. (#316)
* Added new FCM AndroidNotification options * Added support for color strings with alpha values
1 parent 32728b0 commit 3ca24dd

File tree

3 files changed

+561
-58
lines changed

3 files changed

+561
-58
lines changed

messaging/messaging.go

Lines changed: 327 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -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

5355
var (
@@ -176,13 +178,7 @@ type AndroidConfig struct {
176178
func (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.
234218
type 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

Comments
 (0)