Implement Blob support for XMLHttpRequest
Summary: This PR is a followup to https://github.com/facebook/react-native/pull/11417 and should be merged after that one is merged. 1. Add support for creating blobs from strings, not just other blobs 1. Add the `File` constructor which is a superset of `Blob` 1. Add the `FileReader` API which can be used to read blobs as strings or data url (base64) 1. Add support for uploading and downloading blobs via `XMLHttpRequest` and `fetch` 1. Add ability to download local files on Android so you can do `fetch(uri).then(res => res.blob())` to get a blob for a local file (iOS already supported this) 1. Clone the repo https://github.com/expo/react-native-blob-test 1. Change the `package.json` and update `react-native` dependency to point to this branch, then run `npm install` 1. Run the `server.js` file with `node server.js` 1. Open the `index.common.js` file and replace `localhost` with your computer's IP address 1. Start the packager with `yarn start` and run the app on your device If everything went well, all tests should pass, and you should see a screen like this: ![screen shot 2017-06-08 at 7 53 08 pm](https://user-images.githubusercontent.com/1174278/26936407-435bbce2-4c8c-11e7-9ae3-eb104e46961e.png)! Pull to rerun all tests or tap on specific test to re-run it [GENERAL] [FEATURE] [Blob] - Implement blob support for XMLHttpRequest Closes https://github.com/facebook/react-native/pull/11573 Reviewed By: shergin Differential Revision: D6082054 Pulled By: hramos fbshipit-source-id: cc9c174fdefdfaf6e5d9fd7b300120a01a50e8c1
This commit is contained in:
parent
3fc33bb54f
commit
be56a3efee
|
@ -8,19 +8,12 @@
|
|||
*
|
||||
* @providesModule Blob
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const invariant = require('fbjs/lib/invariant');
|
||||
/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error
|
||||
* found when Flow v0.54 was deployed. To see the error delete this comment and
|
||||
* run Flow. */
|
||||
const uuid = require('uuid');
|
||||
|
||||
const { BlobModule } = require('NativeModules');
|
||||
|
||||
import type { BlobProps } from 'BlobTypes';
|
||||
import type {BlobData, BlobOptions} from 'BlobTypes';
|
||||
|
||||
/**
|
||||
* Opaque JS representation of some binary data in native.
|
||||
|
@ -60,51 +53,16 @@ import type { BlobProps } from 'BlobTypes';
|
|||
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/Blob
|
||||
*/
|
||||
class Blob {
|
||||
/**
|
||||
* Size of the data contained in the Blob object, in bytes.
|
||||
*/
|
||||
size: number;
|
||||
/*
|
||||
* String indicating the MIME type of the data contained in the Blob.
|
||||
* If the type is unknown, this string is empty.
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/*
|
||||
* Unique id to identify the blob on native side (non-standard)
|
||||
*/
|
||||
blobId: string;
|
||||
/*
|
||||
* Offset to indicate part of blob, used when sliced (non-standard)
|
||||
*/
|
||||
offset: number;
|
||||
|
||||
/**
|
||||
* Construct blob instance from blob data from native.
|
||||
* Used internally by modules like XHR, WebSocket, etc.
|
||||
*/
|
||||
static create(props: BlobProps): Blob {
|
||||
return Object.assign(Object.create(Blob.prototype), props);
|
||||
}
|
||||
_data: ?BlobData;
|
||||
|
||||
/**
|
||||
* Constructor for JS consumers.
|
||||
* Currently we only support creating Blobs from other Blobs.
|
||||
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob
|
||||
*/
|
||||
constructor(parts: Array<Blob>, options: any) {
|
||||
const blobId = uuid();
|
||||
let size = 0;
|
||||
parts.forEach((part) => {
|
||||
invariant(part instanceof Blob, 'Can currently only create a Blob from other Blobs');
|
||||
size += part.size;
|
||||
});
|
||||
BlobModule.createFromParts(parts, blobId);
|
||||
return Blob.create({
|
||||
blobId,
|
||||
offset: 0,
|
||||
size,
|
||||
});
|
||||
constructor(parts: Array<Blob | string> = [], options?: BlobOptions) {
|
||||
const BlobManager = require('BlobManager');
|
||||
this.data = BlobManager.createFromParts(parts, options).data;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -112,9 +70,22 @@ class Blob {
|
|||
* the data in the specified range of bytes of the source Blob.
|
||||
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/Blob/slice
|
||||
*/
|
||||
set data(data: ?BlobData) {
|
||||
this._data = data;
|
||||
}
|
||||
|
||||
get data(): BlobData {
|
||||
if (!this._data) {
|
||||
throw new Error('Blob has been closed and is no longer available');
|
||||
}
|
||||
|
||||
return this._data;
|
||||
}
|
||||
|
||||
slice(start?: number, end?: number): Blob {
|
||||
let offset = this.offset;
|
||||
let size = this.size;
|
||||
const BlobManager = require('BlobManager');
|
||||
let {offset, size} = this.data;
|
||||
|
||||
if (typeof start === 'number') {
|
||||
if (start > size) {
|
||||
start = size;
|
||||
|
@ -129,8 +100,8 @@ class Blob {
|
|||
size = end - start;
|
||||
}
|
||||
}
|
||||
return Blob.create({
|
||||
blobId: this.blobId,
|
||||
return BlobManager.createFromOptions({
|
||||
blobId: this.data.blobId,
|
||||
offset,
|
||||
size,
|
||||
});
|
||||
|
@ -149,7 +120,24 @@ class Blob {
|
|||
* `new Blob([blob, ...])` actually copies the data in memory.
|
||||
*/
|
||||
close() {
|
||||
BlobModule.release(this.blobId);
|
||||
const BlobManager = require('BlobManager');
|
||||
BlobManager.release(this.data.blobId);
|
||||
this.data = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Size of the data contained in the Blob object, in bytes.
|
||||
*/
|
||||
get size(): number {
|
||||
return this.data.size;
|
||||
}
|
||||
|
||||
/*
|
||||
* String indicating the MIME type of the data contained in the Blob.
|
||||
* If the type is unknown, this string is empty.
|
||||
*/
|
||||
get type(): string {
|
||||
return this.data.type || '';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule BlobManager
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Blob = require('Blob');
|
||||
const BlobRegistry = require('BlobRegistry');
|
||||
const {BlobModule} = require('NativeModules');
|
||||
|
||||
import type {BlobData, BlobOptions} from 'BlobTypes';
|
||||
|
||||
/*eslint-disable no-bitwise */
|
||||
/*eslint-disable eqeqeq */
|
||||
|
||||
/**
|
||||
* Based on the rfc4122-compliant solution posted at
|
||||
* http://stackoverflow.com/questions/105034
|
||||
*/
|
||||
function uuidv4(): string {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
||||
const r = (Math.random() * 16) | 0,
|
||||
v = c == 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Module to manage blobs. Wrapper around the native blob module.
|
||||
*/
|
||||
class BlobManager {
|
||||
/**
|
||||
* If the native blob module is available.
|
||||
*/
|
||||
static isAvailable = !!BlobModule;
|
||||
|
||||
/**
|
||||
* Create blob from existing array of blobs.
|
||||
*/
|
||||
static createFromParts(
|
||||
parts: Array<Blob | string>,
|
||||
options?: BlobOptions,
|
||||
): Blob {
|
||||
const blobId = uuidv4();
|
||||
const items = parts.map(part => {
|
||||
if (
|
||||
part instanceof ArrayBuffer ||
|
||||
(global.ArrayBufferView && part instanceof global.ArrayBufferView)
|
||||
) {
|
||||
throw new Error(
|
||||
"Creating blobs from 'ArrayBuffer' and 'ArrayBufferView' are not supported",
|
||||
);
|
||||
}
|
||||
if (part instanceof Blob) {
|
||||
return {
|
||||
data: part.data,
|
||||
type: 'blob',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
data: String(part),
|
||||
type: 'string',
|
||||
};
|
||||
}
|
||||
});
|
||||
const size = items.reduce((acc, curr) => {
|
||||
if (curr.type === 'string') {
|
||||
return acc + global.unescape(encodeURI(curr.data)).length;
|
||||
} else {
|
||||
return acc + curr.data.size;
|
||||
}
|
||||
}, 0);
|
||||
|
||||
BlobModule.createFromParts(items, blobId);
|
||||
|
||||
return BlobManager.createFromOptions({
|
||||
blobId,
|
||||
offset: 0,
|
||||
size,
|
||||
type: options ? options.type : '',
|
||||
lastModified: options ? options.lastModified : Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create blob instance from blob data from native.
|
||||
* Used internally by modules like XHR, WebSocket, etc.
|
||||
*/
|
||||
static createFromOptions(options: BlobData): Blob {
|
||||
BlobRegistry.register(options.blobId);
|
||||
return Object.assign(Object.create(Blob.prototype), {data: options});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deallocate resources for a blob.
|
||||
*/
|
||||
static release(blobId: string): void {
|
||||
BlobRegistry.unregister(blobId);
|
||||
if (BlobRegistry.has(blobId)) {
|
||||
return;
|
||||
}
|
||||
BlobModule.release(blobId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject the blob content handler in the networking module to support blob
|
||||
* requests and responses.
|
||||
*/
|
||||
static addNetworkingHandler(): void {
|
||||
BlobModule.addNetworkingHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate the websocket should return a blob for incoming binary
|
||||
* messages.
|
||||
*/
|
||||
static addWebSocketHandler(socketId: number): void {
|
||||
BlobModule.addWebSocketHandler(socketId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate the websocket should no longer return a blob for incoming
|
||||
* binary messages.
|
||||
*/
|
||||
static removeWebSocketHandler(socketId: number): void {
|
||||
BlobModule.removeWebSocketHandler(socketId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a blob message to a websocket.
|
||||
*/
|
||||
static sendOverSocket(blob: Blob, socketId: number): void {
|
||||
BlobModule.sendOverSocket(blob.data, socketId);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BlobManager;
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule BlobRegistry
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
const registry: {[key: string]: number} = {};
|
||||
|
||||
const register = (id: string) => {
|
||||
if (registry[id]) {
|
||||
registry[id]++;
|
||||
} else {
|
||||
registry[id] = 1;
|
||||
}
|
||||
};
|
||||
|
||||
const unregister = (id: string) => {
|
||||
if (registry[id]) {
|
||||
registry[id]--;
|
||||
if (registry[id] <= 0) {
|
||||
delete registry[id];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const has = (id: string) => {
|
||||
return registry[id] && registry[id] > 0;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
register,
|
||||
unregister,
|
||||
has,
|
||||
};
|
|
@ -8,18 +8,21 @@
|
|||
*
|
||||
* @providesModule BlobTypes
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
export type BlobProps = {
|
||||
export type BlobData = {
|
||||
blobId: string,
|
||||
offset: number,
|
||||
size: number,
|
||||
name?: string,
|
||||
type?: string,
|
||||
lastModified?: number,
|
||||
};
|
||||
|
||||
export type FileProps = BlobProps & {
|
||||
name: string,
|
||||
export type BlobOptions = {
|
||||
type: string,
|
||||
lastModified: number,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule File
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const Blob = require('Blob');
|
||||
|
||||
const invariant = require('fbjs/lib/invariant');
|
||||
|
||||
import type {BlobOptions} from 'BlobTypes';
|
||||
|
||||
/**
|
||||
* The File interface provides information about files.
|
||||
*/
|
||||
class File extends Blob {
|
||||
/**
|
||||
* Constructor for JS consumers.
|
||||
*/
|
||||
constructor(
|
||||
parts: Array<Blob | string>,
|
||||
name: string,
|
||||
options?: BlobOptions,
|
||||
) {
|
||||
invariant(
|
||||
parts != null && name != null,
|
||||
'Failed to construct `File`: Must pass both `parts` and `name` arguments.',
|
||||
);
|
||||
|
||||
super(parts, options);
|
||||
this.data.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of the file.
|
||||
*/
|
||||
get name(): string {
|
||||
invariant(this.data.name != null, 'Files must have a name set.');
|
||||
return this.data.name;
|
||||
}
|
||||
|
||||
/*
|
||||
* Last modified time of the file.
|
||||
*/
|
||||
get lastModified(): number {
|
||||
return this.data.lastModified || 0;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = File;
|
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule FileReader
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const EventTarget = require('event-target-shim');
|
||||
const Blob = require('Blob');
|
||||
const {FileReaderModule} = require('NativeModules');
|
||||
|
||||
type ReadyState =
|
||||
| 0 // EMPTY
|
||||
| 1 // LOADING
|
||||
| 2; // DONE
|
||||
|
||||
type ReaderResult = string | ArrayBuffer;
|
||||
|
||||
const READER_EVENTS = [
|
||||
'abort',
|
||||
'error',
|
||||
'load',
|
||||
'loadstart',
|
||||
'loadend',
|
||||
'progress',
|
||||
];
|
||||
|
||||
const EMPTY = 0;
|
||||
const LOADING = 1;
|
||||
const DONE = 2;
|
||||
|
||||
class FileReader extends EventTarget(...READER_EVENTS) {
|
||||
static EMPTY = EMPTY;
|
||||
static LOADING = LOADING;
|
||||
static DONE = DONE;
|
||||
|
||||
EMPTY = EMPTY;
|
||||
LOADING = LOADING;
|
||||
DONE = DONE;
|
||||
|
||||
_readyState: ReadyState;
|
||||
_error: ?Error;
|
||||
_result: ?ReaderResult;
|
||||
_aborted: boolean = false;
|
||||
_subscriptions: Array<*> = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._reset();
|
||||
}
|
||||
|
||||
_reset(): void {
|
||||
this._readyState = EMPTY;
|
||||
this._error = null;
|
||||
this._result = null;
|
||||
}
|
||||
|
||||
_clearSubscriptions(): void {
|
||||
this._subscriptions.forEach(sub => sub.remove());
|
||||
this._subscriptions = [];
|
||||
}
|
||||
|
||||
_setReadyState(newState: ReadyState) {
|
||||
this._readyState = newState;
|
||||
this.dispatchEvent({type: 'readystatechange'});
|
||||
if (newState === DONE) {
|
||||
if (this._aborted) {
|
||||
this.dispatchEvent({type: 'abort'});
|
||||
} else if (this._error) {
|
||||
this.dispatchEvent({type: 'error'});
|
||||
} else {
|
||||
this.dispatchEvent({type: 'load'});
|
||||
}
|
||||
this.dispatchEvent({type: 'loadend'});
|
||||
}
|
||||
}
|
||||
|
||||
readAsArrayBuffer() {
|
||||
throw new Error('FileReader.readAsArrayBuffer is not implemented');
|
||||
}
|
||||
|
||||
readAsDataURL(blob: Blob) {
|
||||
this._aborted = false;
|
||||
|
||||
FileReaderModule.readAsDataURL(blob.data).then(
|
||||
(text: string) => {
|
||||
if (this._aborted) {
|
||||
return;
|
||||
}
|
||||
this._result = text;
|
||||
this._setReadyState(DONE);
|
||||
},
|
||||
error => {
|
||||
if (this._aborted) {
|
||||
return;
|
||||
}
|
||||
this._error = error;
|
||||
this._setReadyState(DONE);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
readAsText(blob: Blob, encoding: string = 'UTF-8') {
|
||||
this._aborted = false;
|
||||
|
||||
FileReaderModule.readAsText(blob.data, encoding).then(
|
||||
(text: string) => {
|
||||
if (this._aborted) {
|
||||
return;
|
||||
}
|
||||
this._result = text;
|
||||
this._setReadyState(DONE);
|
||||
},
|
||||
error => {
|
||||
if (this._aborted) {
|
||||
return;
|
||||
}
|
||||
this._error = error;
|
||||
this._setReadyState(DONE);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
abort() {
|
||||
this._aborted = true;
|
||||
// only call onreadystatechange if there is something to abort, as per spec
|
||||
if (this._readyState !== EMPTY && this._readyState !== DONE) {
|
||||
this._reset();
|
||||
this._setReadyState(DONE);
|
||||
}
|
||||
// Reset again after, in case modified in handler
|
||||
this._reset();
|
||||
}
|
||||
|
||||
get readyState(): ReadyState {
|
||||
return this._readyState;
|
||||
}
|
||||
|
||||
get error(): ?Error {
|
||||
return this._error;
|
||||
}
|
||||
|
||||
get result(): ?ReaderResult {
|
||||
return this._result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FileReader;
|
|
@ -7,12 +7,18 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
19BA88FE1F84391700741C5A /* RCTFileReaderModule.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = ADDFBA6A1F33455F0064C998 /* RCTFileReaderModule.h */; };
|
||||
19BA88FF1F84392900741C5A /* RCTFileReaderModule.h in Headers */ = {isa = PBXBuildFile; fileRef = ADDFBA6A1F33455F0064C998 /* RCTFileReaderModule.h */; };
|
||||
19BA89001F84392F00741C5A /* RCTFileReaderModule.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = ADDFBA6A1F33455F0064C998 /* RCTFileReaderModule.h */; };
|
||||
19BA89011F84393D00741C5A /* RCTFileReaderModule.m in Sources */ = {isa = PBXBuildFile; fileRef = ADDFBA6B1F33455F0064C998 /* RCTFileReaderModule.m */; };
|
||||
AD0871131E215B28007D136D /* RCTBlobManager.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */; };
|
||||
AD0871161E215EC9007D136D /* RCTBlobManager.h in Headers */ = {isa = PBXBuildFile; fileRef = AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */; };
|
||||
AD0871181E215ED1007D136D /* RCTBlobManager.h in Headers */ = {isa = PBXBuildFile; fileRef = AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */; };
|
||||
AD08711A1E2162C8007D136D /* RCTBlobManager.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */; };
|
||||
AD9A43C31DFC7126008DC588 /* RCTBlobManager.m in Sources */ = {isa = PBXBuildFile; fileRef = AD9A43C21DFC7126008DC588 /* RCTBlobManager.m */; };
|
||||
ADD01A711E09404A00F6D226 /* RCTBlobManager.m in Sources */ = {isa = PBXBuildFile; fileRef = AD9A43C21DFC7126008DC588 /* RCTBlobManager.m */; };
|
||||
AD9A43C31DFC7126008DC588 /* RCTBlobManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = AD9A43C21DFC7126008DC588 /* RCTBlobManager.mm */; };
|
||||
ADD01A711E09404A00F6D226 /* RCTBlobManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = AD9A43C21DFC7126008DC588 /* RCTBlobManager.mm */; };
|
||||
ADDFBA6C1F33455F0064C998 /* RCTFileReaderModule.h in Headers */ = {isa = PBXBuildFile; fileRef = ADDFBA6A1F33455F0064C998 /* RCTFileReaderModule.h */; };
|
||||
ADDFBA6D1F33455F0064C998 /* RCTFileReaderModule.m in Sources */ = {isa = PBXBuildFile; fileRef = ADDFBA6B1F33455F0064C998 /* RCTFileReaderModule.m */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
|
@ -22,6 +28,7 @@
|
|||
dstPath = include/RCTBlob;
|
||||
dstSubfolderSpec = 16;
|
||||
files = (
|
||||
19BA88FE1F84391700741C5A /* RCTFileReaderModule.h in Copy Headers */,
|
||||
AD08711A1E2162C8007D136D /* RCTBlobManager.h in Copy Headers */,
|
||||
);
|
||||
name = "Copy Headers";
|
||||
|
@ -33,6 +40,7 @@
|
|||
dstPath = include/RCTBlob;
|
||||
dstSubfolderSpec = 16;
|
||||
files = (
|
||||
19BA89001F84392F00741C5A /* RCTFileReaderModule.h in Copy Headers */,
|
||||
AD0871131E215B28007D136D /* RCTBlobManager.h in Copy Headers */,
|
||||
);
|
||||
name = "Copy Headers";
|
||||
|
@ -42,17 +50,21 @@
|
|||
|
||||
/* Begin PBXFileReference section */
|
||||
358F4ED71D1E81A9004DF814 /* libRCTBlob.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTBlob.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBlobManager.h; sourceTree = "<group>"; };
|
||||
AD9A43C21DFC7126008DC588 /* RCTBlobManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBlobManager.m; sourceTree = "<group>"; };
|
||||
AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTBlobManager.h; sourceTree = "<group>"; };
|
||||
AD9A43C21DFC7126008DC588 /* RCTBlobManager.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTBlobManager.mm; sourceTree = "<group>"; };
|
||||
ADD01A681E09402E00F6D226 /* libRCTBlob-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libRCTBlob-tvOS.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
ADDFBA6A1F33455F0064C998 /* RCTFileReaderModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTFileReaderModule.h; sourceTree = "<group>"; };
|
||||
ADDFBA6B1F33455F0064C998 /* RCTFileReaderModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTFileReaderModule.m; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
358F4ECE1D1E81A9004DF814 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ADDFBA6A1F33455F0064C998 /* RCTFileReaderModule.h */,
|
||||
ADDFBA6B1F33455F0064C998 /* RCTFileReaderModule.m */,
|
||||
AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */,
|
||||
AD9A43C21DFC7126008DC588 /* RCTBlobManager.m */,
|
||||
AD9A43C21DFC7126008DC588 /* RCTBlobManager.mm */,
|
||||
358F4ED81D1E81A9004DF814 /* Products */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
|
@ -77,6 +89,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
AD0871161E215EC9007D136D /* RCTBlobManager.h in Headers */,
|
||||
ADDFBA6C1F33455F0064C998 /* RCTFileReaderModule.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -84,6 +97,7 @@
|
|||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
19BA88FF1F84392900741C5A /* RCTFileReaderModule.h in Headers */,
|
||||
AD0871181E215ED1007D136D /* RCTBlobManager.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -132,7 +146,7 @@
|
|||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 0730;
|
||||
ORGANIZATIONNAME = "Silk Labs";
|
||||
ORGANIZATIONNAME = Facebook;
|
||||
TargetAttributes = {
|
||||
358F4ED61D1E81A9004DF814 = {
|
||||
CreatedOnToolsVersion = 7.3;
|
||||
|
@ -166,7 +180,8 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
AD9A43C31DFC7126008DC588 /* RCTBlobManager.m in Sources */,
|
||||
ADDFBA6D1F33455F0064C998 /* RCTFileReaderModule.m in Sources */,
|
||||
AD9A43C31DFC7126008DC588 /* RCTBlobManager.mm in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -174,7 +189,8 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
ADD01A711E09404A00F6D226 /* RCTBlobManager.m in Sources */,
|
||||
19BA89011F84393D00741C5A /* RCTFileReaderModule.m in Sources */,
|
||||
ADD01A711E09404A00F6D226 /* RCTBlobManager.mm in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -13,4 +13,18 @@
|
|||
|
||||
@interface RCTBlobManager : NSObject <RCTBridgeModule, RCTURLRequestHandler>
|
||||
|
||||
- (NSString *)store:(NSData *)data;
|
||||
|
||||
- (void)store:(NSData *)data withId:(NSString *)blobId;
|
||||
|
||||
- (NSData *)resolve:(NSDictionary<NSString *, id> *)blob;
|
||||
|
||||
- (NSData *)resolve:(NSString *)blobId offset:(NSInteger)offset size:(NSInteger)size;
|
||||
|
||||
- (NSData *)resolveURL:(NSURL *)url;
|
||||
|
||||
- (void)remove:(NSString *)blobId;
|
||||
|
||||
- (void)createFromParts:(NSArray<NSDictionary<NSString *, id> *> *)parts withId:(NSString *)blobId;
|
||||
|
||||
@end
|
||||
|
|
|
@ -1,218 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
#import "RCTBlobManager.h"
|
||||
|
||||
#import <React/RCTConvert.h>
|
||||
#import <React/RCTWebSocketModule.h>
|
||||
|
||||
static NSString *const kBlobUriScheme = @"blob";
|
||||
|
||||
@interface _RCTBlobContentHandler : NSObject <RCTWebSocketContentHandler>
|
||||
|
||||
- (instancetype)initWithBlobManager:(RCTBlobManager *)blobManager;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation RCTBlobManager
|
||||
{
|
||||
NSMutableDictionary<NSString *, NSData *> *_blobs;
|
||||
_RCTBlobContentHandler *_contentHandler;
|
||||
NSOperationQueue *_queue;
|
||||
}
|
||||
|
||||
RCT_EXPORT_MODULE(BlobModule)
|
||||
|
||||
@synthesize bridge = _bridge;
|
||||
|
||||
+ (BOOL)requiresMainQueueSetup
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSDictionary<NSString *, id> *)constantsToExport
|
||||
{
|
||||
return @{
|
||||
@"BLOB_URI_SCHEME": kBlobUriScheme,
|
||||
@"BLOB_URI_HOST": [NSNull null],
|
||||
};
|
||||
}
|
||||
|
||||
- (dispatch_queue_t)methodQueue
|
||||
{
|
||||
return [[_bridge webSocketModule] methodQueue];
|
||||
}
|
||||
|
||||
- (NSString *)store:(NSData *)data
|
||||
{
|
||||
NSString *blobId = [NSUUID UUID].UUIDString;
|
||||
[self store:data withId:blobId];
|
||||
return blobId;
|
||||
}
|
||||
|
||||
- (void)store:(NSData *)data withId:(NSString *)blobId
|
||||
{
|
||||
if (!_blobs) {
|
||||
_blobs = [NSMutableDictionary new];
|
||||
}
|
||||
|
||||
_blobs[blobId] = data;
|
||||
}
|
||||
|
||||
- (NSData *)resolve:(NSDictionary<NSString *, id> *)blob
|
||||
{
|
||||
NSString *blobId = [RCTConvert NSString:blob[@"blobId"]];
|
||||
NSNumber *offset = [RCTConvert NSNumber:blob[@"offset"]];
|
||||
NSNumber *size = [RCTConvert NSNumber:blob[@"size"]];
|
||||
|
||||
return [self resolve:blobId
|
||||
offset:offset ? [offset integerValue] : 0
|
||||
size:size ? [size integerValue] : -1];
|
||||
}
|
||||
|
||||
- (NSData *)resolve:(NSString *)blobId offset:(NSInteger)offset size:(NSInteger)size
|
||||
{
|
||||
NSData *data = _blobs[blobId];
|
||||
if (!data) {
|
||||
return nil;
|
||||
}
|
||||
if (offset != 0 || (size != -1 && size != data.length)) {
|
||||
data = [data subdataWithRange:NSMakeRange(offset, size)];
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(enableBlobSupport:(nonnull NSNumber *)socketID)
|
||||
{
|
||||
if (!_contentHandler) {
|
||||
_contentHandler = [[_RCTBlobContentHandler alloc] initWithBlobManager:self];
|
||||
}
|
||||
[[_bridge webSocketModule] setContentHandler:_contentHandler forSocketID:socketID];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(disableBlobSupport:(nonnull NSNumber *)socketID)
|
||||
{
|
||||
[[_bridge webSocketModule] setContentHandler:nil forSocketID:socketID];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(sendBlob:(NSDictionary *)blob socketID:(nonnull NSNumber *)socketID)
|
||||
{
|
||||
[[_bridge webSocketModule] sendData:[self resolve:blob] forSocketID:socketID];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(createFromParts:(NSArray<NSDictionary<NSString *, id> *> *)parts withId:(NSString *)blobId)
|
||||
{
|
||||
NSMutableData *data = [NSMutableData new];
|
||||
for (NSDictionary<NSString *, id> *part in parts) {
|
||||
NSData *partData = [self resolve:part];
|
||||
[data appendData:partData];
|
||||
}
|
||||
[self store:data withId:blobId];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(release:(NSString *)blobId)
|
||||
{
|
||||
[_blobs removeObjectForKey:blobId];
|
||||
}
|
||||
|
||||
#pragma mark - RCTURLRequestHandler methods
|
||||
|
||||
- (BOOL)canHandleRequest:(NSURLRequest *)request
|
||||
{
|
||||
return [request.URL.scheme caseInsensitiveCompare:kBlobUriScheme] == NSOrderedSame;
|
||||
}
|
||||
|
||||
- (id)sendRequest:(NSURLRequest *)request withDelegate:(id<RCTURLRequestDelegate>)delegate
|
||||
{
|
||||
// Lazy setup
|
||||
if (!_queue) {
|
||||
_queue = [NSOperationQueue new];
|
||||
_queue.maxConcurrentOperationCount = 2;
|
||||
}
|
||||
|
||||
__weak __block NSBlockOperation *weakOp;
|
||||
__block NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
|
||||
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL
|
||||
MIMEType:nil
|
||||
expectedContentLength:-1
|
||||
textEncodingName:nil];
|
||||
|
||||
[delegate URLRequest:weakOp didReceiveResponse:response];
|
||||
|
||||
NSURLComponents *components = [[NSURLComponents alloc] initWithURL:request.URL resolvingAgainstBaseURL:NO];
|
||||
|
||||
NSString *blobId = components.path;
|
||||
NSInteger offset = 0;
|
||||
NSInteger size = -1;
|
||||
|
||||
if (components.queryItems) {
|
||||
for (NSURLQueryItem *queryItem in components.queryItems) {
|
||||
if ([queryItem.name isEqualToString:@"offset"]) {
|
||||
offset = [queryItem.value integerValue];
|
||||
}
|
||||
if ([queryItem.name isEqualToString:@"size"]) {
|
||||
size = [queryItem.value integerValue];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSData *data;
|
||||
if (blobId) {
|
||||
data = [self resolve:blobId offset:offset size:size];
|
||||
}
|
||||
NSError *error;
|
||||
if (data) {
|
||||
[delegate URLRequest:weakOp didReceiveData:data];
|
||||
} else {
|
||||
error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorBadURL userInfo:nil];
|
||||
}
|
||||
[delegate URLRequest:weakOp didCompleteWithError:error];
|
||||
}];
|
||||
|
||||
weakOp = op;
|
||||
[_queue addOperation:op];
|
||||
return op;
|
||||
}
|
||||
|
||||
- (void)cancelRequest:(NSOperation *)op
|
||||
{
|
||||
[op cancel];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation _RCTBlobContentHandler {
|
||||
__weak RCTBlobManager *_blobManager;
|
||||
}
|
||||
|
||||
- (instancetype)initWithBlobManager:(RCTBlobManager *)blobManager
|
||||
{
|
||||
if (self = [super init]) {
|
||||
_blobManager = blobManager;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)processMessage:(id)message forSocketID:(NSNumber *)socketID withType:(NSString *__autoreleasing _Nonnull *)type
|
||||
{
|
||||
if (![message isKindOfClass:[NSData class]]) {
|
||||
*type = @"text";
|
||||
return message;
|
||||
}
|
||||
|
||||
*type = @"blob";
|
||||
return @{
|
||||
@"blobId": [_blobManager store:message],
|
||||
@"offset": @0,
|
||||
@"size": @(((NSData *)message).length),
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,290 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
#import "RCTBlobManager.h"
|
||||
|
||||
#import <mutex>
|
||||
|
||||
#import <React/RCTConvert.h>
|
||||
#import <React/RCTNetworking.h>
|
||||
#import <React/RCTWebSocketModule.h>
|
||||
|
||||
static NSString *const kBlobURIScheme = @"blob";
|
||||
|
||||
@interface RCTBlobManager () <RCTNetworkingRequestHandler, RCTNetworkingResponseHandler, RCTWebSocketContentHandler>
|
||||
|
||||
@end
|
||||
|
||||
@implementation RCTBlobManager
|
||||
{
|
||||
// Blobs should be thread safe since they are used from the websocket and networking module,
|
||||
// make sure to use proper locking when accessing this.
|
||||
NSMutableDictionary<NSString *, NSData *> *_blobs;
|
||||
std::mutex _blobsMutex;
|
||||
|
||||
NSOperationQueue *_queue;
|
||||
}
|
||||
|
||||
RCT_EXPORT_MODULE(BlobModule)
|
||||
|
||||
@synthesize bridge = _bridge;
|
||||
|
||||
- (void)setBridge:(RCTBridge *)bridge
|
||||
{
|
||||
_bridge = bridge;
|
||||
|
||||
std::lock_guard<std::mutex> lock(_blobsMutex);
|
||||
_blobs = [NSMutableDictionary new];
|
||||
}
|
||||
|
||||
+ (BOOL)requiresMainQueueSetup
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSDictionary<NSString *, id> *)constantsToExport
|
||||
{
|
||||
return @{
|
||||
@"BLOB_URI_SCHEME": kBlobURIScheme,
|
||||
@"BLOB_URI_HOST": [NSNull null],
|
||||
};
|
||||
}
|
||||
|
||||
- (NSString *)store:(NSData *)data
|
||||
{
|
||||
NSString *blobId = [NSUUID UUID].UUIDString;
|
||||
[self store:data withId:blobId];
|
||||
return blobId;
|
||||
}
|
||||
|
||||
- (void)store:(NSData *)data withId:(NSString *)blobId
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_blobsMutex);
|
||||
_blobs[blobId] = data;
|
||||
}
|
||||
|
||||
- (NSData *)resolve:(NSDictionary<NSString *, id> *)blob
|
||||
{
|
||||
NSString *blobId = [RCTConvert NSString:blob[@"blobId"]];
|
||||
NSNumber *offset = [RCTConvert NSNumber:blob[@"offset"]];
|
||||
NSNumber *size = [RCTConvert NSNumber:blob[@"size"]];
|
||||
return [self resolve:blobId
|
||||
offset:offset ? [offset integerValue] : 0
|
||||
size:size ? [size integerValue] : -1];
|
||||
}
|
||||
|
||||
- (NSData *)resolve:(NSString *)blobId offset:(NSInteger)offset size:(NSInteger)size
|
||||
{
|
||||
NSData *data;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_blobsMutex);
|
||||
data = _blobs[blobId];
|
||||
}
|
||||
if (!data) {
|
||||
return nil;
|
||||
}
|
||||
if (offset != 0 || (size != -1 && size != data.length)) {
|
||||
data = [data subdataWithRange:NSMakeRange(offset, size)];
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
- (NSData *)resolveURL:(NSURL *)url
|
||||
{
|
||||
NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
|
||||
|
||||
NSString *blobId = components.path;
|
||||
NSInteger offset = 0;
|
||||
NSInteger size = -1;
|
||||
|
||||
if (components.queryItems) {
|
||||
for (NSURLQueryItem *queryItem in components.queryItems) {
|
||||
if ([queryItem.name isEqualToString:@"offset"]) {
|
||||
offset = [queryItem.value integerValue];
|
||||
}
|
||||
if ([queryItem.name isEqualToString:@"size"]) {
|
||||
size = [queryItem.value integerValue];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (blobId) {
|
||||
return [self resolve:blobId offset:offset size:size];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)remove:(NSString *)blobId
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_blobsMutex);
|
||||
[_blobs removeObjectForKey:blobId];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(addNetworkingHandler)
|
||||
{
|
||||
dispatch_async(_bridge.networking.methodQueue, ^{
|
||||
[self->_bridge.networking addRequestHandler:self];
|
||||
[self->_bridge.networking addResponseHandler:self];
|
||||
});
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(addWebSocketHandler:(nonnull NSNumber *)socketID)
|
||||
{
|
||||
dispatch_async(_bridge.webSocketModule.methodQueue, ^{
|
||||
[self->_bridge.webSocketModule setContentHandler:self forSocketID:socketID];
|
||||
});
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(removeWebSocketHandler:(nonnull NSNumber *)socketID)
|
||||
{
|
||||
dispatch_async(_bridge.webSocketModule.methodQueue, ^{
|
||||
[self->_bridge.webSocketModule setContentHandler:nil forSocketID:socketID];
|
||||
});
|
||||
}
|
||||
|
||||
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
|
||||
RCT_EXPORT_METHOD(sendOverSocket:(NSDictionary *)blob socketID:(nonnull NSNumber *)socketID)
|
||||
{
|
||||
dispatch_async(_bridge.webSocketModule.methodQueue, ^{
|
||||
[self->_bridge.webSocketModule sendData:[self resolve:blob] forSocketID:socketID];
|
||||
});
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(createFromParts:(NSArray<NSDictionary<NSString *, id> *> *)parts withId:(NSString *)blobId)
|
||||
{
|
||||
NSMutableData *data = [NSMutableData new];
|
||||
for (NSDictionary<NSString *, id> *part in parts) {
|
||||
NSString *type = [RCTConvert NSString:part[@"type"]];
|
||||
|
||||
if ([type isEqualToString:@"blob"]) {
|
||||
NSData *partData = [self resolve:part[@"data"]];
|
||||
[data appendData:partData];
|
||||
} else if ([type isEqualToString:@"string"]) {
|
||||
NSData *partData = [[RCTConvert NSString:part[@"data"]] dataUsingEncoding:NSUTF8StringEncoding];
|
||||
[data appendData:partData];
|
||||
} else {
|
||||
[NSException raise:@"Invalid type for blob" format:@"%@ is invalid", type];
|
||||
}
|
||||
}
|
||||
[self store:data withId:blobId];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(release:(NSString *)blobId)
|
||||
{
|
||||
[self remove:blobId];
|
||||
}
|
||||
|
||||
#pragma mark - RCTURLRequestHandler methods
|
||||
|
||||
- (BOOL)canHandleRequest:(NSURLRequest *)request
|
||||
{
|
||||
return [request.URL.scheme caseInsensitiveCompare:kBlobURIScheme] == NSOrderedSame;
|
||||
}
|
||||
|
||||
- (id)sendRequest:(NSURLRequest *)request withDelegate:(id<RCTURLRequestDelegate>)delegate
|
||||
{
|
||||
// Lazy setup
|
||||
if (!_queue) {
|
||||
_queue = [NSOperationQueue new];
|
||||
_queue.maxConcurrentOperationCount = 2;
|
||||
}
|
||||
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
__weak __block NSBlockOperation *weakOp;
|
||||
__block NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
|
||||
__typeof(self) strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
return;
|
||||
}
|
||||
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL
|
||||
MIMEType:nil
|
||||
expectedContentLength:-1
|
||||
textEncodingName:nil];
|
||||
|
||||
[delegate URLRequest:weakOp didReceiveResponse:response];
|
||||
|
||||
NSData *data = [strongSelf resolveURL:response.URL];
|
||||
NSError *error;
|
||||
if (data) {
|
||||
[delegate URLRequest:weakOp didReceiveData:data];
|
||||
} else {
|
||||
error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorBadURL userInfo:nil];
|
||||
}
|
||||
[delegate URLRequest:weakOp didCompleteWithError:error];
|
||||
}];
|
||||
|
||||
weakOp = op;
|
||||
[_queue addOperation:op];
|
||||
return op;
|
||||
}
|
||||
|
||||
- (void)cancelRequest:(NSOperation *)op
|
||||
{
|
||||
[op cancel];
|
||||
}
|
||||
|
||||
#pragma mark - RCTNetworkingRequestHandler methods
|
||||
|
||||
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
|
||||
- (BOOL)canHandleNetworkingRequest:(NSDictionary *)data
|
||||
{
|
||||
return data[@"blob"] != nil;
|
||||
}
|
||||
|
||||
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
|
||||
- (NSDictionary *)handleNetworkingRequest:(NSDictionary *)data
|
||||
{
|
||||
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
|
||||
NSDictionary *blob = [RCTConvert NSDictionary:data[@"blob"]];
|
||||
|
||||
NSString *contentType = @"application/octet-stream";
|
||||
NSString *blobType = [RCTConvert NSString:blob[@"type"]];
|
||||
if (blobType != nil && blobType.length > 0) {
|
||||
contentType = blob[@"type"];
|
||||
}
|
||||
|
||||
return @{@"body": [self resolve:blob], @"contentType": contentType};
|
||||
}
|
||||
|
||||
- (BOOL)canHandleNetworkingResponse:(NSString *)responseType
|
||||
{
|
||||
return [responseType isEqualToString:@"blob"];
|
||||
}
|
||||
|
||||
- (id)handleNetworkingResponse:(NSURLResponse *)response data:(NSData *)data
|
||||
{
|
||||
return @{
|
||||
@"blobId": [self store:data],
|
||||
@"offset": @0,
|
||||
@"size": @(data.length),
|
||||
@"name": [response suggestedFilename],
|
||||
@"type": [response MIMEType],
|
||||
};
|
||||
}
|
||||
|
||||
#pragma mark - RCTWebSocketContentHandler methods
|
||||
|
||||
- (id)processWebsocketMessage:(id)message
|
||||
forSocketID:(NSNumber *)socketID
|
||||
withType:(NSString *__autoreleasing _Nonnull *)type
|
||||
{
|
||||
if (![message isKindOfClass:[NSData class]]) {
|
||||
*type = @"text";
|
||||
return message;
|
||||
}
|
||||
|
||||
*type = @"blob";
|
||||
return @{
|
||||
@"blobId": [self store:message],
|
||||
@"offset": @0,
|
||||
@"size": @(((NSData *)message).length),
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
#import <React/RCTBridgeModule.h>
|
||||
|
||||
@interface RCTFileReaderModule : NSObject <RCTBridgeModule>
|
||||
|
||||
@end
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
|
||||
#import "RCTFileReaderModule.h"
|
||||
|
||||
#import <React/RCTBridge.h>
|
||||
#import <React/RCTConvert.h>
|
||||
|
||||
#import "RCTBlobManager.h"
|
||||
|
||||
|
||||
@implementation RCTFileReaderModule
|
||||
|
||||
RCT_EXPORT_MODULE(FileReaderModule)
|
||||
|
||||
@synthesize bridge = _bridge;
|
||||
|
||||
RCT_EXPORT_METHOD(readAsText:(NSDictionary<NSString *, id> *)blob
|
||||
encoding:(NSString *)encoding
|
||||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
RCTBlobManager *blobManager = [[self bridge] moduleForClass:[RCTBlobManager class]];
|
||||
NSData *data = [blobManager resolve:blob];
|
||||
|
||||
if (data == nil) {
|
||||
reject(RCTErrorUnspecified,
|
||||
[NSString stringWithFormat:@"Unable to resolve data for blob: %@", [RCTConvert NSString:blob[@"blobId"]]], nil);
|
||||
} else {
|
||||
NSStringEncoding stringEncoding;
|
||||
|
||||
if (encoding == nil) {
|
||||
stringEncoding = NSUTF8StringEncoding;
|
||||
} else {
|
||||
stringEncoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding((CFStringRef) encoding));
|
||||
}
|
||||
|
||||
NSString *text = [[NSString alloc] initWithData:data encoding:stringEncoding];
|
||||
|
||||
resolve(text);
|
||||
}
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(readAsDataURL:(NSDictionary<NSString *, id> *)blob
|
||||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
RCTBlobManager *blobManager = [[self bridge] moduleForClass:[RCTBlobManager class]];
|
||||
NSData *data = [blobManager resolve:blob];
|
||||
|
||||
if (data == nil) {
|
||||
reject(RCTErrorUnspecified,
|
||||
[NSString stringWithFormat:@"Unable to resolve data for blob: %@", [RCTConvert NSString:blob[@"blobId"]]], nil);
|
||||
} else {
|
||||
NSString *type = [RCTConvert NSString:blob[@"type"]];
|
||||
NSString *text = [NSString stringWithFormat:@"data:%@;base64,%@",
|
||||
type != nil && [type length] > 0 ? type : @"application/octet-stream",
|
||||
[data base64EncodedStringWithOptions:0]];
|
||||
|
||||
resolve(text);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -52,16 +52,16 @@ if (BlobModule && typeof BlobModule.BLOB_URI_SCHEME === 'string') {
|
|||
*/
|
||||
class URL {
|
||||
constructor() {
|
||||
throw new Error('Creating BlobURL objects is not supported yet.');
|
||||
throw new Error('Creating URL objects is not supported yet.');
|
||||
}
|
||||
|
||||
static createObjectURL(blob: Blob) {
|
||||
if (BLOB_URL_PREFIX === null) {
|
||||
throw new Error('Cannot create URL for blob!');
|
||||
}
|
||||
return `${BLOB_URL_PREFIX}${blob.blobId}?offset=${blob.offset}&size=${
|
||||
blob.size
|
||||
}`;
|
||||
return `${BLOB_URL_PREFIX}${blob.data.blobId}?offset=${
|
||||
blob.data.offset
|
||||
}&size=${blob.size}`;
|
||||
}
|
||||
|
||||
static revokeObjectURL(url: string) {
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
const BlobModule = {
|
||||
createFromParts() {},
|
||||
release() {},
|
||||
};
|
||||
|
||||
module.exports = BlobModule;
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
const FileReaderModule = {
|
||||
async readAsText() {
|
||||
return '';
|
||||
},
|
||||
async readAsDataURL() {
|
||||
return 'data:text/plain;base64,NDI=';
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = FileReaderModule;
|
|
@ -0,0 +1,84 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @format
|
||||
* @emails oncall+react_native
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
jest.setMock('NativeModules', {
|
||||
BlobModule: require('../__mocks__/BlobModule'),
|
||||
});
|
||||
|
||||
var Blob = require('Blob');
|
||||
|
||||
describe('Blob', function() {
|
||||
it('should create empty blob', () => {
|
||||
const blob = new Blob();
|
||||
expect(blob).toBeInstanceOf(Blob);
|
||||
expect(blob.data.offset).toBe(0);
|
||||
expect(blob.data.size).toBe(0);
|
||||
expect(blob.size).toBe(0);
|
||||
expect(blob.type).toBe('');
|
||||
});
|
||||
|
||||
it('should create blob from other blobs and strings', () => {
|
||||
const blobA = new Blob();
|
||||
const blobB = new Blob();
|
||||
const textA = 'i \u2665 dogs';
|
||||
const textB = '\uD800\uDC00';
|
||||
const textC =
|
||||
'Z\u0351\u036B\u0343\u036A\u0302\u036B\u033D\u034F\u0334\u0319\u0324' +
|
||||
'\u031E\u0349\u035A\u032F\u031E\u0320\u034DA\u036B\u0357\u0334\u0362' +
|
||||
'\u0335\u031C\u0330\u0354L\u0368\u0367\u0369\u0358\u0320G\u0311\u0357' +
|
||||
'\u030E\u0305\u035B\u0341\u0334\u033B\u0348\u034D\u0354\u0339O\u0342' +
|
||||
'\u030C\u030C\u0358\u0328\u0335\u0339\u033B\u031D\u0333!\u033F\u030B' +
|
||||
'\u0365\u0365\u0302\u0363\u0310\u0301\u0301\u035E\u035C\u0356\u032C' +
|
||||
'\u0330\u0319\u0317';
|
||||
|
||||
blobA.data.size = 34540;
|
||||
blobB.data.size = 65452;
|
||||
|
||||
const blob = new Blob([blobA, blobB, textA, textB, textC]);
|
||||
|
||||
expect(blob.size).toBe(
|
||||
blobA.size +
|
||||
blobB.size +
|
||||
global.Buffer.byteLength(textA, 'UTF-8') +
|
||||
global.Buffer.byteLength(textB, 'UTF-8') +
|
||||
global.Buffer.byteLength(textC, 'UTF-8'),
|
||||
);
|
||||
expect(blob.type).toBe('');
|
||||
});
|
||||
|
||||
it('should slice a blob', () => {
|
||||
const blob = new Blob();
|
||||
|
||||
blob.data.size = 34546;
|
||||
|
||||
const sliceA = blob.slice(0, 2354);
|
||||
|
||||
expect(sliceA.data.offset).toBe(0);
|
||||
expect(sliceA.size).toBe(2354);
|
||||
expect(sliceA.type).toBe('');
|
||||
|
||||
const sliceB = blob.slice(2384, 7621);
|
||||
|
||||
expect(sliceB.data.offset).toBe(2384);
|
||||
expect(sliceB.size).toBe(7621 - 2384);
|
||||
expect(sliceB.type).toBe('');
|
||||
});
|
||||
|
||||
it('should close a blob', () => {
|
||||
const blob = new Blob();
|
||||
|
||||
blob.close();
|
||||
|
||||
expect(() => blob.size).toThrow();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @format
|
||||
* @emails oncall+react_native
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
jest.setMock('NativeModules', {
|
||||
BlobModule: require('../__mocks__/BlobModule'),
|
||||
});
|
||||
|
||||
var Blob = require('Blob');
|
||||
var BlobManager = require('BlobManager');
|
||||
|
||||
describe('BlobManager', function() {
|
||||
it('should create blob from parts', () => {
|
||||
const blob = BlobManager.createFromParts([], {type: 'text/html'});
|
||||
expect(blob).toBeInstanceOf(Blob);
|
||||
expect(blob.type).toBe('text/html');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @format
|
||||
* @emails oncall+react_native
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
jest.setMock('NativeModules', {
|
||||
BlobModule: require('../__mocks__/BlobModule'),
|
||||
});
|
||||
|
||||
const File = require('File');
|
||||
|
||||
describe('File', function() {
|
||||
it('should create empty file', () => {
|
||||
const file = new File([], 'test.jpg');
|
||||
expect(file).toBeInstanceOf(File);
|
||||
expect(file.data.offset).toBe(0);
|
||||
expect(file.data.size).toBe(0);
|
||||
expect(file.size).toBe(0);
|
||||
expect(file.type).toBe('');
|
||||
expect(file.name).toBe('test.jpg');
|
||||
expect(file.lastModified).toEqual(expect.any(Number));
|
||||
});
|
||||
|
||||
it('should create empty file with type', () => {
|
||||
const file = new File([], 'test.jpg', {type: 'image/jpeg'});
|
||||
expect(file.type).toBe('image/jpeg');
|
||||
});
|
||||
|
||||
it('should create empty file with lastModified', () => {
|
||||
const file = new File([], 'test.jpg', {lastModified: 1337});
|
||||
expect(file.lastModified).toBe(1337);
|
||||
});
|
||||
|
||||
it('should throw on invalid arguments', () => {
|
||||
expect(() => new File()).toThrow();
|
||||
expect(() => new File([])).toThrow();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @format
|
||||
* @emails oncall+react_native
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
jest.unmock('event-target-shim').setMock('NativeModules', {
|
||||
BlobModule: require('../__mocks__/BlobModule'),
|
||||
FileReaderModule: require('../__mocks__/FileReaderModule'),
|
||||
});
|
||||
|
||||
var Blob = require('Blob');
|
||||
var FileReader = require('FileReader');
|
||||
|
||||
describe('FileReader', function() {
|
||||
it('should read blob as text', async () => {
|
||||
const e = await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = resolve;
|
||||
reader.onerror = reject;
|
||||
reader.readAsText(new Blob());
|
||||
});
|
||||
expect(e.target.result).toBe('');
|
||||
});
|
||||
|
||||
it('should read blob as data URL', async () => {
|
||||
const e = await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = resolve;
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(new Blob());
|
||||
});
|
||||
expect(e.target.result).toBe('data:text/plain;base64,NDI=');
|
||||
});
|
||||
});
|
|
@ -171,6 +171,8 @@ polyfillGlobal('Request', () => require('fetch').Request);
|
|||
polyfillGlobal('Response', () => require('fetch').Response);
|
||||
polyfillGlobal('WebSocket', () => require('WebSocket'));
|
||||
polyfillGlobal('Blob', () => require('Blob'));
|
||||
polyfillGlobal('File', () => require('File'));
|
||||
polyfillGlobal('FileReader', () => require('FileReader'));
|
||||
polyfillGlobal('URL', () => require('URL'));
|
||||
|
||||
// Set up alert
|
||||
|
|
|
@ -10,6 +10,22 @@
|
|||
#import <React/RCTEventEmitter.h>
|
||||
#import <React/RCTNetworkTask.h>
|
||||
|
||||
@protocol RCTNetworkingRequestHandler <NSObject>
|
||||
|
||||
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
|
||||
- (BOOL)canHandleNetworkingRequest:(NSDictionary *)data;
|
||||
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
|
||||
- (NSDictionary *)handleNetworkingRequest:(NSDictionary *)data;
|
||||
|
||||
@end
|
||||
|
||||
@protocol RCTNetworkingResponseHandler <NSObject>
|
||||
|
||||
- (BOOL)canHandleNetworkingResponse:(NSString *)responseType;
|
||||
- (id)handleNetworkingResponse:(NSURLResponse *)response data:(NSData *)data;
|
||||
|
||||
@end
|
||||
|
||||
@interface RCTNetworking : RCTEventEmitter
|
||||
|
||||
/**
|
||||
|
@ -24,6 +40,14 @@
|
|||
- (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request
|
||||
completionBlock:(RCTURLRequestCompletionBlock)completionBlock;
|
||||
|
||||
- (void)addRequestHandler:(id<RCTNetworkingRequestHandler>)handler;
|
||||
|
||||
- (void)addResponseHandler:(id<RCTNetworkingResponseHandler>)handler;
|
||||
|
||||
- (void)removeRequestHandler:(id<RCTNetworkingRequestHandler>)handler;
|
||||
|
||||
- (void)removeResponseHandler:(id<RCTNetworkingResponseHandler>)handler;
|
||||
|
||||
@end
|
||||
|
||||
@interface RCTBridge (RCTNetworking)
|
||||
|
|
|
@ -18,6 +18,8 @@ const convertRequestBody = require('convertRequestBody');
|
|||
|
||||
import type {RequestBody} from 'convertRequestBody';
|
||||
|
||||
import type { NativeResponseType } from './XMLHttpRequest';
|
||||
|
||||
class RCTNetworking extends NativeEventEmitter {
|
||||
|
||||
isAvailable: boolean = true;
|
||||
|
@ -32,7 +34,7 @@ class RCTNetworking extends NativeEventEmitter {
|
|||
url: string,
|
||||
headers: Object,
|
||||
data: RequestBody,
|
||||
responseType: 'text' | 'base64',
|
||||
responseType: NativeResponseType,
|
||||
incrementalUpdates: boolean,
|
||||
timeout: number,
|
||||
callback: (requestId: number) => any,
|
||||
|
|
|
@ -131,12 +131,20 @@ static NSString *RCTGenerateFormBoundary()
|
|||
NSMutableDictionary<NSNumber *, RCTNetworkTask *> *_tasksByRequestID;
|
||||
std::mutex _handlersLock;
|
||||
NSArray<id<RCTURLRequestHandler>> *_handlers;
|
||||
NSMutableArray<id<RCTNetworkingRequestHandler>> *_requestHandlers;
|
||||
NSMutableArray<id<RCTNetworkingResponseHandler>> *_responseHandlers;
|
||||
}
|
||||
|
||||
@synthesize methodQueue = _methodQueue;
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
|
||||
- (void)invalidate
|
||||
{
|
||||
_requestHandlers = nil;
|
||||
_responseHandlers = nil;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)supportedEvents
|
||||
{
|
||||
return @[@"didCompleteNetworkResponse",
|
||||
|
@ -297,6 +305,8 @@ RCT_EXPORT_MODULE()
|
|||
*
|
||||
* - {"formData": [...]}: list of data payloads that will be combined into a multipart/form-data request
|
||||
*
|
||||
* - {"blob": {...}}: an object representing a blob
|
||||
*
|
||||
* If successful, the callback be called with a result dictionary containing the following (optional) keys:
|
||||
*
|
||||
* - @"body" (NSData): the body of the request
|
||||
|
@ -312,6 +322,15 @@ RCT_EXPORT_MODULE()
|
|||
if (!query) {
|
||||
return callback(nil, nil);
|
||||
}
|
||||
for (id<RCTNetworkingRequestHandler> handler in _requestHandlers) {
|
||||
if ([handler canHandleNetworkingRequest:query]) {
|
||||
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
|
||||
NSDictionary *body = [handler handleNetworkingRequest:query];
|
||||
if (body) {
|
||||
return callback(nil, body);
|
||||
}
|
||||
}
|
||||
}
|
||||
NSData *body = [RCTConvert NSData:query[@"string"]];
|
||||
if (body) {
|
||||
return callback(nil, @{@"body": body});
|
||||
|
@ -417,6 +436,7 @@ RCT_EXPORT_MODULE()
|
|||
|
||||
- (void)sendData:(NSData *)data
|
||||
responseType:(NSString *)responseType
|
||||
response:(NSURLResponse *)response
|
||||
forTask:(RCTNetworkTask *)task
|
||||
{
|
||||
RCTAssertThread(_methodQueue, @"sendData: must be called on method queue");
|
||||
|
@ -425,23 +445,31 @@ RCT_EXPORT_MODULE()
|
|||
return;
|
||||
}
|
||||
|
||||
NSString *responseString;
|
||||
if ([responseType isEqualToString:@"text"]) {
|
||||
// No carry storage is required here because the entire data has been loaded.
|
||||
responseString = [RCTNetworking decodeTextData:data fromResponse:task.response withCarryData:nil];
|
||||
if (!responseString) {
|
||||
RCTLogWarn(@"Received data was not a string, or was not a recognised encoding.");
|
||||
return;
|
||||
id responseData = nil;
|
||||
for (id<RCTNetworkingResponseHandler> handler in _responseHandlers) {
|
||||
if ([handler canHandleNetworkingResponse:responseType]) {
|
||||
responseData = [handler handleNetworkingResponse:response data:data];
|
||||
break;
|
||||
}
|
||||
} else if ([responseType isEqualToString:@"base64"]) {
|
||||
responseString = [data base64EncodedStringWithOptions:0];
|
||||
} else {
|
||||
RCTLogWarn(@"Invalid responseType: %@", responseType);
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray<id> *responseJSON = @[task.requestID, responseString];
|
||||
[self sendEventWithName:@"didReceiveNetworkData" body:responseJSON];
|
||||
if (!responseData) {
|
||||
if ([responseType isEqualToString:@"text"]) {
|
||||
// No carry storage is required here because the entire data has been loaded.
|
||||
responseData = [RCTNetworking decodeTextData:data fromResponse:task.response withCarryData:nil];
|
||||
if (!responseData) {
|
||||
RCTLogWarn(@"Received data was not a string, or was not a recognised encoding.");
|
||||
return;
|
||||
}
|
||||
} else if ([responseType isEqualToString:@"base64"]) {
|
||||
responseData = [data base64EncodedStringWithOptions:0];
|
||||
} else {
|
||||
RCTLogWarn(@"Invalid responseType: %@", responseType);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
[self sendEventWithName:@"didReceiveNetworkData" body:@[task.requestID, responseData]];
|
||||
}
|
||||
|
||||
- (void)sendRequest:(NSURLRequest *)request
|
||||
|
@ -523,7 +551,10 @@ RCT_EXPORT_MODULE()
|
|||
// Unless we were sending incremental (text) chunks to JS, all along, now
|
||||
// is the time to send the request body to JS.
|
||||
if (!(incrementalUpdates && [responseType isEqualToString:@"text"])) {
|
||||
[strongSelf sendData:data responseType:responseType forTask:task];
|
||||
[strongSelf sendData:data
|
||||
responseType:responseType
|
||||
response:response
|
||||
forTask:task];
|
||||
}
|
||||
NSArray *responseJSON = @[task.requestID,
|
||||
RCTNullIfNil(error.localizedDescription),
|
||||
|
@ -553,6 +584,32 @@ RCT_EXPORT_MODULE()
|
|||
|
||||
#pragma mark - Public API
|
||||
|
||||
- (void)addRequestHandler:(id<RCTNetworkingRequestHandler>)handler
|
||||
{
|
||||
if (!_requestHandlers) {
|
||||
_requestHandlers = [NSMutableArray new];
|
||||
}
|
||||
[_requestHandlers addObject:handler];
|
||||
}
|
||||
|
||||
- (void)addResponseHandler:(id<RCTNetworkingResponseHandler>)handler
|
||||
{
|
||||
if (!_responseHandlers) {
|
||||
_responseHandlers = [NSMutableArray new];
|
||||
}
|
||||
[_responseHandlers addObject:handler];
|
||||
}
|
||||
|
||||
- (void)removeRequestHandler:(id<RCTNetworkingRequestHandler>)handler
|
||||
{
|
||||
[_requestHandlers removeObject:handler];
|
||||
}
|
||||
|
||||
- (void)removeResponseHandler:(id<RCTNetworkingResponseHandler>)handler
|
||||
{
|
||||
[_responseHandlers removeObject:handler];
|
||||
}
|
||||
|
||||
- (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request completionBlock:(RCTURLRequestCompletionBlock)completionBlock
|
||||
{
|
||||
id<RCTURLRequestHandler> handler = [self handlerForRequest:request];
|
||||
|
|
|
@ -23,9 +23,11 @@ const invariant = require('fbjs/lib/invariant');
|
|||
* found when Flow v0.54 was deployed. To see the error delete this comment and
|
||||
* run Flow. */
|
||||
const warning = require('fbjs/lib/warning');
|
||||
const BlobManager = require('BlobManager');
|
||||
|
||||
type ResponseType = '' | 'arraybuffer' | 'blob' | 'document' | 'json' | 'text';
|
||||
type Response = ?Object | string;
|
||||
export type NativeResponseType = 'base64' | 'blob' | 'text';
|
||||
export type ResponseType = '' | 'arraybuffer' | 'blob' | 'document' | 'json' | 'text';
|
||||
export type Response = ?Object | string;
|
||||
|
||||
type XHRInterceptor = {
|
||||
requestSent(
|
||||
|
@ -54,6 +56,11 @@ type XHRInterceptor = {
|
|||
): void,
|
||||
};
|
||||
|
||||
// The native blob module is optional so inject it here if available.
|
||||
if (BlobManager.isAvailable) {
|
||||
BlobManager.addNetworkingHandler();
|
||||
}
|
||||
|
||||
const UNSENT = 0;
|
||||
const OPENED = 1;
|
||||
const HEADERS_RECEIVED = 2;
|
||||
|
@ -200,6 +207,10 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
|||
SUPPORTED_RESPONSE_TYPES[responseType] || responseType === 'document',
|
||||
`The provided value '${responseType}' is unsupported in this environment.`
|
||||
);
|
||||
|
||||
if (responseType === 'blob') {
|
||||
invariant(BlobManager.isAvailable, 'Native module BlobModule is required for blob support');
|
||||
}
|
||||
this._responseType = responseType;
|
||||
}
|
||||
|
||||
|
@ -242,10 +253,11 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
|||
break;
|
||||
|
||||
case 'blob':
|
||||
this._cachedResponse = new global.Blob(
|
||||
[base64.toByteArray(this._response).buffer],
|
||||
{type: this.getResponseHeader('content-type') || ''}
|
||||
);
|
||||
if (typeof this._response === 'object' && this._response) {
|
||||
this._cachedResponse = BlobManager.createFromOptions(this._response);
|
||||
} else {
|
||||
throw new Error(`Invalid response for blob: ${this._response}`);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'json':
|
||||
|
@ -493,10 +505,13 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
|||
(args) => this.__didCompleteResponse(...args)
|
||||
));
|
||||
|
||||
let nativeResponseType = 'text';
|
||||
if (this._responseType === 'arraybuffer' || this._responseType === 'blob') {
|
||||
let nativeResponseType: NativeResponseType = 'text';
|
||||
if (this._responseType === 'arraybuffer') {
|
||||
nativeResponseType = 'base64';
|
||||
}
|
||||
if (this._responseType === 'blob') {
|
||||
nativeResponseType = 'blob';
|
||||
}
|
||||
|
||||
invariant(this._method, 'Request method needs to be defined.');
|
||||
invariant(this._url, 'Request URL needs to be defined.');
|
||||
|
|
|
@ -8,25 +8,30 @@
|
|||
*
|
||||
* @providesModule convertRequestBody
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const binaryToBase64 = require('binaryToBase64');
|
||||
|
||||
const Blob = require('Blob');
|
||||
const FormData = require('FormData');
|
||||
|
||||
export type RequestBody =
|
||||
string
|
||||
| string
|
||||
| Blob
|
||||
| FormData
|
||||
| {uri: string}
|
||||
| ArrayBuffer
|
||||
| $ArrayBufferView
|
||||
;
|
||||
| $ArrayBufferView;
|
||||
|
||||
function convertRequestBody(body: RequestBody): Object {
|
||||
if (typeof body === 'string') {
|
||||
return {string: body};
|
||||
}
|
||||
if (body instanceof Blob) {
|
||||
return {blob: body.data};
|
||||
}
|
||||
if (body instanceof FormData) {
|
||||
return {formData: body.getParts()};
|
||||
}
|
||||
|
|
|
@ -13,8 +13,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
@protocol RCTWebSocketContentHandler <NSObject>
|
||||
|
||||
- (id)processMessage:(id __nullable)message forSocketID:(NSNumber *)socketID
|
||||
withType:(NSString *__nonnull __autoreleasing *__nonnull)type;
|
||||
- (id)processWebsocketMessage:(id __nullable)message
|
||||
forSocketID:(NSNumber *)socketID
|
||||
withType:(NSString *__nonnull __autoreleasing *__nonnull)type;
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
@implementation RCTWebSocketModule
|
||||
{
|
||||
NSMutableDictionary<NSNumber *, RCTSRWebSocket *> *_sockets;
|
||||
NSMutableDictionary<NSNumber *, id> *_contentHandlers;
|
||||
NSMutableDictionary<NSNumber *, id<RCTWebSocketContentHandler>> *_contentHandlers;
|
||||
}
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
|
@ -53,8 +53,9 @@ RCT_EXPORT_MODULE()
|
|||
@"websocketClosed"];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
- (void)invalidate
|
||||
{
|
||||
_contentHandlers = nil;
|
||||
for (RCTSRWebSocket *socket in _sockets.allValues) {
|
||||
socket.delegate = nil;
|
||||
[socket close];
|
||||
|
@ -135,7 +136,7 @@ RCT_EXPORT_METHOD(close:(nonnull NSNumber *)socketID)
|
|||
NSNumber *socketID = [webSocket reactTag];
|
||||
id contentHandler = _contentHandlers[socketID];
|
||||
if (contentHandler) {
|
||||
message = [contentHandler processMessage:message forSocketID:socketID withType:&type];
|
||||
message = [contentHandler processWebsocketMessage:message forSocketID:socketID withType:&type];
|
||||
} else {
|
||||
if ([message isKindOfClass:[NSData class]]) {
|
||||
type = @"binary";
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
const Blob = require('Blob');
|
||||
const EventTarget = require('event-target-shim');
|
||||
const NativeEventEmitter = require('NativeEventEmitter');
|
||||
const BlobManager = require('BlobManager');
|
||||
const NativeModules = require('NativeModules');
|
||||
const Platform = require('Platform');
|
||||
const WebSocketEvent = require('WebSocketEvent');
|
||||
|
@ -147,19 +148,20 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
|
|||
throw new Error('binaryType must be either \'blob\' or \'arraybuffer\'');
|
||||
}
|
||||
if (this._binaryType === 'blob' || binaryType === 'blob') {
|
||||
const BlobModule = NativeModules.BlobModule;
|
||||
invariant(BlobModule, 'Native module BlobModule is required for blob support');
|
||||
if (BlobModule) {
|
||||
if (binaryType === 'blob') {
|
||||
BlobModule.enableBlobSupport(this._socketId);
|
||||
} else {
|
||||
BlobModule.disableBlobSupport(this._socketId);
|
||||
}
|
||||
invariant(BlobManager.isAvailable, 'Native module BlobModule is required for blob support');
|
||||
if (binaryType === 'blob') {
|
||||
BlobManager.addWebSocketHandler(this._socketId);
|
||||
} else {
|
||||
BlobManager.removeWebSocketHandler(this._socketId);
|
||||
}
|
||||
}
|
||||
this._binaryType = binaryType;
|
||||
}
|
||||
|
||||
get binaryType(): ?BinaryType {
|
||||
return this._binaryType;
|
||||
}
|
||||
|
||||
close(code?: number, reason?: string): void {
|
||||
if (this.readyState === this.CLOSING ||
|
||||
this.readyState === this.CLOSED) {
|
||||
|
@ -176,9 +178,8 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
|
|||
}
|
||||
|
||||
if (data instanceof Blob) {
|
||||
const BlobModule = NativeModules.BlobModule;
|
||||
invariant(BlobModule, 'Native module BlobModule is required for blob support');
|
||||
BlobModule.sendBlob(data, this._socketId);
|
||||
invariant(BlobManager.isAvailable, 'Native module BlobModule is required for blob support');
|
||||
BlobManager.sendOverSocket(data, this._socketId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -212,6 +213,10 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
|
|||
} else {
|
||||
WebSocketModule.close(this._socketId);
|
||||
}
|
||||
|
||||
if (BlobManager.isAvailable && this._binaryType === 'blob') {
|
||||
BlobManager.removeWebSocketHandler(this._socketId);
|
||||
}
|
||||
}
|
||||
|
||||
_unregisterEvents(): void {
|
||||
|
@ -231,7 +236,7 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
|
|||
data = base64.toByteArray(ev.data).buffer;
|
||||
break;
|
||||
case 'blob':
|
||||
data = Blob.create(ev.data);
|
||||
data = BlobManager.createFromOptions(ev.data);
|
||||
break;
|
||||
}
|
||||
this.dispatchEvent(new WebSocketEvent('message', { data }));
|
||||
|
|
|
@ -53,6 +53,8 @@
|
|||
192F69B91E82409A008692C7 /* RCTConvert_YGValueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 192F69B61E82409A008692C7 /* RCTConvert_YGValueTests.m */; };
|
||||
192F69BA1E82409A008692C7 /* RCTNativeAnimatedNodesManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 192F69B71E82409A008692C7 /* RCTNativeAnimatedNodesManagerTests.m */; };
|
||||
192F69DA1E8240E2008692C7 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 13E501A31D07A502005F35D8 /* libRCTAnimation.a */; };
|
||||
19BA88D51F84344F00741C5A /* RCTBlobManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 19BA88D41F84344F00741C5A /* RCTBlobManagerTests.m */; };
|
||||
19BA89031F8439A700741C5A /* libRCTBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5281CA511EEAC9A700AC40CD /* libRCTBlob.a */; };
|
||||
272E6B3F1BEA849E001FCF37 /* UpdatePropertiesExampleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 272E6B3C1BEA849E001FCF37 /* UpdatePropertiesExampleView.m */; };
|
||||
27B885561BED29AF00008352 /* RCTRootViewIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 27B885551BED29AF00008352 /* RCTRootViewIntegrationTests.m */; };
|
||||
27F441EC1BEBE5030039B79C /* FlexibleSizeExampleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F441E81BEBE5030039B79C /* FlexibleSizeExampleView.m */; };
|
||||
|
@ -515,6 +517,7 @@
|
|||
192F69B51E82409A008692C7 /* RCTAnimationUtilsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAnimationUtilsTests.m; sourceTree = "<group>"; };
|
||||
192F69B61E82409A008692C7 /* RCTConvert_YGValueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert_YGValueTests.m; sourceTree = "<group>"; };
|
||||
192F69B71E82409A008692C7 /* RCTNativeAnimatedNodesManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTNativeAnimatedNodesManagerTests.m; sourceTree = "<group>"; };
|
||||
19BA88D41F84344F00741C5A /* RCTBlobManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTBlobManagerTests.m; sourceTree = "<group>"; };
|
||||
272E6B3B1BEA849E001FCF37 /* UpdatePropertiesExampleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = UpdatePropertiesExampleView.h; path = RNTester/NativeExampleViews/UpdatePropertiesExampleView.h; sourceTree = "<group>"; };
|
||||
272E6B3C1BEA849E001FCF37 /* UpdatePropertiesExampleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = UpdatePropertiesExampleView.m; path = RNTester/NativeExampleViews/UpdatePropertiesExampleView.m; sourceTree = "<group>"; };
|
||||
27B885551BED29AF00008352 /* RCTRootViewIntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRootViewIntegrationTests.m; sourceTree = "<group>"; };
|
||||
|
@ -553,6 +556,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
19BA89031F8439A700741C5A /* libRCTBlob.a in Frameworks */,
|
||||
192F69DA1E8240E2008692C7 /* libRCTAnimation.a in Frameworks */,
|
||||
14D6D71E1B2222EF001FB087 /* libRCTActionSheet.a in Frameworks */,
|
||||
14D6D7201B2222EF001FB087 /* libRCTGeolocation.a in Frameworks */,
|
||||
|
@ -767,6 +771,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
192F69B51E82409A008692C7 /* RCTAnimationUtilsTests.m */,
|
||||
19BA88D41F84344F00741C5A /* RCTBlobManagerTests.m */,
|
||||
192F69B61E82409A008692C7 /* RCTConvert_YGValueTests.m */,
|
||||
192F69B71E82409A008692C7 /* RCTNativeAnimatedNodesManagerTests.m */,
|
||||
13B6C1A21C34225900D3FAF5 /* RCTURLUtilsTests.m */,
|
||||
|
@ -884,6 +889,13 @@
|
|||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
19BA89021F8439A700741C5A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
272E6B3A1BEA846C001FCF37 /* NativeExampleViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1683,6 +1695,7 @@
|
|||
8385CF041B87479200C6273E /* RCTImageLoaderHelpers.m in Sources */,
|
||||
192F69B91E82409A008692C7 /* RCTConvert_YGValueTests.m in Sources */,
|
||||
BC9C03401DC9F1D600B1C635 /* RCTDevMenuTests.m in Sources */,
|
||||
19BA88D51F84344F00741C5A /* RCTBlobManagerTests.m in Sources */,
|
||||
68FF44381CF6111500720EFD /* RCTBundleURLProviderTests.m in Sources */,
|
||||
8385CEF51B873B5C00C6273E /* RCTImageLoaderTests.m in Sources */,
|
||||
);
|
||||
|
|
|
@ -97,6 +97,7 @@
|
|||
2DD323E71DA2DE3F000FE1B8 /* libRCTSettings-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DD323C81DA2DD8B000FE1B8 /* libRCTSettings-tvOS.a */; };
|
||||
2DD323E81DA2DE3F000FE1B8 /* libRCTText-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DD323D01DA2DD8B000FE1B8 /* libRCTText-tvOS.a */; };
|
||||
2DD323E91DA2DE3F000FE1B8 /* libRCTWebSocket-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DD323D51DA2DD8B000FE1B8 /* libRCTWebSocket-tvOS.a */; };
|
||||
2DD323EA1DA2DE3F000FE1B8 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DD323D91DA2DD8B000FE1B8 /* libReact.a */; };
|
||||
3578590A1B28D2CF00341EDB /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 357859011B28D2C500341EDB /* libRCTLinking.a */; };
|
||||
39AA31A41DC1DFDC000F7EBB /* RCTUnicodeDecodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 39AA31A31DC1DFDC000F7EBB /* RCTUnicodeDecodeTests.m */; };
|
||||
3D05746D1DE6008900184BB4 /* libRCTPushNotification-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D05746C1DE6008900184BB4 /* libRCTPushNotification-tvOS.a */; };
|
||||
|
@ -117,6 +118,9 @@
|
|||
83636F8F1B53F22C009F943E /* RCTUIManagerScenarioTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 83636F8E1B53F22C009F943E /* RCTUIManagerScenarioTests.m */; };
|
||||
8385CEF51B873B5C00C6273E /* RCTImageLoaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8385CEF41B873B5C00C6273E /* RCTImageLoaderTests.m */; };
|
||||
8385CF041B87479200C6273E /* RCTImageLoaderHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 8385CF031B87479200C6273E /* RCTImageLoaderHelpers.m */; };
|
||||
ADAC7A091E093BB900D77272 /* libRCTBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADBDB9F11DFEC24500ED6528 /* libRCTBlob.a */; };
|
||||
ADBDBA0D1DFEC24D00ED6528 /* libRCTBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADBDB9F11DFEC24500ED6528 /* libRCTBlob.a */; };
|
||||
ADD01A631E093FA900F6D226 /* libRCTBlob-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADD01A471E093FA100F6D226 /* libRCTBlob-tvOS.a */; };
|
||||
BC9C03401DC9F1D600B1C635 /* RCTDevMenuTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BC9C033F1DC9F1D600B1C635 /* RCTDevMenuTests.m */; };
|
||||
C654F14C1EB34D0C000B7A9A /* RNTesterTestModule.m in Sources */ = {isa = PBXBuildFile; fileRef = C654F14B1EB34D0C000B7A9A /* RNTesterTestModule.m */; };
|
||||
C654F16E1EB34D14000B7A9A /* RNTesterTestModule.m in Sources */ = {isa = PBXBuildFile; fileRef = C654F14B1EB34D0C000B7A9A /* RNTesterTestModule.m */; };
|
||||
|
@ -383,6 +387,20 @@
|
|||
remoteGlobalIDString = 134814201AA4EA6300B7C361;
|
||||
remoteInfo = RCTSettings;
|
||||
};
|
||||
ADBDB9F01DFEC24500ED6528 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = ADBDB9E81DFEC24500ED6528 /* RCTBlob.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 358F4ED71D1E81A9004DF814;
|
||||
remoteInfo = RCTBlob;
|
||||
};
|
||||
ADD01A461E093FA100F6D226 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = ADBDB9E81DFEC24500ED6528 /* RCTBlob.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = ADAC7A2E1E093EF800D77272;
|
||||
remoteInfo = "RCTBlob-tvOS";
|
||||
};
|
||||
D85B829B1AB6D5CE003F4FE2 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */;
|
||||
|
@ -470,6 +488,7 @@
|
|||
8385CEF41B873B5C00C6273E /* RCTImageLoaderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoaderTests.m; sourceTree = "<group>"; };
|
||||
8385CF031B87479200C6273E /* RCTImageLoaderHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoaderHelpers.m; sourceTree = "<group>"; };
|
||||
8385CF051B8747A000C6273E /* RCTImageLoaderHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTImageLoaderHelpers.h; sourceTree = "<group>"; };
|
||||
ADBDB9E81DFEC24500ED6528 /* RCTBlob.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTBlob.xcodeproj; path = ../../Libraries/Blob/RCTBlob.xcodeproj; sourceTree = "<group>"; };
|
||||
BC9C033F1DC9F1D600B1C635 /* RCTDevMenuTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDevMenuTests.m; sourceTree = "<group>"; };
|
||||
C654F14B1EB34D0C000B7A9A /* RNTesterTestModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNTesterTestModule.m; sourceTree = "<group>"; };
|
||||
D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = ../Libraries/Vibration/RCTVibration.xcodeproj; sourceTree = "<group>"; };
|
||||
|
@ -544,6 +563,7 @@
|
|||
2DD323E61DA2DE3F000FE1B8 /* libRCTNetwork-tvOS.a in Frameworks */,
|
||||
3D05746D1DE6008900184BB4 /* libRCTPushNotification-tvOS.a in Frameworks */,
|
||||
2DD323E71DA2DE3F000FE1B8 /* libRCTSettings-tvOS.a in Frameworks */,
|
||||
ADD01A631E093FA900F6D226 /* libRCTBlob-tvOS.a in Frameworks */,
|
||||
2DD323E81DA2DE3F000FE1B8 /* libRCTText-tvOS.a in Frameworks */,
|
||||
2DD323E91DA2DE3F000FE1B8 /* libRCTWebSocket-tvOS.a in Frameworks */,
|
||||
);
|
||||
|
@ -755,6 +775,21 @@
|
|||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
14AADF001AC3DB95002390C9 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
14AADF041AC3DB95002390C9 /* libReact.a */,
|
||||
2DD323D91DA2DD8B000FE1B8 /* libReact.a */,
|
||||
3D3C08811DE3424E00C268FA /* libyoga.a */,
|
||||
3D3C08831DE3424E00C268FA /* libyoga.a */,
|
||||
3D05748C1DE6008900184BB4 /* libcxxreact.a */,
|
||||
3D05748E1DE6008900184BB4 /* libcxxreact.a */,
|
||||
3D0574901DE6008900184BB4 /* libjschelpers.a */,
|
||||
3D0574921DE6008900184BB4 /* libjschelpers.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
14D6D6EA1B2205C0001FB087 /* OCMock */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -892,6 +927,15 @@
|
|||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ADBDB9E91DFEC24500ED6528 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ADBDB9F11DFEC24500ED6528 /* libRCTBlob.a */,
|
||||
ADD01A471E093FA100F6D226 /* libRCTBlob-tvOS.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D85B82921AB6D5CE003F4FE2 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1082,6 +1126,10 @@
|
|||
ProductGroup = 13E5019D1D07A502005F35D8 /* Products */;
|
||||
ProjectRef = 13E5019C1D07A502005F35D8 /* RCTAnimation.xcodeproj */;
|
||||
},
|
||||
{
|
||||
ProductGroup = ADBDB9E91DFEC24500ED6528 /* Products */;
|
||||
ProjectRef = ADBDB9E81DFEC24500ED6528 /* RCTBlob.xcodeproj */;
|
||||
},
|
||||
{
|
||||
ProductGroup = 138DEE031B9EDDDB007F4EA5 /* Products */;
|
||||
ProjectRef = 138DEE021B9EDDDB007F4EA5 /* RCTCameraRoll.xcodeproj */;
|
||||
|
@ -1320,6 +1368,13 @@
|
|||
remoteRef = 2DD323D41DA2DD8B000FE1B8 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
2DD323D91DA2DD8B000FE1B8 /* libReact.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = libReact.a;
|
||||
remoteRef = 2DD323D81DA2DD8B000FE1B8 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
357859011B28D2C500341EDB /* libRCTLinking.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
|
@ -1376,6 +1431,20 @@
|
|||
remoteRef = 834C36D11AF8DA610019C93C /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
ADBDB9F11DFEC24500ED6528 /* libRCTBlob.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = libRCTBlob.a;
|
||||
remoteRef = ADBDB9F01DFEC24500ED6528 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
ADD01A471E093FA100F6D226 /* libRCTBlob-tvOS.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = "libRCTBlob-tvOS.a";
|
||||
remoteRef = ADD01A461E093FA100F6D226 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
D85B829C1AB6D5CE003F4FE2 /* libRCTVibration.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
*/
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import <RCTBlob/RCTBlobManager.h>
|
||||
|
||||
@interface RCTBlobManagerTests : XCTestCase
|
||||
|
||||
@end
|
||||
|
||||
@implementation RCTBlobManagerTests
|
||||
{
|
||||
RCTBlobManager *_module;
|
||||
NSMutableData *_data;
|
||||
NSString *_blobId;
|
||||
}
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
[super setUp];
|
||||
|
||||
_module = [RCTBlobManager new];
|
||||
[_module setValue:nil forKey:@"bridge"];
|
||||
NSInteger size = 120;
|
||||
_data = [NSMutableData dataWithCapacity:size];
|
||||
for (NSInteger i = 0; i < size / 4; i++) {
|
||||
uint32_t randomBits = arc4random();
|
||||
[_data appendBytes:(void *)&randomBits length:4];
|
||||
}
|
||||
_blobId = [NSUUID UUID].UUIDString;
|
||||
[_module store:_data withId:_blobId];
|
||||
}
|
||||
|
||||
- (void)testResolve
|
||||
{
|
||||
XCTAssertTrue([_data isEqualToData:[_module resolve:_blobId offset:0 size:_data.length]]);
|
||||
NSData *rangeData = [_data subdataWithRange:NSMakeRange(30, _data.length - 30)];
|
||||
XCTAssertTrue([rangeData isEqualToData:[_module resolve:_blobId offset:30 size:_data.length - 30]]);
|
||||
}
|
||||
|
||||
- (void)testResolveMap
|
||||
{
|
||||
NSDictionary<NSString *, id> *map = @{
|
||||
@"blobId": _blobId,
|
||||
@"size": @(_data.length),
|
||||
@"offset": @0,
|
||||
};
|
||||
XCTAssertTrue([_data isEqualToData:[_module resolve:map]]);
|
||||
}
|
||||
|
||||
- (void)testResolveURL
|
||||
{
|
||||
NSURLComponents *components = [NSURLComponents new];
|
||||
[components setPath:_blobId];
|
||||
[components setQuery:[NSString stringWithFormat:@"offset=0&size=%lu", (unsigned long)_data.length]];
|
||||
XCTAssertTrue([_data isEqualToData:[_module resolveURL:[components URL]]]);
|
||||
}
|
||||
|
||||
- (void)testRemove
|
||||
{
|
||||
XCTAssertNotNil([_module resolve:_blobId offset:0 size:_data.length]);
|
||||
[_module remove:_blobId];
|
||||
XCTAssertNil([_module resolve:_blobId offset:0 size:_data.length]);
|
||||
}
|
||||
|
||||
- (void)testCreateFromParts
|
||||
{
|
||||
NSDictionary<NSString *, id> *blobData = @{
|
||||
@"blobId": _blobId,
|
||||
@"offset": @0,
|
||||
@"size": @(_data.length),
|
||||
};
|
||||
NSDictionary<NSString *, id> *blob = @{
|
||||
@"data": blobData,
|
||||
@"type": @"blob",
|
||||
};
|
||||
NSString *stringData = @"i \u2665 dogs";
|
||||
NSDictionary<NSString *, id> *string = @{
|
||||
@"data": stringData,
|
||||
@"type": @"string",
|
||||
};
|
||||
NSString *resultId = [NSUUID UUID].UUIDString;
|
||||
NSArray<id> *parts = @[blob, string];
|
||||
|
||||
[_module createFromParts:parts withId:resultId];
|
||||
|
||||
NSMutableData *expectedData = [NSMutableData new];
|
||||
[expectedData appendData:_data];
|
||||
[expectedData appendData:[stringData dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
|
||||
NSData *result = [_module resolve:resultId offset:0 size:expectedData.length];
|
||||
|
||||
XCTAssertTrue([expectedData isEqualToData:result]);
|
||||
}
|
||||
|
||||
@end
|
|
@ -166,7 +166,7 @@ Pod::Spec.new do |s|
|
|||
|
||||
s.subspec "RCTBlob" do |ss|
|
||||
ss.dependency "React/Core"
|
||||
ss.source_files = "Libraries/Blob/*.{h,m}"
|
||||
ss.source_files = "Libraries/Blob/*.{h,m,mm}"
|
||||
ss.preserve_paths = "Libraries/Blob/*.js"
|
||||
end
|
||||
|
||||
|
|
|
@ -14,11 +14,13 @@ rn_android_library(
|
|||
react_native_dep("libraries/fbcore/src/main/java/com/facebook/common/logging:logging"),
|
||||
react_native_dep("third-party/java/infer-annotations:infer-annotations"),
|
||||
react_native_dep("third-party/java/jsr-305:jsr-305"),
|
||||
react_native_dep("third-party/java/okhttp:okhttp3"),
|
||||
react_native_dep("third-party/java/okio:okio"),
|
||||
react_native_target("java/com/facebook/react:react"),
|
||||
react_native_target("java/com/facebook/react/bridge:bridge"),
|
||||
react_native_target("java/com/facebook/react/common:common"),
|
||||
react_native_target("java/com/facebook/react/module/annotations:annotations"),
|
||||
react_native_target("java/com/facebook/react/modules/network:network"),
|
||||
react_native_target("java/com/facebook/react/modules/websocket:websocket"),
|
||||
],
|
||||
)
|
||||
|
|
|
@ -7,9 +7,15 @@
|
|||
*/
|
||||
package com.facebook.react.modules.blob;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.MediaStore;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
|
@ -19,13 +25,25 @@ import com.facebook.react.bridge.ReadableMap;
|
|||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.common.MapBuilder;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
import com.facebook.react.modules.network.NetworkingModule;
|
||||
import com.facebook.react.modules.websocket.WebSocketModule;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.ResponseBody;
|
||||
import okio.ByteString;
|
||||
|
||||
@ReactModule(name = BlobModule.NAME)
|
||||
|
@ -35,27 +53,100 @@ public class BlobModule extends ReactContextBaseJavaModule {
|
|||
|
||||
private final Map<String, byte[]> mBlobs = new HashMap<>();
|
||||
|
||||
protected final WebSocketModule.ContentHandler mContentHandler =
|
||||
new WebSocketModule.ContentHandler() {
|
||||
@Override
|
||||
public void onMessage(String text, WritableMap params) {
|
||||
params.putString("data", text);
|
||||
private final WebSocketModule.ContentHandler mWebSocketContentHandler =
|
||||
new WebSocketModule.ContentHandler() {
|
||||
@Override
|
||||
public void onMessage(String text, WritableMap params) {
|
||||
params.putString("data", text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(ByteString bytes, WritableMap params) {
|
||||
byte[] data = bytes.toByteArray();
|
||||
|
||||
WritableMap blob = Arguments.createMap();
|
||||
|
||||
blob.putString("blobId", store(data));
|
||||
blob.putInt("offset", 0);
|
||||
blob.putInt("size", data.length);
|
||||
|
||||
params.putMap("data", blob);
|
||||
params.putString("type", "blob");
|
||||
}
|
||||
};
|
||||
|
||||
private final NetworkingModule.UriHandler mNetworkingUriHandler =
|
||||
new NetworkingModule.UriHandler() {
|
||||
@Override
|
||||
public boolean supports(Uri uri, String responseType) {
|
||||
String scheme = uri.getScheme();
|
||||
boolean isRemote = scheme.equals("http") || scheme.equals("https");
|
||||
|
||||
return (!isRemote && responseType.equals("blob"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public WritableMap fetch(Uri uri) throws IOException {
|
||||
byte[] data = getBytesFromUri(uri);
|
||||
|
||||
WritableMap blob = Arguments.createMap();
|
||||
blob.putString("blobId", store(data));
|
||||
blob.putInt("offset", 0);
|
||||
blob.putInt("size", data.length);
|
||||
blob.putString("type", getMimeTypeFromUri(uri));
|
||||
|
||||
// Needed for files
|
||||
blob.putString("name", getNameFromUri(uri));
|
||||
blob.putDouble("lastModified", getLastModifiedFromUri(uri));
|
||||
|
||||
return blob;
|
||||
}
|
||||
};
|
||||
|
||||
private final NetworkingModule.RequestBodyHandler mNetworkingRequestBodyHandler =
|
||||
new NetworkingModule.RequestBodyHandler() {
|
||||
@Override
|
||||
public boolean supports(ReadableMap data) {
|
||||
return data.hasKey("blob");
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBody toRequestBody(ReadableMap data, String contentType) {
|
||||
String type = contentType;
|
||||
if (data.hasKey("type") && !data.getString("type").isEmpty()) {
|
||||
type = data.getString("type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(ByteString bytes, WritableMap params) {
|
||||
byte[] data = bytes.toByteArray();
|
||||
|
||||
WritableMap blob = Arguments.createMap();
|
||||
|
||||
blob.putString("blobId", store(data));
|
||||
blob.putInt("offset", 0);
|
||||
blob.putInt("size", data.length);
|
||||
|
||||
params.putMap("data", blob);
|
||||
params.putString("type", "blob");
|
||||
if (type == null) {
|
||||
type = "application/octet-stream";
|
||||
}
|
||||
};
|
||||
ReadableMap blob = data.getMap("blob");
|
||||
String blobId = blob.getString("blobId");
|
||||
byte[] bytes = resolve(
|
||||
blobId,
|
||||
blob.getInt("offset"),
|
||||
blob.getInt("size"));
|
||||
|
||||
return RequestBody.create(MediaType.parse(type), bytes);
|
||||
}
|
||||
};
|
||||
|
||||
private final NetworkingModule.ResponseHandler mNetworkingResponseHandler =
|
||||
new NetworkingModule.ResponseHandler() {
|
||||
@Override
|
||||
public boolean supports(String responseType) {
|
||||
return responseType.equals("blob");
|
||||
}
|
||||
|
||||
@Override
|
||||
public WritableMap toResponseData(ResponseBody body) throws IOException {
|
||||
byte[] data = body.bytes();
|
||||
WritableMap blob = Arguments.createMap();
|
||||
blob.putString("blobId", store(data));
|
||||
blob.putInt("offset", 0);
|
||||
blob.putInt("size", data.length);
|
||||
return blob;
|
||||
}
|
||||
};
|
||||
|
||||
public BlobModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
|
@ -67,8 +158,7 @@ public class BlobModule extends ReactContextBaseJavaModule {
|
|||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Map getConstants() {
|
||||
public @Nullable Map<String, Object> getConstants() {
|
||||
// The application can register BlobProvider as a ContentProvider so that blobs are resolvable.
|
||||
// If it does, it needs to tell us what authority was used via this string resource.
|
||||
Resources resources = getReactApplicationContext().getResources();
|
||||
|
@ -78,8 +168,8 @@ public class BlobModule extends ReactContextBaseJavaModule {
|
|||
return null;
|
||||
}
|
||||
|
||||
return MapBuilder.of(
|
||||
"BLOB_URI_SCHEME", "content", "BLOB_URI_HOST", resources.getString(resourceId));
|
||||
return MapBuilder.<String, Object>of(
|
||||
"BLOB_URI_SCHEME", "content", "BLOB_URI_HOST", resources.getString(resourceId));
|
||||
}
|
||||
|
||||
public String store(byte[] data) {
|
||||
|
@ -96,8 +186,7 @@ public class BlobModule extends ReactContextBaseJavaModule {
|
|||
mBlobs.remove(blobId);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public byte[] resolve(Uri uri) {
|
||||
public @Nullable byte[] resolve(Uri uri) {
|
||||
String blobId = uri.getLastPathSegment();
|
||||
int offset = 0;
|
||||
int size = -1;
|
||||
|
@ -112,8 +201,7 @@ public class BlobModule extends ReactContextBaseJavaModule {
|
|||
return resolve(blobId, offset, size);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public byte[] resolve(String blobId, int offset, int size) {
|
||||
public @Nullable byte[] resolve(String blobId, int offset, int size) {
|
||||
byte[] data = mBlobs.get(blobId);
|
||||
if (data == null) {
|
||||
return null;
|
||||
|
@ -121,33 +209,101 @@ public class BlobModule extends ReactContextBaseJavaModule {
|
|||
if (size == -1) {
|
||||
size = data.length - offset;
|
||||
}
|
||||
if (offset > 0) {
|
||||
if (offset > 0 || size != data.length) {
|
||||
data = Arrays.copyOfRange(data, offset, offset + size);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public byte[] resolve(ReadableMap blob) {
|
||||
public @Nullable byte[] resolve(ReadableMap blob) {
|
||||
return resolve(blob.getString("blobId"), blob.getInt("offset"), blob.getInt("size"));
|
||||
}
|
||||
|
||||
private byte[] getBytesFromUri(Uri contentUri) throws IOException {
|
||||
InputStream is = getReactApplicationContext().getContentResolver().openInputStream(contentUri);
|
||||
|
||||
if (is == null) {
|
||||
throw new FileNotFoundException("File not found for " + contentUri);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
|
||||
int bufferSize = 1024;
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
int len;
|
||||
while ((len = is.read(buffer)) != -1) {
|
||||
byteBuffer.write(buffer, 0, len);
|
||||
}
|
||||
return byteBuffer.toByteArray();
|
||||
}
|
||||
|
||||
private String getNameFromUri(Uri contentUri) {
|
||||
if (contentUri.getScheme().equals("file")) {
|
||||
return contentUri.getLastPathSegment();
|
||||
}
|
||||
String[] projection = {MediaStore.MediaColumns.DISPLAY_NAME};
|
||||
Cursor metaCursor = getReactApplicationContext()
|
||||
.getContentResolver()
|
||||
.query(contentUri, projection, null, null, null);
|
||||
if (metaCursor != null) {
|
||||
try {
|
||||
if (metaCursor.moveToFirst()) {
|
||||
return metaCursor.getString(0);
|
||||
}
|
||||
} finally {
|
||||
metaCursor.close();
|
||||
}
|
||||
}
|
||||
return contentUri.getLastPathSegment();
|
||||
}
|
||||
|
||||
private long getLastModifiedFromUri(Uri contentUri) {
|
||||
if (contentUri.getScheme().equals("file")) {
|
||||
return new File(contentUri.toString()).lastModified();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private String getMimeTypeFromUri(Uri contentUri) {
|
||||
String type = getReactApplicationContext().getContentResolver().getType(contentUri);
|
||||
|
||||
if (type == null) {
|
||||
String ext = MimeTypeMap.getFileExtensionFromUrl(contentUri.getPath());
|
||||
if (ext != null) {
|
||||
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
|
||||
}
|
||||
}
|
||||
|
||||
if (type == null) {
|
||||
type = "";
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
private WebSocketModule getWebSocketModule() {
|
||||
return getReactApplicationContext().getNativeModule(WebSocketModule.class);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void enableBlobSupport(final int id) {
|
||||
getWebSocketModule().setContentHandler(id, mContentHandler);
|
||||
public void addNetworkingHandler() {
|
||||
NetworkingModule networkingModule = getReactApplicationContext().getNativeModule(NetworkingModule.class);
|
||||
networkingModule.addUriHandler(mNetworkingUriHandler);
|
||||
networkingModule.addRequestBodyHandler(mNetworkingRequestBodyHandler);
|
||||
networkingModule.addResponseHandler(mNetworkingResponseHandler);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void disableBlobSupport(final int id) {
|
||||
public void addWebSocketHandler(final int id) {
|
||||
getWebSocketModule().setContentHandler(id, mWebSocketContentHandler);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeWebSocketHandler(final int id) {
|
||||
getWebSocketModule().setContentHandler(id, null);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void sendBlob(ReadableMap blob, int id) {
|
||||
public void sendOverSocket(ReadableMap blob, int id) {
|
||||
byte[] data = resolve(blob.getString("blobId"), blob.getInt("offset"), blob.getInt("size"));
|
||||
|
||||
if (data != null) {
|
||||
|
@ -160,15 +316,27 @@ public class BlobModule extends ReactContextBaseJavaModule {
|
|||
@ReactMethod
|
||||
public void createFromParts(ReadableArray parts, String blobId) {
|
||||
int totalBlobSize = 0;
|
||||
ArrayList<ReadableMap> partList = new ArrayList<>(parts.size());
|
||||
ArrayList<byte[]> partList = new ArrayList<>(parts.size());
|
||||
for (int i = 0; i < parts.size(); i++) {
|
||||
ReadableMap part = parts.getMap(i);
|
||||
totalBlobSize += part.getInt("size");
|
||||
partList.add(i, part);
|
||||
switch (part.getString("type")) {
|
||||
case "blob":
|
||||
ReadableMap blob = part.getMap("data");
|
||||
totalBlobSize += blob.getInt("size");
|
||||
partList.add(i, resolve(blob));
|
||||
break;
|
||||
case "string":
|
||||
byte[] bytes = part.getString("data").getBytes(Charset.forName("UTF-8"));
|
||||
totalBlobSize += bytes.length;
|
||||
partList.add(i, bytes);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid type for blob: " + part.getString("type"));
|
||||
}
|
||||
}
|
||||
ByteBuffer buffer = ByteBuffer.allocate(totalBlobSize);
|
||||
for (ReadableMap part : partList) {
|
||||
buffer.put(resolve(part));
|
||||
for (byte[] bytes : partList) {
|
||||
buffer.put(bytes);
|
||||
}
|
||||
store(buffer.array(), blobId);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
package com.facebook.react.modules.blob;
|
||||
|
||||
import android.util.Base64;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
|
||||
|
||||
@ReactModule(name = FileReaderModule.NAME)
|
||||
public class FileReaderModule extends ReactContextBaseJavaModule {
|
||||
|
||||
protected static final String NAME = "FileReaderModule";
|
||||
private static final String ERROR_INVALID_BLOB = "ERROR_INVALID_BLOB";
|
||||
|
||||
public FileReaderModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
private BlobModule getBlobModule() {
|
||||
return getReactApplicationContext().getNativeModule(BlobModule.class);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void readAsText(ReadableMap blob, String encoding, Promise promise) {
|
||||
|
||||
byte[] bytes = getBlobModule().resolve(
|
||||
blob.getString("blobId"),
|
||||
blob.getInt("offset"),
|
||||
blob.getInt("size"));
|
||||
|
||||
if (bytes == null) {
|
||||
promise.reject(ERROR_INVALID_BLOB, "The specified blob is invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
promise.resolve(new String(bytes, encoding));
|
||||
} catch (Exception e) {
|
||||
promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void readAsDataURL(ReadableMap blob, Promise promise) {
|
||||
byte[] bytes = getBlobModule().resolve(
|
||||
blob.getString("blobId"),
|
||||
blob.getInt("offset"),
|
||||
blob.getInt("size"));
|
||||
|
||||
if (bytes == null) {
|
||||
promise.reject(ERROR_INVALID_BLOB, "The specified blob is invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("data:");
|
||||
|
||||
if (blob.hasKey("type") && !blob.getString("type").isEmpty()) {
|
||||
sb.append(blob.getString("type"));
|
||||
} else {
|
||||
sb.append("application/octet-stream");
|
||||
}
|
||||
|
||||
sb.append(";base64,");
|
||||
sb.append(Base64.encodeToString(bytes, Base64.NO_WRAP));
|
||||
|
||||
promise.resolve(sb.toString());
|
||||
} catch (Exception e) {
|
||||
promise.reject(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,20 +6,9 @@
|
|||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
package com.facebook.react.modules.network;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.Reader;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.util.Base64;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
|
@ -35,6 +24,17 @@ import com.facebook.react.common.network.OkHttpCallUtil;
|
|||
import com.facebook.react.module.annotations.ReactModule;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.CookieJar;
|
||||
|
@ -56,6 +56,52 @@ import okio.ByteString;
|
|||
@ReactModule(name = NetworkingModule.NAME)
|
||||
public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||
|
||||
/**
|
||||
* Allows to implement a custom fetching process for specific URIs. It is the handler's job
|
||||
* to fetch the URI and return the JS body payload.
|
||||
*/
|
||||
public interface UriHandler {
|
||||
/**
|
||||
* Returns if the handler should be used for an URI.
|
||||
*/
|
||||
boolean supports(Uri uri, String responseType);
|
||||
|
||||
/**
|
||||
* Fetch the URI and return the JS body payload.
|
||||
*/
|
||||
WritableMap fetch(Uri uri) throws IOException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows adding custom handling to build the {@link RequestBody} from the JS body payload.
|
||||
*/
|
||||
public interface RequestBodyHandler {
|
||||
/**
|
||||
* Returns if the handler should be used for a JS body payload.
|
||||
*/
|
||||
boolean supports(ReadableMap map);
|
||||
|
||||
/**
|
||||
* Returns the {@link RequestBody} for the JS body payload.
|
||||
*/
|
||||
RequestBody toRequestBody(ReadableMap map, String contentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows adding custom handling to build the JS body payload from the {@link ResponseBody}.
|
||||
*/
|
||||
public interface ResponseHandler {
|
||||
/**
|
||||
* Returns if the handler should be used for a response type.
|
||||
*/
|
||||
boolean supports(String responseType);
|
||||
|
||||
/**
|
||||
* Returns the JS body payload for the {@link ResponseBody}.
|
||||
*/
|
||||
WritableMap toResponseData(ResponseBody body) throws IOException;
|
||||
}
|
||||
|
||||
protected static final String NAME = "Networking";
|
||||
|
||||
private static final String CONTENT_ENCODING_HEADER_NAME = "content-encoding";
|
||||
|
@ -73,6 +119,9 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
|||
private final @Nullable String mDefaultUserAgent;
|
||||
private final CookieJarContainer mCookieJarContainer;
|
||||
private final Set<Integer> mRequestIds;
|
||||
private final List<RequestBodyHandler> mRequestBodyHandlers = new ArrayList<>();
|
||||
private final List<UriHandler> mUriHandlers = new ArrayList<>();
|
||||
private final List<ResponseHandler> mResponseHandlers = new ArrayList<>();
|
||||
private boolean mShuttingDown;
|
||||
|
||||
/* package */ NetworkingModule(
|
||||
|
@ -154,6 +203,34 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
|||
|
||||
mCookieHandler.destroy();
|
||||
mCookieJarContainer.removeCookieJar();
|
||||
|
||||
mRequestBodyHandlers.clear();
|
||||
mResponseHandlers.clear();
|
||||
mUriHandlers.clear();
|
||||
}
|
||||
|
||||
public void addUriHandler(UriHandler handler) {
|
||||
mUriHandlers.add(handler);
|
||||
}
|
||||
|
||||
public void addRequestBodyHandler(RequestBodyHandler handler) {
|
||||
mRequestBodyHandlers.add(handler);
|
||||
}
|
||||
|
||||
public void addResponseHandler(ResponseHandler handler) {
|
||||
mResponseHandlers.add(handler);
|
||||
}
|
||||
|
||||
public void removeUriHandler(UriHandler handler) {
|
||||
mUriHandlers.remove(handler);
|
||||
}
|
||||
|
||||
public void removeRequestBodyHandler(RequestBodyHandler handler) {
|
||||
mRequestBodyHandlers.remove(handler);
|
||||
}
|
||||
|
||||
public void removeResponseHandler(ResponseHandler handler) {
|
||||
mResponseHandlers.remove(handler);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
|
@ -170,13 +247,31 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
|||
final boolean useIncrementalUpdates,
|
||||
int timeout,
|
||||
boolean withCredentials) {
|
||||
final RCTDeviceEventEmitter eventEmitter = getEventEmitter();
|
||||
|
||||
try {
|
||||
Uri uri = Uri.parse(url);
|
||||
|
||||
// Check if a handler is registered
|
||||
for (UriHandler handler : mUriHandlers) {
|
||||
if (handler.supports(uri, responseType)) {
|
||||
WritableMap res = handler.fetch(uri);
|
||||
ResponseUtil.onDataReceived(eventEmitter, requestId, res);
|
||||
ResponseUtil.onRequestSuccess(eventEmitter, requestId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
ResponseUtil.onRequestError(eventEmitter, requestId, e.getMessage(), e);
|
||||
return;
|
||||
}
|
||||
|
||||
Request.Builder requestBuilder = new Request.Builder().url(url);
|
||||
|
||||
if (requestId != 0) {
|
||||
requestBuilder.tag(requestId);
|
||||
}
|
||||
|
||||
final RCTDeviceEventEmitter eventEmitter = getEventEmitter();
|
||||
OkHttpClient.Builder clientBuilder = mClient.newBuilder();
|
||||
|
||||
if (!withCredentials) {
|
||||
|
@ -237,8 +332,22 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
|||
String contentEncoding = requestHeaders.get(CONTENT_ENCODING_HEADER_NAME);
|
||||
requestBuilder.headers(requestHeaders);
|
||||
|
||||
// Check if a handler is registered
|
||||
RequestBodyHandler handler = null;
|
||||
if (data != null) {
|
||||
for (RequestBodyHandler curHandler : mRequestBodyHandlers) {
|
||||
if (curHandler.supports(data)) {
|
||||
handler = curHandler;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data == null) {
|
||||
requestBuilder.method(method, RequestBodyUtil.getEmptyBody(method));
|
||||
} else if (handler != null) {
|
||||
RequestBody requestBody = handler.toRequestBody(data, contentType);
|
||||
requestBuilder.method(method, requestBody);
|
||||
} else if (data.hasKey(REQUEST_BODY_KEY_STRING)) {
|
||||
if (contentType == null) {
|
||||
ResponseUtil.onRequestError(
|
||||
|
@ -360,6 +469,16 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
|||
|
||||
ResponseBody responseBody = response.body();
|
||||
try {
|
||||
// Check if a handler is registered
|
||||
for (ResponseHandler handler : mResponseHandlers) {
|
||||
if (handler.supports(responseType)) {
|
||||
WritableMap res = handler.toResponseData(responseBody);
|
||||
ResponseUtil.onDataReceived(eventEmitter, requestId, res);
|
||||
ResponseUtil.onRequestSuccess(eventEmitter, requestId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If JS wants progress updates during the download, and it requested a text response,
|
||||
// periodically send response data updates to JS.
|
||||
if (useIncrementalUpdates && responseType.equals("text")) {
|
||||
|
|
|
@ -9,14 +9,14 @@
|
|||
|
||||
package com.facebook.react.modules.network;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
|
||||
/**
|
||||
* Util methods to send network responses to JS.
|
||||
*/
|
||||
|
@ -72,6 +72,17 @@ public class ResponseUtil {
|
|||
eventEmitter.emit("didReceiveNetworkData", args);
|
||||
}
|
||||
|
||||
public static void onDataReceived(
|
||||
RCTDeviceEventEmitter eventEmitter,
|
||||
int requestId,
|
||||
WritableMap data) {
|
||||
WritableArray args = Arguments.createArray();
|
||||
args.pushInt(requestId);
|
||||
args.pushMap(data);
|
||||
|
||||
eventEmitter.emit("didReceiveNetworkData", args);
|
||||
}
|
||||
|
||||
public static void onRequestError(
|
||||
RCTDeviceEventEmitter eventEmitter,
|
||||
int requestId,
|
||||
|
|
|
@ -30,6 +30,7 @@ import com.facebook.react.module.model.ReactModuleInfoProvider;
|
|||
import com.facebook.react.modules.accessibilityinfo.AccessibilityInfoModule;
|
||||
import com.facebook.react.modules.appstate.AppStateModule;
|
||||
import com.facebook.react.modules.blob.BlobModule;
|
||||
import com.facebook.react.modules.blob.FileReaderModule;
|
||||
import com.facebook.react.modules.camera.CameraRollManager;
|
||||
import com.facebook.react.modules.camera.ImageEditingManager;
|
||||
import com.facebook.react.modules.camera.ImageStoreManager;
|
||||
|
@ -125,6 +126,14 @@ public class MainReactPackage extends LazyReactPackage {
|
|||
return new BlobModule(context);
|
||||
}
|
||||
}),
|
||||
ModuleSpec.nativeModuleSpec(
|
||||
FileReaderModule.class,
|
||||
new Provider<NativeModule>() {
|
||||
@Override
|
||||
public NativeModule get() {
|
||||
return new FileReaderModule(context);
|
||||
}
|
||||
}),
|
||||
ModuleSpec.nativeModuleSpec(
|
||||
AsyncStorageModule.class,
|
||||
new Provider<NativeModule>() {
|
||||
|
|
|
@ -26,6 +26,7 @@ rn_robolectric_test(
|
|||
react_native_target("java/com/facebook/react/common/network:network"),
|
||||
react_native_target("java/com/facebook/react/devsupport:interfaces"),
|
||||
react_native_target("java/com/facebook/react/jstasks:jstasks"),
|
||||
react_native_target("java/com/facebook/react/modules/blob:blob"),
|
||||
react_native_target("java/com/facebook/react/modules/camera:camera"),
|
||||
react_native_target("java/com/facebook/react/modules/clipboard:clipboard"),
|
||||
react_native_target("java/com/facebook/react/modules/common:common"),
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
package com.facebook.react.modules.blob;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.JavaOnlyArray;
|
||||
import com.facebook.react.bridge.JavaOnlyMap;
|
||||
import com.facebook.react.bridge.ReactTestHelper;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.powermock.api.mockito.PowerMockito;
|
||||
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.modules.junit4.rule.PowerMockRule;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
@PrepareForTest({Arguments.class})
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"})
|
||||
@Config(manifest = Config.NONE)
|
||||
public class BlobModuleTest {
|
||||
|
||||
private byte[] mBytes;
|
||||
private String mBlobId;
|
||||
private BlobModule mBlobModule;
|
||||
|
||||
@Rule
|
||||
public PowerMockRule rule = new PowerMockRule();
|
||||
|
||||
@Before
|
||||
public void prepareModules() throws Exception {
|
||||
PowerMockito.mockStatic(Arguments.class);
|
||||
Mockito.when(Arguments.createMap()).thenAnswer(new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
return new JavaOnlyMap();
|
||||
}
|
||||
});
|
||||
|
||||
mBytes = new byte[120];
|
||||
new Random().nextBytes(mBytes);
|
||||
|
||||
mBlobModule = new BlobModule(ReactTestHelper.createCatalystContextForTest());
|
||||
mBlobId = mBlobModule.store(mBytes);
|
||||
}
|
||||
|
||||
@After
|
||||
public void cleanUp() {
|
||||
mBlobModule.remove(mBlobId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolve() {
|
||||
assertArrayEquals(mBytes, mBlobModule.resolve(mBlobId, 0, mBytes.length));
|
||||
byte[] expectedRange = Arrays.copyOfRange(mBytes, 30, mBytes.length);
|
||||
assertArrayEquals(expectedRange, mBlobModule.resolve(mBlobId, 30, mBytes.length - 30));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveUri() {
|
||||
Uri uri = new Uri.Builder()
|
||||
.appendPath(mBlobId)
|
||||
.appendQueryParameter("offset", "0")
|
||||
.appendQueryParameter("size", String.valueOf(mBytes.length))
|
||||
.build();
|
||||
|
||||
assertArrayEquals(mBytes, mBlobModule.resolve(uri));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveMap() {
|
||||
JavaOnlyMap blob = new JavaOnlyMap();
|
||||
blob.putString("blobId", mBlobId);
|
||||
blob.putInt("offset", 0);
|
||||
blob.putInt("size", mBytes.length);
|
||||
|
||||
assertArrayEquals(mBytes, mBlobModule.resolve(blob));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemove() {
|
||||
assertNotNull(mBlobModule.resolve(mBlobId, 0, mBytes.length));
|
||||
|
||||
mBlobModule.remove(mBlobId);
|
||||
|
||||
assertNull(mBlobModule.resolve(mBlobId, 0, mBytes.length));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateFromParts() {
|
||||
String id = UUID.randomUUID().toString();
|
||||
|
||||
JavaOnlyMap blobData = new JavaOnlyMap();
|
||||
blobData.putString("blobId", mBlobId);
|
||||
blobData.putInt("offset", 0);
|
||||
blobData.putInt("size", mBytes.length);
|
||||
JavaOnlyMap blob = new JavaOnlyMap();
|
||||
blob.putMap("data", blobData);
|
||||
blob.putString("type", "blob");
|
||||
|
||||
String stringData = "i \u2665 dogs";
|
||||
byte[] stringBytes = stringData.getBytes(Charset.forName("UTF-8"));
|
||||
JavaOnlyMap string = new JavaOnlyMap();
|
||||
string.putString("data", stringData);
|
||||
string.putString("type", "string");
|
||||
|
||||
JavaOnlyArray parts = new JavaOnlyArray();
|
||||
parts.pushMap(blob);
|
||||
parts.pushMap(string);
|
||||
|
||||
mBlobModule.createFromParts(parts, id);
|
||||
|
||||
int resultSize = mBytes.length + stringBytes.length;
|
||||
|
||||
byte[] result = mBlobModule.resolve(id, 0, resultSize);
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(resultSize);
|
||||
buffer.put(mBytes);
|
||||
buffer.put(stringBytes);
|
||||
|
||||
assertArrayEquals(result, buffer.array());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRelease() {
|
||||
assertNotNull(mBlobModule.resolve(mBlobId, 0, mBytes.length));
|
||||
|
||||
mBlobModule.release(mBlobId);
|
||||
|
||||
assertNull(mBlobModule.resolve(mBlobId, 0, mBytes.length));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue