Skip to content

Instantly share code, notes, and snippets.

@mrsasha
Created May 27, 2022 09:31
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/891284ff631e8d82f9bb822fdace337b to your computer and use it in GitHub Desktop.
Save mrsasha/891284ff631e8d82f9bb822fdace337b to your computer and use it in GitHub Desktop.
HomeViewModel v1
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.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.uniservice.ServiceDTO
import com.uniwhere.kmp.hydra.be.dto.uniservice.entity.EntityType
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.getDate
import lu.gian.uniwhere.core.extensions.dto.hasBeenPaid
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.UWDates
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.IconTitleTile
import lu.gian.uniwhere.features.home.data.model.home.QuickActionCollection
import lu.gian.uniwhere.features.home.data.model.home.QuickActionDestination
import lu.gian.uniwhere.features.home.data.model.home.QuickActionTile
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.uicomponents.data.model.tile.HeaderTile
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
) : 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
fun checkForUnsupportedUniversity(firstTime: Boolean) {
_stateOfHome.value = StatesOfHome.ShowLoading
viewModelScope.launch {
isUnsupportedService = isServiceUnsupported()
if (isUnsupportedService && firstTime) {
_stateOfHome.value = StatesOfHome.UnsupportedUniversity
}
sessionCheckup()
}
}
fun sessionCheckup() {
Timber.d("[CHECKUP] Starting Checkup")
if (wasAppUpdated()) {
val savedVersion = settingsRepository.savedAppVersion.get()
if (savedVersion <= 92000) {
// Identify user!
identifyOnSegment()
}
Timber.d("[sessionCheckup] : New app update detected")
} else {
Timber.d("[sessionCheckup] : App first start, begin checkup")
}
updateConf()
}
private fun updateConf() {
_stateOfHome.value = StatesOfHome.ShowLoading
Timber.d("[updateConf] Updating Bootstrap and Conf")
val apiAuth = localDataSource.getApiAuth()
apiAuth?.let {
Utils.initFacebookSDK()
log("[updateConf] Check Crashlytics")
} ?: {
// TODO What if apiAuth is null?
}
viewModelScope.launch {
try {
val configuration = coreRepository.getConf()
localDataSource.deleteConfiguration()
localDataSource.saveConfiguration(configuration)
// Update also the selected service
val actualService = localDataSource.getSelectedService()
val serviceCode = actualService?.serviceCode ?: ""
val updateService = coreRepository.getService(serviceCode)
localDataSource.saveSelectedService(updateService)
updateUser()
} catch (e: Exception) {
val error = when (e) {
is HttpException -> {
when (e.code()) {
401 -> {
performLogout()
return@launch
}
404 -> UWError.Builder(UWErrorCode.BACKEND_SERVICE_NOT_FOUND, e)
.setLocalDataSource(localDataSource)
.build()
else -> UWError.Builder(UWErrorCode.BACKEND_SERVER_ERROR, e)
.setLocalDataSource(localDataSource)
.build()
}
}
is UnknownHostException -> UWError.Builder(UWErrorCode.APP_OFFLINE, e)
.setLocalDataSource(localDataSource)
.build()
is ConnectException, is SocketTimeoutException, is SSLHandshakeException ->
UWError.Builder(UWErrorCode.APP_CONNECTION_FAILED, e)
.setLocalDataSource(localDataSource)
.build()
else -> UWError.Builder(UWErrorCode.APP_UNKNOWN_ERROR, e)
.setLocalDataSource(localDataSource)
.build()
}
_stateOfHome.value = StatesOfHome.Error
_banner.value = UWErrorResourcesMapper.raiseError(error)
_stateOfHome.value = StatesOfHome.HideLoading
Timber.e(error, "[updateConf] Error: $error")
}
}
}
private fun updateUser() {
Timber.d("[updateUser()] updating user")
// Reset cached value
coreRepository.currentUserDTO = 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
val newUser = if (token != null && token != userFromMemory?.fcmToken) {
coreRepository.updateFcmToken(token)
} else {
coreRepository.getUser()
}
apiAuth?.copy(user = newUser)?.let {
localDataSource.saveApiAuth(it)
}
coreRepository.refreshReviewList()
dataUpdate()
} catch (error: Exception) {
handleAuthGenericError(error)
Timber.e(error, "[updateUser] Error: $error")
_stateOfHome.value = StatesOfHome.HideLoading
}
}
}
private fun dataUpdate() {
Timber.d("[dataUpdate()] updating data")
viewModelScope.launch(Dispatchers.Main) {
try {
val currentUniAccountWrapper = coreRepository.currentUniAccountWrapper
//TODO we need an IF to manage the other case
currentUniAccountWrapper?.universityAccountDTO?.let { account ->
val t = uniServiceRepository.getAllEntities(account.accountId)
Timber.d("dataUpdate - All entities fetched correctly")
Timber.v(t.toString())
Timber.d("dataUpdate - University Account - ${account.toString()}")
localDataSource.getUniServiceTrackingData()?.let { trackData ->
val service = localDataSource.getSelectedService()
service?.let { ser ->
trackData.syncType?.let { syncType ->
analyticsHelper.trackEvent(
AnalyticsTrackActions.universityDataReceived, mapOf(
AnalyticsTrackPropertyKeys.status to "success",
AnalyticsTrackPropertyKeys.universityCode to ser.serviceCode,
AnalyticsTrackPropertyKeys.syncType to syncType,
AnalyticsTrackPropertyKeys.error to "none"
)
)
}
}
}
val keysIterator = t.keys()
val recapMessage = StringBuilder().append("Recap of found entities:\n")
while (keysIterator.hasNext()) {
val key = keysIterator.next()
try {
val entityType = EntityType.valueOf(key)
val jsonArray = t.getJSONArray(key)
uniServiceRepository.deserializeAndSaveEntity(entityType, jsonArray)
Timber.v(String.format("Entity %s successfully stored", key))
Timber.v(
String.format(
"Found ${
t.getJSONArray(key).length()
} items of entity: %s", key
)
)
recapMessage.append(
"\tFound ${t.getJSONArray(key).length()} items of entity: $key\n"
)
} catch (e: Exception) {
Timber.e(e,
String.format(
"Skipping storing entity %s because of the exception: %s",
key,
e.message
)
)
}
}
log(recapMessage.toString())
//TODO why? don't we already load this above?
val universityAccount = coreRepository.getUniversityAccount(currentUniAccountWrapper.universityAccountDTO.accountId)
localDataSource.updateUniAccountDTO(universityAccount)
localDataSource.setSyncStatus(
UniversitySyncStatus.SYNCED,
universityAccount.accountId
)
settingsRepository.lastAutoRefresh.set(System.currentTimeMillis())
val transcriptResult = coreRepository.getTranscript(universityAccount.accountId)
localDataSource.saveTranscript(transcriptResult)
settingsRepository.studyPlanRefreshRequired.set(true)
val examStats = coreRepository.getExamStats(universityAccount.accountId)
// delete previous exam stats, to be sure
localDataSource.deleteExamStats()
localDataSource.saveExamStats(examStats)
generateHomeItems()
}
} catch (e: Exception) {
Timber.e(e, "Error fetching all entities. Tried too many times already. Error: $e")
val error = when (e) {
is HttpException -> {
when (e.code()) {
401 -> UWError.Builder(UWErrorCode.BACKEND_LOGIN_FAILED, e)
else -> UWError.Builder(UWErrorCode.BACKEND_SERVER_ERROR, e)
}
}
is UnknownHostException -> UWError.Builder(UWErrorCode.APP_OFFLINE, e)
is ConnectException, is SocketTimeoutException, is SSLHandshakeException ->
UWError.Builder(UWErrorCode.APP_CONNECTION_FAILED, e)
else -> UWError.Builder(UWErrorCode.APP_UNKNOWN_ERROR, e)
}.context("ERROR", "Error fetching all entities. Tried too many times already")
.setLocalDataSource(localDataSource)
.build()
localDataSource.getUniServiceTrackingData()?.let { trackData ->
val service = localDataSource.getSelectedService()
service?.let { ser ->
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 "failure",
AnalyticsTrackPropertyKeys.universityCode to ser.serviceCode,
AnalyticsTrackPropertyKeys.syncType to syncType,
AnalyticsTrackPropertyKeys.error to errorCode
)
)
}
}
}
_stateOfHome.value = StatesOfHome.HideLoading
_banner.value = UWErrorResourcesMapper.raiseError(error)
}
}
tokenRefresh()
}
//TODO why now?
private fun tokenRefresh() {
Timber.d("[CHECK-UP] Refreshing token")
val lastUserUpdateTimestamp = settingsRepository.lastUserUpdate.get()
// 7 days
if ((System.currentTimeMillis() - lastUserUpdateTimestamp) > lu.gian.uniwhere.core.BuildConfig.USER_REFRESH_INTERVAL) {
viewModelScope.launch {
try {
val apiAuth = coreRepository.updateAccessToken()
localDataSource.saveApiAuth(apiAuth)
settingsRepository.lastUserUpdate.set(System.currentTimeMillis())
generateHomeItems()
} catch (e: Exception) {
handleAuthGenericError(e)
}
}
} else {
Timber.d("[CHECK-UP] Token refresh not necessary, skipping")
generateHomeItems()
}
}
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() {
// recompute the tiles
Timber.d("[generateHomeItems] : Computing Homepage Tiles")
//items.clear()
_stateOfHome.value = StatesOfHome.ShowLoading
Timber.v("[generateHomeItems] Getting current UniAccount")
val uniAccount = coreRepository.currentUniAccountWrapper
if (uniAccount == 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(uniAccount))
} 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(generateQuickActions())
items.addAll(generateAdministrationTile())
items.add(HomeUtils.getLastUpdateTile(settingsRepository, resourceProvider))
val state = StatesOfHome.HomeState(
items = items,
homeHeader = generateHomeHeader()
)
_stateOfHome.value = state
}
viewModelScope.launch {
checkRatingSettings()
}
}
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(uniAccountWrapper: UniAccountWrapper): List<HomeTileItem> {
val items = mutableListOf<HomeTileItem>()
Timber.d("[generateSyncIssueTile] Generating Sync Issue Tile")
if (uniAccountWrapper.syncStatus != 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 = uniAccountWrapper.universityAccountDTO.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 generateQuickActions(): List<HomeTileItem> {
val items = mutableListOf<HomeTileItem>()
Timber.d("[generateQuickActions] Generating Quick Actions Tile")
// header
items.add(
HomeTileItem(
data = HeaderTile(
title = resourceProvider.getString(R.string.HOME_quick_actions)
),
itemType = HomeTileItemType.HEADER
)
)
val actions = mutableListOf<QuickActionTile>()
actions.add(
QuickActionTile(
title = resourceProvider.getString(R.string.HOME_quick_action_focus_time),
iconResId = R.drawable.ic_quick_action_focus_time,
destination = QuickActionDestination.FOCUS_TIME,
color = resourceProvider.getColor(R.color.green)
)
)
actions.add(
QuickActionTile(
title = resourceProvider.getString(R.string.HOME_quick_action_web_mail),
iconResId = R.drawable.ic_home_quick_action_mail,
destination = QuickActionDestination.WEBMAIL,
color = resourceProvider.getColor(R.color.red)
)
)
actions.add(
QuickActionTile(
title = resourceProvider.getString(R.string.HOME_quick_action_exam_search),
iconResId = R.drawable.ic_home_quick_action_search,
destination = QuickActionDestination.SEARCH,
color = resourceProvider.getColor(R.color.cyan)
)
)
items.add(
HomeTileItem(
data = QuickActionCollection(
actions = actions
),
itemType = HomeTileItemType.QUICK_ACTION_TILE
)
)
return items
}
private fun generateAdministrationTile(): List<HomeTileItem> {
val items = mutableListOf<HomeTileItem>()
Timber.d("[generateAdministrationTile] Generating Administration Tile")
var headerAdded = false
val service = localDataSource.getSelectedService()
if (service != null) {
val entityTypes = service.entityTypes
// Taxes?
if (entityTypes.contains(EntityType.TAX)) {
if (!headerAdded) {
// header
items.add(
HomeTileItem(
data = HeaderTile(
title = resourceProvider.getString(R.string.HOME_administration)
),
itemType = HomeTileItemType.HEADER
)
)
headerAdded = true
}
// add the tax tile
var taxes = localDataSource.getTaxes()
val stringsToBold = mutableListOf<String>()
val todayMillis = System.currentTimeMillis()
taxes = taxes.sortedBy { it.dueDate }
val nextTax = taxes.singleOrNull {
!it.hasBeenPaid() && (it.dueDate ?: 0L) >= todayMillis
}
val taxShown: String
val taxTileContent = if (nextTax == null) {
taxShown = "none"
resourceProvider.getString(R.string.HOME_no_taxes_content)
} else {
taxShown = "due_fee"
val amount = nextTax.amount
stringsToBold.add(amount)
val formattedDate = if (nextTax.dueDate != null) {
UWDates.getDayMonthYearStringDate(nextTax.dueDate!!)
} else {
"N/A"
}
stringsToBold.add(formattedDate)
resourceProvider.getString(R.string.HOME_taxes_content, amount, formattedDate)
}
// generate tile
items.add(
HomeTileItem(
data = IconTitleTile(
title = resourceProvider.getString(R.string.HOME_tax_title),
subtitle = taxTileContent,
stringsToBold = stringsToBold,
color = resourceProvider.getColor(R.color.yellow),
iconResId = R.drawable.ic_icon_dollar,
destination = HomeDestination.MONEY,
taxShown = taxShown
),
itemType = HomeTileItemType.ICON_TITLE_TILE
)
)
}
// Tests? Applied or Available
if (entityTypes.contains(EntityType.APPLIEDTEST) ||
entityTypes.contains(EntityType.AVAILABLETEST) ||
entityTypes.contains(EntityType.AVAILABLEPARTIALTEST)
) {
if (!headerAdded) {
// header
items.add(
HomeTileItem(
data = HeaderTile(
title = resourceProvider.getString(R.string.HOME_administration)
),
itemType = HomeTileItemType.HEADER
)
)
headerAdded = true
}
val stringsToBold = mutableListOf<String>()
var stringContent = resourceProvider.getString(R.string.HOME_no_test)
// check for applied test
val appliedList = localDataSource.getAppliedTests()
// check for available tests
val availableList = localDataSource.getAvailableTests()
var segmentShown = "none"
var dayToTest: Int? = null
var numberOfTest: Int? = null
if (appliedList.isNotEmpty()) {
val today = Calendar.getInstance()
val firstTest = appliedList
.filter { it.getDate().after(today) }
.minByOrNull { it.testDate }
firstTest?.let { test ->
val formattedDate = UWDates.getDayMonthYearStringDate(test.testDate)
val testsNumber = appliedList.size.toString()
stringsToBold.add(testsNumber)
stringsToBold.add(formattedDate)
stringContent = resourceProvider.getString(
R.string.HOME_applied_test,
testsNumber,
formattedDate
)
// Tracking Stuff
segmentShown = "applied_preview"
val diff = test.getDate().timeInMillis - today.timeInMillis
dayToTest = (diff / (24 * 60 * 60 * 1000)).toInt()
numberOfTest = appliedList.size
}
} else {
if (availableList.isNotEmpty()) {
val firstTest = availableList.minByOrNull { it.applicationOpening ?: 0 }
firstTest?.let { test ->
val formattedDate = if (test.applicationOpening != null) {
UWDates.getDayMonthYearStringDate(test.applicationOpening!!)
} else {
"N/A"
}
val testsNumber = availableList.size.toString()
stringsToBold.add(testsNumber)
stringsToBold.add(formattedDate)
stringContent = resourceProvider.getString(
R.string.HOME_available_tests,
testsNumber,
formattedDate
)
segmentShown = "available_preview"
val today = Calendar.getInstance()
val diff = test.getDate().timeInMillis - today.timeInMillis
dayToTest = (diff / (24 * 60 * 60 * 1000)).toInt()
numberOfTest = availableList.size
}
}
}
// generate tile
items.add(
HomeTileItem(
data = IconTitleTile(
title = resourceProvider.getString(R.string.HOME_test_title),
subtitle = stringContent,
stringsToBold = stringsToBold,
color = resourceProvider.getColor(R.color.yellow),
iconResId = R.drawable.ic_icon_calendar,
destination = HomeDestination.TESTS,
testPreviewShown = segmentShown,
daysToTest = dayToTest,
numberOfTests = numberOfTest
),
itemType = HomeTileItemType.ICON_TITLE_TILE
)
)
}
// Webmail
if (service.wmEnabled) {
if (!headerAdded) {
// header
items.add(
HomeTileItem(
data = HeaderTile(
title = resourceProvider.getString(R.string.HOME_administration)
),
itemType = HomeTileItemType.HEADER
)
)
}
items.add(
HomeTileItem(
data = IconTitleTile(
title = resourceProvider.getString(R.string.WEBMAIL_title),
subtitle = resourceProvider.getString(R.string.HOME_webmail_content),
color = resourceProvider.getColor(R.color.yellow),
iconResId = R.drawable.ic_icon_mail,
destination = HomeDestination.WEBMAIL
),
itemType = HomeTileItemType.ICON_TITLE_TILE
)
)
}
}
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 {
localDataSource.getSelectedService()?.let { serviceDTO ->
return serviceDTO.enabled
}
return 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)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment