Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/pages/versions/unversioned/sdk/notifications.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,12 @@ To configure `expo-notifications`, use the built-in [config plugin](/config-plug
description:
'Local path to an image to use as the icon for push notifications. 96x96 all-white png with transparency.',
},
{
name: 'largeIcon',
platform: 'android',
description:
'Local path to an image to use as the large icon for notifications. The image is resized to 64x64 dp and shown next to the notification text. A notification that carries its own image uses that image instead.',
},
{
name: 'color',
default: '#ffffff',
Expand Down Expand Up @@ -401,6 +407,7 @@ Here is an example of using the config plugin in the app config file:
"expo-notifications",
{
"icon": "./local/assets/notification_icon.png",
"largeIcon": "./local/assets/notification_large_icon.png",
"color": "#ffffff",
"defaultChannel": "default",
"sounds": [
Expand Down
1 change: 1 addition & 0 deletions packages/expo-file-system/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

### 🐛 Bug fixes

- Fix `File.readableStream()` returning zeroed bytes and writing past the requested region when a BYOB read targets a view that starts at a non-zero offset. ([#49234](https://github.com/expo/expo/pull/49234) by [@dennytosp](https://github.com/dennytosp))
- [Android][iOS] Fix `File.size` returning `null` for a missing or unreadable file. ([#49086](https://github.com/expo/expo/pull/49086)) by [@ACHP](https://github.com/ACHP))
- [iOS] Fix wrong permissions for text() and bytes(). ([#42422](https://github.com/expo/expo/pull/42422)) by [@simoneldevig](https://github.com/simoneldevig))
- Fixed `copyAsync` on iOS copying the unedited original when a `ph://` asset has edits applied in Photos. ([#48248](https://github.com/expo/expo/pull/48248) by [@CoffeeFlux](https://github.com/CoffeeFlux))
Expand Down
106 changes: 106 additions & 0 deletions packages/expo-file-system/src/internal/__tests__/streams-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type { FileHandle } from '../../File.types';
import { FileSystemReadableStreamSource } from '../streams';

const CONTENTS = new Uint8Array(64).map((_, index) => index + 1);

/** A handle over an in-memory buffer, reading sequentially like a real file handle. */
function createHandle(contents: Uint8Array = CONTENTS) {
let position = 0;
const requestedLengths: number[] = [];
const handle = {
readBytes: async (length: number) => {
requestedLengths.push(length);
const slice = contents.subarray(position, position + length);
position += slice.length;
return new Uint8Array(slice);
},
close: () => {},
};
return { handle: handle as unknown as FileHandle, requestedLengths };
}

/**
* A `ReadableByteStreamController` stand-in. `byobRequest.view` covers the region of the
* caller's buffer the stream still has to fill, which is what the spec hands to `pull`.
* jsdom has no `ReadableStream`, so the source is driven directly.
*/
function createController(view: ArrayBufferView) {
const responded: number[] = [];
const controller = {
byobRequest: { view, respond: (bytesWritten: number) => responded.push(bytesWritten) },
close: () => {},
enqueue: () => {},
};
return { controller: controller as unknown as ReadableByteStreamController, responded };
}

describe(FileSystemReadableStreamSource, () => {
it('fills a BYOB view that starts at a non-zero offset in its buffer', async () => {
const { handle } = createHandle();
const view = new Uint8Array(new ArrayBuffer(32), 8, 16);
const { controller, responded } = createController(view);

await new FileSystemReadableStreamSource(handle).pull(controller);

expect(responded).toEqual([16]);
expect(Array.from(view)).toEqual(Array.from(CONTENTS.subarray(0, 16)));
});

it('requests as many bytes as the BYOB view can hold', async () => {
const { handle, requestedLengths } = createHandle();
const view = new Uint8Array(new ArrayBuffer(32), 8, 16);
const { controller } = createController(view);

await new FileSystemReadableStreamSource(handle).pull(controller);

expect(requestedLengths).toEqual([16]);
});

it('does not write outside the BYOB view', async () => {
const { handle } = createHandle();
const buffer = new ArrayBuffer(32);
const view = new Uint8Array(buffer, 8, 16);
const { controller } = createController(view);

await new FileSystemReadableStreamSource(handle).pull(controller);

const whole = new Uint8Array(buffer);
expect(Array.from(whole.subarray(0, 8))).toEqual(new Array(8).fill(0));
expect(Array.from(whole.subarray(24))).toEqual(new Array(8).fill(0));
});

it('fills a BYOB view that starts at offset zero', async () => {
const { handle } = createHandle();
const view = new Uint8Array(new ArrayBuffer(16));
const { controller, responded } = createController(view);

await new FileSystemReadableStreamSource(handle).pull(controller);

expect(responded).toEqual([16]);
expect(Array.from(view)).toEqual(Array.from(CONTENTS.subarray(0, 16)));
});

it('fills a BYOB view that is not a Uint8Array', async () => {
const { handle } = createHandle();
const buffer = new ArrayBuffer(32);
const view = new Uint16Array(buffer, 8, 8);
const { controller, responded } = createController(view);

await new FileSystemReadableStreamSource(handle).pull(controller);

expect(responded).toEqual([16]);
expect(Array.from(new Uint8Array(buffer, 8, 16))).toEqual(Array.from(CONTENTS.subarray(0, 16)));
});

it('closes the stream when the handle is exhausted', async () => {
const { handle } = createHandle(new Uint8Array(0));
const view = new Uint8Array(new ArrayBuffer(32), 8, 16);
const { controller, responded } = createController(view);
const close = jest.spyOn(controller, 'close');

await new FileSystemReadableStreamSource(handle).pull(controller);

expect(close).toHaveBeenCalled();
expect(responded).toEqual([0]);
});
});
13 changes: 4 additions & 9 deletions packages/expo-file-system/src/internal/streams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,15 @@ export class FileSystemReadableStreamSource implements UnderlyingByteSource {
}

// TODO: Optimize by adding a native method that can write into a TypedArray at a given offset.
const bytes = await this.handle.readBytes(theView.byteLength - theView.byteOffset);
// `byteLength` is already the size of the region to fill, so `byteOffset` must not be
// subtracted from it, and `set` takes an offset relative to the view it is called on.
const bytes = await this.handle.readBytes(theView.byteLength);
if (bytes.length === 0) {
controller.close();
controller.byobRequest.respond(0);
return;
}
if (theView instanceof Uint8Array) {
theView.set(bytes, theView.byteOffset);
} else {
const array = new Uint8Array(theView.buffer);
for (let i = 0; i < bytes.length; i++) {
array[i + (theView.byteOffset ?? 0)] = bytes[i]!;
}
}
new Uint8Array(theView.buffer, theView.byteOffset, theView.byteLength).set(bytes);
controller.byobRequest.respond(bytes.length);
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/expo-notifications/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

- [ios] Forward notification center calls to a `UNUserNotificationCenterDelegate` that another library set, so that both libraries keep working. ([#48313](https://github.com/expo/expo/pull/48313) by [@vonovak](https://github.com/vonovak))
- [ios] Add support for grouping notifications via `threadIdentifier`. ([#49429](https://github.com/expo/expo/pull/49429) by [@vonovak](https://github.com/vonovak))
- [Android] Add a `largeIcon` config plugin property that sets the notification large icon. ([#49481](https://github.com/expo/expo/pull/49481) by [@expo-bot](https://github.com/expo-bot))

### 🐛 Bug fixes

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { fs, vol } from 'memfs';
import * as path from 'path';

import { setNotificationIconAsync, setNotificationSounds } from '../withNotificationsAndroid';
import {
setNotificationIconAsync,
setNotificationLargeIconAsync,
setNotificationSounds,
} from '../withNotificationsAndroid';

export function getDirFromFS(fsJSON: Record<string, string | null>, rootDir: string) {
return Object.entries(fsJSON)
Expand Down Expand Up @@ -40,6 +44,17 @@ const LIST_OF_GENERATED_NOTIFICATION_FILES = [
'android/app/src/main/res/raw/notification_sound.wav',
];

const LIST_OF_GENERATED_LARGE_ICON_FILES = [
'android/app/src/main/res/drawable-mdpi/notification_large_icon.png',
'android/app/src/main/res/drawable-hdpi/notification_large_icon.png',
'android/app/src/main/res/drawable-xhdpi/notification_large_icon.png',
'android/app/src/main/res/drawable-xxhdpi/notification_large_icon.png',
'android/app/src/main/res/drawable-xxxhdpi/notification_large_icon.png',
'android/app/src/main/res/values/colors.xml',
'assets/notificationIcon.png',
'assets/notification_sound.wav',
];

const iconPath = path.resolve(__dirname, './fixtures/icon.png');
const soundPath = path.resolve(__dirname, './fixtures/cat.wav');

Expand Down Expand Up @@ -76,6 +91,25 @@ describe('Android notifications configuration', () => {
expect(Object.keys(after).sort()).toEqual(LIST_OF_GENERATED_NOTIFICATION_FILES.sort());
});

it('writes the large icon files as expected', async () => {
await setNotificationLargeIconAsync(projectRoot, '/app/assets/notificationIcon.png');

const after = getDirFromFS(vol.toJSON(), projectRoot);
expect(Object.keys(after).sort()).toEqual(LIST_OF_GENERATED_LARGE_ICON_FILES.sort());
});

it('Safely remove the large icon if it exists, and ignore if it doesnt', async () => {
const before = getDirFromFS(vol.toJSON(), projectRoot);
await setNotificationLargeIconAsync(projectRoot, '/app/assets/notificationIcon.png');

await setNotificationLargeIconAsync(projectRoot, null);
expect(getDirFromFS(vol.toJSON(), projectRoot)).toMatchObject(before);

// now remove again to make sure we don't throw in that case
await setNotificationLargeIconAsync(projectRoot, null);
expect(getDirFromFS(vol.toJSON(), projectRoot)).toMatchObject(before);
});

it('Safely remove icon if it exists, and ignore if it doesnt', async () => {
const before = getDirFromFS(vol.toJSON(), projectRoot);
// first set the icon
Expand Down
7 changes: 7 additions & 0 deletions packages/expo-notifications/plugin/src/withNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ export type NotificationsPluginProps = {
* @platform android
*/
icon?: string;
/**
* Local path to an image to use as the large icon for notifications. The image is resized to
* 64x64 dp and shown next to the notification text. A notification that carries its own image
* uses that image instead.
* @platform android
*/
largeIcon?: string;
/**
* Tint color for the push notification image when it appears in the notification tray.
* @default '#ffffff'
Expand Down
Loading
Loading