Skip to content

Commit a80078f

Browse files
authored
Implemented messaging.CriticalSound API (#238)
* Implemented messaging.CriticalSound API * Disallowing empty strings in APNs sound config * Minor simplification * Accepting any Truthy value for critical
1 parent 6840951 commit a80078f

File tree

3 files changed

+159
-4
lines changed

3 files changed

+159
-4
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Unreleased
22

3+
- `messaging.Aps` class now supports configuring a critical sound. A new
4+
`messaging.CriticalSound` class has been introduced for this purpose.
35
- [changed] Dropped support for Python 3.3.
46

57
# v2.14.0

firebase_admin/messaging.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
# pylint: disable=too-many-lines
16+
1517
"""Firebase Cloud Messaging module."""
1618

1719
import datetime
@@ -395,7 +397,8 @@ class Aps(object):
395397
Args:
396398
alert: A string or a ``messaging.ApsAlert`` instance (optional).
397399
badge: A number representing the badge to be displayed with the message (optional).
398-
sound: Name of the sound file to be played with the message (optional).
400+
sound: Name of the sound file to be played with the message or a
401+
``messaging.CriticalSound`` instance (optional).
399402
content_available: A boolean indicating whether to configure a background update
400403
notification (optional).
401404
category: String identifier representing the message type (optional).
@@ -418,6 +421,25 @@ def __init__(self, alert=None, badge=None, sound=None, content_available=None, c
418421
self.custom_data = custom_data
419422

420423

424+
class CriticalSound(object):
425+
"""Critical alert sound configuration that can be included in ``messaging.Aps``.
426+
427+
Args:
428+
name: The name of a sound file in your app's main bundle or in the ``Library/Sounds``
429+
folder of your app's container directory. Specify the string ``default`` to play the
430+
system sound.
431+
critical: Set to ``True`` to set the critical alert flag on the sound configuration
432+
(optional).
433+
volume: The volume for the critical alert's sound. Must be a value between 0.0 (silent)
434+
and 1.0 (full volume) (optional).
435+
"""
436+
437+
def __init__(self, name, critical=None, volume=None):
438+
self.name = name
439+
self.critical = critical
440+
self.volume = volume
441+
442+
421443
class ApsAlert(object):
422444
"""An alert that can be included in ``messaging.Aps``.
423445
@@ -738,7 +760,7 @@ def encode_aps(cls, aps):
738760
result = {
739761
'alert': cls.encode_aps_alert(aps.alert),
740762
'badge': _Validators.check_number('Aps.badge', aps.badge),
741-
'sound': _Validators.check_string('Aps.sound', aps.sound),
763+
'sound': cls.encode_aps_sound(aps.sound),
742764
'category': _Validators.check_string('Aps.category', aps.category),
743765
'thread-id': _Validators.check_string('Aps.thread_id', aps.thread_id),
744766
}
@@ -756,6 +778,29 @@ def encode_aps(cls, aps):
756778
result[key] = val
757779
return cls.remove_null_values(result)
758780

781+
@classmethod
782+
def encode_aps_sound(cls, sound):
783+
"""Encodes an APNs sound configuration into JSON."""
784+
if sound is None:
785+
return None
786+
if sound and isinstance(sound, six.string_types):
787+
return sound
788+
if not isinstance(sound, CriticalSound):
789+
raise ValueError(
790+
'Aps.sound must be a non-empty string or an instance of CriticalSound class.')
791+
result = {
792+
'name': _Validators.check_string('CriticalSound.name', sound.name, non_empty=True),
793+
'volume': _Validators.check_number('CriticalSound.volume', sound.volume),
794+
}
795+
if sound.critical:
796+
result['critical'] = 1
797+
if not result['name']:
798+
raise ValueError('CriticalSond.name must be a non-empty string.')
799+
volume = result['volume']
800+
if volume is not None and (volume < 0 or volume > 1):
801+
raise ValueError('CriticalSound.volume must be in the interval [0,1].')
802+
return cls.remove_null_values(result)
803+
759804
@classmethod
760805
def encode_aps_alert(cls, alert):
761806
"""Encodes an ApsAlert instance into JSON."""

tests/test_messaging.py

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -721,12 +721,12 @@ def test_invalid_badge(self, data):
721721
expected = 'Aps.badge must be a number.'
722722
assert str(excinfo.value) == expected
723723

724-
@pytest.mark.parametrize('data', NON_STRING_ARGS)
724+
@pytest.mark.parametrize('data', NON_STRING_ARGS + [''])
725725
def test_invalid_sound(self, data):
726726
aps = messaging.Aps(sound=data)
727727
with pytest.raises(ValueError) as excinfo:
728728
self._encode_aps(aps)
729-
expected = 'Aps.sound must be a string.'
729+
expected = 'Aps.sound must be a non-empty string or an instance of CriticalSound class.'
730730
assert str(excinfo.value) == expected
731731

732732
@pytest.mark.parametrize('data', NON_STRING_ARGS)
@@ -832,6 +832,114 @@ def test_aps_custom_data(self):
832832
check_encoding(msg, expected)
833833

834834

835+
class TestApsSoundEncoder(object):
836+
837+
def _check_sound(self, sound):
838+
with pytest.raises(ValueError) as excinfo:
839+
check_encoding(messaging.Message(
840+
topic='topic', apns=messaging.APNSConfig(
841+
payload=messaging.APNSPayload(aps=messaging.Aps(sound=sound))
842+
)
843+
))
844+
return excinfo
845+
846+
@pytest.mark.parametrize('data', NON_STRING_ARGS)
847+
def test_invalid_name(self, data):
848+
sound = messaging.CriticalSound(name=data)
849+
excinfo = self._check_sound(sound)
850+
expected = 'CriticalSound.name must be a non-empty string.'
851+
assert str(excinfo.value) == expected
852+
853+
@pytest.mark.parametrize('data', [list(), tuple(), dict(), 'foo'])
854+
def test_invalid_volume(self, data):
855+
sound = messaging.CriticalSound(name='default', volume=data)
856+
excinfo = self._check_sound(sound)
857+
expected = 'CriticalSound.volume must be a number.'
858+
assert str(excinfo.value) == expected
859+
860+
@pytest.mark.parametrize('data', [-0.1, 1.1])
861+
def test_volume_out_of_range(self, data):
862+
sound = messaging.CriticalSound(name='default', volume=data)
863+
excinfo = self._check_sound(sound)
864+
expected = 'CriticalSound.volume must be in the interval [0,1].'
865+
assert str(excinfo.value) == expected
866+
867+
def test_sound_string(self):
868+
msg = messaging.Message(
869+
topic='topic',
870+
apns=messaging.APNSConfig(
871+
payload=messaging.APNSPayload(aps=messaging.Aps(sound='default'))
872+
)
873+
)
874+
expected = {
875+
'topic': 'topic',
876+
'apns': {
877+
'payload': {
878+
'aps': {
879+
'sound': 'default',
880+
},
881+
}
882+
},
883+
}
884+
check_encoding(msg, expected)
885+
886+
def test_critical_sound(self):
887+
msg = messaging.Message(
888+
topic='topic',
889+
apns=messaging.APNSConfig(
890+
payload=messaging.APNSPayload(
891+
aps=messaging.Aps(
892+
sound=messaging.CriticalSound(
893+
name='default',
894+
critical=True,
895+
volume=0.5
896+
)
897+
),
898+
)
899+
)
900+
)
901+
expected = {
902+
'topic': 'topic',
903+
'apns': {
904+
'payload': {
905+
'aps': {
906+
'sound': {
907+
'name': 'default',
908+
'critical': 1,
909+
'volume': 0.5,
910+
},
911+
},
912+
}
913+
},
914+
}
915+
check_encoding(msg, expected)
916+
917+
def test_critical_sound_name_only(self):
918+
msg = messaging.Message(
919+
topic='topic',
920+
apns=messaging.APNSConfig(
921+
payload=messaging.APNSPayload(
922+
aps=messaging.Aps(
923+
sound=messaging.CriticalSound(name='default')
924+
),
925+
)
926+
)
927+
)
928+
expected = {
929+
'topic': 'topic',
930+
'apns': {
931+
'payload': {
932+
'aps': {
933+
'sound': {
934+
'name': 'default',
935+
},
936+
},
937+
}
938+
},
939+
}
940+
check_encoding(msg, expected)
941+
942+
835943
class TestApsAlertEncoder(object):
836944

837945
def _check_alert(self, alert):

0 commit comments

Comments
 (0)