From fdce2cf477cc98d4035431e1419e046d1db6d2d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sat, 19 Nov 2022 22:50:51 +0100 Subject: [PATCH] Improve error handling in horizontal tile on dashboard (#2053) --- .../wulkanowy/data/mappers/MessageMapper.kt | 10 +- .../data/repositories/MessageRepository.kt | 14 +- .../ui/modules/dashboard/DashboardItem.kt | 19 ++- .../modules/dashboard/DashboardPresenter.kt | 82 ++++++----- .../dashboard/adapters/DashboardAdapter.kt | 136 ++++++++++-------- .../wulkanowy/utils/ContextExtension.kt | 7 + app/src/main/res/drawable/ic_error_filled.xml | 9 ++ .../item_dashboard_horizontal_group.xml | 53 ++++++- 8 files changed, 228 insertions(+), 102 deletions(-) create mode 100644 app/src/main/res/drawable/ic_error_filled.xml diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt index 87111dd4d..120eb183a 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt @@ -6,12 +6,18 @@ import io.github.wulkanowy.sdk.pojo.Message as SdkMessage import io.github.wulkanowy.sdk.pojo.MessageAttachment as SdkMessageAttachment import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient -fun List.mapToEntities(student: Student, mailbox: Mailbox?, allMailboxes: List): List = map { +fun List.mapToEntities( + student: Student, + mailbox: Mailbox?, + allMailboxes: List +): List = map { Message( messageGlobalKey = it.globalKey, mailboxKey = mailbox?.globalKey ?: allMailboxes.find { box -> box.fullName == it.mailbox - }?.globalKey!!, + }?.globalKey.let { mailboxKey -> + requireNotNull(mailboxKey) { "Can't find ${it.mailbox} in $allMailboxes" } + }, email = student.email, messageId = it.id, correspondents = it.correspondents, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt index f8be4296d..f95b8dbec 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt @@ -3,7 +3,7 @@ package io.github.wulkanowy.data.repositories import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R -import io.github.wulkanowy.data.Resource +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.dao.MailboxDao import io.github.wulkanowy.data.db.dao.MessageAttachmentDao @@ -14,9 +14,7 @@ import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED import io.github.wulkanowy.data.enums.MessageFolder.TRASHED import io.github.wulkanowy.data.mappers.mapFromEntities import io.github.wulkanowy.data.mappers.mapToEntities -import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.data.pojos.MessageDraft -import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Folder @@ -194,7 +192,9 @@ class MessageRepository @Inject constructor( it.isEmpty() || isExpired || forceRefresh }, query = { mailboxDao.loadAll(student.email, student.symbol, student.schoolSymbol) }, - fetch = { sdk.init(student).getMailboxes().mapToEntities(student) }, + fetch = { + sdk.init(student).getMailboxes().mapToEntities(student) + }, saveFetchResult = { old, new -> mailboxDao.deleteAll(old uniqueSubtract new) mailboxDao.insertAll(new uniqueSubtract old) @@ -207,7 +207,11 @@ class MessageRepository @Inject constructor( val mailbox = getMailboxByStudentUseCase(student) return if (mailbox == null) { - getMailboxes(student, forceRefresh = true).toFirstResult() + getMailboxes(student, forceRefresh = true) + .onResourceError { throw it } + .onResourceSuccess { Timber.i("Found ${it.size} new mailboxes") } + .waitForResult() + getMailboxByStudentUseCase(student) } else mailbox } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt index e220ae236..d019dea68 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt @@ -33,18 +33,27 @@ sealed class DashboardItem(val type: Type) { } data class HorizontalGroup( - val unreadMessagesCount: Int? = null, - val attendancePercentage: Double? = null, - val luckyNumber: Int? = null, + val unreadMessagesCount: Cell? = null, + val attendancePercentage: Cell? = null, + val luckyNumber: Cell? = null, override val error: Throwable? = null, override val isLoading: Boolean = false ) : DashboardItem(Type.HORIZONTAL_GROUP) { + data class Cell( + val data: T?, + val error: Boolean, + val isLoading: Boolean, + ) { + val isHidden: Boolean + get() = data == null && !error && !isLoading + } + override val isDataLoaded - get() = unreadMessagesCount != null || attendancePercentage != null || luckyNumber != null + get() = unreadMessagesCount?.isLoading == false || attendancePercentage?.isLoading == false || luckyNumber?.isLoading == false val isFullDataLoaded - get() = luckyNumber != -1 && attendancePercentage != -1.0 && unreadMessagesCount != -1 + get() = luckyNumber?.isLoading != true && attendancePercentage?.isLoading != true && unreadMessagesCount?.isLoading != true } data class Grades( diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index cb92b0043..22b0d267e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -226,50 +226,71 @@ class DashboardPresenter @Inject constructor( private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) { flow { - val semester = semesterRepository.getCurrentSemester(student) - val mailbox = messageRepository.getMailboxByStudent(student) val selectedTiles = preferencesRepository.selectedDashboardTiles - val flowSuccess = flowOf(Resource.Success(null)) + val luckyNumberFlow = luckyNumberRepository.getLuckyNumber(student, forceRefresh) .mapResourceData { it ?: LuckyNumber(0, LocalDate.now(), 0) } + .onResourceError { errorHandler.dispatch(it) } .takeIf { DashboardItem.Tile.LUCKY_NUMBER in selectedTiles } ?: flowSuccess - val messageFLow = messageRepository.getMessages( - student = student, - mailbox = mailbox, - folder = MessageFolder.RECEIVED, - forceRefresh = forceRefresh - ).takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowSuccess + val messageFLow = flatResourceFlow { + val mailbox = messageRepository.getMailboxByStudent(student) - val attendanceFlow = attendanceSummaryRepository.getAttendanceSummary( - student = student, - semester = semester, - subjectId = -1, - forceRefresh = forceRefresh - ).takeIf { DashboardItem.Tile.ATTENDANCE in selectedTiles } ?: flowSuccess + messageRepository.getMessages( + student = student, + mailbox = mailbox, + folder = MessageFolder.RECEIVED, + forceRefresh = forceRefresh + ) + } + .onResourceError { errorHandler.dispatch(it) } + .takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowSuccess + + val attendanceFlow = flatResourceFlow { + val semester = semesterRepository.getCurrentSemester(student) + attendanceSummaryRepository.getAttendanceSummary( + student = student, + semester = semester, + subjectId = -1, + forceRefresh = forceRefresh + ) + } + .onResourceError { errorHandler.dispatch(it) } + .takeIf { DashboardItem.Tile.ATTENDANCE in selectedTiles } ?: flowSuccess emitAll( combine( - luckyNumberFlow, - messageFLow, - attendanceFlow + flow = luckyNumberFlow, + flow2 = messageFLow, + flow3 = attendanceFlow, ) { luckyNumberResource, messageResource, attendanceResource -> val resList = listOf(luckyNumberResource, messageResource, attendanceResource) - resList.firstNotNullOfOrNull { it.errorOrNull }?.let { throw it } - val isLoading = resList.any { it is Resource.Loading } - - val luckyNumber = luckyNumberResource.dataOrNull?.luckyNumber - val messageCount = messageResource.dataOrNull?.count { it.unread } - val attendancePercentage = attendanceResource.dataOrNull?.calculatePercentage() DashboardItem.HorizontalGroup( - isLoading = isLoading, - attendancePercentage = if (attendancePercentage == 0.0 && isLoading) -1.0 else attendancePercentage, - unreadMessagesCount = if (messageCount == 0 && isLoading) -1 else messageCount, - luckyNumber = if (luckyNumber == 0 && isLoading) -1 else luckyNumber + isLoading = resList.any { it is Resource.Loading }, + error = resList.map { it.errorOrNull }.let { errors -> + if (errors.all { it != null }) { + errors.firstOrNull() + } else null + }, + attendancePercentage = DashboardItem.HorizontalGroup.Cell( + data = attendanceResource.dataOrNull?.calculatePercentage(), + error = attendanceResource.errorOrNull != null, + isLoading = attendanceResource is Resource.Loading, + ), + unreadMessagesCount = DashboardItem.HorizontalGroup.Cell( + data = messageResource.dataOrNull?.count { it.unread }, + error = messageResource.errorOrNull != null, + isLoading = messageResource is Resource.Loading, + ), + luckyNumber = DashboardItem.HorizontalGroup.Cell( + data = luckyNumberResource.dataOrNull?.luckyNumber, + error = luckyNumberResource.errorOrNull != null, + isLoading = luckyNumberResource is Resource.Loading, + ) ) }) } @@ -280,11 +301,8 @@ class DashboardPresenter @Inject constructor( if (it.isLoading) { Timber.i("Loading horizontal group data started") - - if (it.isFullDataLoaded) { - firstLoadedItemList += DashboardItem.Type.HORIZONTAL_GROUP - } } else { + firstLoadedItemList += DashboardItem.Type.HORIZONTAL_GROUP Timber.i("Loading horizontal group result: Success") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt index a3c423a8b..2c06e45fd 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt @@ -171,81 +171,105 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { + updateMarginsRelative( + end = if (isAttendanceHidden && isMessagesHidden && !isLuckyNumberHidden) { + 0 + } else context.dpToPx(8f).toInt() + ) + } + } + } + + private fun ItemDashboardHorizontalGroupBinding.bindMessages( + item: DashboardItem.HorizontalGroup, + isWideErrorShow: Boolean + ) { + dashboardHorizontalGroupItemMessageError.isVisible = item.unreadMessagesCount?.error == true + with(dashboardHorizontalGroupItemMessageValue) { + isVisible = item.unreadMessagesCount?.error != true + text = item.unreadMessagesCount?.data.toString() + } + with(dashboardHorizontalGroupItemMessageContainer) { + isVisible = item.unreadMessagesCount?.isHidden == false && !isWideErrorShow + setOnClickListener { onMessageTileClickListener() } + } + } + + private fun ItemDashboardHorizontalGroupBinding.bindAttendance( + item: DashboardItem.HorizontalGroup, + isWideErrorShow: Boolean + ) { + val attendancePercentage = item.attendancePercentage?.data val attendanceColor = when { attendancePercentage == null || attendancePercentage == .0 -> { - context.getThemeAttrColor(R.attr.colorOnSurface) + root.context.getThemeAttrColor(R.attr.colorOnSurface) } attendancePercentage <= ATTENDANCE_SECOND_WARNING_THRESHOLD -> { - context.getThemeAttrColor(R.attr.colorPrimary) + root.context.getThemeAttrColor(R.attr.colorPrimary) } attendancePercentage <= ATTENDANCE_FIRST_WARNING_THRESHOLD -> { - context.getThemeAttrColor(R.attr.colorTimetableChange) + root.context.getThemeAttrColor(R.attr.colorTimetableChange) } - else -> context.getThemeAttrColor(R.attr.colorOnSurface) + else -> root.context.getThemeAttrColor(R.attr.colorOnSurface) } val attendanceString = if (attendancePercentage == null || attendancePercentage == .0) { - context.getString(R.string.dashboard_horizontal_group_no_data) + root.context.getString(R.string.dashboard_horizontal_group_no_data) } else { "%.2f%%".format(attendancePercentage) } - with(binding.dashboardHorizontalGroupItemAttendanceValue) { + dashboardHorizontalGroupItemAttendanceError.isVisible = + item.attendancePercentage?.error == true + with(dashboardHorizontalGroupItemAttendanceValue) { + isVisible = item.attendancePercentage?.error != true text = attendanceString setTextColor(attendanceColor) } - - with(binding) { - dashboardHorizontalGroupItemMessageValue.text = unreadMessagesCount.toString() - dashboardHorizontalGroupItemLuckyValue.text = if (luckyNumber == 0) { - context.getString(R.string.dashboard_horizontal_group_no_data) - } else luckyNumber?.toString() - - dashboardHorizontalGroupItemInfoContainer.isVisible = error != null || isLoadingVisible - dashboardHorizontalGroupItemInfoProgress.isVisible = isLoadingVisible - dashboardHorizontalGroupItemInfoErrorText.isVisible = error != null - - with(dashboardHorizontalGroupItemLuckyContainer) { - isVisible = luckyNumber != null && luckyNumber != -1 && !isLoadingVisible - setOnClickListener { onLuckyNumberTileClickListener() } - - updateLayoutParams { - updateMarginsRelative( - end = if (attendancePercentage == null && unreadMessagesCount == null && luckyNumber != null) { - 0 - } else { - context.dpToPx(8f).toInt() - } - ) + with(dashboardHorizontalGroupItemAttendanceContainer) { + isVisible = item.attendancePercentage?.isHidden == false && !isWideErrorShow + setOnClickListener { onAttendanceTileClickListener() } + updateLayoutParams { + matchConstraintPercentWidth = when { + item.luckyNumber?.isHidden == true && item.unreadMessagesCount?.isHidden == true -> 1.0f + item.luckyNumber?.isHidden == true || item.unreadMessagesCount?.isHidden == true -> 0.5f + else -> 0.4f } } - - with(dashboardHorizontalGroupItemAttendanceContainer) { - isVisible = - attendancePercentage != null && attendancePercentage != -1.0 && !isLoadingVisible - updateLayoutParams { - matchConstraintPercentWidth = when { - luckyNumber == null && unreadMessagesCount == null -> 1.0f - luckyNumber == null || unreadMessagesCount == null -> 0.5f - else -> 0.4f - } - } - setOnClickListener { onAttendanceTileClickListener() } - } - - with(dashboardHorizontalGroupItemMessageContainer) { - isVisible = - unreadMessagesCount != null && unreadMessagesCount != -1 && !isLoadingVisible - setOnClickListener { onMessageTileClickListener() } - } } } diff --git a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt index dd91d36d4..cc4c5aaa4 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt @@ -2,9 +2,11 @@ package io.github.wulkanowy.utils import android.annotation.SuppressLint import android.content.Context +import android.content.res.ColorStateList import android.graphics.* import android.text.TextPaint import android.util.DisplayMetrics.DENSITY_DEFAULT +import android.widget.ImageView import androidx.annotation.* import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils @@ -12,6 +14,7 @@ import androidx.core.graphics.applyCanvas import androidx.core.graphics.drawable.RoundedBitmapDrawable import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.core.graphics.drawable.toBitmap +import androidx.core.widget.ImageViewCompat @ColorInt @@ -85,3 +88,7 @@ fun Context.createNameInitialsDrawable( return RoundedBitmapDrawableFactory.create(this.resources, bitmap) .apply { isCircular = true } } + +fun ImageView.setTint(@ColorInt color: Int) { + ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(color)) +} diff --git a/app/src/main/res/drawable/ic_error_filled.xml b/app/src/main/res/drawable/ic_error_filled.xml new file mode 100644 index 000000000..61b575dc6 --- /dev/null +++ b/app/src/main/res/drawable/ic_error_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/item_dashboard_horizontal_group.xml b/app/src/main/res/layout/item_dashboard_horizontal_group.xml index 1d43d5115..0c59d1ebf 100644 --- a/app/src/main/res/layout/item_dashboard_horizontal_group.xml +++ b/app/src/main/res/layout/item_dashboard_horizontal_group.xml @@ -37,9 +37,25 @@ app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" + app:layout_goneMarginEnd="16dp" app:tint="?colorOnSurface" tools:ignore="ContentDescription" /> + + + + + tools:text="16" + tools:visibility="visible" /> @@ -145,9 +178,25 @@ app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" + app:layout_goneMarginEnd="16dp" app:tint="?colorOnSurface" tools:ignore="ContentDescription" /> + + - \ No newline at end of file +