Skip to content

Commit

Permalink
add progressListener for android when using FormData to upload files
Browse files Browse the repository at this point in the history
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 facebook#7256

Differential Revision: D3390087

fbshipit-source-id: 7f3e53c80072fff397afd6f5fe17bf0f2ecd83b2
  • Loading branch information
tantan authored and Facebook Github Bot 7 committed Jun 4, 2016
1 parent 0f35f7c commit e63ea3a
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 17 deletions.
62 changes: 55 additions & 7 deletions Examples/UIExplorer/XHRExample.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ var {
TextInput,
TouchableHighlight,
View,
Image,
CameraRoll
} = ReactNative;

var XHRExampleHeaders = require('./XHRExampleHeaders');
Expand Down Expand Up @@ -127,6 +129,8 @@ class Downloader extends React.Component {
}
}

var PAGE_SIZE = 20;

class FormUploader extends React.Component {

_isMounted: boolean;
Expand All @@ -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() {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -320,6 +364,10 @@ var styles = StyleSheet.create({
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'grey',
},
randomPhoto: {
width: 50,
height: 50,
},
textButton: {
color: 'blue',
},
Expand Down
15 changes: 7 additions & 8 deletions Examples/UIExplorer/XHRExample.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -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});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -239,7 +239,19 @@ public void sendRequest(
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));
Expand Down Expand Up @@ -298,6 +310,18 @@ private void readWithProgress(
}
}

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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
};
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ public void writeTo(BufferedSink sink) throws IOException {
};
}

/**
* 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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
Arguments.class,
Call.class,
RequestBodyUtil.class,
ProgressRequestBody.class,
ProgressRequestListener.class,
MultipartBody.class,
MultipartBody.Builder.class,
NetworkingModule.class,
Expand Down Expand Up @@ -262,6 +264,7 @@ public void testMultipartPostRequestSimple() throws Exception {
.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();
Expand Down Expand Up @@ -316,6 +319,7 @@ public void testMultipartPostRequestHeaders() throws Exception {
.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"),
Expand Down Expand Up @@ -378,6 +382,7 @@ public void testMultipartPostRequestBody() throws Exception {
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);
Expand Down

0 comments on commit e63ea3a

Please sign in to comment.