Migrate com.facebook.react.modules.network.RequestBodyUtil to Kotlin (#49584)

Summary:
Migrate com.facebook.react.modules.network.RequestBodyUtil to Kotlin

## Changelog:

[INTERNAL] - Migrate com.facebook.react.modules.network.RequestBodyUtil to Kotlin

Pull Request resolved: https://github.com/facebook/react-native/pull/49584

Test Plan:
```bash
yarn test-android
yarn android
```

Reviewed By: arushikesarwani94

Differential Revision: D69980421

Pulled By: cortinico

fbshipit-source-id: aa66661b2b79afdfd41963e7896b9a01f12af5cb
This commit is contained in:
Mateo Guzmán
2025-02-21 12:45:00 -08:00
committed by Facebook GitHub Bot
parent f85cca0ee5
commit 0aa8af906d
3 changed files with 331 additions and 166 deletions
@@ -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;
}
}
}
@@ -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
}
}
}
@@ -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<Context>()
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<Context>()
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<Context>()
val contentResolver = mock<ContentResolver>()
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<Context>()
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())
}
}