[components] Issue https://github.com/mozilla-mobile/android-components/issues/1325: (lib-fetch-httpurlconnection): A concept-fetch implementation using HttpURLConnection.

This commit is contained in:
Sebastian Kaspari
2018-11-12 20:14:38 +01:00
parent a6d47e1fcc
commit a2794e745a
9 changed files with 269 additions and 30 deletions

View File

@@ -0,0 +1,25 @@
# [Android Components](../../../README.md) > Libraries > Fetch-HttpURLConnection
A [concept-fetch](../../concept/fetch/README.md) implementation using [HttpURLConnection](https://developer.android.com/reference/java/net/HttpURLConnection.html).
This implementation of `concept-fetch` uses [HttpURLConnection](https://developer.android.com/reference/java/net/HttpURLConnection.html) from the standard library of the Android System. Therefore this component has no third-party dependencies and is smaller than other implementations. It's intended use is for apps that have strict APK size constraints.
## Usage
### Setting up the dependency
Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):
```Groovy
implementation "org.mozilla.components:lib-fetch-httpurlconnection:{latest-version}"
```
### Performing requests
See the [concept-fetch documentation](../../concept/fetch/README.md) for generic examples of using the API of components implementing `concept-fetch`.
## License
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/

View File

@@ -0,0 +1,38 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion Config.compileSdkVersion
defaultConfig {
minSdkVersion Config.minSdkVersion
targetSdkVersion Config.targetSdkVersion
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation Deps.kotlin_stdlib
implementation Deps.kotlin_coroutines
implementation project(':concept-fetch')
testImplementation Deps.testing_junit
testImplementation Deps.testing_robolectric
testImplementation Deps.testing_mockito
testImplementation project(':tooling-fetch-tests')
}
apply from: '../../../publish.gradle'
ext.configurePublish(Config.componentsGroupId, archivesBaseName, gradle.componentDescriptions[archivesBaseName])

View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,5 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="mozilla.components.lib.fetch.httpurlconnection" />

View File

@@ -0,0 +1,152 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.lib.fetch.httpurlconnection
import mozilla.components.concept.fetch.Client
import mozilla.components.concept.fetch.Headers
import mozilla.components.concept.fetch.MutableHeaders
import mozilla.components.concept.fetch.Request
import mozilla.components.concept.fetch.Response
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.zip.GZIPInputStream
/**
* [HttpURLConnection] implementation of [Client].
*/
class HttpURLConnectionClient : Client() {
@Throws(IOException::class)
override fun fetch(request: Request): Response {
val connection = (URL(request.url).openConnection() as HttpURLConnection)
connection.setupWith(request)
connection.addHeadersFrom(request, defaultHeaders = defaultHeaders)
connection.addBodyFrom(request)
return connection.toResponse()
}
}
private fun HttpURLConnection.addBodyFrom(request: Request) {
if (request.body == null) {
return
}
request.body?.let { body ->
doOutput = true
body.useStream { inStream ->
outputStream.use { outStream ->
inStream
.buffered()
.copyTo(outStream)
outStream.flush()
}
}
}
}
private fun HttpURLConnection.setupWith(request: Request) {
requestMethod = request.method.name
instanceFollowRedirects = request.redirect == Request.Redirect.FOLLOW
request.connectTimeout?.let { (timeout, unit) ->
connectTimeout = unit.toMillis(timeout).toInt()
}
request.readTimeout?.let { (timeout, unit) ->
readTimeout = unit.toMillis(timeout).toInt()
}
}
private fun HttpURLConnection.addHeadersFrom(request: Request, defaultHeaders: Headers) {
defaultHeaders.filter { header ->
request.headers?.contains(header.name) != true
}.forEach { header ->
setRequestProperty(header.name, header.value)
}
request.headers?.forEach { header ->
addRequestProperty(header.name, header.value)
}
}
private fun HttpURLConnection.toResponse(): Response {
return Response(
url.toString(),
responseCode,
translateHeaders(this),
createBody(this)
)
}
private fun translateHeaders(connection: HttpURLConnection): Headers {
val headers = MutableHeaders()
var index = 0
while (connection.getHeaderField(index) != null) {
val name = connection.getHeaderFieldKey(index)
if (name == null) {
index++
continue
}
val value = connection.getHeaderField(index)
headers.append(name, value)
index++
}
return headers
}
private fun createBody(connection: HttpURLConnection): Response.Body {
val gzipped = connection.contentEncoding == "gzip"
withFileNotFoundExceptionIgnored {
return HttpUrlConnectionBody(
connection,
connection.inputStream,
gzipped
)
}
withFileNotFoundExceptionIgnored {
return HttpUrlConnectionBody(
connection,
connection.errorStream,
gzipped
)
}
return EmptyBody()
}
private class EmptyBody : Response.Body("".byteInputStream())
private class HttpUrlConnectionBody(
private val connection: HttpURLConnection,
stream: InputStream,
gzipped: Boolean
) : Response.Body(if (gzipped) GZIPInputStream(stream) else stream) {
override fun close() {
super.close()
connection.disconnect()
}
}
private inline fun withFileNotFoundExceptionIgnored(block: () -> Unit) {
try {
block()
} catch (e: FileNotFoundException) {
// Ignore
}
}

View File

@@ -0,0 +1,21 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.lib.fetch.httpurlconnection
import mozilla.components.concept.fetch.Client
import org.junit.Assert.assertTrue
import org.junit.Test
class HttpUrlConnectionFetchTestCases : mozilla.components.tooling.fetch.tests.FetchTestCases() {
override fun createNewClient(): Client = HttpURLConnectionClient()
// Inherits test methods from generic test suite base class
@Test
fun `Client instance`() {
// We need at least one test case defined here so that this is recognized as test class.
assertTrue(createNewClient() is HttpURLConnectionClient)
}
}