Skip to content

Commit 87c54a7

Browse files
giantslogikfacebook-github-bot
authored andcommitted
fix fetch of content scheme urls failing on Android (#50122)
Summary: This PR fixes #48762 - fix creating Blobs from Android 'content://' scheme urls was failing on the js side due to [this check](https://github.com/JakeChampion/fetch/blob/ba5cf1ed2e02ebb96fa1e60b4fd2eb04071b60e4/fetch.js#L547) ## Changelog: [ANDROID] [FIXED] - fix fetch of content scheme uris failing on Android. Pull Request resolved: #50122 Test Plan: Used the App here to test Android Blob creation. https://github.com/giantslogik/blob-large-file-fetch. EDIT: Added tester to RNTester Reviewed By: rshest Differential Revision: D73576300 Pulled By: javache fbshipit-source-id: 3fa5966aabd10d5fbe9f441948309c66e7113199
1 parent ac8b01f commit 87c54a7

File tree

7 files changed

+184
-5
lines changed

7 files changed

+184
-5
lines changed

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.kt

+4
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,10 @@ public class NetworkingModule(
256256
for (handler in uriHandlers) {
257257
if (handler.supports(uri, responseType)) {
258258
val res = handler.fetch(uri)
259+
// fix: UriHandlers which are not using file:// scheme fail in whatwg-fetch at this line
260+
// https://github.com/JakeChampion/fetch/blob/main/fetch.js#L547
261+
ResponseUtil.onResponseReceived(
262+
reactApplicationContext, requestId, 200, Arguments.createMap(), url)
259263
ResponseUtil.onDataReceived(reactApplicationContext, requestId, res)
260264
ResponseUtil.onRequestSuccess(reactApplicationContext, requestId)
261265
return

packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/NativeSampleTurboModuleSpec.java

+3
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,7 @@ public WritableMap getObjectAssert(ReadableMap arg) {
141141

142142
@ReactMethod
143143
public void promiseAssert(Promise promise) {}
144+
145+
@ReactMethod
146+
public void getImageUrl(Promise promise) {}
144147
}

packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/ReactCommon/SampleTurboModuleSpec.cpp

+20
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,24 @@ __hostFunction_NativeSampleTurboModuleSpecJSI_promiseAssert(
310310
cachedMethodId);
311311
}
312312

313+
static facebook::jsi::Value
314+
__hostFunction_NativeSampleTurboModuleSpecJSI_getImageUrl(
315+
facebook::jsi::Runtime& rt,
316+
TurboModule& turboModule,
317+
const facebook::jsi::Value* args,
318+
size_t count) {
319+
static jmethodID cachedMethodId = nullptr;
320+
return static_cast<JavaTurboModule&>(turboModule)
321+
.invokeJavaMethod(
322+
rt,
323+
PromiseKind,
324+
"getImageUrl",
325+
"(Lcom/facebook/react/bridge/Promise;)V",
326+
args,
327+
count,
328+
cachedMethodId);
329+
}
330+
313331
NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(
314332
const JavaTurboModule::InitParams& params)
315333
: JavaTurboModule(params) {
@@ -351,6 +369,8 @@ NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(
351369
1, __hostFunction_NativeSampleTurboModuleSpecJSI_getObjectAssert};
352370
methodMap_["promiseAssert"] = MethodMetadata{
353371
0, __hostFunction_NativeSampleTurboModuleSpecJSI_promiseAssert};
372+
methodMap_["getImageUrl"] = MethodMetadata{
373+
0, __hostFunction_NativeSampleTurboModuleSpecJSI_getImageUrl};
354374
eventEmitterMap_["onPress"] =
355375
std::make_shared<AsyncEventEmitter<folly::dynamic>>();
356376
eventEmitterMap_["onClick"] =

packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleTurboModule.kt

+32-5
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77

88
package com.facebook.fbreact.specs
99

10+
import android.net.Uri
1011
import android.os.Build
1112
import android.util.DisplayMetrics
1213
import android.widget.Toast
14+
import androidx.activity.ComponentActivity
15+
import androidx.activity.result.contract.ActivityResultContracts
1316
import com.facebook.proguard.annotations.DoNotStrip
1417
import com.facebook.react.bridge.Arguments
1518
import com.facebook.react.bridge.Callback
@@ -24,6 +27,7 @@ import com.facebook.react.bridge.WritableNativeMap
2427
import com.facebook.react.module.annotations.ReactModule
2528
import com.facebook.react.turbomodule.core.interfaces.BindingsInstallerHolder
2629
import com.facebook.react.turbomodule.core.interfaces.TurboModuleWithJSIBindings
30+
import java.util.UUID
2731

2832
@DoNotStrip
2933
@ReactModule(name = SampleTurboModule.NAME)
@@ -172,7 +176,7 @@ public class SampleTurboModule(private val context: ReactApplicationContext) :
172176

173177
@DoNotStrip
174178
@Suppress("unused")
175-
override fun getValueWithPromise(error: Boolean, promise: Promise?) {
179+
override fun getValueWithPromise(error: Boolean, promise: Promise) {
176180
if (error) {
177181
promise?.reject(
178182
"code 1", "intentional promise rejection", Throwable("promise intentionally rejected"))
@@ -189,13 +193,13 @@ public class SampleTurboModule(private val context: ReactApplicationContext) :
189193

190194
@DoNotStrip
191195
@Suppress("unused")
192-
override fun getObjectThrows(arg: ReadableMap?): WritableMap? {
196+
override fun getObjectThrows(arg: ReadableMap): WritableMap {
193197
error("Intentional exception from JVM getObjectThrows with $arg")
194198
}
195199

196200
@DoNotStrip
197201
@Suppress("unused")
198-
override fun promiseThrows(promise: Promise?) {
202+
override fun promiseThrows(promise: Promise) {
199203
error("Intentional exception from JVM promiseThrows")
200204
}
201205

@@ -207,17 +211,40 @@ public class SampleTurboModule(private val context: ReactApplicationContext) :
207211

208212
@DoNotStrip
209213
@Suppress("unused")
210-
override fun getObjectAssert(arg: ReadableMap?): WritableMap? {
214+
override fun getObjectAssert(arg: ReadableMap): WritableMap? {
211215
assert(false) { "Intentional assert from JVM getObjectAssert with $arg" }
212216
return null
213217
}
214218

215219
@DoNotStrip
216220
@Suppress("unused")
217-
override fun promiseAssert(promise: Promise?) {
221+
override fun promiseAssert(promise: Promise) {
218222
assert(false) { "Intentional assert from JVM promiseAssert" }
219223
}
220224

225+
@DoNotStrip
226+
@Suppress("unused")
227+
override fun getImageUrl(promise: Promise) {
228+
val activity = context.getCurrentActivity() as? ComponentActivity
229+
if (activity != null) {
230+
val key = UUID.randomUUID().toString()
231+
activity.activityResultRegistry
232+
.register(
233+
key,
234+
ActivityResultContracts.GetContent(),
235+
{ uri: Uri? ->
236+
if (uri != null) {
237+
promise.resolve(uri.toString())
238+
} else {
239+
promise.resolve(null)
240+
}
241+
})
242+
.launch("image/*")
243+
} else {
244+
promise.reject("error", "Unable to obtain an image uri without current activity")
245+
}
246+
}
247+
221248
private fun log(method: String, input: Any?, output: Any?) {
222249
toast?.cancel()
223250
val message = StringBuilder("Method :")

packages/react-native/src/private/specs_DEPRECATED/modules/NativeSampleTurboModule.js

+3
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ export interface Spec extends TurboModule {
5959
+voidFuncAssert?: () => void;
6060
+getObjectAssert?: (arg: Object) => Object;
6161
+promiseAssert?: () => Promise<void>;
62+
63+
// Android-only
64+
+getImageUrl?: () => Promise<string | null>;
6265
}
6366

6467
export default (TurboModuleRegistry.getEnforcing<Spec>(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @flow strict-local
9+
*/
10+
11+
'use strict';
12+
13+
import RNTesterBlock from '../../components/RNTesterBlock';
14+
import RNTesterPage from '../../components/RNTesterPage';
15+
import RNTesterText from '../../components/RNTesterText';
16+
import React from 'react';
17+
import {
18+
Image,
19+
Platform,
20+
StyleSheet,
21+
ToastAndroid,
22+
TouchableOpacity,
23+
View,
24+
} from 'react-native';
25+
import NativeSampleTurboModule from 'react-native/Libraries/TurboModule/samples/NativeSampleTurboModule';
26+
27+
function blobToBase64(blob: Blob) {
28+
return new Promise<string>((resolve, reject) => {
29+
const reader = new FileReader();
30+
reader.onloadend = () => {
31+
var result = reader.result;
32+
if (typeof result === 'string') {
33+
resolve(result);
34+
} else {
35+
reject('error: incompatible types');
36+
}
37+
};
38+
reader.readAsDataURL(blob);
39+
});
40+
}
41+
42+
const ContentSelector = (): React.Node => {
43+
const [base64Image, setBase64Image] = React.useState('');
44+
const imageSelector = React.useCallback(async () => {
45+
try {
46+
const uri = await NativeSampleTurboModule.getImageUrl?.();
47+
if (uri != null) {
48+
console.log({uri});
49+
const response = await fetch(uri);
50+
const blob = await response.blob();
51+
setBase64Image(await blobToBase64(blob));
52+
}
53+
} catch (e) {
54+
ToastAndroid.show('' + e, ToastAndroid.LONG);
55+
}
56+
}, []);
57+
58+
return (
59+
<>
60+
<TouchableOpacity onPress={imageSelector}>
61+
<View style={[styles.button, styles.buttonIntent]}>
62+
<RNTesterText>Select Image</RNTesterText>
63+
</View>
64+
</TouchableOpacity>
65+
{base64Image !== '' && (
66+
<Image style={styles.image} source={{uri: base64Image}} />
67+
)}
68+
</>
69+
);
70+
};
71+
72+
class ContentURLAndroidExample extends React.Component<{}, {}> {
73+
render(): React.Node {
74+
return (
75+
<RNTesterPage title={'fetch content:// scheme urls on Android as a Blob'}>
76+
{Platform.OS === 'android' && (
77+
<RNTesterBlock title="Content fetch">
78+
<RNTesterText style={styles.textSeparator}>
79+
Choose content to fetch.
80+
</RNTesterText>
81+
<ContentSelector />
82+
</RNTesterBlock>
83+
)}
84+
</RNTesterPage>
85+
);
86+
}
87+
}
88+
89+
const styles = StyleSheet.create({
90+
textSeparator: {
91+
paddingBottom: 8,
92+
},
93+
button: {
94+
padding: 10,
95+
backgroundColor: '#3B5998',
96+
marginBottom: 10,
97+
},
98+
buttonIntent: {
99+
backgroundColor: '#009688',
100+
},
101+
image: {
102+
width: '100%',
103+
resizeMode: 'cover',
104+
height: '300',
105+
},
106+
});
107+
108+
exports.title = 'ContentURLAndroid';
109+
exports.description = 'Android specific fetch content:// scheme urls as blob.';
110+
exports.examples = [
111+
{
112+
title: 'fetch content:// urls as blob',
113+
render(): React.MixedElement {
114+
return <ContentURLAndroidExample />;
115+
},
116+
},
117+
];

packages/rn-tester/js/utils/RNTesterList.android.js

+5
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ const APIs: Array<RNTesterModuleInfo> = ([
187187
category: 'Basic',
188188
module: require('../examples/AppState/AppStateExample'),
189189
},
190+
{
191+
key: 'ContentURLAndroid',
192+
category: 'Android',
193+
module: require('../examples/ContentURLAndroid/ContentURLAndroid'),
194+
},
190195
{
191196
key: 'URLExample',
192197
category: 'Basic',

0 commit comments

Comments
 (0)