diff --git a/Libraries/Blob/Blob.js b/Libraries/Blob/Blob.js new file mode 100644 index 00000000000..0180e15e2ab --- /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 00000000000..8201df47bc3 --- /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 00000000000..64d1d77449f --- /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 00000000000..03b3ffc60a6 --- /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 00000000000..bf7a82bb970 --- /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 00000000000..7eac3d5206f --- /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 4c161413e36..80b3687b32d 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 822b6f4f3ce..b2012bc4afb 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 5a420b70e6f..12f6da8a0d5 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 0e4362bcf65..4969876b43d 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 40b086de24e..83896e969e4 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 c010bf4be3f..d8168f09e57 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 5c8e54fe2d3..070512a3e1a 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 21900cdde25..6a753b27bd3 --- 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 68389ddcaf3..e3718577206 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 17701404417..66de385a23f 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 6a94c8d54a5..4f15f596e71 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 00000000000..936f5e5719d --- /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 00000000000..e213bb749dd --- /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 00000000000..2404f2dbab1 --- /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 8f95b674f0b..17df446110b 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 d6688926cc7..fdd003be9c9 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 1d2e839ecae..c4a32965d3d 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 e4471789dbd..e0c1aa68859 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 */