diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index 5d937697..ad30a752 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -275,6 +275,23 @@ public void AndroidConfig() BodyLocKey = "body-loc-key", BodyLocArgs = new List() { "arg3", "arg4" }, ChannelId = "channel-id", + Ticker = "ticker", + Sticky = false, + EventTimestamp = DateTime.Parse("2020-06-27T16:29:06.032691000-04:00"), + LocalOnly = true, + Priority = AndroidNotification.PriorityType.HIGH, + VibrateTimingsMillis = new long[] { 1000L, 1001L }, + DefaultVibrateTimings = false, + DefaultSound = true, + LightSettings = new LightSettings + { + Color = new LightSettingsColor { Red = 0.2f, Green = 0.4f, Blue = 0.6f }, + LightOnDurationMillis = 1002L, + LightOffDurationMillis = 1003L, + }, + DefaultLightSettings = false, + Visibility = AndroidNotification.VisibilityType.PUBLIC, + NotificationCount = 10, }, FcmOptions = new AndroidFcmOptions() { @@ -309,6 +326,23 @@ public void AndroidConfig() { "body_loc_key", "body-loc-key" }, { "body_loc_args", new JArray() { "arg3", "arg4" } }, { "channel_id", "channel-id" }, + { "ticker", "ticker" }, + { "sticky", false }, + { "local_only", true }, + { "default_vibrate_timings", false }, + { "default_sound", true }, + { + "light_settings", new JObject() + { + { "color", "#336699" }, { "light_on_duration", "1.002000000s" }, { "light_off_duration", "1.003000000s" }, + } + }, + { "default_light_settings", false }, + { "notification_count", 10 }, + { "notification_priority", "PRIORITY_HIGH" }, + { "visibility", "PUBLIC" }, + { "vibrate_timings", new JArray() { "1s", "1.001000000s" } }, + { "event_time", "2020-06-27T20:29:06.032691000Z" }, } }, { @@ -379,6 +413,7 @@ public void AndroidConfigDeserialization() Notification = new AndroidNotification() { Title = "title", + EventTimestamp = DateTime.Parse("2020-06-27T20:29:06.032691000Z"), }, }; var json = NewtonsoftJsonSerializer.Instance.Serialize(original); @@ -423,6 +458,23 @@ public void AndroidNotificationDeserialization() BodyLocKey = "body-loc-key", BodyLocArgs = new List() { "arg3", "arg4" }, ChannelId = "channel-id", + Ticker = "ticker", + Sticky = false, + EventTimestamp = DateTime.Parse("2020-06-27T16:29:06.032691000-04:00"), + LocalOnly = true, + Priority = AndroidNotification.PriorityType.HIGH, + VibrateTimingsMillis = new long[] { 1000L, 1001L }, + DefaultVibrateTimings = false, + DefaultSound = true, + LightSettings = new LightSettings + { + Color = new LightSettingsColor { Red = 0.2F, Green = 0.4F, Blue = 0.6F, Alpha = 1.0F }, + LightOnDurationMillis = 1002L, + LightOffDurationMillis = 1003L, + }, + DefaultLightSettings = false, + Visibility = AndroidNotification.VisibilityType.PUBLIC, + NotificationCount = 10, }; var json = NewtonsoftJsonSerializer.Instance.Serialize(original); var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json); @@ -439,6 +491,21 @@ public void AndroidNotificationDeserialization() Assert.Equal(original.BodyLocKey, copy.BodyLocKey); Assert.Equal(original.BodyLocArgs, copy.BodyLocArgs); Assert.Equal(original.ChannelId, copy.ChannelId); + Assert.Equal(original.Ticker, copy.Ticker); + Assert.Equal(original.Sticky, copy.Sticky); + Assert.Equal(original.EventTimestamp, copy.EventTimestamp); + Assert.Equal(original.LocalOnly, copy.LocalOnly); + Assert.Equal(original.Priority, copy.Priority); + Assert.Equal(original.VibrateTimingsMillis, copy.VibrateTimingsMillis); + Assert.Equal(original.DefaultVibrateTimings, copy.DefaultVibrateTimings); + Assert.Equal(original.DefaultSound, copy.DefaultSound); + Assert.Equal(original.LightSettings.Color.Red, copy.LightSettings.Color.Red); + Assert.Equal(original.LightSettings.Color.Blue, copy.LightSettings.Color.Blue); + Assert.Equal(original.LightSettings.Color.Green, copy.LightSettings.Color.Green); + Assert.Equal(original.LightSettings.Color.Alpha, copy.LightSettings.Color.Alpha); + Assert.Equal(original.DefaultLightSettings, copy.DefaultLightSettings); + Assert.Equal(original.Visibility, copy.Visibility); + Assert.Equal(original.NotificationCount, copy.NotificationCount); } [Fact] diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs index 4742a848..c640c382 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs @@ -14,8 +14,11 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text.RegularExpressions; +using FirebaseAdmin.Messaging.Util; +using Google.Apis.Util; using Newtonsoft.Json; namespace FirebaseAdmin.Messaging @@ -26,6 +29,58 @@ namespace FirebaseAdmin.Messaging /// public sealed class AndroidNotification { + /// + /// Priority levels that can be set on an . + /// + public enum PriorityType + { + /// + /// Minimum priority notification. + /// + MIN, + + /// + /// Low priority notification. + /// + LOW, + + /// + /// Default priority notification. + /// + DEFAULT, + + /// + /// High priority notification. + /// + HIGH, + + /// + /// Maximum priority notification. + /// + MAX, + } + + /// + /// Visibility levels that can be set on an . + /// + public enum VisibilityType + { + /// + /// Private visibility. + /// + PRIVATE, + + /// + /// Public visibility. + /// + PUBLIC, + + /// + /// Secret visibility. + /// + SECRET, + } + /// /// Gets or sets the title of the Android notification. When provided, overrides the title /// set via . @@ -118,6 +173,265 @@ public sealed class AndroidNotification [JsonProperty("channel_id")] public string ChannelId { get; set; } + /// + /// Gets or sets the "ticker" text which is sent to accessibility services. Prior to API level 21 + /// (Lollipop), gets or sets the text that is displayed in the status bar when the notification + /// first arrives. + /// + [JsonProperty("ticker")] + public string Ticker { get; set; } + + /// + /// Gets or sets a value indicating whether the notification is automatically dismissed + /// or persists when the user clicks it in the panel. When set to false, + /// the notification is automatically dismissed. When set to true, the notification persists. + /// + [JsonProperty("sticky")] + public bool Sticky { get; set; } + + /// + /// Gets or sets the time that the event in the notification occurred for notifications + /// that inform users about events with an absolute time reference. Notifications in the panel + /// are sorted by this time. + /// + [JsonIgnore] + public DateTime EventTimestamp { get; set; } + + /// + /// Gets or sets a value indicating whether or not this notification is relevant only to + /// the current device. Some notifications can be bridged to other devices for remote display, + /// such as a Wear OS watch. This hint can be set to recommend this notification not be bridged. + /// See Wear OS guides. + /// + [JsonProperty("local_only")] + public bool LocalOnly { get; set; } + + /// + /// Gets or sets the relative priority for this notification. Priority is an indication of how much of + /// the user's attention should be consumed by this notification. Low-priority notifications + /// may be hidden from the user in certain situations, while the user might be interrupted + /// for a higher-priority notification. + /// + [JsonIgnore] + public AndroidNotification.PriorityType? Priority { get; set; } + + /// + /// Gets or sets a list of vibration timings in milliseconds in the array to use. The first value in the + /// array indicates the duration to wait before turning the vibrator on. The next value + /// indicates the duration to keep the vibrator on. Subsequent values alternate between + /// duration to turn the vibrator off and to turn the vibrator on. If is set and + /// is set to true, the default value is used instead of + /// the user-specified vibrate_timings. A duration in seconds with up to nine fractional digits, + /// terminated by 's'.Example: "3.5s". + /// + [JsonIgnore] + public long[] VibrateTimingsMillis { get; set; } + + /// + /// Gets or sets a value indicating whether or not to use the default vibration timings. If set to true, use the Android + /// Sets the whether to use the default vibration timings. If set to true, use the Android + /// in config.xml. + /// If is set to true and is also set, + /// the default value is used instead of the user-specified . + /// + [JsonProperty("default_vibrate_timings")] + public bool DefaultVibrateTimings { get; set; } + + /// + /// Gets or sets a value indicating whether or not to use the default sound. If set to true, use the Android framework's + /// default sound for the notification. Default values are specified in config.xml. + /// + [JsonProperty("default_sound")] + public bool DefaultSound { get; set; } + + /// + /// Gets or sets the settings to control the notification's LED blinking rate and color if LED is + /// available on the device. The total blinking time is controlled by the OS. + /// + [JsonProperty("light_settings")] + public LightSettings LightSettings { get; set; } + + /// + /// Gets or sets a value indicating whether or not to use the default light settings. + /// If set to true, use the Android framework's default LED light settings for the notification. Default values are + /// specified in config.xml. If is set to true and is also set, + /// the user-specified is used instead of the default value. + /// + [JsonProperty("default_light_settings")] + public bool DefaultLightSettings { get; set; } + + /// + /// Gets or sets the visibility of this notification. + /// + [JsonIgnore] + public AndroidNotification.VisibilityType? Visibility { get; set; } + + /// + /// Gets or sets the number of items this notification represents. May be displayed as a badge + /// count for launchers that support badging. If not invoked then notification count is left unchanged. + /// For example, this might be useful if you're using just one notification to represent + /// multiple new messages but you want the count here to represent the number of total + /// new messages.If zero or unspecified, systems that support badging use the default, + /// which is to increment a number displayed on the long-press menu each time a new notification arrives. + /// + [JsonProperty("notification_count")] + public int? NotificationCount { get; set; } + + /// + /// Gets or sets the string representation of the property. + /// + [JsonProperty("notification_priority")] + private string PriorityString + { + get + { + switch (this.Priority) + { + case PriorityType.MIN: + return "PRIORITY_MIN"; + case PriorityType.LOW: + return "PRIORITY_LOW"; + case PriorityType.DEFAULT: + return "PRIORITY_DEFAULT"; + case PriorityType.HIGH: + return "PRIORITY_HIGH"; + case PriorityType.MAX: + return "PRIORITY_MAX"; + default: + return null; + } + } + + set + { + switch (value) + { + case "PRIORITY_MIN": + this.Priority = PriorityType.MIN; + return; + case "PRIORITY_LOW": + this.Priority = PriorityType.LOW; + return; + case "PRIORITY_DEFAULT": + this.Priority = PriorityType.DEFAULT; + return; + case "PRIORITY_HIGH": + this.Priority = PriorityType.HIGH; + return; + case "PRIORITY_MAX": + this.Priority = PriorityType.MAX; + return; + default: + throw new ArgumentException( + $"Invalid priority value: {value}. Only 'PRIORITY_MIN', 'PRIORITY_LOW', ''PRIORITY_DEFAULT' " + + "'PRIORITY_HIGH','PRIORITY_MAX' are allowed."); + } + } + } + + /// + /// Gets or sets the string representation of the property. + /// + [JsonProperty("visibility")] + private string VisibilityString + { + get + { + switch (this.Visibility) + { + case VisibilityType.PUBLIC: + return "PUBLIC"; + case VisibilityType.PRIVATE: + return "PRIVATE"; + case VisibilityType.SECRET: + return "SECRET"; + default: + return null; + } + } + + set + { + switch (value) + { + case "PUBLIC": + this.Visibility = VisibilityType.PUBLIC; + return; + case "PRIVATE": + this.Visibility = VisibilityType.PRIVATE; + return; + case "SECRET": + this.Visibility = VisibilityType.SECRET; + return; + default: + throw new ArgumentException( + $"Invalid visibility value: {value}. Only 'PUBLIC', 'PRIVATE', ''SECRET' are allowed."); + } + } + } + + /// + /// Gets or sets the string representation of the property. + /// + [JsonProperty("vibrate_timings")] + private List VibrateTimingsString + { + get + { + var timingsStringList = new List(); + if (this.VibrateTimingsMillis == null) + { + return null; + } + + foreach (var value in this.VibrateTimingsMillis) + { + timingsStringList.Add(TimeConverter.LongMillisToString(value)); + } + + return timingsStringList; + } + + set + { + if (value.Count == 0) + { + throw new ArgumentException("Invalid VibrateTimingsMillis. VibrateTimingsMillis should be a non-empty list of strings"); + } + + var timingsLongList = new List(); + + foreach (var timingString in value) + { + timingsLongList.Add(TimeConverter.StringToLongMillis(timingString)); + } + + this.VibrateTimingsMillis = timingsLongList.ToArray(); + } + } + + /// + /// Gets or sets the string representation of the property. + /// + [JsonProperty("event_time")] + private string EventTimeString + { + get + { + return this.EventTimestamp.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss.ffffff000'Z'"); + } + + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException("Invalid event timestamp. Event timestamp should be a non-empty string"); + } + + this.EventTimestamp = DateTime.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.None); + } + } + /// /// Copies this notification, and validates the content of it to ensure that it can be /// serialized into the JSON format expected by the FCM service. @@ -139,6 +453,18 @@ internal AndroidNotification CopyAndValidate() BodyLocKey = this.BodyLocKey, BodyLocArgs = this.BodyLocArgs?.ToList(), ChannelId = this.ChannelId, + Ticker = this.Ticker, + Sticky = this.Sticky, + EventTimestamp = this.EventTimestamp, + LocalOnly = this.LocalOnly, + Priority = this.Priority, + VibrateTimingsMillis = this.VibrateTimingsMillis, + DefaultVibrateTimings = this.DefaultVibrateTimings, + DefaultSound = this.DefaultSound, + LightSettings = this.LightSettings, + DefaultLightSettings = this.DefaultLightSettings, + Visibility = this.Visibility, + NotificationCount = this.NotificationCount, }; if (copy.Color != null && !Regex.Match(copy.Color, "^#[0-9a-fA-F]{6}$").Success) { diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/LightSettings.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/LightSettings.cs new file mode 100644 index 00000000..2b1128f2 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/LightSettings.cs @@ -0,0 +1,123 @@ +// Copyright 2020, Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Text; +using System.Text.RegularExpressions; +using FirebaseAdmin.Messaging.Util; +using Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + /// + /// Represents light settings in an Android Notification. + /// + public sealed class LightSettings + { + /// + /// Gets or sets the lightSettingsColor value in the light settings. + /// + [JsonIgnore] + public LightSettingsColor Color { get; set; } + + /// + /// Gets or sets the light on duration in milliseconds. + /// + [JsonIgnore] + public long LightOnDurationMillis { get; set; } + + /// + /// Gets or sets the light off duration in milliseconds. + /// + [JsonIgnore] + public long LightOffDurationMillis { get; set; } + + /// + /// Gets or sets a string representation of . + /// + [JsonProperty("color")] + private string LightSettingsColorString + { + get + { + var colorStringBuilder = new StringBuilder(); + + colorStringBuilder + .Append("#") + .Append(Convert.ToInt32(this.Color.Red * 255).ToString("X")) + .Append(Convert.ToInt32(this.Color.Green * 255).ToString("X")) + .Append(Convert.ToInt32(this.Color.Blue * 255).ToString("X")); + + return colorStringBuilder.ToString(); + } + + set + { + var pattern = new Regex("^#[0-9a-fA-F]{6}$"); + + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException("Invalid LightSettingsColor. LightSettingsColor annot be null or empty"); + } + + if (!pattern.IsMatch(value)) + { + throw new ArgumentException($"Invalid LightSettingsColor {value}. LightSettingsColor must be in the form #RRGGBB"); + } + + this.Color = new LightSettingsColor + { + Red = Convert.ToInt32(value.Substring(1, 2), 16) / 255.0f, + Green = Convert.ToInt32(value.Substring(3, 2), 16) / 255.0f, + Blue = Convert.ToInt32(value.Substring(5, 2), 16) / 255.0f, + Alpha = 1.0f, + }; + } + } + + /// + /// Gets or sets the string representation of . + /// + [JsonProperty("light_on_duration")] + private string LightOnDurationMillisString + { + get + { + return TimeConverter.LongMillisToString(this.LightOnDurationMillis); + } + + set + { + this.LightOnDurationMillis = TimeConverter.StringToLongMillis(value); + } + } + + /// + /// Gets or sets the string representation of . + /// + [JsonProperty("light_off_duration")] + private string LightOffDurationMillisString + { + get + { + return TimeConverter.LongMillisToString(this.LightOffDurationMillis); + } + + set + { + this.LightOffDurationMillis = TimeConverter.StringToLongMillis(value); + } + } + } +} diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/LightSettingsColor.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/LightSettingsColor.cs new file mode 100644 index 00000000..c6eb3a5d --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/LightSettingsColor.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + /// + /// A class representing color in LightSettings. + /// + public class LightSettingsColor + { + /// + /// Gets or sets the red component. + /// + public float Red { get; set; } + + /// + /// Gets or sets the green component. + /// + public float Green { get; set; } + + /// + /// Gets or sets the blue component. + /// + public float Blue { get; set; } + + /// + /// Gets or sets the alpha component. + /// + public float Alpha { get; set; } + } +} diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Util/TimeConverter.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Util/TimeConverter.cs new file mode 100644 index 00000000..45be63d3 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Util/TimeConverter.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace FirebaseAdmin.Messaging.Util +{ + /// + /// Converter from long milliseconds to string and vice versa. + /// + internal static class TimeConverter + { + /// + /// Converts long milliseconds to the FCM string representation. + /// + /// Milliseconds as a long. + /// An FCM string representation of the long milliseconds. + public static string LongMillisToString(long longMillis) + { + var seconds = Math.Floor(Convert.ToDouble(longMillis / 1000)); + var subsecondNanos = Convert.ToDecimal((longMillis - (seconds * 1000L)) * 1E6); + + if (subsecondNanos > 0) + { + return string.Format("{0:0}.{1:000000000}s", seconds, subsecondNanos); + } + else + { + return string.Format("{0}s", seconds); + } + } + + /// + /// Converts an FCM representation of time into milliseconds of type long. + /// + /// An FCM representation of time. + /// The string time as a long. + public static long StringToLongMillis(string timingString) + { + return Convert.ToInt64(Convert.ToDouble(timingString.TrimEnd('s')) * 1000); + } + } +}