├── Hoodies-Network ├── consumer-rules.pro ├── .gitignore ├── libs │ ├── http-2.2.1.jar │ └── sun-common-server.jar ├── src │ ├── androidTest │ │ ├── assets │ │ │ └── drawables │ │ │ │ └── orangeimage.png │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── gap │ │ │ │ └── hoodies_network │ │ │ │ ├── ResponseDeliveryInstant.kt │ │ │ │ ├── mockwebserver │ │ │ │ ├── Get.kt │ │ │ │ ├── Put.kt │ │ │ │ ├── Patch.kt │ │ │ │ ├── Post.kt │ │ │ │ ├── Delete.kt │ │ │ │ ├── Options.kt │ │ │ │ ├── EchoDelay.kt │ │ │ │ ├── WantsKeyHeader.kt │ │ │ │ ├── CookieFactory.kt │ │ │ │ ├── CookieInspector.kt │ │ │ │ ├── Weather.kt │ │ │ │ ├── ImageReturn.kt │ │ │ │ ├── ServerManager.kt │ │ │ │ ├── HttpBinClone.kt │ │ │ │ └── Html.kt │ │ │ │ ├── testObjects │ │ │ │ ├── CallResponse.kt │ │ │ │ ├── Headers.kt │ │ │ │ ├── EmptyEncryptionDecryptionInterceptor.kt │ │ │ │ ├── testInterceptor.kt │ │ │ │ ├── WeatherResponse.kt │ │ │ │ └── EncryptionDecryptionInterceptor.kt │ │ │ │ ├── NetworkConnectionTest.kt │ │ │ │ ├── ResponseTest.kt │ │ │ │ ├── FormUrlEncodedRequestTest.kt │ │ │ │ ├── HeaderTest.kt │ │ │ │ ├── MultipleRequestTest.kt │ │ │ │ ├── EncryptionDecryptionTest.kt │ │ │ │ ├── UrlResolverTest.kt │ │ │ │ ├── RetryTest.kt │ │ │ │ ├── FileUploadRequestTests.kt │ │ │ │ ├── InterceptorTests.kt │ │ │ │ ├── MultiRequestTest.kt │ │ │ │ └── SocketTimeOutTest.kt │ │ └── AndroidManifest.xml │ └── main │ │ ├── kotlin │ │ └── com │ │ │ └── gap │ │ │ └── hoodies_network │ │ │ ├── cache │ │ │ ├── configuration │ │ │ │ ├── CacheDisabled.kt │ │ │ │ ├── CacheConfiguration.kt │ │ │ │ └── CacheEnabled.kt │ │ │ ├── persistentstorage │ │ │ │ ├── CacheDatabase.kt │ │ │ │ ├── CachedData.kt │ │ │ │ └── CacheDao.kt │ │ │ └── EncryptedCache.kt │ │ │ ├── cookies │ │ │ ├── PersistentCookieJar.kt │ │ │ ├── persistentstorage │ │ │ │ ├── EncryptedCookieDatabase.kt │ │ │ │ ├── EncryptedCookie.kt │ │ │ │ ├── EncryptedCookieDao.kt │ │ │ │ ├── PersistentCookieStore.kt │ │ │ │ └── EncryptedDaoWrapperForCookies.kt │ │ │ └── CookieJar.kt │ │ │ ├── core │ │ │ ├── HoodiesNetworkError.kt │ │ │ ├── HoodiesNetworkErrorDescription.kt │ │ │ ├── Result.kt │ │ │ └── Response.kt │ │ │ ├── connection │ │ │ ├── Network.kt │ │ │ └── queue │ │ │ │ ├── NetworkHandler.kt │ │ │ │ ├── QueueHandler.kt │ │ │ │ └── RequestQueue.kt │ │ │ ├── request │ │ │ ├── RetryableCancellableMutableRequest.kt │ │ │ ├── CancellableMutableRequest.kt │ │ │ ├── FileRequest.kt │ │ │ ├── json │ │ │ │ ├── JsonRequest.kt │ │ │ │ ├── JsonArrayRequest.kt │ │ │ │ └── JsonObjectRequest.kt │ │ │ ├── query │ │ │ │ ├── UrlQueryParamEncodedRequest.kt │ │ │ │ └── UrlQueryParamRequest.kt │ │ │ ├── FormUrlEncodedRequest.kt │ │ │ ├── FileUploadRequest.kt │ │ │ ├── StringRequest.kt │ │ │ └── Request.kt │ │ │ ├── delivery │ │ │ ├── ResponseDelivery.kt │ │ │ └── ResponseDeliveryExecutor.kt │ │ │ ├── utils │ │ │ ├── Generated.java │ │ │ └── NetworkHelper.kt │ │ │ ├── config │ │ │ ├── HeaderKeyValues.kt │ │ │ ├── IsOnline.kt │ │ │ ├── HttpClientConfig.kt │ │ │ └── UrlResolver.kt │ │ │ ├── header │ │ │ ├── Header.kt │ │ │ └── HttpHeaderParser.kt │ │ │ ├── interceptor │ │ │ ├── Interceptor.kt │ │ │ └── EncryptionDecryptionInterceptor.kt │ │ │ ├── mockwebserver │ │ │ ├── MockWebServerManager.kt │ │ │ ├── WebServerHandler.kt │ │ │ └── HttpCall.kt │ │ │ └── keystore │ │ │ └── CacheKeyManager.kt │ │ └── AndroidManifest.xml ├── build.gradle └── proguard-rules.pro ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── settings.gradle ├── examples ├── CallResponse.kt ├── Headers.kt ├── WeatherResponse.kt ├── PutRequestViewModel.kt ├── GetHtmlViewModel.kt ├── DeleteRequestViewModel.kt ├── GetRawHtmlViewModel.kt ├── JsonObjectRequestViewModel.kt ├── GetJsonViewModel.kt ├── GetRawJsonViewModel.kt ├── ImageRequestViewModel.kt ├── JsonArrayRequestViewModel.kt ├── SendGetRequestWithParametersViewModel.kt ├── SendGetRequestWithUrlEncodedParametersViewModel.kt ├── SettingSessionInterceptorsAndTimeout.kt ├── benchmarkTesting-benchmarkData.json └── HTTPMethodsTYPED ├── .github ├── workflows │ ├── run_tests.yml │ ├── lint_report.yml │ ├── release_build.yml │ └── coverage.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── LICENSE.md ├── .gitignore ├── jacoco.gradle └── gradlew /Hoodies-Network/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Hoodies-Network/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | temp/b.txt -------------------------------------------------------------------------------- /Hoodies-Network/libs/http-2.2.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gapinc/hoodies-network/HEAD/Hoodies-Network/libs/http-2.2.1.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gapinc/hoodies-network/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /Hoodies-Network/libs/sun-common-server.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gapinc/hoodies-network/HEAD/Hoodies-Network/libs/sun-common-server.jar -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/assets/drawables/orangeimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gapinc/hoodies-network/HEAD/Hoodies-Network/src/androidTest/assets/drawables/orangeimage.png -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/cache/configuration/CacheDisabled.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.cache.configuration 2 | 3 | class CacheDisabled: CacheConfiguration -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/cache/configuration/CacheConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.cache.configuration 2 | 3 | interface CacheConfiguration { 4 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | android.enableJetifier=true 2 | android.useAndroidX=true 3 | kapt.use.worker.api=false 4 | buildNumber= 5 | artifactory_username= 6 | artifactory_password= 7 | org.gradle.jvmargs=-Xmx4096M 8 | android.disableAutomaticComponentCreation=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Nov 01 15:45:44 EDT 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 3 | repositories { 4 | google() 5 | mavenLocal() 6 | mavenCentral() 7 | } 8 | } 9 | 10 | rootProject.name = "Hoodies-Network" 11 | include ':Hoodies-Network' -------------------------------------------------------------------------------- /examples/CallResponse.kt: -------------------------------------------------------------------------------- 1 | import com.google.gson.annotations.SerializedName 2 | 3 | data class CallResponse( 4 | @field:SerializedName("headers") val headers: Headers, 5 | @field:SerializedName("origin") val origin: String, 6 | @field:SerializedName("data") val data: String, 7 | @field:SerializedName("url") val url: String, 8 | ) -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/ResponseDeliveryInstant.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network 2 | 3 | import com.gap.hoodies_network.delivery.ResponseDeliveryExecutor 4 | import java.util.concurrent.Executor 5 | 6 | class ResponseDeliveryInstant : ResponseDeliveryExecutor( 7 | Executor { obj: Runnable -> obj.run() } 8 | ) 9 | -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/mockwebserver/Get.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.mockwebserver 2 | 3 | 4 | class Get : WebServerHandler() { 5 | override fun handleRequest(call: HttpCall) { 6 | get { 7 | call.respond(200, HttpBinClone().handleRequest(call)) 8 | } 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/mockwebserver/Put.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.mockwebserver 2 | 3 | 4 | class Put : WebServerHandler() { 5 | override fun handleRequest(call: HttpCall) { 6 | put { 7 | call.respond(200, HttpBinClone().handleRequest(call)) 8 | } 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/mockwebserver/Patch.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.mockwebserver 2 | 3 | 4 | class Patch : WebServerHandler() { 5 | override fun handleRequest(call: HttpCall) { 6 | patch { 7 | call.respond(200, HttpBinClone().handleRequest(call)) 8 | } 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/mockwebserver/Post.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.mockwebserver 2 | 3 | 4 | class Post : WebServerHandler() { 5 | override fun handleRequest(call: HttpCall) { 6 | post { 7 | call.respond(200, HttpBinClone().handleRequest(call)) 8 | } 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/mockwebserver/Delete.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.mockwebserver 2 | 3 | 4 | class Delete : WebServerHandler() { 5 | override fun handleRequest(call: HttpCall) { 6 | delete { 7 | call.respond(200, HttpBinClone().handleRequest(call)) 8 | } 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/mockwebserver/Options.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.mockwebserver 2 | 3 | 4 | class Options : WebServerHandler() { 5 | override fun handleRequest(call: HttpCall) { 6 | options { 7 | call.respond(200, HttpBinClone().handleRequest(call)) 8 | } 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/cookies/PersistentCookieJar.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.cookies 2 | 3 | import android.content.Context 4 | import com.gap.hoodies_network.cookies.persistentstorage.PersistentCookieStore 5 | 6 | class PersistentCookieJar(instanceName: String, context: Context) : CookieJar(PersistentCookieStore(instanceName, context)) -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/core/HoodiesNetworkError.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.core 2 | 3 | /** 4 | * Exception style class encapsulating Gap Network errors 5 | */ 6 | data class HoodiesNetworkError( 7 | override val message: String?, 8 | var code : Int, 9 | override var cause: Throwable? = null 10 | ) : Exception(message) 11 | -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/cache/configuration/CacheEnabled.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.cache.configuration 2 | 3 | import android.content.Context 4 | import java.time.Duration 5 | 6 | class CacheEnabled( 7 | val staleDataThreshold: Duration = Duration.ofHours(1), 8 | val encryptionEnabled: Boolean = false, 9 | val applicationContext: Context 10 | ) : CacheConfiguration -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/connection/Network.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.connection 2 | 3 | import com.gap.hoodies_network.core.HoodiesNetworkError 4 | import com.gap.hoodies_network.core.Response 5 | import com.gap.hoodies_network.request.Request 6 | 7 | interface Network { 8 | @Throws(HoodiesNetworkError::class) 9 | fun executeRequest(request: Request): Response 10 | } 11 | -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/core/HoodiesNetworkErrorDescription.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.core 2 | 3 | const val UNKNOWN_ERROR_CODE = -1 4 | const val OUT_OF_MEMORY_ERROR_CODE = -2 5 | const val JSON_SYNTAX_ERROR_CODE = -3 6 | const val EXCEPTION_ERROR_CODE = -4 7 | const val UNSUPPORTED_ENCODING_ERROR_CODE = - 5 8 | const val JSON_ERROR_CODE = - 6 9 | const val NULL_POINTER_ERROR_CODE = -7 10 | -------------------------------------------------------------------------------- /examples/Headers.kt: -------------------------------------------------------------------------------- 1 | import com.google.gson.annotations.SerializedName 2 | 3 | data class Headers( 4 | @field:SerializedName("Accept") val accept: String, 5 | @field:SerializedName("Accept-Encoding") val acceptEncoding: String, 6 | @field:SerializedName("Postman-Token") val postmanToken: String, 7 | @field:SerializedName("User-Agent") val userAgent: String, 8 | @field:SerializedName("X-Amzn-Trace-Id") val xAmznTraceId: String, 9 | ) -------------------------------------------------------------------------------- /Hoodies-Network/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/testObjects/CallResponse.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.testObjects 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class CallResponse( 6 | @field:SerializedName("headers") val headers: Headers, 7 | @field:SerializedName("origin") val origin: String, 8 | @field:SerializedName("data") val data: String, 9 | @field:SerializedName("url") val url: String 10 | ) -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/request/RetryableCancellableMutableRequest.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.request 2 | 3 | import com.gap.hoodies_network.connection.queue.RequestQueue 4 | 5 | class RetryableCancellableMutableRequest( 6 | request: Request 7 | ) : CancellableMutableRequest(request) { 8 | fun retryRequest() { 9 | request.retryingRequest = true 10 | RequestQueue.instance?.enqueue(request) 11 | } 12 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/cache/persistentstorage/CacheDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.cache.persistentstorage 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.gap.hoodies_network.utils.Generated 6 | 7 | @Generated 8 | @Database(entities = [CachedData::class], version = 1, exportSchema = false) 9 | abstract class CacheDatabase : RoomDatabase() { 10 | abstract fun cacheDao(): CacheDao 11 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/core/Result.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.core 2 | 3 | import com.gap.hoodies_network.header.Header 4 | 5 | /** 6 | * Result class to handle Success and Failure of requests 7 | */ 8 | sealed class Result 9 | 10 | data class Success(val value: T, val headers: List
? = null, val url: String? = null) : Result() 11 | data class Failure(val reason: E) : Result() -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/delivery/ResponseDelivery.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.delivery 2 | 3 | import com.gap.hoodies_network.core.Response 4 | import com.gap.hoodies_network.core.HoodiesNetworkError 5 | import com.gap.hoodies_network.request.Request 6 | 7 | interface ResponseDelivery { 8 | fun postResponse(request: Request, response: Response) 9 | 10 | fun postError(request: Request, error: HoodiesNetworkError) 11 | } 12 | -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/request/CancellableMutableRequest.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.request 2 | 3 | import com.gap.hoodies_network.core.HoodiesNetworkError 4 | 5 | open class CancellableMutableRequest( 6 | val request: Request 7 | ) { 8 | fun cancelRequest(result: com.gap.hoodies_network.core.Result<*, HoodiesNetworkError>) { 9 | request.requestIsCancelled = true 10 | request.cancellationResult = result 11 | } 12 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/mockwebserver/EchoDelay.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.mockwebserver 2 | 3 | 4 | class EchoDelay : WebServerHandler() { 5 | override fun handleRequest(call: HttpCall) { 6 | get { 7 | val delayLength = call.httpExchange.requestURI.toString().split("/").last() 8 | 9 | Thread.sleep(delayLength.toLong() * 1000L) 10 | 11 | call.respond(200, "{\"delay\":\"$delayLength\"}") 12 | } 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/cookies/persistentstorage/EncryptedCookieDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.cookies.persistentstorage 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.gap.hoodies_network.utils.Generated 6 | 7 | @Generated 8 | @Database(entities = [EncryptedCookie::class], version = 1, exportSchema = false) 9 | abstract class EncryptedCookieDatabase : RoomDatabase() { 10 | abstract fun encryptedCookieDao(): EncryptedCookieDao 11 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/mockwebserver/WantsKeyHeader.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.mockwebserver 2 | 3 | 4 | class WantsKeyHeader : WebServerHandler() { 5 | override fun handleRequest(call: HttpCall) { 6 | get { 7 | val key = call.getHeaders().getFirst("Key") 8 | 9 | if (key != "20") { 10 | call.respond(403, "Unauthorized") 11 | } else { 12 | call.respond(200, "Success!") 13 | } 14 | } 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /.github/workflows/run_tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | android_tests: 8 | runs-on: macos-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Set up JDK 11 12 | uses: actions/setup-java@v1 13 | with: 14 | java-version: 11 15 | 16 | - name: Instrumentation Tests 17 | uses: reactivecircus/android-emulator-runner@v2 18 | with: 19 | arch: 'x86_64' 20 | api-level: 30 21 | script: ./gradlew connectedAndroidTest 22 | -------------------------------------------------------------------------------- /.github/workflows/lint_report.yml: -------------------------------------------------------------------------------- 1 | name: Android Lint 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | android-lint: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: set up JDK 11 12 | uses: actions/setup-java@v1 13 | with: 14 | java-version: 11 15 | - run: ./gradlew lint 16 | - uses: github/codeql-action/upload-sarif@v2 17 | if: success() || failure() 18 | with: 19 | sarif_file: app/build/reports/lint-results-debug.sarif 20 | category: lint -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/testObjects/Headers.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.testObjects 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class Headers( 6 | @field:SerializedName("Accept") val accept: String, 7 | @field:SerializedName("Accept-Encoding") val acceptEncoding: String, 8 | @field:SerializedName("Postman-Token") val postmanToken: String, 9 | @field:SerializedName("User-Agent") val userAgent: String, 10 | @field:SerializedName("X-Amzn-Trace-Id") val xAmznTraceId: String, 11 | ) -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/utils/Generated.java: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.utils; 2 | 3 | import static java.lang.annotation.ElementType.CONSTRUCTOR; 4 | import static java.lang.annotation.ElementType.METHOD; 5 | import static java.lang.annotation.ElementType.TYPE; 6 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 7 | 8 | import java.lang.annotation.Documented; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.Target; 11 | 12 | @Documented 13 | @Retention(RUNTIME) 14 | @Target({TYPE, METHOD, CONSTRUCTOR}) 15 | public @interface Generated { 16 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/cookies/persistentstorage/EncryptedCookie.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.cookies.persistentstorage 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import com.gap.hoodies_network.utils.Generated 7 | 8 | @Entity 9 | @Generated 10 | data class EncryptedCookie( 11 | @PrimaryKey(autoGenerate = true) val id: Int, 12 | @ColumnInfo(name = "host") val host: String?, 13 | @ColumnInfo(name = "cookie") val cookie: String?, 14 | @ColumnInfo(name = "iv") val iv: String?, 15 | @ColumnInfo(name = "hash") val hash: Int, 16 | ) -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/cache/persistentstorage/CachedData.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.cache.persistentstorage 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import com.gap.hoodies_network.utils.Generated 7 | 8 | @Entity 9 | @Generated 10 | data class CachedData( 11 | @PrimaryKey(autoGenerate = true) val id: Int, 12 | @ColumnInfo(name = "url") val url: String, 13 | @ColumnInfo(name = "bodyHash") val bodyHash: Int, 14 | @ColumnInfo(name = "cachedAt") val cachedAt: Long, 15 | @ColumnInfo(name = "data") var data: String, 16 | @ColumnInfo(name = "iv") var iv: String?, 17 | ) -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/mockwebserver/CookieFactory.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.mockwebserver 2 | 3 | import com.sun.net.httpserver.Headers 4 | import org.json.JSONObject 5 | 6 | 7 | class CookieFactory : WebServerHandler() { 8 | override fun handleRequest(call: HttpCall) { 9 | post { 10 | val body = JSONObject(call.getBodyString()) 11 | 12 | val respHeaders = Headers() 13 | for (key in body.keys()) { 14 | respHeaders.add("Set-Cookie", "$key=${body.get(key)}; SameSite=Strict; HttpOnly") 15 | } 16 | 17 | call.setResponseHeaders(respHeaders) 18 | 19 | call.respond(200, "{}") 20 | } 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/mockwebserver/CookieInspector.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.mockwebserver 2 | 3 | import org.json.JSONObject 4 | 5 | class CookieInspector : WebServerHandler() { 6 | override fun handleRequest(call: HttpCall) { 7 | post { 8 | val response = JSONObject() 9 | 10 | call.getHeaders()["Cookie"]?.forEach { cookieLine -> 11 | cookieLine.split("; ").forEach { singleCookie -> 12 | val cookieParts = singleCookie.split("=") 13 | response.put(cookieParts[0], cookieParts[1]) 14 | } 15 | } 16 | 17 | call.respond(200, response.toString()) 18 | } 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/NetworkConnectionTest.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network 2 | 3 | import android.content.Context 4 | import androidx.test.core.app.ApplicationProvider 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import org.junit.Assert 7 | import org.junit.Before 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | 11 | @RunWith(AndroidJUnit4::class) 12 | class NetworkConnectionTest { 13 | private lateinit var context: Context 14 | 15 | @Before 16 | fun setUp() { 17 | context = ApplicationProvider.getApplicationContext() 18 | } 19 | 20 | @Test 21 | fun isOnline() { 22 | Assert.assertEquals(true, com.gap.hoodies_network.config.isOnline(context)) 23 | } 24 | } -------------------------------------------------------------------------------- /.github/workflows/release_build.yml: -------------------------------------------------------------------------------- 1 | name: Release Build and Publish 2 | 3 | on: 4 | push: 5 | branches: [ release/** ] 6 | 7 | jobs: 8 | build_and_publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | 14 | - name: Set up JDK 11 15 | uses: actions/setup-java@v1 16 | with: 17 | java-version: 11 18 | 19 | - name: Grant Permission for gradlew to execute 20 | run: chmod +x gradlew 21 | 22 | - name: Build AAR 23 | run: ./gradlew assembleRelease 24 | 25 | - name: Publish to GitHub Package Registry 26 | run: ./gradlew publish 27 | env: 28 | GPR_USER: ${{ github.actor }} 29 | GPR_KEY: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/cache/persistentstorage/CacheDao.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.cache.persistentstorage 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.Query 6 | import com.gap.hoodies_network.utils.Generated 7 | 8 | @Dao 9 | @Generated 10 | interface CacheDao { 11 | @Insert 12 | fun insert(vararg item: CachedData) 13 | 14 | @Query("SELECT * FROM cacheddata WHERE iv = :iv") 15 | fun getByIv(iv: String): List 16 | 17 | @Query("SELECT * FROM cacheddata WHERE url = :url AND bodyHash = :bodyHash") 18 | fun get(url: String, bodyHash: Int): CachedData? 19 | 20 | @Query("DELETE FROM cacheddata WHERE url = :url AND bodyHash = :bodyHash") 21 | fun delete(url: String, bodyHash: Int) 22 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/testObjects/EmptyEncryptionDecryptionInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.testObjects 2 | 3 | import android.content.Context 4 | import com.gap.hoodies_network.interceptor.EncryptionDecryptionInterceptor 5 | 6 | class EmptyEncryptionDecryptionInterceptor(override val context: Context) : EncryptionDecryptionInterceptor { 7 | override fun encryptRequest(requestBodyOrUrlQueryParamKeyValue: ByteArray): ByteArray { 8 | return requestBodyOrUrlQueryParamKeyValue 9 | } 10 | 11 | override fun encryptAdditionalHeaders(additionalHeaderValue: ByteArray): ByteArray { 12 | return additionalHeaderValue 13 | } 14 | 15 | override fun decryptResponse(response: ByteArray): ByteArray { 16 | return response 17 | } 18 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/config/HeaderKeyValues.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.config 2 | 3 | // General 4 | const val CONTENT_TYPE_KEY = "Content-Type" 5 | const val MULTIDB_ENABLED = "X-MULTIDB-ENABLED" 6 | const val ACCEPT_LANGUAGE = "Accept-Language" 7 | const val AUTHORIZATION_KEY = "Authorization" 8 | 9 | 10 | 11 | //values 12 | const val MULTIDB_ENABLED_VALUE = "true" 13 | const val ACCEPT_LANGUAGE_VALUE = "en-US" 14 | 15 | const val APPLICATION_JSON = "application/json" 16 | 17 | // Access & Refresh Token 18 | const val CLIENT_ID_VALUE = "BRONGA" 19 | const val CLIENT_OS_VALUE ="IOS/Android" 20 | const val SESSION_ID_KEY = "sessionId" 21 | const val CLIENT_OS = "clientOS" 22 | const val CLIENT_ID = "clientAppId" 23 | const val FORM_URL_ENCODED = "application/x-www-form-urlencoded" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Android (please complete the following information):** 27 | - Device: [e.g. Galaxy S22] 28 | - OS: [e.g. One UI 5.1] 29 | - Version [e.g. API version 30] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/cookies/persistentstorage/EncryptedCookieDao.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.cookies.persistentstorage 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.Query 6 | import com.gap.hoodies_network.utils.Generated 7 | 8 | @Dao 9 | @Generated 10 | interface EncryptedCookieDao { 11 | @Query("SELECT * FROM encryptedcookie") 12 | fun getAll(): List 13 | 14 | @Query("SELECT * FROM encryptedcookie WHERE host = :host") 15 | fun getByHost(host: String): List 16 | 17 | @Query("SELECT * FROM encryptedcookie WHERE iv = :iv") 18 | fun getByIv(iv: String): List 19 | 20 | @Query("DELETE FROM encryptedcookie") 21 | fun deleteAll() 22 | 23 | @Query("DELETE FROM encryptedcookie WHERE hash = :hash") 24 | fun deleteByHash(hash: Int): Int 25 | 26 | @Query("SELECT DISTINCT host FROM encryptedcookie") 27 | fun getAllHosts(): List 28 | 29 | @Insert 30 | fun insert(vararg encryptedcookie: EncryptedCookie) 31 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/request/FileRequest.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.request 2 | 3 | import com.gap.hoodies_network.core.HoodiesNetworkError 4 | import com.gap.hoodies_network.core.Response 5 | import java.io.File 6 | import java.net.CookieManager 7 | import java.util.* 8 | 9 | abstract class FileRequest( 10 | url: String?, method: String?, files: List, multipartBoundary: String, 11 | private val responseListener: Response.ResponseListener?, 12 | errorListener: Response.ErrorListener?, cookieManager: CookieManager? 13 | ) : 14 | Request(url!!, method!!, files, multipartBoundary, errorListener, cookieManager) { 15 | private val mLock = Any() 16 | 17 | @Throws(HoodiesNetworkError::class) 18 | abstract override fun parseNetworkResponse(response: Response?): Response? 19 | override fun deliverResponse(response: Response?) { 20 | var listener: Response.ResponseListener? 21 | synchronized(mLock) { listener = responseListener } 22 | listener?.onResponse(response) 23 | } 24 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/mockwebserver/Weather.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.mockwebserver 2 | 3 | 4 | class Weather : WebServerHandler() { 5 | override fun handleRequest(call: HttpCall) { 6 | get { 7 | val params = call.getFormUrlEncodedParameters() 8 | 9 | if (params["q"] == "London,uk" && params["appid"] == "2b1fd2d7f77ccf1b7de9b441571b39b8") 10 | call.respond(200, "{\"coord\":{\"lon\":-0.13,\"lat\":51.51},\"weather\":[{\"id\":300,\"main\":\"Drizzle\",\"description\":\"light intensity drizzle\",\"icon\":\"09d\"}],\"base\":\"stations\",\"main\":{\"temp\":280.32,\"pressure\":1012,\"humidity\":81,\"temp_min\":279.15,\"temp_max\":281.15},\"visibility\":10000,\"wind\":{\"speed\":4.1,\"deg\":80},\"clouds\":{\"all\":90},\"dt\":1485789600,\"sys\":{\"type\":1,\"id\":5091,\"message\":0.0103,\"country\":\"GB\",\"sunrise\":1485762037,\"sunset\":1485794875},\"id\":2643743,\"name\":\"London\",\"cod\":200}") 11 | else 12 | call.respond(404, "Query params incorrect") 13 | } 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Gap Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /examples/WeatherResponse.kt: -------------------------------------------------------------------------------- 1 | import com.google.gson.annotations.SerializedName 2 | 3 | data class WeatherResponse( 4 | @field:SerializedName("coord") val coord: CoordData?, 5 | @field:SerializedName("weather") val weather: List?, 6 | @field:SerializedName("main") val main: MainData?, 7 | @field:SerializedName("clouds") val clouds: CloudsData? 8 | ) 9 | 10 | data class WeatherData( 11 | @field:SerializedName("id") val id: String?, 12 | @field:SerializedName("main") val main: String?, 13 | @field:SerializedName("description") val description: String?, 14 | @field:SerializedName("icon") val clouds: String? 15 | ) 16 | 17 | data class CoordData( 18 | @field:SerializedName("lat") val latitude: String?, 19 | @field:SerializedName("lon") val longitude: String? 20 | ) 21 | 22 | data class MainData( 23 | @field:SerializedName("temp") val temp: String?, 24 | @field:SerializedName("pressure") val pressure: String?, 25 | @field:SerializedName("humidity") val humidity: String? 26 | ) 27 | 28 | data class CloudsData( 29 | @field:SerializedName("all") val all: String? 30 | ) -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/header/Header.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.header 2 | 3 | import android.text.TextUtils 4 | 5 | 6 | /** 7 | * Header class is a model class of Header 8 | * 9 | * @param name must be not null/required 10 | * @param value must be not null/required 11 | * 12 | */ 13 | class Header(private val name: String, private val value: String) { 14 | 15 | fun getName(): String { 16 | return this.name 17 | } 18 | 19 | fun getValue(): String { 20 | return this.value 21 | } 22 | 23 | override fun equals(other: Any?): Boolean { 24 | if (this === other) { 25 | return true 26 | } 27 | if (other == null || javaClass != other.javaClass) { 28 | return false 29 | } 30 | val header = other as Header 31 | return TextUtils.equals(name, header.name) && TextUtils.equals(value, header.value) 32 | } 33 | 34 | override fun hashCode(): Int { 35 | return name.hashCode() 36 | } 37 | 38 | override fun toString(): String { 39 | return "Header[name=$name,value=$value]" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/testObjects/testInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.testObjects 2 | 3 | import android.content.Context 4 | import com.gap.hoodies_network.core.HoodiesNetworkError 5 | import com.gap.hoodies_network.core.Result 6 | import com.gap.hoodies_network.interceptor.Interceptor 7 | import com.gap.hoodies_network.request.Request 8 | import com.gap.hoodies_network.request.CancellableMutableRequest 9 | import com.gap.hoodies_network.request.RetryableCancellableMutableRequest 10 | 11 | class testInterceptor(context: Context) : Interceptor( context) { 12 | override fun interceptRequest(identifier: String, cancellableMutableRequest: CancellableMutableRequest) { 13 | 14 | } 15 | 16 | override fun interceptError(error: HoodiesNetworkError, retryableCancellableMutableRequest: RetryableCancellableMutableRequest, autoRetryAttempts: Int) { 17 | 18 | } 19 | 20 | override fun interceptNetwork(isOnline: Boolean, cancellableMutableRequest: CancellableMutableRequest) { 21 | 22 | } 23 | 24 | override fun interceptResponse(result: Result<*, HoodiesNetworkError>, request: Request?) { 25 | 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/connection/queue/NetworkHandler.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.connection.queue 2 | 3 | import com.gap.hoodies_network.connection.Network 4 | import com.gap.hoodies_network.core.HoodiesNetworkError 5 | import com.gap.hoodies_network.core.Response 6 | import com.gap.hoodies_network.delivery.ResponseDelivery 7 | import com.gap.hoodies_network.request.Request 8 | 9 | /** 10 | * NetworkHandler class does execute the request 11 | * 12 | * @param mDelivery 13 | * 14 | */ 15 | class NetworkHandler(private val mDelivery: ResponseDelivery) { 16 | fun executeRequest(request: Request, network: Network) { 17 | try { 18 | // execute network request 19 | val networkResponse: Response = network.executeRequest(request) 20 | request.parseNetworkResponse(networkResponse)?.let { 21 | mDelivery.postResponse( 22 | request, 23 | it 24 | ) 25 | } 26 | } catch (hoodiesNetworkError: HoodiesNetworkError) { 27 | mDelivery.postError(request, hoodiesNetworkError) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/interceptor/Interceptor.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.interceptor 2 | 3 | import android.content.Context 4 | import com.gap.hoodies_network.core.HoodiesNetworkError 5 | import com.gap.hoodies_network.core.Result 6 | import com.gap.hoodies_network.request.Request 7 | import com.gap.hoodies_network.request.CancellableMutableRequest 8 | import com.gap.hoodies_network.request.RetryableCancellableMutableRequest 9 | 10 | //Context is required here for interceptNetwork 11 | 12 | open class Interceptor(val context: Context?) { 13 | 14 | open fun interceptRequest(identifier: String, cancellableMutableRequest: CancellableMutableRequest) { 15 | //Stub 16 | } 17 | 18 | open fun interceptError(error: HoodiesNetworkError, retryableCancellableMutableRequest: RetryableCancellableMutableRequest, autoRetryAttempts: Int) { 19 | //Stub 20 | } 21 | 22 | open fun interceptNetwork(isOnline: Boolean, cancellableMutableRequest: CancellableMutableRequest) { 23 | //Stub 24 | } 25 | 26 | open fun interceptResponse(result: Result<*, HoodiesNetworkError>, request: Request?) { 27 | //Stub 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/cookies/persistentstorage/PersistentCookieStore.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.cookies.persistentstorage 2 | 3 | import android.content.Context 4 | import java.net.CookieStore 5 | import java.net.HttpCookie 6 | import java.net.URI 7 | 8 | internal class PersistentCookieStore(instanceName: String, context: Context) : CookieStore { 9 | private val roomDb = EncryptedDaoWrapperForCookies(instanceName, context) 10 | 11 | override fun add(uri: URI, cookie: HttpCookie) { 12 | roomDb.insert(uri, cookie) 13 | } 14 | 15 | override fun get(uri: URI): MutableList { 16 | return roomDb.getByHost(uri).toMutableList() 17 | } 18 | 19 | override fun getCookies(): MutableList { 20 | return roomDb.getAll().toMutableList() 21 | } 22 | 23 | override fun getURIs(): MutableList { 24 | return roomDb.getAllHosts().toMutableList() 25 | } 26 | 27 | override fun remove(uri: URI, cookie: HttpCookie): Boolean { 28 | return roomDb.deleteCookie(cookie) 29 | } 30 | 31 | override fun removeAll(): Boolean { 32 | roomDb.deleteAll() 33 | return true 34 | } 35 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/testObjects/WeatherResponse.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.testObjects 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class WeatherResponse( 6 | @field:SerializedName("coord") val coord: CoordData?, 7 | @field:SerializedName("weather") val weather: List?, 8 | @field:SerializedName("main") val main: MainData?, 9 | @field:SerializedName("clouds") val clouds: CloudsData? 10 | ) 11 | 12 | data class WeatherData( 13 | @field:SerializedName("id") val id: String?, 14 | @field:SerializedName("main") val main: String?, 15 | @field:SerializedName("description") val description: String?, 16 | @field:SerializedName("icon") val clouds: String? 17 | ) 18 | 19 | data class CoordData( 20 | @field:SerializedName("lat") val latitude: String?, 21 | @field:SerializedName("lon") val longitude: String? 22 | ) 23 | 24 | data class MainData( 25 | @field:SerializedName("temp") val temp: String?, 26 | @field:SerializedName("pressure") val pressure: String?, 27 | @field:SerializedName("humidity") val humidity: String? 28 | ) 29 | 30 | data class CloudsData( 31 | @field:SerializedName("all") val all: String? 32 | ) -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/request/json/JsonRequest.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.request.json 2 | 3 | import com.gap.hoodies_network.cache.EncryptedCache 4 | import com.gap.hoodies_network.core.HoodiesNetworkError 5 | import com.gap.hoodies_network.core.Response 6 | import com.gap.hoodies_network.request.Request 7 | import java.net.CookieManager 8 | 9 | /** 10 | * JsonRequest class parses response to json 11 | */ 12 | abstract class JsonRequest( 13 | url: String?, method: String?, requestBody: String, 14 | private val responseListener: Response.ResponseListener?, 15 | errorListener: Response.ErrorListener?, 16 | encryptedCache: EncryptedCache, 17 | cookieManager: CookieManager? 18 | ) : 19 | Request(url!!, method!!, requestBody, errorListener, encryptedCache, cookieManager) { 20 | private val mLock = Any() 21 | 22 | @Throws(HoodiesNetworkError::class) 23 | abstract override fun parseNetworkResponse(response: Response?): Response? 24 | override fun deliverResponse(response: Response?) { 25 | var listener: Response.ResponseListener? 26 | synchronized(mLock) { listener = responseListener } 27 | listener?.onResponse(response) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/config/IsOnline.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.config 2 | 3 | import android.content.Context 4 | import android.net.ConnectivityManager 5 | import android.net.NetworkCapabilities 6 | 7 | /** 8 | * Function to check if device is connected to the internet. 9 | * @param context - Context 10 | */ 11 | fun isOnline(context: Context?): Boolean { 12 | if (context != null) { 13 | val connectivityManager = 14 | context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 15 | val capabilities = 16 | connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) 17 | if (capabilities != null) { 18 | when { 19 | capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> { 20 | return true 21 | } 22 | capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> { 23 | return true 24 | } 25 | capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> { 26 | return true 27 | } 28 | } 29 | } 30 | return false 31 | } 32 | return false 33 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/mockwebserver/ImageReturn.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.mockwebserver 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.drawable.BitmapDrawable 6 | import android.graphics.drawable.Drawable 7 | import java.io.ByteArrayOutputStream 8 | import java.io.InputStream 9 | 10 | 11 | class ImageReturn(private val context: Context) : WebServerHandler() { 12 | override fun handleRequest(call: HttpCall) { 13 | get { 14 | // access file from assets folder 15 | val inputStream: InputStream = context.assets.open("drawables/orangeimage.png") 16 | // create drawable 17 | val drawable = Drawable.createFromStream(inputStream, null) 18 | // convert drawable to bitmap 19 | val bitmap = (drawable as BitmapDrawable).bitmap 20 | val stream = ByteArrayOutputStream() 21 | // compress bitmap 22 | bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream) 23 | // convert bitmap to byte array 24 | val byteArray: ByteArray = stream.toByteArray() 25 | // pass byte array to call's respond method 26 | call.respond(200, byteArray) 27 | } 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/header/HttpHeaderParser.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.header 2 | 3 | import java.nio.charset.Charset 4 | 5 | /** 6 | * HttpHeaderParser class does the parsing of header based on charset 7 | */ 8 | class HttpHeaderParser { 9 | 10 | companion object { 11 | private const val HEADER_CONTENT_TYPE = "Content-Type" 12 | private const val DEFAULT_CONTENT_CHARSET = "UTF-8" 13 | 14 | private const val PAIR_LENGTH = 2 15 | fun parseCharset(headers: Map): Charset { 16 | return parseCharset(headers, charset(DEFAULT_CONTENT_CHARSET)) 17 | } 18 | 19 | fun parseCharset(headers: Map, defaultCharset: Charset): Charset { 20 | val contentType = headers[HEADER_CONTENT_TYPE] ?: return defaultCharset 21 | val params = contentType.split(";").dropLastWhile { it.isEmpty() } 22 | .toTypedArray() 23 | for (i in 1 until params.size) { 24 | val pair = params[i].trim { it <= ' ' }.split("=").dropLastWhile { it.isEmpty() } 25 | .toTypedArray() 26 | if (pair.size == PAIR_LENGTH && "charset" == pair[0]) { 27 | return charset(pair[1]) 28 | } 29 | } 30 | return defaultCharset 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/interceptor/EncryptionDecryptionInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.interceptor 2 | 3 | import android.content.Context 4 | import java.io.IOException 5 | 6 | /** 7 | * Implement this interface to encrypt requests and responses. 8 | * If you do not want to perform any encryption for a particular case, just return the input ByteArray 9 | */ 10 | interface EncryptionDecryptionInterceptor { 11 | val context: Context 12 | @Throws(IOException::class) 13 | 14 | /** 15 | * @param requestBodyOrUrlQueryParamKeyValue - Request body encryption works in 2 ways: 16 | * 1. GET params (in URL, urlQueryEncoded, etc): This function is called to encrypt every key and value 17 | * 2. Parameters in body (other requests): This function is called once to encrypt the entire body before the request is sent 18 | */ 19 | fun encryptRequest(requestBodyOrUrlQueryParamKeyValue: ByteArray) : ByteArray 20 | 21 | /** 22 | * @param additionalHeaderValue - Called to encrypt every value for additional headers 23 | */ 24 | fun encryptAdditionalHeaders(additionalHeaderValue: ByteArray) : ByteArray 25 | 26 | /** 27 | * @param response - The raw response from the server 28 | * This is called before any response parsing to decrypt any encrypted response 29 | */ 30 | fun decryptResponse(response: ByteArray) : ByteArray 31 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/testObjects/EncryptionDecryptionInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.testObjects 2 | 3 | import android.content.Context 4 | import com.gap.hoodies_network.interceptor.EncryptionDecryptionInterceptor 5 | import com.gap.hoodies_network.keystore.CacheKeyManager 6 | import java.util.* 7 | import javax.crypto.Cipher 8 | import javax.crypto.spec.GCMParameterSpec 9 | 10 | class EncryptionDecryptionInterceptor(override val context: Context) : EncryptionDecryptionInterceptor { 11 | val iv = byteArrayOf(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1) 12 | 13 | override fun encryptRequest(requestBodyOrUrlQueryParamKeyValue: ByteArray): ByteArray { 14 | val ciphertext = runAES(requestBodyOrUrlQueryParamKeyValue, iv, Cipher.ENCRYPT_MODE) 15 | return Base64.getEncoder().encode(ciphertext) 16 | } 17 | 18 | override fun encryptAdditionalHeaders(additionalHeaderValue: ByteArray): ByteArray { 19 | val ciphertext = runAES(additionalHeaderValue, iv, Cipher.ENCRYPT_MODE) 20 | return Base64.getEncoder().encode(ciphertext) 21 | } 22 | 23 | override fun decryptResponse(response: ByteArray): ByteArray { 24 | return response 25 | } 26 | 27 | fun runAES(input: ByteArray, iv: ByteArray, cipherMode: Int): ByteArray { 28 | val cipher = Cipher.getInstance("AES/GCM/NoPadding") 29 | cipher.init(cipherMode, CacheKeyManager.getKey(), GCMParameterSpec(128, iv)) 30 | return cipher.doFinal(input) 31 | } 32 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/ResponseTest.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network 2 | 3 | import android.graphics.Bitmap 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import com.gap.hoodies_network.header.Header 6 | import com.gap.hoodies_network.core.Response 7 | import org.junit.Assert.assertEquals 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import java.util.* 11 | 12 | @RunWith(AndroidJUnit4::class) 13 | class ResponseTest { 14 | @Test 15 | fun mapToList() { 16 | val headers: MutableList
= ArrayList
() 17 | headers.add(Header("key1", "value1")) 18 | headers.add(Header("key2", "value2")) 19 | val resp = Response(200, null, 10, headers) 20 | val expectedHeaders: MutableMap = HashMap() 21 | expectedHeaders["key1"] = "value1" 22 | expectedHeaders["key2"] = "value2" 23 | val map: Map? = Response.toHeaderMap(resp.getAllHeaders()) 24 | assertEquals(expectedHeaders, map) 25 | } 26 | 27 | @Test 28 | fun nullValuesDontCrashAndStatusCode() { 29 | Response(null as Bitmap?) 30 | Response(null as ByteArray?) 31 | Response(0, null, 0, null) 32 | Response(0, null, null, 0) 33 | 34 | val resp = Response(-1) 35 | assertEquals(resp.statusCode, -1) 36 | } 37 | 38 | @Test 39 | fun toHeaderMap() { 40 | assertEquals(Response.toHeaderMap(null), null) 41 | assertEquals(Response.toHeaderMap(listOf()), emptyMap()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/request/json/JsonArrayRequest.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.request.json 2 | 3 | import com.gap.hoodies_network.cache.EncryptedCache 4 | import com.gap.hoodies_network.core.* 5 | import com.gap.hoodies_network.core.HoodiesNetworkError 6 | import com.gap.hoodies_network.utils.NetworkHelper 7 | import org.json.JSONArray 8 | import java.net.CookieManager 9 | 10 | /** 11 | * JsonArrayRequest class parses response to jsonArray 12 | */ 13 | class JsonArrayRequest @JvmOverloads constructor( 14 | url: String?, method: String?, jsonRequest: JSONArray?, 15 | responseListener: Response.ResponseListener? = null, 16 | errorListener: Response.ErrorListener? = null, 17 | encryptedCache: EncryptedCache, 18 | cookieManager: CookieManager? 19 | ) : 20 | JsonRequest( 21 | url, method, jsonRequest.toString(), responseListener, 22 | errorListener, encryptedCache, cookieManager 23 | ) { 24 | @JvmOverloads 25 | constructor( 26 | url: String?, method: String?, 27 | responseListener: Response.ResponseListener? = null, 28 | errorListener: Response.ErrorListener? = null, 29 | encryptedCache: EncryptedCache, 30 | cookieManager: CookieManager? 31 | ) : this(url, method, null, responseListener, errorListener, encryptedCache, cookieManager) 32 | 33 | @Throws(HoodiesNetworkError::class) 34 | override fun parseNetworkResponse(response: Response?): Response? { 35 | val networkHelper = NetworkHelper() 36 | return networkHelper.getParseNetworkResponse(response, true) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/request/json/JsonObjectRequest.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.request.json 2 | 3 | import com.gap.hoodies_network.cache.EncryptedCache 4 | import com.gap.hoodies_network.core.* 5 | import com.gap.hoodies_network.core.HoodiesNetworkError 6 | import com.gap.hoodies_network.utils.NetworkHelper 7 | import org.json.JSONObject 8 | import java.net.CookieManager 9 | 10 | /** 11 | * JsonObjectRequest class parses response to jsonObject 12 | */ 13 | class JsonObjectRequest @JvmOverloads constructor( 14 | url: String?, method: String?, jsonRequest: JSONObject?, 15 | responseListener: Response.ResponseListener? = null, 16 | errorListener: Response.ErrorListener? = null, 17 | encryptedCache: EncryptedCache, 18 | cookieManager: CookieManager? 19 | ) : 20 | JsonRequest( 21 | url, method, jsonRequest.toString(), responseListener, 22 | errorListener, encryptedCache, cookieManager 23 | ) { 24 | @JvmOverloads 25 | constructor( 26 | url: String?, method: String?, 27 | responseListener: Response.ResponseListener? = null, 28 | errorListener: Response.ErrorListener? = null, 29 | encryptedCache: EncryptedCache, 30 | cookieManager: CookieManager? 31 | ) : this(url, method, null, responseListener, errorListener, encryptedCache, cookieManager) 32 | 33 | @Throws(HoodiesNetworkError::class) 34 | override fun parseNetworkResponse(response: Response?): Response? { 35 | val networkHelper = NetworkHelper() 36 | return networkHelper.getParseNetworkResponse(response) 37 | } 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /examples/PutRequestViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.gap.hoodies_network.config.* 8 | import com.gap.hoodies_network.core.HoodiesNetworkError 9 | import com.gap.hoodies_network.core.HoodiesNetworkClient 10 | import com.google.gson.Gson 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.launch 13 | import kotlinx.coroutines.withContext 14 | import java.net.MalformedURLException 15 | import java.util.HashMap 16 | 17 | class PutRequestViewModel : ViewModel() { 18 | 19 | private var mobileHttpClient: HoodiesNetworkClient = HoodiesNetworkClient.Builder().baseUrl("https://httpbin.org/").build() 20 | 21 | internal var putResponse = MutableLiveData() 22 | 23 | internal fun sendPutRequest() { 24 | viewModelScope.launch(Dispatchers.Main) { 25 | val result: Result = withContext(Dispatchers.IO) { 26 | mobileHttpClient.putRaw("put", "") 27 | } 28 | when (result) { 29 | is Success -> { 30 | putResponse.postValue(result.value) 31 | } 32 | is Failure -> { 33 | putResponse.postValue(getError(result.reason)) 34 | } 35 | } 36 | } 37 | } 38 | 39 | fun getError(HoodiesNetworkError: HoodiesNetworkError): String { 40 | return "Code Error: " + HoodiesNetworkError.code.toString() + " Message: " + HoodiesNetworkError.message 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /examples/GetHtmlViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.gap.hoodies_network.config.* 8 | import com.gap.hoodies_network.core.HoodiesNetworkError 9 | import com.gap.hoodies_network.core.HoodiesNetworkClient 10 | import com.google.gson.Gson 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.launch 13 | import kotlinx.coroutines.withContext 14 | import java.net.MalformedURLException 15 | import java.util.HashMap 16 | 17 | class GetHtmlViewModel : ViewModel() { 18 | 19 | private var mobileHttpClient: HoodiesNetworkClient = HoodiesNetworkClient.Builder().baseUrl("https://httpbin.org/").build() 20 | 21 | internal var getHtmlResponse = MutableLiveData() 22 | 23 | internal fun sendGetHtmlRequest() { 24 | viewModelScope.launch(Dispatchers.Main) { 25 | val result = withContext(Dispatchers.IO) { 26 | val url = "html" 27 | mobileHttpClient.getHtml(url) 28 | } 29 | when (result) { 30 | is Success -> { 31 | getHtmlResponse.postValue(result.value) 32 | } 33 | is Failure -> { 34 | getHtmlResponse.postValue(result.reason.message) 35 | } 36 | } 37 | } 38 | } 39 | 40 | fun getError(HoodiesNetworkError: HoodiesNetworkError): String { 41 | return "Code Error: " + HoodiesNetworkError.code.toString() + " Message: " + HoodiesNetworkError.message 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /examples/DeleteRequestViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.gap.hoodies_network.config.* 8 | import com.gap.hoodies_network.core.HoodiesNetworkError 9 | import com.gap.hoodies_network.core.HoodiesNetworkClient 10 | import com.google.gson.Gson 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.launch 13 | import kotlinx.coroutines.withContext 14 | import java.net.MalformedURLException 15 | import java.util.HashMap 16 | 17 | class DeleteRequestViewModel : ViewModel() { 18 | 19 | private var mobileHttpClient: HoodiesNetworkClient = HoodiesNetworkClient.Builder().baseUrl("https://httpbin.org/").build() 20 | 21 | internal var deleteResponse = MutableLiveData() 22 | 23 | internal fun sendDeleteRequest() { 24 | viewModelScope.launch(Dispatchers.Main) { 25 | val result: Result = withContext(Dispatchers.IO) { 26 | mobileHttpClient.deleteRaw("delete") 27 | } 28 | when (result) { 29 | is Success -> { 30 | deleteResponse.postValue(result.value) 31 | } 32 | is Failure -> { 33 | deleteResponse.postValue(getError(result.reason)) 34 | } 35 | } 36 | } 37 | } 38 | 39 | fun getError(HoodiesNetworkError: HoodiesNetworkError): String { 40 | return "Code Error: " + HoodiesNetworkError.code.toString() + " Message: " + HoodiesNetworkError.message 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /examples/GetRawHtmlViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.gap.hoodies_network.config.* 8 | import com.gap.hoodies_network.core.HoodiesNetworkError 9 | import com.gap.hoodies_network.core.HoodiesNetworkClient 10 | import com.google.gson.Gson 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.launch 13 | import kotlinx.coroutines.withContext 14 | import java.net.MalformedURLException 15 | import java.util.HashMap 16 | 17 | class GetRawHtmlViewModel : ViewModel() { 18 | 19 | private var mobileHttpClient: HoodiesNetworkClient = HoodiesNetworkClient.Builder().baseUrl("https://httpbin.org/").build() 20 | 21 | internal var getRawHtmlResponse = MutableLiveData() 22 | 23 | internal fun sendGetHtmlRequest() { 24 | viewModelScope.launch(Dispatchers.Main) { 25 | val result = withContext(Dispatchers.IO) { 26 | val url = "html" 27 | mobileHttpClient.getHtml(url) 28 | } 29 | when (result) { 30 | is Success -> { 31 | getRawHtmlResponse.postValue(result.value) 32 | } 33 | is Failure -> { 34 | getRawHtmlResponse.postValue(getError(result.reason)) 35 | } 36 | } 37 | } 38 | } 39 | 40 | fun getError(HoodiesNetworkError: HoodiesNetworkError): String { 41 | return "Code Error: " + HoodiesNetworkError.code.toString() + " Message: " + HoodiesNetworkError.message 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated files 2 | bin/ 3 | gen/ 4 | out/ 5 | 6 | # Gradle files 7 | .gradle 8 | .gradle/ 9 | build/ 10 | .idea/ 11 | 12 | # Local configuration file (sdk path, etc) 13 | local.properties 14 | gradle.properties 15 | gradlew 16 | gradlew.bat 17 | 18 | # Signing files 19 | .signing/ 20 | 21 | # Proguard folder generated by Eclipse 22 | proguard/ 23 | 24 | # Log Files 25 | *.log 26 | 27 | # Android Studio 28 | /*/build/ 29 | /*/local.properties 30 | /*/out 31 | /*/*/build 32 | /*/*/production 33 | captures/ 34 | .navigation/ 35 | *.ipr 36 | *~ 37 | *.swp 38 | 39 | # Android Patch 40 | gen-external-apklibs 41 | 42 | # External native build folder generated in Android Studio 2.2 and later 43 | .externalNativeBuild 44 | 45 | # NDK 46 | obj/ 47 | 48 | # IntelliJ IDEA 49 | *.iml 50 | *.iws 51 | /out/ 52 | 53 | # User-specific configurations 54 | .idea/* 55 | .idea/caches/ 56 | .idea/libraries/ 57 | .idea/shelf/ 58 | .idea/workspace.xml 59 | .idea/tasks.xml 60 | .idea/.name 61 | .idea/compiler.xml 62 | .idea/copyright/profiles_settings.xml 63 | .idea/encodings.xml 64 | .idea/misc.xml 65 | .idea/modules.xml 66 | .idea/scopes/scope_settings.xml 67 | .idea/dictionaries 68 | .idea/vcs.xml 69 | .idea/jsLibraryMappings.xml 70 | .idea/datasources.xml 71 | .idea/dataSources.ids 72 | .idea/sqlDataSources.xml 73 | .idea/dynamic.xml 74 | .idea/uiDesigner.xml 75 | .idea/assetWizardSettings.xml 76 | 77 | # OS-specific files 78 | .DS_Store 79 | .DS_Store? 80 | ._* 81 | .Spotlight-V100 82 | .Trashes 83 | ehthumbs.db 84 | Thumbs.db 85 | 86 | # mpeltonen/sbt-idea plugin 87 | .idea_modules/ 88 | 89 | ### AndroidStudio Patch ### 90 | 91 | !/gradle/wrapper/gradle-wrapper.jar 92 | /Gemfile 93 | /Gemfile.lock 94 | -------------------------------------------------------------------------------- /examples/JsonObjectRequestViewModel.kt: -------------------------------------------------------------------------------- 1 | import androidx.lifecycle.* 2 | import com.gap.hoodies_network.config.* 3 | import com.gap.hoodies_network.core.HoodiesNetworkError 4 | import com.gap.hoodies_network.core.HoodiesNetworkClient 5 | import com.google.gson.Gson 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.launch 8 | import kotlinx.coroutines.withContext 9 | import org.json.JSONArray 10 | import org.json.JSONObject 11 | 12 | /** 13 | * ViewModel to send a JSON object to an endpoint 14 | */ 15 | class JsonObjectRequestViewModel : ViewModel() { 16 | 17 | private var mobileHttpClient: HoodiesNetworkClient = HoodiesNetworkClient.Builder().baseUrl("http://localhost:6969/").build() 18 | private val gson = Gson() 19 | internal var jsonObjectResponse = MutableLiveData() 20 | 21 | internal fun sendJsonObjectRequest(): LiveData { 22 | viewModelScope.launch(Dispatchers.Main) { 23 | val url = "post" //baseurl + this, so http://localhost:6969/post 24 | val jsonObject = JSONObject("{\"name\":\"Test\", \"age\":25}") //We will send this JsonObject 25 | val result: Result = withContext(Dispatchers.IO) { 26 | mobileHttpClient.post(url, jsonObject) 27 | } 28 | when (result) { 29 | is Success -> { 30 | jsonObjectResponse.postValue(gson.toJson(result.value)) 31 | } 32 | is Failure -> { 33 | jsonObjectResponse.postValue(gson.toJson(result.reason.message)) 34 | } 35 | } 36 | } 37 | return jsonObjectResponse 38 | } 39 | } -------------------------------------------------------------------------------- /examples/GetJsonViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.gap.hoodies_network.config.* 8 | import com.gap.hoodies_network.core.HoodiesNetworkError 9 | import com.gap.hoodies_network.core.HoodiesNetworkClient 10 | import com.google.gson.Gson 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.launch 13 | import kotlinx.coroutines.withContext 14 | import java.net.MalformedURLException 15 | import java.util.HashMap 16 | 17 | class GetJsonViewModel : ViewModel() { 18 | 19 | private var mobileHttpClient: HoodiesNetworkClient = HoodiesNetworkClient.Builder().baseUrl("https://httpbin.org/").build() 20 | 21 | internal var getJsonResponse = MutableLiveData() 22 | 23 | internal fun sendGetJsonRequest() { 24 | viewModelScope.launch(Dispatchers.Main) { 25 | val result: Result = withContext(Dispatchers.IO) { 26 | val url = "get" 27 | mobileHttpClient.get(url) 28 | } 29 | when (result) { 30 | is Success -> { 31 | getJsonResponse.postValue(gson.toJson(result.value)) 32 | } 33 | is Failure -> { 34 | getJsonResponse.postValue(result.reason.message) 35 | } 36 | } 37 | } 38 | } 39 | 40 | fun getError(HoodiesNetworkError: HoodiesNetworkError): String { 41 | return "Code Error: " + HoodiesNetworkError.code.toString() + " Message: " + HoodiesNetworkError.message 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /examples/GetRawJsonViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.gap.hoodies_network.config.* 8 | import com.gap.hoodies_network.core.HoodiesNetworkError 9 | import com.gap.hoodies_network.core.HoodiesNetworkClient 10 | import com.google.gson.Gson 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.launch 13 | import kotlinx.coroutines.withContext 14 | import java.net.MalformedURLException 15 | import java.util.HashMap 16 | 17 | class GetRawJsonViewModel : ViewModel() { 18 | 19 | private var mobileHttpClient: HoodiesNetworkClient = HoodiesNetworkClient.Builder().baseUrl("https://httpbin.org/").build() 20 | 21 | internal var getRawJsonResponse = MutableLiveData() 22 | 23 | internal fun sendGetJsonRequest() { 24 | viewModelScope.launch(Dispatchers.Main) { 25 | val result: Result = withContext(Dispatchers.IO) { 26 | val url = "get" 27 | mobileHttpClient.getRaw(url) 28 | } 29 | when (result) { 30 | is Success -> { 31 | getRawJsonResponse.postValue(result.value) 32 | } 33 | is Failure -> { 34 | getRawJsonResponse.postValue(getError(result.reason)) 35 | } 36 | } 37 | } 38 | } 39 | 40 | fun getError(HoodiesNetworkError: HoodiesNetworkError): String { 41 | return "Code Error: " + HoodiesNetworkError.code.toString() + " Message: " + HoodiesNetworkError.message 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/FormUrlEncodedRequestTest.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import com.gap.hoodies_network.cache.EncryptedCache 6 | import com.gap.hoodies_network.mockwebserver.ServerManager 7 | import com.gap.hoodies_network.request.FormUrlEncodedRequest 8 | import com.gap.hoodies_network.request.Request 9 | import org.junit.After 10 | import org.junit.Assert.assertEquals 11 | import org.junit.Before 12 | import org.junit.Test 13 | import org.junit.runner.RunWith 14 | import java.io.UnsupportedEncodingException 15 | 16 | @RunWith(AndroidJUnit4::class) 17 | class FormUrlEncodedRequestTest { 18 | val mContext = InstrumentationRegistry.getInstrumentation().context 19 | 20 | @Before 21 | fun startMockWebServer() { 22 | ServerManager.setup(mContext) 23 | } 24 | 25 | @After 26 | fun stopServer() { 27 | ServerManager.stop() 28 | } 29 | 30 | @Test 31 | fun requestFormattingTest() { 32 | val cache = EncryptedCache(null) 33 | val requestMap: MutableMap = HashMap() 34 | requestMap["key1"] = "value1" 35 | requestMap["key2"] = "value2" 36 | requestMap["key3"] = "value3" 37 | var request: FormUrlEncodedRequest? = null 38 | try { 39 | request = 40 | FormUrlEncodedRequest("", Request.Method.POST, requestMap, null, null, cache, null) 41 | } catch (e: UnsupportedEncodingException) { 42 | e.printStackTrace() 43 | } 44 | assertEquals("key1=value1&key2=value2&key3=value3", request?.getBody()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/ImageRequestViewModel.kt: -------------------------------------------------------------------------------- 1 | import android.graphics.Bitmap 2 | import android.widget.ImageView 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.gap.hoodies_network.config.Failure 7 | import com.gap.hoodies_network.config.HoodiesNetworkClient 8 | import com.gap.hoodies_network.config.Success 9 | import com.gap.hoodies_network.core.HoodiesNetworkClient 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.launch 12 | import kotlinx.coroutines.withContext 13 | 14 | /** 15 | * ViewModel to fetch an image from an endpoint 16 | * A Bitmap is returned, various settings are configurable 17 | */ 18 | class ImageRequestViewModel : ViewModel() { 19 | private var mobileHttpClient: HoodiesNetworkClient = HoodiesNetworkClient.Builder().baseUrl("https://httpbin.org/").build() 20 | val getImageResponse = MutableLiveData() 21 | 22 | fun sendGetImageRequest() { 23 | viewModelScope.launch(Dispatchers.Main) { 24 | val result = withContext(Dispatchers.IO) { 25 | mobileHttpClient.getImage( 26 | "image", //https://httpbin.org/image 27 | null, 28 | 0, 29 | 0, 30 | ImageView.ScaleType.CENTER, 31 | Bitmap.Config.ALPHA_8 32 | ) 33 | } 34 | when (result) { 35 | is Success -> { 36 | getImageResponse.setValue(result.value) 37 | } 38 | is Failure -> { 39 | getImageResponse.setValue(null) 40 | 41 | } 42 | } 43 | } 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /examples/JsonArrayRequestViewModel.kt: -------------------------------------------------------------------------------- 1 | import androidx.lifecycle.* 2 | import com.gap.hoodies_network.config.* 3 | import com.gap.hoodies_network.core.HoodiesNetworkError 4 | import com.gap.hoodies_network.core.HoodiesNetworkClient 5 | import com.google.gson.Gson 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.launch 8 | import kotlinx.coroutines.withContext 9 | import org.json.JSONArray 10 | import org.json.JSONObject 11 | 12 | /** 13 | * ViewModel to send a JSONarray to an endpoint 14 | */ 15 | class JsonArrayRequestViewModel : ViewModel() { 16 | 17 | private var mobileHttpClient: HoodiesNetworkClient = HoodiesNetworkClient.Builder().baseUrl("https://httpbin.org/").build() 18 | private val gson = Gson() 19 | internal var jsonArrayResponse = MutableLiveData() 20 | 21 | internal fun sendJsonArrayRequest() { 22 | viewModelScope.launch(Dispatchers.Main) { 23 | val url = "post" //baseurl + this, so http://localhost:6969/ 24 | val jsonArray = JSONArray( 25 | "[{\"name\":\"Test 1\", \"age\":25}," + 26 | "{\"name\":\"Test 2\", \"age\":22},{\"name\":\"Test 3\", \"age\":21}]" 27 | ) //We will send this JsonArray 28 | val result: Result = withContext(Dispatchers.IO) { 29 | mobileHttpClient.post(url, jsonArray) 30 | } 31 | when (result) { 32 | is Success -> { 33 | jsonArrayResponse.postValue(gson.toJson(result.value)) 34 | } 35 | is Failure -> { 36 | jsonArrayResponse.postValue(gson.toJson(result.reason.message)) 37 | } 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Measure Coverage 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | measure_coverage: 8 | runs-on: macos-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Set up JDK 11 12 | uses: actions/setup-java@v1 13 | with: 14 | java-version: 11 15 | 16 | - name: Grant execute permission for gradlew 17 | run: chmod +x gradlew 18 | 19 | - name: Run Coverage 20 | uses: reactivecircus/android-emulator-runner@v2 21 | with: 22 | arch: 'x86_64' 23 | api-level: 30 24 | script: ./gradlew jacocoTestReport 25 | 26 | - name: Upload Report 27 | uses: 'actions/upload-artifact@v2' 28 | with: 29 | name: report.xml 30 | path: ${{ github.workspace }}/Hoodies-Network/build/reports/coverage/androidTest/debug/report.xml 31 | 32 | - name: Jacoco Report to PR 33 | id: jacoco 34 | uses: madrapps/jacoco-report@v1.1 35 | with: 36 | path: ${{ github.workspace }}/Hoodies-Network/build/reports/coverage/androidTest/debug/report.xml 37 | token: ${{ secrets.GITHUB_TOKEN }} 38 | min-coverage-overall: 80 39 | min-coverage-changed-files: 0 40 | debug-mode: false 41 | update-comment: true 42 | title: Test Coverage Report 43 | 44 | - name: Get the Coverage info 45 | run: | 46 | echo "Total coverage ${{ steps.jacoco.outputs.coverage-overall }}" 47 | 48 | - name: Fail PR if overall coverage is less than 80% 49 | if: ${{ steps.jacoco.outputs.coverage-overall < 80.0 }} 50 | uses: actions/github-script@v6 51 | with: 52 | script: | 53 | core.setFailed('Overall coverage is less than 80%!') -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/connection/queue/QueueHandler.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.connection.queue 2 | 3 | import android.os.Process 4 | import com.gap.hoodies_network.connection.Network 5 | import com.gap.hoodies_network.request.Request 6 | import java.util.concurrent.BlockingQueue 7 | 8 | /** 9 | * QueueHandler class processes Requests in queue 10 | * 11 | * @param queue 12 | * @param networkHandler 13 | * @param network 14 | * 15 | */ 16 | class QueueHandler internal constructor( 17 | queue: BlockingQueue>, 18 | networkHandler: NetworkHandler, 19 | network: Network 20 | ) : Thread() { 21 | // NO SONAR 22 | private val mQueue: BlockingQueue> = queue 23 | private val mNetworkHandler: NetworkHandler = networkHandler 24 | private val mNetwork: Network = network 25 | 26 | @Volatile 27 | private var mQuit = false 28 | override fun run() { 29 | Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND) 30 | while (true) { 31 | try { 32 | processRequest() 33 | } catch (e: InterruptedException) { 34 | if (mQuit) { 35 | currentThread().interrupt() 36 | return 37 | } 38 | } 39 | } 40 | } 41 | 42 | @Throws(InterruptedException::class) 43 | private fun processRequest() { 44 | val request: Request = mQueue.take() 45 | processRequest(request) 46 | } 47 | 48 | private fun processRequest(request: Request) { 49 | mNetworkHandler.executeRequest(request, mNetwork) 50 | } 51 | 52 | /** 53 | * dispatcher to quit immediately forcefully. If any requests are still in the queue, they are 54 | * not guaranteed to be processed. 55 | */ 56 | fun quit() { 57 | mQuit = true 58 | interrupt() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/cookies/CookieJar.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.cookies 2 | 3 | import java.net.CookieManager 4 | import java.net.CookieStore 5 | import java.net.HttpCookie 6 | import java.net.URI 7 | 8 | open class CookieJar(cookieStore: CookieStore = CookieManager().cookieStore) : CookieManager(cookieStore, null) { 9 | 10 | /** 11 | * Gets all the cookies for a specified host 12 | * This is path-independent - the HttpCookie's Path attribute is ignored 13 | * So, cookies returned by this function may not be sent to all URLs on a particular host if Path attributes are used 14 | */ 15 | fun getCookiesForHost(host: URI) : List { 16 | return this.cookieStore.get(host) 17 | } 18 | 19 | /** 20 | * Gets all cookies stored in the CookieJar 21 | */ 22 | fun getAllCookies() : List { 23 | return this.cookieStore.cookies 24 | } 25 | 26 | /** 27 | * Gets all hosts that are associated with a cookie stored in the CookieJar 28 | */ 29 | fun getAllHosts() : List { 30 | return this.cookieStore.urIs 31 | } 32 | 33 | /** 34 | * Overwrites all of the existing cookie for a specified host with the provided list 35 | */ 36 | fun setCookiesForHost(host: URI, cookies: List) { 37 | this.cookieStore.get(host).forEach { 38 | this.cookieStore.remove(host, it) 39 | } 40 | 41 | cookies.forEach { 42 | this.cookieStore.add(host, it) 43 | } 44 | } 45 | 46 | /** 47 | * Adds a single cookie to the CookieJar for a host 48 | */ 49 | fun addCookieForHost(host: URI, cookie: HttpCookie) { 50 | this.cookieStore.add(host, cookie) 51 | } 52 | 53 | /** 54 | * Removes every single cookie in the CookieJar 55 | */ 56 | fun removeAllCookies() { 57 | this.cookieStore.removeAll() 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/mockwebserver/MockWebServerManager.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.mockwebserver 2 | 3 | import com.gap.hoodies_network.utils.Generated 4 | import com.sun.net.httpserver.HttpServer 5 | import java.net.InetSocketAddress 6 | 7 | /** 8 | * This class manages the MockWebServer 9 | */ 10 | @Generated 11 | class MockWebServerManager(builder: Builder) { 12 | 13 | private val httpServer: HttpServer = HttpServer.create(InetSocketAddress(builder.port), 0) 14 | 15 | init { 16 | for (item in builder.context) 17 | httpServer.createContext(item.key, item.value.internalHandler) 18 | } 19 | 20 | /** 21 | * Starts the MockWebServer 22 | */ 23 | fun start() = apply { 24 | httpServer.start() 25 | } 26 | 27 | /** 28 | * Stops the MockWebServer 29 | */ 30 | fun stop() { 31 | httpServer.stop(0) 32 | } 33 | 34 | /** 35 | * Builder for the MockWebServer 36 | */ 37 | class Builder { 38 | internal val context: HashMap = HashMap() 39 | internal var port: Int = 6969 40 | 41 | /** 42 | * Called to add API endpoints to be served by the MockWebServer 43 | * For more details, see the WebServerHandler documentation 44 | */ 45 | fun addContext(key: String, value: WebServerHandler) = apply { 46 | context[key] = value 47 | } 48 | 49 | /** 50 | * Specifies the port the MockWebServer should use. Default is 6969 51 | */ 52 | fun usePort(port: Int) = apply { 53 | this.port = port 54 | } 55 | 56 | /** 57 | * Starts the MockWebServer and returns a MockWebServerManager object 58 | * To stop the MockWebServer, call the MockWebServerManager's stop() method 59 | */ 60 | fun start() : MockWebServerManager { 61 | return MockWebServerManager(this).start() 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/request/query/UrlQueryParamEncodedRequest.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.request.query 2 | 3 | import android.util.Log 4 | import com.gap.hoodies_network.cache.EncryptedCache 5 | import com.gap.hoodies_network.core.Response 6 | import com.gap.hoodies_network.request.StringRequest 7 | import com.gap.hoodies_network.utils.NetworkHelper 8 | import java.io.UnsupportedEncodingException 9 | import java.net.CookieManager 10 | import java.net.URL 11 | 12 | /** 13 | * UrlQueryParamEncodedRequest class appends query params to url of encoded request 14 | * 15 | * @param host 16 | * @param method 17 | * @param scheme 18 | * @param queryParams 19 | * @param responseListener 20 | * @param errorListener 21 | * 22 | */ 23 | class UrlQueryParamEncodedRequest( 24 | host: String, 25 | method: String, 26 | scheme: String, 27 | queryParams: Map?, 28 | responseListener: Response.ResponseListener?, 29 | errorListener: Response.ErrorListener?, 30 | encryptedCache: EncryptedCache, 31 | cookieManager: CookieManager? 32 | ) : StringRequest( 33 | convertToQueryParamEncodedURL(scheme, host, queryParams), 34 | method, 35 | convertToQueryParamEncodedURL(scheme, host, queryParams), 36 | responseListener, 37 | errorListener, 38 | encryptedCache, 39 | cookieManager 40 | ) { 41 | companion object { 42 | @Throws(UnsupportedEncodingException::class) 43 | private fun convertToQueryParamEncodedURL( 44 | scheme: String, 45 | host: String, 46 | queryParams: Map? 47 | ): String { 48 | val networkHelper = NetworkHelper() 49 | val finalURL = URL( 50 | networkHelper.getFinalURL( 51 | queryParams, host, 52 | scheme 53 | ) 54 | ).toString() 55 | Log.d("QueryParam Encoded URL-", finalURL + "") 56 | return finalURL 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/HeaderTest.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network 2 | 3 | import android.util.Log 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import androidx.test.platform.app.InstrumentationRegistry 6 | import com.gap.hoodies_network.config.* 7 | import com.gap.hoodies_network.header.Header 8 | import com.gap.hoodies_network.header.HttpHeaderParser 9 | import com.gap.hoodies_network.mockwebserver.ServerManager 10 | import org.junit.After 11 | import org.junit.Assert.* 12 | import org.junit.Before 13 | import org.junit.Test 14 | import org.junit.runner.RunWith 15 | 16 | @RunWith(AndroidJUnit4::class) 17 | class HeaderTest { 18 | val mContext = InstrumentationRegistry.getInstrumentation().context 19 | 20 | @Before 21 | fun startMockWebServer() { 22 | ServerManager.setup(mContext) 23 | } 24 | 25 | @After 26 | fun stopServer() { 27 | ServerManager.stop() 28 | } 29 | 30 | @Test 31 | fun headerTest() { 32 | val header = Header("key", "value") 33 | assertEquals("key", header.getName()) 34 | assertEquals("value", header.getValue()) 35 | val header2 = Header("key2", "value2") 36 | val b: Boolean = header == header2 37 | assertFalse(b) 38 | val b2: Boolean = header.equals(null) 39 | assertFalse(b2) 40 | val hashCode: Int = header.hashCode() 41 | val hashCode2: Int = header2.hashCode() 42 | assertNotEquals(hashCode.toLong(), hashCode2.toLong()) 43 | val str: String = header.toString() 44 | assertNotNull(str) 45 | } 46 | 47 | @Test 48 | fun headerParserTest() { 49 | val defaultHeaders = hashMapOf( 50 | CONTENT_TYPE_KEY to APPLICATION_JSON, MULTIDB_ENABLED to MULTIDB_ENABLED_VALUE, 51 | CLIENT_ID to CLIENT_ID_VALUE, 52 | CLIENT_OS to CLIENT_OS_VALUE 53 | ) 54 | 55 | val charset = HttpHeaderParser.parseCharset(defaultHeaders) 56 | Log.e("charset",charset.toString()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/mockwebserver/ServerManager.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.mockwebserver 2 | 3 | import android.content.Context 4 | 5 | class ServerManager { 6 | companion object { 7 | var server: MockWebServerManager? = null 8 | 9 | fun setup(context:Context?) { 10 | 11 | val builder = MockWebServerManager.Builder() 12 | 13 | //HttpBin replica 14 | builder.addContext("/post", Post()) 15 | builder.addContext("/get", Get()) 16 | builder.addContext("/options", Options()) 17 | builder.addContext("/put", Put()) 18 | builder.addContext("/delete", Delete()) 19 | builder.addContext("/patch", Patch()) 20 | builder.addContext("/html", Html()) 21 | builder.addContext("/image", ImageReturn(context!!)) 22 | 23 | //Postman echo replica 24 | builder.addContext("/echo", EchoDelay()) 25 | 26 | //OpenWeatherMap sample replica 27 | builder.addContext("/weather", Weather()) 28 | 29 | //JsonTodos replica 30 | builder.addContext("/todos", JsonTodos()) 31 | 32 | //Cookie testing setup 33 | builder.addContext("/cookie_factory", CookieFactory()) 34 | builder.addContext("/cookie_inspector", CookieInspector()) 35 | 36 | //Interceptor testing setup 37 | builder.addContext("/wants_key", WantsKeyHeader()) 38 | //Sometimes the tests get run in parallel and fail because the port is already in use 39 | //For those cases, we will wait here until the server can start 40 | 41 | var started = false 42 | 43 | while (!started) { 44 | try { 45 | server = builder.start() 46 | started = true 47 | } catch (e: Exception) { 48 | Thread.sleep(100) 49 | } 50 | } 51 | } 52 | 53 | fun stop() { 54 | server?.stop() 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/request/FormUrlEncodedRequest.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.request 2 | 3 | import com.gap.hoodies_network.cache.EncryptedCache 4 | import com.gap.hoodies_network.core.Response 5 | import java.io.UnsupportedEncodingException 6 | import java.net.CookieManager 7 | import java.net.URLEncoder 8 | 9 | 10 | /** 11 | * FormUrlEncodedRequest class converts request body to a parameterized string 12 | * 13 | * @param url not null 14 | * @param method not null 15 | * @param requestBody can be null 16 | * @param responseListener can be null 17 | * @param errorListener can be null 18 | * 19 | */ 20 | class FormUrlEncodedRequest( 21 | url: String, 22 | method: String, 23 | requestBody: Map?, 24 | responseListener: Response.ResponseListener?, 25 | errorListener: Response.ErrorListener?, 26 | encryptedCache: EncryptedCache, 27 | cookieManager: CookieManager? 28 | ) : StringRequest( 29 | url, 30 | method, 31 | convertToParameterizedString(requestBody), 32 | responseListener, 33 | errorListener, 34 | encryptedCache, 35 | cookieManager 36 | ) { 37 | companion object { 38 | @Throws(UnsupportedEncodingException::class) 39 | private fun convertToParameterizedString(requestBody: Map?): String { 40 | return if (requestBody != null) { 41 | val result = StringBuilder() 42 | var first = true 43 | for ((key, value) in requestBody) { 44 | if (first) { 45 | first = false 46 | } else { 47 | result.append("&") 48 | } 49 | result.append(URLEncoder.encode(key, "UTF-8")) 50 | result.append("=") 51 | result.append(URLEncoder.encode(value, "UTF-8")) 52 | } 53 | result.toString() 54 | } else { 55 | return "" // @MSP replaced null with empty string 56 | } 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/request/query/UrlQueryParamRequest.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.request.query 2 | 3 | import android.util.Log 4 | import com.gap.hoodies_network.cache.EncryptedCache 5 | import com.gap.hoodies_network.core.Response 6 | import com.gap.hoodies_network.request.StringRequest 7 | import com.gap.hoodies_network.utils.NetworkHelper 8 | import java.io.UnsupportedEncodingException 9 | import java.net.CookieManager 10 | import java.net.URL 11 | import java.net.URLDecoder 12 | 13 | /** 14 | * UrlQueryParamRequest class appends query params to url of request 15 | * 16 | * @param host 17 | * @param method 18 | * @param scheme 19 | * @param queryParams 20 | * @param responseListener 21 | * @param errorListener 22 | * 23 | */ 24 | class UrlQueryParamRequest( 25 | host: String, 26 | method: String, 27 | scheme: String, 28 | queryParams: Map?, 29 | responseListener: Response.ResponseListener?, 30 | errorListener: Response.ErrorListener?, 31 | encryptedCache: EncryptedCache, 32 | cookieManager: CookieManager? 33 | ) : StringRequest( 34 | convertToQueryParamURL(scheme, host, queryParams), 35 | method, 36 | convertToQueryParamURL(scheme, host, queryParams), 37 | responseListener, 38 | errorListener, 39 | encryptedCache, 40 | cookieManager 41 | ) { 42 | companion object { 43 | 44 | @Throws(UnsupportedEncodingException::class) 45 | private fun convertToQueryParamURL( 46 | scheme: String, 47 | host: String, 48 | queryParams: Map? 49 | ): String { 50 | val networkHelper = NetworkHelper() 51 | val finalURL = URL( 52 | URLDecoder.decode( 53 | networkHelper.getFinalURL( 54 | queryParams, host, 55 | scheme 56 | ), "UTF-8" 57 | ) 58 | ).toString() 59 | Log.d("QueryParamURL-", finalURL + "") 60 | return finalURL 61 | 62 | } 63 | 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/MultipleRequestTest.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import com.gap.hoodies_network.core.* 5 | import com.gap.hoodies_network.mockwebserver.ServerManager 6 | import com.gap.hoodies_network.testObjects.testInterceptor 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.runBlocking 9 | import org.junit.After 10 | import org.junit.Assert 11 | import org.junit.Before 12 | import org.junit.Test 13 | 14 | /** 15 | * Implemented tests sending multiple network requests 16 | */ 17 | class MultipleRequestTest { 18 | private val interceptor = testInterceptor(InstrumentationRegistry.getInstrumentation().context) 19 | 20 | data class DelayResponse( 21 | val delay: Int 22 | ) 23 | 24 | @Before 25 | fun startMockWebServer() { 26 | ServerManager.setup(interceptor.context) 27 | } 28 | 29 | @After 30 | fun stopServer() { 31 | ServerManager.stop() 32 | } 33 | 34 | @Test 35 | fun sendMultipleRequest() { 36 | /* You can customize this. */ 37 | val count = 3 38 | val delayList = ArrayList>() 39 | 40 | runBlocking(Dispatchers.IO) { 41 | val client = HoodiesNetworkClient.Builder().baseUrl("http://localhost:6969/").addInterceptor(interceptor).build() 42 | 43 | for( i in 1 .. count ) { 44 | val result = client.get("echo/$i") 45 | delayList.add(result) 46 | } 47 | 48 | Assert.assertEquals(delayList.size, count) 49 | delayList.forEachIndexed { index, item -> 50 | when (item) { 51 | is Success -> { 52 | println("${index + 1} Request Successful -> Delay: {${item.value.delay}}") 53 | } 54 | is Failure -> { 55 | println("${index + 1} Request Failure -> Error: {${item.reason}}") 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /examples/SendGetRequestWithParametersViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.gap.hoodies_network.config.* 8 | import com.gap.hoodies_network.core.HoodiesNetworkError 9 | import com.gap.hoodies_network.core.HoodiesNetworkClient 10 | import com.google.gson.Gson 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.launch 13 | import kotlinx.coroutines.withContext 14 | import java.net.MalformedURLException 15 | import java.util.HashMap 16 | 17 | class SendGetRequestWithParametersViewModel : ViewModel() { 18 | 19 | private var weatherHttpClient: HoodiesNetworkClient = HoodiesNetworkClient.Builder().baseUrl("https://samples.openweathermap.org").build() 20 | 21 | internal var getURLQueryResponse = MutableLiveData() 22 | 23 | internal fun sendGetUrlQueryRequest() { 24 | try { 25 | viewModelScope.launch(Dispatchers.Main) { 26 | val result: Result = withContext(Dispatchers.IO) { 27 | val queryParams: HashMap = HashMap() 28 | queryParams["q"] = "London,uk" 29 | queryParams["appid"] = "2b1fd2d7f77ccf1b7de9b441571b39b8" 30 | val api = "/data/2.5/weather" 31 | weatherHttpClient.getUrlQueryParam( 32 | queryParams = queryParams, 33 | api = api 34 | ) 35 | } 36 | when (result) { 37 | is Success -> { 38 | getURLQueryResponse.postValue(gson.toJson(result.value)) 39 | } 40 | is Failure -> { 41 | getURLQueryResponse.postValue(result.reason.message) 42 | } 43 | } 44 | } 45 | } catch (e: MalformedURLException) { 46 | Log.e("ERROR", e.toString()) 47 | } 48 | } 49 | 50 | 51 | fun getError(HoodiesNetworkError: HoodiesNetworkError): String { 52 | return "Code Error: " + HoodiesNetworkError.code.toString() + " Message: " + HoodiesNetworkError.message 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/request/FileUploadRequest.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.request 2 | 3 | import android.util.Log 4 | import com.gap.hoodies_network.core.* 5 | import com.gap.hoodies_network.header.HttpHeaderParser 6 | import org.json.JSONException 7 | import org.json.JSONObject 8 | import java.io.File 9 | import java.io.UnsupportedEncodingException 10 | import java.net.CookieManager 11 | import java.util.* 12 | 13 | class FileUploadRequest( 14 | url: String?, 15 | files: List, 16 | multipartBoundary: String, 17 | method: HoodiesNetworkClient.HttpMethod, 18 | responseListener: Response.ResponseListener? = null, 19 | errorListener: Response.ErrorListener?, 20 | cookieManager: CookieManager? 21 | ) : FileRequest( 22 | url, 23 | method.value, 24 | files, 25 | multipartBoundary, 26 | responseListener, 27 | errorListener, 28 | cookieManager 29 | ) { 30 | 31 | @Throws(HoodiesNetworkError::class) 32 | override fun parseNetworkResponse(response: Response?): Response? { 33 | return try { 34 | val jsonString = response?.getData()?.let { 35 | Response.toHeaderMap(response.getAllHeaders())?.let { it1 -> 36 | HttpHeaderParser.parseCharset( 37 | it1, 38 | charset(PROTOCOL_CHARSET) 39 | ) 40 | }?.let { it2 -> 41 | String( 42 | it, 43 | it2 44 | ) 45 | } 46 | } 47 | response?.setResultResponse(JSONObject(jsonString!!)) 48 | response 49 | } catch (e: UnsupportedEncodingException) { 50 | Log.e("parseNetworkResponse", e.toString()) 51 | throw HoodiesNetworkError(e.message, UNSUPPORTED_ENCODING_ERROR_CODE) 52 | } catch (e: JSONException) { 53 | Log.e("parseNetworkResponse", e.toString()) 54 | throw HoodiesNetworkError(e.message, JSON_ERROR_CODE) 55 | } catch (e: NullPointerException) { 56 | Log.e("parseNetworkResponse", e.toString()) 57 | throw HoodiesNetworkError(e.message, NULL_POINTER_ERROR_CODE) 58 | } 59 | } 60 | 61 | companion object { 62 | const val PROTOCOL_CHARSET = "utf-8" 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /examples/SendGetRequestWithUrlEncodedParametersViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.gap.hoodies_network.config.* 8 | import com.gap.hoodies_network.core.HoodiesNetworkError 9 | import com.gap.hoodies_network.core.HoodiesNetworkClient 10 | import com.google.gson.Gson 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.launch 13 | import kotlinx.coroutines.withContext 14 | import java.net.MalformedURLException 15 | import java.util.HashMap 16 | 17 | class SendGetRequestWithUrlEncodedParametersViewModel : ViewModel() { 18 | 19 | private var weatherHttpClient: HoodiesNetworkClient = HoodiesNetworkClient.Builder().baseUrl("https://samples.openweathermap.org").build() 20 | 21 | internal var getQueryParamEncodedResponse = MutableLiveData() 22 | 23 | internal fun sendGetQueryParamEncodedRequest() { 24 | try { 25 | viewModelScope.launch(Dispatchers.Main) { 26 | val result: Result = withContext(Dispatchers.IO) { 27 | val queryParams: HashMap = HashMap() 28 | queryParams["q"] = "London,uk" 29 | queryParams["appid"] = "2b1fd2d7f77ccf1b7de9b441571b39b8" 30 | val api = "/data/2.5/weather" 31 | weatherHttpClient.getUrlQueryParamEncoded( 32 | queryParams = queryParams, 33 | api = api 34 | ) 35 | } 36 | when (result) { 37 | is Success -> { 38 | getQueryParamEncodedResponse.postValue(gson.toJson(result.value)) 39 | } 40 | is Failure -> { 41 | getQueryParamEncodedResponse.postValue(result.reason.message) 42 | } 43 | } 44 | } 45 | } catch (e: MalformedURLException) { 46 | Log.e("ERROR", e.toString()) 47 | } 48 | } 49 | 50 | fun getError(HoodiesNetworkError: HoodiesNetworkError): String { 51 | return "Code Error: " + HoodiesNetworkError.code.toString() + " Message: " + HoodiesNetworkError.message 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/mockwebserver/HttpBinClone.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.mockwebserver 2 | 3 | import org.json.JSONObject 4 | import java.lang.StringBuilder 5 | 6 | class HttpBinClone { 7 | fun handleRequest(call: HttpCall) : String { 8 | val response = JSONObject() 9 | val headers = JSONObject() 10 | 11 | val files = JSONObject() 12 | 13 | if (call.getHeaders()["Content-type"]?.firstOrNull()?.contains("multipart/form-data;") == true) { 14 | val boundary = call.getHeaders()["Content-type"]?.firstOrNull()?.split("boundary=")?.last()!! 15 | 16 | val bodyParts = call.getBodyString().split("--$boundary") 17 | 18 | for (part in bodyParts){ 19 | val lines = part.split("\n") 20 | if (lines.size > 3) { 21 | val fileName = lines[1].split(" name=").last().split(";").first() 22 | val fileContents = StringBuilder() 23 | println(lines.size) 24 | lines.subList(3, lines.size).forEach { if (it.isNotBlank()) { fileContents.append(it.replace("\r","")) } } 25 | 26 | files.put(fileName, fileContents) 27 | } 28 | } 29 | } 30 | 31 | response.put("files", files) 32 | 33 | 34 | for (item in call.getHeaders()) { 35 | headers.put(item.key, (item.value.firstOrNull()) ?: "") 36 | } 37 | 38 | val query: String = call.httpExchange.requestURI.query ?: "" 39 | 40 | response.put("headers", headers) 41 | response.put("url", "http://localhost:6969${call.httpExchange.requestURI}$query") 42 | response.put("origin", call.httpExchange.remoteAddress.toString()) 43 | 44 | if (call.getHeaders()["Content-Type"]?.firstOrNull() == "application/x-www-form-urlencoded") { 45 | response.put("data", call.getFormUrlEncodedParameters()) 46 | } else { 47 | response.put("data", call.getBodyString()) 48 | } 49 | 50 | try { 51 | val args = JSONObject() 52 | for (item in call.getFormUrlEncodedParameters()) { 53 | args.put(item.key, item.value) 54 | } 55 | response.put("args", args) 56 | } catch (e: Exception) { 57 | //Unsupported 58 | } 59 | 60 | return response.toString() 61 | } 62 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/delivery/ResponseDeliveryExecutor.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.delivery 2 | 3 | import android.os.Handler 4 | import com.gap.hoodies_network.core.Response 5 | import com.gap.hoodies_network.core.HoodiesNetworkError 6 | import com.gap.hoodies_network.request.Request 7 | import java.util.concurrent.Executor 8 | 9 | 10 | /** 11 | * ResponseDeliveryExecutor class executes response and error for responseDelivery interface 12 | */ 13 | open class ResponseDeliveryExecutor : ResponseDelivery { 14 | private val mResponseExecutor: Executor 15 | 16 | /** 17 | * Creates a new response delivery interface. 18 | * 19 | * @param handler [Handler] to post responses on 20 | */ 21 | constructor(handler: Handler) { 22 | // Make an Executor that just wraps the handler. 23 | mResponseExecutor = Executor { command -> 24 | handler.post(command) 25 | } 26 | } 27 | 28 | /** 29 | * Creates a new response delivery interface, mock able version for testing. 30 | * 31 | * @param executor For running delivery tasks 32 | */ 33 | constructor(executor: Executor) { 34 | mResponseExecutor = executor 35 | } 36 | 37 | override fun postResponse(request: Request, response: Response) { 38 | mResponseExecutor.execute(ResponseDeliveryExecutorRunnable(request, response, null, true)) 39 | } 40 | 41 | override fun postError(request: Request, error: HoodiesNetworkError) { 42 | mResponseExecutor.execute(ResponseDeliveryExecutorRunnable(request, null, error, false)) 43 | } 44 | 45 | private class ResponseDeliveryExecutorRunnable( 46 | request: Request, 47 | response: Response?, 48 | error: HoodiesNetworkError?, 49 | success: Boolean 50 | ) : 51 | Runnable { 52 | private val mRequest: Request = request 53 | private val mResponse: Response? = response 54 | private val isSuccess: Boolean = success 55 | private val hoodiesNetworkError: HoodiesNetworkError? = error 56 | override fun run() { 57 | if (isSuccess) { 58 | mRequest.deliverResponse(mResponse) 59 | } else { 60 | hoodiesNetworkError?.let { 61 | mRequest.deliverError(it) 62 | } 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /examples/SettingSessionInterceptorsAndTimeout.kt: -------------------------------------------------------------------------------- 1 | import android.graphics.Bitmap 2 | import android.widget.ImageView 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.gap.hoodies_network.config.Failure 7 | import com.gap.hoodies_network.config.HoodiesNetworkClient 8 | import com.gap.hoodies_network.config.Success 9 | import com.gap.hoodies_network.core.HoodiesNetworkClient 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.launch 12 | import kotlinx.coroutines.withContext 13 | 14 | /** 15 | * ViewModel to fetch an image from an endpoint 16 | * A Bitmap is returned, various settings are configurable 17 | */ 18 | class ImageRequestViewModel : ViewModel() { 19 | val getImageResponse = MutableLiveData() 20 | 21 | fun sendGetImageRequest() { 22 | //Setting interceptor and default headers 23 | private val sessionInterceptor: SessionInterceptor = SessionInterceptor() 24 | val defaultHeaders = hashMapOf( 25 | CONTENT_TYPE_KEY to APPLICATION_JSON, MULTIDB_ENABLED to MULTIDB_ENABLED_VALUE, 26 | CLIENT_ID to CLIENT_ID_VALUE, 27 | CLIENT_OS to CLIENT_OS_VALUE 28 | ) 29 | var mobileHttpClient: HoodiesNetworkClient = HoodiesNetworkClient.Builder().baseUrl("https://httpbin.org/").addHeaders(defaultHeaders).addInterceptor(sessionInterceptor).build() 30 | 31 | //Set timeouts to 1000ms 32 | HttpClientConfig.setConnectTimeOut(1000) 33 | HttpClientConfig.setReadTimeOut(1000) 34 | 35 | //Reset timeout to default values like this: 36 | //HttpClientConfig.setFactoryDefaultConfiguration() 37 | 38 | viewModelScope.launch(Dispatchers.Main) { 39 | val result = withContext(Dispatchers.IO) { 40 | mobileHttpClient.getImage( 41 | "image", //https://httpbin.org/image 42 | null, 43 | 0, 44 | 0, 45 | ImageView.ScaleType.CENTER, 46 | Bitmap.Config.ALPHA_8 47 | ) 48 | } 49 | when (result) { 50 | is Success -> { 51 | getImageResponse.setValue(result.value) 52 | } 53 | is Failure -> { 54 | getImageResponse.setValue(null) 55 | 56 | } 57 | } 58 | } 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/config/HttpClientConfig.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.config 2 | 3 | import java.time.Duration 4 | 5 | /** 6 | * HttpClientConfig class handles connect timeout and read timeout configs 7 | */ 8 | class HttpClientConfig { 9 | 10 | companion object { 11 | private const val SOCKET_CONNECT_TIMEOUT = 60*1000 12 | private const val SOCKET_READ_TIMEOUT = 60*1000 13 | private var socketConnectTimeout: Int = SOCKET_CONNECT_TIMEOUT 14 | private var socketReadTimeout: Int = SOCKET_READ_TIMEOUT 15 | 16 | /** 17 | * It returns the connect time out in Duration Type seconds 18 | */ 19 | fun getConnectTimeOutDuration(): Duration { 20 | return Duration.ofMillis(socketConnectTimeout.toLong()) 21 | } 22 | 23 | /** 24 | * It returns the read time out in Duration Type seconds 25 | */ 26 | fun getReadTimeoutDuration(): Duration { 27 | return Duration.ofMillis(socketReadTimeout.toLong()) 28 | } 29 | 30 | /** 31 | * Connect Timeout values are in Duration Type seconds. 32 | * Set Time Out Values to zero means no Socket Time Out at all. 33 | * Socket will wait for the connection to complete/connect, if value set to 0. 34 | * If Time out is set to more than 0, Sockets will close/timeout connect operations after the given time. 35 | * */ 36 | fun setConnectTimeOut(connectTimeOutInSeconds: Duration) { 37 | socketConnectTimeout = connectTimeOutInSeconds.toMillis().toInt() 38 | } 39 | 40 | /** 41 | * Read Timeout values are in Duration Type seconds. 42 | * Set Time Out Values to zero means no Socket Time Out at all. 43 | * Socket will wait for the read operation to complete, if value set to 0. 44 | * If Time out is set to more than 0, Sockets will close/timeout read operations after the given time. 45 | * */ 46 | fun setReadTimeOut(readTimeOutInSeconds: Duration) { 47 | socketReadTimeout = readTimeOutInSeconds.toMillis().toInt() 48 | } 49 | 50 | /** 51 | * This function will set the http client configuration to factory defaults 52 | */ 53 | 54 | fun setFactoryDefaultConfiguration() { 55 | socketConnectTimeout = SOCKET_CONNECT_TIMEOUT 56 | socketReadTimeout = SOCKET_READ_TIMEOUT 57 | } 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/cookies/persistentstorage/EncryptedDaoWrapperForCookies.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.cookies.persistentstorage 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import com.gap.hoodies_network.cache.EncryptedCache 6 | import com.google.gson.Gson 7 | import java.net.HttpCookie 8 | import java.net.URI 9 | import java.util.* 10 | import javax.crypto.Cipher 11 | 12 | class EncryptedDaoWrapperForCookies(instanceName: String, context: Context) { 13 | private val db = Room.databaseBuilder(context, EncryptedCookieDatabase::class.java, instanceName).build().encryptedCookieDao() 14 | 15 | fun getAll() : List { 16 | return db.getAll().map{ decryptCookie(it).cookie }.toList() 17 | } 18 | 19 | fun getByHost(host: URI) : List { 20 | return db.getByHost(uriToHost(host)).map{ decryptCookie(it).cookie }.toList() 21 | } 22 | 23 | private fun decryptCookie(encryptedCookie: EncryptedCookie) : CookieAndId { 24 | val iv = Base64.getDecoder().decode(encryptedCookie.iv) 25 | val decryptedCookieJson = EncryptedCache.runAES(Base64.getDecoder().decode(encryptedCookie.cookie), iv, Cipher.DECRYPT_MODE).decodeToString() 26 | 27 | return CookieAndId(Gson().fromJson(decryptedCookieJson, HttpCookie::class.java), encryptedCookie.id) 28 | } 29 | 30 | fun deleteAll() { 31 | db.deleteAll() 32 | } 33 | 34 | fun deleteCookie(cookie: HttpCookie) : Boolean { 35 | return db.deleteByHash(cookie.hashCode()) > 0 36 | } 37 | 38 | fun getAllHosts() : List { 39 | return db.getAllHosts().map{ URI(it) }.toList() 40 | } 41 | 42 | fun insert(host: URI, cookie: HttpCookie) { 43 | val cookieJson = Gson().toJson(cookie) 44 | var iv = EncryptedCache.genIV() 45 | 46 | //Make sure the IV is unique 47 | while (db.getByIv(Base64.getEncoder().encodeToString(iv)).isNotEmpty()) 48 | iv = EncryptedCache.genIV() 49 | 50 | val encryptedCookieJson = Base64.getEncoder().encodeToString(EncryptedCache.runAES(cookieJson.encodeToByteArray(), iv, Cipher.ENCRYPT_MODE)) 51 | 52 | db.insert(EncryptedCookie(0, uriToHost(host), encryptedCookieJson, Base64.getEncoder().encodeToString(iv), cookie.hashCode())) 53 | } 54 | 55 | private fun uriToHost(uri: URI) : String { 56 | return "${uri.scheme.replace("https", "http")}://${uri.host}" 57 | } 58 | 59 | data class CookieAndId(val cookie: HttpCookie, val id: Int) 60 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/EncryptionDecryptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network 2 | 3 | import android.os.Build 4 | import androidx.annotation.RequiresApi 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import androidx.test.platform.app.InstrumentationRegistry 7 | import com.gap.hoodies_network.mockwebserver.ServerManager 8 | import org.junit.After 9 | import org.junit.Assert 10 | import org.junit.Before 11 | import org.junit.Test 12 | import org.junit.runner.RunWith 13 | import java.util.* 14 | import javax.crypto.Cipher 15 | import javax.crypto.spec.IvParameterSpec 16 | import javax.crypto.spec.SecretKeySpec 17 | 18 | @RunWith(AndroidJUnit4::class) 19 | class EncryptionDecryptionTest { 20 | val mContext = InstrumentationRegistry.getInstrumentation().context 21 | 22 | @Before 23 | fun startMockWebServer() { 24 | ServerManager.setup(mContext) 25 | } 26 | 27 | @After 28 | fun stopServer() { 29 | ServerManager.stop() 30 | } 31 | 32 | @Test 33 | fun encryptAndDecryptString() { 34 | val algorithm = "AES/CBC/PKCS5Padding" 35 | val key = SecretKeySpec("1234567890123456".toByteArray(), "AES") 36 | val iv = IvParameterSpec(ByteArray(16)) 37 | val testString = "test12345" 38 | val encryptedString = encrypt( 39 | algorithm = algorithm, 40 | key = key, 41 | iv = iv, 42 | inputText = testString 43 | ) 44 | val decryptedString = decrypt( 45 | algorithm = algorithm, 46 | key = key, 47 | iv = iv, 48 | cipherText = encryptedString 49 | ) 50 | 51 | Assert.assertEquals(decryptedString, testString) 52 | } 53 | 54 | @RequiresApi(Build.VERSION_CODES.O) 55 | fun encrypt(algorithm: String, inputText: String, key: SecretKeySpec, iv: IvParameterSpec): String { 56 | val cipher = Cipher.getInstance(algorithm) 57 | cipher.init(Cipher.ENCRYPT_MODE, key, iv) 58 | val cipherText = cipher.doFinal(inputText.toByteArray()) 59 | return Base64.getEncoder().encodeToString(cipherText) 60 | } 61 | 62 | @RequiresApi(Build.VERSION_CODES.O) 63 | fun decrypt(algorithm: String, cipherText: String, key: SecretKeySpec, iv: IvParameterSpec): String { 64 | val cipher = Cipher.getInstance(algorithm) 65 | cipher.init(Cipher.DECRYPT_MODE, key, iv) 66 | val plainText = cipher.doFinal(Base64.getDecoder().decode(cipherText)) 67 | return String(plainText) 68 | } 69 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/mockwebserver/WebServerHandler.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.mockwebserver 2 | 3 | import com.gap.hoodies_network.utils.Generated 4 | import com.sun.net.httpserver.HttpExchange 5 | import com.sun.net.httpserver.HttpHandler 6 | 7 | /** 8 | * Base class for handling requests in the MockWebServer 9 | * For every endpoint being served by the MockWebServer, a class inheriting from WebServerHandler must be made 10 | * Then, the class needs to override handleRequest(call: HttpCall) in order to handle the request using a ktor-like syntax 11 | */ 12 | @Generated 13 | open class WebServerHandler { 14 | var postRunnable: Runnable? = null 15 | var getRunnable: Runnable? = null 16 | var putRunnable: Runnable? = null 17 | var deleteRunnable: Runnable? = null 18 | var optionsRunnable: Runnable? = null 19 | var patchRunnable: Runnable? = null 20 | 21 | val internalHandler = HttpHandler { exchange -> 22 | handleRequest(HttpCall(exchange)) 23 | 24 | when(exchange.requestMethod) { 25 | "POST" -> postRunnable?.run() ?: return405(exchange) 26 | "GET" -> getRunnable?.run() ?: return405(exchange) 27 | "PUT" -> putRunnable?.run() ?: return405(exchange) 28 | "DELETE" -> deleteRunnable?.run() ?: return405(exchange) 29 | "OPTIONS" -> optionsRunnable?.run() ?: return405(exchange) 30 | "PATCH" -> patchRunnable?.run() ?: return405(exchange) 31 | } 32 | } 33 | 34 | private fun return405(exchange: HttpExchange) { 35 | val error = "Method not allowed" 36 | exchange.sendResponseHeaders(405, error.length.toLong()) 37 | exchange.responseBody.write(error.encodeToByteArray()) 38 | exchange.responseBody.flush() 39 | exchange.responseBody.close() 40 | } 41 | 42 | /** 43 | * This class needs to be overridden with your logic 44 | */ 45 | open fun handleRequest(call: HttpCall) { 46 | //Left open for overriding 47 | } 48 | 49 | fun post(runnable: Runnable) { 50 | postRunnable = runnable 51 | } 52 | 53 | fun get(runnable: Runnable) { 54 | getRunnable = runnable 55 | } 56 | 57 | fun put(runnable: Runnable) { 58 | putRunnable = runnable 59 | } 60 | 61 | fun delete(runnable: Runnable) { 62 | deleteRunnable = runnable 63 | } 64 | 65 | fun options(runnable: Runnable) { 66 | optionsRunnable = runnable 67 | } 68 | 69 | fun patch(runnable: Runnable) { 70 | patchRunnable = runnable 71 | } 72 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/UrlResolverTest.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import com.gap.hoodies_network.config.UrlResolver 5 | import org.junit.Assert 6 | import org.junit.Assert.assertEquals 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | import java.net.URL 10 | 11 | @RunWith(AndroidJUnit4::class) 12 | class UrlResolverTest { 13 | @Test 14 | fun resolveHttpProtocolHttpsTest() { 15 | val url = URL("https://gap.com/order") 16 | val urlString= UrlResolver.resolveHttpProtocol(url) 17 | assertEquals("https", urlString) 18 | } 19 | 20 | @Test 21 | fun resolveHttpProtocolHttpTest(){ 22 | val url = URL("http://gap.com/order") 23 | val urlString = UrlResolver.resolveHttpProtocol(url) 24 | assertEquals("http", urlString) 25 | } 26 | 27 | @Test 28 | fun resolveUrlWithoutHttpTest(){ 29 | val url = UrlResolver.resolveUrl("gap.com/order") 30 | assertEquals("gap.com/order", url) 31 | } 32 | @Test 33 | fun resolveUrlWithHttpTest(){ 34 | val url = UrlResolver.resolveUrl("http://gap.com/order") 35 | assertEquals("gap.com/order", url) 36 | } 37 | 38 | @Test 39 | fun validateUrlTest(){ 40 | val url = UrlResolver.validateUrl("https://gap.com/order") 41 | val url1 = UrlResolver.validateUrl("http://gap.com/order") 42 | val url2 = UrlResolver.validateUrl("gap.com/order") 43 | Assert.assertEquals("https://gap.com/order",url) 44 | Assert.assertEquals("http://gap.com/order",url1) 45 | Assert.assertEquals("https://gap.com/order",url2) 46 | } 47 | 48 | @Test 49 | fun getProtocolTest(){ 50 | val url = UrlResolver.getProtocol("https://gap.com/order") 51 | Assert.assertEquals("https", url) 52 | } 53 | 54 | @Test 55 | fun wrongProtocolFixed() { 56 | val url = UrlResolver.resolveHttpProtocol(URL("ftp://gap.com")) 57 | Assert.assertEquals(url, "https") 58 | } 59 | 60 | @Test 61 | fun rawIP() { 62 | val res = UrlResolver.resolveUrl("8.8.8.8") 63 | assertEquals(res, "8.8.8.8") 64 | } 65 | 66 | @Test 67 | fun validateURL() { 68 | var url = UrlResolver.validateUrl("https:/gap.com") 69 | Assert.assertEquals(url, "https://gap.com") 70 | 71 | url = UrlResolver.validateUrl("http:/gap.com") 72 | Assert.assertEquals(url, "http://gap.com") 73 | 74 | url = UrlResolver.validateUrl("://gap.com") 75 | Assert.assertEquals(url, "https://gap.com") 76 | 77 | url = UrlResolver.validateUrl("//gap.com") 78 | Assert.assertEquals(url, "https://gap.com") 79 | } 80 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/utils/NetworkHelper.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.utils 2 | 3 | import android.net.Uri 4 | import android.util.Log 5 | import com.gap.hoodies_network.core.* 6 | import com.gap.hoodies_network.header.HttpHeaderParser 7 | import org.json.JSONException 8 | import org.json.JSONObject 9 | import java.io.UnsupportedEncodingException 10 | 11 | class NetworkHelper { 12 | 13 | 14 | fun getFinalURL( 15 | queryParams: Map?, 16 | host: String, 17 | scheme: String 18 | ): String { 19 | var mUrl = Uri.Builder() 20 | 21 | mUrl = getAppendQueryParameter(queryParams, mUrl) 22 | return scheme+"://"+host+mUrl.build().toString() 23 | } 24 | 25 | private fun getAppendQueryParameter(queryParams: Map?, uri: Uri.Builder) : Uri.Builder { 26 | uri.apply { 27 | if (queryParams != null) { 28 | for ((key, value) in queryParams) { 29 | appendQueryParameter(key, value) 30 | Log.d("queryParams=", ("$key = $value").toString()) 31 | } 32 | } 33 | } 34 | 35 | return uri 36 | } 37 | 38 | fun getParseNetworkResponse(response: Response?, array: Boolean = false): Response?{ 39 | try { 40 | val jsonString = response?.getData()?.let { 41 | Response.toHeaderMap(response.getAllHeaders())?.let { it1 -> 42 | HttpHeaderParser.parseCharset( 43 | it1, 44 | charset(NetworkHelper.PROTOCOL_CHARSET) 45 | ) 46 | }?.let { it2 -> 47 | String( 48 | it, 49 | it2 50 | ) 51 | } 52 | } 53 | 54 | if (array) 55 | response?.setResultResponse(jsonString!!) 56 | else 57 | response?.setResultResponse(JSONObject(jsonString!!)) 58 | 59 | return response 60 | } catch (e: UnsupportedEncodingException) { 61 | Log.e("parseNetworkResponse", e.toString()) 62 | throw HoodiesNetworkError(e.message, UNSUPPORTED_ENCODING_ERROR_CODE) 63 | } catch (e: JSONException) { 64 | Log.e("parseNetworkResponse", e.toString()) 65 | throw HoodiesNetworkError(e.message, JSON_ERROR_CODE) 66 | } catch (e: NullPointerException) { 67 | Log.e("parseNetworkResponse", e.toString()) 68 | throw HoodiesNetworkError(e.message, NULL_POINTER_ERROR_CODE) 69 | } 70 | } 71 | companion object { 72 | const val PROTOCOL_CHARSET = "utf-8" 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/keystore/CacheKeyManager.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.keystore 2 | 3 | import android.security.keystore.KeyGenParameterSpec 4 | import android.security.keystore.KeyProperties 5 | import java.security.KeyStore 6 | import javax.crypto.KeyGenerator 7 | import javax.crypto.SecretKey 8 | 9 | 10 | class CacheKeyManager { 11 | companion object { 12 | //This is our key alias 13 | private const val keyAlias = "HoodiesNetworkCacheKey" 14 | 15 | //This is our key 16 | private var key: SecretKey? = null 17 | 18 | /** 19 | * If our key has been cached, returns it immediately 20 | * Otherwise, checks if a key has been generated and generates new one if necessary 21 | * Then, fetches key, caches, and returns it 22 | */ 23 | @Synchronized 24 | fun getKey() : SecretKey { 25 | if (key != null) 26 | return key!! 27 | 28 | if (!doesKeyExist()) 29 | generateNewKey() 30 | 31 | //Create keystore instance 32 | val keystore = KeyStore.getInstance("AndroidKeyStore") 33 | keystore.load(null) 34 | 35 | //Otherwise, let's fetch the existing key 36 | println("HoodiesNetworkCache fetching existing key from KeyStore") 37 | val secretKeyEntry = keystore.getEntry(keyAlias, null) as KeyStore.SecretKeyEntry 38 | key = secretKeyEntry.secretKey 39 | return key!! 40 | } 41 | 42 | /** 43 | * Create keystore instance and check if key exists 44 | */ 45 | @Synchronized 46 | private fun doesKeyExist() : Boolean { 47 | println("HoodiesNetworkCache checking if encryption key exists") 48 | val keystore = KeyStore.getInstance("AndroidKeyStore") 49 | keystore.load(null) 50 | return keystore.containsAlias(keyAlias) 51 | } 52 | 53 | /** 54 | * Generate a new key and store in KeyStore 55 | */ 56 | @Synchronized 57 | private fun generateNewKey() { 58 | println("HoodiesNetworkCache generating new encryption key and putting into KeyStore") 59 | val keyGenerator = KeyGenerator 60 | .getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") 61 | val keyGenParameterSpec = KeyGenParameterSpec.Builder( 62 | keyAlias, 63 | KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT 64 | ) 65 | .setBlockModes(KeyProperties.BLOCK_MODE_GCM) 66 | .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) 67 | .setRandomizedEncryptionRequired(false) //We need this so we can provide our own IV 68 | .build() 69 | 70 | keyGenerator.init(keyGenParameterSpec) 71 | keyGenerator.generateKey() 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/mockwebserver/HttpCall.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.mockwebserver 2 | 3 | import android.net.Uri 4 | import com.gap.hoodies_network.utils.Generated 5 | import com.sun.net.httpserver.Headers 6 | import com.sun.net.httpserver.HttpExchange 7 | 8 | /** 9 | * Provides a friendly interface for interacting with HTTP requests and sending responses 10 | */ 11 | @Generated 12 | class HttpCall(val httpExchange: HttpExchange) { 13 | 14 | /** 15 | * Returns the request's body as a ByteArray 16 | */ 17 | fun getBodyByteArray() : ByteArray { 18 | return httpExchange.requestBody.readBytes() 19 | } 20 | 21 | /** 22 | * Returns the request's body as a String 23 | */ 24 | fun getBodyString() : String { 25 | return getBodyByteArray().decodeToString() 26 | } 27 | 28 | /** 29 | * Gets FormUrlEncoded parameters from the GET request's URL or non-GET request body 30 | */ 31 | fun getFormUrlEncodedParameters() : Map { 32 | val body = if (httpExchange.requestMethod == "GET") { 33 | httpExchange.requestURI.query 34 | } else { 35 | getBodyString() 36 | } 37 | 38 | val map = hashMapOf() 39 | 40 | //take the String with POST params and turn it into a HashMap 41 | var last = 0 42 | var next: Int 43 | val l = body.length 44 | while (last < l) { 45 | next = body.indexOf('&', last) 46 | if (next == -1) next = l 47 | if (next > last) { 48 | val eqPos: Int = body.indexOf('=', last) 49 | if (eqPos < 0 || eqPos > next) { 50 | map[Uri.decode((body.substring(last, next)))] = "" 51 | } else { 52 | map[Uri.decode(body.substring(last, eqPos))] = Uri.decode(body.substring(eqPos + 1, next)) 53 | } 54 | } 55 | last = next + 1 56 | } 57 | 58 | return map 59 | } 60 | 61 | /** 62 | * Returns the request headers 63 | */ 64 | fun getHeaders() : Headers { 65 | return httpExchange.requestHeaders 66 | } 67 | 68 | /** 69 | * Used to set the response headers 70 | */ 71 | fun setResponseHeaders(headers: Headers) { 72 | for (header in headers) 73 | httpExchange.responseHeaders[header.key] = header.value 74 | } 75 | 76 | /** 77 | * Used to respond with an HTTP status code and response String 78 | */ 79 | fun respond(code: Int, response: String) { 80 | respond(code, response.toByteArray()) 81 | } 82 | 83 | /** 84 | * Used to respond with an HTTP status code and response ByteArray 85 | */ 86 | fun respond(code: Int, response: ByteArray) { 87 | httpExchange.sendResponseHeaders(code, response.size.toLong()) 88 | httpExchange.responseBody.write(response) 89 | httpExchange.responseBody.flush() 90 | httpExchange.responseBody.close() 91 | } 92 | } -------------------------------------------------------------------------------- /jacoco.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'jacoco' 2 | 3 | jacoco { 4 | toolVersion = '0.8.7' 5 | reportsDir = file("$buildDir/reports") 6 | } 7 | 8 | configurations.all{ 9 | resolutionStrategy { 10 | eachDependency { details -> 11 | if ('org.jacoco' == details.requested.group) { 12 | details.useVersion "0.8.7" 13 | } 14 | } 15 | } 16 | } 17 | 18 | tasks.withType(Test) { 19 | jacoco.includeNoLocationClasses = true 20 | jacoco.excludes = ['jdk.internal.*'] 21 | } 22 | 23 | task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest', 'createDebugCoverageReport']) { 24 | reports { 25 | xml.enabled = true 26 | html.enabled = true 27 | csv.enabled = true 28 | } 29 | 30 | def fileFilter = [ 31 | // data binding 32 | 'android/databinding/**/*.class', 33 | '**/android/databinding/*Binding.class', 34 | '**/android/databinding/*', 35 | '**/androidx/databinding/*', 36 | '**/BR.*', 37 | // android 38 | '**/R.class', 39 | '**/R$*.class', 40 | '**/BuildConfig.*', 41 | '**/Manifest*.*', 42 | '**/*Test*.*', 43 | 'android/**/*.*', 44 | // kotlin 45 | '**/*MapperImpl*.*', 46 | '**/*$ViewInjector*.*', 47 | '**/*$ViewBinder*.*', 48 | '**/BuildConfig.*', 49 | '**/*Component*.*', 50 | '**/*BR*.*', 51 | '**/Manifest*.*', 52 | '**/*$Lambda$*.*', 53 | '**/*Companion*.*', 54 | '**/*Module*.*', 55 | '**/*Dagger*.*', 56 | '**/*Hilt*.*', 57 | '**/*MembersInjector*.*', 58 | '**/*_MembersInjector.class', 59 | '**/*_Factory*.*', 60 | '**/*_Provide*Factory*.*', 61 | '**/*Extensions*.*', 62 | // sealed and data classes 63 | '**/*$Result.*', 64 | '**/*$Result$*.*', 65 | // adapters generated by moshi 66 | '**/*JsonAdapter.*', 67 | ] 68 | 69 | def kotlinClasses = fileTree(dir: "$buildDir/tmp/kotlin-classes/debug", excludes: fileFilter) 70 | 71 | def javaClasses = fileTree(dir: "$buildDir/intermediates/javac/debug/classes/", excludes: fileFilter) 72 | 73 | sourceDirectories.setFrom(files(["src/main/kotlin"])) 74 | 75 | classDirectories.setFrom(files([ kotlinClasses, javaClasses ] )) 76 | 77 | executionData.setFrom(fileTree(dir: "$buildDir", includes: [ "jacoco/testReleaseUnitTest.exec", "outputs/code_coverage/debugAndroidTest/connected/*.ec", "outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec" ])) 78 | } 79 | -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/RetryTest.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNCHECKED_CAST") 2 | 3 | package com.gap.hoodies_network 4 | 5 | import android.content.Context 6 | import androidx.test.ext.junit.runners.AndroidJUnit4 7 | import androidx.test.platform.app.InstrumentationRegistry 8 | import com.gap.hoodies_network.cache.configuration.CacheDisabled 9 | import com.gap.hoodies_network.config.HttpClientConfig 10 | import com.gap.hoodies_network.core.Failure 11 | import com.gap.hoodies_network.core.HoodiesNetworkClient 12 | import com.gap.hoodies_network.core.HoodiesNetworkError 13 | import com.gap.hoodies_network.core.Result 14 | import com.gap.hoodies_network.core.Success 15 | import com.gap.hoodies_network.mockwebserver.ServerManager 16 | import com.gap.hoodies_network.request.Request 17 | import com.gap.hoodies_network.request.RetryableCancellableMutableRequest 18 | import kotlinx.coroutines.CancellableContinuation 19 | import kotlinx.coroutines.runBlocking 20 | import kotlinx.coroutines.suspendCancellableCoroutine 21 | import org.junit.After 22 | import org.junit.Assert 23 | import org.junit.Before 24 | import org.junit.Test 25 | import org.junit.runner.RunWith 26 | import java.time.Duration 27 | 28 | 29 | @RunWith(AndroidJUnit4::class) 30 | class RetryTest { 31 | val mContext = InstrumentationRegistry.getInstrumentation().context 32 | 33 | @Before 34 | fun startMockWebServer() { 35 | ServerManager.setup(mContext) 36 | } 37 | 38 | @After 39 | fun stopServer() { 40 | ServerManager.stop() 41 | } 42 | 43 | class testInterceptor(context: Context) : com.gap.hoodies_network.interceptor.Interceptor(context) { 44 | var runs = 0 45 | 46 | override fun interceptError( 47 | error: HoodiesNetworkError, 48 | retryableCancellableMutableRequest: RetryableCancellableMutableRequest, 49 | autoRetryAttempts: Int 50 | ) { 51 | runs = autoRetryAttempts 52 | } 53 | } 54 | 55 | val interceptor = testInterceptor(InstrumentationRegistry.getInstrumentation().targetContext) 56 | 57 | @Test 58 | fun retryRequest() { 59 | runBlocking { 60 | 61 | val client = HoodiesNetworkClient.Builder() 62 | .baseUrl("http://localhost:6969/") 63 | .addInterceptor(interceptor) 64 | .retryOnConnectionFailure(true, HoodiesNetworkClient.RetryCount.RETRY_TWICE) 65 | .build().nonInlinedClient 66 | 67 | HttpClientConfig.setConnectTimeOut(Duration.ofSeconds(2)) 68 | HttpClientConfig.setReadTimeOut(Duration.ofSeconds(2)) 69 | 70 | val result = suspendCancellableCoroutine { continuation: CancellableContinuation> -> 71 | val request = client.getRequestUrlQueryParamEncoded( 72 | "echo/10","id", HoodiesNetworkClient.HttpMethod.GET, hashMapOf(), 73 | continuation = continuation, resultType = String::class.java 74 | ) 75 | client.sendRequest("id", request as Request, continuation, CacheDisabled()) 76 | } 77 | 78 | when (result) { 79 | is Success -> { 80 | throw Exception("This request should've timed out") 81 | } 82 | is Failure -> { 83 | Assert.assertEquals(interceptor.runs, 3) 84 | } 85 | } 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/connection/queue/RequestQueue.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.connection.queue 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import android.util.Log 6 | import com.gap.hoodies_network.connection.BaseNetwork 7 | import com.gap.hoodies_network.connection.Network 8 | import com.gap.hoodies_network.delivery.ResponseDelivery 9 | import com.gap.hoodies_network.delivery.ResponseDeliveryExecutor 10 | import com.gap.hoodies_network.request.Request 11 | import java.util.concurrent.PriorityBlockingQueue 12 | import javax.net.ssl.SSLSocketFactory 13 | 14 | /** 15 | * RequestQueue class handles enqueue and dequeue of requests' queue 16 | * 17 | * @param sslHost 18 | * @param sslSocketFactory 19 | * 20 | */ 21 | class RequestQueue constructor(sslHost: String?, sslSocketFactory: SSLSocketFactory?) { 22 | 23 | private val mNetworkQueue: PriorityBlockingQueue> = 24 | PriorityBlockingQueue>() 25 | 26 | /** The gapnetworkandroid dispatchers. */ 27 | private val mQueueDispatchers: Array = 28 | arrayOfNulls(DEFAULT_NETWORK_THREAD_POOL_SIZE) 29 | private val mNetwork: Network 30 | private val mResponseDelivery: ResponseDelivery 31 | 32 | fun enqueue(request: Request) { 33 | try { 34 | mNetworkQueue.add(request) 35 | } catch (e: Exception) { 36 | Log.e("exception in enqueue", e.toString()) 37 | } 38 | } 39 | 40 | fun dequeue(): Request? { 41 | return mNetworkQueue.poll() 42 | } 43 | 44 | fun hasItems(): Boolean { 45 | return !mNetworkQueue.isEmpty() 46 | } 47 | 48 | fun size(): Int { 49 | return mNetworkQueue.size 50 | } 51 | 52 | /** 53 | * Starts the dispatchers in this queue 54 | */ 55 | private fun startDispatchers() { 56 | /** If any currently dispatchers are running, stop them */ 57 | stopDispatchers() 58 | 59 | /**create n/w dispatchers up to the pool size */ 60 | for (i in mQueueDispatchers.indices) { 61 | val apiManager = NetworkHandler(mResponseDelivery) 62 | val networkDispatcher = QueueHandler(mNetworkQueue, apiManager, mNetwork) 63 | mQueueDispatchers[i] = networkDispatcher 64 | networkDispatcher.start() 65 | } 66 | } 67 | 68 | private fun stopDispatchers() { 69 | for (mQueueDispatcher in mQueueDispatchers) { 70 | mQueueDispatcher?.quit() 71 | } 72 | } 73 | 74 | companion object { 75 | private var requestQueue: RequestQueue? = null 76 | private const val DEFAULT_NETWORK_THREAD_POOL_SIZE = 4 77 | val instance: RequestQueue? 78 | get() { 79 | synchronized(RequestQueue::class.java) { 80 | if (requestQueue == null) { 81 | requestQueue = RequestQueue(null, null) 82 | } 83 | return requestQueue 84 | } 85 | } 86 | 87 | fun getInstance(sslHost: String?, sslSocketFactory: SSLSocketFactory?): RequestQueue? { 88 | synchronized(RequestQueue::class.java) { 89 | if (requestQueue == null) { 90 | requestQueue = RequestQueue(sslHost, sslSocketFactory) 91 | } 92 | return requestQueue 93 | } 94 | } 95 | } 96 | 97 | init { 98 | mNetwork = BaseNetwork(sslHost, sslSocketFactory) 99 | mResponseDelivery = ResponseDeliveryExecutor(Handler(Looper.getMainLooper())) 100 | startDispatchers() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/core/Response.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNCHECKED_CAST") 2 | 3 | package com.gap.hoodies_network.core 4 | 5 | import android.graphics.Bitmap 6 | import com.gap.hoodies_network.header.Header 7 | import java.net.HttpURLConnection 8 | import java.util.* 9 | 10 | /** 11 | * Response class handles response data 12 | */ 13 | 14 | class Response { 15 | private var bitmap: Bitmap? = null 16 | var statusCode = 0 17 | private var data: ByteArray? = null 18 | private var allHeaders: List
? = null 19 | var networkTimeMs: Long = 0 20 | var result: T? = null 21 | var url: String? = null 22 | 23 | internal constructor( 24 | statusCode: Int, 25 | data: ByteArray?, 26 | networkTimeMs: Long, 27 | allHeaders: List? 28 | ) : this(statusCode, data, allHeaders, networkTimeMs) 29 | 30 | internal constructor( 31 | statusCode: Int, 32 | data: ByteArray?, 33 | networkTimeMs: Long, 34 | allHeaders: List?, 35 | url: String 36 | ) : this(statusCode, data, allHeaders, networkTimeMs) { 37 | this.url = url 38 | } 39 | 40 | internal constructor(data: ByteArray?) : this( 41 | HttpURLConnection.HTTP_OK, 42 | data, /* networkTimeMs= */ 43 | 0, 44 | emptyList
() 45 | ) 46 | 47 | internal constructor(bitmap: Bitmap?) { 48 | this.bitmap = bitmap 49 | } 50 | 51 | fun getBitmap(): Bitmap? { 52 | return bitmap 53 | } 54 | 55 | fun setBitmap(bitmap: Bitmap?) { 56 | this.bitmap = bitmap 57 | } 58 | 59 | fun getData(): ByteArray? { 60 | return data 61 | } 62 | 63 | fun setData(data: ByteArray?) { 64 | this.data = data 65 | } 66 | 67 | internal constructor( 68 | statusCode: Int, 69 | data: ByteArray?, 70 | allHeaders: List?, 71 | networkTimeMs: Long 72 | ) { 73 | this.statusCode = statusCode 74 | this.data = data 75 | if (allHeaders == null) { 76 | this.allHeaders = null 77 | } else { 78 | this.allHeaders = Collections.unmodifiableList(allHeaders) as List
? 79 | } 80 | this.networkTimeMs = networkTimeMs 81 | } 82 | 83 | constructor(statusCode: Int) { 84 | this.statusCode = statusCode 85 | } 86 | 87 | fun setResultResponse(result: T) { 88 | this.result = result 89 | } 90 | 91 | fun getAllHeaders(): List
? { 92 | return allHeaders 93 | } 94 | 95 | /** gapnetworkandroid calls response listeners */ 96 | fun interface ResponseListener { 97 | fun onResponse(response: Response?) 98 | } 99 | /** gapnetworkandroid calls error listeners */ 100 | fun interface ErrorListener { 101 | fun onErrorResponse(error: HoodiesNetworkError) 102 | } 103 | 104 | /** bitmap calls listeners */ 105 | interface BitmapResponseListener { 106 | fun onResponse(bitmap: Bitmap?) 107 | fun onError(response: HoodiesNetworkError) 108 | } 109 | 110 | companion object { 111 | fun toHeaderMap(allHeaders: List
?): Map? { 112 | 113 | if (allHeaders == null) { 114 | return null 115 | } 116 | if (allHeaders.isEmpty()) { 117 | return emptyMap() 118 | } 119 | val headers: MutableMap = 120 | TreeMap(java.lang.String.CASE_INSENSITIVE_ORDER) 121 | /** Later elements in the list take precedence.*/ 122 | for (header in allHeaders) { 123 | headers[header.getName()] = header.getValue() 124 | } 125 | return headers 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/request/StringRequest.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.request 2 | 3 | import android.util.Log 4 | import androidx.annotation.GuardedBy 5 | import com.gap.hoodies_network.cache.EncryptedCache 6 | import com.gap.hoodies_network.core.* 7 | import com.gap.hoodies_network.header.HttpHeaderParser 8 | import com.gap.hoodies_network.core.HoodiesNetworkError 9 | import com.gap.hoodies_network.core.NULL_POINTER_ERROR_CODE 10 | import com.gap.hoodies_network.core.UNSUPPORTED_ENCODING_ERROR_CODE 11 | import org.json.JSONObject 12 | import java.io.UnsupportedEncodingException 13 | import java.net.CookieManager 14 | 15 | 16 | /** 17 | * StringRequest class handles the string requests 18 | * 19 | * @param url 20 | * @param method 21 | * @param requestBody 22 | * @param responseListener 23 | * @param errorListener 24 | * 25 | */ 26 | open class StringRequest( 27 | url: String, method: String, requestBody: String, 28 | @GuardedBy("mLock") private val responseListener: Response.ResponseListener?, 29 | errorListener: Response.ErrorListener?, 30 | encryptedCache: EncryptedCache, 31 | cookieManager: CookieManager? 32 | ) : Request(url, method, requestBody, errorListener, encryptedCache, cookieManager) { 33 | private val mLock = Any() 34 | 35 | constructor( 36 | url: String, 37 | method: String, 38 | jsonRequest: JSONObject? = null, 39 | encryptedCache: EncryptedCache, 40 | cookieManager: CookieManager? 41 | ) : this( 42 | url, 43 | method, 44 | jsonRequest, 45 | null, 46 | null, 47 | encryptedCache, 48 | cookieManager 49 | ) 50 | 51 | constructor( 52 | url: String, 53 | method: String, 54 | responseListener: Response.ResponseListener?, 55 | errorListener: Response.ErrorListener?, 56 | encryptedCache: EncryptedCache, 57 | cookieManager: CookieManager? 58 | ) : this( 59 | url, 60 | method, 61 | null as JSONObject?, 62 | responseListener, 63 | errorListener, 64 | encryptedCache, 65 | cookieManager 66 | ) 67 | 68 | constructor( 69 | url: String, 70 | method: String, 71 | requestBody: JSONObject?, 72 | responseListener: Response.ResponseListener?, 73 | errorListener: Response.ErrorListener?, 74 | encryptedCache: EncryptedCache, 75 | cookieManager: CookieManager? 76 | ) : this( 77 | url, 78 | method, 79 | requestBody.toString(), 80 | responseListener, 81 | errorListener, 82 | encryptedCache, 83 | cookieManager 84 | ) 85 | 86 | @Throws(HoodiesNetworkError::class) 87 | override fun parseNetworkResponse(response: Response?): Response? { 88 | val parsedResponse: String 89 | return try { 90 | parsedResponse = response?.getData()?.let { 91 | Response.toHeaderMap(response.getAllHeaders())?.let { it1 -> 92 | HttpHeaderParser.parseCharset( 93 | it1 94 | ) 95 | 96 | }?.let { it2 -> 97 | String( 98 | it, 99 | it2 100 | ) 101 | } 102 | 103 | }.toString() 104 | response?.setResultResponse(parsedResponse) 105 | response 106 | } catch (e: UnsupportedEncodingException) { 107 | Log.e("parseNetworkResponse", e.toString()) 108 | throw HoodiesNetworkError(e.message, UNSUPPORTED_ENCODING_ERROR_CODE) 109 | } catch (e: NullPointerException) { 110 | Log.e("parseNetworkResponse", e.toString()) 111 | throw HoodiesNetworkError(e.message, NULL_POINTER_ERROR_CODE) 112 | } 113 | } 114 | 115 | override fun deliverResponse(response: Response?) { 116 | var listener: Response.ResponseListener? 117 | synchronized(mLock) { listener = responseListener } 118 | listener?.onResponse(response) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/mockwebserver/Html.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.mockwebserver 2 | 3 | 4 | class Html : WebServerHandler() { 5 | override fun handleRequest(call: HttpCall) { 6 | get { 7 | call.respond(200, "\n" + 8 | "\n" + 9 | " \n" + 10 | " \n" + 11 | " \n" + 12 | "

Herman Melville - Moby-Dick

\n" + 13 | "\n" + 14 | "
\n" + 15 | "

\n" + 16 | " Availing himself of the mild, summer-cool weather that now reigned in these latitudes, and in preparation for the peculiarly active pursuits shortly to be anticipated, Perth, the begrimed, blistered old blacksmith, had not removed his portable forge to the hold again, after concluding his contributory work for Ahab's leg, but still retained it on deck, fast lashed to ringbolts by the foremast; being now almost incessantly invoked by the headsmen, and harpooneers, and bowsmen to do some little job for them; altering, or repairing, or new shaping their various weapons and boat furniture. Often he would be surrounded by an eager circle, all waiting to be served; holding boat-spades, pike-heads, harpoons, and lances, and jealously watching his every sooty movement, as he toiled. Nevertheless, this old man's was a patient hammer wielded by a patient arm. No murmur, no impatience, no petulance did come from him. Silent, slow, and solemn; bowing over still further his chronically broken back, he toiled away, as if toil were life itself, and the heavy beating of his hammer the heavy beating of his heart. And so it was.—Most miserable! A peculiar walk in this old man, a certain slight but painful appearing yawing in his gait, had at an early period of the voyage excited the curiosity of the mariners. And to the importunity of their persisted questionings he had finally given in; and so it came to pass that every one now knew the shameful story of his wretched fate. Belated, and not innocently, one bitter winter's midnight, on the road running between two country towns, the blacksmith half-stupidly felt the deadly numbness stealing over him, and sought refuge in a leaning, dilapidated barn. The issue was, the loss of the extremities of both feet. Out of this revelation, part by part, at last came out the four acts of the gladness, and the one long, and as yet uncatastrophied fifth act of the grief of his life's drama. He was an old man, who, at the age of nearly sixty, had postponedly encountered that thing in sorrow's technicals called ruin. He had been an artisan of famed excellence, and with plenty to do; owned a house and garden; embraced a youthful, daughter-like, loving wife, and three blithe, ruddy children; every Sunday went to a cheerful-looking church, planted in a grove. But one night, under cover of darkness, and further concealed in a most cunning disguisement, a desperate burglar slid into his happy home, and robbed them all of everything. And darker yet to tell, the blacksmith himself did ignorantly conduct this burglar into his family's heart. It was the Bottle Conjuror! Upon the opening of that fatal cork, forth flew the fiend, and shrivelled up his home. Now, for prudent, most wise, and economic reasons, the blacksmith's shop was in the basement of his dwelling, but with a separate entrance to it; so that always had the young and loving healthy wife listened with no unhappy nervousness, but with vigorous pleasure, to the stout ringing of her young-armed old husband's hammer; whose reverberations, muffled by passing through the floors and walls, came up to her, not unsweetly, in her nursery; and so, to stout Labor's iron lullaby, the blacksmith's infants were rocked to slumber. Oh, woe on woe! Oh, Death, why canst thou not sometimes be timely? Hadst thou taken this old blacksmith to thyself ere his full ruin came upon him, then had the young widow had a delicious grief, and her orphans a truly venerable, legendary sire to dream of in their after years; and all of them a care-killing competency.\n" + 17 | "

\n" + 18 | "
\n" + 19 | " \n" + 20 | "") 21 | } 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/config/UrlResolver.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.config 2 | 3 | import android.util.Log 4 | import java.net.MalformedURLException 5 | import java.net.URL 6 | 7 | 8 | /** 9 | * UrlResolver class resolves different parts of url like protocol and hostname 10 | * 11 | */ 12 | class UrlResolver { 13 | 14 | companion object { 15 | internal const val PROTOCOL_HTTPS = "https" 16 | private const val PROTOCOL_HTTP = "http" 17 | private var mProtocol = PROTOCOL_HTTPS 18 | 19 | /** 20 | * It resolves the url protocol scheme from the specified `url`. 21 | * By default, it will use @param PROTOCOL_HTTPS, If no protocol scheme is provided in the url. 22 | */ 23 | @PublishedApi 24 | internal fun resolveHttpProtocol(url: URL): String { 25 | val protocol = url.protocol 26 | if (protocol.equals(PROTOCOL_HTTPS) || protocol.equals(PROTOCOL_HTTP)) { 27 | setProtocol(protocol) 28 | } else { 29 | setProtocol(PROTOCOL_HTTPS) 30 | } 31 | return mProtocol 32 | } 33 | 34 | /** 35 | * This @fun will return the host name of baseUrl 36 | * if no protocol is provided in the baseUrl , PROTOCOL_HTTPS will be used by default" 37 | */ 38 | @PublishedApi 39 | @Throws(MalformedURLException::class) 40 | internal fun resolveUrl(baseUrl: String?): String? { 41 | val mUrl = validateUrl(baseUrl) 42 | if (mUrl.isNullOrBlank()) return mUrl 43 | val url = URL(mUrl) 44 | val domain: String = url.authority + url.path 45 | return if (domain.isNotBlank()) { 46 | Log.e("getHostName ", domain) 47 | if (domain.startsWith("www.")) domain.substring(4) else domain 48 | } else { 49 | baseUrl 50 | } 51 | } 52 | 53 | 54 | /** 55 | * This @fun validateUrl(baseUrl: String?) will validate the url by adding the missing protocol 56 | * scheme and will return a valid url. 57 | * 58 | * If @param baseUrl is already valid or null then it will return it, as it is. 59 | * If @param baseUrl start with "://" then PROTOCOL_HTTPS will be concatenated as 60 | * a prefix to baseurl. 61 | * If @param baseUrl start with "//" then PROTOCOL_HTTPS.plus("//") will be 62 | * concatenated as a prefix to baseurl. 63 | * If @param baseUrl DO NOT start with "http://" or "https://" then PROTOCOL_HTTPS.plus("://") 64 | * will be concatenated as a prefix to baseurl. 65 | */ 66 | 67 | @PublishedApi 68 | internal fun validateUrl(baseUrl: String?): String? { 69 | 70 | if (baseUrl.isNullOrBlank()) return baseUrl 71 | var mUrl = baseUrl.lowercase() 72 | if (mUrl.startsWith("https://") || mUrl.startsWith("http://")) { 73 | return mUrl 74 | } 75 | if (mUrl.startsWith("https:/")){ 76 | return insertCharAtUrl(mUrl,7) 77 | } 78 | if(mUrl.startsWith("http:/")) { 79 | return insertCharAtUrl(mUrl,6) 80 | } 81 | if (mUrl.startsWith("://")) { 82 | mUrl = PROTOCOL_HTTPS.plus(baseUrl.lowercase()) 83 | } else if (mUrl.startsWith("//")) { 84 | mUrl = PROTOCOL_HTTPS.plus(":").plus(baseUrl.lowercase()) 85 | } else if (!mUrl.startsWith("https://") || !mUrl.startsWith("http://")) { 86 | mUrl = PROTOCOL_HTTPS.plus("://").plus(baseUrl.lowercase()) 87 | } 88 | return mUrl 89 | } 90 | 91 | /** 92 | * This @fun will get the value of http client protocol scheme 93 | * By default, it will use @param PROTOCOL_HTTPS, If no protocol is provided in the baseurl. 94 | */ 95 | @PublishedApi 96 | internal fun getProtocol(baseUrl: String?): String { 97 | return resolveHttpProtocol(URL(validateUrl(baseUrl))) 98 | } 99 | 100 | /** 101 | * This private @fun is for internal use only, that will set the value of http client protocol scheme 102 | */ 103 | private fun setProtocol(scheme: String) { 104 | mProtocol = scheme 105 | } 106 | 107 | private fun insertCharAtUrl(url: String, index: Int): String{ 108 | val sb = StringBuilder(url) 109 | sb.insert(index,'/') 110 | return sb.toString() 111 | } 112 | 113 | } 114 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/FileUploadRequestTests.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network 2 | 3 | 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import com.gap.hoodies_network.config.CLIENT_ID 6 | import com.gap.hoodies_network.config.CLIENT_ID_VALUE 7 | import com.gap.hoodies_network.config.CLIENT_OS 8 | import com.gap.hoodies_network.config.CLIENT_OS_VALUE 9 | import com.gap.hoodies_network.core.Failure 10 | import com.gap.hoodies_network.core.HoodiesNetworkClient 11 | import com.gap.hoodies_network.core.Success 12 | import com.gap.hoodies_network.mockwebserver.ServerManager 13 | import com.gap.hoodies_network.testObjects.testInterceptor 14 | import kotlinx.coroutines.runBlocking 15 | import org.json.JSONObject 16 | import org.junit.After 17 | import org.junit.Assert.assertEquals 18 | import org.junit.Assert.assertTrue 19 | import org.junit.Before 20 | import org.junit.Test 21 | import java.io.* 22 | import java.util.* 23 | 24 | class FileUploadRequestTests { 25 | 26 | private val baseURL = "http://localhost:6969/" 27 | 28 | private val interceptor = testInterceptor(InstrumentationRegistry.getInstrumentation().context) 29 | private val defaultHeaders = hashMapOf( 30 | CLIENT_ID to CLIENT_ID_VALUE, 31 | CLIENT_OS to CLIENT_OS_VALUE, 32 | "Test" to "File Upload", 33 | "Library" to "Network" 34 | ) 35 | val context = InstrumentationRegistry.getInstrumentation().context 36 | val fileDir = context.filesDir.absoluteFile.toString() 37 | val file1 = File(fileDir) 38 | val file2 = File(fileDir) 39 | val file3 = File(fileDir) 40 | val fileFirst = File(file1, "a1.txt").apply { 41 | writeText("First file") 42 | } 43 | val fileSecond = File(file2, "a2.txt").apply { 44 | writeText("Second file") 45 | } 46 | val fileThird = File(file3, "a3.txt").apply { 47 | writeText("Third file") 48 | } 49 | 50 | @Before 51 | fun startMockWebServer() { 52 | ServerManager.setup(InstrumentationRegistry.getInstrumentation().context) 53 | } 54 | 55 | @After 56 | fun stopServer() { 57 | ServerManager.stop() 58 | } 59 | 60 | @Test 61 | fun fileTest() { 62 | runBlocking { 63 | val client = HoodiesNetworkClient.Builder() 64 | .baseUrl(baseURL) 65 | .addInterceptor(interceptor) 66 | .build() 67 | 68 | when (val result = client.postMultipartFiles("post", arrayListOf(fileFirst), defaultHeaders )) { 69 | is Success -> { 70 | val files = JSONObject(result.value).getJSONObject("files") 71 | assertEquals(files.getString("file0"), "First file") 72 | } 73 | is Failure -> { 74 | throw result.reason 75 | } 76 | } 77 | } 78 | } 79 | 80 | @Test 81 | fun twoFilesTest() { 82 | runBlocking { 83 | val client = HoodiesNetworkClient.Builder() 84 | .baseUrl(baseURL) 85 | .addInterceptor(interceptor) 86 | .build() 87 | 88 | 89 | when (val result = client.postMultipartFiles("post", arrayListOf(fileFirst, fileSecond), defaultHeaders )) { 90 | is Success -> { 91 | val files = JSONObject(result.value).getJSONObject("files") 92 | assertEquals(files.getString("file0"), "First file") 93 | assertEquals(files.getString("file1"), "Second file") 94 | } 95 | is Failure -> { 96 | throw result.reason 97 | } 98 | } 99 | } 100 | } 101 | 102 | @Test 103 | fun threeFilesTest() { 104 | runBlocking { 105 | val client = HoodiesNetworkClient.Builder() 106 | .baseUrl(baseURL) 107 | .addInterceptor(interceptor) 108 | .build() 109 | 110 | 111 | when (val result = client.postMultipartFiles("post", arrayListOf(fileFirst, fileSecond, fileThird), defaultHeaders )) { 112 | is Success -> { 113 | val files = JSONObject(result.value).getJSONObject("files") 114 | assertEquals(files.getString("file0"), "First file") 115 | assertEquals(files.getString("file1"), "Second file") 116 | assertEquals(files.getString("file2"), "Third file") 117 | } 118 | is Failure -> { 119 | throw result.reason 120 | } 121 | } 122 | } 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /Hoodies-Network/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | id 'maven-publish' 5 | id 'org.jetbrains.dokka' 6 | id 'jacoco' 7 | id 'org.ajoberstar.git-publish' 8 | id 'kotlin-kapt' 9 | } 10 | apply from: '../jacoco.gradle' 11 | 12 | def moduleArtifactId = 'hoodies-networkandroid' 13 | def moduleGroupId = 'com.gap.androidlibraries' 14 | def versionName = '1.0.1' 15 | 16 | android { 17 | compileSdk 32 18 | 19 | defaultConfig { 20 | minSdk 28 21 | targetSdk 32 22 | 23 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 24 | consumerProguardFiles "consumer-rules.pro" 25 | } 26 | 27 | buildTypes { 28 | release { 29 | minifyEnabled false 30 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 31 | } 32 | debug { 33 | testCoverageEnabled true 34 | } 35 | } 36 | 37 | compileOptions { 38 | sourceCompatibility JavaVersion.VERSION_1_8 39 | targetCompatibility JavaVersion.VERSION_1_8 40 | } 41 | 42 | kotlinOptions { 43 | jvmTarget = '1.8' 44 | } 45 | 46 | testOptions { 47 | unitTests.returnDefaultValues = true 48 | unitTests.includeAndroidResources = true 49 | 50 | unitTests.all { 51 | finalizedBy jacocoTestReport 52 | } 53 | jacocoTestReport { 54 | reports { 55 | xml.enabled true 56 | } 57 | } 58 | } 59 | 60 | lint { 61 | sarifReport true 62 | } 63 | 64 | buildFeatures { 65 | compose true 66 | } 67 | composeOptions { 68 | kotlinCompilerExtensionVersion '1.1.1' 69 | } 70 | 71 | tasks.withType(Test) { 72 | useJUnitPlatform() 73 | testLogging { 74 | events("passed", "skipped", "failed") 75 | } 76 | 77 | maxParallelForks = 1 78 | 79 | } 80 | 81 | } 82 | 83 | publishing { 84 | publications { 85 | aar(MavenPublication) { 86 | groupId moduleGroupId 87 | version versionName 88 | artifactId moduleArtifactId 89 | afterEvaluate { 90 | artifact bundleReleaseAar 91 | } 92 | //generate pom nodes for dependencies 93 | pom.withXml { 94 | def dependenciesNode = asNode().appendNode('dependencies') 95 | configurations.implementation.allDependencies.each { dependency -> 96 | if (dependency.name != "unspecified") { 97 | def dependencyNode = dependenciesNode.appendNode('dependency') 98 | dependencyNode.appendNode('groupId', dependency.group) 99 | dependencyNode.appendNode('artifactId', dependency.name) 100 | dependencyNode.appendNode('version', dependency.version) 101 | } 102 | } 103 | } 104 | } 105 | } 106 | repositories { 107 | maven { 108 | name = "GitHubPackages" 109 | url = uri('https://maven.pkg.github.com/gapinc/hoodies-network') 110 | 111 | credentials { 112 | username = System.getenv("GPR_USER") 113 | password = System.getenv("GPR_KEY") 114 | } 115 | } 116 | } 117 | } 118 | 119 | 120 | 121 | dependencies { 122 | implementation "androidx.core:core-ktx:1.6.0" 123 | implementation 'androidx.appcompat:appcompat:1.3.1' 124 | implementation 'com.google.android.material:material:1.4.0' 125 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' 126 | implementation "androidx.compose.runtime:runtime:1.1.1" 127 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 128 | api 'com.google.code.gson:gson:2.8.8' 129 | implementation files('libs/http-2.2.1.jar') 130 | implementation files('libs/sun-common-server.jar') 131 | testImplementation 'junit:junit:4.12' 132 | androidTestImplementation 'androidx.test:core:1.4.0' 133 | androidTestImplementation 'org.mockito:mockito-android:4.8.0' 134 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 135 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 136 | implementation 'androidx.room:room-runtime:2.4.2' 137 | annotationProcessor 'androidx.room:room-compiler:2.4.2' 138 | kapt("androidx.room:room-compiler:2.4.2") 139 | } 140 | 141 | dokkaJavadoc { 142 | outputDirectory.set(file("${rootDir}/dokka")) 143 | } 144 | 145 | allprojects { 146 | configurations.all { 147 | resolutionStrategy.force 'org.objenesis:objenesis:2.6' 148 | } 149 | } 150 | 151 | gitPublishPush {} 152 | 153 | -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/InterceptorTests.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import com.gap.hoodies_network.core.Failure 6 | import com.gap.hoodies_network.core.HoodiesNetworkClient 7 | import com.gap.hoodies_network.core.HoodiesNetworkError 8 | import com.gap.hoodies_network.core.Success 9 | import com.gap.hoodies_network.mockwebserver.ServerManager 10 | import com.gap.hoodies_network.request.CancellableMutableRequest 11 | import com.gap.hoodies_network.request.RetryableCancellableMutableRequest 12 | import kotlinx.coroutines.runBlocking 13 | import org.junit.After 14 | import org.junit.Assert 15 | import org.junit.Before 16 | import org.junit.Test 17 | import org.junit.runner.RunWith 18 | 19 | @RunWith(AndroidJUnit4::class) 20 | class InterceptorTests { 21 | @Before 22 | fun startMockWebServer() { 23 | ServerManager.setup(InstrumentationRegistry.getInstrumentation().context) 24 | } 25 | 26 | @After 27 | fun stopServer() { 28 | ServerManager.stop() 29 | } 30 | 31 | @Test 32 | fun cancelRequestInInterceptNetworkTest() { 33 | runBlocking { 34 | val interceptor = object: com.gap.hoodies_network.interceptor.Interceptor(InstrumentationRegistry.getInstrumentation().context) { 35 | override fun interceptNetwork(isOnline: Boolean, cancellableMutableRequest: CancellableMutableRequest) { 36 | cancellableMutableRequest.cancelRequest(Success("Cancelled!")) 37 | } 38 | 39 | override fun interceptRequest(identifier: String, cancellableMutableRequest: CancellableMutableRequest) { 40 | throw Exception("Request wasn't cancelled!") 41 | } 42 | } 43 | 44 | val client = HoodiesNetworkClient.Builder() 45 | .baseUrl("http://localhost:6969/") 46 | .addInterceptor(interceptor) 47 | .build() 48 | 49 | when (val result = client.getRaw("echo/10")) { 50 | is Success -> { 51 | Assert.assertEquals(result.value, "Cancelled!") 52 | } 53 | is Failure -> { 54 | throw Exception("Request wasn't cancelled!") 55 | } 56 | } 57 | } 58 | } 59 | 60 | @Test 61 | fun cancelRequestInInterceptRequestTest() { 62 | runBlocking { 63 | val interceptor = object: com.gap.hoodies_network.interceptor.Interceptor(InstrumentationRegistry.getInstrumentation().context) { 64 | override fun interceptRequest(identifier: String, cancellableMutableRequest: CancellableMutableRequest) { 65 | cancellableMutableRequest.cancelRequest(Success("Cancelled!")) 66 | } 67 | } 68 | 69 | val client = HoodiesNetworkClient.Builder() 70 | .baseUrl("http://localhost:6969/") 71 | .addInterceptor(interceptor) 72 | .build() 73 | 74 | when (val result = client.getRaw("echo/10")) { 75 | is Success -> { 76 | Assert.assertEquals(result.value, "Cancelled!") 77 | } 78 | is Failure -> { 79 | throw Exception("Request wasn't cancelled!") 80 | } 81 | } 82 | } 83 | } 84 | 85 | @Test 86 | fun retryRequestTest() { 87 | runBlocking { 88 | var counter = 0 89 | val interceptor = object: com.gap.hoodies_network.interceptor.Interceptor(InstrumentationRegistry.getInstrumentation().context) { 90 | override fun interceptError( 91 | error: HoodiesNetworkError, 92 | retryableCancellableMutableRequest: RetryableCancellableMutableRequest, 93 | autoRetryAttempts: Int 94 | ) { 95 | if (error.code == 403) { 96 | val headers = retryableCancellableMutableRequest.request.getHeaders().toMutableMap() 97 | headers["key"] = counter++.toString() 98 | retryableCancellableMutableRequest.request.setRequestHeaders(headers) 99 | 100 | retryableCancellableMutableRequest.retryRequest() 101 | } 102 | } 103 | } 104 | 105 | val client = HoodiesNetworkClient.Builder() 106 | .baseUrl("http://localhost:6969/") 107 | .addInterceptor(interceptor) 108 | .build() 109 | 110 | when (val result = client.getRaw("wants_key")) { 111 | is Success -> { 112 | Assert.assertEquals(result.value, "Success!") 113 | } 114 | is Failure -> { 115 | throw Exception(result.reason) 116 | } 117 | } 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /Hoodies-Network/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | #Specify the classes needed to keep in the app while shrinking 24 | 25 | 26 | -dontwarn java.lang.invoke** 27 | # Retain generic type information for use by reflection by converters and adapters. 28 | -keepattributes Signature 29 | # Retain declared checked exceptions for use by a Proxy instance. 30 | -keepattributes Exceptions 31 | -keepattributes *Annotation* 32 | -keepattributes EnclosingMethod 33 | 34 | -keepattributes SourceFile,LineNumberTable 35 | -keep public class * extends java.lang.Exception 36 | 37 | -dontwarn java.lang.invoke** 38 | 39 | #-keep public class * extends com.gap.network.** { *; } 40 | -keep class !com.gap.hoodies_network.**,!com.gap.hoodies_network.** { ;} 41 | -keep class com.gap.hoodies_network.config.** { ;} 42 | -keep class com.gap.hoodies_network.core.** { ; } 43 | -keep class com.gap.hoodies_network.cache.** { ;} 44 | -keep class com.gap.hoodies_network.connection.** { ;} 45 | -keep class com.gap.hoodies_network.delivery.** { ;} 46 | -keep class com.gap.hoodies_network.header.** { ;} 47 | -keep class com.gap.hoodies_network.interceptor.** { ;} 48 | -keep class com.gap.hoodies_network.request.** { ;} 49 | -keep class com.gap.hoodies_network.connection.queue.** { ;} 50 | -keep class com.gap.hoodies_network.request.json.** { ;} 51 | -keep class com.gap.hoodies_network.request.query.** { ;} 52 | 53 | -keep class com.gap.hoodies_network.core.GapHttpClient$** { *;} 54 | -keep class com.gap.hoodies_network.config.HttpClientConfig$** { *;} 55 | 56 | -keep class * { 57 | public protected *; 58 | } 59 | 60 | -keep class com.gap.hoodies_network.core.HoodiesNetworkClient { *; } 61 | 62 | 63 | -keep class com.gap.hoodies_network.request.json.JsonObjectRequest$** { *;} 64 | 65 | -keep interface * { ;} 66 | -keep class com.gap.hoodies_network.request.json.JsonObjectRequest { 67 | 68 | } 69 | -keep class com.gap.hoodies_network.config.HttpClientConfig$Companion 70 | -keep class com.gap.hoodies_network.config.UrlResolver$Companion 71 | -keep class com.gap.hoodies_network.connection.queue.RequestQueue$Companion 72 | -keep class com.gap.hoodies_network.cache.DiskBasedCache$Companion 73 | -keep class com.gap.hoodies_network.connection.BaseNetwork$Companion 74 | -keep class com.gap.hoodies_network.core.Response$Companion 75 | -keep class com.gap.hoodies_network.header.HttpHeaderParser$Companion 76 | -keep class com.gap.hoodies_network.request.FormUrlEncodedRequest$Companion 77 | -keep class com.gap.hoodies_network.request.ImageRequest$Companion 78 | -keep class com.gap.hoodies_network.request.Request$Method 79 | -keep class com.gap.hoodies_network.request.query.UrlQueryParamEncodedRequest$Companion 80 | 81 | -keep class org.json.JSONArray** { *;} 82 | -keep class org.json.JSONObject** { *;} 83 | -keep class org.json.** { *; } 84 | 85 | -keepclassmembernames class * { 86 | public protected ; 87 | } 88 | 89 | -keepclassmembers class * { 90 | @android.webkit.JavascriptInterface ; 91 | } 92 | 93 | -keep class com.google.gson.stream.** { *; } 94 | -keep class com.google.gson.examples.android.model.** { ; } 95 | 96 | -keep class org.json.JSONException.** {*;} 97 | -keep class org.json.JSONObject.** {*;} 98 | -keep class java.io.UnsupportedEncodingException.** {*;} 99 | 100 | # Application classes that will be serialized/deserialized over Gson 101 | -keep class com.google.gson.examples.android.model.** { ; } 102 | 103 | ## Prevent proguard from stripping interface information from TypeAdapterFactory, 104 | 105 | 106 | ## Coroutines 107 | -keep class kotlinx.coroutines.android.** {*;} 108 | -keep class kotlin.coroutines.Continuation.** { *;} 109 | -keep class kotlin.coroutines.CoroutineContext.**{ *;} 110 | -keep class kotlinx.coroutines.CoroutineStart.** { *;} 111 | -keep class kotlin.coroutines.SuspendFunction.** { *;} 112 | -keep class kotlinx.coroutines.Job.** { *;} 113 | 114 | 115 | -dontwarn kotlinx.coroutines.flow.** 116 | 117 | -keepclassmembers class * { 118 | @android.webkit.JavascriptInterface ; 119 | } 120 | -keep class com.d.c.** { 121 | *; 122 | } 123 | -keep class com.a.f.** { 124 | *; 125 | } 126 | 127 | 128 | -assumenosideeffects class android.util.Log { 129 | public static int d(...); 130 | public static int v(...); 131 | public static int i(...); 132 | public static int w(...); 133 | public static int e(...); 134 | public static int wtf(...); 135 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/cache/EncryptedCache.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.cache 2 | 3 | import androidx.room.Room 4 | import com.gap.hoodies_network.cache.configuration.CacheEnabled 5 | import com.gap.hoodies_network.cache.persistentstorage.CacheDao 6 | import com.gap.hoodies_network.interceptor.EncryptionDecryptionInterceptor 7 | import com.gap.hoodies_network.keystore.CacheKeyManager 8 | import com.gap.hoodies_network.request.Request 9 | import com.gap.hoodies_network.cache.persistentstorage.CacheDatabase 10 | import com.gap.hoodies_network.cache.persistentstorage.CachedData 11 | import com.gap.hoodies_network.core.Response 12 | import kotlinx.coroutines.CoroutineScope 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.launch 15 | import java.security.SecureRandom 16 | import java.time.Duration 17 | import java.time.OffsetDateTime 18 | import java.time.ZoneOffset 19 | import java.util.* 20 | import javax.crypto.Cipher 21 | import javax.crypto.spec.GCMParameterSpec 22 | 23 | class EncryptedCache( 24 | var cacheConfig: CacheEnabled? = null, 25 | var encryptionDecryptionInterceptor: EncryptionDecryptionInterceptor? = null 26 | ) { 27 | private var db: CacheDao? = null 28 | 29 | init { 30 | if (cacheConfig != null) 31 | db = Room.databaseBuilder(cacheConfig!!.applicationContext, CacheDatabase::class.java, "HoodiesNetworkCache").build().cacheDao() 32 | } 33 | 34 | fun cacheRequestResult(data: ByteArray, request: Request<*>) { 35 | //Abort if config is null 36 | cacheConfig ?: return 37 | 38 | CoroutineScope(Dispatchers.IO).launch { 39 | val cachedData = if (cacheConfig!!.encryptionEnabled) { 40 | var iv = genIV() 41 | 42 | //Make sure the IV is unique 43 | while (db!!.getByIv(Base64.getEncoder().encodeToString(iv)).isNotEmpty()) 44 | iv = genIV() 45 | 46 | //Encrypt the data 47 | val encryptedData = runAES(data, iv, Cipher.ENCRYPT_MODE) 48 | 49 | CachedData( 50 | 0, 51 | request.getUrl(), 52 | request.getBody().hashCode(), 53 | getCurrentSeconds(), 54 | Base64.getEncoder().encodeToString(encryptedData), 55 | Base64.getEncoder().encodeToString(iv) 56 | ) 57 | } else { 58 | CachedData( 59 | 0, 60 | request.getUrl(), 61 | request.getBody().hashCode(), 62 | getCurrentSeconds(), 63 | Base64.getEncoder().encodeToString(data), 64 | null 65 | ) 66 | } 67 | 68 | db!!.delete(request.getUrl(), request.getBody().hashCode()) 69 | db!!.insert(cachedData) 70 | } 71 | } 72 | 73 | fun getCachedData(request: Request) { 74 | val cachedData = db!!.get(request.getUrl(), request.getBody().hashCode())!! 75 | 76 | val data = if (cachedData.iv != null) { 77 | runAES(Base64.getDecoder().decode(cachedData.data), Base64.getDecoder().decode(cachedData.iv), Cipher.DECRYPT_MODE) 78 | } else { 79 | Base64.getDecoder().decode(cachedData.data) 80 | } 81 | 82 | val cachedResponse = Response( 83 | 200, 84 | data, 85 | 0, 86 | listOf() 87 | ) 88 | 89 | request.parseNetworkResponse(cachedResponse)?.let { 90 | request.deliverResponse(it) 91 | } 92 | } 93 | 94 | /** 95 | * Method to get current date and time at utc in seconds since UNIX epoch 96 | * @return [String] of current date 97 | */ 98 | private fun getCurrentSeconds(): Long { 99 | return OffsetDateTime.now(ZoneOffset.ofHours(0)).toEpochSecond() 100 | } 101 | 102 | /** 103 | * Method to check if data in datastore is stale 104 | * @param request - The network request 105 | * @return [Boolean] 106 | */ 107 | fun isDataStale(request: Request): Boolean { 108 | val data = db!!.get(request.getUrl(), request.getBody().hashCode()) 109 | 110 | return data == null || Duration.ofSeconds(getCurrentSeconds() - data.cachedAt) > cacheConfig!!.staleDataThreshold 111 | } 112 | 113 | companion object { 114 | /** 115 | * Method to encrypt/decrypt data 116 | * This is used for encryptedCache and persistent cookie storage 117 | * 118 | * @param input - the input data 119 | * @param iv - the IV 120 | * @param cipherMode - ENCRYPT_MODE or DECRYPT_MODE 121 | */ 122 | fun runAES(input: ByteArray, iv: ByteArray, cipherMode: Int): ByteArray { 123 | val cipher = Cipher.getInstance("AES/GCM/NoPadding") 124 | cipher.init(cipherMode, CacheKeyManager.getKey(), GCMParameterSpec(128, iv)) 125 | return cipher.doFinal(input) 126 | } 127 | 128 | /** 129 | * Generates SecureRandom 16-byte IV 130 | */ 131 | fun genIV() : ByteArray { 132 | val r = SecureRandom() 133 | val ivBytes = ByteArray(12) 134 | r.nextBytes(ivBytes) 135 | 136 | return ivBytes 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /examples/benchmarkTesting-benchmarkData.json: -------------------------------------------------------------------------------- 1 | { 2 | "context": { 3 | "build": { 4 | "device": "TC52X", 5 | "fingerprint": "Zebra/TC52AXS/TC52X:11/11-16-05.00-RG-U120-STD-HEL-04/227:user/release-keys", 6 | "model": "TC52AX", 7 | "version": { 8 | "sdk": 30 9 | } 10 | }, 11 | "cpuCoreCount": 8, 12 | "cpuLocked": false, 13 | "cpuMaxFreqHz": -1, 14 | "memTotalBytes": 3844374528, 15 | "sustainedPerformanceModeEnabled": false 16 | }, 17 | "benchmarks": [ 18 | { 19 | "name": "benchmark_gap_network", 20 | "params": {}, 21 | "className": "com.example.benchmark.ServicesBenchmark", 22 | "totalRunTimeNs": 1048072603, 23 | "metrics": { 24 | "timeNs": { 25 | "minimum": 36093, 26 | "maximum": 483526, 27 | "median": 45365, 28 | "runs": [ 29 | 61904, 30 | 65773, 31 | 72619, 32 | 61510, 33 | 63958, 34 | 66681, 35 | 64055, 36 | 61220, 37 | 61108, 38 | 66168, 39 | 64308, 40 | 64538, 41 | 62805, 42 | 64635, 43 | 66011, 44 | 80982, 45 | 70602, 46 | 63750, 47 | 67492, 48 | 483526, 49 | 326123, 50 | 321317, 51 | 330959, 52 | 78355, 53 | 38809, 54 | 37671, 55 | 36733, 56 | 36875, 57 | 45491, 58 | 40558, 59 | 37924, 60 | 37656, 61 | 44151, 62 | 40424, 63 | 40245, 64 | 36093, 65 | 41622, 66 | 42827, 67 | 41302, 68 | 40052, 69 | 44553, 70 | 38645, 71 | 43459, 72 | 43184, 73 | 41421, 74 | 39695, 75 | 38430, 76 | 40215, 77 | 45238, 78 | 40067 79 | ] 80 | } 81 | }, 82 | "warmupIterations": 3368, 83 | "repeatIterations": 7, 84 | "thermalThrottleSleepSeconds": 0 85 | }, 86 | { 87 | "name": "benchmark_retrofit2", 88 | "params": {}, 89 | "className": "com.example.benchmark.ServicesBenchmark", 90 | "totalRunTimeNs": 1381785572, 91 | "metrics": { 92 | "timeNs": { 93 | "minimum": 196875, 94 | "maximum": 675781, 95 | "median": 241094, 96 | "runs": [ 97 | 212213, 98 | 243906, 99 | 241380, 100 | 219401, 101 | 238672, 102 | 219531, 103 | 213854, 104 | 200833, 105 | 203307, 106 | 196875, 107 | 205651, 108 | 201823, 109 | 240807, 110 | 223281, 111 | 250546, 112 | 251302, 113 | 204244, 114 | 217838, 115 | 279635, 116 | 218646, 117 | 200286, 118 | 201067, 119 | 199687, 120 | 202734, 121 | 302864, 122 | 246015, 123 | 240781, 124 | 221171, 125 | 258541, 126 | 225859, 127 | 208776, 128 | 214088, 129 | 217552, 130 | 313672, 131 | 283646, 132 | 358854, 133 | 348515, 134 | 323958, 135 | 330599, 136 | 336745, 137 | 361328, 138 | 675781, 139 | 424791, 140 | 404766, 141 | 418047, 142 | 428203, 143 | 431198, 144 | 441250, 145 | 432083, 146 | 455937 147 | ] 148 | } 149 | }, 150 | "warmupIterations": 3003, 151 | "repeatIterations": 2, 152 | "thermalThrottleSleepSeconds": 0 153 | } 154 | ] 155 | } -------------------------------------------------------------------------------- /examples/HTTPMethodsTYPED: -------------------------------------------------------------------------------- 1 | Examples of available HTTP METHODS (Typed, all features available) 2 | 3 | Making a GET Json Request 4 | 5 | @ExperimentalCoroutinesApi 6 | private fun sendGetJsonRequest() { 7 | lifecycleScope.launch(Dispatchers.Main) { 8 | val result: Result = withContext(Dispatchers.IO) { 9 | mobileHttpClient.get(endpoint / url / here) 10 | } 11 | when (result) { 12 | is Success -> { 13 | val gson = Gson() 14 | binding.resultView.text = gson.toJson(result.value) 15 | } 16 | is Failure -> { 17 | binding.resultView.text = result.reason.message 18 | } 19 | } 20 | } 21 | } 22 | 23 | Making a GET HTML Request 24 | 25 | @ExperimentalCoroutinesApi 26 | private fun sendGetHtmlRequest() { 27 | lifecycleScope.launch(Dispatchers.Main) { 28 | val result = withContext(Dispatchers.IO) { 29 | mobileHttpClient.getHtml(endpoint / url / here) 30 | } 31 | when (result) { 32 | is Success -> { 33 | binding.resultView.text = result.value 34 | } 35 | is Failure -> { 36 | binding.resultView.text = result.reason.message 37 | } 38 | } 39 | } 40 | } 41 | 42 | Making a GET Image Request 43 | 44 | @ExperimentalCoroutinesApi 45 | private fun sendGetImageRequest() { 46 | lifecycleScope.launch(Dispatchers.Main) { 47 | val result = withContext(Dispatchers.IO) { 48 | mobileHttpClient.getImage( 49 | binding.editText.text.toString(), 50 | 0, 51 | 0, 52 | ImageView.ScaleType.CENTER, 53 | Bitmap.Config.ALPHA_8 54 | ) 55 | } 56 | when (result) { 57 | is Success -> { 58 | binding.resultView.setImageBitmap(result.value) 59 | } 60 | is Failure -> { 61 | binding.errorView.text = result.reason.message 62 | } 63 | } 64 | } 65 | } 66 | 67 | Making a POST Json Request 68 | 69 | @ExperimentalCoroutinesApi 70 | private fun sendPostJsonRequest() { 71 | lifecycleScope.launch(Dispatchers.Main) { 72 | val result: Result = withContext(Dispatchers.IO) { 73 | mobileHttpClient.post(endpoint / url / here) 74 | } 75 | when (result) { 76 | is Success -> { 77 | val gson = Gson() 78 | binding.resultView.text = gson.toJson(result.value) 79 | } 80 | is Failure -> { 81 | binding.resultView.text = result.reason.message 82 | } 83 | } 84 | } 85 | } 86 | 87 | Making a POST Json Object Request 88 | 89 | private fun sendJsonObjectRequest() { 90 | lifecycleScope.launch(Dispatchers.Main) { 91 | val url = binding.editText.text.toString() 92 | val jsonObject = JSONObject("{\"name\":\"Test\", \"age\":25}") 93 | val result: Result = withContext(Dispatchers.IO) { 94 | mobileHttpClient.post(url, jsonObject) 95 | } 96 | when (result) { 97 | is Success -> { 98 | binding.resultView.text = gson.toJson(result.value) 99 | } 100 | is Failure -> { 101 | binding.resultView.text = result.reason.message 102 | } 103 | } 104 | } 105 | } 106 | 107 | Making a POST Json Array Request 108 | 109 | private fun sendJsonArrayRequest() { 110 | lifecycleScope.launch(Dispatchers.Main) { 111 | val url = binding.editText.text.toString() 112 | val jsonArray = JSONArray( 113 | "[{\"name\":\"Test 1\", \"age\":25}," + 114 | "{\"name\":\"Test 2\", \"age\":22},{\"name\":\"Test 3\", \"age\":21}]" 115 | ) 116 | 117 | val result: Result = withContext(Dispatchers.IO) { 118 | mobileHttpClient.post(url, jsonArray) 119 | } 120 | when (result) { 121 | is Success -> { 122 | binding.resultView.text = gson.toJson(result.value) 123 | } 124 | is Failure -> { 125 | binding.resultView.text = result.reason.message 126 | } 127 | } 128 | } 129 | } 130 | 131 | Making a PATCH Request 132 | 133 | private fun sendPatchRequest() { 134 | lifecycleScope.launch(Dispatchers.Main) { 135 | val result: Result = withContext(Dispatchers.IO) { 136 | mobileHttpClient.patch(endpoint / url / here, body, headers) 137 | } 138 | when (result) { 139 | is Success -> { 140 | val gson = Gson() 141 | binding.resultView.text = gson.toJson(result.value) 142 | } 143 | is Failure -> { 144 | binding.resultView.text = result.reason.message 145 | } 146 | } 147 | } 148 | } 149 | 150 | Making a multipart file upload request with multiple files 151 | 152 | when ( 153 | val result = 154 | client.postMultipartFiles("post", arrayListOf(fileOne, fileTwo), defaultHeaders)) 155 | 156 | { 157 | is Success -> { 158 | val files = JSONObject(result.value).getJSONObject("files") 159 | assertEquals(files.getString("file0"), "First file") 160 | assertEquals(files.getString("file1"), "Second file") 161 | } 162 | is Failure -> { 163 | throw result.reason 164 | } 165 | } -------------------------------------------------------------------------------- /Hoodies-Network/src/main/kotlin/com/gap/hoodies_network/request/Request.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network.request 2 | 3 | import com.gap.hoodies_network.cache.EncryptedCache 4 | import com.gap.hoodies_network.core.HoodiesNetworkError 5 | import com.gap.hoodies_network.core.Response 6 | import java.io.File 7 | import java.net.CookieManager 8 | import java.nio.charset.StandardCharsets 9 | import java.nio.file.Files 10 | import java.nio.file.Paths 11 | import java.util.* 12 | import java.util.Collections.emptyMap 13 | 14 | /** 15 | * Network request base class 16 | * 17 | * @param the type of parsed response which request expects. 18 | */ 19 | abstract class Request : Comparable?> { 20 | 21 | private val method: String 22 | private val url: String 23 | var requestBody: String 24 | var cache: EncryptedCache = EncryptedCache() 25 | var cookieManager: CookieManager? = null 26 | var files: List? = null 27 | var multipartBoundary: String = "" 28 | private var headers: Map = emptyMap() 29 | var errorListener: Response.ErrorListener? = null 30 | internal var requestIsCancelled = false 31 | internal var retryingRequest = false 32 | internal var cancellationResult: com.gap.hoodies_network.core.Result<*, HoodiesNetworkError>? = null 33 | 34 | @Throws(HoodiesNetworkError::class) 35 | abstract fun parseNetworkResponse(response: Response?): Response? 36 | abstract fun deliverResponse(response: Response?) 37 | 38 | fun deliverError(hoodiesNetworkError: HoodiesNetworkError) = 39 | errorListener?.onErrorResponse(hoodiesNetworkError) 40 | 41 | /** Supported request methods. */ 42 | interface Method { 43 | companion object { 44 | const val GET = "GET" 45 | const val POST = "POST" 46 | const val PUT = "PUT" 47 | const val DELETE = "DELETE" 48 | const val HEAD = "HEAD" 49 | const val OPTIONS = "OPTIONS" 50 | const val TRACE = "TRACE" 51 | const val PATCH = "PATCH" 52 | } 53 | } 54 | 55 | constructor( 56 | url: String, 57 | method: String, 58 | encryptedCache: EncryptedCache, 59 | cookieManager: CookieManager? 60 | ) { 61 | this.url = url 62 | this.method = method 63 | this.requestBody = "" // @MSP replaced null with empty string 64 | this.cache = encryptedCache 65 | this.cookieManager = cookieManager 66 | } 67 | 68 | constructor( 69 | url: String, 70 | method: String, 71 | requestBody: String, 72 | errorListener: Response.ErrorListener?, 73 | encryptedCache: EncryptedCache, 74 | cookieManager: CookieManager? 75 | ) { 76 | this.url = url 77 | this.method = method 78 | this.errorListener = errorListener 79 | this.requestBody = requestBody 80 | this.cache = encryptedCache 81 | this.cookieManager = cookieManager 82 | } 83 | 84 | constructor( 85 | url: String, 86 | method: String, 87 | files: List, 88 | multipartBoundary: String, 89 | errorListener: Response.ErrorListener?, 90 | cookieManager: CookieManager? 91 | ) { 92 | this.url = url 93 | this.method = method 94 | this.requestBody = "" 95 | this.multipartBoundary = multipartBoundary 96 | this.files = files 97 | this.errorListener = errorListener 98 | this.cookieManager = cookieManager 99 | } 100 | 101 | /** Return the method for this request. Can be one of the values in [Method]. */ 102 | open fun getMethod(): String { 103 | return this.method 104 | } 105 | 106 | /** return the url of request */ 107 | open fun getUrl(): String { 108 | return this.url 109 | } 110 | 111 | fun getBody(): String { 112 | return this.requestBody 113 | } 114 | 115 | fun getFile(): ArrayList { 116 | return postMultipartFormData(files) 117 | } 118 | 119 | /** return the headers of request */ 120 | open fun getHeaders(): Map { 121 | return this.headers 122 | } 123 | 124 | fun setRequestHeaders(headers: Map) { 125 | this.headers = headers 126 | } 127 | 128 | override fun compareTo(other: Request?): Int { 129 | return 0 130 | } 131 | 132 | fun postMultipartFormData(data: List?): ArrayList { 133 | val byteArrays = ArrayList() 134 | val separator = 135 | "--$multipartBoundary\r\nContent-Disposition: multipart/form-data; name=".toByteArray( 136 | StandardCharsets.UTF_8 137 | ) 138 | 139 | //If data null, return empty ByteArrays 140 | data ?: return arrayListOf(ByteArray(0)) 141 | 142 | for (file in data) { 143 | byteArrays.add(separator) 144 | 145 | val path = Paths.get(file.toURI()) 146 | val mimeType = Files.probeContentType(path) 147 | byteArrays.add( 148 | "file${data.indexOf(file)}; filename=\"${path.fileName}\"\r\nContent-Type: $mimeType\r\n\r\n".toByteArray( 149 | StandardCharsets.UTF_8 150 | ) 151 | ) 152 | byteArrays.add(Files.readAllBytes(path)) 153 | byteArrays.add("\r\n".toByteArray(StandardCharsets.UTF_8)) 154 | } 155 | byteArrays.add("--$multipartBoundary--".toByteArray(StandardCharsets.UTF_8)) 156 | 157 | return byteArrays 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/MultiRequestTest.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNCHECKED_CAST") 2 | 3 | package com.gap.hoodies_network 4 | 5 | import android.util.Log 6 | import androidx.test.ext.junit.runners.AndroidJUnit4 7 | import androidx.test.platform.app.InstrumentationRegistry 8 | import com.gap.hoodies_network.cache.EncryptedCache 9 | import com.gap.hoodies_network.request.json.JsonArrayRequest 10 | import com.gap.hoodies_network.request.json.JsonObjectRequest 11 | import com.gap.hoodies_network.request.Request 12 | import com.gap.hoodies_network.connection.queue.RequestQueue 13 | import com.gap.hoodies_network.mockwebserver.ServerManager 14 | import com.gap.hoodies_network.request.StringRequest 15 | import org.json.JSONArray 16 | import org.json.JSONException 17 | import org.json.JSONObject 18 | import org.junit.After 19 | import org.junit.Assert.* 20 | import org.junit.Before 21 | import org.junit.Test 22 | import org.junit.runner.RunWith 23 | import java.net.URL 24 | import java.util.concurrent.TimeUnit 25 | 26 | @RunWith(AndroidJUnit4::class) 27 | class MultiRequestTest { 28 | val mContext = InstrumentationRegistry.getInstrumentation().context 29 | 30 | @Before 31 | fun startMockWebServer() { 32 | ServerManager.setup(mContext) 33 | } 34 | 35 | @After 36 | fun stopServer() { 37 | ServerManager.stop() 38 | } 39 | 40 | @Test 41 | fun multipleRequestTest() { 42 | val cache = EncryptedCache(null) 43 | val testUrl = "http://localhost:6969/echo/1" 44 | val queue = RequestQueue.getInstance(URL(testUrl).host, null) 45 | assertNull(queue?.dequeue()) 46 | val requestOne = StringRequest( 47 | "http://localhost:6969/echo/1", 48 | Request.Method.GET, 49 | { response -> 50 | println( 51 | "requestOne result - " + response?.result 52 | ) 53 | assertNotNull(response) 54 | }, 55 | { error -> println("error requestOne - " + error.message) }, cache, null 56 | ) 57 | val requestTwo = StringRequest( 58 | "http://localhost:6969/echo/2", 59 | Request.Method.GET, 60 | { response -> 61 | println( 62 | "requestTwo result - " + response?.result 63 | ) 64 | println("timeMS - " + response?.networkTimeMs) 65 | assertNotNull(response) 66 | }, 67 | { error -> println("error requestTwo - " + error.message) }, cache, null 68 | ) 69 | val requestThree = StringRequest( 70 | "http://localhost:6969/todos/30", 71 | Request.Method.GET, 72 | encryptedCache = cache, 73 | cookieManager = null 74 | ) 75 | val body = JSONObject() 76 | try { 77 | body.put("key", "value") 78 | } catch (e: JSONException) { 79 | Log.e("error", e.toString()) 80 | } 81 | val bodyArr = JSONArray() 82 | try { 83 | bodyArr.put(0, body) 84 | } catch (e: JSONException) { 85 | Log.e("error", e.toString()) 86 | } 87 | val requestFour = StringRequest( 88 | "http://localhost:6969/echo/1", 89 | Request.Method.GET, body, encryptedCache = cache, 90 | cookieManager = null 91 | ) 92 | val requestFive = JsonObjectRequest( 93 | "http://localhost:6969/echo/1", 94 | Request.Method.GET, body, encryptedCache = cache, 95 | cookieManager = null 96 | ) 97 | 98 | val requestSix = JsonArrayRequest( 99 | "http://localhost:6969/todos/", 100 | Request.Method.GET, bodyArr, encryptedCache = cache, 101 | cookieManager = null 102 | ) 103 | 104 | val requestSeven = StringRequest( 105 | "http://localhost:6969/echo/3", 106 | Request.Method.GET, 107 | { response -> 108 | println( 109 | "requestSeven result - " + response?.result 110 | ) 111 | assertNotNull(response) 112 | }, 113 | { error -> println("error requestSeven - " + error.message) }, cache, null 114 | ) 115 | val requestEight = StringRequest( 116 | "http://localhost:6969/echo/4", 117 | Request.Method.GET, 118 | { response -> 119 | println( 120 | "requestEight result - " + response?.result 121 | ) 122 | println("timeMS - " + response?.networkTimeMs) 123 | assertNotNull(response) 124 | }, 125 | { error -> println("error requestTwo - " + error.message) }, cache, null 126 | ) 127 | assertNotSame(requestOne.hashCode(), requestTwo.hashCode()) 128 | assertNotEquals(requestOne, requestFour) 129 | queue?.enqueue(requestOne as Request) 130 | queue?.enqueue(requestTwo as Request) 131 | queue?.enqueue(requestThree as Request) 132 | queue?.enqueue(requestFour as Request) 133 | queue?.enqueue(requestFive as Request) 134 | queue?.enqueue(requestSix as Request) 135 | queue?.enqueue(requestSeven as Request) 136 | queue?.enqueue(requestEight as Request) 137 | if (queue != null) { 138 | assertTrue(queue.hasItems()) 139 | } 140 | 141 | try { 142 | TimeUnit.SECONDS.sleep(15) 143 | } catch (e: InterruptedException) { 144 | e.printStackTrace() 145 | } 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | Please set the JAVA_HOME variable in your environment to match the 103 | location of your Java installation." 104 | fi 105 | 106 | # Increase the maximum file descriptors if we can. 107 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 108 | MAX_FD_LIMIT=`ulimit -H -n` 109 | if [ $? -eq 0 ] ; then 110 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 111 | MAX_FD="$MAX_FD_LIMIT" 112 | fi 113 | ulimit -n $MAX_FD 114 | if [ $? -ne 0 ] ; then 115 | warn "Could not set maximum file descriptor limit: $MAX_FD" 116 | fi 117 | else 118 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 119 | fi 120 | fi 121 | 122 | # For Darwin, add options to specify how the application appears in the dock 123 | if $darwin; then 124 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 125 | fi 126 | 127 | # For Cygwin or MSYS, switch paths to Windows format before running java 128 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 129 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 130 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 131 | 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /Hoodies-Network/src/androidTest/kotlin/com/gap/hoodies_network/SocketTimeOutTest.kt: -------------------------------------------------------------------------------- 1 | package com.gap.hoodies_network 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import com.gap.hoodies_network.config.HttpClientConfig 6 | import com.gap.hoodies_network.connection.queue.RequestQueue 7 | import com.gap.hoodies_network.mockwebserver.ServerManager 8 | import org.junit.After 9 | import org.junit.Assert.* 10 | import org.junit.Before 11 | import org.junit.Test 12 | import org.junit.runner.RunWith 13 | import java.net.HttpURLConnection 14 | import java.net.SocketTimeoutException 15 | import java.net.URL 16 | import org.mockito.Mockito 17 | import java.io.IOException 18 | import java.lang.Exception 19 | import java.net.URLConnection 20 | import java.time.Duration 21 | 22 | 23 | @RunWith(AndroidJUnit4::class) 24 | class SocketTimeOutTest { 25 | val mContext = InstrumentationRegistry.getInstrumentation().context 26 | 27 | private var queue: RequestQueue? = null 28 | private val testUrl = "http://localhost:6969/echo/2" 29 | 30 | companion object{ 31 | private var CONNECT_TIMEOUT=1 32 | private var READ_TIMEOUT=1 33 | } 34 | 35 | @Before 36 | fun setUp() { 37 | queue = RequestQueue.getInstance(URL(testUrl).host, null) 38 | ServerManager.setup(mContext) 39 | } 40 | 41 | @After 42 | fun stopServer() { 43 | ServerManager.stop() 44 | } 45 | 46 | open class UrlWrapper(spec: String?) { 47 | var url: URL = URL(spec) 48 | 49 | @Throws(IOException::class) 50 | open fun openConnection(): URLConnection { 51 | return url.openConnection() 52 | } 53 | } 54 | 55 | @Test 56 | @Throws(Exception::class) 57 | fun socketConnectTimeOutTest() { 58 | val url = Mockito.mock(UrlWrapper::class.java) 59 | val mockConnection = Mockito.mock(HttpURLConnection::class.java) 60 | 61 | Mockito.`when`(url.openConnection()).thenReturn(mockConnection) 62 | assertTrue(url.openConnection() is HttpURLConnection) 63 | 64 | /** This is expected exception to throw to simulate a Socket Connect Timeout */ 65 | val expectedException = SocketTimeoutException() 66 | /** mockConnection should throw the timeout exception instead of returning a response code */ 67 | Mockito.`when`(mockConnection.responseCode).thenThrow(expectedException) 68 | /** Now its ready to call the client code */ 69 | val baseNet = BaseNet() 70 | /** It should catch the Socket Connect Timeout and return false */ 71 | assertFalse(baseNet.testConnectTimeOut()) 72 | } 73 | 74 | 75 | @Test 76 | fun socketReadTimeOutTest() { 77 | val url = Mockito.mock(UrlWrapper::class.java) 78 | val mockConnection = Mockito.mock(HttpURLConnection::class.java) 79 | Mockito.`when`(url.openConnection()).thenReturn(mockConnection) 80 | assertTrue(url.openConnection() is HttpURLConnection) 81 | 82 | /** This is expected exception to throw to simulate a Socket Connect Timeout */ 83 | val expectedException = SocketTimeoutException() 84 | /** mockConnection should throw the timeout exception instead of returning a response code */ 85 | Mockito.`when`(mockConnection.responseCode).thenThrow(expectedException) 86 | /** Now its ready to call the client code */ 87 | val baseNet = BaseNet() 88 | /** It should catch the Socket Read Timeout and return false */ 89 | assertFalse(baseNet.testReadTimeOut()) 90 | 91 | } 92 | 93 | open class BaseNet { 94 | private val url = "http://localhost:6969/echo/2" 95 | open fun testConnectTimeOut(): Boolean { 96 | return try { 97 | CONNECT_TIMEOUT = 1 98 | READ_TIMEOUT = 5000 99 | HttpURLConnection.setFollowRedirects(false) 100 | val con = URL(url).openConnection() as HttpURLConnection 101 | con.requestMethod = "HEAD" 102 | con.connectTimeout = CONNECT_TIMEOUT /**set connect timeout in MilliSeconds */ 103 | con.readTimeout = READ_TIMEOUT /** set read timeout in MilliSeconds */ 104 | con.responseCode == HttpURLConnection.HTTP_OK 105 | } catch (e: SocketTimeoutException) { 106 | println( 107 | "SocketTimeoutException - " + e.localizedMessage 108 | ) 109 | false 110 | } catch (e: IOException) { 111 | println( 112 | "IOException - " + e.localizedMessage 113 | ) 114 | false 115 | } 116 | finally { 117 | // con.disconnect() 118 | 119 | } 120 | } 121 | 122 | open fun testReadTimeOut(): Boolean { 123 | return try { 124 | CONNECT_TIMEOUT = 5000 125 | READ_TIMEOUT = 1 126 | HttpURLConnection.setFollowRedirects(false) 127 | val con = URL(url).openConnection() as HttpURLConnection 128 | con.requestMethod = "HEAD" 129 | con.connectTimeout = CONNECT_TIMEOUT /**set connect timeout in MilliSeconds */ 130 | con.readTimeout = READ_TIMEOUT /** set read timeout in MilliSeconds */ 131 | con.responseCode == HttpURLConnection.HTTP_OK 132 | } catch (e: SocketTimeoutException) { 133 | println( 134 | "SocketTimeoutException - " + e.localizedMessage 135 | ) 136 | false 137 | } catch (e: IOException) { 138 | println( 139 | "IOException - " + e.localizedMessage 140 | ) 141 | false 142 | } 143 | finally { 144 | // con.disconnect() 145 | } 146 | } 147 | } 148 | 149 | @Test 150 | fun configSetTimeoutsDurationMilliSeconds() { 151 | HttpClientConfig.setConnectTimeOut(Duration.ofMillis(5)) 152 | HttpClientConfig.setReadTimeOut(Duration.ofMillis(4)) 153 | 154 | assertEquals(HttpClientConfig.getConnectTimeOutDuration(), Duration.ofMillis(5)) 155 | assertEquals(HttpClientConfig.getReadTimeoutDuration(), Duration.ofMillis(4)) 156 | 157 | HttpClientConfig.setFactoryDefaultConfiguration() 158 | 159 | assertEquals(HttpClientConfig.getConnectTimeOutDuration(), Duration.ofMillis(60000)) 160 | assertEquals(HttpClientConfig.getReadTimeoutDuration(), Duration.ofMillis(60000)) 161 | } 162 | 163 | @Test 164 | fun configSetTimeoutsDurationSeconds() { 165 | HttpClientConfig.setConnectTimeOut(Duration.ofSeconds(5)) 166 | HttpClientConfig.setReadTimeOut(Duration.ofSeconds(4)) 167 | 168 | assertEquals(HttpClientConfig.getConnectTimeOutDuration(), Duration.ofSeconds(5)) 169 | assertEquals(HttpClientConfig.getReadTimeoutDuration(), Duration.ofSeconds(4)) 170 | 171 | HttpClientConfig.setFactoryDefaultConfiguration() 172 | 173 | assertEquals(HttpClientConfig.getConnectTimeOutDuration(), Duration.ofSeconds(60)) 174 | assertEquals(HttpClientConfig.getReadTimeoutDuration(), Duration.ofSeconds(60)) 175 | } 176 | 177 | } 178 | --------------------------------------------------------------------------------