From 1b92132fb03d444cb07af7a0458fc5dac869f80d Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Tue, 29 Jun 2021 22:04:43 +0200 Subject: [PATCH 1/9] Register `BlobProvider` as a ContentProvider --- packages/rn-tester/android/app/src/main/AndroidManifest.xml | 5 +++++ .../rn-tester/android/app/src/main/res/values/strings.xml | 1 + 2 files changed, 6 insertions(+) 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 From 2116f5044a6769357da248b5971edb24ced69c7f Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Tue, 29 Jun 2021 22:05:06 +0200 Subject: [PATCH 2/9] Add blob object URL as Image component source example in RNTester app --- .../js/examples/Image/ImageExample.js | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/packages/rn-tester/js/examples/Image/ImageExample.js b/packages/rn-tester/js/examples/Image/ImageExample.js index ef2be0a0605a0e..5eaa26d695c782 100644 --- a/packages/rn-tester/js/examples/Image/ImageExample.js +++ b/packages/rn-tester/js/examples/Image/ImageExample.js @@ -32,6 +32,40 @@ type ImageSource = $ReadOnly<{| uri: string, |}>; +type BlobImageExampleState = {| + objectURL: ?string, +|}; + +type BlobImageExampleProps = $ReadOnly<{| + url: string, +|}>; + +class BlobImageExample extends React.Component< + BlobImageExampleProps, + BlobImageExampleState, +> { + state = { + objectURL: null, + }; + + componentDidMount() { + (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 ? ( + + ) : ( + Object URL not created yet + ); + } +} + type NetworkImageCallbackExampleState = {| events: Array, startLoadPrefetched: boolean, @@ -608,6 +642,14 @@ 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 ' + From 2ffbd5a9ba2234b39ffba35636bb04fb0c934079 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Tue, 29 Jun 2021 22:12:45 +0200 Subject: [PATCH 3/9] Fix `BlobProvider.openFile` for blobs larger than pipe capacity --- .../react/modules/blob/BlobProvider.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) 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..e70a9acfc8706b 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 @@ -86,11 +86,20 @@ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundEx ParcelFileDescriptor readSide = pipe[0]; ParcelFileDescriptor writeSide = pipe[1]; - try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) { - outputStream.write(data); - } catch (IOException exception) { - return null; - } + Thread writer = new Thread() { + public void run() { + try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) { + // If the blob data is larger than pipe capacity (64 KB), a synchronous write call + // would fill up the whole buffer and block until the bytes are read (i.e. never). + // Writing from a separate thread allows us to return the read side descriptor + // immediately so that the reader can start reading. + outputStream.write(data); + } catch (IOException exception) { + // no-op + } + } + }; + writer.start(); return readSide; } From 57e317304362dca8b00069f85a8b561c1bec35c9 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Tue, 29 Jun 2021 22:31:39 +0200 Subject: [PATCH 4/9] Declare `data` and `writeSide` final --- .../java/com/facebook/react/modules/blob/BlobProvider.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 e70a9acfc8706b..d4057a0a60c49f 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 @@ -72,7 +72,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,7 +84,7 @@ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundEx return null; } ParcelFileDescriptor readSide = pipe[0]; - ParcelFileDescriptor writeSide = pipe[1]; + final ParcelFileDescriptor writeSide = pipe[1]; Thread writer = new Thread() { public void run() { From 005d4a685117388226db9d3c4d916be47aa56031 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Tue, 29 Jun 2021 22:52:17 +0200 Subject: [PATCH 5/9] Fix Flow errors --- packages/rn-tester/js/examples/Image/ImageExample.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/rn-tester/js/examples/Image/ImageExample.js b/packages/rn-tester/js/examples/Image/ImageExample.js index 5eaa26d695c782..2272b01db238f7 100644 --- a/packages/rn-tester/js/examples/Image/ImageExample.js +++ b/packages/rn-tester/js/examples/Image/ImageExample.js @@ -48,7 +48,7 @@ class BlobImageExample extends React.Component< objectURL: null, }; - componentDidMount() { + UNSAFE_componentWillMount() { (async () => { const result = await fetch(this.props.url); const blob = await result.blob(); @@ -58,7 +58,7 @@ class BlobImageExample extends React.Component< } render() { - return this.state.objectURL ? ( + return this.state.objectURL !== null ? ( ) : ( Object URL not created yet @@ -647,7 +647,9 @@ exports.examples = [ 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 ; + return ( + + ); }, }, { From 82d32841d2926bffa97367edbe25cb82511f602f Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Tue, 29 Jun 2021 23:06:05 +0200 Subject: [PATCH 6/9] Fix code style --- .../react/modules/blob/BlobProvider.java | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) 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 d4057a0a60c49f..25794c7628ba59 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 @@ -86,19 +86,21 @@ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundEx ParcelFileDescriptor readSide = pipe[0]; final ParcelFileDescriptor writeSide = pipe[1]; - Thread writer = new Thread() { - public void run() { - try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) { - // If the blob data is larger than pipe capacity (64 KB), a synchronous write call - // would fill up the whole buffer and block until the bytes are read (i.e. never). - // Writing from a separate thread allows us to return the read side descriptor - // immediately so that the reader can start reading. - outputStream.write(data); - } catch (IOException exception) { - // no-op - } - } - }; + Thread writer = + new Thread() { + public void run() { + try (OutputStream outputStream = + new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) { + // If the blob data is larger than pipe capacity (64 KB), a synchronous write call + // would fill up the whole buffer and block until the bytes are read (i.e. never). + // Writing from a separate thread allows us to return the read side descriptor + // immediately so that the reader can start reading. + outputStream.write(data); + } catch (IOException exception) { + // no-op + } + } + }; writer.start(); return readSide; From 4b886e2efb30195ddd437bd048c88f0af424126c Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Wed, 14 Jul 2021 11:40:20 +0200 Subject: [PATCH 7/9] Conditionally create new thread --- .../react/modules/blob/BlobProvider.java | 25 ++++++++++--- .../js/examples/Image/ImageExample.js | 37 +++++++++++++++---- 2 files changed, 49 insertions(+), 13 deletions(-) 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 25794c7628ba59..c7437420aea14d 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 @@ -23,6 +23,8 @@ public final class BlobProvider extends ContentProvider { + private final static int PIPE_CAPACITY = 65536; + @Override public boolean onCreate() { return true; @@ -86,22 +88,33 @@ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundEx ParcelFileDescriptor readSide = pipe[0]; final ParcelFileDescriptor writeSide = pipe[1]; - Thread writer = + 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. + Thread writer = new Thread() { public void run() { try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) { - // If the blob data is larger than pipe capacity (64 KB), a synchronous write call - // would fill up the whole buffer and block until the bytes are read (i.e. never). - // Writing from a separate thread allows us to return the read side descriptor - // immediately so that the reader can start reading. outputStream.write(data); } catch (IOException exception) { // no-op } } }; - writer.start(); + writer.start(); + } return readSide; } diff --git a/packages/rn-tester/js/examples/Image/ImageExample.js b/packages/rn-tester/js/examples/Image/ImageExample.js index 2272b01db238f7..7bd8eda21a62af 100644 --- a/packages/rn-tester/js/examples/Image/ImageExample.js +++ b/packages/rn-tester/js/examples/Image/ImageExample.js @@ -32,18 +32,15 @@ type ImageSource = $ReadOnly<{| uri: string, |}>; -type BlobImageExampleState = {| +type BlobImageState = {| objectURL: ?string, |}; -type BlobImageExampleProps = $ReadOnly<{| +type BlobImageProps = $ReadOnly<{| url: string, |}>; -class BlobImageExample extends React.Component< - BlobImageExampleProps, - BlobImageExampleState, -> { +class BlobImage extends React.Component { state = { objectURL: null, }; @@ -66,6 +63,27 @@ class BlobImageExample extends React.Component< } } +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, @@ -648,7 +666,12 @@ exports.examples = [ 'then it will be resolved using `BlobProvider` (Android) or `RCTBlobManager` (iOS).': string), render: function(): React.Node { return ( - + ); }, }, From 58c835489fe365065e1b4d7fcd9c53baf749e5c4 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Wed, 14 Jul 2021 12:18:41 +0200 Subject: [PATCH 8/9] Fix code style --- .../react/modules/blob/BlobProvider.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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 c7437420aea14d..b5ff835e9ff965 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 @@ -23,7 +23,7 @@ public final class BlobProvider extends ContentProvider { - private final static int PIPE_CAPACITY = 65536; + private static final int PIPE_CAPACITY = 65536; @Override public boolean onCreate() { @@ -103,16 +103,16 @@ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundEx // 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. Thread writer = - new Thread() { - public void run() { - try (OutputStream outputStream = - new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) { - outputStream.write(data); - } catch (IOException exception) { - // no-op + new Thread() { + public void run() { + try (OutputStream outputStream = + new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) { + outputStream.write(data); + } catch (IOException exception) { + // no-op + } } - } - }; + }; writer.start(); } From f8d3b85f27be0683a268d514a335653d5a56e42e Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Wed, 14 Jul 2021 13:21:36 +0200 Subject: [PATCH 9/9] Use single thread executor --- .../com/facebook/react/modules/blob/BlobProvider.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 b5ff835e9ff965..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,11 +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; @@ -102,8 +106,8 @@ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundEx // 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. - Thread writer = - new Thread() { + Runnable writer = + new Runnable() { public void run() { try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) { @@ -113,7 +117,7 @@ public void run() { } } }; - writer.start(); + executor.submit(writer); } return readSide;