From a38e47ca171eb6a40a54b7f9e2f9e49b0c02aa93 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 18 Dec 2018 11:24:27 -0800 Subject: [PATCH] Implemented CriticalSound API for FCM (#233) * Implemented CriticalSounf API for FCM * Made the name field required * Ignoring empty values in sound field * Documentation updates --- CHANGELOG.md | 2 + .../com/google/firebase/messaging/Aps.java | 21 +++- .../firebase/messaging/CriticalSound.java | 115 ++++++++++++++++++ .../firebase/messaging/MessageTest.java | 95 ++++++++++++--- 4 files changed, 216 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/google/firebase/messaging/CriticalSound.java diff --git a/CHANGELOG.md b/CHANGELOG.md index f2767d13c..d59a9fbb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- `Aps` class now supports configuring a critical sound. A new + `CriticalSound` class has been introduced for this purpose. - [fixed] `Firestore` instances initialized by the SDK are now cleaned up, when `FirebaseApp.delete()` is called. diff --git a/src/main/java/com/google/firebase/messaging/Aps.java b/src/main/java/com/google/firebase/messaging/Aps.java index 17912d1e3..10c5af181 100644 --- a/src/main/java/com/google/firebase/messaging/Aps.java +++ b/src/main/java/com/google/firebase/messaging/Aps.java @@ -35,6 +35,8 @@ public class Aps { private Aps(Builder builder) { checkArgument(Strings.isNullOrEmpty(builder.alertString) || (builder.alert == null), "Multiple alert specifications (string and ApsAlert) found."); + checkArgument(Strings.isNullOrEmpty(builder.sound) || (builder.criticalSound == null), + "Multiple sound specifications (sound and CriticalSound) found."); ImmutableMap.Builder fields = ImmutableMap.builder(); if (builder.alert != null) { fields.put("alert", builder.alert); @@ -44,8 +46,10 @@ private Aps(Builder builder) { if (builder.badge != null) { fields.put("badge", builder.badge); } - if (builder.sound != null) { + if (!Strings.isNullOrEmpty(builder.sound)) { fields.put("sound", builder.sound); + } else if (builder.criticalSound != null) { + fields.put("sound", builder.criticalSound.getFields()); } if (builder.contentAvailable) { fields.put("content-available", 1); @@ -82,6 +86,7 @@ public static class Builder { private ApsAlert alert; private Integer badge; private String sound; + private CriticalSound criticalSound; private boolean contentAvailable; private boolean mutableContent; private String category; @@ -125,7 +130,8 @@ public Builder setBadge(int badge) { } /** - * Sets the sound to be played with the message. + * Sets the sound to be played with the message. For critical alerts use the + * {@link #setSound(CriticalSound)} method. * * @param sound Sound file name or {@code "default"}. * @return This builder. @@ -135,6 +141,17 @@ public Builder setSound(String sound) { return this; } + /** + * Sets the critical alert sound to be played with the message. + * + * @param sound A {@link CriticalSound} instance containing the alert sound configuration. + * @return This builder. + */ + public Builder setSound(CriticalSound sound) { + this.criticalSound = sound; + return this; + } + /** * Specifies whether to configure a background update notification. * diff --git a/src/main/java/com/google/firebase/messaging/CriticalSound.java b/src/main/java/com/google/firebase/messaging/CriticalSound.java new file mode 100644 index 000000000..ed293ba96 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/CriticalSound.java @@ -0,0 +1,115 @@ +/* + * Copyright 2018 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.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import java.util.Map; + +/** + * The sound configuration for APNs critical alerts. + */ +public final class CriticalSound { + + private final Map fields; + + private CriticalSound(Builder builder) { + checkArgument(!Strings.isNullOrEmpty(builder.name), "name must not be null or empty"); + ImmutableMap.Builder fields = ImmutableMap.builder() + .put("name", builder.name); + if (builder.critical) { + fields.put("critical", 1); + } + if (builder.volume != null) { + checkArgument(builder.volume >= 0 && builder.volume <= 1, + "volume must be in the interval [0,1]"); + fields.put("volume", builder.volume); + } + this.fields = fields.build(); + } + + Map getFields() { + return fields; + } + + /** + * Creates a new {@link CriticalSound.Builder}. + * + * @return A {@link CriticalSound.Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + + private boolean critical; + private String name; + private Double volume; + + private Builder() { + } + + /** + * Sets the critical alert flag on the sound configuration. + * + * @param critical True to set the critical alert flag. + * @return This builder. + */ + public Builder setCritical(boolean critical) { + this.critical = critical; + return this; + } + + /** + * The name of a sound file in your app's main bundle or in the {@code Library/Sounds} folder + * of your app’s container directory. Specify the string {@code default} to play the system + * sound. + * + * @param name Sound file name. + * @return This builder. + */ + public Builder setName(String name) { + this.name = name; + return this; + } + + /** + * The volume for the critical alert's sound. Must be a value between 0.0 (silent) and 1.0 + * (full volume). + * + * @param volume A volume between 0.0 (inclusive) and 1.0 (inclusive). + * @return This builder. + */ + public Builder setVolume(double volume) { + this.volume = volume; + return this; + } + + /** + * Builds a new {@link CriticalSound} instance from the fields set on this builder. + * + * @return A non-null {@link CriticalSound}. + * @throws IllegalArgumentException If the volume value is out of range. + */ + public CriticalSound build() { + return new CriticalSound(this); + } + } +} diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index a44cd2a81..5b53b1a4b 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -448,6 +448,72 @@ public void testApnsMessageWithPayloadAndAps() throws IOException { message); } + @Test + public void testApnsMessageWithCriticalSound() throws IOException { + Message message = Message.builder() + .setApnsConfig(ApnsConfig.builder() + .setAps(Aps.builder() + .setSound(CriticalSound.builder() // All fields + .setCritical(true) + .setName("default") + .setVolume(0.5) + .build()) + .build()) + .build()) + .setTopic("test-topic") + .build(); + Map payload = ImmutableMap.of( + "aps", ImmutableMap.builder() + .put("sound", ImmutableMap.of( + "critical", new BigDecimal(1), + "name", "default", + "volume", new BigDecimal(0.5))) + .build()); + assertJsonEquals( + ImmutableMap.of( + "topic", "test-topic", + "apns", ImmutableMap.of("payload", payload)), + message); + + message = Message.builder() + .setApnsConfig(ApnsConfig.builder() + .setAps(Aps.builder() + .setSound(CriticalSound.builder() // Name field only + .setName("default") + .build()) + .build()) + .build()) + .setTopic("test-topic") + .build(); + payload = ImmutableMap.of( + "aps", ImmutableMap.builder() + .put("sound", ImmutableMap.of("name", "default")) + .build()); + assertJsonEquals( + ImmutableMap.of( + "topic", "test-topic", + "apns", ImmutableMap.of("payload", payload)), + message); + } + + @Test + public void testInvalidCriticalSound() { + List soundBuilders = ImmutableList.of( + CriticalSound.builder(), + CriticalSound.builder().setCritical(true).setVolume(0.5), + CriticalSound.builder().setVolume(-0.1), + CriticalSound.builder().setVolume(1.1) + ); + for (int i = 0; i < soundBuilders.size(); i++) { + try { + soundBuilders.get(i).build(); + fail("No error thrown for invalid sound: " + i); + } catch (IllegalArgumentException expected) { + // expected + } + } + } + @Test public void testInvalidApnsConfig() { List configBuilders = ImmutableList.of( @@ -464,23 +530,22 @@ public void testInvalidApnsConfig() { } } - Aps.Builder builder = Aps.builder().setAlert("string").setAlert(ApsAlert.builder().build()); - try { - builder.build(); - fail("No error thrown for invalid aps"); - } catch (IllegalArgumentException expected) { - // expected - } - - builder = Aps.builder().setMutableContent(true).putCustomData("mutable-content", 1); - try { - builder.build(); - fail("No error thrown for invalid aps"); - } catch (IllegalArgumentException expected) { - // expected + List apsBuilders = ImmutableList.of( + Aps.builder().setAlert("string").setAlert(ApsAlert.builder().build()), + Aps.builder().setSound("default").setSound(CriticalSound.builder() + .setName("default") + .build()), + Aps.builder().setMutableContent(true).putCustomData("mutable-content", 1) + ); + for (int i = 0; i < apsBuilders.size(); i++) { + try { + apsBuilders.get(i).build(); + fail("No error thrown for invalid aps: " + i); + } catch (IllegalArgumentException expected) { + // expected + } } - List notificationBuilders = ImmutableList.of( ApsAlert.builder().addLocalizationArg("foo"), ApsAlert.builder().addTitleLocalizationArg("foo"),