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 baf039a223128f..96141bafb6a086 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 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 ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundEx
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 ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundEx
return null;
}
ParcelFileDescriptor readSide = pipe[0];
- ParcelFileDescriptor writeSide = pipe[1];
-
- try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) {
- outputStream.write(data);
- } catch (IOException exception) {
- return null;
+ final ParcelFileDescriptor writeSide = pipe[1];
+
+ 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 d47ee519ae3180..e2e88576591989 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 10487f504c9208..50058dbb1f29be 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 ef2be0a0605a0e..7bd8eda21a62af 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 ' +