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 @@ -258,8 +260,23 @@ cookieID, unencryptedPayload, ); + if (resultPayload.encryptedPayload) { + return { + data: { keyserverID, ...resultPayload }, + }; + } + + if (resultPayload.rescind) { + return { + data: { keyserverID, ...resultPayload }, + }; + } + return { - data: { keyserverID, ...resultPayload }, + data: { + keyserverID, + ...resultPayload, + }, }; } @@ -383,22 +400,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 +429,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 +519,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> { @@ -1239,6 +1240,7 @@ data: { ...notification.data, messageInfos }, }; + const priority = 'high'; if (!shouldBeEncrypted) { const notificationToSend = Buffer.byteLength(JSON.stringify(copyWithMessageInfos)) <= @@ -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 notificationData = { + badge: unreadCount.toString(), + badgeOnly: '1', + }; const notification = { data: { ...notificationData, keyserverID }, }; const preparePromise: Promise = (async () => { let targetedNotifications: $ReadOnlyArray; + const priority = 'normal'; 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,38 +14,49 @@ +encryptionOrder?: number, }; -type AndroidNotificationPayloadBase = { +export type AndroidVisualNotificationPayloadBase = $ReadOnly<{ +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 = $ReadOnly< | { - ...AndroidNotificationPayloadBase, + ...AndroidVisualNotificationPayloadBase, +messageInfos?: string, } | { - ...AndroidNotificationPayloadBase, + ...AndroidVisualNotificationPayloadBase, +blobHash: string, +encryptionKey: string, - }; + }, +>; -export type AndroidNotification = { - +data: { +export type AndroidVisualNotification = { + +data: $ReadOnly<{ +id?: string, - +badgeOnly?: string, +keyserverID: string, - ...AndroidNotificationPayload | { +encryptedPayload: string }, - }, + ... + | { + ...AndroidVisualNotificationPayloadBase, + +messageInfos?: string, + } + | { + ...AndroidVisualNotificationPayloadBase, + +blobHash: string, + +encryptionKey: string, + } + | { +encryptedPayload: string }, + }>, }; export type AndroidNotificationRescind = { - +data: { + +data: $ReadOnly<{ +keyserverID: string, ... | { @@ -57,14 +68,37 @@ +encryptionFailed?: string, } | { +encryptedPayload: string }, - }, + }>, }; -export type TargetedAndroidNotification = { - +notification: AndroidNotification | AndroidNotificationRescind, +export type AndroidBadgeOnlyNotification = { + +data: $ReadOnly<{ + +keyserverID: string, + ... + | { + +badge: string, + +badgeOnly: '1', + +encryptionFailed?: string, + } + | { +encryptedPayload: string }, + }>, +}; + +type AndroidNotificationWithPriority = + | { + +notification: AndroidVisualNotification, + +priority: 'high', + } + | { + +notification: AndroidBadgeOnlyNotification | AndroidNotificationRescind, + +priority: 'normal', + }; + +export type TargetedAndroidNotification = $ReadOnly<{ + ...AndroidNotificationWithPriority, +deviceToken: string, +encryptionOrder?: number, -}; +}>; export type TargetedWebNotification = { +notification: WebNotification, 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 @@ -38,6 +38,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; +import java.util.Map; import me.leolin.shortcutbadger.ShortcutBadger; import org.json.JSONException; import org.json.JSONObject; @@ -57,6 +58,8 @@ private static final String KEYSERVER_ID_KEY = "keyserverID"; private static final String CHANNEL_ID = "default"; private static final long[] VIBRATION_SPEC = {500, 500}; + private static final Map NOTIF_PRIORITY_VERBOSE = + Map.of(0, "UNKNOWN", 1, "HIGH", 2, "NORMAL"); // Those and future MMKV-related constants should match // similar constants in NotificationService.mm @@ -103,7 +106,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 +176,32 @@ this.displayNotification(message); } + private void handleAlteredNotificationPriority(RemoteMessage message) { + if (!StaffUtils.isStaffRelease()) { + return; + } + + int originalPriority = message.getOriginalPriority(); + int priority = message.getPriority(); + + String priorityName = NOTIF_PRIORITY_VERBOSE.get(priority); + String originalPriorityName = NOTIF_PRIORITY_VERBOSE.get(originalPriority); + + if (priorityName == null || originalPriorityName == null) { + // Technically this will never happen as + // it would violate FCM documentation + return; + } + + if (priority != originalPriority) { + displayErrorMessageNotification( + "System changed notification priority from " + priorityName + " to " + + originalPriorityName, + "Notification priority altered.", + null); + } + } + private boolean isAppInForeground() { return ProcessLifecycleOwner.get().getLifecycle().getCurrentState() == Lifecycle.State.RESUMED;