Skip to content

Commit

Permalink
Android: Support HTTP headers for source prop on <Image> components
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Adam Comella committed Sep 26, 2016
1 parent 0ce2bbd commit 2743ba2
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 6 deletions.
7 changes: 7 additions & 0 deletions Libraries/Image/Image.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,18 @@ var Image = React.createClass({
* `uri` is a string representing the resource identifier for the image, which
* could be an http address, a local file path, or a static image
* resource (which should be wrapped in the `require('./path/to/image.png')` function).
*
* `headers` is an object representing the HTTP headers to send along with the request
* for a remote image.
*
* This prop can also contain several remote `uri`, specified together with
* their width and height. The native side will then choose the best `uri` to display
* based on the measured size of the image container.
*/
source: PropTypes.oneOfType([
PropTypes.shape({
uri: PropTypes.string,
headers: PropTypes.objectOf(PropTypes.string),
}),
// Opaque type returned by require('./image.jpg')
PropTypes.number,
Expand Down Expand Up @@ -289,6 +294,7 @@ var Image = React.createClass({
style,
shouldNotifyLoadEvents: !!(onLoadStart || onLoad || onLoadEnd),
src: sources,
headers: source.headers,
loadingIndicatorSrc: loadingIndicatorSource ? loadingIndicatorSource.uri : null,
});

Expand Down Expand Up @@ -335,6 +341,7 @@ var styles = StyleSheet.create({
var cfg = {
nativeOnly: {
src: true,
headers: true,
loadingIndicatorSrc: true,
shouldNotifyLoadEvents: true,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import com.facebook.react.modules.network.OkHttpClientProvider;
import com.facebook.soloader.SoLoader;

import okhttp3.OkHttpClient;

/**
* Module to initialize the Fresco library.
*
Expand Down Expand Up @@ -114,8 +116,10 @@ private static ImagePipelineConfig getDefaultConfig(Context context) {
HashSet<RequestListener> requestListeners = new HashSet<>();
requestListeners.add(new SystraceRequestListener());

OkHttpClient okHttpClient = OkHttpClientProvider.getOkHttpClient();
return OkHttpImagePipelineConfigFactory
.newBuilder(context.getApplicationContext(), OkHttpClientProvider.getOkHttpClient())
.newBuilder(context.getApplicationContext(), okHttpClient)
.setNetworkFetcher(new NetworkFetcher(okHttpClient))
.setDownsampleEnabled(false)
.setRequestListeners(requestListeners)
.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
* <p/>
* 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.fresco;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;

import android.net.Uri;
import android.os.Looper;
import android.os.SystemClock;

import com.facebook.common.logging.FLog;
import com.facebook.imagepipeline.backends.okhttp3.OkHttpNetworkFetcher;
import com.facebook.imagepipeline.producers.BaseProducerContextCallbacks;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import okhttp3.CacheControl;
import okhttp3.Callback;
import okhttp3.Call;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

class NetworkFetcher extends OkHttpNetworkFetcher {

This comment has been minimized.

Copy link
@lambdapioneer

lambdapioneer Oct 5, 2016

Maybe call this ReactOkHttpNetworkFetcher to highlight that parts of it are specific to react native


private static final String TAG = "NetworkFetcher";

private final OkHttpClient mOkHttpClient;
private final Executor mCancellationExecutor;

/**
* @param okHttpClient client to use
*/
public NetworkFetcher(OkHttpClient okHttpClient) {
super(okHttpClient);
mOkHttpClient = okHttpClient;
mCancellationExecutor = okHttpClient.dispatcher().executorService();
}

private Map<String, String> getHeaders(ReadableMap readableMap) {
if (readableMap == null) {
return null;
}
ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
Map<String, String> map = new HashMap<>();
while (iterator.hasNextKey()) {
String key = iterator.nextKey();
String value = readableMap.getString(key);
map.put(key, value);
}
return map;
}

@Override
public void fetch(final OkHttpNetworkFetchState fetchState, final Callback callback) {
fetchState.submitTime = SystemClock.elapsedRealtime();
final Uri uri = fetchState.getUri();
Map<String, String> requestHeaders = null;
if (fetchState.getContext().getImageRequest() instanceof NetworkImageRequest) {
NetworkImageRequest networkImageRequest = (NetworkImageRequest)
fetchState.getContext().getImageRequest();
requestHeaders = getHeaders(networkImageRequest.getHeaders());
}
if (requestHeaders == null) {
requestHeaders = Collections.emptyMap();
}
final Request request = new Request.Builder()
.cacheControl(new CacheControl.Builder().noStore().build())
.url(uri.toString())
.headers(Headers.of(requestHeaders))
.get()
.build();
final Call call = mOkHttpClient.newCall(request);

This comment has been minimized.

Copy link
@lambdapioneer

lambdapioneer Oct 5, 2016

As the lines from here (and some from above) are identically with the okhttp3/OkHttpNetworkFetcher, we should refactor that code in a helper method there and then call it from both places :)


fetchState.getContext().addCallbacks(
new BaseProducerContextCallbacks() {
@Override
public void onCancellationRequested() {
if (Looper.myLooper() != Looper.getMainLooper()) {
call.cancel();
} else {
mCancellationExecutor.execute(new Runnable() {
@Override
public void run() {
call.cancel();
}
});
}
}
});

call.enqueue(
new okhttp3.Callback() {
@Override
public void onResponse(Call c, Response response) {
fetchState.responseTime = SystemClock.elapsedRealtime();
final ResponseBody body = response.body();
try {
long contentLength = body.contentLength();
if (contentLength < 0) {
contentLength = 0;
}
callback.onResponse(body.byteStream(), (int) contentLength);
} catch (Exception e) {
handleException(call, e, callback);
} finally {
try {
body.close();
} catch (Exception e) {
FLog.w(TAG, "Exception when closing response body", e);
}
}
}

@Override
public void onFailure(final Call c, final IOException e) {
handleException(call, e, callback);
}
});
}

/**
* Handles exceptions.
*
* <p> OkHttp notifies callers of cancellations via an IOException. If IOException is caught
* after request cancellation, then the exception is interpreted as successful cancellation
* and onCancellation is called. Otherwise onFailure is called.
*/
private void handleException(final Call call, final Exception e, final Callback callback) {
if (call.isCanceled()) {
callback.onCancellation();
} else {
callback.onFailure(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
* <p/>
* 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.fresco;

import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import com.facebook.react.bridge.ReadableMap;

/** Extended ImageRequest with request headers */
public class NetworkImageRequest extends ImageRequest {

This comment has been minimized.

Copy link
@lambdapioneer

lambdapioneer Oct 5, 2016

perhaps call it ReactNetworkImageRequest


/** Headers for the request */
private final ReadableMap mHeaders;

public static NetworkImageRequest fromBuilder(ImageRequestBuilder builder,

This comment has been minimized.

Copy link
@lambdapioneer

lambdapioneer Oct 5, 2016

fromBuilderWithHeaders

ReadableMap headers) {
return new NetworkImageRequest(builder, headers);
}

protected NetworkImageRequest(ImageRequestBuilder builder, ReadableMap headers) {
super(builder);
this.mHeaders = headers;
}

public ReadableMap getHeaders() {
return mHeaders;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ android_library(
react_native_dep('third-party/java/jsr-305:jsr-305'),
react_native_target('java/com/facebook/react/bridge:bridge'),
react_native_target('java/com/facebook/react/common:common'),
react_native_target('java/com/facebook/react/modules/fresco:fresco'),
react_native_target('java/com/facebook/react/uimanager/annotations:annotations'),
react_native_target('java/com/facebook/react/uimanager:uimanager'),
react_native_target('java/com/facebook/react/views/imagehelper:withmultisource'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.facebook.drawee.controller.AbstractDraweeControllerBuilder;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.SimpleViewManager;
Expand Down Expand Up @@ -169,6 +170,11 @@ public void setLoadHandlersRegistered(ReactImageView view, boolean shouldNotifyL
view.setShouldNotifyLoadEvents(shouldNotifyLoadEvents);
}

@ReactProp(name = "headers")
public void setHeaders(ReactImageView view, ReadableMap headers) {
view.setHeaders(headers);
}

@Override
public @Nullable Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.modules.fresco.NetworkImageRequest;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.events.EventDispatcher;
Expand Down Expand Up @@ -161,6 +162,7 @@ public void process(Bitmap output, Bitmap source) {
private final @Nullable Object mCallerContext;
private int mFadeDurationMs = -1;
private boolean mProgressiveRenderingEnabled;
private ReadableMap mHeaders;

// We can't specify rounding in XML, so have to do so here
private static GenericDraweeHierarchy buildHierarchy(Context context) {
Expand Down Expand Up @@ -311,6 +313,10 @@ private void cornerRadii(float[] computedCorners) {
computedCorners[2] = mBorderCornerRadii != null && !CSSConstants.isUndefined(mBorderCornerRadii[2]) ? mBorderCornerRadii[2] : defaultBorderRadius;
computedCorners[3] = mBorderCornerRadii != null && !CSSConstants.isUndefined(mBorderCornerRadii[3]) ? mBorderCornerRadii[3] : defaultBorderRadius;
}

public void setHeaders(ReadableMap headers) {
mHeaders = headers;
}

public void maybeUpdateView() {
if (!mIsDirty) {
Expand Down Expand Up @@ -371,12 +377,13 @@ public void maybeUpdateView() {

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

ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(mImageSource.getUri())
ImageRequestBuilder imageRequestBuilder = ImageRequestBuilder.newBuilderWithSource(mImageSource.getUri())
.setPostprocessor(postprocessor)
.setResizeOptions(resizeOptions)
.setAutoRotateEnabled(true)
.setProgressiveRenderingEnabled(mProgressiveRenderingEnabled)
.build();
.setProgressiveRenderingEnabled(mProgressiveRenderingEnabled);

ImageRequest imageRequest = NetworkImageRequest.fromBuilder(imageRequestBuilder, mHeaders);

// This builder is reused
mDraweeControllerBuilder.reset();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.facebook.csslayout.CSSNode;
import com.facebook.drawee.controller.AbstractDraweeControllerBuilder;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.views.text.ReactTextInlineImageShadowNode;
import com.facebook.react.views.text.TextInlineImageSpan;
Expand All @@ -33,6 +34,7 @@
public class FrescoBasedReactTextInlineImageShadowNode extends ReactTextInlineImageShadowNode {

private @Nullable Uri mUri;
private ReadableMap mHeaders;
private final AbstractDraweeControllerBuilder mDraweeControllerBuilder;
private final @Nullable Object mCallerContext;

Expand Down Expand Up @@ -68,10 +70,19 @@ public void setSource(@Nullable ReadableArray sources) {
mUri = uri;
}

@ReactProp(name = "headers")
public void setHeaders(ReadableMap headers) {
mHeaders = headers;
}

public @Nullable Uri getUri() {
return mUri;
}

public ReadableMap getHeaders() {
return mHeaders;
}

// TODO: t9053573 is tracking that this code should be shared
private static @Nullable Uri getResourceDrawableUri(Context context, @Nullable String name) {
if (name == null || name.isEmpty()) {
Expand Down Expand Up @@ -103,6 +114,7 @@ public TextInlineImageSpan buildInlineImageSpan() {
height,
width,
getUri(),
getHeaders(),
getDraweeControllerBuilder(),
getCallerContext());
}
Expand Down
Loading

1 comment on commit 2743ba2

@lambdapioneer
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks very close to what I've thought of. Happy to accept the Fresco PR for refactoring out the common part of the OkHttp3 code in an extra method. However, I don't think that should block this version to ship.

I cannot say much regarding the changes on the react-native side.

Please sign in to comment.