From e63ea3acc4e6b0cf4efbe6c186c212c54e1d64e7 Mon Sep 17 00:00:00 2001 From: tantan Date: Sat, 4 Jun 2016 08:42:37 -0700 Subject: [PATCH] add progressListener for android when using FormData to upload files Summary: When using FormData upload images or files, in Android version, network module cannot send an event for showing progress. This PR will solve this issue. I changed example in XHRExample for Android, you can see uploading progress in warning yellow bar. Closes https://github.com/facebook/react-native/pull/7256 Differential Revision: D3390087 fbshipit-source-id: 7f3e53c80072fff397afd6f5fe17bf0f2ecd83b2 --- Examples/UIExplorer/XHRExample.android.js | 62 ++++++++++++++-- Examples/UIExplorer/XHRExample.ios.js | 15 ++-- .../modules/network/NetworkingModule.java | 28 +++++++- .../modules/network/ProgressRequestBody.java | 70 +++++++++++++++++++ .../network/ProgressRequestListener.java | 15 ++++ .../modules/network/RequestBodyUtil.java | 7 ++ .../modules/network/NetworkingModuleTest.java | 5 ++ 7 files changed, 185 insertions(+), 17 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestBody.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestListener.java diff --git a/Examples/UIExplorer/XHRExample.android.js b/Examples/UIExplorer/XHRExample.android.js index 7cadaa4ff..991f88d45 100644 --- a/Examples/UIExplorer/XHRExample.android.js +++ b/Examples/UIExplorer/XHRExample.android.js @@ -24,6 +24,8 @@ var { TextInput, TouchableHighlight, View, + Image, + CameraRoll } = ReactNative; var XHRExampleHeaders = require('./XHRExampleHeaders'); @@ -127,6 +129,8 @@ class Downloader extends React.Component { } } +var PAGE_SIZE = 20; + class FormUploader extends React.Component { _isMounted: boolean; @@ -143,6 +147,8 @@ class FormUploader extends React.Component { this._isMounted = true; this._addTextParam = this._addTextParam.bind(this); this._upload = this._upload.bind(this); + this._fetchRandomPhoto = this._fetchRandomPhoto.bind(this); + this._fetchRandomPhoto(); } _addTextParam() { @@ -151,6 +157,25 @@ class FormUploader extends React.Component { this.setState({textParams}); } + _fetchRandomPhoto() { + CameraRoll.getPhotos( + {first: PAGE_SIZE} + ).then( + (data) => { + if (!this._isMounted) { + return; + } + var edges = data.edges; + var edge = edges[Math.floor(Math.random() * edges.length)]; + var randomPhoto = edge && edge.node && edge.node.image; + if (randomPhoto) { + this.setState({randomPhoto}); + } + }, + (error) => undefined + ); + } + componentWillUnmount() { this._isMounted = false; } @@ -201,19 +226,29 @@ class FormUploader extends React.Component { this.state.textParams.forEach( (param) => formdata.append(param.name, param.value) ); - if (xhr.upload) { - xhr.upload.onprogress = (event) => { - console.log('upload onprogress', event); - if (event.lengthComputable) { - this.setState({uploadProgress: event.loaded / event.total}); - } - }; + if (this.state.randomPhoto) { + formdata.append('image', {...this.state.randomPhoto, type:'image/jpg', name: 'image.jpg'}); } + xhr.upload.onprogress = (event) => { + console.log('upload onprogress', event); + if (event.lengthComputable) { + this.setState({uploadProgress: event.loaded / event.total}); + } + }; xhr.send(formdata); this.setState({isUploading: true}); } render() { + var image = null; + if (this.state.randomPhoto) { + image = ( + + ); + } var textItems = this.state.textParams.map((item, index) => ( + + + Random photo from your library + ( + update + ) + + {image} + {textItems} formdata.append(param.name, param.value) ); - if (xhr.upload) { - xhr.upload.onprogress = (event) => { - console.log('upload onprogress', event); - if (event.lengthComputable) { - this.setState({uploadProgress: event.loaded / event.total}); - } - }; - } + xhr.upload.onprogress = (event) => { + console.log('upload onprogress', event); + if (event.lengthComputable) { + this.setState({uploadProgress: event.loaded / event.total}); + } + }; + xhr.send(formdata); this.setState({isUploading: true}); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java index 97a803965..91afa7862 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java @@ -56,7 +56,7 @@ public final class NetworkingModule extends ReactContextBaseJavaModule { private static final String REQUEST_BODY_KEY_URI = "uri"; private static final String REQUEST_BODY_KEY_FORMDATA = "formData"; private static final String USER_AGENT_HEADER_NAME = "user-agent"; - + private static final int CHUNK_TIMEOUT_NS = 100 * 1000000; // 100ms private static final int MAX_CHUNK_SIZE_BETWEEN_FLUSHES = 8 * 1024; // 8K private final OkHttpClient mClient; @@ -239,7 +239,19 @@ public final class NetworkingModule extends ReactContextBaseJavaModule { if (multipartBuilder == null) { return; } - requestBuilder.method(method, multipartBuilder.build()); + + requestBuilder.method(method, RequestBodyUtil.createProgressRequest(multipartBuilder.build(), new ProgressRequestListener() { + long last = System.nanoTime(); + + @Override + public void onRequestProgress(long bytesWritten, long contentLength, boolean done) { + long now = System.nanoTime(); + if (done || shouldDispatch(now, last)) { + onDataSend(executorToken, requestId, bytesWritten,contentLength); + last = now; + } + } + })); } else { // Nothing in data payload, at least nothing we could understand anyway. requestBuilder.method(method, RequestBodyUtil.getEmptyBody(method)); @@ -298,6 +310,18 @@ public final class NetworkingModule extends ReactContextBaseJavaModule { } } + private static boolean shouldDispatch(long now, long last) { + return last + CHUNK_TIMEOUT_NS < now; + } + + private void onDataSend(ExecutorToken ExecutorToken, int requestId, long progress, long total) { + WritableArray args = Arguments.createArray(); + args.pushInt(requestId); + args.pushInt((int) progress); + args.pushInt((int) total); + getEventEmitter(ExecutorToken).emit("didSendNetworkData", args); + } + private void onDataReceived(ExecutorToken ExecutorToken, int requestId, String data) { WritableArray args = Arguments.createArray(); args.pushInt(requestId); diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestBody.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestBody.java new file mode 100644 index 000000000..0e511f926 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestBody.java @@ -0,0 +1,70 @@ +/** + * 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.network; + +import java.io.IOException; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okhttp3.internal.Util; +import okio.BufferedSink; +import okio.Buffer; +import okio.Sink; +import okio.ForwardingSink; +import okio.ByteString; +import okio.Okio; +import okio.Source; + +public class ProgressRequestBody extends RequestBody { + + private final RequestBody mRequestBody; + private final ProgressRequestListener mProgressListener; + private BufferedSink mBufferedSink; + + public ProgressRequestBody(RequestBody requestBody, ProgressRequestListener progressListener) { + mRequestBody = requestBody; + mProgressListener = progressListener; + } + + @Override + public MediaType contentType() { + return mRequestBody.contentType(); + } + + @Override + public long contentLength() throws IOException { + return mRequestBody.contentLength(); + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + if (mBufferedSink == null) { + mBufferedSink = Okio.buffer(sink(sink)); + } + mRequestBody.writeTo(mBufferedSink); + mBufferedSink.flush(); + } + + private Sink sink(Sink sink) { + return new ForwardingSink(sink) { + long bytesWritten = 0L; + long contentLength = 0L; + + @Override + public void write(Buffer source, long byteCount) throws IOException { + super.write(source, byteCount); + if (contentLength == 0) { + contentLength = contentLength(); + } + bytesWritten += byteCount; + mProgressListener.onRequestProgress(bytesWritten, contentLength, bytesWritten == contentLength); + } + }; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestListener.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestListener.java new file mode 100644 index 000000000..10230e6dc --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestListener.java @@ -0,0 +1,15 @@ +/** + * 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.network; + + +public interface ProgressRequestListener { + void onRequestProgress(long bytesWritten, long contentLength, boolean done); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java index 0290a23fd..1d5a5e1d9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java @@ -114,6 +114,13 @@ import okio.Source; }; } + /** + * Creates a ProgressRequestBody that can be used for showing uploading progress + */ + public static ProgressRequestBody createProgressRequest(RequestBody requestBody, ProgressRequestListener listener) { + return new ProgressRequestBody(requestBody, listener); + } + /** * Creates a empty RequestBody if required by the http method spec, otherwise use null */ diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java index ec04b5cbd..8e5f40cb7 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java @@ -60,6 +60,8 @@ import static org.mockito.Mockito.when; Arguments.class, Call.class, RequestBodyUtil.class, + ProgressRequestBody.class, + ProgressRequestListener.class, MultipartBody.class, MultipartBody.Builder.class, NetworkingModule.class, @@ -262,6 +264,7 @@ public class NetworkingModuleTest { .thenReturn(mock(InputStream.class)); when(RequestBodyUtil.create(any(MediaType.class), any(InputStream.class))) .thenReturn(mock(RequestBody.class)); + when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressRequestListener.class))).thenCallRealMethod(); JavaOnlyMap body = new JavaOnlyMap(); JavaOnlyArray formData = new JavaOnlyArray(); @@ -316,6 +319,7 @@ public class NetworkingModuleTest { .thenReturn(mock(InputStream.class)); when(RequestBodyUtil.create(any(MediaType.class), any(InputStream.class))) .thenReturn(mock(RequestBody.class)); + when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressRequestListener.class))).thenCallRealMethod(); List headers = Arrays.asList( JavaOnlyArray.of("Accept", "text/plain"), @@ -378,6 +382,7 @@ public class NetworkingModuleTest { when(RequestBodyUtil.getFileInputStream(any(ReactContext.class), any(String.class))) .thenReturn(inputStream); when(RequestBodyUtil.create(any(MediaType.class), any(InputStream.class))).thenCallRealMethod(); + when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressRequestListener.class))).thenCallRealMethod(); when(inputStream.available()).thenReturn("imageUri".length()); final MultipartBody.Builder multipartBuilder = mock(MultipartBody.Builder.class);