diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index 0ecc1cab..c7e2dbd4 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -158,6 +158,21 @@ public void AndroidConfig() { "k1", "v1" }, { "k2", "v2" }, }, + Notification = new AndroidNotification() + { + Title = "title", + Body = "body", + Icon = "icon", + Color = "#112233", + Sound = "sound", + Tag = "tag", + ClickAction = "click-action", + TitleLocKey = "title-loc-key", + TitleLocArgs = new List(){ "arg1", "arg2" }, + BodyLocKey = "body-loc-key", + BodyLocArgs = new List(){ "arg3", "arg4" }, + ChannelId = "channel-id", + }, }, }; var expected = new JObject() @@ -170,7 +185,24 @@ public void AndroidConfig() { "priority", "high" }, { "ttl", "0.010000000s" }, { "restricted_package_name", "test-pkg-name" }, - {"data", new JObject(){{"k1", "v1"}, {"k2", "v2"}}}, + { "data", new JObject(){{"k1", "v1"}, {"k2", "v2"}} }, + { + "notification", new JObject() + { + { "title", "title" }, + { "body", "body" }, + { "icon", "icon" }, + { "color", "#112233" }, + { "sound", "sound" }, + { "tag", "tag" }, + { "click_action", "click-action" }, + { "title_loc_key", "title-loc-key" }, + { "title_loc_args", new JArray(){"arg1", "arg2"} }, + { "body_loc_key", "body-loc-key" }, + { "body_loc_args", new JArray(){"arg3", "arg4"} }, + { "channel_id", "channel-id" }, + } + }, } }, }; @@ -225,6 +257,57 @@ public void AndroidConfigInvalidTTL() Assert.Throws(() => message.Validate()); } + [Fact] + public void AndroidNotificationInvalidColor() + { + var message = new Message() + { + Topic = "test-topic", + AndroidConfig = new AndroidConfig() + { + Notification = new AndroidNotification() + { + Color = "not-a-color" + }, + }, + }; + Assert.Throws(() => message.Validate()); + } + + [Fact] + public void AndroidNotificationInvalidTitleLocArgs() + { + var message = new Message() + { + Topic = "test-topic", + AndroidConfig = new AndroidConfig() + { + Notification = new AndroidNotification() + { + TitleLocArgs = new List(){"arg"}, + }, + }, + }; + Assert.Throws(() => message.Validate()); + } + + [Fact] + public void AndroidNotificationInvalidBodyLocArgs() + { + var message = new Message() + { + Topic = "test-topic", + AndroidConfig = new AndroidConfig() + { + Notification = new AndroidNotification() + { + BodyLocArgs = new List(){"arg"}, + }, + }, + }; + Assert.Throws(() => message.Validate()); + } + private void AssertJsonEquals(JObject expected, Message actual) { var json = NewtonsoftJsonSerializer.Instance.Serialize(actual.Validate()); diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs index 769bc2fb..3be35959 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs @@ -53,6 +53,11 @@ public sealed class AndroidConfig /// public IReadOnlyDictionary Data { get; set; } + /// + /// The Android notification to be included in the message. + /// + public AndroidNotification Notification { get; set; } + internal ValidatedAndroidConfig Validate() { return new ValidatedAndroidConfig() @@ -62,6 +67,7 @@ internal ValidatedAndroidConfig Validate() TimeToLive = this.TtlString, RestrictedPackageName = this.RestrictedPackageName, Data = this.Data, + Notification = this.Notification?.Validate(), }; } @@ -134,6 +140,9 @@ internal sealed class ValidatedAndroidConfig [JsonProperty("data")] internal IReadOnlyDictionary Data { get; set; } + + [JsonProperty("notification")] + internal ValidatedAndroidNotification Notification { get; set; } } /// diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs new file mode 100644 index 00000000..63e0fddf --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs @@ -0,0 +1,188 @@ +// Copyright 2018, 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.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + /// + /// Represents the Android-specific notification options that can be included in a + /// . + /// + public sealed class AndroidNotification + { + + /// + /// The title of the Android notification. When provided, overrides the title set + /// via . + /// + public string Title { get; set; } + + /// + /// The title of the Android notification. When provided, overrides the title set + /// via . + /// + public string Body { get; set; } + + /// + /// The icon of the Android notification. + /// + public string Icon { get; set; } + + /// + /// The notification icon color. Must be of the form #RRGGBB. + /// + public string Color { get; set; } + + /// + /// The sound to be played when the device receives the notification. + /// + public string Sound { get; set; } + + /// + /// The notification tag. This is an identifier used to replace existing notifications in + /// the notification drawer. If not specified, each request creates a new notification. + /// + public string Tag { get; set; } + + /// + /// The action associated with a user click on the notification. If specified, an activity + /// with a matching Intent Filter is launched when a user clicks on the notification. + /// + public string ClickAction { get; set; } + + /// + /// Sets the key of the title string in the app's string resources to use to localize the + /// title text. + /// . + /// + public string TitleLocKey { get; set; } + + /// + /// The collection of resource key strings that will be used in place of the format + /// specifiers in . + /// + public IEnumerable TitleLocArgs { get; set; } + + /// + /// Sets the key of the body string in the app's string resources to use to localize the + /// body text. + /// . + /// + public string BodyLocKey { get; set; } + + /// + /// The collection of resource key strings that will be used in place of the format + /// specifiers in . + /// + public IEnumerable BodyLocArgs { get; set; } + + /// + /// Sets the Android notification channel ID (new in Android O). The app must create a + /// channel with this channel ID before any notification with this channel ID is received. + /// If you don't send this channel ID in the request, or if the channel ID provided has + /// not yet been created by the app, FCM uses the channel ID specified in the app manifest. + /// + public string ChannelId { get; set; } + + /// + /// Validates the content and structure of this notification, and converts it into the + /// type. This return type can be safely + /// serialized into a JSON string that is acceptable to the FCM backend service. + /// + internal ValidatedAndroidNotification Validate() + { + if (Color != null) { + if (!Regex.Match(Color, "^#[0-9a-fA-F]{6}$").Success) + { + throw new ArgumentException("Color must be in the form #RRGGBB"); + } + } + if (TitleLocArgs != null && TitleLocArgs.Any()) { + if (string.IsNullOrEmpty(TitleLocKey)) + { + throw new ArgumentException("TitleLocKey is required when specifying TitleLocArgs"); + } + } + if (BodyLocArgs != null && BodyLocArgs.Any()) { + if (string.IsNullOrEmpty(BodyLocKey)) + { + throw new ArgumentException("BodyLocKey is required when specifying BodyLocArgs"); + } + } + return new ValidatedAndroidNotification() + { + Title = this.Title, + Body = this.Body, + Icon = this.Icon, + Color = this.Color, + Sound = this.Sound, + Tag = this.Tag, + ClickAction = this.ClickAction, + TitleLocKey = this.TitleLocKey, + TitleLocArgs = this.TitleLocArgs, + BodyLocKey = this.BodyLocKey, + BodyLocArgs = this.BodyLocArgs, + ChannelId = this.ChannelId, + }; + } + } + + /// + /// Represents a validated Android notification that can be serialized into the JSON format + /// accepted by the FCM backend service. + /// + internal sealed class ValidatedAndroidNotification + { + [JsonProperty("title")] + internal string Title { get; set; } + + [JsonProperty("body")] + internal string Body { get; set; } + + [JsonProperty("icon")] + internal string Icon { get; set; } + + [JsonProperty("color")] + internal string Color { get; set; } + + [JsonProperty("sound")] + internal string Sound { get; set; } + + [JsonProperty("tag")] + internal string Tag { get; set; } + + [JsonProperty("click_action")] + internal string ClickAction { get; set; } + + [JsonProperty("title_loc_key")] + internal string TitleLocKey { get; set; } + + [JsonProperty("title_loc_args")] + internal IEnumerable TitleLocArgs { get; set; } + + [JsonProperty("body_loc_key")] + internal string BodyLocKey { get; set; } + + [JsonProperty("body_loc_args")] + internal IEnumerable BodyLocArgs { get; set; } + + [JsonProperty("channel_id")] + internal string ChannelId { get; set; } + } +}