Skip to content

Commit 424b2b8

Browse files
authored
Add backend support for push notification sounds with custom extensions (#2759)
# What this PR does Instead of always adding `.aiff` or `.mp3` at the end of notification sound names depending on the platform (iOS vs Android), add them only if no extension is present already. This should make it possible to use sounds with custom extensions. ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
1 parent bb9f647 commit 424b2b8

10 files changed

+107
-58
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- Shift Swap Requests Web UI ([#2593](https://github.com/grafana/oncall/issues/2593))
13-
- Final schedule shifts should lay in one line [1665](https://github.com/grafana/oncall/issues/1665)
13+
- Final schedule shifts should lay in one line ([#1665](https://github.com/grafana/oncall/issues/1665))
14+
- Add backend support for push notification sounds with custom extensions by @vadimkerr ([#2759](https://github.com/grafana/oncall/pull/2759))
1415

1516
### Changed
1617

engine/apps/mobile_app/demo_push.py

+8-14
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from firebase_admin.messaging import APNSPayload, Aps, ApsAlert, CriticalSound, Message
77

88
from apps.mobile_app.exceptions import DeviceNotSet
9-
from apps.mobile_app.tasks import FCMMessageData, MessageType, _construct_fcm_message, _send_push_notification, logger
9+
from apps.mobile_app.tasks import _construct_fcm_message, _send_push_notification, logger
10+
from apps.mobile_app.types import FCMMessageData, MessageType, Platform
1011
from apps.user_management.models import User
1112

1213
if typing.TYPE_CHECKING:
@@ -38,27 +39,22 @@ def _get_test_escalation_fcm_message(user: User, device_to_notify: "FCMDevice",
3839

3940
# APNS only allows to specify volume for critical notifications
4041
apns_volume = mobile_app_user_settings.important_notification_volume if critical else None
41-
apns_sound_name = (
42-
mobile_app_user_settings.important_notification_sound_name
43-
if critical
44-
else mobile_app_user_settings.default_notification_sound_name
45-
) + MobileAppUserSettings.IOS_SOUND_NAME_EXTENSION # iOS app expects the filename to have an extension
42+
message_type = MessageType.IMPORTANT if critical else MessageType.DEFAULT
43+
apns_sound_name = mobile_app_user_settings.get_notification_sound_name(message_type, Platform.IOS)
4644

4745
fcm_message_data: FCMMessageData = {
4846
"title": get_test_push_title(critical),
4947
# Pass user settings, so the Android app can use them to play the correct sound and volume
50-
"default_notification_sound_name": (
51-
mobile_app_user_settings.default_notification_sound_name
52-
+ MobileAppUserSettings.ANDROID_SOUND_NAME_EXTENSION
48+
"default_notification_sound_name": mobile_app_user_settings.get_notification_sound_name(
49+
MessageType.DEFAULT, Platform.ANDROID
5350
),
5451
"default_notification_volume_type": mobile_app_user_settings.default_notification_volume_type,
5552
"default_notification_volume": str(mobile_app_user_settings.default_notification_volume),
5653
"default_notification_volume_override": json.dumps(
5754
mobile_app_user_settings.default_notification_volume_override
5855
),
59-
"important_notification_sound_name": (
60-
mobile_app_user_settings.important_notification_sound_name
61-
+ MobileAppUserSettings.ANDROID_SOUND_NAME_EXTENSION
56+
"important_notification_sound_name": mobile_app_user_settings.get_notification_sound_name(
57+
MessageType.IMPORTANT, Platform.ANDROID
6258
),
6359
"important_notification_volume_type": mobile_app_user_settings.important_notification_volume_type,
6460
"important_notification_volume": str(mobile_app_user_settings.important_notification_volume),
@@ -84,8 +80,6 @@ def _get_test_escalation_fcm_message(user: User, device_to_notify: "FCMDevice",
8480
),
8581
)
8682

87-
message_type = MessageType.CRITICAL if critical else MessageType.NORMAL
88-
8983
return _construct_fcm_message(message_type, device_to_notify, thread_id, fcm_message_data, apns_payload)
9084

9185

engine/apps/mobile_app/models.py

+20
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from apps.auth_token import constants, crypto
1212
from apps.auth_token.models import BaseAuthToken
13+
from apps.mobile_app.types import MessageType, Platform
1314

1415
if typing.TYPE_CHECKING:
1516
from apps.user_management.models import Organization, User
@@ -175,3 +176,22 @@ class VolumeType(models.TextChoices):
175176

176177
locale = models.CharField(max_length=50, null=True)
177178
time_zone = models.CharField(max_length=100, default="UTC")
179+
180+
def get_notification_sound_name(self, message_type: MessageType, platform: Platform) -> str:
181+
sound_name = {
182+
MessageType.DEFAULT: self.default_notification_sound_name,
183+
MessageType.IMPORTANT: self.important_notification_sound_name,
184+
MessageType.INFO: self.info_notification_sound_name,
185+
}[message_type]
186+
187+
# If sound name already contains an extension, return it as is
188+
if "." in sound_name:
189+
return sound_name
190+
191+
# Add appropriate extension based on platform, for cases when no extension is specified in the sound name
192+
extension = {
193+
Platform.IOS: self.IOS_SOUND_NAME_EXTENSION,
194+
Platform.ANDROID: self.ANDROID_SOUND_NAME_EXTENSION,
195+
}[platform]
196+
197+
return f"{sound_name}{extension}"

engine/apps/mobile_app/tasks.py

+14-36
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import logging
44
import math
55
import typing
6-
from enum import Enum
76

87
import humanize
98
import pytz
@@ -20,6 +19,7 @@
2019
from apps.alerts.models import AlertGroup
2120
from apps.base.utils import live_settings
2221
from apps.mobile_app.alert_rendering import get_push_notification_subtitle
22+
from apps.mobile_app.types import FCMMessageData, MessageType, Platform
2323
from apps.schedules.models import ShiftSwapRequest
2424
from apps.schedules.models.on_call_schedule import OnCallSchedule, ScheduleEvent
2525
from apps.user_management.models import User
@@ -36,18 +36,6 @@
3636
logger.setLevel(logging.DEBUG)
3737

3838

39-
class MessageType(str, Enum):
40-
NORMAL = "oncall.message"
41-
CRITICAL = "oncall.critical_message"
42-
INFO = "oncall.info"
43-
44-
45-
class FCMMessageData(typing.TypedDict):
46-
title: str
47-
subtitle: typing.Optional[str]
48-
body: typing.Optional[str]
49-
50-
5139
def send_push_notification_to_fcm_relay(message: Message) -> requests.Response:
5240
"""
5341
Send push notification to FCM relay on cloud instance: apps.mobile_app.fcm_relay.FCMRelayView
@@ -168,11 +156,8 @@ def _get_alert_group_escalation_fcm_message(
168156

169157
# APNS only allows to specify volume for critical notifications
170158
apns_volume = mobile_app_user_settings.important_notification_volume if critical else None
171-
apns_sound_name = (
172-
mobile_app_user_settings.important_notification_sound_name
173-
if critical
174-
else mobile_app_user_settings.default_notification_sound_name
175-
) + MobileAppUserSettings.IOS_SOUND_NAME_EXTENSION # iOS app expects the filename to have an extension
159+
message_type = MessageType.IMPORTANT if critical else MessageType.DEFAULT
160+
apns_sound_name = mobile_app_user_settings.get_notification_sound_name(message_type, Platform.IOS)
176161

177162
fcm_message_data: FCMMessageData = {
178163
"title": alert_title,
@@ -183,18 +168,16 @@ def _get_alert_group_escalation_fcm_message(
183168
# alert_group.status is an int so it must be casted...
184169
"status": str(alert_group.status),
185170
# Pass user settings, so the Android app can use them to play the correct sound and volume
186-
"default_notification_sound_name": (
187-
mobile_app_user_settings.default_notification_sound_name
188-
+ MobileAppUserSettings.ANDROID_SOUND_NAME_EXTENSION
171+
"default_notification_sound_name": mobile_app_user_settings.get_notification_sound_name(
172+
MessageType.DEFAULT, Platform.ANDROID
189173
),
190174
"default_notification_volume_type": mobile_app_user_settings.default_notification_volume_type,
191175
"default_notification_volume": str(mobile_app_user_settings.default_notification_volume),
192176
"default_notification_volume_override": json.dumps(
193177
mobile_app_user_settings.default_notification_volume_override
194178
),
195-
"important_notification_sound_name": (
196-
mobile_app_user_settings.important_notification_sound_name
197-
+ MobileAppUserSettings.ANDROID_SOUND_NAME_EXTENSION
179+
"important_notification_sound_name": mobile_app_user_settings.get_notification_sound_name(
180+
MessageType.IMPORTANT, Platform.ANDROID
198181
),
199182
"important_notification_volume_type": mobile_app_user_settings.important_notification_volume_type,
200183
"important_notification_volume": str(mobile_app_user_settings.important_notification_volume),
@@ -222,8 +205,6 @@ def _get_alert_group_escalation_fcm_message(
222205
),
223206
)
224207

225-
message_type = MessageType.CRITICAL if critical else MessageType.NORMAL
226-
227208
return _construct_fcm_message(message_type, device_to_notify, thread_id, fcm_message_data, apns_payload)
228209

229210

@@ -268,8 +249,6 @@ def _get_youre_going_oncall_fcm_message(
268249
thread_id = f"{schedule.public_primary_key}:{user.public_primary_key}:going-oncall"
269250

270251
mobile_app_user_settings, _ = MobileAppUserSettings.objects.get_or_create(user=user)
271-
info_notification_sound_name = mobile_app_user_settings.info_notification_sound_name
272-
273252
notification_title = _get_youre_going_oncall_notification_title(seconds_until_going_oncall)
274253
notification_subtitle = _get_youre_going_oncall_notification_subtitle(
275254
schedule, schedule_event, mobile_app_user_settings
@@ -278,7 +257,9 @@ def _get_youre_going_oncall_fcm_message(
278257
data: FCMMessageData = {
279258
"title": notification_title,
280259
"subtitle": notification_subtitle,
281-
"info_notification_sound_name": f"{info_notification_sound_name}{MobileAppUserSettings.ANDROID_SOUND_NAME_EXTENSION}",
260+
"info_notification_sound_name": mobile_app_user_settings.get_notification_sound_name(
261+
MessageType.INFO, Platform.ANDROID
262+
),
282263
"info_notification_volume_type": mobile_app_user_settings.info_notification_volume_type,
283264
"info_notification_volume": str(mobile_app_user_settings.info_notification_volume),
284265
"info_notification_volume_override": json.dumps(mobile_app_user_settings.info_notification_volume_override),
@@ -290,7 +271,7 @@ def _get_youre_going_oncall_fcm_message(
290271
alert=ApsAlert(title=notification_title, subtitle=notification_subtitle),
291272
sound=CriticalSound(
292273
critical=False,
293-
name=f"{info_notification_sound_name}{MobileAppUserSettings.IOS_SOUND_NAME_EXTENSION}",
274+
name=mobile_app_user_settings.get_notification_sound_name(MessageType.INFO, Platform.IOS),
294275
),
295276
custom_data={
296277
"interruption-level": "time-sensitive",
@@ -641,8 +622,6 @@ def _shift_swap_request_fcm_message(
641622
device_to_notify: "FCMDevice",
642623
mobile_app_user_settings: "MobileAppUserSettings",
643624
) -> Message:
644-
from apps.mobile_app.models import MobileAppUserSettings
645-
646625
thread_id = f"{shift_swap_request.public_primary_key}:{user.public_primary_key}:ssr"
647626
notification_title = "New shift swap request"
648627
beneficiary_name = shift_swap_request.beneficiary.name or shift_swap_request.beneficiary.username
@@ -655,8 +634,8 @@ def _shift_swap_request_fcm_message(
655634
"title": notification_title,
656635
"subtitle": notification_subtitle,
657636
"route": route,
658-
"info_notification_sound_name": (
659-
mobile_app_user_settings.info_notification_sound_name + MobileAppUserSettings.ANDROID_SOUND_NAME_EXTENSION
637+
"info_notification_sound_name": mobile_app_user_settings.get_notification_sound_name(
638+
MessageType.INFO, Platform.ANDROID
660639
),
661640
"info_notification_volume_type": mobile_app_user_settings.info_notification_volume_type,
662641
"info_notification_volume": str(mobile_app_user_settings.info_notification_volume),
@@ -669,8 +648,7 @@ def _shift_swap_request_fcm_message(
669648
alert=ApsAlert(title=notification_title, subtitle=notification_subtitle),
670649
sound=CriticalSound(
671650
critical=False,
672-
name=mobile_app_user_settings.info_notification_sound_name
673-
+ MobileAppUserSettings.IOS_SOUND_NAME_EXTENSION,
651+
name=mobile_app_user_settings.get_notification_sound_name(MessageType.INFO, Platform.IOS),
674652
),
675653
custom_data={
676654
"interruption-level": "time-sensitive",

engine/apps/mobile_app/tests/test_demo_push.py

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def test_test_escalation_fcm_message_user_settings(
3434
assert message.apns.payload.aps.badge is None
3535
assert message.apns.payload.aps.alert.title == get_test_push_title(critical=False)
3636
assert message.data["title"] == get_test_push_title(critical=False)
37+
assert message.data["type"] == "oncall.message"
3738

3839

3940
@pytest.mark.django_db
@@ -67,6 +68,7 @@ def test_escalation_fcm_message_user_settings_critical(
6768
assert message.apns.payload.aps.badge is None
6869
assert message.apns.payload.aps.alert.title == get_test_push_title(critical=True)
6970
assert message.data["title"] == get_test_push_title(critical=True)
71+
assert message.data["type"] == "oncall.critical_message"
7072

7173

7274
@pytest.mark.django_db

engine/apps/mobile_app/tests/test_notify_user.py

+2
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ def test_fcm_message_user_settings(
234234
assert message.data["important_notification_volume"] == "0.8"
235235
assert message.data["important_notification_volume_override"] == "true"
236236
assert message.data["important_notification_override_dnd"] == "true"
237+
assert message.data["type"] == "oncall.message"
237238

238239
# Check APNS notification sound is set correctly
239240
apns_sound = message.apns.payload.aps.sound
@@ -265,6 +266,7 @@ def test_fcm_message_user_settings_critical(
265266
assert message.data["important_notification_volume"] == "0.8"
266267
assert message.data["important_notification_volume_override"] == "true"
267268
assert message.data["important_notification_override_dnd"] == "true"
269+
assert message.data["type"] == "oncall.critical_message"
268270

269271
# Check APNS notification sound is set correctly
270272
apns_sound = message.apns.payload.aps.sound

engine/apps/mobile_app/tests/test_shift_swap_request.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from apps.mobile_app.tasks import (
1010
SSR_EARLIEST_NOTIFICATION_OFFSET,
1111
SSR_NOTIFICATION_WINDOW,
12-
MessageType,
1312
_get_shift_swap_requests_to_notify,
1413
_has_user_been_notified_for_shift_swap_request,
1514
_mark_shift_swap_request_notified_for_user,
@@ -261,7 +260,7 @@ def test_notify_user_about_shift_swap_request(make_organization, make_user, make
261260
assert mock_send_push_notification.call_args.args[0] == device_to_notify
262261

263262
message: Message = mock_send_push_notification.call_args.args[1]
264-
assert message.data["type"] == MessageType.INFO
263+
assert message.data["type"] == "oncall.info"
265264
assert message.data["title"] == "New shift swap request"
266265
assert message.data["subtitle"] == "John Doe, Test Schedule"
267266
assert (

engine/apps/mobile_app/tests/test_user_settings.py

+35
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
from rest_framework import status
44
from rest_framework.test import APIClient
55

6+
from apps.mobile_app.models import MobileAppUserSettings
7+
from apps.mobile_app.types import MessageType, Platform
8+
69

710
@pytest.mark.django_db
811
def test_user_settings_get(make_organization_and_user_with_mobile_app_auth_token):
@@ -140,3 +143,35 @@ def test_user_settings_time_zone_must_be_valid(make_organization_and_user_with_m
140143

141144
response = client.put(url, data=null_timezone, format="json", HTTP_AUTHORIZATION=auth_token)
142145
assert response.status_code == status.HTTP_400_BAD_REQUEST
146+
147+
148+
@pytest.mark.parametrize(
149+
"message_type,platform,sound_names,expected_sound_name",
150+
[
151+
(MessageType.DEFAULT, Platform.ANDROID, ["default", "empty", "empty"], "default.mp3"),
152+
(MessageType.DEFAULT, Platform.ANDROID, ["default.extension", "empty", "empty"], "default.extension"),
153+
(MessageType.DEFAULT, Platform.IOS, ["default", "empty", "empty"], "default.aiff"),
154+
(MessageType.DEFAULT, Platform.IOS, ["default.extension", "empty", "empty"], "default.extension"),
155+
(MessageType.IMPORTANT, Platform.ANDROID, ["empty", "important", "empty"], "important.mp3"),
156+
(MessageType.IMPORTANT, Platform.ANDROID, ["empty", "important.extension", "empty"], "important.extension"),
157+
(MessageType.IMPORTANT, Platform.IOS, ["empty", "important", "empty"], "important.aiff"),
158+
(MessageType.IMPORTANT, Platform.IOS, ["empty", "important.extension", "empty"], "important.extension"),
159+
(MessageType.INFO, Platform.ANDROID, ["empty", "empty", "info"], "info.mp3"),
160+
(MessageType.INFO, Platform.ANDROID, ["empty", "empty", "info.extension"], "info.extension"),
161+
(MessageType.INFO, Platform.IOS, ["empty", "empty", "info"], "info.aiff"),
162+
(MessageType.INFO, Platform.IOS, ["empty", "empty", "info.extension"], "info.extension"),
163+
],
164+
)
165+
@pytest.mark.django_db
166+
def test_get_notification_sound_name(
167+
make_organization_and_user, message_type, platform, sound_names, expected_sound_name
168+
):
169+
organization, user = make_organization_and_user()
170+
mobile_app_user_settings = MobileAppUserSettings.objects.create(
171+
user=user,
172+
default_notification_sound_name=sound_names[0],
173+
important_notification_sound_name=sound_names[1],
174+
info_notification_sound_name=sound_names[2],
175+
)
176+
177+
assert mobile_app_user_settings.get_notification_sound_name(message_type, platform) == expected_sound_name

engine/apps/mobile_app/tests/test_your_going_oncall_notification.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from apps.mobile_app import tasks
1010
from apps.mobile_app.models import FCMDevice, MobileAppUserSettings
11+
from apps.mobile_app.types import MessageType, Platform
1112
from apps.schedules.models import OnCallScheduleCalendar, OnCallScheduleICal, OnCallScheduleWeb
1213
from apps.schedules.models.on_call_schedule import ScheduleEvent
1314

@@ -217,9 +218,7 @@ def test_get_youre_going_oncall_fcm_message(
217218
data = {
218219
"title": mock_notification_title,
219220
"subtitle": mock_notification_subtitle,
220-
"info_notification_sound_name": (
221-
maus.info_notification_sound_name + MobileAppUserSettings.ANDROID_SOUND_NAME_EXTENSION
222-
),
221+
"info_notification_sound_name": maus.get_notification_sound_name(MessageType.INFO, Platform.ANDROID),
223222
"info_notification_volume_type": maus.info_notification_volume_type,
224223
"info_notification_volume": str(maus.info_notification_volume),
225224
"info_notification_volume_override": json.dumps(maus.info_notification_volume_override),
@@ -233,7 +232,7 @@ def test_get_youre_going_oncall_fcm_message(
233232

234233
mock_aps_alert.assert_called_once_with(title=mock_notification_title, subtitle=mock_notification_subtitle)
235234
mock_critical_sound.assert_called_once_with(
236-
critical=False, name=maus.info_notification_sound_name + MobileAppUserSettings.IOS_SOUND_NAME_EXTENSION
235+
critical=False, name=maus.get_notification_sound_name(MessageType.INFO, Platform.IOS)
237236
)
238237
mock_aps.assert_called_once_with(
239238
thread_id=notification_thread_id,
@@ -249,7 +248,7 @@ def test_get_youre_going_oncall_fcm_message(
249248
mock_get_youre_going_oncall_notification_title.assert_called_once_with(seconds_until_going_oncall)
250249

251250
mock_construct_fcm_message.assert_called_once_with(
252-
tasks.MessageType.INFO, device, notification_thread_id, data, mock_apns_payload.return_value
251+
MessageType.INFO, device, notification_thread_id, data, mock_apns_payload.return_value
253252
)
254253

255254

engine/apps/mobile_app/types.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import typing
2+
from enum import StrEnum
3+
4+
5+
class MessageType(StrEnum):
6+
DEFAULT = "oncall.message"
7+
IMPORTANT = "oncall.critical_message"
8+
INFO = "oncall.info"
9+
10+
11+
class Platform(StrEnum):
12+
ANDROID = "android"
13+
IOS = "ios"
14+
15+
16+
class FCMMessageData(typing.TypedDict):
17+
title: str
18+
subtitle: typing.Optional[str]
19+
body: typing.Optional[str]

0 commit comments

Comments
 (0)