From ed903099b42259958d8a6eb3af1a6460c1bc8b2c Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Wed, 26 Jul 2017 08:12:12 -0700 Subject: [PATCH] Add blob implementation with WebSocket integration Summary: This is the first PR from a series of PRs grabbou and me will make to add blob support to React Native. The next PR will include blob support for XMLHttpRequest. I'd like to get this merged with minimal changes to preserve the attribution. My next PR can contain bigger changes. Blobs are used to transfer binary data between server and client. Currently React Native lacks a way to deal with binary data. The only thing that comes close is uploading files through a URI. Current workarounds to transfer binary data includes encoding and decoding them to base64 and and transferring them as string, which is not ideal, since it increases the payload size and the whole payload needs to be sent via the bridge every time changes are made. The PR adds a way to deal with blobs via a new native module. The blob is constructed on the native side and the data never needs to pass through the bridge. Currently the only way to create a blob is to receive a blob from the server via websocket. The PR is largely a direct port of https://github.com/silklabs/silk/tree/master/react-native-blobs by philikon into RN (with changes to integrate with RN), and attributed as such. > **Note:** This is a breaking change for all people running iOS without CocoaPods. You will have to manually add `RCTBlob.xcodeproj` to your `Libraries` and then, add it to Build Phases. Just follow the process of manual linking. We'll also need to document this process in the release notes. Related discussion - https://github.com/facebook/react-native/issues/11103 - `Image` can't show image when `URL.createObjectURL` is used with large images on Android The websocket integration can be tested via a simple server, ```js const fs = require('fs'); const http = require('http'); const WebSocketServer = require('ws').Server; const wss = new WebSocketServer({ server: http.createServer().listen(7232), }); wss.on('connection', (ws) => { ws.on('message', (d) => { console.log(d); }); ws.send(fs.readFileSync('./some-file')); }); ``` Then on the client, ```js var ws = new WebSocket('ws://localhost:7232'); ws.binaryType = 'blob'; ws.onerror = (error) => { console.error(error); }; ws.onmessage = (e) => { console.log(e.data); ws.send(e.data); }; ``` cc brentvatne ide Closes https://github.com/facebook/react-native/pull/11417 Reviewed By: sahrens Differential Revision: D5188484 Pulled By: javache fbshipit-source-id: 6afcbc4d19aa7a27b0dc9d52701ba400e7d7e98f --- Libraries/Blob/Blob.js | 153 ++++++++ Libraries/Blob/BlobTypes.js | 25 ++ .../Blob/RCTBlob.xcodeproj/project.pbxproj | 344 +++++++++++++++++ Libraries/Blob/RCTBlobManager.h | 16 + Libraries/Blob/RCTBlobManager.m | 213 +++++++++++ Libraries/Blob/URL.js | 69 ++++ Libraries/Core/InitializeCore.js | 2 + Libraries/WebSocket/RCTWebSocketModule.h | 24 +- Libraries/WebSocket/RCTWebSocketModule.m | 75 +++- Libraries/WebSocket/RCTWebSocketObserver.h | 17 +- .../WebSocket}/RCTWebSocketObserverProtocol.h | 0 Libraries/WebSocket/WebSocket.js | 96 +++-- RNTester/RNTester.xcodeproj/project.pbxproj | 51 ++- RNTester/js/WebSocketExample.js | 138 ++++--- RNTester/js/http_test_server.js | 0 RNTester/js/websocket_test_server.js | 7 + React.podspec | 9 +- React/DevSupport/RCTPackagerConnection.m | 2 +- React/React.xcodeproj/project.pbxproj | 26 +- .../java/com/facebook/react/modules/blob/BUCK | 22 ++ .../react/modules/blob/BlobModule.java | 180 +++++++++ .../react/modules/blob/BlobProvider.java | 99 +++++ .../modules/websocket/WebSocketModule.java | 151 +++++--- .../main/java/com/facebook/react/shell/BUCK | 1 + .../react/shell/MainReactPackage.java | 351 ++++++++++-------- .../ios/HelloWorld.xcodeproj/project.pbxproj | 36 +- 26 files changed, 1800 insertions(+), 307 deletions(-) create mode 100644 Libraries/Blob/Blob.js create mode 100644 Libraries/Blob/BlobTypes.js create mode 100755 Libraries/Blob/RCTBlob.xcodeproj/project.pbxproj create mode 100755 Libraries/Blob/RCTBlobManager.h create mode 100755 Libraries/Blob/RCTBlobManager.m create mode 100644 Libraries/Blob/URL.js rename {React/Base => Libraries/WebSocket}/RCTWebSocketObserverProtocol.h (100%) mode change 100644 => 100755 RNTester/js/http_test_server.js mode change 100644 => 100755 RNTester/js/websocket_test_server.js create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/blob/BUCK create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobProvider.java diff --git a/Libraries/Blob/Blob.js b/Libraries/Blob/Blob.js new file mode 100644 index 000000000..0180e15e2 --- /dev/null +++ b/Libraries/Blob/Blob.js @@ -0,0 +1,153 @@ +/** + * 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 Blob + * @flow + */ + +'use strict'; + +const invariant = require('fbjs/lib/invariant'); +const uuid = require('uuid'); + +const { BlobModule } = require('NativeModules'); + +import type { BlobProps } from 'BlobTypes'; + +/** + * Opaque JS representation of some binary data in native. + * + * The API is modeled after the W3C Blob API, with one caveat + * regarding explicit deallocation. Refer to the `close()` + * method for further details. + * + * Example usage in a React component: + * + * class WebSocketImage extends React.Component { + * state = {blob: null}; + * componentDidMount() { + * let ws = this.ws = new WebSocket(...); + * ws.binaryType = 'blob'; + * ws.onmessage = (event) => { + * if (this.state.blob) { + * this.state.blob.close(); + * } + * this.setState({blob: event.data}); + * }; + * } + * componentUnmount() { + * if (this.state.blob) { + * this.state.blob.close(); + * } + * this.ws.close(); + * } + * render() { + * if (!this.state.blob) { + * return ; + * } + * return ; + * } + * } + * + * 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); + } + + /** + * 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, 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, + }); + } + + /* + * This method is used to create a new Blob object containing + * the data in the specified range of bytes of the source Blob. + * Reference: https://developer.mozilla.org/en-US/docs/Web/API/Blob/slice + */ + slice(start?: number, end?: number): Blob { + let offset = this.offset; + let size = this.size; + if (typeof start === 'number') { + if (start > size) { + start = size; + } + offset += start; + size -= start; + + if (typeof end === 'number') { + if (end < 0) { + end = this.size + end; + } + size = end - start; + } + } + return Blob.create({ + blobId: this.blobId, + offset, + size, + }); + } + + /** + * This method is in the standard, but not actually implemented by + * any browsers at this point. It's important for how Blobs work in + * React Native, however, since we cannot de-allocate resources automatically, + * so consumers need to explicitly de-allocate them. + * + * Note that the semantics around Blobs created via `blob.slice()` + * and `new Blob([blob])` are different. `blob.slice()` creates a + * new *view* onto the same binary data, so calling `close()` on any + * of those views is enough to deallocate the data, whereas + * `new Blob([blob, ...])` actually copies the data in memory. + */ + close() { + BlobModule.release(this.blobId); + } +} + +module.exports = Blob; diff --git a/Libraries/Blob/BlobTypes.js b/Libraries/Blob/BlobTypes.js new file mode 100644 index 000000000..8201df47b --- /dev/null +++ b/Libraries/Blob/BlobTypes.js @@ -0,0 +1,25 @@ +/** + * 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 BlobTypes + * @flow + */ + +'use strict'; + +export type BlobProps = { + blobId: string, + offset: number, + size: number, + type?: string, +}; + +export type FileProps = BlobProps & { + name: string, + lastModified: number, +}; diff --git a/Libraries/Blob/RCTBlob.xcodeproj/project.pbxproj b/Libraries/Blob/RCTBlob.xcodeproj/project.pbxproj new file mode 100755 index 000000000..64d1d7744 --- /dev/null +++ b/Libraries/Blob/RCTBlob.xcodeproj/project.pbxproj @@ -0,0 +1,344 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 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 */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 358F4ED51D1E81A9004DF814 /* Copy Headers */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = include/RCTBlob; + dstSubfolderSpec = 16; + files = ( + AD08711A1E2162C8007D136D /* RCTBlobManager.h in Copy Headers */, + ); + name = "Copy Headers"; + runOnlyForDeploymentPostprocessing = 0; + }; + AD0871121E215B16007D136D /* Copy Headers */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = include/RCTBlob; + dstSubfolderSpec = 16; + files = ( + AD0871131E215B28007D136D /* RCTBlobManager.h in Copy Headers */, + ); + name = "Copy Headers"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* 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 = ""; }; + AD9A43C21DFC7126008DC588 /* RCTBlobManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBlobManager.m; sourceTree = ""; }; + ADD01A681E09402E00F6D226 /* libRCTBlob-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libRCTBlob-tvOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXGroup section */ + 358F4ECE1D1E81A9004DF814 = { + isa = PBXGroup; + children = ( + AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */, + AD9A43C21DFC7126008DC588 /* RCTBlobManager.m */, + 358F4ED81D1E81A9004DF814 /* Products */, + ); + sourceTree = ""; + }; + 358F4ED81D1E81A9004DF814 /* Products */ = { + isa = PBXGroup; + children = ( + 358F4ED71D1E81A9004DF814 /* libRCTBlob.a */, + ADD01A681E09402E00F6D226 /* libRCTBlob-tvOS.a */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + AD0871151E215EB7007D136D /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + AD0871161E215EC9007D136D /* RCTBlobManager.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD0871171E215ECC007D136D /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + AD0871181E215ED1007D136D /* RCTBlobManager.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 358F4ED61D1E81A9004DF814 /* RCTBlob */ = { + isa = PBXNativeTarget; + buildConfigurationList = 358F4EE01D1E81A9004DF814 /* Build configuration list for PBXNativeTarget "RCTBlob" */; + buildPhases = ( + AD0871151E215EB7007D136D /* Headers */, + 358F4ED51D1E81A9004DF814 /* Copy Headers */, + 358F4ED31D1E81A9004DF814 /* Sources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RCTBlob; + productName = SLKBlobs; + productReference = 358F4ED71D1E81A9004DF814 /* libRCTBlob.a */; + productType = "com.apple.product-type.library.static"; + }; + ADD01A671E09402E00F6D226 /* RCTBlob-tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = ADD01A6E1E09402E00F6D226 /* Build configuration list for PBXNativeTarget "RCTBlob-tvOS" */; + buildPhases = ( + AD0871171E215ECC007D136D /* Headers */, + AD0871121E215B16007D136D /* Copy Headers */, + ADD01A641E09402E00F6D226 /* Sources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "RCTBlob-tvOS"; + productName = "RCTBlob-tvOS"; + productReference = ADD01A681E09402E00F6D226 /* libRCTBlob-tvOS.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 358F4ECF1D1E81A9004DF814 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0730; + ORGANIZATIONNAME = "Silk Labs"; + TargetAttributes = { + 358F4ED61D1E81A9004DF814 = { + CreatedOnToolsVersion = 7.3; + }; + ADD01A671E09402E00F6D226 = { + CreatedOnToolsVersion = 8.2; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 358F4ED21D1E81A9004DF814 /* Build configuration list for PBXProject "RCTBlob" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 358F4ECE1D1E81A9004DF814; + productRefGroup = 358F4ED81D1E81A9004DF814 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 358F4ED61D1E81A9004DF814 /* RCTBlob */, + ADD01A671E09402E00F6D226 /* RCTBlob-tvOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 358F4ED31D1E81A9004DF814 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AD9A43C31DFC7126008DC588 /* RCTBlobManager.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + ADD01A641E09402E00F6D226 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ADD01A711E09404A00F6D226 /* RCTBlobManager.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 358F4EDE1D1E81A9004DF814 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 358F4EDF1D1E81A9004DF814 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 358F4EE11D1E81A9004DF814 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 358F4EE21D1E81A9004DF814 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; + ADD01A6F1E09402E00F6D226 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + TVOS_DEPLOYMENT_TARGET = 10.1; + }; + name = Debug; + }; + ADD01A701E09402E00F6D226 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + TVOS_DEPLOYMENT_TARGET = 10.1; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 358F4ED21D1E81A9004DF814 /* Build configuration list for PBXProject "RCTBlob" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 358F4EDE1D1E81A9004DF814 /* Debug */, + 358F4EDF1D1E81A9004DF814 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 358F4EE01D1E81A9004DF814 /* Build configuration list for PBXNativeTarget "RCTBlob" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 358F4EE11D1E81A9004DF814 /* Debug */, + 358F4EE21D1E81A9004DF814 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + ADD01A6E1E09402E00F6D226 /* Build configuration list for PBXNativeTarget "RCTBlob-tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + ADD01A6F1E09402E00F6D226 /* Debug */, + ADD01A701E09402E00F6D226 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 358F4ECF1D1E81A9004DF814 /* Project object */; +} diff --git a/Libraries/Blob/RCTBlobManager.h b/Libraries/Blob/RCTBlobManager.h new file mode 100755 index 000000000..03b3ffc60 --- /dev/null +++ b/Libraries/Blob/RCTBlobManager.h @@ -0,0 +1,16 @@ +/** + * 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 +#import +#import + +@interface RCTBlobManager : NSObject + +@end diff --git a/Libraries/Blob/RCTBlobManager.m b/Libraries/Blob/RCTBlobManager.m new file mode 100755 index 000000000..bf7a82bb9 --- /dev/null +++ b/Libraries/Blob/RCTBlobManager.m @@ -0,0 +1,213 @@ +/** + * 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 +#import + +static NSString *const kBlobUriScheme = @"blob"; + +@interface _RCTBlobContentHandler : NSObject + +- (instancetype)initWithBlobManager:(RCTBlobManager *)blobManager; + +@end + + +@implementation RCTBlobManager +{ + NSMutableDictionary *_blobs; + _RCTBlobContentHandler *_contentHandler; + NSOperationQueue *_queue; +} + +RCT_EXPORT_MODULE(BlobModule) + +@synthesize bridge = _bridge; + +- (NSDictionary *)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 *)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 *> *)parts withId:(NSString *)blobId) +{ + NSMutableData *data = [NSMutableData new]; + for (NSDictionary *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)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 diff --git a/Libraries/Blob/URL.js b/Libraries/Blob/URL.js new file mode 100644 index 000000000..7eac3d520 --- /dev/null +++ b/Libraries/Blob/URL.js @@ -0,0 +1,69 @@ +/** + * 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 URL + * @flow + */ + +'use strict'; + +const Blob = require('Blob'); + +const { BlobModule } = require('NativeModules'); + +let BLOB_URL_PREFIX = null; + +if (typeof BlobModule.BLOB_URI_SCHEME === 'string') { + BLOB_URL_PREFIX = BlobModule.BLOB_URI_SCHEME + ':'; + if (typeof BlobModule.BLOB_URI_HOST === 'string') { + BLOB_URL_PREFIX += `//${BlobModule.BLOB_URI_HOST}/`; + } +} + +/** + * To allow Blobs be accessed via `content://` URIs, + * you need to register `BlobProvider` as a ContentProvider in your app's `AndroidManifest.xml`: + * + * ```xml + * + * + * + * + * + * ``` + * And then define the `blob_provider_authority` string in `res/values/strings.xml`. + * Use a dotted name that's entirely unique to your app: + * + * ```xml + * + * your.app.package.blobs + * + * ``` + */ +class URL { + constructor() { + throw new Error('Creating BlobURL 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}`; + } + + static revokeObjectURL(url: string) { + // Do nothing. + } +} + +module.exports = URL; diff --git a/Libraries/Core/InitializeCore.js b/Libraries/Core/InitializeCore.js index 4c161413e..80b3687b3 100644 --- a/Libraries/Core/InitializeCore.js +++ b/Libraries/Core/InitializeCore.js @@ -170,6 +170,8 @@ polyfillGlobal('Headers', () => require('fetch').Headers); polyfillGlobal('Request', () => require('fetch').Request); polyfillGlobal('Response', () => require('fetch').Response); polyfillGlobal('WebSocket', () => require('WebSocket')); +polyfillGlobal('Blob', () => require('Blob')); +polyfillGlobal('URL', () => require('URL')); // Set up alert if (!global.alert) { diff --git a/Libraries/WebSocket/RCTWebSocketModule.h b/Libraries/WebSocket/RCTWebSocketModule.h index 822b6f4f3..b2012bc4a 100644 --- a/Libraries/WebSocket/RCTWebSocketModule.h +++ b/Libraries/WebSocket/RCTWebSocketModule.h @@ -9,6 +9,28 @@ #import -@interface RCTWebSocketModule : RCTEventEmitter +NS_ASSUME_NONNULL_BEGIN + +@protocol RCTWebSocketContentHandler + +- (id)processMessage:(id __nullable)message forSocketID:(NSNumber *)socketID + withType:(NSString *__nonnull __autoreleasing *__nonnull)type; @end + +@interface RCTWebSocketModule : RCTEventEmitter + +// Register a custom handler for a specific websocket. The handler will be strongly held by the WebSocketModule. +- (void)setContentHandler:(id __nullable)handler forSocketID:(NSNumber *)socketID; + +- (void)sendData:(NSData *)data forSocketID:(nonnull NSNumber *)socketID; + +@end + +@interface RCTBridge (RCTWebSocketModule) + +- (RCTWebSocketModule *)webSocketModule; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Libraries/WebSocket/RCTWebSocketModule.m b/Libraries/WebSocket/RCTWebSocketModule.m index 5a420b70e..12f6da8a0 100644 --- a/Libraries/WebSocket/RCTWebSocketModule.m +++ b/Libraries/WebSocket/RCTWebSocketModule.m @@ -36,11 +36,15 @@ @implementation RCTWebSocketModule { - NSMutableDictionary *_sockets; + NSMutableDictionary *_sockets; + NSMutableDictionary *_contentHandlers; } RCT_EXPORT_MODULE() +// Used by RCTBlobModule +@synthesize methodQueue = _methodQueue; + - (NSArray *)supportedEvents { return @[@"websocketMessage", @@ -60,7 +64,7 @@ RCT_EXPORT_MODULE() RCT_EXPORT_METHOD(connect:(NSURL *)URL protocols:(NSArray *)protocols headers:(NSDictionary *)headers socketID:(nonnull NSNumber *)socketID) { NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL]; - + // We load cookies from sharedHTTPCookieStorage (shared with XHR and // fetch). To get secure cookies for wss URLs, replace wss with https // in the URL. @@ -72,7 +76,7 @@ RCT_EXPORT_METHOD(connect:(NSURL *)URL protocols:(NSArray *)protocols headers:(N // Load and set the cookie header. NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:components.URL]; request.allHTTPHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; - + // Load supplied headers [headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { [request addValue:[RCTConvert NSString:value] forHTTPHeaderField:key]; @@ -88,15 +92,19 @@ RCT_EXPORT_METHOD(connect:(NSURL *)URL protocols:(NSArray *)protocols headers:(N [webSocket open]; } -RCT_EXPORT_METHOD(send:(NSString *)message socketID:(nonnull NSNumber *)socketID) +RCT_EXPORT_METHOD(send:(NSString *)message forSocketID:(nonnull NSNumber *)socketID) { [_sockets[socketID] send:message]; } -RCT_EXPORT_METHOD(sendBinary:(NSString *)base64String socketID:(nonnull NSNumber *)socketID) +RCT_EXPORT_METHOD(sendBinary:(NSString *)base64String forSocketID:(nonnull NSNumber *)socketID) { - NSData *message = [[NSData alloc] initWithBase64EncodedString:base64String options:0]; - [_sockets[socketID] send:message]; + [self sendData:[[NSData alloc] initWithBase64EncodedString:base64String options:0] forSocketID:socketID]; +} + +- (void)sendData:(NSData *)data forSocketID:(nonnull NSNumber *)socketID +{ + [_sockets[socketID] send:data]; } RCT_EXPORT_METHOD(ping:(nonnull NSNumber *)socketID) @@ -110,14 +118,36 @@ RCT_EXPORT_METHOD(close:(nonnull NSNumber *)socketID) [_sockets removeObjectForKey:socketID]; } +- (void)setContentHandler:(id)handler forSocketID:(NSString *)socketID +{ + if (!_contentHandlers) { + _contentHandlers = [NSMutableDictionary new]; + } + _contentHandlers[socketID] = handler; +} + #pragma mark - RCTSRWebSocketDelegate methods - (void)webSocket:(RCTSRWebSocket *)webSocket didReceiveMessage:(id)message { - BOOL binary = [message isKindOfClass:[NSData class]]; + NSString *type; + + NSNumber *socketID = [webSocket reactTag]; + id contentHandler = _contentHandlers[socketID]; + if (contentHandler) { + message = [contentHandler processMessage:message forSocketID:socketID withType:&type]; + } else { + if ([message isKindOfClass:[NSData class]]) { + type = @"binary"; + message = [message base64EncodedStringWithOptions:0]; + } else { + type = @"text"; + } + } + [self sendEventWithName:@"websocketMessage" body:@{ - @"data": binary ? [message base64EncodedStringWithOptions:0] : message, - @"type": binary ? @"binary" : @"text", + @"data": message, + @"type": type, @"id": webSocket.reactTag }]; } @@ -131,21 +161,36 @@ RCT_EXPORT_METHOD(close:(nonnull NSNumber *)socketID) - (void)webSocket:(RCTSRWebSocket *)webSocket didFailWithError:(NSError *)error { + NSNumber *socketID = [webSocket reactTag]; + _contentHandlers[socketID] = nil; [self sendEventWithName:@"websocketFailed" body:@{ - @"message":error.localizedDescription, - @"id": webSocket.reactTag + @"message": error.localizedDescription, + @"id": socketID }]; } -- (void)webSocket:(RCTSRWebSocket *)webSocket didCloseWithCode:(NSInteger)code - reason:(NSString *)reason wasClean:(BOOL)wasClean +- (void)webSocket:(RCTSRWebSocket *)webSocket + didCloseWithCode:(NSInteger)code + reason:(NSString *)reason + wasClean:(BOOL)wasClean { + NSNumber *socketID = [webSocket reactTag]; + _contentHandlers[socketID] = nil; [self sendEventWithName:@"websocketClosed" body:@{ @"code": @(code), @"reason": RCTNullIfNil(reason), @"clean": @(wasClean), - @"id": webSocket.reactTag + @"id": socketID }]; } @end + +@implementation RCTBridge (RCTWebSocketModule) + +- (RCTWebSocketModule *)webSocketModule +{ + return [self moduleForClass:[RCTWebSocketModule class]]; +} + +@end diff --git a/Libraries/WebSocket/RCTWebSocketObserver.h b/Libraries/WebSocket/RCTWebSocketObserver.h index 0e4362bcf..4969876b4 100644 --- a/Libraries/WebSocket/RCTWebSocketObserver.h +++ b/Libraries/WebSocket/RCTWebSocketObserver.h @@ -8,11 +8,24 @@ */ #import -#import #if RCT_DEV // Only supported in dev mode -@interface RCTWebSocketObserver : NSObject +@protocol RCTWebSocketObserverDelegate + +- (void)didReceiveWebSocketMessage:(NSDictionary *)message; + +@end + +@interface RCTWebSocketObserver : NSObject + +- (instancetype)initWithURL:(NSURL *)url; + +@property (nonatomic, weak) id delegate; + +- (void)start; +- (void)stop; + @end #endif diff --git a/React/Base/RCTWebSocketObserverProtocol.h b/Libraries/WebSocket/RCTWebSocketObserverProtocol.h similarity index 100% rename from React/Base/RCTWebSocketObserverProtocol.h rename to Libraries/WebSocket/RCTWebSocketObserverProtocol.h diff --git a/Libraries/WebSocket/WebSocket.js b/Libraries/WebSocket/WebSocket.js index 40b086de2..83896e969 100644 --- a/Libraries/WebSocket/WebSocket.js +++ b/Libraries/WebSocket/WebSocket.js @@ -11,17 +11,35 @@ */ 'use strict'; -const NativeEventEmitter = require('NativeEventEmitter'); -const Platform = require('Platform'); -const RCTWebSocketModule = require('NativeModules').WebSocketModule; -const WebSocketEvent = require('WebSocketEvent'); -const binaryToBase64 = require('binaryToBase64'); - +const Blob = require('Blob'); const EventTarget = require('event-target-shim'); +const NativeEventEmitter = require('NativeEventEmitter'); +const NativeModules = require('NativeModules'); +const Platform = require('Platform'); +const WebSocketEvent = require('WebSocketEvent'); + const base64 = require('base64-js'); +const binaryToBase64 = require('binaryToBase64'); +const invariant = require('fbjs/lib/invariant'); + +const {WebSocketModule} = NativeModules; import type EventSubscription from 'EventSubscription'; +type ArrayBufferView = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array + | DataView + +type BinaryType = 'blob' | 'arraybuffer' + const CONNECTING = 0; const OPEN = 1; const CLOSING = 2; @@ -58,22 +76,22 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) { _socketId: number; _eventEmitter: NativeEventEmitter; _subscriptions: Array; + _binaryType: ?BinaryType; onclose: ?Function; onerror: ?Function; onmessage: ?Function; onopen: ?Function; - binaryType: ?string; bufferedAmount: number; extension: ?string; protocol: ?string; readyState: number = CONNECTING; url: ?string; - // This module depends on the native `RCTWebSocketModule` module. If you don't include it, + // This module depends on the native `WebSocketModule` module. If you don't include it, // `WebSocket.isAvailable` will return `false`, and WebSocket constructor will throw an error - static isAvailable: boolean = !!RCTWebSocketModule; + static isAvailable: boolean = !!WebSocketModule; constructor(url: string, protocols: ?string | ?Array, options: ?{origin?: string}) { super(); @@ -87,13 +105,35 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) { if (!WebSocket.isAvailable) { throw new Error('Cannot initialize WebSocket module. ' + - 'Native module RCTWebSocketModule is missing.'); + 'Native module WebSocketModule is missing.'); } - this._eventEmitter = new NativeEventEmitter(RCTWebSocketModule); + this._eventEmitter = new NativeEventEmitter(WebSocketModule); this._socketId = nextWebSocketId++; this._registerEvents(); - RCTWebSocketModule.connect(url, protocols, options, this._socketId); + WebSocketModule.connect(url, protocols, options, this._socketId); + } + + get binaryType(): ?BinaryType { + return this._binaryType; + } + + set binaryType(binaryType: BinaryType): void { + if (binaryType !== 'blob' && binaryType !== 'arraybuffer') { + 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); + } + } + } + this._binaryType = binaryType; } close(code?: number, reason?: string): void { @@ -106,18 +146,25 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) { this._close(code, reason); } - send(data: string | ArrayBuffer | $ArrayBufferView): void { + send(data: string | ArrayBuffer | ArrayBufferView | Blob): void { if (this.readyState === this.CONNECTING) { throw new Error('INVALID_STATE_ERR'); } + if (data instanceof Blob) { + const BlobModule = NativeModules.BlobModule; + invariant(BlobModule, 'Native module BlobModule is required for blob support'); + BlobModule.sendBlob(data, this._socketId); + return; + } + if (typeof data === 'string') { - RCTWebSocketModule.send(data, this._socketId); + WebSocketModule.send(data, this._socketId); return; } if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) { - RCTWebSocketModule.sendBinary(binaryToBase64(data), this._socketId); + WebSocketModule.sendBinary(binaryToBase64(data), this._socketId); return; } @@ -129,7 +176,7 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) { throw new Error('INVALID_STATE_ERR'); } - RCTWebSocketModule.ping(this._socketId); + WebSocketModule.ping(this._socketId); } _close(code?: number, reason?: string): void { @@ -137,9 +184,9 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) { // See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent const statusCode = typeof code === 'number' ? code : CLOSE_NORMAL; const closeReason = typeof reason === 'string' ? reason : ''; - RCTWebSocketModule.close(statusCode, closeReason, this._socketId); + WebSocketModule.close(statusCode, closeReason, this._socketId); } else { - RCTWebSocketModule.close(this._socketId); + WebSocketModule.close(this._socketId); } } @@ -154,9 +201,16 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) { if (ev.id !== this._socketId) { return; } - this.dispatchEvent(new WebSocketEvent('message', { - data: (ev.type === 'binary') ? base64.toByteArray(ev.data).buffer : ev.data - })); + let data = ev.data; + switch (ev.type) { + case 'binary': + data = base64.toByteArray(ev.data).buffer; + break; + case 'blob': + data = Blob.create(ev.data); + break; + } + this.dispatchEvent(new WebSocketEvent('message', { data })); }), this._eventEmitter.addListener('websocketOpen', ev => { if (ev.id !== this._socketId) { diff --git a/RNTester/RNTester.xcodeproj/project.pbxproj b/RNTester/RNTester.xcodeproj/project.pbxproj index c010bf4be..d8168f09e 100644 --- a/RNTester/RNTester.xcodeproj/project.pbxproj +++ b/RNTester/RNTester.xcodeproj/project.pbxproj @@ -37,7 +37,6 @@ 1497CFB01B21F5E400C1F8F2 /* RCTFontTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1497CFA81B21F5E400C1F8F2 /* RCTFontTests.m */; }; 1497CFB11B21F5E400C1F8F2 /* RCTEventDispatcherTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1497CFA91B21F5E400C1F8F2 /* RCTEventDispatcherTests.m */; }; 1497CFB31B21F5E400C1F8F2 /* RCTUIManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1497CFAB1B21F5E400C1F8F2 /* RCTUIManagerTests.m */; }; - 14AADF051AC3DBB1002390C9 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14AADF041AC3DB95002390C9 /* libReact.a */; }; 14B6DA821B276C5900BF4DD1 /* libRCTTest.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58005BEE1ABA80530062E044 /* libRCTTest.a */; }; 14D6D7111B220EB3001FB087 /* libOCMock.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14D6D7101B220EB3001FB087 /* libOCMock.a */; }; 14D6D71E1B2222EF001FB087 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */; }; @@ -106,6 +105,7 @@ 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 */; }; + 3D0E379D1F1CC77200DCAC9F /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14AADF041AC3DB95002390C9 /* libReact.a */; }; 3D13F8481D6F6AF900E69E0E /* ImageInBundle.png in Resources */ = {isa = PBXBuildFile; fileRef = 3D13F8441D6F6AF200E69E0E /* ImageInBundle.png */; }; 3D13F84A1D6F6AFD00E69E0E /* OtherImages.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3D13F8451D6F6AF200E69E0E /* OtherImages.xcassets */; }; 3D299BAF1D33EBFA00FA1057 /* RCTLoggingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D299BAE1D33EBFA00FA1057 /* RCTLoggingTests.m */; }; @@ -114,6 +114,8 @@ 3D56F9F11D6F6E9B00F53A06 /* RNTesterBundle.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 3D13F83E1D6F6AE000E69E0E /* RNTesterBundle.bundle */; }; 3DB99D0C1BA0340600302749 /* RNTesterIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DB99D0B1BA0340600302749 /* RNTesterIntegrationTests.m */; }; 3DD981D61D33C6FB007DC7BE /* RNTesterUnitTestsBundle.js in Resources */ = {isa = PBXBuildFile; fileRef = 3DD981D51D33C6FB007DC7BE /* RNTesterUnitTestsBundle.js */; }; + 52C11BBB1EEACA7100C1A058 /* libRCTBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5281CA511EEAC9A700AC40CD /* libRCTBlob.a */; }; + 52C11BE11EEACA7800C1A058 /* libRCTBlob-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5281CA531EEAC9A700AC40CD /* libRCTBlob-tvOS.a */; }; 68FF44381CF6111500720EFD /* RCTBundleURLProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68FF44371CF6111500720EFD /* RCTBundleURLProviderTests.m */; }; 834C36EC1AF8DED70019C93C /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 834C36D21AF8DA610019C93C /* libRCTSettings.a */; }; 83636F8F1B53F22C009F943E /* RCTUIManagerScenarioTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 83636F8E1B53F22C009F943E /* RCTUIManagerScenarioTests.m */; }; @@ -392,6 +394,20 @@ remoteGlobalIDString = 139D7E881E25C6D100323FB7; remoteInfo = "double-conversion"; }; + 5281CA501EEAC9A700AC40CD /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5281CA4B1EEAC9A700AC40CD /* RCTBlob.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 358F4ED71D1E81A9004DF814; + remoteInfo = RCTBlob; + }; + 5281CA521EEAC9A700AC40CD /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5281CA4B1EEAC9A700AC40CD /* RCTBlob.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = ADD01A681E09402E00F6D226; + remoteInfo = "RCTBlob-tvOS"; + }; 58005BED1ABA80530062E044 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 58005BE41ABA80530062E044 /* RCTTest.xcodeproj */; @@ -489,6 +505,7 @@ 3D2AFAF41D646CF80089D1A3 /* legacy_image@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "legacy_image@2x.png"; path = "RNTester/legacy_image@2x.png"; sourceTree = ""; }; 3DB99D0B1BA0340600302749 /* RNTesterIntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNTesterIntegrationTests.m; sourceTree = ""; }; 3DD981D51D33C6FB007DC7BE /* RNTesterUnitTestsBundle.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = RNTesterUnitTestsBundle.js; sourceTree = ""; }; + 5281CA4B1EEAC9A700AC40CD /* RCTBlob.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTBlob.xcodeproj; path = ../Libraries/Blob/RCTBlob.xcodeproj; sourceTree = ""; }; 58005BE41ABA80530062E044 /* RCTTest.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTTest.xcodeproj; path = ../Libraries/RCTTest/RCTTest.xcodeproj; sourceTree = ""; }; 68FF44371CF6111500720EFD /* RCTBundleURLProviderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBundleURLProviderTests.m; sourceTree = ""; }; 83636F8E1B53F22C009F943E /* RCTUIManagerScenarioTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIManagerScenarioTests.m; sourceTree = ""; }; @@ -526,8 +543,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3D0E379D1F1CC77200DCAC9F /* libReact.a in Frameworks */, + 52C11BBB1EEACA7100C1A058 /* libRCTBlob.a in Frameworks */, 2D66FF8F1ECA406D00F0A767 /* libART.a in Frameworks */, - 14AADF051AC3DBB1002390C9 /* libReact.a in Frameworks */, 147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */, 134454601AAFCABD003F0779 /* libRCTAdSupport.a in Frameworks */, 13E501F11D07A84A005F35D8 /* libRCTAnimation.a in Frameworks */, @@ -564,6 +582,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 52C11BE11EEACA7800C1A058 /* libRCTBlob-tvOS.a in Frameworks */, 2D66FF901ECA407E00F0A767 /* libART-tvOS.a in Frameworks */, 2DD323EA1DA2DE3F000FE1B8 /* libReact.a in Frameworks */, 2DD323E31DA2DE3F000FE1B8 /* libRCTAnimation.a in Frameworks */, @@ -598,6 +617,7 @@ 1316A21D1AA397F400C0188E /* Libraries */ = { isa = PBXGroup; children = ( + 5281CA4B1EEAC9A700AC40CD /* RCTBlob.xcodeproj */, 2D66FF5F1ECA405900F0A767 /* ART.xcodeproj */, 14AADEFF1AC3DB95002390C9 /* React.xcodeproj */, 14E0EEC81AB118F7000DECC3 /* RCTActionSheet.xcodeproj */, @@ -875,6 +895,15 @@ path = RNTester/RNTesterBundle; sourceTree = ""; }; + 5281CA4C1EEAC9A700AC40CD /* Products */ = { + isa = PBXGroup; + children = ( + 5281CA511EEAC9A700AC40CD /* libRCTBlob.a */, + 5281CA531EEAC9A700AC40CD /* libRCTBlob-tvOS.a */, + ); + name = Products; + sourceTree = ""; + }; 58005BE51ABA80530062E044 /* Products */ = { isa = PBXGroup; children = ( @@ -1117,6 +1146,10 @@ ProductGroup = 13E5019D1D07A502005F35D8 /* Products */; ProjectRef = 13E5019C1D07A502005F35D8 /* RCTAnimation.xcodeproj */; }, + { + ProductGroup = 5281CA4C1EEAC9A700AC40CD /* Products */; + ProjectRef = 5281CA4B1EEAC9A700AC40CD /* RCTBlob.xcodeproj */; + }, { ProductGroup = 138DEE031B9EDDDB007F4EA5 /* Products */; ProjectRef = 138DEE021B9EDDDB007F4EA5 /* RCTCameraRoll.xcodeproj */; @@ -1418,6 +1451,20 @@ remoteRef = 3D507F431EBC88B700B56834 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 5281CA511EEAC9A700AC40CD /* libRCTBlob.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTBlob.a; + remoteRef = 5281CA501EEAC9A700AC40CD /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 5281CA531EEAC9A700AC40CD /* libRCTBlob-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libRCTBlob-tvOS.a"; + remoteRef = 5281CA521EEAC9A700AC40CD /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 58005BEE1ABA80530062E044 /* libRCTTest.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; diff --git a/RNTester/js/WebSocketExample.js b/RNTester/js/WebSocketExample.js index 5c8e54fe2..070512a3e 100644 --- a/RNTester/js/WebSocketExample.js +++ b/RNTester/js/WebSocketExample.js @@ -6,8 +6,8 @@ * 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 * @providesModule WebSocketExample + * @format */ 'use strict'; @@ -16,23 +16,19 @@ const React = require('react'); const ReactNative = require('react-native'); const { + Image, PixelRatio, + ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, - ScrollView, View, } = ReactNative; const DEFAULT_WS_URL = 'ws://localhost:5555/'; const DEFAULT_HTTP_URL = 'http://localhost:5556/'; -const WS_EVENTS = [ - 'close', - 'error', - 'message', - 'open', -]; +const WS_EVENTS = ['close', 'error', 'message', 'open']; const WS_STATES = [ /* 0 */ 'CONNECTING', /* 1 */ 'OPEN', @@ -42,7 +38,11 @@ const WS_STATES = [ class Button extends React.Component { render(): React.Element { - const label = {this.props.label}; + const label = ( + + {this.props.label} + + ); if (this.props.disabled) { return ( @@ -51,9 +51,7 @@ class Button extends React.Component { ); } return ( - + {label} ); @@ -64,39 +62,84 @@ class Row extends React.Component { render(): React.Element { return ( - {this.props.label} - {this.props.value} + + {this.props.label} + + {this.props.value + ? + {this.props.value} + + : null} + {this.props.children} ); } } +class WebSocketImage extends React.Component { + ws: ?WebSocket = null; + state: {blob: ?Blob} = {blob: null}; + componentDidMount() { + let ws = (this.ws = new WebSocket(this.props.url)); + ws.binaryType = 'blob'; + ws.onmessage = event => { + if (event.data instanceof Blob) { + const blob = event.data; + if (this.state.blob) { + this.state.blob.close(); + } + this.setState({blob}); + } + }; + ws.onopen = event => { + ws.send('getImage'); + }; + } + componentUnmount() { + if (this.state.blob) { + this.state.blob.close(); + } + this.ws && this.ws.close(); + } + render() { + if (!this.state.blob) { + return ; + } + return ( + + ); + } +} + function showValue(value) { if (value === undefined || value === null) { return '(no value)'; } - console.log('typeof Uint8Array', typeof Uint8Array); - if (typeof ArrayBuffer !== 'undefined' && - typeof Uint8Array !== 'undefined' && - value instanceof ArrayBuffer) { + if ( + typeof ArrayBuffer !== 'undefined' && + typeof Uint8Array !== 'undefined' && + value instanceof ArrayBuffer + ) { return `ArrayBuffer {${String(Array.from(new Uint8Array(value)))}}`; } return value; } type State = { - url: string; - httpUrl: string; - fetchStatus: ?string; - socket: ?WebSocket; - socketState: ?number; - lastSocketEvent: ?string; - lastMessage: ?string | ?ArrayBuffer; - outgoingMessage: string; + url: string, + httpUrl: string, + fetchStatus: ?string, + socket: ?WebSocket, + socketState: ?number, + lastSocketEvent: ?string, + lastMessage: ?string | ?ArrayBuffer, + outgoingMessage: string, }; class WebSocketExample extends React.Component { - static title = 'WebSocket'; static description = 'WebSocket API'; @@ -127,10 +170,7 @@ class WebSocketExample extends React.Component { this.state.socket.close(); }; - // Ideally this would be a MessageEvent, but Flow's definition - // doesn't inherit from Event, so it's 'any' for now. - // See https://github.com/facebook/flow/issues/1654. - _onSocketEvent = (event: any) => { + _onSocketEvent = (event: MessageEvent) => { const state: any = { socketState: event.target.readyState, lastSocketEvent: event.type, @@ -153,7 +193,7 @@ class WebSocketExample extends React.Component { this.setState({ fetchStatus: 'fetching', }); - fetch(this.state.httpUrl).then((response) => { + fetch(this.state.httpUrl).then(response => { if (response.status >= 200 && response.status < 400) { this.setState({ fetchStatus: 'OK', @@ -163,9 +203,11 @@ class WebSocketExample extends React.Component { }; _sendBinary = () => { - if (!this.state.socket || - typeof ArrayBuffer === 'undefined' || - typeof Uint8Array === 'undefined') { + if ( + !this.state.socket || + typeof ArrayBuffer === 'undefined' || + typeof Uint8Array === 'undefined' + ) { return; } const {outgoingMessage} = this.state; @@ -180,9 +222,8 @@ class WebSocketExample extends React.Component { render(): React.Element { const socketState = WS_STATES[this.state.socketState || -1]; const canConnect = - !this.state.socket || - this.state.socket.readyState >= WebSocket.CLOSING; - const canSend = !!this.state.socket; + !this.state.socket || this.state.socket.readyState >= WebSocket.CLOSING; + const canSend = socketState === 'OPEN'; return ( @@ -191,10 +232,7 @@ class WebSocketExample extends React.Component { ./RNTester/js/websocket_test_server.js - + { label="Last message received" value={showValue(this.state.lastMessage)} /> + + {canSend ? : null} + this.setState({url})} + onChangeText={url => this.setState({url})} value={this.state.url} /> @@ -226,7 +267,7 @@ class WebSocketExample extends React.Component { style={styles.textInput} autoCorrect={false} placeholder="Type message here..." - onChangeText={(outgoingMessage) => this.setState({outgoingMessage})} + onChangeText={outgoingMessage => this.setState({outgoingMessage})} value={this.state.outgoingMessage} /> @@ -244,14 +285,14 @@ class WebSocketExample extends React.Component { To start the HTTP test server: - ./RNTester/http_test_server.js + ./RNTester/js/http_test_server.js this.setState({httpUrl})} + onChangeText={httpUrl => this.setState({httpUrl})} value={this.state.httpUrl} /> @@ -263,13 +304,14 @@ class WebSocketExample extends React.Component { - {this.state.fetchStatus === 'OK' ? 'Done. Check your WS server console to see if the next WS requests include the cookie (should be "wstest=OK")' : '-'} + {this.state.fetchStatus === 'OK' + ? 'Done. Check your WS server console to see if the next WS requests include the cookie (should be "wstest=OK")' + : '-'} ); } - } const styles = StyleSheet.create({ diff --git a/RNTester/js/http_test_server.js b/RNTester/js/http_test_server.js old mode 100644 new mode 100755 diff --git a/RNTester/js/websocket_test_server.js b/RNTester/js/websocket_test_server.js old mode 100644 new mode 100755 index 21900cdde..6a753b27b --- a/RNTester/js/websocket_test_server.js +++ b/RNTester/js/websocket_test_server.js @@ -16,6 +16,9 @@ const WebSocket = require('ws'); +const fs = require('fs'); +const path = require('path'); + console.log(`\ Test server for WebSocketExample @@ -33,6 +36,10 @@ server.on('connection', (ws) => { if (respondWithBinary) { message = new Buffer(message); } + if (message === 'getImage') { + message = fs.readFileSync(path.resolve(__dirname, 'flux@3x.png')); + } + console.log('Replying with:', message); ws.send(message); }); diff --git a/React.podspec b/React.podspec index 68389ddca..e37185772 100644 --- a/React.podspec +++ b/React.podspec @@ -148,9 +148,15 @@ Pod::Spec.new do |s| ss.header_dir = "RCTAnimation" end + s.subspec "RCTBlob" do |ss| + ss.dependency "React/Core" + ss.source_files = "Libraries/Blob/*.{h,m}" + ss.preserve_paths = "Libraries/Blob/*.js" + end + s.subspec "RCTCameraRoll" do |ss| ss.dependency "React/Core" - ss.dependency "React/RCTImage" + ss.dependency 'React/RCTImage' ss.source_files = "Libraries/CameraRoll/*.{h,m}" end @@ -192,6 +198,7 @@ Pod::Spec.new do |s| s.subspec "RCTWebSocket" do |ss| ss.dependency "React/Core" + ss.dependency "React/RCTBlob" ss.source_files = "Libraries/WebSocket/*.{h,m}" end diff --git a/React/DevSupport/RCTPackagerConnection.m b/React/DevSupport/RCTPackagerConnection.m index 177014044..66de385a2 100644 --- a/React/DevSupport/RCTPackagerConnection.m +++ b/React/DevSupport/RCTPackagerConnection.m @@ -19,7 +19,7 @@ #import #import #import -#import +#import #import "RCTPackagerConnectionBridgeConfig.h" #import "RCTReloadPackagerMethod.h" diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 6a94c8d54..4f15f596e 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -352,6 +352,12 @@ 3D0B842C1EC0B4EA00B2BD8E /* RCTTVView.m in Sources */ = {isa = PBXBuildFile; fileRef = 130443D71E401AD800D93A67 /* RCTTVView.m */; }; 3D0B842F1EC0B51200B2BD8E /* RCTTVNavigationEventEmitter.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D0B842D1EC0B51200B2BD8E /* RCTTVNavigationEventEmitter.h */; }; 3D0B84301EC0B51200B2BD8E /* RCTTVNavigationEventEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D0B842E1EC0B51200B2BD8E /* RCTTVNavigationEventEmitter.m */; }; + 3D0E378A1F1CC40000DCAC9F /* RCTWebSocketModule.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D0E37891F1CC40000DCAC9F /* RCTWebSocketModule.h */; }; + 3D0E378C1F1CC58C00DCAC9F /* RCTWebSocketObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D0E378B1F1CC58C00DCAC9F /* RCTWebSocketObserver.h */; }; + 3D0E378D1F1CC58F00DCAC9F /* RCTWebSocketObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D0E378B1F1CC58C00DCAC9F /* RCTWebSocketObserver.h */; }; + 3D0E378E1F1CC59100DCAC9F /* RCTWebSocketModule.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D0E37891F1CC40000DCAC9F /* RCTWebSocketModule.h */; }; + 3D0E378F1F1CC5CF00DCAC9F /* RCTWebSocketModule.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 3D0E37891F1CC40000DCAC9F /* RCTWebSocketModule.h */; }; + 3D0E37901F1CC5E100DCAC9F /* RCTWebSocketModule.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 3D0E37891F1CC40000DCAC9F /* RCTWebSocketModule.h */; }; 3D1E68DB1CABD13900DD7465 /* RCTDisplayLink.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D1E68D91CABD13900DD7465 /* RCTDisplayLink.m */; }; 3D302F1E1DF8265A00D6DDAE /* JavaScriptCore.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 3D7A27DC1DE32541002E3F95 /* JavaScriptCore.h */; }; 3D302F1F1DF8265A00D6DDAE /* JSCWrapper.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 3D7A27DE1DE32541002E3F95 /* JSCWrapper.h */; }; @@ -399,7 +405,6 @@ 3D302F4D1DF828F800D6DDAE /* RCTURLRequestDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 1345A83A1B265A0E00583190 /* RCTURLRequestDelegate.h */; }; 3D302F4E1DF828F800D6DDAE /* RCTURLRequestHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 1345A83B1B265A0E00583190 /* RCTURLRequestHandler.h */; }; 3D302F4F1DF828F800D6DDAE /* RCTUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 83CBBA4F1A601E3B00E9B192 /* RCTUtils.h */; }; - 3D302F501DF828F800D6DDAE /* RCTWebSocketObserverProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DB910701C74B21600838BBE /* RCTWebSocketObserverProtocol.h */; }; 3D302F541DF828F800D6DDAE /* RCTJSCSamplingProfiler.h in Headers */ = {isa = PBXBuildFile; fileRef = 369123DF1DDC75850095B341 /* RCTJSCSamplingProfiler.h */; }; 3D302F551DF828F800D6DDAE /* RCTAccessibilityManager.h in Headers */ = {isa = PBXBuildFile; fileRef = E9B20B791B500126007A2DA7 /* RCTAccessibilityManager.h */; }; 3D302F561DF828F800D6DDAE /* RCTAlertManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 13B07FE71A69327A00A75B9A /* RCTAlertManager.h */; }; @@ -604,7 +609,6 @@ 3D80D9481DF6FA890028D040 /* RCTURLRequestDelegate.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 1345A83A1B265A0E00583190 /* RCTURLRequestDelegate.h */; }; 3D80D9491DF6FA890028D040 /* RCTURLRequestHandler.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 1345A83B1B265A0E00583190 /* RCTURLRequestHandler.h */; }; 3D80D94A1DF6FA890028D040 /* RCTUtils.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 83CBBA4F1A601E3B00E9B192 /* RCTUtils.h */; }; - 3D80D94B1DF6FA890028D040 /* RCTWebSocketObserverProtocol.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 3DB910701C74B21600838BBE /* RCTWebSocketObserverProtocol.h */; }; 3D80D94F1DF6FA890028D040 /* RCTJSCSamplingProfiler.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 369123DF1DDC75850095B341 /* RCTJSCSamplingProfiler.h */; }; 3D80D9501DF6FA890028D040 /* RCTAccessibilityManager.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = E9B20B791B500126007A2DA7 /* RCTAccessibilityManager.h */; }; 3D80D9511DF6FA890028D040 /* RCTAlertManager.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 13B07FE71A69327A00A75B9A /* RCTAlertManager.h */; }; @@ -718,7 +722,6 @@ 3D80DA421DF820620028D040 /* RCTURLRequestDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 1345A83A1B265A0E00583190 /* RCTURLRequestDelegate.h */; }; 3D80DA431DF820620028D040 /* RCTURLRequestHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 1345A83B1B265A0E00583190 /* RCTURLRequestHandler.h */; }; 3D80DA441DF820620028D040 /* RCTUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 83CBBA4F1A601E3B00E9B192 /* RCTUtils.h */; }; - 3D80DA451DF820620028D040 /* RCTWebSocketObserverProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DB910701C74B21600838BBE /* RCTWebSocketObserverProtocol.h */; }; 3D80DA491DF820620028D040 /* RCTJSCSamplingProfiler.h in Headers */ = {isa = PBXBuildFile; fileRef = 369123DF1DDC75850095B341 /* RCTJSCSamplingProfiler.h */; }; 3D80DA4A1DF820620028D040 /* RCTAccessibilityManager.h in Headers */ = {isa = PBXBuildFile; fileRef = E9B20B791B500126007A2DA7 /* RCTAccessibilityManager.h */; }; 3D80DA4B1DF820620028D040 /* RCTAlertManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 13B07FE71A69327A00A75B9A /* RCTAlertManager.h */; }; @@ -856,7 +859,6 @@ 3DA981E31E5B0F29004F2374 /* RCTURLRequestDelegate.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 1345A83A1B265A0E00583190 /* RCTURLRequestDelegate.h */; }; 3DA981E41E5B0F29004F2374 /* RCTURLRequestHandler.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 1345A83B1B265A0E00583190 /* RCTURLRequestHandler.h */; }; 3DA981E51E5B0F29004F2374 /* RCTUtils.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 83CBBA4F1A601E3B00E9B192 /* RCTUtils.h */; }; - 3DA981E61E5B0F29004F2374 /* RCTWebSocketObserverProtocol.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 3DB910701C74B21600838BBE /* RCTWebSocketObserverProtocol.h */; }; 3DA981E91E5B0F7F004F2374 /* RCTJSCSamplingProfiler.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 369123DF1DDC75850095B341 /* RCTJSCSamplingProfiler.h */; }; 3DA981EA1E5B0F7F004F2374 /* RCTAccessibilityManager.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = E9B20B791B500126007A2DA7 /* RCTAccessibilityManager.h */; }; 3DA981EB1E5B0F7F004F2374 /* RCTAlertManager.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 13B07FE71A69327A00A75B9A /* RCTAlertManager.h */; }; @@ -1191,6 +1193,7 @@ dstPath = include/React; dstSubfolderSpec = 16; files = ( + 3D0E37901F1CC5E100DCAC9F /* RCTWebSocketModule.h in Copy Headers */, 5960C1BF1F0804F50066FD5B /* RCTLayoutAnimation.h in Copy Headers */, 5960C1C01F0804F50066FD5B /* RCTLayoutAnimationGroup.h in Copy Headers */, C6827DFC1EF1801B00D66BEF /* RCTJSEnvironment.h in Copy Headers */, @@ -1314,7 +1317,6 @@ 3DA981E31E5B0F29004F2374 /* RCTURLRequestDelegate.h in Copy Headers */, 3DA981E41E5B0F29004F2374 /* RCTURLRequestHandler.h in Copy Headers */, 3DA981E51E5B0F29004F2374 /* RCTUtils.h in Copy Headers */, - 3DA981E61E5B0F29004F2374 /* RCTWebSocketObserverProtocol.h in Copy Headers */, ); name = "Copy Headers"; runOnlyForDeploymentPostprocessing = 0; @@ -1412,6 +1414,7 @@ dstPath = include/React; dstSubfolderSpec = 16; files = ( + 3D0E378F1F1CC5CF00DCAC9F /* RCTWebSocketModule.h in Copy Headers */, 5960C1BD1F0804DF0066FD5B /* RCTLayoutAnimation.h in Copy Headers */, 5960C1BE1F0804DF0066FD5B /* RCTLayoutAnimationGroup.h in Copy Headers */, C6827DFB1EF1800E00D66BEF /* RCTJSEnvironment.h in Copy Headers */, @@ -1462,7 +1465,6 @@ 3D80D9481DF6FA890028D040 /* RCTURLRequestDelegate.h in Copy Headers */, 3D80D9491DF6FA890028D040 /* RCTURLRequestHandler.h in Copy Headers */, 3D80D94A1DF6FA890028D040 /* RCTUtils.h in Copy Headers */, - 3D80D94B1DF6FA890028D040 /* RCTWebSocketObserverProtocol.h in Copy Headers */, 3D80D94F1DF6FA890028D040 /* RCTJSCSamplingProfiler.h in Copy Headers */, 3D80D9501DF6FA890028D040 /* RCTAccessibilityManager.h in Copy Headers */, 3D80D9511DF6FA890028D040 /* RCTAlertManager.h in Copy Headers */, @@ -1846,6 +1848,8 @@ 3D0B84291EC0B49400B2BD8E /* RCTTVRemoteHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTVRemoteHandler.m; sourceTree = ""; }; 3D0B842D1EC0B51200B2BD8E /* RCTTVNavigationEventEmitter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTVNavigationEventEmitter.h; sourceTree = ""; }; 3D0B842E1EC0B51200B2BD8E /* RCTTVNavigationEventEmitter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTVNavigationEventEmitter.m; sourceTree = ""; }; + 3D0E37891F1CC40000DCAC9F /* RCTWebSocketModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTWebSocketModule.h; path = WebSocket/RCTWebSocketModule.h; sourceTree = ""; }; + 3D0E378B1F1CC58C00DCAC9F /* RCTWebSocketObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTWebSocketObserver.h; path = WebSocket/RCTWebSocketObserver.h; sourceTree = ""; }; 3D1E68D81CABD13900DD7465 /* RCTDisplayLink.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDisplayLink.h; sourceTree = ""; }; 3D1E68D91CABD13900DD7465 /* RCTDisplayLink.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDisplayLink.m; sourceTree = ""; }; 3D1FA07A1DE4F2EA00E03CC6 /* RCTNetworking.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTNetworking.h; sourceTree = ""; }; @@ -1931,7 +1935,6 @@ 3D92B10B1E0369AD0018521A /* Unicode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Unicode.h; sourceTree = ""; }; 3D92B10C1E0369AD0018521A /* Value.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Value.cpp; sourceTree = ""; }; 3D92B10D1E0369AD0018521A /* Value.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Value.h; sourceTree = ""; }; - 3DB910701C74B21600838BBE /* RCTWebSocketObserverProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebSocketObserverProtocol.h; sourceTree = ""; }; 3DCC92BA1E94458B00EF89A8 /* YGEnums.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = YGEnums.c; sourceTree = ""; }; 3DF1BE801F26576400068F1A /* JSCTracing.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = JSCTracing.cpp; sourceTree = ""; }; 3DF1BE811F26576400068F1A /* JSCTracing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSCTracing.h; sourceTree = ""; }; @@ -2478,6 +2481,8 @@ 3D7BFD2A1EA8E3D3008DFB7A /* WebSocket */ = { isa = PBXGroup; children = ( + 3D0E378B1F1CC58C00DCAC9F /* RCTWebSocketObserver.h */, + 3D0E37891F1CC40000DCAC9F /* RCTWebSocketModule.h */, 3D7BFD2B1EA8E3FA008DFB7A /* RCTReconnectingWebSocket.h */, 3D7BFD2C1EA8E3FA008DFB7A /* RCTSRWebSocket.h */, ); @@ -2628,7 +2633,6 @@ 1345A83B1B265A0E00583190 /* RCTURLRequestHandler.h */, 83CBBA4F1A601E3B00E9B192 /* RCTUtils.h */, 83CBBA501A601E3B00E9B192 /* RCTUtils.m */, - 3DB910701C74B21600838BBE /* RCTWebSocketObserverProtocol.h */, ); path = Base; sourceTree = ""; @@ -2716,6 +2720,7 @@ 3D7AA9C51E548CDB001955CF /* NSDataBigString.h in Headers */, 5960C1BA1F0804A00066FD5B /* RCTLayoutAnimationGroup.h in Headers */, 13134C991E296B2A00B9F3CB /* RCTCxxMethod.h in Headers */, + 3D0E378D1F1CC58F00DCAC9F /* RCTWebSocketObserver.h in Headers */, 3D302F471DF828F800D6DDAE /* RCTPlatform.h in Headers */, 13134C951E296B2A00B9F3CB /* RCTObjcExecutor.h in Headers */, 590D7BFE1EBD458B00D8A370 /* RCTShadowView+Layout.h in Headers */, @@ -2741,6 +2746,7 @@ 3D302F2E1DF828F800D6DDAE /* RCTBridgeDelegate.h in Headers */, 3D302F2F1DF828F800D6DDAE /* RCTBridgeMethod.h in Headers */, 130E3D8A1E6A083600ACE484 /* RCTDevSettings.h in Headers */, + 3D0E378E1F1CC59100DCAC9F /* RCTWebSocketModule.h in Headers */, 3D302F301DF828F800D6DDAE /* RCTBridgeModule.h in Headers */, 3D302F311DF828F800D6DDAE /* RCTBundleURLProvider.h in Headers */, 3D302F321DF828F800D6DDAE /* RCTConvert.h in Headers */, @@ -2777,7 +2783,6 @@ 3D302F4D1DF828F800D6DDAE /* RCTURLRequestDelegate.h in Headers */, 3D302F4E1DF828F800D6DDAE /* RCTURLRequestHandler.h in Headers */, 3D302F4F1DF828F800D6DDAE /* RCTUtils.h in Headers */, - 3D302F501DF828F800D6DDAE /* RCTWebSocketObserverProtocol.h in Headers */, 3D302F541DF828F800D6DDAE /* RCTJSCSamplingProfiler.h in Headers */, 3D302F551DF828F800D6DDAE /* RCTAccessibilityManager.h in Headers */, 3D302F561DF828F800D6DDAE /* RCTAlertManager.h in Headers */, @@ -3060,12 +3065,12 @@ 3D80DA421DF820620028D040 /* RCTURLRequestDelegate.h in Headers */, 3D80DA431DF820620028D040 /* RCTURLRequestHandler.h in Headers */, 3D80DA441DF820620028D040 /* RCTUtils.h in Headers */, - 3D80DA451DF820620028D040 /* RCTWebSocketObserverProtocol.h in Headers */, 13134C981E296B2A00B9F3CB /* RCTCxxMethod.h in Headers */, 3D80DA491DF820620028D040 /* RCTJSCSamplingProfiler.h in Headers */, 3D80DA4A1DF820620028D040 /* RCTAccessibilityManager.h in Headers */, 3D80DA4B1DF820620028D040 /* RCTAlertManager.h in Headers */, 3D80DA4C1DF820620028D040 /* RCTAppState.h in Headers */, + 3D0E378C1F1CC58C00DCAC9F /* RCTWebSocketObserver.h in Headers */, 3D80DA4D1DF820620028D040 /* RCTAsyncLocalStorage.h in Headers */, 3D80DA4E1DF820620028D040 /* RCTClipboard.h in Headers */, 3D80DA4F1DF820620028D040 /* RCTDevLoadingView.h in Headers */, @@ -3092,6 +3097,7 @@ C6194AB01EF156280034D062 /* RCTPackagerConnectionConfig.h in Headers */, CF2731C01E7B8DE40044CA4F /* RCTDeviceInfo.h in Headers */, 3D80DA611DF820620028D040 /* RCTAnimationType.h in Headers */, + 3D0E378A1F1CC40000DCAC9F /* RCTWebSocketModule.h in Headers */, 3D80DA621DF820620028D040 /* RCTAutoInsetsProtocol.h in Headers */, 3D80DA631DF820620028D040 /* RCTBorderDrawing.h in Headers */, 3D80DA641DF820620028D040 /* RCTBorderStyle.h in Headers */, diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BUCK new file mode 100644 index 000000000..936f5e571 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BUCK @@ -0,0 +1,22 @@ +include_defs("//ReactAndroid/DEFS") + +android_library( + name = "blob", + srcs = glob(["**/*.java"]), + visibility = [ + "PUBLIC", + ], + deps = [ + react_native_dep("libraries/fbcore/src/main/java/com/facebook/common/logging:logging"), + react_native_dep("third-party/android/support-annotations:android-support-annotations"), + react_native_dep("third-party/android/support/v4:lib-support-v4"), + 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/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/websocket:websocket"), + ], +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobModule.java new file mode 100644 index 000000000..e213bb749 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobModule.java @@ -0,0 +1,180 @@ +/** + * 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.content.res.Resources; +import android.net.Uri; +import android.support.annotation.Nullable; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; +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.websocket.WebSocketModule; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import okio.ByteString; + +@ReactModule(name = BlobModule.NAME) +public class BlobModule extends ReactContextBaseJavaModule { + + protected static final String NAME = "BlobModule"; + + private final Map mBlobs = new HashMap<>(); + + protected final WebSocketModule.ContentHandler mContentHandler = + 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"); + } + }; + + public BlobModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return NAME; + } + + @Override + @Nullable + public Map 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(); + String packageName = getReactApplicationContext().getPackageName(); + int resourceId = resources.getIdentifier("blob_provider_authority", "string", packageName); + if (resourceId == 0) { + return null; + } + + return MapBuilder.of( + "BLOB_URI_SCHEME", "content", "BLOB_URI_HOST", resources.getString(resourceId)); + } + + public String store(byte[] data) { + String blobId = UUID.randomUUID().toString(); + store(data, blobId); + return blobId; + } + + public void store(byte[] data, String blobId) { + mBlobs.put(blobId, data); + } + + public void remove(String blobId) { + mBlobs.remove(blobId); + } + + @Nullable + public byte[] resolve(Uri uri) { + String blobId = uri.getLastPathSegment(); + int offset = 0; + int size = -1; + String offsetParam = uri.getQueryParameter("offset"); + if (offsetParam != null) { + offset = Integer.parseInt(offsetParam, 10); + } + String sizeParam = uri.getQueryParameter("size"); + if (sizeParam != null) { + size = Integer.parseInt(sizeParam, 10); + } + return resolve(blobId, offset, size); + } + + @Nullable + public byte[] resolve(String blobId, int offset, int size) { + byte[] data = mBlobs.get(blobId); + if (data == null) { + return null; + } + if (size == -1) { + size = data.length - offset; + } + if (offset > 0) { + data = Arrays.copyOfRange(data, offset, offset + size); + } + return data; + } + + @Nullable + public byte[] resolve(ReadableMap blob) { + return resolve(blob.getString("blobId"), blob.getInt("offset"), blob.getInt("size")); + } + + private WebSocketModule getWebSocketModule() { + return getReactApplicationContext().getNativeModule(WebSocketModule.class); + } + + @ReactMethod + public void enableBlobSupport(final int id) { + getWebSocketModule().setContentHandler(id, mContentHandler); + } + + @ReactMethod + public void disableBlobSupport(final int id) { + getWebSocketModule().setContentHandler(id, null); + } + + @ReactMethod + public void sendBlob(ReadableMap blob, int id) { + byte[] data = resolve(blob.getString("blobId"), blob.getInt("offset"), blob.getInt("size")); + + if (data != null) { + getWebSocketModule().sendBinary(ByteString.of(data), id); + } else { + getWebSocketModule().sendBinary((ByteString) null, id); + } + } + + @ReactMethod + public void createFromParts(ReadableArray parts, String blobId) { + int totalBlobSize = 0; + ArrayList 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); + } + ByteBuffer buffer = ByteBuffer.allocate(totalBlobSize); + for (ReadableMap part : partList) { + buffer.put(resolve(part)); + } + store(buffer.array(), blobId); + } + + @ReactMethod + public void release(String blobId) { + remove(blobId); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobProvider.java b/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobProvider.java new file mode 100644 index 000000000..2404f2dba --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobProvider.java @@ -0,0 +1,99 @@ +/** + * 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.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.support.annotation.Nullable; +import com.facebook.react.ReactApplication; +import com.facebook.react.ReactNativeHost; +import com.facebook.react.bridge.ReactContext; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; + +public final class BlobProvider extends ContentProvider { + + @Override + public boolean onCreate() { + return true; + } + + @Override + public @Nullable Cursor query( + Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + return null; + } + + @Override + public @Nullable String getType(Uri uri) { + return null; + } + + @Override + public @Nullable Uri insert(Uri uri, ContentValues values) { + return null; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + return 0; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + return 0; + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + if (!mode.equals("r")) { + throw new FileNotFoundException("Cannot open " + uri.toString() + " in mode '" + mode + "'"); + } + + BlobModule blobModule = null; + Context context = getContext().getApplicationContext(); + if (context instanceof ReactApplication) { + ReactNativeHost host = ((ReactApplication) context).getReactNativeHost(); + ReactContext reactContext = host.getReactInstanceManager().getCurrentReactContext(); + blobModule = reactContext.getNativeModule(BlobModule.class); + } + + if (blobModule == null) { + throw new RuntimeException("No blob module associated with BlobProvider"); + } + + byte[] data = blobModule.resolve(uri); + if (data == null) { + throw new FileNotFoundException("Cannot open " + uri.toString() + ", blob not found."); + } + + ParcelFileDescriptor[] pipe; + try { + pipe = ParcelFileDescriptor.createPipe(); + } catch (IOException exception) { + return null; + } + ParcelFileDescriptor readSide = pipe[0]; + ParcelFileDescriptor writeSide = pipe[1]; + + OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(writeSide); + try { + outputStream.write(data); + outputStream.close(); + } catch (IOException exception) { + return null; + } + + return readSide; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java index 8f95b674f..17df44611 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java @@ -9,16 +9,6 @@ package com.facebook.react.modules.websocket; -import javax.annotation.Nullable; - -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - import com.facebook.common.logging.FLog; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactApplicationContext; @@ -34,7 +24,14 @@ import com.facebook.react.common.ReactConstants; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.modules.network.ForwardingCookieHandler; - +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; @@ -43,9 +40,16 @@ import okhttp3.WebSocketListener; import okio.ByteString; @ReactModule(name = "WebSocketModule", hasConstants = false) -public class WebSocketModule extends ReactContextBaseJavaModule { +public final class WebSocketModule extends ReactContextBaseJavaModule { + + public interface ContentHandler { + void onMessage(String text, WritableMap params); + + void onMessage(ByteString byteString, WritableMap params); + } private final Map mWebSocketConnections = new HashMap<>(); + private final Map mContentHandlers = new HashMap<>(); private ReactContext mReactContext; private ForwardingCookieHandler mCookieHandler; @@ -67,6 +71,14 @@ public class WebSocketModule extends ReactContextBaseJavaModule { return "WebSocketModule"; } + public void setContentHandler(final int id, final ContentHandler contentHandler) { + if (contentHandler != null) { + mContentHandlers.put(id, contentHandler); + } else { + mContentHandlers.remove(id); + } + } + @ReactMethod public void connect( final String url, @@ -79,9 +91,7 @@ public class WebSocketModule extends ReactContextBaseJavaModule { .readTimeout(0, TimeUnit.MINUTES) // Disable timeouts for read .build(); - Request.Builder builder = new Request.Builder() - .tag(id) - .url(url); + Request.Builder builder = new Request.Builder().tag(id).url(url); String cookie = getCookie(url); if (cookie != null) { @@ -124,49 +134,65 @@ public class WebSocketModule extends ReactContextBaseJavaModule { } } - client.newWebSocket(builder.build(), new WebSocketListener() { + client.newWebSocket( + builder.build(), + new WebSocketListener() { - @Override - public void onOpen(WebSocket webSocket, Response response) { - mWebSocketConnections.put(id, webSocket); - WritableMap params = Arguments.createMap(); - params.putInt("id", id); - sendEvent("websocketOpen", params); - } + @Override + public void onOpen(WebSocket webSocket, Response response) { + mWebSocketConnections.put(id, webSocket); + WritableMap params = Arguments.createMap(); + params.putInt("id", id); + sendEvent("websocketOpen", params); + } - @Override - public void onClosed(WebSocket webSocket, int code, String reason) { - WritableMap params = Arguments.createMap(); - params.putInt("id", id); - params.putInt("code", code); - params.putString("reason", reason); - sendEvent("websocketClosed", params); - } + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + WritableMap params = Arguments.createMap(); + params.putInt("id", id); + params.putInt("code", code); + params.putString("reason", reason); + sendEvent("websocketClosed", params); + } - @Override - public void onFailure(WebSocket webSocket, Throwable t, Response response) { - notifyWebSocketFailed(id, t.getMessage()); - } + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + notifyWebSocketFailed(id, t.getMessage()); + } - @Override - public void onMessage(WebSocket webSocket, String text) { - WritableMap params = Arguments.createMap(); - params.putInt("id", id); - params.putString("data", text); - params.putString("type", "text"); - sendEvent("websocketMessage", params); - } + @Override + public void onMessage(WebSocket webSocket, String text) { + WritableMap params = Arguments.createMap(); + params.putInt("id", id); + params.putString("type", "text"); - @Override - public void onMessage(WebSocket webSocket, ByteString bytes) { - String text = bytes.base64(); - WritableMap params = Arguments.createMap(); - params.putInt("id", id); - params.putString("data", text); - params.putString("type", "binary"); - sendEvent("websocketMessage", params); - } - }); + ContentHandler contentHandler = mContentHandlers.get(id); + if (contentHandler != null) { + contentHandler.onMessage(text, params); + } else { + params.putString("data", text); + } + sendEvent("websocketMessage", params); + } + + @Override + public void onMessage(WebSocket webSocket, ByteString bytes) { + WritableMap params = Arguments.createMap(); + params.putInt("id", id); + params.putString("type", "binary"); + + ContentHandler contentHandler = mContentHandlers.get(id); + if (contentHandler != null) { + contentHandler.onMessage(bytes, params); + } else { + String text = bytes.base64(); + + params.putString("data", text); + } + + sendEvent("websocketMessage", params); + } + }); // Trigger shutdown of the dispatcher's executor so this process can exit cleanly client.dispatcher().executorService().shutdown(); @@ -183,6 +209,7 @@ public class WebSocketModule extends ReactContextBaseJavaModule { try { client.close(code, reason); mWebSocketConnections.remove(id); + mContentHandlers.remove(id); } catch (Exception e) { FLog.e( ReactConstants.TAG, @@ -219,6 +246,19 @@ public class WebSocketModule extends ReactContextBaseJavaModule { } } + public void sendBinary(ByteString byteString, int id) { + WebSocket client = mWebSocketConnections.get(id); + if (client == null) { + // This is a programmer error + throw new RuntimeException("Cannot send a message. Unknown WebSocket id " + id); + } + try { + client.send(byteString); + } catch (Exception e) { + notifyWebSocketFailed(id, e.getMessage()); + } + } + @ReactMethod public void ping(int id) { WebSocket client = mWebSocketConnections.get(id); @@ -243,10 +283,9 @@ public class WebSocketModule extends ReactContextBaseJavaModule { /** * Get the default HTTP(S) origin for a specific WebSocket URI * - * @param String uri + * @param uri * @return A string of the endpoint converted to HTTP protocol (http[s]://host[:port]) */ - private static String getDefaultOrigin(String uri) { try { String defaultOrigin; @@ -280,7 +319,7 @@ public class WebSocketModule extends ReactContextBaseJavaModule { /** * Get the cookie for a specific domain * - * @param String uri + * @param uri * @return The cookie header or null if none is set */ private String getCookie(String uri) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK index d6688926c..fdd003be9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK @@ -23,6 +23,7 @@ android_library( react_native_target("java/com/facebook/react/module/model:model"), react_native_target("java/com/facebook/react/modules/accessibilityinfo:accessibilityinfo"), react_native_target("java/com/facebook/react/modules/appstate:appstate"), + 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/core:core"), diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index 1d2e839ec..c4a32965d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -13,7 +13,6 @@ import android.content.SharedPreferences; import android.preference.PreferenceManager; import com.facebook.react.LazyReactPackage; import com.facebook.react.animated.NativeAnimatedModule; -import com.facebook.react.bridge.JavaScriptModule; import com.facebook.react.bridge.ModuleSpec; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; @@ -30,6 +29,7 @@ import com.facebook.react.flat.RCTVirtualTextManager; 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.camera.CameraRollManager; import com.facebook.react.modules.camera.ImageEditingManager; import com.facebook.react.modules.camera.ImageStoreManager; @@ -74,12 +74,10 @@ import com.facebook.react.views.toolbar.ReactToolbarManager; import com.facebook.react.views.view.ReactViewManager; import com.facebook.react.views.viewpager.ReactViewPagerManager; import com.facebook.react.views.webview.ReactWebViewManager; - -import javax.inject.Provider; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; +import javax.inject.Provider; /** * Package defining basic modules and view managers. @@ -101,150 +99,207 @@ public class MainReactPackage extends LazyReactPackage { @Override public List getNativeModules(final ReactApplicationContext context) { return Arrays.asList( - new ModuleSpec(AccessibilityInfoModule.class, new Provider() { - @Override - public NativeModule get() { - return new AccessibilityInfoModule(context); - } - }), - new ModuleSpec(AppStateModule.class, new Provider() { - @Override - public NativeModule get() { - return new AppStateModule(context); - } - }), - new ModuleSpec(AsyncStorageModule.class, new Provider() { - @Override - public NativeModule get() { - return new AsyncStorageModule(context); - } - }), - new ModuleSpec(CameraRollManager.class, new Provider() { - @Override - public NativeModule get() { - return new CameraRollManager(context); - } - }), - new ModuleSpec(ClipboardModule.class, new Provider() { - @Override - public NativeModule get() { - return new ClipboardModule(context); - } - }), - new ModuleSpec(DatePickerDialogModule.class, new Provider() { - @Override - public NativeModule get() { - return new DatePickerDialogModule(context); - } - }), - new ModuleSpec(DialogModule.class, new Provider() { - @Override - public NativeModule get() { - return new DialogModule(context); - } - }), - new ModuleSpec(FrescoModule.class, new Provider() { - @Override - public NativeModule get() { - return new FrescoModule(context, true, mConfig != null ? mConfig.getFrescoConfig() : null); - } - }), - new ModuleSpec(I18nManagerModule.class, new Provider() { - @Override - public NativeModule get() { - return new I18nManagerModule(context); - } - }), - new ModuleSpec(ImageEditingManager.class, new Provider() { - @Override - public NativeModule get() { - return new ImageEditingManager(context); - } - }), - new ModuleSpec(ImageLoaderModule.class, new Provider() { - @Override - public NativeModule get() { - return new ImageLoaderModule(context); - } - }), - new ModuleSpec(ImageStoreManager.class, new Provider() { - @Override - public NativeModule get() { - return new ImageStoreManager(context); - } - }), - new ModuleSpec(IntentModule.class, new Provider() { - @Override - public NativeModule get() { - return new IntentModule(context); - } - }), - new ModuleSpec(LocationModule.class, new Provider() { - @Override - public NativeModule get() { - return new LocationModule(context); - } - }), - new ModuleSpec(NativeAnimatedModule.class, new Provider() { - @Override - public NativeModule get() { - return new NativeAnimatedModule(context); - } - }), - new ModuleSpec(NetworkingModule.class, new Provider() { - @Override - public NativeModule get() { - return new NetworkingModule(context); - } - }), - new ModuleSpec(NetInfoModule.class, new Provider() { - @Override - public NativeModule get() { - return new NetInfoModule(context); - } - }), - new ModuleSpec(PermissionsModule.class, new Provider() { - @Override - public NativeModule get() { - return new PermissionsModule(context); - } - }), - new ModuleSpec(ShareModule.class, new Provider() { - @Override - public NativeModule get() { - return new ShareModule(context); - } - }), - new ModuleSpec(StatusBarModule.class, new Provider() { - @Override - public NativeModule get() { - return new StatusBarModule(context); - } - }), - new ModuleSpec(TimePickerDialogModule.class, new Provider() { - @Override - public NativeModule get() { - return new TimePickerDialogModule(context); - } - }), - new ModuleSpec(ToastModule.class, new Provider() { - @Override - public NativeModule get() { - return new ToastModule(context); - } - }), - new ModuleSpec(VibrationModule.class, new Provider() { - @Override - public NativeModule get() { - return new VibrationModule(context); - } - }), - new ModuleSpec(WebSocketModule.class, new Provider() { - @Override - public NativeModule get() { - return new WebSocketModule(context); - } - })); + new ModuleSpec( + AccessibilityInfoModule.class, + new Provider() { + @Override + public NativeModule get() { + return new AccessibilityInfoModule(context); + } + }), + new ModuleSpec( + AppStateModule.class, + new Provider() { + @Override + public NativeModule get() { + return new AppStateModule(context); + } + }), + new ModuleSpec( + BlobModule.class, + new Provider() { + @Override + public NativeModule get() { + return new BlobModule(context); + } + }), + new ModuleSpec( + AsyncStorageModule.class, + new Provider() { + @Override + public NativeModule get() { + return new AsyncStorageModule(context); + } + }), + new ModuleSpec( + CameraRollManager.class, + new Provider() { + @Override + public NativeModule get() { + return new CameraRollManager(context); + } + }), + new ModuleSpec( + ClipboardModule.class, + new Provider() { + @Override + public NativeModule get() { + return new ClipboardModule(context); + } + }), + new ModuleSpec( + DatePickerDialogModule.class, + new Provider() { + @Override + public NativeModule get() { + return new DatePickerDialogModule(context); + } + }), + new ModuleSpec( + DialogModule.class, + new Provider() { + @Override + public NativeModule get() { + return new DialogModule(context); + } + }), + new ModuleSpec( + FrescoModule.class, + new Provider() { + @Override + public NativeModule get() { + return new FrescoModule( + context, true, mConfig != null ? mConfig.getFrescoConfig() : null); + } + }), + new ModuleSpec( + I18nManagerModule.class, + new Provider() { + @Override + public NativeModule get() { + return new I18nManagerModule(context); + } + }), + new ModuleSpec( + ImageEditingManager.class, + new Provider() { + @Override + public NativeModule get() { + return new ImageEditingManager(context); + } + }), + new ModuleSpec( + ImageLoaderModule.class, + new Provider() { + @Override + public NativeModule get() { + return new ImageLoaderModule(context); + } + }), + new ModuleSpec( + ImageStoreManager.class, + new Provider() { + @Override + public NativeModule get() { + return new ImageStoreManager(context); + } + }), + new ModuleSpec( + IntentModule.class, + new Provider() { + @Override + public NativeModule get() { + return new IntentModule(context); + } + }), + new ModuleSpec( + LocationModule.class, + new Provider() { + @Override + public NativeModule get() { + return new LocationModule(context); + } + }), + new ModuleSpec( + NativeAnimatedModule.class, + new Provider() { + @Override + public NativeModule get() { + return new NativeAnimatedModule(context); + } + }), + new ModuleSpec( + NetworkingModule.class, + new Provider() { + @Override + public NativeModule get() { + return new NetworkingModule(context); + } + }), + new ModuleSpec( + NetInfoModule.class, + new Provider() { + @Override + public NativeModule get() { + return new NetInfoModule(context); + } + }), + new ModuleSpec( + PermissionsModule.class, + new Provider() { + @Override + public NativeModule get() { + return new PermissionsModule(context); + } + }), + new ModuleSpec( + ShareModule.class, + new Provider() { + @Override + public NativeModule get() { + return new ShareModule(context); + } + }), + new ModuleSpec( + StatusBarModule.class, + new Provider() { + @Override + public NativeModule get() { + return new StatusBarModule(context); + } + }), + new ModuleSpec( + TimePickerDialogModule.class, + new Provider() { + @Override + public NativeModule get() { + return new TimePickerDialogModule(context); + } + }), + new ModuleSpec( + ToastModule.class, + new Provider() { + @Override + public NativeModule get() { + return new ToastModule(context); + } + }), + new ModuleSpec( + VibrationModule.class, + new Provider() { + @Override + public NativeModule get() { + return new VibrationModule(context); + } + }), + new ModuleSpec( + WebSocketModule.class, + new Provider() { + @Override + public NativeModule get() { + return new WebSocketModule(context); + } + })); } @Override diff --git a/local-cli/templates/HelloWorld/ios/HelloWorld.xcodeproj/project.pbxproj b/local-cli/templates/HelloWorld/ios/HelloWorld.xcodeproj/project.pbxproj index e4471789d..e0c1aa688 100644 --- a/local-cli/templates/HelloWorld/ios/HelloWorld.xcodeproj/project.pbxproj +++ b/local-cli/templates/HelloWorld/ios/HelloWorld.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 2DCD954D1E0B4F2C00145EB5 /* HelloWorldTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* HelloWorldTests.m */; }; 5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */; }; 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; + ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADBDB9271DFEBF0700ED6528 /* libRCTBlob.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -228,6 +229,13 @@ remoteGlobalIDString = 58B5119B1A9E6C1200147676; remoteInfo = RCTText; }; + ADBDB9261DFEBF0700ED6528 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 358F4ED71D1E81A9004DF814; + remoteInfo = RCTBlob; + }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ @@ -255,6 +263,7 @@ 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = "../node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj"; sourceTree = ""; }; 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = ""; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; + ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTBlob.xcodeproj; path = "../node_modules/react-native/Libraries/Blob/RCTBlob.xcodeproj"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -270,6 +279,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */, + 5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */, 146834051AC3E58100842450 /* libReact.a in Frameworks */, 5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */, 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */, @@ -411,6 +422,7 @@ 3DAD3EAB1DF850E9000B6D8A /* libcxxreact.a */, 3DAD3EAD1DF850E9000B6D8A /* libjschelpers.a */, 3DAD3EAF1DF850E9000B6D8A /* libjschelpers.a */, + 3DAD3EA31DF850E9000B6D8A /* libReact-tvOS.a */, ); name = Products; sourceTree = ""; @@ -439,6 +451,7 @@ 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */, 146833FF1AC3E56700842450 /* React.xcodeproj */, 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */, + ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */, 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */, 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */, 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */, @@ -483,6 +496,14 @@ name = Products; sourceTree = ""; }; + ADBDB9201DFEBF0600ED6528 /* Products */ = { + isa = PBXGroup; + children = ( + ADBDB9271DFEBF0700ED6528 /* libRCTBlob.a */, + ); + name = Products; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -602,6 +623,10 @@ ProductGroup = 5E91572E1DD0AC6500FF2AA8 /* Products */; ProjectRef = 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */; }, + { + ProductGroup = ADBDB9201DFEBF0600ED6528 /* Products */; + ProjectRef = ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */; + }, { ProductGroup = 00C302B61ABCB90400DB3ED1 /* Products */; ProjectRef = 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */; @@ -748,10 +773,10 @@ remoteRef = 3DAD3E981DF850E9000B6D8A /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 3DAD3EA31DF850E9000B6D8A /* libReact.a */ = { + 3DAD3EA31DF850E9000B6D8A /* libReact-tvOS.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; - path = libReact.a; + path = "libReact-tvOS.a"; remoteRef = 3DAD3EA21DF850E9000B6D8A /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -825,6 +850,13 @@ remoteRef = 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + ADBDB9271DFEBF0700ED6528 /* libRCTBlob.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTBlob.a; + remoteRef = ADBDB9261DFEBF0700ED6528 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */