diff --git a/network/README.md b/network/README.md
new file mode 100644
index 00000000..323ac55b
--- /dev/null
+++ b/network/README.md
@@ -0,0 +1,2 @@
+network
+====
diff --git a/network/build.gradle b/network/build.gradle
new file mode 100644
index 00000000..d451b123
--- /dev/null
+++ b/network/build.gradle
@@ -0,0 +1,48 @@
+apply from: "../android-configs/lib-config.gradle"
+
+dependencies {
+ def okhttpVersion = "3.14.1"
+ def retrofitVersion = "2.8.1"
+ def junitVersion = '4.13.2'
+ def mockitoVersion = "4.4.0"
+ def coroutineVersion = "1.4.0"
+
+ implementation("com.squareup.okhttp3:okhttp")
+ implementation("com.squareup.retrofit2:retrofit")
+
+ testImplementation("junit:junit")
+ testImplementation("org.mockito:mockito-core")
+ testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
+
+ constraints {
+ implementation("com.squareup.okhttp3:okhttp") {
+ version {
+ require(okhttpVersion)
+ }
+ }
+
+ implementation("com.squareup.retrofit2:retrofit") {
+ version {
+ require(retrofitVersion)
+ }
+ }
+
+ implementation("junit:junit") {
+ version {
+ require(junitVersion)
+ }
+ }
+
+ testImplementation("org.mockito:mockito-core") {
+ version {
+ require(mockitoVersion)
+ }
+ }
+
+ testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") {
+ version {
+ require(coroutineVersion)
+ }
+ }
+ }
+}
diff --git a/network/src/main/AndroidManifest.xml b/network/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..c53946bb
--- /dev/null
+++ b/network/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt b/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt
new file mode 100644
index 00000000..fb9b1ef2
--- /dev/null
+++ b/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt
@@ -0,0 +1,55 @@
+package ru.touchin.network.blocking
+
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+import ru.touchin.network.utils.getAnnotation
+
+/**
+ * Custom [Call] implementation for handling blocking and pending requests.
+ * @param callDelegate is delegate of default Call implementation
+ */
+class BlockingCall(
+ private val callDelegate: Call
+) : Call by callDelegate {
+
+ override fun enqueue(callback: Callback) {
+ val isBlocking = callDelegate.blocking != null
+
+ if (PendingRequestsManager.isPending) {
+ PendingRequestsManager.addPendingRequest(
+ call = this,
+ callback = callback,
+ isBlocking = isBlocking
+ )
+ return
+ }
+
+ if (isBlocking) PendingRequestsManager.isPending = true
+
+ callDelegate.enqueue(object: Callback {
+ override fun onResponse(call: Call, response: Response) {
+ callback.onResponse(call, response)
+
+ if (call.blocking != null) {
+ PendingRequestsManager.isPending = false
+ PendingRequestsManager.executePendingRequests()
+ }
+ }
+
+ override fun onFailure(call: Call, t: Throwable) {
+ PendingRequestsManager.isPending = false
+ callback.onFailure(call, t)
+
+ val (isBlockingInternal, cancelOnFail) = callDelegate.blocking
+ .let { (it != null) to (it?.cancelRequestsOnFail == true) }
+ when {
+ isBlockingInternal && cancelOnFail -> PendingRequestsManager.dropPendingRequests()
+ isBlockingInternal -> PendingRequestsManager.executePendingRequests()
+ }
+ }
+ })
+ }
+
+ private val Call.blocking get() = request().getAnnotation(BlockingRequest::class.java)
+}
diff --git a/network/src/main/java/ru/touchin/network/blocking/BlockingRequest.kt b/network/src/main/java/ru/touchin/network/blocking/BlockingRequest.kt
new file mode 100644
index 00000000..9f2cf0a5
--- /dev/null
+++ b/network/src/main/java/ru/touchin/network/blocking/BlockingRequest.kt
@@ -0,0 +1,11 @@
+package ru.touchin.network.blocking
+
+/**
+ * Annotation that is used for methods of retrofit services to tag request as blocking one.
+ * It means that every upcoming request will be pended until this method finished.
+ * @param cancelRequestsOnFail if true then all pending requests will be canceled
+ * if blocking request fails
+ */
+@Retention(AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.FUNCTION)
+annotation class BlockingRequest(val cancelRequestsOnFail: Boolean = false)
diff --git a/network/src/main/java/ru/touchin/network/blocking/BlockingRequestCallAdapter.kt b/network/src/main/java/ru/touchin/network/blocking/BlockingRequestCallAdapter.kt
new file mode 100644
index 00000000..93219fa9
--- /dev/null
+++ b/network/src/main/java/ru/touchin/network/blocking/BlockingRequestCallAdapter.kt
@@ -0,0 +1,33 @@
+package ru.touchin.network.blocking
+
+import retrofit2.Call
+import retrofit2.CallAdapter
+import retrofit2.Retrofit
+import java.lang.IllegalStateException
+import java.lang.reflect.ParameterizedType
+import java.lang.reflect.Type
+
+/**
+ * CallAdapter for Retrofit instance used for handling [BlockingRequest] methods
+ * Returns [BlockingCall] as a custom adaptation of [Call]
+ */
+class BlockingRequestCallAdapter private constructor(
+ private val responseType: Type
+) : CallAdapter {
+
+ companion object {
+ fun create() = object : CallAdapter.Factory() {
+ override fun get(returnType: Type, annotations: Array, retrofit: Retrofit): CallAdapter<*, *>? {
+ return when {
+ getRawType(returnType) != Call::class.java -> null
+ returnType !is ParameterizedType -> throw IllegalStateException("return type must be parametrized")
+ else -> BlockingRequestCallAdapter(responseType = returnType.actualTypeArguments[0])
+ }
+ }
+ }
+ }
+
+ override fun responseType(): Type = responseType
+
+ override fun adapt(call: Call): Any = BlockingCall(call)
+}
diff --git a/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt b/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt
new file mode 100644
index 00000000..2d2a9484
--- /dev/null
+++ b/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt
@@ -0,0 +1,87 @@
+package ru.touchin.network.blocking
+
+import retrofit2.Call
+import retrofit2.Callback
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/**
+ * Manager that holds the list of requests and provides methods to interact with them.
+ * Provided as a singleton and can be used to prevent sending requests right away
+ * e.g. via [BlockingRequest].
+ */
+object PendingRequestsManager {
+
+ //Using ReentrantLock to avoid concurrency pitfalls when interacting with pendingRequests
+ private val pendingRequestsLock = ReentrantLock()
+
+ private val pendingRequests = mutableListOf()
+
+ private val internalAtomicPending = AtomicBoolean(false)
+
+ /**
+ * Flag that show if requests should be stock in [pendingRequests] or be enqueued right away
+ * Wrapper of atomic [internalAtomicPending]
+ */
+ var isPending: Boolean
+ get() = internalAtomicPending.get()
+ set(value) { internalAtomicPending.set(value) }
+
+ /**
+ * Shows how many requests are stock
+ */
+ fun getPendingRequestsCount() = pendingRequests.count()
+
+ /**
+ * Used for adding requests to stock
+ * @param call is retrofit method
+ * @param callback used to provide actions when requests finished with success or error
+ */
+ fun addPendingRequest(call: Call, callback: Callback, isBlocking: Boolean) {
+ pendingRequestsLock.withLock {
+ pendingRequests.add(PendingRequestData(call, callback, isBlocking))
+ }
+ }
+
+ /**
+ * Used to execute and clear all stocked requests
+ */
+ fun executePendingRequests() {
+ applyActionToPendingRequests { call.enqueue(callback) }
+ }
+
+ /**
+ * Used to cancel and clear all stocked requests
+ */
+ fun dropPendingRequests() {
+ applyActionToPendingRequests { call.cancel() }
+ }
+
+ private fun applyActionToPendingRequests(action: PendingRequestData.() -> Unit) {
+ pendingRequestsLock.withLock {
+ with (pendingRequests.iterator()) {
+ while (hasNext()) {
+ val requestData = next()
+ remove()
+
+ requestData.action()
+ if (requestData.isBlocking) break
+ }
+ }
+ }
+ }
+
+ /**
+ * Contains data of stocked requests
+ * @param call is retrofit request we want to stock
+ * @param callback used to call methods onResponse and onFailure of method
+ * @param isBlocking shows if request we add to stock is blocking and all following requests must be pended
+ */
+ internal class PendingRequestData(
+ val call: Call,
+ val callback: Callback,
+ val isBlocking: Boolean = false
+ )
+
+}
diff --git a/network/src/main/java/ru/touchin/network/utils/Request.kt b/network/src/main/java/ru/touchin/network/utils/Request.kt
new file mode 100644
index 00000000..192a22ca
--- /dev/null
+++ b/network/src/main/java/ru/touchin/network/utils/Request.kt
@@ -0,0 +1,7 @@
+package ru.touchin.network.utils
+
+import okhttp3.Request
+import retrofit2.Invocation
+
+fun Request.getAnnotation(annotation: Class) =
+ tag(Invocation::class.java)?.method()?.getAnnotation(annotation)
diff --git a/network/src/test/java/PendingRequestsManagerTest.kt b/network/src/test/java/PendingRequestsManagerTest.kt
new file mode 100644
index 00000000..92664d82
--- /dev/null
+++ b/network/src/test/java/PendingRequestsManagerTest.kt
@@ -0,0 +1,67 @@
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ObsoleteCoroutinesApi
+import kotlinx.coroutines.joinAll
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.newFixedThreadPoolContext
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.mock
+import retrofit2.Call
+import retrofit2.Callback
+import ru.touchin.network.blocking.PendingRequestsManager
+
+@Suppress("UNCHECKED_CAST")
+@ObsoleteCoroutinesApi
+class PendingRequestsManagerTest {
+
+ @Before
+ fun `Clear pending requests list`() {
+ PendingRequestsManager.dropPendingRequests()
+ }
+
+ @Test
+ fun `Assert pending requests add synchronization`() = runBlocking {
+ runOnFixedThreadScope {
+ 1.rangeTo(1000).map { launch { addRequestsAsync() } }.joinAll()
+ }
+
+ assertEquals(10000, PendingRequestsManager.getPendingRequestsCount())
+ }
+
+ @Test
+ fun `Assert pending requests synchronization`() = runBlocking {
+ runOnFixedThreadScope {
+ repeat(1000) { addRequestsAsync() }
+
+ val executeJob = launch {
+ PendingRequestsManager.executePendingRequests()
+ }
+
+ val addJobs2 = 1.rangeTo(1000).map { launch {
+ addRequestsAsync()
+ } }
+
+ executeJob.join()
+ addJobs2.joinAll()
+ }
+
+ assertEquals(10000, PendingRequestsManager.getPendingRequestsCount())
+ }
+
+ private fun addRequestsAsync() {
+ repeat(10) {
+ PendingRequestsManager.addPendingRequest(
+ call = mock(Call::class.java) as Call,
+ callback = mock(Callback::class.java) as Callback
+ )
+ }
+ }
+
+ private suspend fun runOnFixedThreadScope(block: suspend CoroutineScope.() -> Unit) {
+ CoroutineScope(newFixedThreadPoolContext(5, "synchronizationPool"))
+ .launch { block() }
+ .join()
+ }
+}
diff --git a/utils/src/test/java/DateFormatUtilsTest.kt b/utils/src/test/java/DateFormatUtilsTest.kt
index 8036ede3..80c5f40a 100644
--- a/utils/src/test/java/DateFormatUtilsTest.kt
+++ b/utils/src/test/java/DateFormatUtilsTest.kt
@@ -1,5 +1,5 @@
import org.joda.time.DateTime
-import org.junit.Assert
+import org.junit.Assert.assertEquals
import org.junit.Test
import ru.touchin.roboswag.core.utils.DateFormatUtils
@@ -11,7 +11,7 @@ class DateFormatUtilsTest {
value = "2015-04-29",
format = DateFormatUtils.Format.DATE_FORMAT
)
- Assert.assertEquals(DateTime(2015, 4, 29, 0, 0, 0), dateTime)
+ assertEquals(DateTime(2015, 4, 29, 0, 0, 0), dateTime)
}
@Test
@@ -23,6 +23,6 @@ class DateFormatUtilsTest {
format = DateFormatUtils.Format.DATE_TIME_FORMAT,
defaultValue = currentDateTime
)
- Assert.assertEquals(currentDateTime, dateTime)
+ assertEquals(currentDateTime, dateTime)
}
}