Фикс краша при получении расписания с включённым VPN.

Допилено обновление расписания без перезагрузки всего экрана.
This commit is contained in:
2024-10-02 00:22:51 +04:00
parent f2724d275b
commit 4db3a7d1c2
20 changed files with 312 additions and 147 deletions

View File

@@ -1,7 +1,5 @@
package ru.n08i40k.polytechnic.next.data
import java.lang.Exception
sealed interface MyResult<out R> {
data class Success<out T>(val data: T) : MyResult<T>
data class Failure(val exception: Exception) : MyResult<Nothing>

View File

@@ -32,12 +32,12 @@ class LocalNetworkCacheRepository
}
override suspend fun get(url: String): CachedResponse? {
if (this.hash == null)
return null
// Если кешированого ответа нет, то возвращаем null
// Если хеши не совпадают и локальный хеш присутствует, то возвращаем null
val response = cacheMap[url]
val response = cacheMap[url] ?: return null
if (response?.hash != this.hash)
if (response.hash != this.hash && this.hash != null)
return null
return response

View File

@@ -1,7 +1,7 @@
package ru.n08i40k.polytechnic.next.data.schedule
import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.model.Group
interface ScheduleRepository {
suspend fun getGroup(): MyResult<Group>

View File

@@ -4,13 +4,13 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
import ru.n08i40k.polytechnic.next.model.Day
import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
import ru.n08i40k.polytechnic.next.model.Lesson
import ru.n08i40k.polytechnic.next.model.LessonTime
import ru.n08i40k.polytechnic.next.model.LessonType
import ru.n08i40k.polytechnic.next.data.MyResult
class FakeScheduleRepository : ScheduleRepository {
@Suppress("SpellCheckingInspection")

View File

@@ -1,7 +1,6 @@
package ru.n08i40k.polytechnic.next.data.schedule.impl
import android.content.Context
import com.android.volley.toolbox.RequestFuture
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
@@ -12,7 +11,7 @@ import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetRequest
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetRequestData
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetResponse
import ru.n08i40k.polytechnic.next.network.tryFuture
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
class RemoteScheduleRepository(private val context: Context) : ScheduleRepository {
@@ -27,18 +26,18 @@ class RemoteScheduleRepository(private val context: Context) : ScheduleRepositor
if (groupName.isEmpty())
return@withContext MyResult.Failure(IllegalArgumentException("No group name provided!"))
val future = RequestFuture.newFuture<ScheduleGetResponse>()
ScheduleGetRequest(
ScheduleGetRequestData(groupName),
context,
future,
future
).send()
val response = tryFuture {
ScheduleGetRequest(
ScheduleGetRequestData(groupName),
context,
it,
it
)
}
try {
MyResult.Success(future.get().group)
} catch (exception: Exception) {
MyResult.Failure(exception)
when (response) {
is MyResult.Failure -> response
is MyResult.Success -> MyResult.Success(response.data.group)
}
}
}

View File

@@ -7,7 +7,6 @@ import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.data.users.ProfileRepository
import ru.n08i40k.polytechnic.next.model.Profile
import ru.n08i40k.polytechnic.next.model.UserRole
import java.lang.Exception
class FakeProfileRepository : ProfileRepository {
private var counter = 0

View File

@@ -1,28 +1,23 @@
package ru.n08i40k.polytechnic.next.data.users.impl
import android.content.Context
import com.android.volley.toolbox.RequestFuture
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.data.users.ProfileRepository
import ru.n08i40k.polytechnic.next.model.Profile
import ru.n08i40k.polytechnic.next.network.data.profile.UsersMeRequest
import ru.n08i40k.polytechnic.next.network.tryFuture
class RemoteProfileRepository(private val context: Context) : ProfileRepository {
override suspend fun getProfile(): MyResult<Profile> {
return withContext(Dispatchers.IO) {
val responseFuture = RequestFuture.newFuture<Profile>()
UsersMeRequest(
context,
responseFuture,
responseFuture
).send()
try {
MyResult.Success(responseFuture.get())
} catch (exception: Exception) {
MyResult.Failure(exception)
tryFuture {
UsersMeRequest(
context,
it,
it
)
}
}
}

View File

@@ -5,10 +5,15 @@ import android.content.Context
import com.android.volley.Request
import com.android.volley.RequestQueue
import com.android.volley.Response
import com.android.volley.VolleyError
import com.android.volley.toolbox.HurlStack
import com.android.volley.toolbox.RequestFuture
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
import ru.n08i40k.polytechnic.next.data.MyResult
import java.security.cert.X509Certificate
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeoutException
import java.util.logging.Logger
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
@@ -82,3 +87,23 @@ open class RequestBase(
return headers
}
}
fun <ResultT, RequestT : RequestBase> tryFuture(
buildRequest: (RequestFuture<ResultT>) -> RequestT
): MyResult<ResultT> {
val future = RequestFuture.newFuture<ResultT>()
buildRequest(future).send()
return tryGet(future)
}
fun <T> tryGet(future: RequestFuture<T>): MyResult<T> {
return try {
MyResult.Success(future.get())
} catch (exception: VolleyError) {
MyResult.Failure(exception)
} catch (exception: ExecutionException) {
MyResult.Failure(exception.cause as VolleyError)
} catch (exception: TimeoutException) {
MyResult.Failure(exception)
}
}

View File

@@ -2,7 +2,6 @@ package ru.n08i40k.polytechnic.next.network.data
import android.content.Context
import com.android.volley.Response
import com.android.volley.VolleyError
import com.android.volley.toolbox.RequestFuture
import com.android.volley.toolbox.StringRequest
import kotlinx.coroutines.Dispatchers
@@ -16,6 +15,8 @@ import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetCacheStatusR
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetCacheStatusResponse
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleUpdateRequest
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleUpdateRequestData
import ru.n08i40k.polytechnic.next.network.tryFuture
import ru.n08i40k.polytechnic.next.network.tryGet
import java.util.logging.Logger
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
@@ -47,61 +48,58 @@ open class CachedRequest(
)
NetworkConnection.getInstance(context).addToRequestQueue(request)
try {
val encodedMainPage =
Base64.Default.encode(mainPageFuture.get().encodeToByteArray())
MyResult.Success(encodedMainPage)
} catch (exception: Exception) {
MyResult.Failure(exception)
when (val response = tryGet(mainPageFuture)) {
is MyResult.Failure -> response
is MyResult.Success -> {
val encodedMainPage = Base64.Default.encode(response.data.encodeToByteArray())
MyResult.Success(encodedMainPage)
}
}
}
}
private suspend fun updateMainPage(): MyResult<ScheduleGetCacheStatusResponse> {
return withContext(Dispatchers.IO) {
val mainPage = getMainPage()
if (mainPage is MyResult.Failure)
return@withContext mainPage
val updateFuture = RequestFuture.newFuture<ScheduleGetCacheStatusResponse>()
ScheduleUpdateRequest(
ScheduleUpdateRequestData((mainPage as MyResult.Success<String>).data),
context,
updateFuture,
updateFuture
).send()
try {
MyResult.Success(updateFuture.get())
} catch (exception: Exception) {
MyResult.Failure(exception)
when (val mainPage = getMainPage()) {
is MyResult.Failure -> mainPage
is MyResult.Success -> {
tryFuture {
ScheduleUpdateRequest(
ScheduleUpdateRequestData(mainPage.data),
context,
it,
it
)
}
}
}
}
}
override fun send() {
val logger = Logger.getLogger("CachedRequest")
val repository = appContainer.networkCacheRepository
val future = RequestFuture.newFuture<ScheduleGetCacheStatusResponse>()
logger.info("Getting cache status...")
ScheduleGetCacheStatusRequest(context, future, future).send()
try {
val response = future.get()
val cacheStatusResult = tryFuture {
ScheduleGetCacheStatusRequest(context, it, it)
}
if (cacheStatusResult is MyResult.Success) {
val cacheStatus = cacheStatusResult.data
logger.info("Cache status received successfully!")
if (!response.cacheUpdateRequired) {
logger.info("Cache update was not required!")
runBlocking {
repository.setUpdateDates(response.lastCacheUpdate, response.lastScheduleUpdate)
repository.setHash(response.cacheHash)
}
} else {
runBlocking {
repository.setUpdateDates(
cacheStatus.lastCacheUpdate,
cacheStatus.lastScheduleUpdate
)
repository.setHash(cacheStatus.cacheHash)
}
if (cacheStatus.cacheUpdateRequired) {
logger.info("Cache update was required!")
val updateResult = runBlocking { updateMainPage() }
@@ -119,16 +117,11 @@ open class CachedRequest(
is MyResult.Failure -> {
logger.warning("Failed to update cache!")
super.getErrorListener()
?.onErrorResponse(updateResult.exception.cause as VolleyError)
return
}
}
}
} catch (exception: Exception) {
} else {
logger.warning("Failed to get cache status!")
super.getErrorListener()?.onErrorResponse(exception.cause as VolleyError)
return
}
val cachedResponse = runBlocking { repository.get(url) }

View File

@@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material3.Card
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -27,6 +28,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -46,10 +48,13 @@ fun ExpandableCard(
val transition = rememberTransition(transitionState)
Card(modifier = modifier.clickable {
onExpandedChange()
transitionState.targetState = expanded
}) {
Card(
modifier = modifier.clickable {
onExpandedChange()
transitionState.targetState = expanded
},
shape = RectangleShape
) {
Column {
ExpandableCardHeader(title, transition)
ExpandableCardContent(visible = expanded, content = content)
@@ -83,6 +88,7 @@ private fun ExpandableCardContent(
enter = enterTransition,
exit = exitTransition
) {
HorizontalDivider()
content()
}
}

View File

@@ -149,7 +149,7 @@ internal fun ProfileCard(profile: Profile = FakeProfileRepository.exampleProfile
.build()
}
}
context.profileViewModel!!.onUnauthorized()
}) {
Text(stringResource(R.string.sign_out))

View File

@@ -1,6 +1,7 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable
@@ -28,7 +29,8 @@ fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) {
HorizontalPager(
state = pagerState,
contentPadding = PaddingValues(horizontal = 20.dp),
verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.Top,
modifier = Modifier.height(600.dp)
) { page ->
DayCard(
modifier = Modifier.graphicsLayer {
@@ -37,8 +39,8 @@ fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) {
lerp(
start = 0.95f, stop = 1f, fraction = 1f - offset.coerceIn(0f, 1f)
).also { scale ->
scaleX = scale
scaleY = scale
scaleX = 1F - scale + 0.95F
scaleY = 1F - scale + 0.95F
}
alpha = lerp(
start = 0.5f, stop = 1f, fraction = 1f - offset.coerceIn(0f, 1f)

View File

@@ -1,12 +1,8 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -16,10 +12,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import ru.n08i40k.polytechnic.next.MainViewModel
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.MockAppContainer
import ru.n08i40k.polytechnic.next.ui.LoadingContent
@@ -35,25 +28,20 @@ fun ScheduleScreen(
val uiState by scheduleViewModel.uiState.collectAsStateWithLifecycle()
LoadingContent(
empty = uiState.isLoading,
empty = when (uiState) {
is ScheduleUiState.NoSchedule -> uiState.isLoading
is ScheduleUiState.HasSchedule -> false
},
loading = uiState.isLoading,
onRefresh = { onRefreshSchedule() },
verticalArrangement = Arrangement.Top
) {
when (uiState) {
is ScheduleUiState.HasSchedule -> {
Box {
val networkCacheRepository =
hiltViewModel<MainViewModel>(LocalContext.current as ComponentActivity)
.appContainer
.networkCacheRepository
UpdateInfo(networkCacheRepository)
Column {
Spacer(modifier = Modifier.height(200.dp))
DayPager((uiState as ScheduleUiState.HasSchedule).group)
}
Column {
val hasSchedule = uiState as ScheduleUiState.HasSchedule
UpdateInfo(hasSchedule.lastUpdateAt, hasSchedule.updateDates)
DayPager(hasSchedule.group)
}
}

View File

@@ -1,36 +1,26 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.MainViewModel
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.cache.NetworkCacheRepository
import ru.n08i40k.polytechnic.next.data.cache.impl.FakeNetworkCacheRepository
import ru.n08i40k.polytechnic.next.UpdateDates
import ru.n08i40k.polytechnic.next.ui.ExpandableCard
import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
@@ -40,24 +30,21 @@ fun Date.toString(format: String, locale: Locale = Locale.getDefault()): String
return formatter.format(this)
}
fun getCurrentDateTime(): Date {
return Calendar.getInstance().time
}
val expanded = mutableStateOf(false)
@Preview(showBackground = true)
@Composable
fun UpdateInfo(networkCacheRepository: NetworkCacheRepository = FakeNetworkCacheRepository()) {
fun UpdateInfo(
lastUpdateAt: Long = 0,
updateDates: UpdateDates = UpdateDates.newBuilder().build()
) {
var expanded by remember { expanded }
val format = "hh:mm:ss dd.MM.yyyy"
val format = "HH:mm:ss dd.MM.yyyy"
val updateDates = remember { runBlocking { networkCacheRepository.getUpdateDates() } }
val currentDate = remember { getCurrentDateTime().toString(format) }
val cacheUpdateDate = remember { Date(updateDates.cache).toString(format) }
val scheduleUpdateDate = remember { Date(updateDates.schedule).toString(format) }
val currentDate = Date(lastUpdateAt).toString(format)
val cacheUpdateDate = Date(updateDates.cache).toString(format)
val scheduleUpdateDate = Date(updateDates.schedule).toString(format)
ExpandableCard(
expanded = expanded,
@@ -69,19 +56,40 @@ fun UpdateInfo(networkCacheRepository: NetworkCacheRepository = FakeNetworkCache
.fillMaxWidth()
.padding(10.dp)
) {
Row(horizontalArrangement = Arrangement.Center) {
Text(text = stringResource(R.string.last_local_update) + " - ")
Text(text = currentDate, fontWeight = FontWeight.Bold)
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.last_local_update))
Text(
text = currentDate,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
}
Row(horizontalArrangement = Arrangement.Center) {
Text(text = stringResource(R.string.last_server_cache_update) + " - ")
Text(text = cacheUpdateDate, fontWeight = FontWeight.Bold)
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.last_server_cache_update))
Text(
text = cacheUpdateDate,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
}
Row(horizontalArrangement = Arrangement.Center) {
Text(text = stringResource(R.string.last_server_schedule_update) + " - ")
Text(text = scheduleUpdateDate, fontWeight = FontWeight.Bold)
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.last_server_schedule_update))
Text(
text = scheduleUpdateDate,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
}
}

View File

@@ -9,9 +9,12 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ru.n08i40k.polytechnic.next.UpdateDates
import ru.n08i40k.polytechnic.next.data.AppContainer
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.model.Group
import java.util.Date
import java.util.logging.Logger
import javax.inject.Inject
sealed interface ScheduleUiState {
@@ -23,18 +26,22 @@ sealed interface ScheduleUiState {
data class HasSchedule(
val group: Group,
val updateDates: UpdateDates,
val lastUpdateAt: Long,
override val isLoading: Boolean
) : ScheduleUiState
}
private data class ScheduleViewModelState(
val group: Group? = null,
val updateDates: UpdateDates? = null,
val lastUpdateAt: Long = 0,
val isLoading: Boolean = false
) {
fun toUiState(): ScheduleUiState = if (group == null) {
ScheduleUiState.NoSchedule(isLoading)
} else {
ScheduleUiState.HasSchedule(group, isLoading)
ScheduleUiState.HasSchedule(group, updateDates!!, lastUpdateAt, isLoading)
}
}
@@ -43,6 +50,7 @@ class ScheduleViewModel @Inject constructor(
appContainer: AppContainer
) : ViewModel() {
private val scheduleRepository = appContainer.scheduleRepository
private val networkCacheRepository = appContainer.networkCacheRepository
private val viewModelState = MutableStateFlow(ScheduleViewModelState(isLoading = true))
val uiState = viewModelState
@@ -61,8 +69,23 @@ class ScheduleViewModel @Inject constructor(
viewModelState.update {
when (result) {
is MyResult.Success -> it.copy(group = result.data, isLoading = false)
is MyResult.Failure -> it.copy(group = null, isLoading = false)
is MyResult.Success -> {
val updateDates = networkCacheRepository.getUpdateDates()
Logger.getLogger("ScheduleViewModel").info("Updating...")
it.copy(
group = result.data,
updateDates = updateDates,
lastUpdateAt = Date().time,
isLoading = false
)
}
is MyResult.Failure -> it.copy(
group = null,
isLoading = false
)
}
}
}