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 index baf039a223..96141bafb6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobProvider.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobProvider.java @@ -20,9 +20,15 @@ import com.facebook.react.bridge.ReactContext; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; public final class BlobProvider extends ContentProvider { + private static final int PIPE_CAPACITY = 65536; + + private ExecutorService executor = Executors.newSingleThreadExecutor(); + @Override public boolean onCreate() { return true; @@ -72,7 +78,7 @@ public final class BlobProvider extends ContentProvider { throw new RuntimeException("No blob module associated with BlobProvider"); } - byte[] data = blobModule.resolve(uri); + final byte[] data = blobModule.resolve(uri); if (data == null) { throw new FileNotFoundException("Cannot open " + uri.toString() + ", blob not found."); } @@ -84,12 +90,34 @@ public final class BlobProvider extends ContentProvider { return null; } ParcelFileDescriptor readSide = pipe[0]; - ParcelFileDescriptor writeSide = pipe[1]; + final ParcelFileDescriptor writeSide = pipe[1]; - try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) { - outputStream.write(data); - } catch (IOException exception) { - return null; + if (data.length <= PIPE_CAPACITY) { + // If the blob length is less than or equal to pipe capacity (64 KB), + // we can write the data synchronously to the pipe buffer. + try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) { + outputStream.write(data); + } catch (IOException exception) { + return null; + } + } else { + // For blobs larger than 64 KB, a synchronous write would fill up the whole buffer + // and block forever, because there are no readers to empty the buffer. + // Writing from a separate thread allows us to return the read side descriptor + // immediately so that both writer and reader can work concurrently. + // Reading from the pipe empties the buffer and allows the next chunks to be written. + Runnable writer = + new Runnable() { + public void run() { + try (OutputStream outputStream = + new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) { + outputStream.write(data); + } catch (IOException exception) { + // no-op + } + } + }; + executor.submit(writer); } return readSide; diff --git a/packages/rn-tester/android/app/src/main/AndroidManifest.xml b/packages/rn-tester/android/app/src/main/AndroidManifest.xml index d47ee519ae..e2e8857659 100644 --- a/packages/rn-tester/android/app/src/main/AndroidManifest.xml +++ b/packages/rn-tester/android/app/src/main/AndroidManifest.xml @@ -52,6 +52,11 @@ + diff --git a/packages/rn-tester/android/app/src/main/res/values/strings.xml b/packages/rn-tester/android/app/src/main/res/values/strings.xml index 10487f504c..50058dbb1f 100644 --- a/packages/rn-tester/android/app/src/main/res/values/strings.xml +++ b/packages/rn-tester/android/app/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ RNTester App + com.facebook.react.uiapp.blobs diff --git a/packages/rn-tester/js/examples/Image/ImageExample.js b/packages/rn-tester/js/examples/Image/ImageExample.js index ef2be0a060..7bd8eda21a 100644 --- a/packages/rn-tester/js/examples/Image/ImageExample.js +++ b/packages/rn-tester/js/examples/Image/ImageExample.js @@ -32,6 +32,58 @@ type ImageSource = $ReadOnly<{| uri: string, |}>; +type BlobImageState = {| + objectURL: ?string, +|}; + +type BlobImageProps = $ReadOnly<{| + url: string, +|}>; + +class BlobImage extends React.Component { + state = { + objectURL: null, + }; + + UNSAFE_componentWillMount() { + (async () => { + const result = await fetch(this.props.url); + const blob = await result.blob(); + const objectURL = URL.createObjectURL(blob); + this.setState({objectURL}); + })(); + } + + render() { + return this.state.objectURL !== null ? ( + + ) : ( + Object URL not created yet + ); + } +} + +type BlobImageExampleState = {||}; + +type BlobImageExampleProps = $ReadOnly<{| + urls: string[], +|}>; + +class BlobImageExample extends React.Component< + BlobImageExampleProps, + BlobImageExampleState, +> { + render() { + return ( + + {this.props.urls.map(url => ( + + ))} + + ); + } +} + type NetworkImageCallbackExampleState = {| events: Array, startLoadPrefetched: boolean, @@ -608,6 +660,21 @@ exports.examples = [ return ; }, }, + { + title: 'Plain Blob Image', + description: ('If the `source` prop `uri` property is an object URL, ' + + 'then it will be resolved using `BlobProvider` (Android) or `RCTBlobManager` (iOS).': string), + render: function(): React.Node { + return ( + + ); + }, + }, { title: 'Plain Static Image', description: ('Static assets should be placed in the source code tree, and ' +