Created
May 27, 2022 09:42
-
-
Save mrsasha/2156e540235c685129df8112e9733662 to your computer and use it in GitHub Desktop.
HomeViewModel v2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
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