Permalink
Browse files

Android: Support HTTP headers for source prop on <Image> components

Summary:
A copy of #7791 because of our very imperfect tools that mirror the changes from pull requests in the fb monorepo. The internal Phabricator revision for #7791 is in an 'abandoned' state (by foghina probably because of changing teams) and Phabricator doesn't allow me to claim that revision and merge it. Therefore I'm creating a new one.

(It's not foghina's fault, no one probably knew about this "abandoned Phabricator revision" edge case, don't remember we hit it before.)

Will try to keep attribution (git blame) to rigdern when merging.
Closes #12448

Differential Revision: D4584743

Pulled By: mkonicek

fbshipit-source-id: 66e5b88134fca1980adc4cd8a2ff17c42e10022c
  • Loading branch information...
mkonicek authored and facebook-github-bot committed Feb 18, 2017
1 parent c7f2c53 commit 8c0e6ecfc0e8b8ceb0471739ac2db53772d538cb
@@ -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,
@@ -300,6 +305,7 @@ var Image = React.createClass({
style,
shouldNotifyLoadEvents: !!(onLoadStart || onLoad || onLoadEnd || onError),
src: sources,
headers: source.headers,

This comment has been minimized.

Show comment
Hide comment
@npomfret

npomfret Apr 28, 2017

Contributor

@mkonicek I'm having trouble getting this working. In the case where I'm passing an image source with an array of images, where do I put the headers? I assumed I should put one headers prop for each image uri in the array. But that doesn't seem to be working. Looking at this code it seems that the source prop should have headers in the array, next to the images like this:

<Image source=[{uri: foo}, {uri: bar}, {headers: headers}] ...

Which doesn't look right.

@npomfret

npomfret Apr 28, 2017

Contributor

@mkonicek I'm having trouble getting this working. In the case where I'm passing an image source with an array of images, where do I put the headers? I assumed I should put one headers prop for each image uri in the array. But that doesn't seem to be working. Looking at this code it seems that the source prop should have headers in the array, next to the images like this:

<Image source=[{uri: foo}, {uri: bar}, {headers: headers}] ...

Which doesn't look right.

This comment has been minimized.

Show comment
Hide comment
@1st8

1st8 Apr 28, 2017

Same issue here, it is a regression in 0.43.
Multi-source images with headers don't work on android, but on iOS.
It worked in 0.42 though.
I will look into it now.

@1st8

1st8 Apr 28, 2017

Same issue here, it is a regression in 0.43.
Multi-source images with headers don't work on android, but on iOS.
It worked in 0.42 though.
I will look into it now.

This comment has been minimized.

Show comment
Hide comment
@1st8

1st8 Apr 28, 2017

I found a strange workaround:

const headers = { Authorization: 'Bearer foobar' }
// This is how it is supposed to be I think
const source = [{uri: 'foo', headers, ...}, {uri: 'bar', headers, ...}] 
// Workaround for android
source.headers = headers

return <Image source={source} />
@1st8

1st8 Apr 28, 2017

I found a strange workaround:

const headers = { Authorization: 'Bearer foobar' }
// This is how it is supposed to be I think
const source = [{uri: 'foo', headers, ...}, {uri: 'bar', headers, ...}] 
// Workaround for android
source.headers = headers

return <Image source={source} />

This comment has been minimized.

Show comment
Hide comment
@hramos

hramos Aug 24, 2017

Contributor

From what I can tell, the source PropType indicates that a per-source header prop is not supported. We don't have any monitoring in place for comments left in commits as it happens, so #13697 is a better place to discuss this.

@hramos

hramos Aug 24, 2017

Contributor

From what I can tell, the source PropType indicates that a per-source header prop is not supported. We don't have any monitoring in place for comments left in commits as it happens, so #13697 is a better place to discuss this.

loadingIndicatorSrc: loadingIndicatorSource ? loadingIndicatorSource.uri : null,
});
@@ -346,6 +352,7 @@ var styles = StyleSheet.create({
var cfg = {
nativeOnly: {
src: true,
headers: true,
loadingIndicatorSrc: true,
shouldNotifyLoadEvents: true,
},
@@ -277,8 +277,8 @@ dependencies {
compile 'com.android.support:appcompat-v7:23.0.1'
compile 'com.android.support:recyclerview-v7:23.4.0'
compile 'com.facebook.fbui.textlayoutbuilder:textlayoutbuilder:1.0.0'
compile 'com.facebook.fresco:fresco:0.11.0'
compile 'com.facebook.fresco:imagepipeline-okhttp3:0.11.0'
compile 'com.facebook.fresco:fresco:1.0.1'
compile 'com.facebook.fresco:imagepipeline-okhttp3:1.0.1'
compile 'com.facebook.soloader:soloader:0.1.0'
compile 'com.google.code.findbugs:jsr305:3.0.0'
compile 'com.squareup.okhttp3:okhttp:3.4.1'
@@ -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.
*
@@ -124,8 +126,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 ReactOkHttpNetworkFetcher(okHttpClient))
.setDownsampleEnabled(false)
.setRequestListeners(requestListeners);
}
@@ -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 ReactNetworkImageRequest extends ImageRequest {
/** Headers for the request */
private final ReadableMap mHeaders;
public static ReactNetworkImageRequest fromBuilderWithHeaders(ImageRequestBuilder builder,
ReadableMap headers) {
return new ReactNetworkImageRequest(builder, headers);
}
protected ReactNetworkImageRequest(ImageRequestBuilder builder, ReadableMap headers) {
super(builder);
this.mHeaders = headers;
}
public ReadableMap getHeaders() {
return mHeaders;
}
}
@@ -0,0 +1,81 @@
/**
* 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 android.net.Uri;
import android.os.SystemClock;
import com.facebook.imagepipeline.backends.okhttp3.OkHttpNetworkFetcher;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
import okhttp3.CacheControl;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
class ReactOkHttpNetworkFetcher extends OkHttpNetworkFetcher {
private static final String TAG = "ReactOkHttpNetworkFetcher";
private final OkHttpClient mOkHttpClient;
private final Executor mCancellationExecutor;
/**
* @param okHttpClient client to use
*/
public ReactOkHttpNetworkFetcher(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 ReactNetworkImageRequest) {
ReactNetworkImageRequest networkImageRequest = (ReactNetworkImageRequest)
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();
fetchWithRequest(fetchState, callback, request);
}
}
@@ -37,6 +37,7 @@ android_library(
react_native_target('java/com/facebook/react/common:common'),
react_native_target('java/com/facebook/react/module/annotations:annotations'),
react_native_target('java/com/facebook/react/uimanager:uimanager'),
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/views/imagehelper:withmultisource'),
],
@@ -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.module.annotations.ReactModule;
import com.facebook.react.uimanager.PixelUtil;
@@ -171,6 +172,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(
@@ -54,6 +54,7 @@
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.FloatUtil;
import com.facebook.react.modules.fresco.ReactNetworkImageRequest;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.events.EventDispatcher;
@@ -163,6 +164,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) {
@@ -324,6 +326,10 @@ private void cornerRadii(float[] computedCorners) {
computedCorners[2] = mBorderCornerRadii != null && !YogaConstants.isUndefined(mBorderCornerRadii[2]) ? mBorderCornerRadii[2] : defaultBorderRadius;
computedCorners[3] = mBorderCornerRadii != null && !YogaConstants.isUndefined(mBorderCornerRadii[3]) ? mBorderCornerRadii[3] : defaultBorderRadius;
}
public void setHeaders(ReadableMap headers) {
mHeaders = headers;
}
public void maybeUpdateView() {
if (!mIsDirty) {
@@ -384,12 +390,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 = ReactNetworkImageRequest.fromBuilderWithHeaders(imageRequestBuilder, mHeaders);
// This builder is reused
mDraweeControllerBuilder.reset();
@@ -14,6 +14,7 @@ android_library(
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/module/annotations:annotations'),
react_native_target('java/com/facebook/react/modules/fresco:fresco'),
react_native_target('java/com/facebook/react/uimanager:uimanager'),
react_native_target('java/com/facebook/react/uimanager/annotations:annotations'),
react_native_target('java/com/facebook/react/views/text:text'),
@@ -25,6 +25,7 @@
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.uimanager.ViewProps;
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;
@@ -36,6 +37,7 @@
public class FrescoBasedReactTextInlineImageShadowNode extends ReactTextInlineImageShadowNode {
private @Nullable Uri mUri;
private ReadableMap mHeaders;
private final AbstractDraweeControllerBuilder mDraweeControllerBuilder;
private final @Nullable Object mCallerContext;
private float mWidth = YogaConstants.UNDEFINED;
@@ -73,6 +75,11 @@ public void setSource(@Nullable ReadableArray sources) {
mUri = uri;
}
@ReactProp(name = "headers")
public void setHeaders(ReadableMap headers) {
mHeaders = headers;
}
/**
* Besides width/height, all other layout props on inline images are ignored
*/
@@ -100,6 +107,10 @@ public void setHeight(Dynamic height) {
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()) {
@@ -131,6 +142,7 @@ public TextInlineImageSpan buildInlineImageSpan() {
height,
width,
getUri(),
getHeaders(),
getDraweeControllerBuilder(),
getCallerContext());
}
@@ -25,7 +25,9 @@
import com.facebook.drawee.view.DraweeHolder;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.views.text.TextInlineImageSpan;
import com.facebook.react.modules.fresco.ReactNetworkImageRequest;
/**
* FrescoBasedTextInlineImageSpan is a span for Images that are inside <Text/>. It computes
@@ -48,6 +50,7 @@
private int mHeight;
private Uri mUri;
private int mWidth;
private ReadableMap mHeaders;
private @Nullable TextView mTextView;
@@ -56,6 +59,7 @@ public FrescoBasedReactTextInlineImageSpan(
int height,
int width,
@Nullable Uri uri,
ReadableMap headers,
AbstractDraweeControllerBuilder draweeControllerBuilder,
@Nullable Object callerContext) {
mDraweeHolder = new DraweeHolder(
@@ -68,6 +72,7 @@ public FrescoBasedReactTextInlineImageSpan(
mHeight = height;
mWidth = width;
mUri = (uri != null) ? uri : Uri.EMPTY;
mHeaders = headers;
}
/**
@@ -126,8 +131,8 @@ public void draw(
int bottom,
Paint paint) {
if (mDrawable == null) {
ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(mUri)
.build();
ImageRequestBuilder imageRequestBuilder = ImageRequestBuilder.newBuilderWithSource(mUri);
ImageRequest imageRequest = ReactNetworkImageRequest.fromBuilderWithHeaders(imageRequestBuilder, mHeaders);
DraweeController draweeController = mDraweeControllerBuilder
.reset()
@@ -6,6 +6,7 @@ android_library(
deps = [
YOGA_TARGET,
react_native_dep('android_res/com/facebook/catalyst/appcompat:appcompat'),
react_native_dep('libraries/fresco/fresco-react-native:fbcore'),
react_native_dep('libraries/fresco/fresco-react-native:fresco-drawee'),
react_native_dep('libraries/fresco/fresco-react-native:fresco-react-native'),
react_native_dep('libraries/fresco/fresco-react-native:imagepipeline'),
Oops, something went wrong.

1 comment on commit 8c0e6ec

@npomfret

This comment has been minimized.

Show comment
Hide comment
@npomfret

npomfret Apr 27, 2017

Contributor

Do you have an example of how to use this? Can't see anything in the docs...

Contributor

npomfret commented on 8c0e6ec Apr 27, 2017

Do you have an example of how to use this? Can't see anything in the docs...

Please sign in to comment.