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
This commit is contained in:
parent
0f35f7c6d5
commit
e63ea3acc4
|
@ -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 = (
|
||||
<Image
|
||||
source={this.state.randomPhoto}
|
||||
style={styles.randomPhoto}
|
||||
/>
|
||||
);
|
||||
}
|
||||
var textItems = this.state.textParams.map((item, index) => (
|
||||
<View style={styles.paramRow}>
|
||||
<TextInput
|
||||
|
@ -252,6 +287,15 @@ class FormUploader extends React.Component {
|
|||
}
|
||||
return (
|
||||
<View>
|
||||
<View style={[styles.paramRow, styles.photoRow]}>
|
||||
<Text style={styles.photoLabel}>
|
||||
Random photo from your library
|
||||
(<Text style={styles.textButton} onPress={this._fetchRandomPhoto}>
|
||||
update
|
||||
</Text>)
|
||||
</Text>
|
||||
{image}
|
||||
</View>
|
||||
{textItems}
|
||||
<View>
|
||||
<Text
|
||||
|
@ -320,6 +364,10 @@ var styles = StyleSheet.create({
|
|||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: 'grey',
|
||||
},
|
||||
randomPhoto: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
},
|
||||
textButton: {
|
||||
color: 'blue',
|
||||
},
|
||||
|
|
|
@ -231,14 +231,13 @@ 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});
|
||||
}
|
||||
};
|
||||
}
|
||||
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});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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<JavaOnlyArray> 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);
|
||||
|
|
Loading…
Reference in New Issue