diff --git a/src/main/java/com/google/firebase/messaging/AndroidNotification.java b/src/main/java/com/google/firebase/messaging/AndroidNotification.java index ef286d499..adb84c638 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidNotification.java +++ b/src/main/java/com/google/firebase/messaging/AndroidNotification.java @@ -21,9 +21,14 @@ import com.google.api.client.util.Key; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.firebase.internal.NonNull; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Date; import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; /** * Represents the Android-specific notification options that can be included in a {@link Message}. @@ -69,6 +74,51 @@ public class AndroidNotification { @Key("image") private final String image; + + @Key("ticker") + private final String ticker; + + @Key("sticky") + private final Boolean sticky; + + @Key("event_time") + private final String eventTime; + + @Key("local_only") + private final Boolean localOnly; + + @Key("notification_priority") + private final String priority; + + @Key("vibrate_timings") + private final List vibrateTimings; + + @Key("default_vibrate_timings") + private final Boolean defaultVibrateTimings; + + @Key("default_sound") + private final Boolean defaultSound; + + @Key("light_settings") + private final LightSettings lightSettings; + + @Key("default_light_settings") + private final Boolean defaultLightSettings; + + @Key("visibility") + private final String visibility; + + @Key("notification_count") + private final Integer notificationCount; + + private static final Map PRIORITY_MAP = + ImmutableMap.builder() + .put(Priority.MIN, "PRIORITY_MIN") + .put(Priority.LOW, "PRIORITY_LOW") + .put(Priority.DEFAULT, "PRIORITY_DEFAULT") + .put(Priority.HIGH, "PRIORITY_HIGH") + .put(Priority.MAX, "PRIORITY_MAX") + .build(); private AndroidNotification(Builder builder) { this.title = builder.title; @@ -101,6 +151,49 @@ private AndroidNotification(Builder builder) { } this.channelId = builder.channelId; this.image = builder.image; + this.ticker = builder.ticker; + this.sticky = builder.sticky; + this.eventTime = builder.eventTime; + this.localOnly = builder.localOnly; + if (builder.priority != null) { + this.priority = builder.priority.toString(); + } else { + this.priority = null; + } + if (!builder.vibrateTimings.isEmpty()) { + this.vibrateTimings = ImmutableList.copyOf(builder.vibrateTimings); + } else { + this.vibrateTimings = null; + } + this.defaultVibrateTimings = builder.defaultVibrateTimings; + this.defaultSound = builder.defaultSound; + this.lightSettings = builder.lightSettings; + this.defaultLightSettings = builder.defaultLightSettings; + if (builder.visibility != null) { + this.visibility = builder.visibility.name().toLowerCase(); + } else { + this.visibility = null; + } + this.notificationCount = builder.notificationCount; + } + + public enum Priority { + MIN, + LOW, + DEFAULT, + HIGH, + MAX; + + @Override + public String toString() { + return PRIORITY_MAP.get(this); + } + } + + public enum Visibility { + PRIVATE, + PUBLIC, + SECRET, } /** @@ -127,6 +220,18 @@ public static class Builder { private List titleLocArgs = new ArrayList<>(); private String channelId; private String image; + private String ticker; + private Boolean sticky; + private String eventTime; + private Boolean localOnly; + private Priority priority; + private List vibrateTimings = new ArrayList<>(); + private Boolean defaultVibrateTimings; + private Boolean defaultSound; + private LightSettings lightSettings; + private Boolean defaultLightSettings; + private Visibility visibility; + private Integer notificationCount; private Builder() {} @@ -309,6 +414,186 @@ public Builder setImage(String imageUrl) { return this; } + /** + * Sets the "ticker" text, which is sent to accessibility services. Prior to API level 21 + * (Lollipop), sets the text that is displayed in the status bar when the notification + * first arrives. + * + * @param ticker Ticker name. + * @return This builder. + */ + public Builder setTicker(String ticker) { + this.ticker = ticker; + return this; + } + + /** + * Sets the sticky flag. When set to false or unset, the notification is automatically + * dismissed when the user clicks it in the panel. When set to true, the notification + * persists even when the user clicks it. + * + * @param sticky The sticky flag + * @return This builder. + */ + public Builder setSticky(boolean sticky) { + this.sticky = sticky; + return this; + } + + /** + * For notifications that inform users about events with an absolute time reference, sets + * the time that the event in the notification occurred in milliseconds. Notifications + * in the panel are sorted by this time. The time is be formated in RFC3339 UTC "Zulu" + * format, accurate to nanoseconds. Example: "2014-10-02T15:01:23.045123456Z". Note that + * since the time is in milliseconds, the last section of the time representation always + * has 6 leading zeros. + * + * @param eventTimeInMillis The event time in milliseconds + * @return This builder. + */ + public Builder setEventTimeInMillis(long eventTimeInMillis) { + this.eventTime = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'") + .format(new Date(eventTimeInMillis)); + return this; + } + + /** + * Sets 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. + * + * @param localOnly The "local only" flag + * @return This builder. + */ + public Builder setLocalOnly(boolean localOnly) { + this.localOnly = localOnly; + return this; + } + + /** + * 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. + * + * @param priority The priority value, one of the values in {MIN, LOW, DEFAULT, HIGH, MAX} + * @return This builder. + */ + public Builder setPriority(Priority priority) { + this.priority = priority; + return this; + } + + /** + * 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 {@code vibrate_timings} + * is set and {@code default_vibrate_timings} is set to true, the default value is used instead + * of the user-specified {@code vibrate_timings}. + * A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s". + * + * @param vibrateTimingsInMillis List of vibration time in milliseconds + * @return This builder. + */ + public Builder setVibrateTimingsInMillis(long[] vibrateTimingsInMillis) { + List list = new ArrayList<>(); + for (long value : vibrateTimingsInMillis) { + checkArgument(value >= 0, "elements in vibrateTimingsInMillis must not be negative"); + long seconds = TimeUnit.MILLISECONDS.toSeconds(value); + long subsecondNanos = TimeUnit.MILLISECONDS.toNanos(value - seconds * 1000L); + if (subsecondNanos > 0) { + list.add(String.format("%d.%09ds", seconds, subsecondNanos)); + } else { + list.add(String.format("%ds", seconds)); + } + } + this.vibrateTimings = ImmutableList.copyOf(list); + return this; + } + + /** + * Sets the whether to use the default vibration timings. If set to true, use the Android + * framework's default vibrate pattern for the notification. Default values are specified + * in {@code config.xml}. If {@code default_vibrate_timings} is set to true and + * {@code vibrate_timings} is also set, the default value is used instead of the + * user-specified {@code vibrate_timings}. + * + * @param defaultVibrateTimings The flag indicating whether to use the default vibration timings + * @return This builder. + */ + public Builder setDefaultVibrateTimings(boolean defaultVibrateTimings) { + this.defaultVibrateTimings = defaultVibrateTimings; + return this; + } + + /** + * Sets the whether 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. + * + * @param defaultSound The flag indicating whether to use the default sound + * @return This builder. + */ + public Builder setDefaultSound(boolean defaultSound) { + this.defaultSound = defaultSound; + return this; + } + + /** + * 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. + * + * @param lightSettings The light settings to use + * @return This builder. + */ + public Builder setLightSettings(LightSettings lightSettings) { + this.lightSettings = lightSettings; + return this; + } + + /** + * Sets the whether 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 {@code default_light_settings} is set to true and + * {@code light_settings} is also set, the user-specified {@code light_settings} is used + * instead of the default value. + * + * @param defaultLightSettings The flag indicating whether to use the default light + * settings + * @return This builder. + */ + public Builder setDefaultLightSettings(boolean defaultLightSettings) { + this.defaultLightSettings = defaultLightSettings; + return this; + } + + /** + * Sets the visibility of this notification. + * + * @param visibility The visibility value. one of the values in {PRIVATE, PUBLIC, SECRET} + * @return This builder. + */ + public Builder setVisibility(Visibility visibility) { + this.visibility = visibility; + return this; + } + + /** + * Sets the number of items this notification represents. May be displayed as a badge + * count for launchers that support badging. 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. + * + * @param notificationCount The notification count + * @return This builder. + */ + public Builder setNotificationCount(int notificationCount) { + this.notificationCount = notificationCount; + return this; + } + /** * Creates a new {@link AndroidNotification} instance from the parameters set on this builder. * diff --git a/src/main/java/com/google/firebase/messaging/LightSettings.java b/src/main/java/com/google/firebase/messaging/LightSettings.java new file mode 100644 index 000000000..75a692090 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/LightSettings.java @@ -0,0 +1,128 @@ +/* + * Copyright 2019 Google Inc. + * + * 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. + */ + +package com.google.firebase.messaging; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.api.client.util.Key; +import java.util.concurrent.TimeUnit; + +/** + * A class representing light settings in an Android Notification. + */ +public final class LightSettings { + + @Key("color") + private final LightSettingsColor color; + + @Key("light_on_duration") + private final String lightOnDuration; + + @Key("light_off_duration") + private final String lightOffDuration; + + private LightSettings(Builder builder) { + this.color = builder.color; + this.lightOnDuration = builder.lightOnDuration; + this.lightOffDuration = builder.lightOffDuration; + } + + /** + * Creates a new {@link LightSettings.Builder}. + * + * @return A {@link LightSettings.Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private LightSettingsColor color; + private String lightOnDuration; + private String lightOffDuration; + + private Builder() {} + + /** + * Sets the lightSettingsColor value with a string. + * + * @param lightSettingsColor LightSettingsColor specified in the {@code #rrggbb} format. + * @return This builder. + */ + public Builder setColorFromString(String color) { + this.color = LightSettingsColor.fromString(color); + return this; + } + + /** + * Sets the lightSettingsColor value in the light settings. + * + * @param lightSettingsColor Color to be used in the light settings. + * @return This builder. + */ + public Builder setColor(LightSettingsColor color) { + this.color = color; + return this; + } + + /** + * Sets the light on duration in milliseconds. + * + * @param lightOnDurationInMillis The time duration in milliseconds for the LED light to be on. + * @return This builder. + */ + public Builder setLightOnDurationInMillis(long lightOnDurationInMillis) { + this.lightOnDuration = convertToSecondsAndNanosFormat(lightOnDurationInMillis); + return this; + } + + /** + * Sets the light off duration in milliseconds. + * + * @param lightOffDurationInMillis The time duration in milliseconds for the LED light to be + * off. + * @return This builder. + */ + public Builder setLightOffDurationInMillis(long lightOffDurationInMillis) { + this.lightOffDuration = convertToSecondsAndNanosFormat(lightOffDurationInMillis); + return this; + } + + private String convertToSecondsAndNanosFormat(long millis) { + checkArgument(millis >= 0, "Milliseconds duration must not be negative"); + long seconds = TimeUnit.MILLISECONDS.toSeconds(millis); + long subsecondNanos = TimeUnit.MILLISECONDS + .toNanos(millis - seconds * 1000L); + if (subsecondNanos > 0) { + return String.format("%d.%09ds", seconds, subsecondNanos); + } else { + return String.format("%ds", seconds); + } + } + + /** + * Builds a new {@link LightSettings} instance from the fields set on this builder. + * + * @return A non-null {@link LightSettings}. + * @throws IllegalArgumentException If the volume value is out of range. + */ + public LightSettings build() { + return new LightSettings(this); + } + } +} diff --git a/src/main/java/com/google/firebase/messaging/LightSettingsColor.java b/src/main/java/com/google/firebase/messaging/LightSettingsColor.java new file mode 100644 index 000000000..cfec64995 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/LightSettingsColor.java @@ -0,0 +1,71 @@ +/* + * Copyright 2019 Google Inc. + * + * 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. + */ + +package com.google.firebase.messaging; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.api.client.util.Key; + +/** + * A class representing color in LightSettings. + */ +public final class LightSettingsColor { + + @Key("red") + private final Float red; + + @Key("green") + private final Float green; + + @Key("blue") + private final Float blue; + + @Key("alpha") + private final Float alpha; + + /** + * Creates a new {@link LightSettingsColor} using the given red, green, blue, and + * alpha values. + * + * @param red The red component. + * @param green The green component. + * @param blue The blue component. + * @param alpha The alpha component. + */ + public LightSettingsColor(float red, float green, float blue, float alpha) { + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = alpha; + } + + /** + * Creates a new {@link LightSettingsColor} with a string. Alpha of the color will be + * set to 1. + * + * @param rrggbb LightSettingsColor specified in the {@code #rrggbb} format. + * @return A {@link LightSettingsColor} instance. + */ + public static LightSettingsColor fromString(String rrggbb) { + checkArgument(rrggbb.matches("^#[0-9a-fA-F]{6}$"), + "LightSettingsColor must be in the form #RRGGBB"); + float red = (float) Integer.parseInt(rrggbb.substring(1, 3), 16) / 255.0f; + float green = (float) Integer.valueOf(rrggbb.substring(3, 5), 16) / 255.0f; + float blue = (float) Integer.valueOf(rrggbb.substring(5, 7), 16) / 255.0f; + return new LightSettingsColor(red, green, blue, 1.0f); + } +} diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 556f2d9ea..7c8f528df 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -28,6 +28,9 @@ import com.google.firebase.messaging.AndroidConfig.Priority; import java.io.IOException; import java.math.BigDecimal; +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -796,6 +799,87 @@ public void testImageInApnsNotification() throws IOException { .build(); assertJsonEquals(expected, message); } + + @Test + public void testInvalidColorInAndroidNotificationLightSettings() throws IOException { + try { + LightSettings.Builder lightSettingsBuilder = LightSettings.builder() + .setColorFromString("#01020K") + .setLightOnDurationInMillis(1002L) + .setLightOffDurationInMillis(1003L); + + lightSettingsBuilder.build(); + fail("No error thrown for invalid notification"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testExtendedAndroidNotificationParameters() throws IOException { + long[] vibrateTimings = {1000L, 1001L}; + Message message = Message.builder() + .setNotification(new Notification("title", "body")) + .setAndroidConfig(AndroidConfig.builder() + .setNotification(AndroidNotification.builder() + .setTitle("android-title") + .setBody("android-body") + .setTicker("ticker") + .setSticky(true) + .setEventTimeInMillis(1546304523123L) + .setLocalOnly(true) + .setPriority(AndroidNotification.Priority.HIGH) + .setVibrateTimingsInMillis(vibrateTimings) + .setDefaultVibrateTimings(false) + .setDefaultSound(false) + .setLightSettings(LightSettings.builder() + .setColorFromString("#336699") + .setLightOnDurationInMillis(1002L) + .setLightOffDurationInMillis(1003L) + .build()) + .setDefaultLightSettings(false) + .setVisibility(AndroidNotification.Visibility.PUBLIC) + .setNotificationCount(10) + .build()) + .build()) + .setTopic("test-topic") + .build(); + Map notification = ImmutableMap.builder() + .put("title", "title") + .put("body", "body") + .build(); + String eventTime = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'") + .format(new Date(1546304523123L)); + Map androidConfig = ImmutableMap.builder() + .put("notification", ImmutableMap.builder() + .put("title", "android-title") + .put("body", "android-body") + .put("ticker", "ticker") + .put("sticky", true) + .put("event_time", eventTime) + .put("local_only", true) + .put("notification_priority", "PRIORITY_HIGH") + .put("vibrate_timings", ImmutableList.of("1s", "1.001000000s")) + .put("default_vibrate_timings", false) + .put("default_sound", false) + .put("light_settings", ImmutableMap.builder() + .put("color", ImmutableMap.builder() + .put("red", new BigDecimal(new BigInteger("2"), 1)) + .put("green", new BigDecimal(new BigInteger("4"), 1)) + .put("blue", new BigDecimal(new BigInteger("6"), 1)) + .put("alpha", new BigDecimal(new BigInteger("10"), 1)) + .build()) + .put("light_on_duration", "1.002000000s") + .put("light_off_duration", "1.003000000s") + .build()) + .put("default_light_settings", false) + .put("visibility", "public") + .put("notification_count", new BigDecimal(10)) + .build()) + .build(); + assertJsonEquals(ImmutableMap.of( + "topic", "test-topic", "notification", notification, "android", androidConfig), message); + } private static void assertJsonEquals( Map expected, Object actual) throws IOException {