diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java deleted file mode 100644 index 4394fbb2dc2..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.modules.network; - -import android.content.Context; -import android.net.Uri; -import android.util.Base64; -import androidx.annotation.Nullable; -import com.facebook.common.logging.FLog; -import com.facebook.react.common.ReactConstants; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URL; -import java.nio.channels.Channels; -import java.nio.channels.ReadableByteChannel; -import java.util.zip.GZIPOutputStream; -import okhttp3.MediaType; -import okhttp3.RequestBody; -import okio.BufferedSink; -import okio.ByteString; -import okio.Okio; -import okio.Source; - -/** - * Helper class that provides the necessary methods for creating the RequestBody from a file - * specification, such as a contentUri. - */ -/*package*/ class RequestBodyUtil { - - private static final String CONTENT_ENCODING_GZIP = "gzip"; - private static final String NAME = "RequestBodyUtil"; - private static final String TEMP_FILE_SUFFIX = "temp"; - - /** Returns whether encode type indicates the body needs to be gzip-ed. */ - public static boolean isGzipEncoding(@Nullable final String encodingType) { - return CONTENT_ENCODING_GZIP.equalsIgnoreCase(encodingType); - } - - /** - * Returns the input stream for a file given by its contentUri. Returns null if the file has not - * been found or if an error as occurred. - */ - public static @Nullable InputStream getFileInputStream( - Context context, String fileContentUriStr) { - try { - Uri fileContentUri = Uri.parse(fileContentUriStr); - - if (fileContentUri.getScheme().startsWith("http")) { - return getDownloadFileInputStream(context, fileContentUri); - } - - if (fileContentUriStr.startsWith("data:")) { - byte[] decodedDataUrString = Base64.decode(fileContentUriStr.split(",")[1], Base64.DEFAULT); - return new ByteArrayInputStream(decodedDataUrString); - } - - return context.getContentResolver().openInputStream(fileContentUri); - } catch (Exception e) { - FLog.e(ReactConstants.TAG, "Could not retrieve file for contentUri " + fileContentUriStr, e); - return null; - } - } - - /** - * Download and cache a file locally. This should be used when document picker returns a URI that - * points to a file on the network. Returns input stream for the downloaded file. - */ - private static InputStream getDownloadFileInputStream(Context context, Uri uri) - throws IOException { - final File outputDir = context.getApplicationContext().getCacheDir(); - final File file = File.createTempFile(NAME, TEMP_FILE_SUFFIX, outputDir); - file.deleteOnExit(); - - final URL url = new URL(uri.toString()); - try (FileOutputStream stream = new FileOutputStream(file); - InputStream is = url.openStream(); - ReadableByteChannel channel = Channels.newChannel(is)) { - stream.getChannel().transferFrom(channel, 0, Long.MAX_VALUE); - return new FileInputStream(file); - } - } - - /** Creates a RequestBody from a mediaType and gzip-ed body string */ - public static @Nullable RequestBody createGzip(final MediaType mediaType, final String body) { - ByteArrayOutputStream gzipByteArrayOutputStream = new ByteArrayOutputStream(); - try { - OutputStream gzipOutputStream = new GZIPOutputStream(gzipByteArrayOutputStream); - gzipOutputStream.write(body.getBytes()); - gzipOutputStream.close(); - } catch (IOException e) { - return null; - } - return RequestBody.create(mediaType, gzipByteArrayOutputStream.toByteArray()); - } - - /** - * Reference: - * https://github.com/square/okhttp/blob/8c8c3dbcfa91e28de2e13975ec414e07f153fde4/okhttp/src/commonMain/kotlin/okhttp3/internal/-UtilCommon.kt#L281-L288 - * Checked exceptions will be ignored - */ - private static void closeQuietly(Source source) { - try { - source.close(); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - // noop. - } - } - - /** Creates a RequestBody from a mediaType and inputStream given. */ - public static RequestBody create(final MediaType mediaType, final InputStream inputStream) { - return new RequestBody() { - @Override - public MediaType contentType() { - return mediaType; - } - - @Override - public long contentLength() { - try { - return inputStream.available(); - } catch (IOException e) { - return 0; - } - } - - @Override - public void writeTo(BufferedSink sink) throws IOException { - Source source = null; - try { - source = Okio.source(inputStream); - sink.writeAll(source); - } finally { - closeQuietly(source); - } - } - }; - } - - /** Creates a ProgressRequestBody that can be used for showing uploading progress */ - public static ProgressRequestBody createProgressRequest( - RequestBody requestBody, ProgressListener listener) { - return new ProgressRequestBody(requestBody, listener); - } - - /** Creates a empty RequestBody if required by the http method spec, otherwise use null */ - public static RequestBody getEmptyBody(String method) { - if (method.equals("POST") || method.equals("PUT") || method.equals("PATCH")) { - return RequestBody.create(null, ByteString.EMPTY); - } else { - return null; - } - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.kt new file mode 100644 index 00000000000..367b6556375 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.kt @@ -0,0 +1,179 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +@file:Suppress("DEPRECATION_ERROR") // Conflicting okio versions + +package com.facebook.react.modules.network + +import android.content.Context +import android.net.Uri +import android.util.Base64 +import com.facebook.common.logging.FLog +import com.facebook.react.common.ReactConstants +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.net.URL +import java.nio.channels.Channels +import java.util.zip.GZIPOutputStream +import okhttp3.MediaType +import okhttp3.RequestBody +import okio.BufferedSink +import okio.ByteString +import okio.Okio +import okio.Source + +/** + * Helper class that provides the necessary methods for creating the [RequestBody] from a file + * specification, such as a contentUri. + */ +internal object RequestBodyUtil { + private const val CONTENT_ENCODING_GZIP = "gzip" + private const val NAME = "RequestBodyUtil" + private const val TEMP_FILE_SUFFIX = "temp" + + /** Returns whether encode type indicates the body needs to be gzip-ed. */ + @JvmStatic + fun isGzipEncoding(encodingType: String?): Boolean { + return CONTENT_ENCODING_GZIP.equals(encodingType, ignoreCase = true) + } + + /** + * Returns the input stream for a file given by its contentUri. Returns null if the file has not + * been found or if an error as occurred. + */ + @JvmStatic + fun getFileInputStream(context: Context, fileContentUriStr: String): InputStream? { + try { + val fileContentUri = Uri.parse(fileContentUriStr) + + if (fileContentUri.scheme?.startsWith("http") == true) { + return getDownloadFileInputStream(context, fileContentUri) + } + + if (fileContentUriStr.startsWith("data:")) { + val decodedDataUrString = + Base64.decode( + fileContentUriStr + .split(",".toRegex()) + .dropLastWhile { it.isEmpty() } + .toTypedArray()[1], + Base64.DEFAULT) + return ByteArrayInputStream(decodedDataUrString) + } + + return context.contentResolver.openInputStream(fileContentUri) + } catch (e: Exception) { + FLog.e(ReactConstants.TAG, "Could not retrieve file for contentUri $fileContentUriStr", e) + return null + } + } + + /** + * Download and cache a file locally. This should be used when document picker returns a URI that + * points to a file on the network. Returns input stream for the downloaded file. + */ + @Throws(IOException::class) + private fun getDownloadFileInputStream(context: Context, uri: Uri): InputStream { + val outputDir = context.applicationContext.cacheDir + val file = File.createTempFile(NAME, TEMP_FILE_SUFFIX, outputDir) + file.deleteOnExit() + + val url = URL(uri.toString()) + FileOutputStream(file).use { stream -> + url.openStream().use { `is` -> + Channels.newChannel(`is`).use { channel -> + stream.channel.transferFrom(channel, 0, Long.MAX_VALUE) + return FileInputStream(file) + } + } + } + } + + /** Creates a [RequestBody] from a mediaType and gzip-ed body string. */ + @JvmStatic + fun createGzip(mediaType: MediaType?, body: String): RequestBody? { + val gzipByteArrayOutputStream = ByteArrayOutputStream() + try { + val gzipOutputStream: OutputStream = GZIPOutputStream(gzipByteArrayOutputStream) + gzipOutputStream.write(body.toByteArray()) + gzipOutputStream.close() + } catch (e: IOException) { + return null + } + @Suppress("DEPRECATION") + return RequestBody.create(mediaType, gzipByteArrayOutputStream.toByteArray()) + } + + /** + * Reference: + * https://github.com/square/okhttp/blob/8c8c3dbcfa91e28de2e13975ec414e07f153fde4/okhttp/src/commonMain/kotlin/okhttp3/internal/-UtilCommon.kt#L281-L288 + * Checked exceptions will be ignored + */ + private fun closeQuietly(source: Source) { + try { + source.close() + } catch (e: RuntimeException) { + throw e + } catch (e: Exception) { + // no-op + } + } + + /** Creates a [RequestBody] from a mediaType and inputStream given. */ + @JvmStatic + fun create(mediaType: MediaType?, inputStream: InputStream): RequestBody { + return object : RequestBody() { + override fun contentType(): MediaType? { + return mediaType + } + + override fun contentLength(): Long { + return try { + inputStream.available().toLong() + } catch (e: IOException) { + 0 + } + } + + @Throws(IOException::class) + override fun writeTo(sink: BufferedSink) { + var source: Source? = null + try { + source = Okio.source(inputStream) + sink.writeAll(source) + } finally { + source?.let { closeQuietly(it) } + } + } + } + } + + /** Creates a [ProgressRequestBody] that can be used for showing uploading progress. */ + @JvmStatic + fun createProgressRequest( + requestBody: RequestBody?, + listener: ProgressListener? + ): ProgressRequestBody { + return ProgressRequestBody(requestBody, listener) + } + + /** Creates an empty [RequestBody] if required by the http method spec, otherwise use null. */ + @JvmStatic + fun getEmptyBody(method: String): RequestBody? { + return if (method == "POST" || method == "PUT" || method == "PATCH") { + @Suppress("DEPRECATION") RequestBody.create(null, ByteString.EMPTY) + } else { + null + } + } +} diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/RequestBodyUtilTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/RequestBodyUtilTest.kt new file mode 100644 index 00000000000..c20cdb4bf3a --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/RequestBodyUtilTest.kt @@ -0,0 +1,152 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// Conflicting okhttp versions +@file:Suppress("DEPRECATION_ERROR") + +package com.facebook.react.modules.network + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import java.io.ByteArrayInputStream +import java.util.zip.GZIPInputStream +import okhttp3.MediaType +import okio.Buffer +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class RequestBodyUtilTest { + + @Test + fun testIsGzipEncoding() { + assertThat(RequestBodyUtil.isGzipEncoding("gzip")).isTrue() + assertThat(RequestBodyUtil.isGzipEncoding("GzIp")).isTrue() + assertThat(RequestBodyUtil.isGzipEncoding("identity")).isFalse() + assertThat(RequestBodyUtil.isGzipEncoding(null)).isFalse() + } + + @Test + fun testGetFileInputStreamWithHttpUri() { + val context = mock() + val fileUri = "http://example.com/file" + + // Since getDownloadFileInputStream is private and not mocked, it will throw an exception. + val result = RequestBodyUtil.getFileInputStream(context, fileUri) + + assertThat(result).isNull() // Expected null due to exception handling + } + + @Test + fun testGetFileInputStreamWithDataUri() { + val context = mock() + val fileUri = "data:text/plain;base64,SGVsbG8gV29ybGQ=" + + val result = RequestBodyUtil.getFileInputStream(context, fileUri) + + assertThat("Hello World").isEqualTo(result?.bufferedReader()?.use { it.readText() }) + } + + @Test + fun testGetFileInputStreamWithContentUri() { + val context = mock() + val contentResolver = mock() + whenever(context.contentResolver).thenReturn(contentResolver) + + val fileUri = "content://com.example.provider/file" + val testInputStream = ByteArrayInputStream("Sample Content".toByteArray()) + + whenever(contentResolver.openInputStream(Uri.parse(fileUri))).thenReturn(testInputStream) + + val result = RequestBodyUtil.getFileInputStream(context, fileUri) + + assertThat("Sample Content").isEqualTo(result?.bufferedReader()?.use { it.readText() }) + } + + @Test + fun testGetFileInputStreamWithInvalidUri() { + val context = mock() + val invalidUri = "invalid-uri" + + val result = RequestBodyUtil.getFileInputStream(context, invalidUri) + + assertThat(result).isNull() // Expected null due to exception handling + } + + @Test + fun testCreateGzipWithValidInput() { + val mediaType = checkNotNull(MediaType.parse("text/plain")) + val input = "Hello Gzip" + + val requestBody = RequestBodyUtil.createGzip(mediaType, input) + + checkNotNull(requestBody) + + val buffer = Buffer() + requestBody.writeTo(buffer) + + val gzipInputStream = GZIPInputStream(ByteArrayInputStream(buffer.readByteArray())) + val result = gzipInputStream.bufferedReader().use { it.readText() } + + assertThat(input).isEqualTo(result) + } + + @Test + fun testCreateGzipWithEmptyInput() { + val mediaType = checkNotNull(MediaType.parse("text/plain")) + val input = "" + + val requestBody = RequestBodyUtil.createGzip(mediaType, input) + + checkNotNull(requestBody) + + val buffer = Buffer() + requestBody.writeTo(buffer) + + val gzipInputStream = GZIPInputStream(ByteArrayInputStream(buffer.readByteArray())) + val result = gzipInputStream.bufferedReader().use { it.readText() } + + assertThat(input).isEqualTo(result) + } + + @Test + fun testCreateGzipWithNullInput() { + val mediaType = checkNotNull(MediaType.parse("text/plain")) + + val requestBody = RequestBodyUtil.createGzip(mediaType, "") + + checkNotNull(requestBody) + + val buffer = Buffer() + requestBody.writeTo(buffer) + + val gzipInputStream = GZIPInputStream(ByteArrayInputStream(buffer.readByteArray())) + val result = gzipInputStream.bufferedReader().use { it.readText() } + + assertThat(result).isEmpty() + } + + @Test + fun testCreateWithInputStream() { + val mediaType = checkNotNull(MediaType.parse("text/plain")) + val inputStream = ByteArrayInputStream("Test InputStream".toByteArray()) + + val requestBody = RequestBodyUtil.create(mediaType, inputStream) + + checkNotNull(requestBody) + + val buffer = Buffer() + requestBody.writeTo(buffer) + + assertThat("Test InputStream").isEqualTo(buffer.readUtf8()) + } +}