Skip to content

Instantly share code, notes, and snippets.

@mrsasha
Created May 27, 2022 09:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mrsasha/2156e540235c685129df8112e9733662 to your computer and use it in GitHub Desktop.
Save mrsasha/2156e540235c685129df8112e9733662 to your computer and use it in GitHub Desktop.
HomeViewModel v2
package lu.gian.uniwhere.features.home.ui
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.google.firebase.iid.FirebaseInstanceId
import com.uniwhere.kmp.elephas.localdatasource.LocalDataSource
import com.uniwhere.kmp.elephas.model.CredentialsManager
import com.uniwhere.kmp.elephas.model.ExamStatsAverageType
import com.uniwhere.kmp.elephas.model.UWErrorCodeClass
import com.uniwhere.kmp.elephas.model.UniAccountWrapper
import com.uniwhere.kmp.elephas.model.UniversitySyncStatus
import com.uniwhere.kmp.elephas.settings.SettingsRepository
import com.uniwhere.kmp.hydra.be.dto.account.UniversityAccountDTO
import com.uniwhere.kmp.hydra.be.dto.uniservice.ServiceDTO
import com.uniwhere.kmp.hydra.be.dto.uniservice.entity.EntityType
import com.uniwhere.kmp.hydra.be.dto.user.UserDTO
import com.uniwhere.kmp.hydra.uwerror.UWErrorCode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import lu.gian.uniwhere.core.base.BaseViewModel
import lu.gian.uniwhere.core.data.AccountUtils
import lu.gian.uniwhere.core.data.CoreRepository
import lu.gian.uniwhere.core.error.UWErrorResourcesMapper
import lu.gian.uniwhere.core.extensions.dto.ectsDone
import lu.gian.uniwhere.core.extensions.dto.ectsTotal
import lu.gian.uniwhere.core.extensions.dto.hasGrade
import lu.gian.uniwhere.core.extensions.formatNoDecimal
import lu.gian.uniwhere.core.extensions.formatTwoDecimal
import lu.gian.uniwhere.core.utils.Event
import lu.gian.uniwhere.core.utils.ResourceProvider
import lu.gian.uniwhere.core.utils.Utils
import lu.gian.uniwhere.features.home.R
import lu.gian.uniwhere.features.home.data.HomeUtils
import lu.gian.uniwhere.features.home.data.model.home.FakeHomeState
import lu.gian.uniwhere.features.home.data.model.home.HomeDestination
import lu.gian.uniwhere.features.home.data.model.home.HomeHeader
import lu.gian.uniwhere.features.home.data.model.home.HomeLeanCard
import lu.gian.uniwhere.features.home.data.model.home.HomeTileItem
import lu.gian.uniwhere.features.home.data.model.home.HomeTileItemType
import lu.gian.uniwhere.features.home.data.model.home.IconProgressTile
import lu.gian.uniwhere.features.home.data.model.home.StartTutorial
import lu.gian.uniwhere.features.home.data.model.home.StatesOfHome
import lu.gian.uniwhere.libraries.analytics.AnalyticsHelper
import lu.gian.uniwhere.libraries.analytics.AnalyticsIdentifyKeys
import lu.gian.uniwhere.libraries.analytics.AnalyticsTrackActions
import lu.gian.uniwhere.libraries.analytics.AnalyticsTrackPropertyKeys
import lu.gian.uniwhere.libraries.analytics.getSegmentFormattedTime
import lu.gian.uniwhere.libraries.uniservice.model.FirebaseConfigParam
import lu.gian.uniwhere.libraries.uniservice.model.FirebaseConfigResult
import lu.gian.uniwhere.libraries.uniservice.net.RemoteConfigRepository
import lu.gian.uniwhere.libraries.uniservice.net.UniServiceRepository
import lu.gian.uniwhere.libraries.uwerror.UWError
import retrofit2.HttpException
import timber.log.Timber
import java.net.ConnectException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
import java.util.Calendar
import javax.inject.Inject
import javax.net.ssl.SSLHandshakeException
class HomeViewModel @Inject constructor(
val analyticsHelper: AnalyticsHelper,
val settingsRepository: SettingsRepository,
private val resourceProvider: ResourceProvider,
private val localDataSource: LocalDataSource,
private val coreRepository: CoreRepository,
private val accountUtils: AccountUtils,
private val uniServiceRepository: UniServiceRepository,
private val remoteConfigRepository: RemoteConfigRepository,
private val settingRepository: SettingsRepository,
private val homeTileHelper: HomeTileHelper
) : BaseViewModel() {
private val _showStartTutorial = MutableLiveData<Event<StartTutorial>>()
val showStartTutorial: LiveData<Event<StartTutorial>>
get() = _showStartTutorial
private val _stateOfHome = MutableLiveData<StatesOfHome>()
val statesOfHome: LiveData<StatesOfHome>
get() = _stateOfHome
private var isUnsupportedService: Boolean = false
//TODO why do we do this separately here? why not inside sessionCheckup after loading the user?
fun checkForUnsupportedUniversity(firstTime: Boolean) {
Timber.d("[checkForUnsupportedUniversity]")
_stateOfHome.value = StatesOfHome.ShowLoading
viewModelScope.launch {
isUnsupportedService = isServiceUnsupported()
if (isUnsupportedService && firstTime) {
_stateOfHome.value = StatesOfHome.UnsupportedUniversity
} else {
_stateOfHome.value = StatesOfHome.SupportedUniversity
}
}
}
fun sessionCheckup() {
Timber.d("[CHECKUP] Starting Checkup")
_stateOfHome.value = StatesOfHome.ShowLoading
checkAppVersion()
//TODO why do we do this here, and only if we have the user?
//is it because of the T&C acceptance?
//TODO if we're here and we have no user, what happens?
//shouldn't we do this in the MainViewModel already?
val apiAuth = localDataSource.getApiAuth()
if (apiAuth != null) {
Utils.initFacebookSDK()
}
viewModelScope.launch(Dispatchers.Main) {
//TODO we should refresh token first - won't all calls fail if we don't do it? that way we don't have to manage 401 in error cases here
tokenRefresh()
updateConf()
val newUser = updateUser()
val serviceCode = newUser?.referenceServiceCode ?: localDataSource.getSelectedService()?.serviceCode
if (serviceCode == null) {
//TODO what do we do if the user has no service? go to manual sync screen?
Timber.e("[sessionCheckup] user has no service in account!")
} else {
updateServiceConf(serviceCode)
refreshUniversityAccount(serviceCode)
examDataUpdate(serviceCode)
generateHomeItems(coreRepository.currentUniAccountWrapper)
checkRatingSettings()
}
}
}
private fun checkAppVersion() {
if (wasAppUpdated()) {
val savedVersion = settingsRepository.savedAppVersion.get()
if (savedVersion <= 92000) {
// Identify user!
identifyOnSegment()
}
Timber.d("[checkAppVersion] : New app update detected")
} else {
Timber.d("[checkAppVersion] : App first start, begin checkup")
}
}
private suspend fun tokenRefresh() {
val lastUserUpdateTimestamp = settingsRepository.lastUserUpdate.get()
val shouldRefreshToken = (System.currentTimeMillis() - lastUserUpdateTimestamp) > lu.gian.uniwhere.core.BuildConfig.USER_REFRESH_INTERVAL // 7 days
Timber.d("[tokenRefresh] last updated: $lastUserUpdateTimestamp, should refresh $shouldRefreshToken")
if (shouldRefreshToken) {
try {
val apiAuth = coreRepository.updateAccessToken()
localDataSource.saveApiAuth(apiAuth)
settingsRepository.lastUserUpdate.set(System.currentTimeMillis())
} catch (e: Exception) {
handleAuthGenericError(e)
}
} else {
Timber.d("[tokenRefresh] Token refresh not necessary, skipping")
}
}
private suspend fun updateConf() {
Timber.d("[updateConf] Updating Bootstrap and Conf")
try {
val configuration = coreRepository.getConf()
localDataSource.deleteConfiguration()
localDataSource.saveConfiguration(configuration)
} catch (e: Exception) {
//we are not authorized so wrong or expired token, we should logout
if (e is HttpException && e.code() == 401) {
performLogout()
return
}
//TODO why don't we call handleAuthGenericError?
val uwError = createErrorBanner(e)
_stateOfHome.value = StatesOfHome.Error(e.message)
_banner.value = UWErrorResourcesMapper.raiseError(uwError)
_stateOfHome.value = StatesOfHome.HideLoading
Timber.e(e, "[updateConf] Error: $e")
}
}
private fun updateUser(): UserDTO? {
Timber.d("[updateUser()] updating user")
// Reset cached value
coreRepository.currentUserDTO = null
var newUser: UserDTO? = null
viewModelScope.launch {
try {
val token = try {
FirebaseInstanceId.getInstance().instanceId.result?.token
} catch (e: Exception) {
null
}
Timber.d("[updateUser()] - Firebase Token -> $token")
val apiAuth = localDataSource.getApiAuth()
val userFromMemory = apiAuth?.user
newUser = if (token != null && token != userFromMemory?.fcmToken) {
coreRepository.updateFcmToken(token)
} else {
coreRepository.getUser()
}
newUser?.let {
apiAuth?.copy(user = it)?.let { apiAuth ->
localDataSource.saveApiAuth(apiAuth)
}
}
coreRepository.refreshReviewList()
} catch (e: Exception) {
handleAuthGenericError(e)
Timber.e(e, "[updateUser] Error: $e")
_stateOfHome.value = StatesOfHome.HideLoading
_stateOfHome.value = StatesOfHome.Error(e.message)
}
}
return newUser
}
private suspend fun updateServiceConf(serviceCode: String) {
_stateOfHome.value = StatesOfHome.ShowLoading
try {
val updateService = coreRepository.getService(serviceCode)
localDataSource.saveSelectedService(updateService)
} catch (e: Exception) {
//we are not authorized so wrong or expired token, we should logout
// TODO do we need to manage it here? is it probable that it will return 401 here and not previously?
if (e is HttpException && e.code() == 401) {
performLogout()
return
}
//TODO why don't we call handleAuthGenericError?
val uwError = createErrorBanner(e)
_stateOfHome.value = StatesOfHome.Error(e.message)
_banner.value = UWErrorResourcesMapper.raiseError(uwError)
_stateOfHome.value = StatesOfHome.HideLoading
Timber.e(e, "[updateServiceConf] Error: $e")
}
}
private suspend fun getUniversityAccount(serviceCode: String): UniversityAccountDTO? {
val savedAccount = coreRepository.currentUniAccountWrapper?.universityAccountDTO
return if (savedAccount != null) {
savedAccount
} else {
val accounts = coreRepository.getAccountsList()
val universityAccounts = accounts.filterIsInstance<UniversityAccountDTO>().filter { it.serviceCode == serviceCode }
Timber.d("[getUniversityAccount()] accounts size: ${accounts.size}, universityAccounts size: ${universityAccounts.size}")
if (universityAccounts.size == 1) {
universityAccounts[0]
} else {
Timber.e("Too few or too many university accounts!")
null //TODO we already did multiple account check in MainViewModel so maybe we can ignore it here?
}
}
}
private suspend fun refreshUniversityAccount(serviceCode: String) {
try {
val foundAccount = getUniversityAccount(serviceCode) ?: return
val universityAccount = coreRepository.getUniversityAccount(foundAccount.accountId)
Timber.d("[refreshUniversityAccount()] updating university account id: ${foundAccount.accountId} for service $serviceCode")
if (localDataSource.getCurrentUniAccount() != null) {
localDataSource.updateUniAccountDTO(universityAccount)
localDataSource.setSyncStatus(
UniversitySyncStatus.SYNCED,
universityAccount.accountId
)
} else {
localDataSource.saveUniAccount(
UniAccountWrapper(
universityAccountDTO = universityAccount,
uniCredentials = CredentialsManager.from(universityAccount.identifier, ""),
webmailCredentials = CredentialsManager.from(universityAccount.identifier, ""),
syncStatus = UniversitySyncStatus.SYNCED
), true
)
}
uniServiceRepository.refreshAndSaveEntities(foundAccount.accountId)
settingsRepository.lastAutoRefresh.set(System.currentTimeMillis())
trackUniAccountAction("success", serviceCode, null)
} catch (e: Exception) {
Timber.e(e, "Error refreshing university account")
val error = createErrorBanner(e, "ERROR", "Error refreshing university account")
//TODO why don't we track errors in other cases?
trackUniAccountAction("failure", serviceCode, error)
_stateOfHome.value = StatesOfHome.HideLoading
_banner.value = UWErrorResourcesMapper.raiseError(error)
generateHomeItems(coreRepository.currentUniAccountWrapper)
}
}
private suspend fun examDataUpdate(serviceCode: String) {
val accountId = coreRepository.currentUniAccountWrapper?.universityAccountDTO?.accountId ?: return
Timber.d("[examDataUpdate()] updating transcript and exams for account id: $accountId")
try {
val transcriptResult = coreRepository.getTranscript(accountId)
localDataSource.saveTranscript(transcriptResult)
settingsRepository.studyPlanRefreshRequired.set(true)
val examStats = coreRepository.getExamStats(accountId)
localDataSource.deleteExamStats()
localDataSource.saveExamStats(examStats)
} catch (e: Exception) {
Timber.e(e, "Error fetching exams and transcript. Error: $e")
val error = createErrorBanner(e, "ERROR", "Error fetching exams and transcript")
//TODO why don't we track errors in other cases?
trackUniAccountAction("failure", serviceCode, error)
_stateOfHome.value = StatesOfHome.HideLoading
_banner.value = UWErrorResourcesMapper.raiseError(error)
}
}
private fun trackUniAccountAction(status: String, serviceCode: String, error: UWError?) {
localDataSource.getUniServiceTrackingData()?.let { trackData ->
trackData.syncType?.let { syncType ->
val errorCode = when {
error?.getErrorClass() == UWErrorCodeClass.BACKEND -> "backend_failure"
error?.getErrorClass() == UWErrorCodeClass.APP -> "network"
else -> "none"
}
analyticsHelper.trackEvent(
AnalyticsTrackActions.universityDataReceived, mapOf(
AnalyticsTrackPropertyKeys.status to status,
AnalyticsTrackPropertyKeys.universityCode to serviceCode,
AnalyticsTrackPropertyKeys.syncType to syncType,
AnalyticsTrackPropertyKeys.error to errorCode
)
)
}
}
}
private fun isTimeForRating(ratingSettings: FirebaseConfigResult.RatingSettings) {
val uniAccount = coreRepository.currentUniAccountWrapper
Timber.d("Rating Settings - uniAccount : $uniAccount")
Timber.d("Rating Settings - uniAccountisNotNull : ${uniAccount != null}")
val ratingNotYetDone = settingRepository.ratingPopupShowed.get().not()
Timber.d("Rating Settings - alreadyDidIt : $ratingNotYetDone")
val isTimePassed = isTimePassedForRating(ratingSettings)
val isNumberOfAccess = isNumberOfAccessForRating(ratingSettings)
Timber.d("Rating Settings - ratingSettings.settings.androidCanShow : ${ratingSettings.settings.androidCanShow}")
val isWontReviewCountReached = !isWontReviewForRatingReached(ratingSettings)
Timber.d("Rating Settings - isWontReviewCountReached : $isWontReviewCountReached")
val isUniversityAvailableForRating: Boolean = uniAccount?.let { isUniversityAllowedForRating(ratingSettings, it) } ?: false
Timber.d("Rating Settings - isUniversityAvailableForRating : $isUniversityAvailableForRating")
if (uniAccount != null
&& isTimePassed
&& isNumberOfAccess
&& isWontReviewCountReached
&& ratingSettings.settings.androidCanShow
&& isUniversityAvailableForRating
&& ratingNotYetDone
) {
// Did it.
_stateOfHome.value = StatesOfHome.ShowRatingPopup
}
}
private fun isWontReviewForRatingReached(ratingSettings: FirebaseConfigResult.RatingSettings): Boolean {
return settingsRepository.dontWantReview.get() <= ratingSettings.settings.wontReviewMaxCount
}
private fun isNumberOfAccessForRating(ratingSettings: FirebaseConfigResult.RatingSettings): Boolean {
var accessesSoFar = settingRepository.accessCount.get()
if (accessesSoFar <= ratingSettings.settings.numberOfAccessBeforeRating) {
accessesSoFar++
settingRepository.accessCount.set(accessesSoFar)
}
val numberOfAccess = accessesSoFar > ratingSettings.settings.numberOfAccessBeforeRating
Timber.d("Rating Settings - numberOfAccess : $numberOfAccess")
return numberOfAccess
}
private fun isTimePassedForRating(ratingSettings: FirebaseConfigResult.RatingSettings): Boolean {
var firstAccess = settingRepository.firstAccessTimestamp.get()
if (firstAccess == 0L) {
firstAccess = System.currentTimeMillis()
settingRepository.firstAccessTimestamp.set(firstAccess)
}
val timePassedFromLastAccess = firstAccess + ratingSettings.settings.millisDaysBeforeRating < System.currentTimeMillis()
Timber.d("Rating Settings - timePassedFromLastAccess : $timePassedFromLastAccess")
return timePassedFromLastAccess
}
private fun isUniversityAllowedForRating(
ratingSettings: FirebaseConfigResult.RatingSettings,
uniAccount: UniAccountWrapper
) = ratingSettings.settings.universityAllowed.any { university -> university.serviceCode == uniAccount.universityAccountDTO.serviceCode }
private fun generateStartTutorial() {
Timber.v("[generateStartTutorial]")
val selectedService = localDataSource.getSelectedService()
val serviceEnabled = selectedService?.enabled ?: false
val content = if (serviceEnabled) {
resourceProvider.getString(R.string.HOME_start_tutorial_content)
} else {
resourceProvider.getString(R.string.HOME_tutorial_uni_not_supported)
}
val connectBtn = if (serviceEnabled) {
resourceProvider.getString(R.string.HOME_start_tutorial_connect_title)
} else {
resourceProvider.getString(R.string.HOME_uni_not_supported_btn)
}
val startTutorial = StartTutorial(
title = resourceProvider.getString(R.string.HOME_start_tutorial_welcome),
content = content,
connectButtonText = connectBtn,
exploreButtonText = resourceProvider.getString(R.string.HOME_tutorial_explore_btn),
serviceEnabled = serviceEnabled,
color = resourceProvider.getColor(R.color.accent),
iconResId = R.drawable.ic_icon_user_check,
comingSoonUrl = selectedService?.comingSoonUrl
)
_showStartTutorial.value = Event(startTutorial)
}
private fun generateHomeItems(accountWrapper: UniAccountWrapper?) {
Timber.d("[generateHomeItems] current uni account id: ${accountWrapper?.universityAccountDTO?.accountId}")
_stateOfHome.value = StatesOfHome.ShowLoading
if (accountWrapper == null) {
val fakeState = FakeHomeState.getFakeHomeState(
settingsRepository,
accountUtils,
resourceProvider
)
generateStartTutorial()
Timber.v("[generateHomeItems] Setting default Homepage State")
_stateOfHome.value = fakeState
} else {
val items = mutableListOf<HomeTileItem>()
items.addAll(generateUniwhereDevelhopeTile())
if (isServiceEnabled()) {
Timber.v("[generateHomeItems] - isServiceEnabled = ${isServiceEnabled()}")
generateNotSyncedTile()?.let { items.add(it) }
items.addAll(generateSyncIssueTile(accountWrapper.universityAccountDTO.accountId, accountWrapper.syncStatus))
} else {
val service = localDataSource.getSelectedService()
if (service != null) {
Timber.v("[generateHomeItems] - localDataSource.getSelectedService() = not null")
items.addAll(generateServiceInterruptedTile(service))
} else {
Timber.v("[generateHomeItems] - localDataSource.getSelectedService() = null")
}
}
if (isUnsupportedService) {
items.add(generateUnsupportedUniTile())
}
items.addAll(generatePerformanceTile())
items.addAll(generateProgressionTile())
items.addAll(homeTileHelper.generateQuickActions())
items.addAll(generateAdministrationTile())
items.add(HomeUtils.getLastUpdateTile(settingsRepository, resourceProvider))
val state = StatesOfHome.HomeState(
items = items,
homeHeader = generateHomeHeader()
)
_stateOfHome.value = state
}
}
private fun generateHomeHeader(): HomeHeader {
Timber.d("[CHECKUP] Generating Home Header")
val todayPhrases = HomeUtils.getTodayPhrases(resourceProvider, accountUtils)
return HomeHeader(
title = todayPhrases.first,
subtitle = todayPhrases.second
)
}
private fun generateNotSyncedTile(): HomeTileItem? {
val taxCount = localDataSource.getTaxes().size
val examsDoneCount = coreRepository.getDoneTranscripts().size
val examTodoCount = coreRepository.getToDoTranscripts().size
Timber.d("[generateNotSyncedTile] taxCount: $taxCount, examsDoneCount: $examsDoneCount, examTodoCount: $examTodoCount")
val sumCheck = taxCount + examsDoneCount + examTodoCount
return if (sumCheck == 0) {
Timber.v("[generateNotSyncedTile] NotSynced tile added")
HomeTileItem(
data = HomeLeanCard(
title = resourceProvider.getString(R.string.HOME_not_synced_uni_title),
content = resourceProvider.getString(R.string.HOME_not_synced_uni_description),
destination = HomeDestination.UNIWHERE_DEVELOP,
uniAccountId = null,
strokeColor = resourceProvider.getColor(R.color.yellow3)
),
itemType = HomeTileItemType.SYNC_TILE
)
} else {
null
}
}
private fun generateUnsupportedUniTile(): HomeTileItem {
Timber.d("[generateNotSyncedTile] NotSupportedUniversity tile added")
return HomeTileItem(
data = HomeLeanCard(
title = resourceProvider.getString(R.string.HOME_not_supported_uni_title),
content = resourceProvider.getString(R.string.HOME_not_supported_uni_description),
destination = HomeDestination.UNIWHERE_DEVELOP,
uniAccountId = null,
strokeColor = resourceProvider.getColor(R.color.yellow3),
iconId = R.drawable.ic_wip
),
itemType = HomeTileItemType.SYNC_TILE
)
}
private fun generateUniwhereDevelhopeTile(): List<HomeTileItem> {
val items = mutableListOf<HomeTileItem>()
Timber.d("[generateUniwhereDevelhopeTile] Generating Develhope Tile")
val isTileAlreadyShown = settingsRepository.uniwhereDevelhopeMessageShown.get()
val uniwhereAccount = localDataSource.getUniwhereAccount()
val creationDate = uniwhereAccount?.creationDate ?: -1L
val thresholdDateMillis = Calendar.getInstance().apply {
this[Calendar.YEAR] = 2021
this[Calendar.MONTH] = Calendar.OCTOBER
this[Calendar.DAY_OF_MONTH] = 1
}.timeInMillis
if (!isTileAlreadyShown && creationDate < thresholdDateMillis) {
items.add(
HomeTileItem(
data = HomeLeanCard(
title = resourceProvider.getString(R.string.HOME_uniwhere_develhope_title),
content = resourceProvider.getString(R.string.HOME_uniwhere_develhope_subtitle),
destination = HomeDestination.UNIWHERE_DEVELOP,
uniAccountId = null,
serviceName = null
),
itemType = HomeTileItemType.SYNC_TILE
)
)
}
return items
}
private fun generateSyncIssueTile(accountId: Long, status: UniversitySyncStatus): List<HomeTileItem> {
val items = mutableListOf<HomeTileItem>()
Timber.d("[generateSyncIssueTile] Generating Sync Issue Tile")
if (status != UniversitySyncStatus.SYNCED) {
items.add(
HomeTileItem(
data = HomeLeanCard(
title = resourceProvider.getString(R.string.HOME_sync_error_title),
content = resourceProvider.getString(R.string.HOME_sync_error_content),
destination = HomeDestination.ERROR_RESOLUTION,
uniAccountId = accountId
),
itemType = HomeTileItemType.SYNC_TILE
)
)
}
return items
}
private fun generateServiceInterruptedTile(service: ServiceDTO): List<HomeTileItem> {
val items = mutableListOf<HomeTileItem>()
Timber.d("[generateServiceInterruptedTile] Generating Service Interrupted tile")
items.add(
HomeTileItem(
data = HomeLeanCard(
title = resourceProvider.getString(
R.string.HOME_disabled_uni_title,
service.serviceCode
),
content = resourceProvider.getString(
R.string.HOME_disabled_uni_small_description,
service.serviceCode
),
destination = HomeDestination.SERVICE_INTERRUPTED,
uniAccountId = null,
serviceName = service.serviceCode
),
itemType = HomeTileItemType.SYNC_TILE
)
)
return items
}
private fun generatePerformanceTile(): List<HomeTileItem> {
val items = mutableListOf<HomeTileItem>()
Timber.d("[generatePerformanceTile] Generating Performance tile")
val mean = getMean()
var content = resourceProvider.getString(R.string.HOME_tile_performance_content_no_data)
val stringsToBold = mutableListOf<String>()
// exam ratio
val examsDone = coreRepository.getDoneTranscripts()
val exams = coreRepository.getSavedTranscriptList()
val examRatio = if (exams.isEmpty()) {
0.0
} else {
(examsDone.size.toDouble() / exams.size.toDouble()) * 100.0
}
if (mean != 0.0) {
val gpaString = mean.formatTwoDecimal()
val examStats = localDataSource.getExamStats()
val topString = (examStats?.gpaPeersPerformance ?: 0).toString()
stringsToBold.add(gpaString)
stringsToBold.add(topString)
content = resourceProvider.getString(
R.string.HOME_tile_performance_content,
gpaString,
"$topString %"
)
} else {
stringsToBold.add(resourceProvider.getString(R.string.HOME_tile_performance_content_no_data_to_bolditize))
}
val performanceTile = IconProgressTile(
title = resourceProvider.getString(R.string.HOME_tile_performance_title),
subtitle = content,
stringsToBold = stringsToBold,
color = resourceProvider.getColor(R.color.accent),
iconResId = R.drawable.ic_icon_cpu,
progress = examRatio.toInt(),
progressString = "${examsDone.size}/${exams.size}",
destination = HomeDestination.GPA
)
items.add(
HomeTileItem(
data = performanceTile,
itemType = HomeTileItemType.ICON_PROGRESS_TILE
)
)
return items
}
private fun generateProgressionTile(): List<HomeTileItem> {
val items = mutableListOf<HomeTileItem>()
Timber.d("[generateProgressionTile] Generating Progression tile")
val examStats = localDataSource.getExamStats()
val ectsTotal = examStats?.ectsTotal() ?: 0.0
val ectsDone = examStats?.ectsDone() ?: 0.0
val progress = if (ectsTotal == 0.0) {
0.0
} else {
(ectsDone / ectsTotal) * 100.0
}
val ectsString = "$ectsDone ${resourceProvider.getString(R.string.EXAMS_ects_label)}"
val progressionContent = resourceProvider.getString(
R.string.HOME_tile_progression_content,
ectsString, ectsTotal.toString()
)
val progressTile = IconProgressTile(
title = resourceProvider.getString(R.string.HOME_tile_progression_title),
subtitle = progressionContent,
stringsToBold = listOf(ectsString),
color = resourceProvider.getColor(R.color.accent),
iconResId = R.drawable.ic_icon_archive,
progress = progress.toInt(),
progressString = "${progress.formatNoDecimal()}%",
destination = HomeDestination.TRANSCRIPT
)
items.add(
HomeTileItem(
data = progressTile,
itemType = HomeTileItemType.ICON_PROGRESS_TILE
)
)
return items
}
private fun generateAdministrationTile(): List<HomeTileItem> {
val items = mutableListOf<HomeTileItem>()
Timber.d("[generateAdministrationTile]")
val service = localDataSource.getSelectedService()
if (service != null) {
val entityTypes = service.entityTypes
if (entityTypes.contains(EntityType.TAX)) {
items.add(homeTileHelper.createTaxTile(localDataSource.getTaxes()))
}
if (entityTypes.contains(EntityType.APPLIEDTEST) ||
entityTypes.contains(EntityType.AVAILABLETEST) ||
entityTypes.contains(EntityType.AVAILABLEPARTIALTEST)
) {
val appliedList = localDataSource.getAppliedTests()
val availableList = localDataSource.getAvailableTests()
if (appliedList.isNotEmpty()) {
items.add(homeTileHelper.createAppliedTestsTile(appliedList))
} else if (availableList.isNotEmpty()) {
items.add(homeTileHelper.createAvailableTestsTile(availableList))
} else {
items.add(homeTileHelper.createNoTestsTile())
}
}
if (service.wmEnabled) {
items.add(homeTileHelper.createWebmailTile())
}
if (items.isNotEmpty()) {
items.add(0, homeTileHelper.createAdministrationHeader())
}
}
return items
}
private fun getMean(): Double {
val averageType = localDataSource.getExamStatsAverageType()
val stats = localDataSource.getExamStats()
return if (averageType == ExamStatsAverageType.WEIGHTED) {
stats?.gpa ?: 0.0
} else {
stats?.arithmeticMean ?: 0.0
}
}
private fun identifyOnSegment() {
val user = coreRepository.currentUserDTO
val service = localDataSource.getSelectedService()
if (user != null) {
// Signup
// *** Segment identity
// Email
analyticsHelper.identifyEmail(accountUtils.getEmail())
// Full Name
analyticsHelper.identify(AnalyticsIdentifyKeys.fullName, accountUtils.getFullName())
// login
analyticsHelper.identify(
AnalyticsIdentifyKeys.lastAccess,
System.currentTimeMillis().getSegmentFormattedTime()
)
// service choice
if (service != null) {
analyticsHelper.identify(
AnalyticsIdentifyKeys.universitySelected,
service.serviceCode
)
analyticsHelper.identify(AnalyticsIdentifyKeys.country, service.country)
analyticsHelper.identify(AnalyticsIdentifyKeys.isFirstUser, false.toString())
analyticsHelper.identify(
AnalyticsIdentifyKeys.isFeesAvailable,
service.entityTypes.contains(EntityType.TAX).toString()
)
val testsAvailable = (service.entityTypes.contains(EntityType.APPLIEDTEST) ||
service.entityTypes.contains(EntityType.AVAILABLETEST) ||
service.entityTypes.contains(EntityType.AVAILABLEPARTIALTEST))
analyticsHelper.identify(
AnalyticsIdentifyKeys.isTestScheduleAvailable,
testsAvailable.toString()
)
analyticsHelper.identify(
AnalyticsIdentifyKeys.isWebmailAvailable,
service.wmEnabled.toString()
)
localDataSource.getTranscript()?.let { transcriptResult ->
// class_passed_count
val examsDone = transcriptResult.exams.filter { it.hasGrade() }.size
analyticsHelper.identify(
AnalyticsIdentifyKeys.classPassedCount,
examsDone.toString()
)
// class_todo_count
val examsTodo = transcriptResult.exams.filter { !it.hasGrade() }.size
analyticsHelper.identify(
AnalyticsIdentifyKeys.classTodoCount,
examsTodo.toString()
)
}
localDataSource.getExamStats()?.let { examStats ->
examStats.gpaNormalized?.let { gpa ->
analyticsHelper.identify(AnalyticsIdentifyKeys.gpa, gpa.toString())
}
// career_progression
val ectsTotal = examStats.ectsTotal()
val ectsDone = examStats.ectsDone()
val progress = if (ectsTotal == 0.0) {
0.0
} else {
(ectsDone / ectsTotal) * 100.0
}
analyticsHelper.identify(
AnalyticsIdentifyKeys.careerProgression,
progress.toString()
)
// performance_percentile
analyticsHelper.identify(
AnalyticsIdentifyKeys.performancePercentile,
examStats.gpaPeersPerformance.toString()
)
}
}
viewModelScope.launch {
try {
// Just to update the list on the disk
coreRepository.refreshReviewList()
//TODO this only gets them from the network for the analytics - do we need it in this screen then?
coreRepository.getPomodoroUserStats()
} catch (e: Exception) {
// Fine to do nothing
}
}
}
}
fun isServiceEnabled(): Boolean {
return localDataSource.getSelectedService()?.enabled ?: false
}
private suspend fun isServiceUnsupported(): Boolean {
val universityToShow = remoteConfigRepository.getConfig(FirebaseConfigParam.GetUniversityListToShow)
return if (universityToShow is FirebaseConfigResult.UniversityToShow) {
universityToShow.result?.let { uniToShow ->
val selectedService = localDataSource.getSelectedService()?.serviceCode
return uniToShow.servicesToShow.map { it.serviceCode }.contains(selectedService).not()
} ?: false
} else {
false
}
}
private suspend fun checkRatingSettings() {
when (val ratingSettings = remoteConfigRepository.getConfig(FirebaseConfigParam.RatingSettings)) {
is FirebaseConfigResult.RatingSettings -> {
Timber.d("Rating Settings - Remote Config : $ratingSettings")
isTimeForRating(ratingSettings)
}
else -> {
Timber.e("HomeViewModel.checkRatingSettings() - remoteConfigResult not expected: $ratingSettings")
}
}
}
fun getComingSoonUrl(): String? {
return localDataSource.getSelectedService()?.comingSoonUrl
}
private fun wasAppUpdated(): Boolean {
return settingsRepository.savedAppVersion.get() != lu.gian.uniwhere.core.BuildConfig.VERSION_CODE
}
fun setRatingDone() {
settingsRepository.ratingPopupShowed.set(true)
}
fun setWontReview() {
val actualWontReviewCount = settingsRepository.dontWantReview.get() + 1
settingsRepository.dontWantReview.set(actualWontReviewCount)
}
private fun createErrorBanner(error: Exception, contextKey: String? = null, contextValue: String? = null): UWError {
Timber.v("[createErrorBanner()] error: $error")
val errorCode = when (error) {
is HttpException -> {
when (error.code()) {
404 -> UWErrorCode.BACKEND_SERVICE_NOT_FOUND
else -> UWErrorCode.BACKEND_SERVER_ERROR
}
}
is UnknownHostException -> UWErrorCode.APP_OFFLINE
is ConnectException,
is SocketTimeoutException,
is SSLHandshakeException -> UWErrorCode.APP_CONNECTION_FAILED
else -> UWErrorCode.APP_UNKNOWN_ERROR
}
val builder = UWError.Builder(errorCode, error).setLocalDataSource(localDataSource)
contextKey?.let {
builder.context(it, contextValue)
}
return builder.build()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment