Skip to content

Commit 2743ba2

Browse files
author
Adam Comella
committed
Android: Support HTTP headers for source prop on <Image> components
Allows developers to specify headers to include in the HTTP request when fetching a remote image. For example, one might leverage this when fetching an image from an endpoint that requires authentication: ``` <Image style={styles.logo} source={{ uri: 'http://facebook.github.io/react/img/logo_og.png', headers: { Authorization: 'someAuthToken' } }} /> ``` Note that the header values must be strings.
1 parent 0ce2bbd commit 2743ba2

File tree

9 files changed

+231
-6
lines changed

9 files changed

+231
-6
lines changed

Libraries/Image/Image.android.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,18 @@ var Image = React.createClass({
8383
* `uri` is a string representing the resource identifier for the image, which
8484
* could be an http address, a local file path, or a static image
8585
* resource (which should be wrapped in the `require('./path/to/image.png')` function).
86+
*
87+
* `headers` is an object representing the HTTP headers to send along with the request
88+
* for a remote image.
89+
*
8690
* This prop can also contain several remote `uri`, specified together with
8791
* their width and height. The native side will then choose the best `uri` to display
8892
* based on the measured size of the image container.
8993
*/
9094
source: PropTypes.oneOfType([
9195
PropTypes.shape({
9296
uri: PropTypes.string,
97+
headers: PropTypes.objectOf(PropTypes.string),
9398
}),
9499
// Opaque type returned by require('./image.jpg')
95100
PropTypes.number,
@@ -289,6 +294,7 @@ var Image = React.createClass({
289294
style,
290295
shouldNotifyLoadEvents: !!(onLoadStart || onLoad || onLoadEnd),
291296
src: sources,
297+
headers: source.headers,
292298
loadingIndicatorSrc: loadingIndicatorSource ? loadingIndicatorSource.uri : null,
293299
});
294300

@@ -335,6 +341,7 @@ var styles = StyleSheet.create({
335341
var cfg = {
336342
nativeOnly: {
337343
src: true,
344+
headers: true,
338345
loadingIndicatorSrc: true,
339346
shouldNotifyLoadEvents: true,
340347
},

ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import com.facebook.react.modules.network.OkHttpClientProvider;
2929
import com.facebook.soloader.SoLoader;
3030

31+
import okhttp3.OkHttpClient;
32+
3133
/**
3234
* Module to initialize the Fresco library.
3335
*
@@ -114,8 +116,10 @@ private static ImagePipelineConfig getDefaultConfig(Context context) {
114116
HashSet<RequestListener> requestListeners = new HashSet<>();
115117
requestListeners.add(new SystraceRequestListener());
116118

119+
OkHttpClient okHttpClient = OkHttpClientProvider.getOkHttpClient();
117120
return OkHttpImagePipelineConfigFactory
118-
.newBuilder(context.getApplicationContext(), OkHttpClientProvider.getOkHttpClient())
121+
.newBuilder(context.getApplicationContext(), okHttpClient)
122+
.setNetworkFetcher(new NetworkFetcher(okHttpClient))
119123
.setDownsampleEnabled(false)
120124
.setRequestListeners(requestListeners)
121125
.build();
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
* <p/>
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
package com.facebook.react.modules.fresco;
11+
12+
import java.io.IOException;
13+
import java.util.Collections;
14+
import java.util.HashMap;
15+
import java.util.Map;
16+
import java.util.concurrent.Executor;
17+
18+
import android.net.Uri;
19+
import android.os.Looper;
20+
import android.os.SystemClock;
21+
22+
import com.facebook.common.logging.FLog;
23+
import com.facebook.imagepipeline.backends.okhttp3.OkHttpNetworkFetcher;
24+
import com.facebook.imagepipeline.producers.BaseProducerContextCallbacks;
25+
import com.facebook.react.bridge.ReadableMap;
26+
import com.facebook.react.bridge.ReadableMapKeySetIterator;
27+
import okhttp3.CacheControl;
28+
import okhttp3.Callback;
29+
import okhttp3.Call;
30+
import okhttp3.Headers;
31+
import okhttp3.OkHttpClient;
32+
import okhttp3.Request;
33+
import okhttp3.Response;
34+
import okhttp3.ResponseBody;
35+
36+
class NetworkFetcher extends OkHttpNetworkFetcher {
37+
38+
private static final String TAG = "NetworkFetcher";
39+
40+
private final OkHttpClient mOkHttpClient;
41+
private final Executor mCancellationExecutor;
42+
43+
/**
44+
* @param okHttpClient client to use
45+
*/
46+
public NetworkFetcher(OkHttpClient okHttpClient) {
47+
super(okHttpClient);
48+
mOkHttpClient = okHttpClient;
49+
mCancellationExecutor = okHttpClient.dispatcher().executorService();
50+
}
51+
52+
private Map<String, String> getHeaders(ReadableMap readableMap) {
53+
if (readableMap == null) {
54+
return null;
55+
}
56+
ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
57+
Map<String, String> map = new HashMap<>();
58+
while (iterator.hasNextKey()) {
59+
String key = iterator.nextKey();
60+
String value = readableMap.getString(key);
61+
map.put(key, value);
62+
}
63+
return map;
64+
}
65+
66+
@Override
67+
public void fetch(final OkHttpNetworkFetchState fetchState, final Callback callback) {
68+
fetchState.submitTime = SystemClock.elapsedRealtime();
69+
final Uri uri = fetchState.getUri();
70+
Map<String, String> requestHeaders = null;
71+
if (fetchState.getContext().getImageRequest() instanceof NetworkImageRequest) {
72+
NetworkImageRequest networkImageRequest = (NetworkImageRequest)
73+
fetchState.getContext().getImageRequest();
74+
requestHeaders = getHeaders(networkImageRequest.getHeaders());
75+
}
76+
if (requestHeaders == null) {
77+
requestHeaders = Collections.emptyMap();
78+
}
79+
final Request request = new Request.Builder()
80+
.cacheControl(new CacheControl.Builder().noStore().build())
81+
.url(uri.toString())
82+
.headers(Headers.of(requestHeaders))
83+
.get()
84+
.build();
85+
final Call call = mOkHttpClient.newCall(request);
86+
87+
fetchState.getContext().addCallbacks(
88+
new BaseProducerContextCallbacks() {
89+
@Override
90+
public void onCancellationRequested() {
91+
if (Looper.myLooper() != Looper.getMainLooper()) {
92+
call.cancel();
93+
} else {
94+
mCancellationExecutor.execute(new Runnable() {
95+
@Override
96+
public void run() {
97+
call.cancel();
98+
}
99+
});
100+
}
101+
}
102+
});
103+
104+
call.enqueue(
105+
new okhttp3.Callback() {
106+
@Override
107+
public void onResponse(Call c, Response response) {
108+
fetchState.responseTime = SystemClock.elapsedRealtime();
109+
final ResponseBody body = response.body();
110+
try {
111+
long contentLength = body.contentLength();
112+
if (contentLength < 0) {
113+
contentLength = 0;
114+
}
115+
callback.onResponse(body.byteStream(), (int) contentLength);
116+
} catch (Exception e) {
117+
handleException(call, e, callback);
118+
} finally {
119+
try {
120+
body.close();
121+
} catch (Exception e) {
122+
FLog.w(TAG, "Exception when closing response body", e);
123+
}
124+
}
125+
}
126+
127+
@Override
128+
public void onFailure(final Call c, final IOException e) {
129+
handleException(call, e, callback);
130+
}
131+
});
132+
}
133+
134+
/**
135+
* Handles exceptions.
136+
*
137+
* <p> OkHttp notifies callers of cancellations via an IOException. If IOException is caught
138+
* after request cancellation, then the exception is interpreted as successful cancellation
139+
* and onCancellation is called. Otherwise onFailure is called.
140+
*/
141+
private void handleException(final Call call, final Exception e, final Callback callback) {
142+
if (call.isCanceled()) {
143+
callback.onCancellation();
144+
} else {
145+
callback.onFailure(e);
146+
}
147+
}
148+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
* <p/>
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
package com.facebook.react.modules.fresco;
11+
12+
import com.facebook.imagepipeline.request.ImageRequest;
13+
import com.facebook.imagepipeline.request.ImageRequestBuilder;
14+
import com.facebook.react.bridge.ReadableMap;
15+
16+
/** Extended ImageRequest with request headers */
17+
public class NetworkImageRequest extends ImageRequest {
18+
19+
/** Headers for the request */
20+
private final ReadableMap mHeaders;
21+
22+
public static NetworkImageRequest fromBuilder(ImageRequestBuilder builder,
23+
ReadableMap headers) {
24+
return new NetworkImageRequest(builder, headers);
25+
}
26+
27+
protected NetworkImageRequest(ImageRequestBuilder builder, ReadableMap headers) {
28+
super(builder);
29+
this.mHeaders = headers;
30+
}
31+
32+
public ReadableMap getHeaders() {
33+
return mHeaders;
34+
}
35+
}

ReactAndroid/src/main/java/com/facebook/react/views/image/BUCK

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ android_library(
3535
react_native_dep('third-party/java/jsr-305:jsr-305'),
3636
react_native_target('java/com/facebook/react/bridge:bridge'),
3737
react_native_target('java/com/facebook/react/common:common'),
38+
react_native_target('java/com/facebook/react/modules/fresco:fresco'),
3839
react_native_target('java/com/facebook/react/uimanager/annotations:annotations'),
3940
react_native_target('java/com/facebook/react/uimanager:uimanager'),
4041
react_native_target('java/com/facebook/react/views/imagehelper:withmultisource'),

ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.facebook.drawee.controller.AbstractDraweeControllerBuilder;
2222
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
2323
import com.facebook.react.bridge.ReadableArray;
24+
import com.facebook.react.bridge.ReadableMap;
2425
import com.facebook.react.common.MapBuilder;
2526
import com.facebook.react.uimanager.PixelUtil;
2627
import com.facebook.react.uimanager.SimpleViewManager;
@@ -169,6 +170,11 @@ public void setLoadHandlersRegistered(ReactImageView view, boolean shouldNotifyL
169170
view.setShouldNotifyLoadEvents(shouldNotifyLoadEvents);
170171
}
171172

173+
@ReactProp(name = "headers")
174+
public void setHeaders(ReactImageView view, ReadableMap headers) {
175+
view.setHeaders(headers);
176+
}
177+
172178
@Override
173179
public @Nullable Map getExportedCustomDirectEventTypeConstants() {
174180
return MapBuilder.of(

ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import com.facebook.react.bridge.ReactContext;
5252
import com.facebook.react.bridge.ReadableArray;
5353
import com.facebook.react.bridge.ReadableMap;
54+
import com.facebook.react.modules.fresco.NetworkImageRequest;
5455
import com.facebook.react.uimanager.PixelUtil;
5556
import com.facebook.react.uimanager.UIManagerModule;
5657
import com.facebook.react.uimanager.events.EventDispatcher;
@@ -161,6 +162,7 @@ public void process(Bitmap output, Bitmap source) {
161162
private final @Nullable Object mCallerContext;
162163
private int mFadeDurationMs = -1;
163164
private boolean mProgressiveRenderingEnabled;
165+
private ReadableMap mHeaders;
164166

165167
// We can't specify rounding in XML, so have to do so here
166168
private static GenericDraweeHierarchy buildHierarchy(Context context) {
@@ -311,6 +313,10 @@ private void cornerRadii(float[] computedCorners) {
311313
computedCorners[2] = mBorderCornerRadii != null && !CSSConstants.isUndefined(mBorderCornerRadii[2]) ? mBorderCornerRadii[2] : defaultBorderRadius;
312314
computedCorners[3] = mBorderCornerRadii != null && !CSSConstants.isUndefined(mBorderCornerRadii[3]) ? mBorderCornerRadii[3] : defaultBorderRadius;
313315
}
316+
317+
public void setHeaders(ReadableMap headers) {
318+
mHeaders = headers;
319+
}
314320

315321
public void maybeUpdateView() {
316322
if (!mIsDirty) {
@@ -371,12 +377,13 @@ public void maybeUpdateView() {
371377

372378
ResizeOptions resizeOptions = doResize ? new ResizeOptions(getWidth(), getHeight()) : null;
373379

374-
ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(mImageSource.getUri())
380+
ImageRequestBuilder imageRequestBuilder = ImageRequestBuilder.newBuilderWithSource(mImageSource.getUri())
375381
.setPostprocessor(postprocessor)
376382
.setResizeOptions(resizeOptions)
377383
.setAutoRotateEnabled(true)
378-
.setProgressiveRenderingEnabled(mProgressiveRenderingEnabled)
379-
.build();
384+
.setProgressiveRenderingEnabled(mProgressiveRenderingEnabled);
385+
386+
ImageRequest imageRequest = NetworkImageRequest.fromBuilder(imageRequestBuilder, mHeaders);
380387

381388
// This builder is reused
382389
mDraweeControllerBuilder.reset();

ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageShadowNode.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.facebook.csslayout.CSSNode;
2222
import com.facebook.drawee.controller.AbstractDraweeControllerBuilder;
2323
import com.facebook.react.bridge.ReadableArray;
24+
import com.facebook.react.bridge.ReadableMap;
2425
import com.facebook.react.uimanager.annotations.ReactProp;
2526
import com.facebook.react.views.text.ReactTextInlineImageShadowNode;
2627
import com.facebook.react.views.text.TextInlineImageSpan;
@@ -33,6 +34,7 @@
3334
public class FrescoBasedReactTextInlineImageShadowNode extends ReactTextInlineImageShadowNode {
3435

3536
private @Nullable Uri mUri;
37+
private ReadableMap mHeaders;
3638
private final AbstractDraweeControllerBuilder mDraweeControllerBuilder;
3739
private final @Nullable Object mCallerContext;
3840

@@ -68,10 +70,19 @@ public void setSource(@Nullable ReadableArray sources) {
6870
mUri = uri;
6971
}
7072

73+
@ReactProp(name = "headers")
74+
public void setHeaders(ReadableMap headers) {
75+
mHeaders = headers;
76+
}
77+
7178
public @Nullable Uri getUri() {
7279
return mUri;
7380
}
7481

82+
public ReadableMap getHeaders() {
83+
return mHeaders;
84+
}
85+
7586
// TODO: t9053573 is tracking that this code should be shared
7687
private static @Nullable Uri getResourceDrawableUri(Context context, @Nullable String name) {
7788
if (name == null || name.isEmpty()) {
@@ -103,6 +114,7 @@ public TextInlineImageSpan buildInlineImageSpan() {
103114
height,
104115
width,
105116
getUri(),
117+
getHeaders(),
106118
getDraweeControllerBuilder(),
107119
getCallerContext());
108120
}

0 commit comments

Comments
 (0)