diff --git a/core/network/src/main/java/ru/fincode/tsudesk/core/network/NetworkConstants.kt b/core/network/src/main/java/ru/fincode/tsudesk/core/network/Constants.kt similarity index 80% rename from core/network/src/main/java/ru/fincode/tsudesk/core/network/NetworkConstants.kt rename to core/network/src/main/java/ru/fincode/tsudesk/core/network/Constants.kt index a20299b..2816251 100644 --- a/core/network/src/main/java/ru/fincode/tsudesk/core/network/NetworkConstants.kt +++ b/core/network/src/main/java/ru/fincode/tsudesk/core/network/Constants.kt @@ -1,5 +1,5 @@ package ru.fincode.tsudesk.core.network -object NetworkConstants { +object Constants { const val BASE_URL = "https://tulsu.ru/schedule/queries/" } \ No newline at end of file diff --git a/core/network/src/main/java/ru/fincode/tsudesk/core/network/NetworkCall.kt b/core/network/src/main/java/ru/fincode/tsudesk/core/network/NetworkCall.kt index d249f44..fe14753 100644 --- a/core/network/src/main/java/ru/fincode/tsudesk/core/network/NetworkCall.kt +++ b/core/network/src/main/java/ru/fincode/tsudesk/core/network/NetworkCall.kt @@ -1,24 +1,58 @@ package ru.fincode.tsudesk.core.network import retrofit2.HttpException +import retrofit2.Response import ru.fincode.tsudesk.core.network.model.NetworkError +import ru.fincode.tsudesk.core.network.model.NetworkMeta import ru.fincode.tsudesk.core.network.model.NetworkResult +import ru.fincode.tsudesk.core.network.model.NetworkResult.Success import java.io.IOException import java.net.SocketTimeoutException -suspend inline fun safeApiCall( - crossinline block: suspend () -> T +suspend inline fun apiCall( + crossinline block: suspend () -> Response ): NetworkResult { + val startedAt = System.currentTimeMillis() + return try { - NetworkResult.Success(block()) + val response = block() + val finishedAt = System.currentTimeMillis() + + val raw = response.raw() + val meta = NetworkMeta( + startedAtMillis = startedAt, + finishedAtMillis = finishedAt, + sentAtMillis = raw.sentRequestAtMillis, + receivedAtMillis = raw.receivedResponseAtMillis + ) + + if (response.isSuccessful) { + val body = response.body() + if (body != null) { + Success(data = body, meta = meta) + } else { + NetworkResult.Error( + NetworkError.Unknown( + throwable = NullPointerException("Response body is null"), + meta = meta + ) + ) + } + } else { + NetworkResult.Error(NetworkError.Http(code = response.code(), meta = meta)) + } } catch (t: Throwable) { - NetworkResult.Error(t.toNetworkError()) + val meta = NetworkMeta( + startedAtMillis = startedAt, + finishedAtMillis = System.currentTimeMillis() + ) + NetworkResult.Error(t.toNetworkError(meta)) } } -fun Throwable.toNetworkError(): NetworkError = when (this) { - is SocketTimeoutException -> NetworkError.Timeout - is IOException -> NetworkError.NoInternet - is HttpException -> NetworkError.Http(code()) - else -> NetworkError.Unknown(this) +fun Throwable.toNetworkError(meta: NetworkMeta): NetworkError = when (this) { + is SocketTimeoutException -> NetworkError.Timeout(meta) + is IOException -> NetworkError.NoInternet(meta) + is HttpException -> NetworkError.Http(code(), meta) + else -> NetworkError.Unknown(this, meta) } diff --git a/core/network/src/main/java/ru/fincode/tsudesk/core/network/NetworkModule.kt b/core/network/src/main/java/ru/fincode/tsudesk/core/network/di/OkHttpModule.kt similarity index 58% rename from core/network/src/main/java/ru/fincode/tsudesk/core/network/NetworkModule.kt rename to core/network/src/main/java/ru/fincode/tsudesk/core/network/di/OkHttpModule.kt index d3ad925..76f4936 100644 --- a/core/network/src/main/java/ru/fincode/tsudesk/core/network/NetworkModule.kt +++ b/core/network/src/main/java/ru/fincode/tsudesk/core/network/di/OkHttpModule.kt @@ -1,40 +1,30 @@ -package ru.fincode.tsudesk.core.network +package ru.fincode.tsudesk.core.network.di import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import okhttp3.OkHttpClient -import retrofit2.Retrofit +import ru.fincode.tsudesk.core.network.interceptor.DebugInterceptor import java.util.concurrent.TimeUnit import javax.inject.Singleton -import okhttp3.logging.HttpLoggingInterceptor -import ru.fincode.tsudesk.core.network.interceptor.DebugInterceptor @Module @InstallIn(SingletonComponent::class) -object NetworkModule { +object OkHttpModule { private const val TIMEOUT = 30L - val logging = HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY - } @Provides @Singleton - fun provideOkHttpClient(): OkHttpClient = - OkHttpClient.Builder() + fun provideOkHttpClient( + ): OkHttpClient { + return OkHttpClient.Builder() .connectTimeout(TIMEOUT, TimeUnit.SECONDS) .readTimeout(TIMEOUT, TimeUnit.SECONDS) .writeTimeout(TIMEOUT, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .addInterceptor(DebugInterceptor()) .build() - - @Provides - fun provideRetrofit( - provider: RetrofitProvider, - client: OkHttpClient - ): Retrofit = provider.process(NetworkConstants.BASE_URL, client) - -} \ No newline at end of file + } +} diff --git a/core/network/src/main/java/ru/fincode/tsudesk/core/network/di/RetrofitModule.kt b/core/network/src/main/java/ru/fincode/tsudesk/core/network/di/RetrofitModule.kt new file mode 100644 index 0000000..d5056fa --- /dev/null +++ b/core/network/src/main/java/ru/fincode/tsudesk/core/network/di/RetrofitModule.kt @@ -0,0 +1,24 @@ +package ru.fincode.tsudesk.core.network.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import ru.fincode.tsudesk.core.network.Constants +import ru.fincode.tsudesk.core.network.RetrofitProvider +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object RetrofitModule { + + @Provides + @Singleton + fun provideRetrofit( + provider: RetrofitProvider, + client: OkHttpClient + ): Retrofit = + provider.process(Constants.BASE_URL, client) +} diff --git a/core/network/src/main/java/ru/fincode/tsudesk/core/network/model/NetworkError.kt b/core/network/src/main/java/ru/fincode/tsudesk/core/network/model/NetworkError.kt index 5aead0a..951196b 100644 --- a/core/network/src/main/java/ru/fincode/tsudesk/core/network/model/NetworkError.kt +++ b/core/network/src/main/java/ru/fincode/tsudesk/core/network/model/NetworkError.kt @@ -1,8 +1,17 @@ package ru.fincode.tsudesk.core.network.model -sealed class NetworkError { - object NoInternet : NetworkError() - object Timeout : NetworkError() - data class Http(val code: Int) : NetworkError() - data class Unknown(val throwable: Throwable) : NetworkError() -} \ No newline at end of file +sealed class NetworkError(open val meta: NetworkMeta) { + + data class NoInternet(override val meta: NetworkMeta) : NetworkError(meta) + data class Timeout(override val meta: NetworkMeta) : NetworkError(meta) + + data class Http( + val code: Int, + override val meta: NetworkMeta + ) : NetworkError(meta) + + data class Unknown( + val throwable: Throwable, + override val meta: NetworkMeta + ) : NetworkError(meta) +} diff --git a/core/network/src/main/java/ru/fincode/tsudesk/core/network/model/NetworkMeta.kt b/core/network/src/main/java/ru/fincode/tsudesk/core/network/model/NetworkMeta.kt new file mode 100644 index 0000000..7d34680 --- /dev/null +++ b/core/network/src/main/java/ru/fincode/tsudesk/core/network/model/NetworkMeta.kt @@ -0,0 +1,8 @@ +package ru.fincode.tsudesk.core.network.model + +data class NetworkMeta( + val startedAtMillis: Long, + val finishedAtMillis: Long, + val sentAtMillis: Long? = null, + val receivedAtMillis: Long? = null +) diff --git a/core/network/src/main/java/ru/fincode/tsudesk/core/network/model/NetworkResult.kt b/core/network/src/main/java/ru/fincode/tsudesk/core/network/model/NetworkResult.kt index 7e77fbf..ffb3d4b 100644 --- a/core/network/src/main/java/ru/fincode/tsudesk/core/network/model/NetworkResult.kt +++ b/core/network/src/main/java/ru/fincode/tsudesk/core/network/model/NetworkResult.kt @@ -1,6 +1,6 @@ package ru.fincode.tsudesk.core.network.model sealed class NetworkResult { - data class Success(val data: T) : NetworkResult() + data class Success(val data: T, val meta: NetworkMeta) : NetworkResult() data class Error(val error: NetworkError) : NetworkResult() -} \ No newline at end of file +} diff --git a/core/network/src/main/java/ru/fincode/tsudesk/core/network/model/NetworkResultExt.kt b/core/network/src/main/java/ru/fincode/tsudesk/core/network/model/NetworkResultExt.kt index 69d4e04..bbeb3bb 100644 --- a/core/network/src/main/java/ru/fincode/tsudesk/core/network/model/NetworkResultExt.kt +++ b/core/network/src/main/java/ru/fincode/tsudesk/core/network/model/NetworkResultExt.kt @@ -1,8 +1,14 @@ package ru.fincode.tsudesk.core.network.model inline fun NetworkResult.map( - transform: (T) -> R -): NetworkResult = when (this) { - is NetworkResult.Success -> NetworkResult.Success(transform(data)) - is NetworkResult.Error -> this -} + transform: (T, NetworkMeta) -> R +): NetworkResult = + when (val source = this) { + + is NetworkResult.Success -> NetworkResult.Success( + data = transform(source.data, source.meta), + meta = source.meta + ) + + is NetworkResult.Error -> NetworkResult.Error(source.error) + } diff --git a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/datasource/ScheduleRemoteDataSource.kt b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/datasource/ScheduleRemoteDataSource.kt index a90ecda..4c7b75b 100644 --- a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/datasource/ScheduleRemoteDataSource.kt +++ b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/datasource/ScheduleRemoteDataSource.kt @@ -1,19 +1,9 @@ package ru.fincode.tsudesk.feature.schedule.data.datasource import ru.fincode.tsudesk.core.network.model.NetworkResult -import ru.fincode.tsudesk.core.network.safeApiCall -import ru.fincode.tsudesk.feature.schedule.data.remote.ScheduleApi -import ru.fincode.tsudesk.feature.schedule.data.remote.model.LessonDto.ScheduleSearchField.GROUP_P -import ru.fincode.tsudesk.feature.schedule.data.remote.model.LessonDto.ScheduleSearchField.PREP import ru.fincode.tsudesk.feature.schedule.data.remote.model.ScheduleDto -import javax.inject.Inject -class ScheduleRemoteDataSource @Inject constructor( - private val api: ScheduleApi -) { - suspend fun loadScheduleByGroup(number: String): NetworkResult = - safeApiCall { api.getSchedule(GROUP_P, number) } - - suspend fun loadScheduleByTeacher(name: String): NetworkResult = - safeApiCall { api.getSchedule(PREP, name) } +interface ScheduleRemoteDataSource { + suspend fun loadScheduleByGroup(number: String): NetworkResult + suspend fun loadScheduleByTeacher(name: String): NetworkResult } diff --git a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/datasource/ScheduleRemoteDataSourceImpl.kt b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/datasource/ScheduleRemoteDataSourceImpl.kt deleted file mode 100644 index 2d56f2f..0000000 --- a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/datasource/ScheduleRemoteDataSourceImpl.kt +++ /dev/null @@ -1,21 +0,0 @@ -package ru.fincode.tsudesk.feature.schedule.data.datasource - -import ru.fincode.tsudesk.core.network.model.NetworkResult -import ru.fincode.tsudesk.core.network.model.map -import ru.fincode.tsudesk.feature.schedule.data.mapper.ScheduleDtoToDomainMapper -import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity -import ru.fincode.tsudesk.feature.schedule.domain.repository.ScheduleRepository -import javax.inject.Inject - -class ScheduleRemoteDataSourceImpl @Inject constructor( - private val remote: ScheduleRemoteDataSource, - private val mapper: ScheduleDtoToDomainMapper -) : ScheduleRepository { - - override suspend fun loadScheduleByGroup(number: String): NetworkResult = - remote.loadScheduleByGroup(number).map(mapper::invoke) - - override suspend fun loadScheduleByTeacher(name: String): NetworkResult = - remote.loadScheduleByTeacher(name).map(mapper::invoke) - -} diff --git a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/mapper/ScheduleDtoToDomainMapper.kt b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/mapper/ScheduleDtoToDomainMapper.kt index 2515da6..ea18a30 100644 --- a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/mapper/ScheduleDtoToDomainMapper.kt +++ b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/mapper/ScheduleDtoToDomainMapper.kt @@ -1,5 +1,6 @@ package ru.fincode.tsudesk.feature.schedule.data.mapper +import ru.fincode.tsudesk.core.network.model.NetworkMeta import ru.fincode.tsudesk.feature.schedule.data.remote.model.LessonDto import ru.fincode.tsudesk.feature.schedule.data.remote.model.ScheduleDto import ru.fincode.tsudesk.feature.schedule.domain.model.LessonEntity @@ -8,8 +9,11 @@ import javax.inject.Inject class ScheduleDtoToDomainMapper @Inject constructor() { - operator fun invoke(dto: ScheduleDto): ScheduleEntity = - ScheduleEntity(lessons = dto.map(::mapLesson)) + operator fun invoke(dto: ScheduleDto, meta: NetworkMeta): ScheduleEntity = + ScheduleEntity( + lessons = dto.map(::mapLesson), + timestamp = meta.receivedAtMillis ?: meta.finishedAtMillis + ) private fun mapLesson(item: LessonDto): LessonEntity = LessonEntity( @@ -22,4 +26,4 @@ class ScheduleDtoToDomainMapper @Inject constructor() { groupId = item.groups.firstOrNull()?.groupCode.orEmpty(), type = item.type.trim() ) -} +} \ No newline at end of file diff --git a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/remote/ScheduleApi.kt b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/remote/ScheduleApi.kt index b775779..4ba0669 100644 --- a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/remote/ScheduleApi.kt +++ b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/remote/ScheduleApi.kt @@ -1,27 +1,18 @@ package ru.fincode.tsudesk.feature.schedule.data.remote +import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Query -import ru.fincode.tsudesk.feature.schedule.data.remote.model.LessonDto -import ru.fincode.tsudesk.feature.schedule.data.remote.model.LessonDto.* +import ru.fincode.tsudesk.feature.schedule.data.remote.ScheduleApiContract.Path.GET_SCHEDULE_METHOD +import ru.fincode.tsudesk.feature.schedule.data.remote.ScheduleApiContract.Query.SEARCH_FIELD +import ru.fincode.tsudesk.feature.schedule.data.remote.ScheduleApiContract.Query.SEARCH_VALUE import ru.fincode.tsudesk.feature.schedule.data.remote.model.ScheduleDto interface ScheduleApi { - /** - * Расписание по номеру группы. - * Пример: search_field=GROUP_P&search_value=220631 - * https://tulsu.ru/schedule/queries/GetSchedule.php?search_field=GROUP_P&search_value=220631 - * - * - * Расписание по ФИО преподавателя (строкой). - * Пример: search_field=PREP&search_value=Набродова Ирина Николаевна - * - * Важно: Retrofit сам URL-энкодит параметр search_value - * https://tulsu.ru/schedule/queries/GetDates.php?search_value=%D0%9D%D0%B0%D0%B1%D1%80%D0%BE%D0%B4%D0%BE%D0%B2%D0%B0%20%D0%98%D1%80%D0%B8%D0%BD%D0%B0%20%D0%9D%D0%B8%D0%BA%D0%BE%D0%BB%D0%B0%D0%B5%D0%B2%D0%BD%D0%B0 - */ - @GET("GetSchedule.php") + @GET(GET_SCHEDULE_METHOD) suspend fun getSchedule( - @Query("search_field") searchField: String, @Query("search_value") searchValue: String - ): ScheduleDto + @Query(SEARCH_FIELD) searchField: String, + @Query(SEARCH_VALUE) searchValue: String + ): Response } diff --git a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/remote/ScheduleApiContract.kt b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/remote/ScheduleApiContract.kt new file mode 100644 index 0000000..295885e --- /dev/null +++ b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/remote/ScheduleApiContract.kt @@ -0,0 +1,16 @@ +package ru.fincode.tsudesk.feature.schedule.data.remote + +object ScheduleApiContract { + + object Path { + const val GET_SCHEDULE_METHOD = "GetSchedule.php" + } + + object Query { + const val SEARCH_FIELD = "search_field" + const val SEARCH_VALUE = "search_value" + + const val FIELD_GROUP = "GROUP_P" + const val FIELD_TEACHER = "PREP" + } +} diff --git a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/remote/ScheduleRemoteDataSourceImpl.kt b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/remote/ScheduleRemoteDataSourceImpl.kt new file mode 100644 index 0000000..1f435b9 --- /dev/null +++ b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/remote/ScheduleRemoteDataSourceImpl.kt @@ -0,0 +1,28 @@ +package ru.fincode.tsudesk.feature.schedule.data.remote + +import ru.fincode.tsudesk.core.network.model.NetworkResult +import ru.fincode.tsudesk.core.network.apiCall +import ru.fincode.tsudesk.feature.schedule.data.datasource.ScheduleRemoteDataSource +import ru.fincode.tsudesk.feature.schedule.data.remote.model.ScheduleDto +import javax.inject.Inject + +class ScheduleRemoteDataSourceImpl @Inject constructor( + private val api: ScheduleApi +) : ScheduleRemoteDataSource { + + override suspend fun loadScheduleByGroup(number: String): NetworkResult = + apiCall { + api.getSchedule( + searchField = ScheduleApiContract.Query.FIELD_GROUP, + searchValue = number + ) + } + + override suspend fun loadScheduleByTeacher(name: String): NetworkResult = + apiCall { + api.getSchedule( + searchField = ScheduleApiContract.Query.FIELD_TEACHER, + searchValue = name + ) + } +} \ No newline at end of file diff --git a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/di/ScheduleRemoteDataSourceModule.kt b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/di/ScheduleRemoteDataSourceModule.kt new file mode 100644 index 0000000..65204ed --- /dev/null +++ b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/di/ScheduleRemoteDataSourceModule.kt @@ -0,0 +1,20 @@ +package ru.fincode.tsudesk.feature.schedule.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import ru.fincode.tsudesk.feature.schedule.data.datasource.ScheduleRemoteDataSource +import ru.fincode.tsudesk.feature.schedule.data.remote.ScheduleRemoteDataSourceImpl +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class ScheduleRemoteDataSourceModule { + + @Binds + @Singleton + abstract fun bindScheduleRemoteDataSource( + impl: ScheduleRemoteDataSourceImpl + ): ScheduleRemoteDataSource +} diff --git a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/domain/model/ScheduleEntity.kt b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/domain/model/ScheduleEntity.kt index 0f47aa7..7e125bd 100644 --- a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/domain/model/ScheduleEntity.kt +++ b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/domain/model/ScheduleEntity.kt @@ -1,7 +1,8 @@ package ru.fincode.tsudesk.feature.schedule.domain.model data class ScheduleEntity( - val lessons: List + val lessons: List, + val timestamp: Long )