Fix code analysis errors
This commit is contained in:
parent
e4f42baaed
commit
8e9b14cecc
34 changed files with 71 additions and 79 deletions
|
|
@ -4,10 +4,4 @@ import android.app.Application
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class TestShowcaseApplication : Application() {
|
class TestShowcaseApplication : Application()
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,5 @@ object AppModule {
|
||||||
@Provides
|
@Provides
|
||||||
internal fun bindSessionExpirationListener(
|
internal fun bindSessionExpirationListener(
|
||||||
sessionExpirationListenerImpl: SessionExpirationListenerImpl
|
sessionExpirationListenerImpl: SessionExpirationListenerImpl
|
||||||
) : SessionExpirationListener = sessionExpirationListenerImpl
|
): SessionExpirationListener = sessionExpirationListenerImpl
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -7,13 +7,20 @@ import org.fnives.test.showcase.model.session.Session
|
||||||
import kotlin.properties.ReadWriteProperty
|
import kotlin.properties.ReadWriteProperty
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
class SharedPreferencesManagerImpl constructor(private val sharedPreferences: SharedPreferences) : UserDataLocalStorage {
|
class SharedPreferencesManagerImpl(
|
||||||
|
private val sharedPreferences: SharedPreferences
|
||||||
|
) : UserDataLocalStorage {
|
||||||
|
|
||||||
override var session: Session? by SessionDelegate(SESSION_KEY)
|
override var session: Session? by SessionDelegate(SESSION_KEY)
|
||||||
|
|
||||||
private class SessionDelegate(private val key: String) : ReadWriteProperty<SharedPreferencesManagerImpl, Session?> {
|
private class SessionDelegate(private val key: String) :
|
||||||
|
ReadWriteProperty<SharedPreferencesManagerImpl, Session?> {
|
||||||
|
|
||||||
override fun setValue(thisRef: SharedPreferencesManagerImpl, property: KProperty<*>, value: Session?) {
|
override fun setValue(
|
||||||
|
thisRef: SharedPreferencesManagerImpl,
|
||||||
|
property: KProperty<*>,
|
||||||
|
value: Session?
|
||||||
|
) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
thisRef.sharedPreferences.edit().remove(key).apply()
|
thisRef.sharedPreferences.edit().remove(key).apply()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -25,7 +32,10 @@ class SharedPreferencesManagerImpl constructor(private val sharedPreferences: Sh
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getValue(thisRef: SharedPreferencesManagerImpl, property: KProperty<*>): Session? {
|
override fun getValue(
|
||||||
|
thisRef: SharedPreferencesManagerImpl,
|
||||||
|
property: KProperty<*>
|
||||||
|
): Session? {
|
||||||
val values = thisRef.sharedPreferences.getStringSet(key, null)?.toList()
|
val values = thisRef.sharedPreferences.getStringSet(key, null)?.toList()
|
||||||
val accessToken = values?.firstOrNull { it.startsWith(ACCESS_TOKEN_KEY) }
|
val accessToken = values?.firstOrNull { it.startsWith(ACCESS_TOKEN_KEY) }
|
||||||
?.drop(ACCESS_TOKEN_KEY.length) ?: return null
|
?.drop(ACCESS_TOKEN_KEY.length) ?: return null
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,10 @@ import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorag
|
||||||
import org.fnives.test.showcase.model.content.ContentId
|
import org.fnives.test.showcase.model.content.ContentId
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class FavouriteContentLocalStorageImpl @Inject constructor(private val favouriteDao: FavouriteDao) : FavouriteContentLocalStorage {
|
class FavouriteContentLocalStorageImpl @Inject constructor(
|
||||||
|
private val favouriteDao: FavouriteDao
|
||||||
|
) : FavouriteContentLocalStorage {
|
||||||
|
|
||||||
override fun observeFavourites(): Flow<List<ContentId>> =
|
override fun observeFavourites(): Flow<List<ContentId>> =
|
||||||
favouriteDao.get().map { it.map(FavouriteEntity::contentId).map(::ContentId) }
|
favouriteDao.get().map { it.map(FavouriteEntity::contentId).map(::ContentId) }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import org.fnives.test.showcase.R
|
import org.fnives.test.showcase.R
|
||||||
import org.fnives.test.showcase.databinding.ActivityMainBinding
|
import org.fnives.test.showcase.databinding.ActivityMainBinding
|
||||||
import org.fnives.test.showcase.model.content.ContentId
|
import org.fnives.test.showcase.model.content.ContentId
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ internal class FavouriteContentLocalStorageImplTest {
|
||||||
val hiltRule = HiltAndroidRule(this)
|
val hiltRule = HiltAndroidRule(this)
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var sut : FavouriteContentLocalStorage
|
lateinit var sut: FavouriteContentLocalStorage
|
||||||
private lateinit var testDispatcher: TestCoroutineDispatcher
|
private lateinit var testDispatcher: TestCoroutineDispatcher
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
//package org.fnives.test.showcase.testutils.statesetup
|
// package org.fnives.test.showcase.testutils.statesetup
|
||||||
//
|
//
|
||||||
//import kotlinx.coroutines.runBlocking
|
// import kotlinx.coroutines.runBlocking
|
||||||
//import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
|
// import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
|
||||||
//import org.fnives.test.showcase.core.login.LoginUseCase
|
// import org.fnives.test.showcase.core.login.LoginUseCase
|
||||||
//import org.fnives.test.showcase.core.login.LogoutUseCase
|
// import org.fnives.test.showcase.core.login.LogoutUseCase
|
||||||
//import org.fnives.test.showcase.model.auth.LoginCredentials
|
// import org.fnives.test.showcase.model.auth.LoginCredentials
|
||||||
//import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
|
// import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
|
||||||
//import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
|
// import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
|
||||||
//import org.koin.test.KoinTest
|
// import org.koin.test.KoinTest
|
||||||
//import org.koin.test.get
|
// import org.koin.test.get
|
||||||
//
|
//
|
||||||
//object SetupLoggedInState : KoinTest {
|
// object SetupLoggedInState : KoinTest {
|
||||||
//
|
//
|
||||||
// private val logoutUseCase get() = get<LogoutUseCase>()
|
// private val logoutUseCase get() = get<LogoutUseCase>()
|
||||||
// private val loginUseCase get() = get<LoginUseCase>()
|
// private val loginUseCase get() = get<LoginUseCase>()
|
||||||
|
|
@ -28,4 +28,4 @@
|
||||||
// fun setupLogout() {
|
// fun setupLogout() {
|
||||||
// runBlocking { logoutUseCase.invoke() }
|
// runBlocking { logoutUseCase.invoke() }
|
||||||
// }
|
// }
|
||||||
//}
|
// }
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class SetupLoggedInState @Inject constructor(
|
class SetupLoggedInState @Inject constructor(
|
||||||
private val logoutUseCase : LogoutUseCase,
|
private val logoutUseCase: LogoutUseCase,
|
||||||
private val loginUseCase : LoginUseCase,
|
private val loginUseCase: LoginUseCase,
|
||||||
private val isUserLoggedInUseCase: IsUserLoggedInUseCase
|
private val isUserLoggedInUseCase: IsUserLoggedInUseCase
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,9 @@ import org.fnives.test.showcase.model.content.Content
|
||||||
import org.fnives.test.showcase.model.shared.Resource
|
import org.fnives.test.showcase.model.shared.Resource
|
||||||
import org.fnives.test.showcase.network.content.ContentRemoteSource
|
import org.fnives.test.showcase.network.content.ContentRemoteSource
|
||||||
|
|
||||||
internal class ContentRepository @LoggedInModuleInject constructor(private val contentRemoteSource: ContentRemoteSource) {
|
internal class ContentRepository @LoggedInModuleInject constructor(
|
||||||
|
private val contentRemoteSource: ContentRemoteSource
|
||||||
|
) {
|
||||||
|
|
||||||
private val mutableContentFlow = MutableStateFlow(Optional<List<Content>>(null))
|
private val mutableContentFlow = MutableStateFlow(Optional<List<Content>>(null))
|
||||||
private val requestFlow: Flow<Resource<List<Content>>> = flow {
|
private val requestFlow: Flow<Resource<List<Content>>> = flow {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import org.fnives.test.showcase.core.storage.NetworkSessionLocalStorageAdapter
|
||||||
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
|
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
|
||||||
import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
|
import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
|
||||||
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
|
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
|
||||||
import org.fnives.test.showcase.core.di.hilt.ReloadLoggedInModuleInjectModule
|
|
||||||
|
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
@Module
|
@Module
|
||||||
|
|
@ -30,5 +29,5 @@ object CoreModule {
|
||||||
fun provideLogoutUseCase(
|
fun provideLogoutUseCase(
|
||||||
storage: UserDataLocalStorage,
|
storage: UserDataLocalStorage,
|
||||||
reloadLoggedInModuleInjectModule: ReloadLoggedInModuleInjectModule
|
reloadLoggedInModuleInjectModule: ReloadLoggedInModuleInjectModule
|
||||||
) : LogoutUseCase = LogoutUseCase(storage, reloadLoggedInModuleInjectModule)
|
): LogoutUseCase = LogoutUseCase(storage, reloadLoggedInModuleInjectModule)
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
package org.fnives.test.showcase.core.login
|
package org.fnives.test.showcase.core.login
|
||||||
|
|
||||||
|
import org.fnives.test.showcase.core.di.hilt.ReloadLoggedInModuleInjectModule
|
||||||
import org.fnives.test.showcase.core.di.koin.repositoryModule
|
import org.fnives.test.showcase.core.di.koin.repositoryModule
|
||||||
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
|
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
|
||||||
import org.koin.core.context.loadKoinModules
|
import org.koin.core.context.loadKoinModules
|
||||||
import org.koin.mp.KoinPlatformTools
|
import org.koin.mp.KoinPlatformTools
|
||||||
import org.fnives.test.showcase.core.di.hilt.ReloadLoggedInModuleInjectModule
|
|
||||||
|
|
||||||
class LogoutUseCase(
|
class LogoutUseCase(
|
||||||
private val storage: UserDataLocalStorage,
|
private val storage: UserDataLocalStorage,
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,11 @@ package org.fnives.test.showcase.core.login.hilt
|
||||||
|
|
||||||
import kotlinx.coroutines.test.runBlockingTest
|
import kotlinx.coroutines.test.runBlockingTest
|
||||||
import org.fnives.test.showcase.core.content.ContentRepository
|
import org.fnives.test.showcase.core.content.ContentRepository
|
||||||
import org.fnives.test.showcase.core.di.koin.createCoreModule
|
|
||||||
import org.fnives.test.showcase.core.login.LogoutUseCase
|
import org.fnives.test.showcase.core.login.LogoutUseCase
|
||||||
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
|
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
|
||||||
import org.fnives.test.showcase.model.network.BaseUrl
|
|
||||||
import org.junit.jupiter.api.AfterEach
|
|
||||||
import org.junit.jupiter.api.Assertions
|
import org.junit.jupiter.api.Assertions
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.koin.core.context.startKoin
|
|
||||||
import org.koin.core.context.stopKoin
|
|
||||||
import org.koin.test.KoinTest
|
|
||||||
import org.mockito.kotlin.mock
|
import org.mockito.kotlin.mock
|
||||||
import org.mockito.kotlin.times
|
import org.mockito.kotlin.times
|
||||||
import org.mockito.kotlin.verify
|
import org.mockito.kotlin.verify
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import javax.inject.Singleton
|
||||||
@Component(modules = [CoreModule::class, HiltNetworkModule::class, ReloadLoggedInModuleInjectModuleImpl::class, BindsBaseOkHttpClient::class])
|
@Component(modules = [CoreModule::class, HiltNetworkModule::class, ReloadLoggedInModuleInjectModuleImpl::class, BindsBaseOkHttpClient::class])
|
||||||
internal interface TestCoreComponent {
|
internal interface TestCoreComponent {
|
||||||
|
|
||||||
|
|
||||||
@Component.Builder
|
@Component.Builder
|
||||||
interface Builder {
|
interface Builder {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@ import org.fnives.test.showcase.model.content.ImageUrl
|
||||||
import org.fnives.test.showcase.network.shared.ExceptionWrapper
|
import org.fnives.test.showcase.network.shared.ExceptionWrapper
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class ContentRemoteSourceImpl @Inject constructor(private val contentService: ContentService) : ContentRemoteSource {
|
internal class ContentRemoteSourceImpl @Inject constructor(
|
||||||
|
private val contentService: ContentService
|
||||||
|
) : ContentRemoteSource {
|
||||||
|
|
||||||
override suspend fun get(): List<Content> =
|
override suspend fun get(): List<Content> =
|
||||||
ExceptionWrapper.wrap {
|
ExceptionWrapper.wrap {
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ abstract class BindsBaseOkHttpClient {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@SessionLessQualifier
|
@SessionLessQualifier
|
||||||
abstract fun bindsSessionLess(okHttpClient: OkHttpClient) : OkHttpClient
|
abstract fun bindsSessionLess(okHttpClient: OkHttpClient): OkHttpClient
|
||||||
}
|
}
|
||||||
|
|
@ -93,5 +93,4 @@ object HiltNetworkModule {
|
||||||
@Provides
|
@Provides
|
||||||
internal fun provideContentService(@SessionQualifier retrofit: Retrofit): ContentService =
|
internal fun provideContentService(@SessionQualifier retrofit: Retrofit): ContentService =
|
||||||
retrofit.create(ContentService::class.java)
|
retrofit.create(ContentService::class.java)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -3,13 +3,16 @@ package org.fnives.test.showcase.network.session
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class AuthenticationHeaderUtils @Inject constructor(private val networkSessionLocalStorage: NetworkSessionLocalStorage) {
|
internal class AuthenticationHeaderUtils @Inject constructor(
|
||||||
|
private val networkSessionLocalStorage: NetworkSessionLocalStorage
|
||||||
|
) {
|
||||||
|
|
||||||
fun hasToken(okhttpRequest: Request): Boolean =
|
fun hasToken(okhttpRequest: Request): Boolean =
|
||||||
okhttpRequest.header(KEY) == networkSessionLocalStorage.session?.accessToken
|
okhttpRequest.header(KEY) == networkSessionLocalStorage.session?.accessToken
|
||||||
|
|
||||||
fun attachToken(okhttpRequest: Request): Request =
|
fun attachToken(okhttpRequest: Request): Request =
|
||||||
okhttpRequest.newBuilder().header(KEY, networkSessionLocalStorage.session?.accessToken.orEmpty()).build()
|
okhttpRequest.newBuilder()
|
||||||
|
.header(KEY, networkSessionLocalStorage.session?.accessToken.orEmpty()).build()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val KEY = "Authorization"
|
private const val KEY = "Authorization"
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import javax.inject.Singleton
|
||||||
@Component(modules = [HiltNetworkModule::class, BindsBaseOkHttpClient::class])
|
@Component(modules = [HiltNetworkModule::class, BindsBaseOkHttpClient::class])
|
||||||
interface TestNetworkComponent {
|
interface TestNetworkComponent {
|
||||||
|
|
||||||
|
|
||||||
@Component.Builder
|
@Component.Builder
|
||||||
interface Builder {
|
interface Builder {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,22 @@
|
||||||
package org.fnives.test.showcase.network.content.hilt
|
package org.fnives.test.showcase.network.content.hilt
|
||||||
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.fnives.test.showcase.model.network.BaseUrl
|
import org.fnives.test.showcase.network.DaggerTestNetworkComponent
|
||||||
import org.fnives.test.showcase.network.TestNetworkComponent
|
|
||||||
import org.fnives.test.showcase.network.content.ContentRemoteSourceImpl
|
import org.fnives.test.showcase.network.content.ContentRemoteSourceImpl
|
||||||
import org.fnives.test.showcase.network.di.koin.createNetworkModules
|
|
||||||
import org.fnives.test.showcase.network.mockserver.ContentData
|
import org.fnives.test.showcase.network.mockserver.ContentData
|
||||||
import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario
|
import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario
|
||||||
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
|
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
|
||||||
import org.fnives.test.showcase.network.shared.MockServerScenarioSetupExtensions
|
import org.fnives.test.showcase.network.shared.MockServerScenarioSetupExtensions
|
||||||
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
|
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
|
||||||
import org.fnives.test.showcase.network.shared.exceptions.ParsingException
|
import org.fnives.test.showcase.network.shared.exceptions.ParsingException
|
||||||
import org.junit.jupiter.api.AfterEach
|
|
||||||
import org.junit.jupiter.api.Assertions
|
import org.junit.jupiter.api.Assertions
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.RegisterExtension
|
import org.junit.jupiter.api.extension.RegisterExtension
|
||||||
import org.koin.core.context.startKoin
|
|
||||||
import org.koin.core.context.stopKoin
|
|
||||||
import org.koin.test.KoinTest
|
|
||||||
import org.koin.test.inject
|
import org.koin.test.inject
|
||||||
import org.mockito.kotlin.doReturn
|
import org.mockito.kotlin.doReturn
|
||||||
import org.mockito.kotlin.mock
|
import org.mockito.kotlin.mock
|
||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
import org.fnives.test.showcase.network.DaggerTestNetworkComponent
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Suppress("TestFunctionName")
|
@Suppress("TestFunctionName")
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,11 @@ import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
|
||||||
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
|
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
|
||||||
import org.fnives.test.showcase.network.shared.MockServerScenarioSetupExtensions
|
import org.fnives.test.showcase.network.shared.MockServerScenarioSetupExtensions
|
||||||
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
|
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
|
||||||
import org.junit.jupiter.api.AfterEach
|
|
||||||
import org.junit.jupiter.api.Assertions
|
import org.junit.jupiter.api.Assertions
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.RegisterExtension
|
import org.junit.jupiter.api.extension.RegisterExtension
|
||||||
import org.koin.core.context.stopKoin
|
|
||||||
import org.koin.test.KoinTest
|
|
||||||
import org.koin.test.inject
|
import org.koin.test.inject
|
||||||
import org.mockito.kotlin.anyOrNull
|
import org.mockito.kotlin.anyOrNull
|
||||||
import org.mockito.kotlin.doAnswer
|
import org.mockito.kotlin.doAnswer
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue