diff --git a/keyserver/src/push/crypto.js b/keyserver/src/push/crypto.js --- a/keyserver/src/push/crypto.js +++ b/keyserver/src/push/crypto.js @@ -17,8 +17,9 @@ import { toBase64URL } from 'lib/utils/base64.js'; import type { - AndroidNotification, - AndroidNotificationPayload, + AndroidVisualNotification, + AndroidVisualNotificationPayload, + AndroidBadgeOnlyNotification, AndroidNotificationRescind, NotificationTargetDevice, } from './types.js'; @@ -197,18 +198,19 @@ } } -async function encryptAndroidNotification( +async function encryptAndroidVisualNotification( cookieID: string, - notification: AndroidNotification, - notificationSizeValidator?: AndroidNotification => boolean, + notification: AndroidVisualNotification, + notificationSizeValidator?: AndroidVisualNotification => boolean, blobHolder?: ?string, ): Promise<{ - +notification: AndroidNotification, + +notification: AndroidVisualNotification, +payloadSizeExceeded: boolean, +encryptionOrder?: number, }> { - const { id, keyserverID, badgeOnly, ...rest } = notification.data; - let unencryptedData = { badgeOnly, keyserverID }; + const { id, keyserverID, ...rest } = notification.data; + + let unencryptedData = { keyserverID }; if (id) { unencryptedData = { ...unencryptedData, id }; } @@ -221,7 +223,7 @@ let payloadSizeValidator; if (notificationSizeValidator) { payloadSizeValidator = ( - payload: AndroidNotificationPayload | { +encryptedPayload: string }, + payload: AndroidVisualNotificationPayload | { +encryptedPayload: string }, ) => { return notificationSizeValidator({ data: { ...unencryptedData, ...payload }, @@ -246,10 +248,10 @@ }; } -async function encryptAndroidNotificationRescind( +async function encryptAndroidSilentNotification( cookieID: string, - notification: AndroidNotificationRescind, -): Promise { + notification: AndroidNotificationRescind | AndroidBadgeOnlyNotification, +): Promise { // We don't validate payload size for rescind // since they are expected to be small and // never exceed any FCM limit @@ -383,22 +385,24 @@ return Promise.all(notificationPromises); } -function prepareEncryptedAndroidNotifications( +function prepareEncryptedAndroidVisualNotifications( devices: $ReadOnlyArray, - notification: AndroidNotification, - notificationSizeValidator?: (notification: AndroidNotification) => boolean, + notification: AndroidVisualNotification, + notificationSizeValidator?: ( + notification: AndroidVisualNotification, + ) => boolean, ): Promise< $ReadOnlyArray<{ +cookieID: string, +deviceToken: string, - +notification: AndroidNotification, + +notification: AndroidVisualNotification, +payloadSizeExceeded: boolean, +encryptionOrder?: number, }>, > { const notificationPromises = devices.map( async ({ deviceToken, cookieID, blobHolder }) => { - const notif = await encryptAndroidNotification( + const notif = await encryptAndroidVisualNotification( cookieID, notification, notificationSizeValidator, @@ -410,20 +414,20 @@ return Promise.all(notificationPromises); } -function prepareEncryptedAndroidNotificationRescinds( +function prepareEncryptedAndroidSilentNotifications( devices: $ReadOnlyArray, - notification: AndroidNotificationRescind, + notification: AndroidNotificationRescind | AndroidBadgeOnlyNotification, ): Promise< $ReadOnlyArray<{ +cookieID: string, +deviceToken: string, - +notification: AndroidNotificationRescind, + +notification: AndroidNotificationRescind | AndroidBadgeOnlyNotification, +encryptionOrder?: number, }>, > { const notificationPromises = devices.map( async ({ deviceToken, cookieID }) => { - const notif = await encryptAndroidNotificationRescind( + const notif = await encryptAndroidSilentNotification( cookieID, notification, ); @@ -500,8 +504,8 @@ export { prepareEncryptedAPNsNotifications, prepareEncryptedIOSNotificationRescind, - prepareEncryptedAndroidNotifications, - prepareEncryptedAndroidNotificationRescinds, + prepareEncryptedAndroidVisualNotifications, + prepareEncryptedAndroidSilentNotifications, prepareEncryptedWebNotifications, prepareEncryptedWNSNotifications, encryptBlobPayload, diff --git a/keyserver/src/push/rescind.js b/keyserver/src/push/rescind.js --- a/keyserver/src/push/rescind.js +++ b/keyserver/src/push/rescind.js @@ -12,7 +12,7 @@ import { tID } from 'lib/utils/validation-utils.js'; import { - prepareEncryptedAndroidNotificationRescinds, + prepareEncryptedAndroidSilentNotifications, prepareEncryptedIOSNotificationRescind, } from './crypto.js'; import { getAPNsNotificationTopic } from './providers.js'; @@ -378,12 +378,16 @@ keyserverID, }, }; - return await conditionallyEncryptNotification( + const targetedRescinds = await conditionallyEncryptNotification( notification, codeVersion, devices, - prepareEncryptedAndroidNotificationRescinds, + prepareEncryptedAndroidSilentNotifications, ); + return targetedRescinds.map(targetedRescind => ({ + ...targetedRescind, + priority: 'normal', + })); } export { rescindPushNotifs }; diff --git a/keyserver/src/push/send.js b/keyserver/src/push/send.js --- a/keyserver/src/push/send.js +++ b/keyserver/src/push/send.js @@ -43,7 +43,8 @@ import { tID, tPlatformDetails, tShape } from 'lib/utils/validation-utils.js'; import { - prepareEncryptedAndroidNotifications, + prepareEncryptedAndroidVisualNotifications, + prepareEncryptedAndroidSilentNotifications, prepareEncryptedAPNsNotifications, prepareEncryptedWebNotifications, prepareEncryptedWNSNotifications, @@ -51,7 +52,7 @@ import { getAPNsNotificationTopic } from './providers.js'; import { rescindPushNotifs } from './rescind.js'; import type { - AndroidNotification, + AndroidVisualNotification, NotificationTargetDevice, TargetedAndroidNotification, TargetedAPNsNotification, @@ -367,7 +368,7 @@ ); const preparePromise: Promise<$ReadOnlyArray> = (async () => { - const targetedNotifications = await prepareAndroidNotification( + const targetedNotifications = await prepareAndroidVisualNotification( { keyserverID, notifTexts, @@ -1167,7 +1168,7 @@ ...commonNativeNotifInputDataValidator.meta.props, dbID: t.String, }); -async function prepareAndroidNotification( +async function prepareAndroidVisualNotification( inputData: AndroidNotifInputData, devices: $ReadOnlyArray, ): Promise<$ReadOnlyArray> { @@ -1205,6 +1206,7 @@ (canDecryptNonCollapsibleTextNotifs && isNonCollapsibleTextNotif); const { merged, ...rest } = notifTexts; + const priority = 'high'; const notification = { data: { keyserverID, @@ -1247,12 +1249,13 @@ : notification; return devices.map(({ deviceToken }) => ({ + priority, notification: notificationToSend, deviceToken, })); } - const notificationsSizeValidator = (notif: AndroidNotification) => { + const notificationsSizeValidator = (notif: AndroidVisualNotification) => { const serializedNotif = JSON.stringify(notif); return ( !serializedNotif || @@ -1260,11 +1263,12 @@ ); }; - const notifsWithMessageInfos = await prepareEncryptedAndroidNotifications( - devices, - copyWithMessageInfos, - notificationsSizeValidator, - ); + const notifsWithMessageInfos = + await prepareEncryptedAndroidVisualNotifications( + devices, + copyWithMessageInfos, + notificationsSizeValidator, + ); const devicesWithExcessiveSizeNoHolders = notifsWithMessageInfos .filter(({ payloadSizeExceeded }) => payloadSizeExceeded) @@ -1273,6 +1277,7 @@ if (devicesWithExcessiveSizeNoHolders.length === 0) { return notifsWithMessageInfos.map( ({ notification: notif, deviceToken, encryptionOrder }) => ({ + priority, notification: notif, deviceToken, encryptionOrder, @@ -1319,14 +1324,16 @@ })); } - const notifsWithoutMessageInfos = await prepareEncryptedAndroidNotifications( - devicesWithExcessiveSize, - notification, - ); + const notifsWithoutMessageInfos = + await prepareEncryptedAndroidVisualNotifications( + devicesWithExcessiveSize, + notification, + ); const targetedNotifsWithMessageInfos = notifsWithMessageInfos .filter(({ payloadSizeExceeded }) => !payloadSizeExceeded) .map(({ notification: notif, deviceToken, encryptionOrder }) => ({ + priority, notification: notif, deviceToken, encryptionOrder, @@ -1334,6 +1341,7 @@ const targetedNotifsWithoutMessageInfos = notifsWithoutMessageInfos.map( ({ notification: notif, deviceToken, encryptionOrder }) => ({ + priority, notification: notif, deviceToken, encryptionOrder, @@ -1832,22 +1840,25 @@ if (androidVersionsToTokens) { for (const [versionKey, deviceInfos] of androidVersionsToTokens) { const { codeVersion, stateVersion } = stringToVersionKey(versionKey); - const notificationData = - codeVersion < 69 - ? { badge: unreadCount.toString() } - : { badge: unreadCount.toString(), badgeOnly: '1' }; + const priority = 'normal'; + const notificationData = { + badge: unreadCount.toString(), + badgeOnly: '1', + }; const notification = { data: { ...notificationData, keyserverID }, }; const preparePromise: Promise = (async () => { let targetedNotifications: $ReadOnlyArray; if (codeVersion > 222) { - const notificationsArray = await prepareEncryptedAndroidNotifications( - deviceInfos, - notification, - ); + const notificationsArray = + await prepareEncryptedAndroidSilentNotifications( + deviceInfos, + notification, + ); targetedNotifications = notificationsArray.map( ({ notification: notif, deviceToken, encryptionOrder }) => ({ + priority, notification: notif, deviceToken, encryptionOrder, @@ -1855,6 +1866,7 @@ ); } else { targetedNotifications = deviceInfos.map(({ deviceToken }) => ({ + priority, deviceToken, notification, })); diff --git a/keyserver/src/push/types.js b/keyserver/src/push/types.js --- a/keyserver/src/push/types.js +++ b/keyserver/src/push/types.js @@ -14,33 +14,33 @@ +encryptionOrder?: number, }; -type AndroidNotificationPayloadBase = { +type AndroidVisualNotificationPayloadBase = { +badge: string, - +body?: string, - +title?: string, + +body: string, + +title: string, +prefix?: string, - +threadID?: string, + +threadID: string, +collapseKey?: string, + +badgeOnly?: '0', +encryptionFailed?: '1', }; -export type AndroidNotificationPayload = +export type AndroidVisualNotificationPayload = | { - ...AndroidNotificationPayloadBase, + ...AndroidVisualNotificationPayloadBase, +messageInfos?: string, } | { - ...AndroidNotificationPayloadBase, + ...AndroidVisualNotificationPayloadBase, +blobHash: string, +encryptionKey: string, }; -export type AndroidNotification = { +export type AndroidVisualNotification = { +data: { +id?: string, - +badgeOnly?: string, +keyserverID: string, - ...AndroidNotificationPayload | { +encryptedPayload: string }, + ...AndroidVisualNotificationPayload | { +encryptedPayload: string }, }, }; @@ -60,8 +60,30 @@ }, }; +export type AndroidBadgeOnlyNotification = { + +data: { + +keyserverID: string, + ... + | { + +badge: string, + +badgeOnly: '1', + } + | { +encryptedPayload: string }, + }, +}; + +type AndroidNotificationWithPriority = + | { + +notification: AndroidVisualNotification, + +priority: 'high', + } + | { + +notification: AndroidBadgeOnlyNotification | AndroidNotificationRescind, + +priority: 'normal', + }; + export type TargetedAndroidNotification = { - +notification: AndroidNotification | AndroidNotificationRescind, + ...AndroidNotificationWithPriority, +deviceToken: string, +encryptionOrder?: number, }; diff --git a/keyserver/src/push/utils.js b/keyserver/src/push/utils.js --- a/keyserver/src/push/utils.js +++ b/keyserver/src/push/utils.js @@ -119,9 +119,7 @@ return { success: true }; } invariant(fcmProvider, `keyserver/secrets/${pushProfile}.json should exist`); - const options: Object = { - priority: 'high', - }; + const options: Object = {}; if (collapseKey) { options.collapseKey = collapseKey; } @@ -130,9 +128,13 @@ // multicast messages and one of the device tokens is invalid, the resultant // won't explain which of the device tokens is invalid. So we're forced to // avoid the multicast functionality and call it once per deviceToken. + const results = await Promise.all( - targetedNotifications.map(({ notification, deviceToken }) => { - return fcmSinglePush(fcmProvider, notification, deviceToken, options); + targetedNotifications.map(({ notification, deviceToken, priority }) => { + return fcmSinglePush(fcmProvider, notification, deviceToken, { + ...options, + priority, + }); }), ); diff --git a/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java b/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java --- a/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java +++ b/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java @@ -103,7 +103,10 @@ @Override public void onMessageReceived(RemoteMessage message) { - if (message.getData().get(KEYSERVER_ID_KEY) == null) { + handleAlteredNotificationPriority(message); + + if (StaffUtils.isStaffRelease() && + message.getData().get(KEYSERVER_ID_KEY) == null) { displayErrorMessageNotification( "Received notification without keyserver ID.", "Missing keyserver ID.", @@ -170,6 +173,23 @@ this.displayNotification(message); } + private void handleAlteredNotificationPriority(RemoteMessage message) { + if (!StaffUtils.isStaffRelease()) { + return; + } + + int originalPriority = message.getOriginalPriority(); + int priority = message.getPriority(); + + if (priority != originalPriority && + originalPriority == RemoteMessage.PRIORITY_HIGH) { + displayErrorMessageNotification( + "System changed notification priority from HIGH to NORMAL", + "Notification deprioritised.", + null); + } + } + private boolean isAppInForeground() { return ProcessLifecycleOwner.get().getLifecycle().getCurrentState() == Lifecycle.State.RESUMED;