-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Support GZIP compressed JSON string as push notification payload #56154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0c70673
5850f72
6a7db39
87815ee
b14244d
34ee570
3cc57fa
959478f
7f6d9b0
de47c99
c8511be
7c0fde2
9a61c8d
c009ee4
a7593e3
1e50439
bad3241
f8922cf
799b149
fcab066
510df80
563c180
1fc6c25
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package com.expensify.chat.customairshipextender | ||
|
|
||
| import android.util.Base64 | ||
| import java.io.ByteArrayInputStream | ||
| import java.io.ByteArrayOutputStream | ||
| import java.util.zip.GZIPInputStream | ||
|
|
||
| class PayloadHandler { | ||
| private val BUFFER_SIZE = 4096 | ||
| private val GZIP_MAGIC = byteArrayOf(0x1f.toByte(), 0x8b.toByte()) | ||
|
|
||
| fun processPayload(payloadString: String): String = | ||
| runCatching { | ||
| val decoded = Base64.decode(payloadString, Base64.DEFAULT) | ||
| if (!isGzipped(decoded)) error("Input not gzipped") | ||
| return decompressGzip(decoded) | ||
| }.getOrDefault(payloadString) | ||
|
|
||
| private fun isGzipped(decoded: ByteArray) = | ||
| decoded.size >= 2 && decoded[0] == GZIP_MAGIC[0] && decoded[1] == GZIP_MAGIC[1] | ||
|
|
||
| private fun decompressGzip(compressed: ByteArray): String { | ||
| ByteArrayInputStream(compressed).use { bis -> | ||
| GZIPInputStream(bis).use { gis -> | ||
| ByteArrayOutputStream().use { output -> | ||
| val buffer = ByteArray(BUFFER_SIZE) | ||
| var len: Int | ||
| while (gis.read(buffer).also { len = it } > 0) { | ||
| output.write(buffer, 0, len) | ||
| } | ||
|
|
||
| return output.toString("UTF-8") | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,43 @@ | ||
| import type {JsonObject, JsonValue} from '@ua/react-native-airship'; | ||
| import pako from 'pako'; | ||
| import Log from '@libs/Log'; | ||
| import type {PushNotificationData} from './NotificationType'; | ||
|
|
||
| const GZIP_MAGIC_NUMBER = '\x1f\x8b'; | ||
|
|
||
| /** | ||
| * Parse the payload of a push notification. On Android, some notification payloads are sent as a JSON string rather than an object | ||
| */ | ||
| export default function parsePushNotificationPayload(payload: JsonValue | undefined): PushNotificationData | undefined { | ||
| let data = payload; | ||
| if (typeof payload === 'string') { | ||
| try { | ||
| data = JSON.parse(payload) as JsonObject; | ||
| } catch { | ||
| Log.hmmm(`[PushNotification] Failed to parse the payload`, payload); | ||
| data = undefined; | ||
| if (payload === undefined) { | ||
| return undefined; | ||
| } | ||
|
|
||
| // No need to parse if it's already an object | ||
| if (typeof payload !== 'string') { | ||
| return payload as PushNotificationData; | ||
| } | ||
|
|
||
| // Gzipped JSON String | ||
| try { | ||
| const binaryStringPayload = atob(payload); | ||
| if (!binaryStringPayload.startsWith(GZIP_MAGIC_NUMBER)) { | ||
| throw Error(); | ||
| } | ||
| const compressed = Uint8Array.from(binaryStringPayload, (x) => x.charCodeAt(0)); | ||
| const decompressed = pako.inflate(compressed, {to: 'string'}); | ||
| const jsonObject = JSON.parse(decompressed) as JsonObject; | ||
| return jsonObject as PushNotificationData; | ||
| } catch { | ||
| Log.hmmm('[PushNotification] Failed to parse the payload as a Gzipped JSON string', payload); | ||
| } | ||
|
|
||
| // JSON String | ||
| try { | ||
| return JSON.parse(payload) as JsonObject as PushNotificationData; | ||
| } catch { | ||
| Log.hmmm(`[PushNotification] Failed to parse the payload as a JSON string`, payload); | ||
| } | ||
|
|
||
| return data ? (data as PushNotificationData) : undefined; | ||
| return undefined; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import parsePushNotificationPayload from '@libs/Notification/PushNotification/parsePushNotificationPayload'; | ||
|
|
||
| const payloadWithOnyxData = { | ||
| type: 'reportComment', | ||
| title: 'Test Payload', | ||
| onyxData: [ | ||
| { | ||
| key: 'reportActions_2170976176751360', | ||
| onyxMethod: 'merge', | ||
| value: { | ||
| // eslint-disable-next-line @typescript-eslint/naming-convention | ||
| '2463291366241014308': { | ||
| actionName: 'ADDCOMMENT', | ||
| reportID: '2170976176751360', | ||
| reportActionID: '2463291366241014308', | ||
| message: [ | ||
| { | ||
| type: 'COMMENT', | ||
| html: 'Hello world!', | ||
| text: 'Hello world!', | ||
| }, | ||
| ], | ||
| }, | ||
| }, | ||
| }, | ||
| ], | ||
| lastUpdateID: 4024059044, | ||
| previousUpdateID: 4024059043, | ||
| reportID: 2170976176751360, | ||
| reportActionID: '2463291366241014308', | ||
| roomName: '', | ||
| app: 'new', | ||
| }; | ||
|
|
||
| // gzip compressed json string | ||
|
VickyStash marked this conversation as resolved.
|
||
| const compressedPayloadWithOnyxData = | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we perform compression of the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The app doesn't do gzip compression. I'm not sure we should add this functionality just for the mocked data. |
||
| 'H4sIAAAAAAAAA5WQT0/DMAzFv0rJeYe0zVrKbVqR4NDBYZzQhCxqbRVJEyXen6rqd6fJmBjQC7f4/ew8P/eMOoPsLmIWjba01EphS2wWMWpIBrJGR9EzdFJD7YFuu1MJBCN77dkHdt/ji3dqdOvekjjnRZ7FeZbP4zTjl7EKaadr36/QbtHLB5B7b9OzRGRpUoztWSJiHouU3wYdwqcrUGGbRVkun6rqfrX202fbx9KTKdPrtb66Jlxmfh/nYIvnTJebXBntSEkvPaCUOjpqK+ubcCU80R992AzDsBmpBEcvpgbCYC54Ivi84EKMzFg8NHrvpnj6M9rvZP8JZrVWl9v5GozxzxaPbPgE0MCh7v4BAAA='; | ||
|
|
||
| describe('parsePushNotificationPayload', () => { | ||
| it("returns 'undefined' for missing payload", () => { | ||
| expect(parsePushNotificationPayload(undefined)).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('returns untouched input when provided with raw json', () => { | ||
| expect(parsePushNotificationPayload(payloadWithOnyxData)).toStrictEqual(payloadWithOnyxData); | ||
| }); | ||
|
|
||
| it('returns correct json when provided with stringified json', () => { | ||
| const stringifiedPayload = JSON.stringify(payloadWithOnyxData); | ||
| expect(parsePushNotificationPayload(stringifiedPayload)).toStrictEqual(payloadWithOnyxData); | ||
| }); | ||
|
|
||
| it('returns correct json when provided with gzip compressed json string', () => { | ||
| expect(parsePushNotificationPayload(compressedPayloadWithOnyxData)).toStrictEqual(payloadWithOnyxData); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BUFFER_SIZEvalue come from? Is it the 4KB notification limit constraint?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, in this case 4KB is a notification payload size limit.
Hm, I'm not sure if it's possible since it's the limitation on the Apple Push Notification service (APNs) / Android's Firebase Cloud Messaging (FCM) level