diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a2137c7d9..f451135ee 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -31,7 +31,6 @@ - diff --git a/app/src/main/java/com/forcetower/uefs/GooglePlayGamesInstance.kt b/app/src/main/java/com/forcetower/uefs/GooglePlayGamesInstance.kt index d04c83027..37e3360bf 100644 --- a/app/src/main/java/com/forcetower/uefs/GooglePlayGamesInstance.kt +++ b/app/src/main/java/com/forcetower/uefs/GooglePlayGamesInstance.kt @@ -1,133 +1,135 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs - -import android.app.Activity -import android.content.ContextWrapper -import androidx.annotation.StringRes -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.preference.PreferenceManager -import com.forcetower.core.lifecycle.Event -import com.google.android.gms.games.PlayGames -import kotlinx.coroutines.tasks.await - -class GooglePlayGamesInstance( - private val activity: Activity -) : ContextWrapper(activity) { - private var playerName: String? = null - val signInClient = PlayGames.getGamesSignInClient(activity) - val achievementsClient = PlayGames.getAchievementsClient(activity) - - private val preferences = PreferenceManager.getDefaultSharedPreferences(this) - private var playerUnlockedSwitch: Boolean = false - - private val _status = MutableLiveData>() - val connectionStatus: LiveData> - get() = _status - - /** - * Deve ser chamado quando o usuário de conectar com a conta do google para que o objeto possa - * criar todas as dependencias - */ - fun onConnected() { - _status.postValue(Event(GameConnectionStatus.CONNECTED)) - preferences.edit().putBoolean("google_play_games_enabled_v2", true).apply() - unlockAchievement(R.string.achievement_comeou_o_jogo) - if (playerUnlockedSwitch) { - unlockAchievement(R.string.achievement_agora_eu_entendi_agora_eu_saquei) - } - } - - /** - * Deve ser chamado quando o usuário optar por sair do jogo - */ - fun onDisconnected() { - _status.postValue(Event(GameConnectionStatus.DISCONNECTED)) - preferences.edit().putBoolean("google_play_games_enabled_v2", false).apply() - } - - /** - * Chame este método para desconectar o usuário do google play games - */ - fun disconnect() { - // removed. i guess - } - - /** - * Chame este método quando ocorrer uma troca de conta... Por causa de motivos... - */ - fun changePlayerName(other: String) { - playerUnlockedSwitch = playerName != null && other != playerName - playerName = other - if (playerUnlockedSwitch) { - unlockAchievement(R.string.achievement_agora_eu_entendi_agora_eu_saquei) - } - } - - /** - * Retorna se existe alguem conectado às contas do google ou não - */ - suspend fun isConnected(): Boolean { - return PlayGames.getGamesSignInClient(activity).isAuthenticated.await().isAuthenticated - } - - /** - * Descobre se o Google Play Games está ativado ou desativado - */ - fun isPlayGamesEnabled(): Boolean { - return preferences.getBoolean("google_play_games_enabled_v2", false) - } - - /** - * Desbloqueia uma conquista - */ - fun unlockAchievement(@StringRes resource: Int) { - unlockAchievement(getString(resource)) - } - - fun unlockAchievement(achievement: String) { - achievementsClient?.unlock(achievement) - } - - fun revealAchievement(@StringRes resource: Int) { - val id = getString(resource) - achievementsClient?.reveal(id) - } - - fun incrementAchievement(@StringRes resource: Int, step: Int) { - val id = getString(resource) - achievementsClient?.increment(id, step) - } - - fun updateProgress(@StringRes resource: Int, value: Int) { - val id = getString(resource) - achievementsClient?.setSteps(id, value) - } - - fun updateProgress(id: String, value: Int) { - achievementsClient?.setSteps(id, value) - } -} - -enum class GameConnectionStatus { - CONNECTED, DISCONNECTED, LOADING -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs + +import android.app.Activity +import android.content.ContextWrapper +import androidx.annotation.StringRes +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.preference.PreferenceManager +import com.forcetower.core.lifecycle.Event +import com.google.android.gms.games.PlayGames +import kotlinx.coroutines.tasks.await + +class GooglePlayGamesInstance( + private val activity: Activity +) : ContextWrapper(activity) { + private var playerName: String? = null + val signInClient = PlayGames.getGamesSignInClient(activity) + val achievementsClient = PlayGames.getAchievementsClient(activity) + + private val preferences = PreferenceManager.getDefaultSharedPreferences(this) + private var playerUnlockedSwitch: Boolean = false + + private val _status = MutableLiveData>() + val connectionStatus: LiveData> + get() = _status + + /** + * Deve ser chamado quando o usuário de conectar com a conta do google para que o objeto possa + * criar todas as dependencias + */ + fun onConnected() { + _status.postValue(Event(GameConnectionStatus.CONNECTED)) + preferences.edit().putBoolean("google_play_games_enabled_v2", true).apply() + unlockAchievement(R.string.achievement_comeou_o_jogo) + if (playerUnlockedSwitch) { + unlockAchievement(R.string.achievement_agora_eu_entendi_agora_eu_saquei) + } + } + + /** + * Deve ser chamado quando o usuário optar por sair do jogo + */ + fun onDisconnected() { + _status.postValue(Event(GameConnectionStatus.DISCONNECTED)) + preferences.edit().putBoolean("google_play_games_enabled_v2", false).apply() + } + + /** + * Chame este método para desconectar o usuário do google play games + */ + fun disconnect() { + // removed. i guess + } + + /** + * Chame este método quando ocorrer uma troca de conta... Por causa de motivos... + */ + fun changePlayerName(other: String) { + playerUnlockedSwitch = playerName != null && other != playerName + playerName = other + if (playerUnlockedSwitch) { + unlockAchievement(R.string.achievement_agora_eu_entendi_agora_eu_saquei) + } + } + + /** + * Retorna se existe alguem conectado às contas do google ou não + */ + suspend fun isConnected(): Boolean { + return PlayGames.getGamesSignInClient(activity).isAuthenticated.await().isAuthenticated + } + + /** + * Descobre se o Google Play Games está ativado ou desativado + */ + fun isPlayGamesEnabled(): Boolean { + return preferences.getBoolean("google_play_games_enabled_v2", false) + } + + /** + * Desbloqueia uma conquista + */ + fun unlockAchievement(@StringRes resource: Int) { + unlockAchievement(getString(resource)) + } + + fun unlockAchievement(achievement: String) { + achievementsClient?.unlock(achievement) + } + + fun revealAchievement(@StringRes resource: Int) { + val id = getString(resource) + achievementsClient?.reveal(id) + } + + fun incrementAchievement(@StringRes resource: Int, step: Int) { + val id = getString(resource) + achievementsClient?.increment(id, step) + } + + fun updateProgress(@StringRes resource: Int, value: Int) { + val id = getString(resource) + achievementsClient?.setSteps(id, value) + } + + fun updateProgress(id: String, value: Int) { + achievementsClient?.setSteps(id, value) + } +} + +enum class GameConnectionStatus { + CONNECTED, + DISCONNECTED, + LOADING +} diff --git a/app/src/main/java/com/forcetower/uefs/LaunchViewModel.kt b/app/src/main/java/com/forcetower/uefs/LaunchViewModel.kt index 41ec54d91..0bef7b3af 100644 --- a/app/src/main/java/com/forcetower/uefs/LaunchViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/LaunchViewModel.kt @@ -29,8 +29,8 @@ import com.forcetower.uefs.core.task.successOr import dagger.hilt.android.lifecycle.HiltViewModel import dev.forcetower.unes.usecases.auth.HasEnrolledAccessUseCase import dev.forcetower.unes.usecases.version.NotifyNewVersionUseCase -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.launch @HiltViewModel class LaunchViewModel @Inject constructor( diff --git a/app/src/main/java/com/forcetower/uefs/UApplication.kt b/app/src/main/java/com/forcetower/uefs/UApplication.kt index 2b01c2188..395bf1178 100644 --- a/app/src/main/java/com/forcetower/uefs/UApplication.kt +++ b/app/src/main/java/com/forcetower/uefs/UApplication.kt @@ -1,122 +1,123 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs - -import android.app.Application -import android.content.Context -import android.content.SharedPreferences -import androidx.hilt.work.HiltWorkerFactory -import androidx.work.Configuration -import com.forcetower.sagres.SagresNavigator -import com.forcetower.uefs.core.constants.Constants -import com.forcetower.uefs.core.storage.cookies.CachedCookiePersistor -import com.forcetower.uefs.core.storage.cookies.PrefsCookiePersistor -import com.forcetower.uefs.core.work.sync.SyncMainWorker -import com.forcetower.uefs.feature.themeswitcher.ThemePreferencesManager -import com.forcetower.uefs.impl.AndroidBase64Encoder -import com.forcetower.uefs.impl.CrashlyticsTree -import com.forcetower.uefs.impl.SharedPrefsCachePersistence -import com.forcetower.uefs.service.NotificationHelper -import com.google.android.gms.games.PlayGamesSdk -import com.google.android.play.core.splitcompat.SplitCompat -import dagger.hilt.android.HiltAndroidApp -import okhttp3.OkHttpClient -import timber.log.Timber -import javax.inject.Inject - -@HiltAndroidApp -class UApplication : Application(), Configuration.Provider { - @Inject lateinit var preferences: SharedPreferences - @Inject lateinit var workerFactory: HiltWorkerFactory - - var disciplineToolbarDevClickCount = 0 - var messageToolbarDevClickCount = 0 - - override fun attachBaseContext(base: Context) { - super.attachBaseContext(base) - SplitCompat.install(this) - } - - override fun onCreate() { - if (BuildConfig.DEBUG) { - Timber.plant(Timber.DebugTree()) - } else { - Timber.plant(CrashlyticsTree()) - } - super.onCreate() - - if (preferences.getBoolean("google_play_games_enabled_v2", false)) { - PlayGamesSdk.initialize(this) - } - setupDayNightTheme(this) - defineWorker() - } - - private fun defineWorker() { - val worker = preferences.getString("stg_sync_worker_type", "0")?.toIntOrNull() ?: 0 - val period = preferences.getString("stg_sync_frequency", "60")?.toIntOrNull() ?: 60 - when (worker) { - 0 -> SyncMainWorker.createWorker(this, period) - 1 -> Unit // SyncLinkedWorker.createWorker(period, false) - } - } - - /** - * Inicializa o objeto de conexão com o Sagres - */ - @Inject - fun configureSagresNavigator(client: OkHttpClient, cachingCookie: CachedCookiePersistor) { - val selected = preferences.getString(Constants.SELECTED_INSTITUTION_KEY, "UEFS") ?: "UEFS" - SagresNavigator.initialize( - PrefsCookiePersistor(this), - cachingCookie, - selected, - AndroidBase64Encoder(), - SharedPrefsCachePersistence(preferences), - baseClient = client - ) - } - - /** - * Cria/Apaga os canais de notificação - */ - @Inject - fun configureNotifications() { - NotificationHelper(this).createChannels() - } - - override val workManagerConfiguration: Configuration - get() = Configuration.Builder() - .setWorkerFactory(workerFactory) - .build() - - companion object { - // Resetting the theme at every theme change is not a good solution - // Find out a migration path for theme overlays to use in the future - fun setupDayNightTheme(context: Context, resetTheme: Boolean = false) { - ThemePreferencesManager(context).run { - applyTheme() - if (resetTheme) deleteSavedTheme() - retrieveOverlay() - } - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs + +import android.app.Application +import android.content.Context +import android.content.SharedPreferences +import androidx.hilt.work.HiltWorkerFactory +import androidx.work.Configuration +import com.forcetower.sagres.SagresNavigator +import com.forcetower.uefs.core.constants.Constants +import com.forcetower.uefs.core.storage.cookies.CachedCookiePersistor +import com.forcetower.uefs.core.storage.cookies.PrefsCookiePersistor +import com.forcetower.uefs.core.work.sync.SyncMainWorker +import com.forcetower.uefs.feature.themeswitcher.ThemePreferencesManager +import com.forcetower.uefs.impl.AndroidBase64Encoder +import com.forcetower.uefs.impl.CrashlyticsTree +import com.forcetower.uefs.impl.SharedPrefsCachePersistence +import com.forcetower.uefs.service.NotificationHelper +import com.google.android.gms.games.PlayGamesSdk +import com.google.android.play.core.splitcompat.SplitCompat +import dagger.hilt.android.HiltAndroidApp +import javax.inject.Inject +import okhttp3.OkHttpClient +import timber.log.Timber + +@HiltAndroidApp +class UApplication : Application(), Configuration.Provider { + @Inject lateinit var preferences: SharedPreferences + + @Inject lateinit var workerFactory: HiltWorkerFactory + + var disciplineToolbarDevClickCount = 0 + var messageToolbarDevClickCount = 0 + + override fun attachBaseContext(base: Context) { + super.attachBaseContext(base) + SplitCompat.install(this) + } + + override fun onCreate() { + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } else { + Timber.plant(CrashlyticsTree()) + } + super.onCreate() + + if (preferences.getBoolean("google_play_games_enabled_v2", false)) { + PlayGamesSdk.initialize(this) + } + setupDayNightTheme(this) + defineWorker() + } + + private fun defineWorker() { + val worker = preferences.getString("stg_sync_worker_type", "0")?.toIntOrNull() ?: 0 + val period = preferences.getString("stg_sync_frequency", "60")?.toIntOrNull() ?: 60 + when (worker) { + 0 -> SyncMainWorker.createWorker(this, period) + 1 -> Unit // SyncLinkedWorker.createWorker(period, false) + } + } + + /** + * Inicializa o objeto de conexão com o Sagres + */ + @Inject + fun configureSagresNavigator(client: OkHttpClient, cachingCookie: CachedCookiePersistor) { + val selected = preferences.getString(Constants.SELECTED_INSTITUTION_KEY, "UEFS") ?: "UEFS" + SagresNavigator.initialize( + PrefsCookiePersistor(this), + cachingCookie, + selected, + AndroidBase64Encoder(), + SharedPrefsCachePersistence(preferences), + baseClient = client + ) + } + + /** + * Cria/Apaga os canais de notificação + */ + @Inject + fun configureNotifications() { + NotificationHelper(this).createChannels() + } + + override val workManagerConfiguration: Configuration + get() = Configuration.Builder() + .setWorkerFactory(workerFactory) + .build() + + companion object { + // Resetting the theme at every theme change is not a good solution + // Find out a migration path for theme overlays to use in the future + fun setupDayNightTheme(context: Context, resetTheme: Boolean = false) { + ThemePreferencesManager(context).run { + applyTheme() + if (resetTheme) deleteSavedTheme() + retrieveOverlay() + } + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/architecture/receiver/OnUpgradeReceiver.kt b/app/src/main/java/com/forcetower/uefs/architecture/receiver/OnUpgradeReceiver.kt index 8c89b14fd..4f839cbd0 100644 --- a/app/src/main/java/com/forcetower/uefs/architecture/receiver/OnUpgradeReceiver.kt +++ b/app/src/main/java/com/forcetower/uefs/architecture/receiver/OnUpgradeReceiver.kt @@ -34,6 +34,7 @@ import javax.inject.Inject class OnUpgradeReceiver : BroadcastReceiver() { @Inject lateinit var preferences: SharedPreferences + @Inject lateinit var repository: UpgradeRepository diff --git a/app/src/main/java/com/forcetower/uefs/architecture/service/bigtray/BigTrayService.kt b/app/src/main/java/com/forcetower/uefs/architecture/service/bigtray/BigTrayService.kt index e5f3d254a..33c32342d 100644 --- a/app/src/main/java/com/forcetower/uefs/architecture/service/bigtray/BigTrayService.kt +++ b/app/src/main/java/com/forcetower/uefs/architecture/service/bigtray/BigTrayService.kt @@ -1,110 +1,110 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.architecture.service.bigtray - -import android.app.Notification -import android.app.PendingIntent -import android.app.Service -import android.content.Context -import android.content.Intent -import android.os.Build -import androidx.core.app.ServiceCompat -import androidx.lifecycle.LifecycleService -import androidx.lifecycle.Observer -import com.forcetower.uefs.core.model.bigtray.BigTrayData -import com.forcetower.uefs.feature.bigtray.BigTrayRepository -import com.forcetower.uefs.service.NotificationCreator -import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber -import javax.inject.Inject - -@AndroidEntryPoint -class BigTrayService : LifecycleService() { - companion object { - private const val NOTIFICATION_BIG_TRAY = 187745 - const val START_SERVICE_ACTION = "com.forcetower.uefs.bigtray.START_FOREGROUND_SERVICE" - const val STOP_SERVICE_ACTION = "com.forcetower.uefs.bigtray.STOP_FOREGROUND_SERVICE" - - @JvmStatic - fun startService(context: Context) { - val intent = Intent(context, BigTrayService::class.java) - context.startService(intent) - } - } - - @Inject - lateinit var repository: BigTrayRepository - private var running = false - private var trayData: BigTrayData? = null - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - - when (intent?.action) { - START_SERVICE_ACTION -> startComponent() - STOP_SERVICE_ACTION -> stopComponent() - else -> startComponent() - } - - return Service.START_STICKY - } - - private fun stopComponent() { - Timber.d("Stop service action") - running = false - ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) - stopSelf() - } - - private fun startComponent() { - if (!running) { - running = true - Timber.d("Start action!") - startForeground(NOTIFICATION_BIG_TRAY, createNotification()) - repository.beginWith(7000).observe( - this, - Observer { - if (trayData != it) { - trayData = it - startForeground(NOTIFICATION_BIG_TRAY, createNotification(it)) - } - } - ) - } else { - Timber.d("Ignored new run attempt while it's already running") - } - } - - override fun onDestroy() { - super.onDestroy() - repository.requesting = false - running = false - } - - private fun createNotification(data: BigTrayData? = null): Notification { - val intent = Intent(this, BigTrayService::class.java).apply { - action = STOP_SERVICE_ACTION - } - val flags = if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0 - val pending = PendingIntent.getService(this, 0, intent, flags) - return NotificationCreator.showBigTrayNotification(this, data, pending) - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.architecture.service.bigtray + +import android.app.Notification +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.core.app.ServiceCompat +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.Observer +import com.forcetower.uefs.core.model.bigtray.BigTrayData +import com.forcetower.uefs.feature.bigtray.BigTrayRepository +import com.forcetower.uefs.service.NotificationCreator +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import timber.log.Timber + +@AndroidEntryPoint +class BigTrayService : LifecycleService() { + companion object { + private const val NOTIFICATION_BIG_TRAY = 187745 + const val START_SERVICE_ACTION = "com.forcetower.uefs.bigtray.START_FOREGROUND_SERVICE" + const val STOP_SERVICE_ACTION = "com.forcetower.uefs.bigtray.STOP_FOREGROUND_SERVICE" + + @JvmStatic + fun startService(context: Context) { + val intent = Intent(context, BigTrayService::class.java) + context.startService(intent) + } + } + + @Inject + lateinit var repository: BigTrayRepository + private var running = false + private var trayData: BigTrayData? = null + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + + when (intent?.action) { + START_SERVICE_ACTION -> startComponent() + STOP_SERVICE_ACTION -> stopComponent() + else -> startComponent() + } + + return Service.START_STICKY + } + + private fun stopComponent() { + Timber.d("Stop service action") + running = false + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) + stopSelf() + } + + private fun startComponent() { + if (!running) { + running = true + Timber.d("Start action!") + startForeground(NOTIFICATION_BIG_TRAY, createNotification()) + repository.beginWith(7000).observe( + this, + Observer { + if (trayData != it) { + trayData = it + startForeground(NOTIFICATION_BIG_TRAY, createNotification(it)) + } + } + ) + } else { + Timber.d("Ignored new run attempt while it's already running") + } + } + + override fun onDestroy() { + super.onDestroy() + repository.requesting = false + running = false + } + + private fun createNotification(data: BigTrayData? = null): Notification { + val intent = Intent(this, BigTrayService::class.java).apply { + action = STOP_SERVICE_ACTION + } + val flags = if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0 + val pending = PendingIntent.getService(this, 0, intent, flags) + return NotificationCreator.showBigTrayNotification(this, data, pending) + } +} diff --git a/app/src/main/java/com/forcetower/uefs/architecture/service/discipline/DisciplineDetailsLoaderService.kt b/app/src/main/java/com/forcetower/uefs/architecture/service/discipline/DisciplineDetailsLoaderService.kt index 7787bb32b..6a65a17e9 100644 --- a/app/src/main/java/com/forcetower/uefs/architecture/service/discipline/DisciplineDetailsLoaderService.kt +++ b/app/src/main/java/com/forcetower/uefs/architecture/service/discipline/DisciplineDetailsLoaderService.kt @@ -39,8 +39,8 @@ import com.forcetower.uefs.core.storage.repository.DisciplineDetailsRepository import com.forcetower.uefs.core.util.isConnectedToInternet import com.forcetower.uefs.service.NotificationCreator import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber import javax.inject.Inject +import timber.log.Timber @AndroidEntryPoint class DisciplineDetailsLoaderService : LifecycleService() { @@ -59,6 +59,7 @@ class DisciplineDetailsLoaderService : LifecycleService() { @Inject lateinit var repository: DisciplineDetailsRepository + @Inject lateinit var preferences: SharedPreferences diff --git a/app/src/main/java/com/forcetower/uefs/architecture/service/firebase/FirebaseActionsService.kt b/app/src/main/java/com/forcetower/uefs/architecture/service/firebase/FirebaseActionsService.kt index 7ad280f18..70f277546 100644 --- a/app/src/main/java/com/forcetower/uefs/architecture/service/firebase/FirebaseActionsService.kt +++ b/app/src/main/java/com/forcetower/uefs/architecture/service/firebase/FirebaseActionsService.kt @@ -24,13 +24,13 @@ import com.forcetower.uefs.core.storage.repository.FirebaseMessageRepository import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject @AndroidEntryPoint class FirebaseActionsService : FirebaseMessagingService() { diff --git a/app/src/main/java/com/forcetower/uefs/architecture/service/sync/SyncService.kt b/app/src/main/java/com/forcetower/uefs/architecture/service/sync/SyncService.kt index 8c6319212..949f0a8d9 100644 --- a/app/src/main/java/com/forcetower/uefs/architecture/service/sync/SyncService.kt +++ b/app/src/main/java/com/forcetower/uefs/architecture/service/sync/SyncService.kt @@ -33,18 +33,19 @@ import com.forcetower.uefs.core.storage.repository.MicroSyncRepository import com.forcetower.uefs.core.storage.repository.SagresSyncRepository import com.forcetower.uefs.service.NotificationCreator import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject @AndroidEntryPoint class SyncService : LifecycleService() { @Inject lateinit var generalRepository: SagresSyncRepository + @Inject lateinit var microRepository: MicroSyncRepository diff --git a/app/src/main/java/com/forcetower/uefs/architecture/widget/HomeClassWidget.kt b/app/src/main/java/com/forcetower/uefs/architecture/widget/HomeClassWidget.kt index 49fb9e02d..32e85b45c 100644 --- a/app/src/main/java/com/forcetower/uefs/architecture/widget/HomeClassWidget.kt +++ b/app/src/main/java/com/forcetower/uefs/architecture/widget/HomeClassWidget.kt @@ -29,12 +29,12 @@ import com.forcetower.uefs.core.storage.repository.DisciplinesRepository import com.forcetower.uefs.feature.shared.extensions.toTitleCase import com.google.firebase.analytics.FirebaseAnalytics import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch -import javax.inject.Inject @AndroidEntryPoint class HomeClassWidget : AppWidgetProvider() { diff --git a/app/src/main/java/com/forcetower/uefs/architecture/widget/HomeClassWidgetSecondary.kt b/app/src/main/java/com/forcetower/uefs/architecture/widget/HomeClassWidgetSecondary.kt index 9e656863a..994efdbab 100644 --- a/app/src/main/java/com/forcetower/uefs/architecture/widget/HomeClassWidgetSecondary.kt +++ b/app/src/main/java/com/forcetower/uefs/architecture/widget/HomeClassWidgetSecondary.kt @@ -29,12 +29,12 @@ import com.forcetower.uefs.core.storage.repository.DisciplinesRepository import com.forcetower.uefs.feature.shared.extensions.toTitleCase import com.google.firebase.analytics.FirebaseAnalytics import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch -import javax.inject.Inject @AndroidEntryPoint class HomeClassWidgetSecondary : AppWidgetProvider() { diff --git a/app/src/main/java/com/forcetower/uefs/core/constants/Constants.kt b/app/src/main/java/com/forcetower/uefs/core/constants/Constants.kt index 21c7f26f9..ddcff8cab 100644 --- a/app/src/main/java/com/forcetower/uefs/core/constants/Constants.kt +++ b/app/src/main/java/com/forcetower/uefs/core/constants/Constants.kt @@ -24,7 +24,6 @@ object Constants { const val SELECTED_INSTITUTION_KEY = "selected_institution_worker" const val UNES_SERVICE_BASE_URL = "unes.forcetower.dev" const val EDGE_UNES_SERVICE_BASE_URL = "edge-unes.forcetower.dev" - private const val UNES_SERVICE_BASE_UPDATE = "unes.herokuapp.com" const val UNES_SERVICE_URL = "https://$UNES_SERVICE_BASE_URL/api/" const val EDGE_UNES_SERVICE_URL = "https://$EDGE_UNES_SERVICE_BASE_URL/api/" @@ -34,8 +33,6 @@ object Constants { val HARD_DISCIPLINES = mapOf("TEC501" to "__ANY__") val EXECUTOR_WHITELIST = listOf("manual", "universal") - const val ADMOB_TEST_ID = "38D27336B4D54E6E431E86E4ABEE0B20" - const val SERVICE_CLIENT_ID = "1" const val SERVICE_CLIENT_SECRET = "bCP23X90J5anU0H3uxzWg0RwE6BxEo0HDkqr0PZg" const val SERVICE_CLIENT_INSTITUTION = "uefs" diff --git a/app/src/main/java/com/forcetower/uefs/core/effects/purchases/ScoreIncreaseEffect.kt b/app/src/main/java/com/forcetower/uefs/core/effects/purchases/ScoreIncreaseEffect.kt index aeffb1f1d..971c801e6 100644 --- a/app/src/main/java/com/forcetower/uefs/core/effects/purchases/ScoreIncreaseEffect.kt +++ b/app/src/main/java/com/forcetower/uefs/core/effects/purchases/ScoreIncreaseEffect.kt @@ -21,9 +21,9 @@ package com.forcetower.uefs.core.effects.purchases import android.content.SharedPreferences -import timber.log.Timber import java.util.Calendar import javax.inject.Inject +import timber.log.Timber class ScoreIncreaseEffect @Inject constructor( private val preferences: SharedPreferences diff --git a/app/src/main/java/com/forcetower/uefs/core/injection/module/AppModule.kt b/app/src/main/java/com/forcetower/uefs/core/injection/module/AppModule.kt index 9a8984b47..69c3faa9e 100644 --- a/app/src/main/java/com/forcetower/uefs/core/injection/module/AppModule.kt +++ b/app/src/main/java/com/forcetower/uefs/core/injection/module/AppModule.kt @@ -22,7 +22,7 @@ package com.forcetower.uefs.core.injection.module import android.content.Context import android.content.SharedPreferences -import android.webkit.WebSettings +import android.os.Build import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences @@ -44,7 +44,6 @@ import dagger.Reusable import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import timber.log.Timber import javax.inject.Named import javax.inject.Singleton @@ -106,12 +105,24 @@ object AppModule { @Reusable @Named("webViewUA") fun provideWebViewUserAgent(): String { - return try { - System.getProperty("http.agent") ?: "Mozilla/5.0 (Linux; Android 10; MI 9 Build/QKQ1.190825.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/123.0.6312.118 Mobile Safari/537.36" - } catch (error: Throwable) { - Timber.w("Failed to obtain device UserAgent") - Timber.w("UserAgent error ${error.message}") - "Mozilla/5.0 (Linux; Android 10; MI 9 Build/QKQ1.190825.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/123.0.6312.118 Mobile Safari/537.36" - } + val agents = listOf( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15", + "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Mobile Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 16_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 (a) Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (Linux; Android 14; SM-S928B Build/UP1A.231005.007; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.103 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 10; MI 9 Build/QKQ1.190825.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/123.0.6312.118 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36" + ) + return agents.random() + } + + @Provides + @Reusable + @Named("unesUserAgent") + fun provideUnesUserAgent(context: Context): String { + val version = context.packageManager.getPackageInfo(context.packageName, 0).versionName + val parts = version.split(".") + return "UNES/${parts[0]}.${parts[1]}.${parts[2]}.${parts[3]} (Android ${Build.VERSION.RELEASE}; Build/${Build.MODEL})" } } diff --git a/app/src/main/java/com/forcetower/uefs/core/injection/module/FirebaseCoreModule.kt b/app/src/main/java/com/forcetower/uefs/core/injection/module/FirebaseCoreModule.kt index ee3ad546d..0d7db2773 100644 --- a/app/src/main/java/com/forcetower/uefs/core/injection/module/FirebaseCoreModule.kt +++ b/app/src/main/java/com/forcetower/uefs/core/injection/module/FirebaseCoreModule.kt @@ -34,8 +34,8 @@ import dagger.Provides import dagger.Reusable import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import timber.log.Timber import javax.inject.Singleton +import timber.log.Timber @Module @InstallIn(SingletonComponent::class) diff --git a/app/src/main/java/com/forcetower/uefs/core/injection/module/NetworkModule.kt b/app/src/main/java/com/forcetower/uefs/core/injection/module/NetworkModule.kt index 92f01a5b5..29616607a 100644 --- a/app/src/main/java/com/forcetower/uefs/core/injection/module/NetworkModule.kt +++ b/app/src/main/java/com/forcetower/uefs/core/injection/module/NetworkModule.kt @@ -43,6 +43,12 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import java.net.CookieHandler +import java.net.CookieManager +import java.time.ZonedDateTime +import java.util.concurrent.TimeUnit +import javax.inject.Named +import javax.inject.Singleton import kotlinx.coroutines.runBlocking import okhttp3.ConnectionSpec import okhttp3.Interceptor @@ -51,12 +57,6 @@ import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import timber.log.Timber -import java.net.CookieHandler -import java.net.CookieManager -import java.time.ZonedDateTime -import java.util.concurrent.TimeUnit -import javax.inject.Named -import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) @@ -94,10 +94,11 @@ object NetworkModule { HttpLoggingInterceptor { Timber.tag("ok-http").d(it) }.apply { - level = if (BuildConfig.DEBUG) + level = if (BuildConfig.DEBUG) { HttpLoggingInterceptor.Level.BASIC - else + } else { HttpLoggingInterceptor.Level.NONE + } } ) .addInterceptor(chuckerInterceptor) @@ -108,7 +109,7 @@ object NetworkModule { @Singleton fun provideInterceptor( database: UDatabase, - @Named("webViewUA") userAgent: String + @Named("unesUserAgent") userAgent: String ) = Interceptor { chain -> val request = chain.request() val host = request.url.host @@ -141,7 +142,7 @@ object NetworkModule { val renewed = request.newBuilder().headers(newHeaders).build() chain.proceed(renewed) - } else { + } else { val nRequest = request.newBuilder().addHeader("Accept", "application/json").build() chain.proceed(nRequest) } diff --git a/app/src/main/java/com/forcetower/uefs/core/model/api/UResponse.kt b/app/src/main/java/com/forcetower/uefs/core/model/api/UResponse.kt index 55e8bdd2d..45e13e60c 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/api/UResponse.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/api/UResponse.kt @@ -1,27 +1,27 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.model.api - -data class UResponse ( - val success: Boolean = false, - val message: String? = null, - val data: T? = null -) +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.model.api + +data class UResponse( + val success: Boolean = false, + val message: String? = null, + val data: T? = null +) diff --git a/app/src/main/java/com/forcetower/uefs/core/model/bigtray/BigTrayData.kt b/app/src/main/java/com/forcetower/uefs/core/model/bigtray/BigTrayData.kt index c28170e2e..48a4c303d 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/bigtray/BigTrayData.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/bigtray/BigTrayData.kt @@ -1,133 +1,135 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.model.bigtray - -import androidx.core.math.MathUtils.clamp -import com.forcetower.uefs.feature.shared.extensions.toCalendar -import timber.log.Timber -import java.util.Calendar -import java.util.Calendar.SATURDAY -import java.util.Calendar.SUNDAY -import java.util.Objects - -data class BigTrayData( - val open: Boolean, - val quota: String, - val error: Boolean, - val time: Long, - val type: String -) { - companion object { - const val COFFEE = 1 - const val LUNCH = 2 - const val DINNER = 3 - - fun error() = BigTrayData(false, "", true, System.currentTimeMillis(), "") - fun closed() = BigTrayData(false, "0", false, System.currentTimeMillis(), "") - fun createData(values: List) = BigTrayData(true, values[1].trim(), false, System.currentTimeMillis(), values[0].trim()) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || javaClass != other.javaClass) return false - val that = other as BigTrayData? ?: return false - return open == that.open && - quota == that.quota && - error == that.error && - type == that.type - } - - override fun hashCode(): Int { - return Objects.hash(open, quota, error, type, time) - } -} - -fun BigTrayData.getNextMealType(): Int { - val calendar = this.time.toCalendar() - val hour = calendar.get(Calendar.HOUR_OF_DAY) - val minutes = calendar.get(Calendar.MINUTE) - val account = hour * 60 + minutes - - return when { - account < 9.5 * 60 -> BigTrayData.COFFEE - account < 14.5 * 60 -> BigTrayData.LUNCH - account < 20 * 60 -> BigTrayData.DINNER - else -> BigTrayData.COFFEE - } -} - -fun BigTrayData.getPrice(): String { - val type = getNextMealType() - var amount = 0 - try { - amount = this.quota.toInt() - } catch (e: Exception) {} - - return when (type) { - BigTrayData.COFFEE -> if (amount <= 0) "R$ 4,63" else "R$ 0,50" - BigTrayData.LUNCH -> if (amount <= 0) "R$ 8,56" else "R$ 1,00" - BigTrayData.DINNER -> if (amount <= 0) "R$ 3,94" else "R$ 0,70" - else -> "R$ 1,00" - } -} - -fun BigTrayData.getNextMealTime(): String { - val calendar = this.time.toCalendar() - val day = calendar.get(Calendar.DAY_OF_WEEK) - - when (getNextMealType()) { - BigTrayData.COFFEE -> return if (day == SUNDAY) "07h30min às 09h00min" else "06h30min às 09h00min" - BigTrayData.LUNCH -> { - if (day == SUNDAY) return "11h30min às 13h30min" - return if (day == SATURDAY) "11h30min às 14h00min" else "10h30min às 14h00min" - } - else -> { - if (day == SUNDAY) return "17h30min às 19h00min" - return if (day == SATURDAY) "17h30min às 19h00min" else "17h30min às 19h30min" - } - } -} - -fun BigTrayData.isOpen(): Boolean { - var amount = -1 - try { amount = quota.toInt() } catch (e: Exception) {} - return open && amount != -1 -} - -fun BigTrayData.percentage(): Float { - try { - val amount = quota.toFloat() - val type = getNextMealType() - - return clamp( - amount / when (type) { - BigTrayData.LUNCH -> 1450 - BigTrayData.DINNER -> 490 - else -> 320 - }, - 0f, - 1f - ) * 100 - } catch (e: Exception) { - Timber.d(e.message) - } - return 0.0f -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.model.bigtray + +import androidx.core.math.MathUtils.clamp +import com.forcetower.uefs.feature.shared.extensions.toCalendar +import java.util.Calendar +import java.util.Calendar.SATURDAY +import java.util.Calendar.SUNDAY +import java.util.Objects +import timber.log.Timber + +data class BigTrayData( + val open: Boolean, + val quota: String, + val error: Boolean, + val time: Long, + val type: String +) { + companion object { + const val COFFEE = 1 + const val LUNCH = 2 + const val DINNER = 3 + + fun error() = BigTrayData(false, "", true, System.currentTimeMillis(), "") + fun closed() = BigTrayData(false, "0", false, System.currentTimeMillis(), "") + fun createData(values: List) = BigTrayData(true, values[1].trim(), false, System.currentTimeMillis(), values[0].trim()) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + val that = other as BigTrayData? ?: return false + return open == that.open && + quota == that.quota && + error == that.error && + type == that.type + } + + override fun hashCode(): Int { + return Objects.hash(open, quota, error, type, time) + } +} + +fun BigTrayData.getNextMealType(): Int { + val calendar = this.time.toCalendar() + val hour = calendar.get(Calendar.HOUR_OF_DAY) + val minutes = calendar.get(Calendar.MINUTE) + val account = hour * 60 + minutes + + return when { + account < 9.5 * 60 -> BigTrayData.COFFEE + account < 14.5 * 60 -> BigTrayData.LUNCH + account < 20 * 60 -> BigTrayData.DINNER + else -> BigTrayData.COFFEE + } +} + +fun BigTrayData.getPrice(): String { + val type = getNextMealType() + var amount = 0 + try { + amount = this.quota.toInt() + } catch (e: Exception) {} + + return when (type) { + BigTrayData.COFFEE -> if (amount <= 0) "R$ 4,63" else "R$ 0,50" + BigTrayData.LUNCH -> if (amount <= 0) "R$ 8,56" else "R$ 1,00" + BigTrayData.DINNER -> if (amount <= 0) "R$ 3,94" else "R$ 0,70" + else -> "R$ 1,00" + } +} + +fun BigTrayData.getNextMealTime(): String { + val calendar = this.time.toCalendar() + val day = calendar.get(Calendar.DAY_OF_WEEK) + + when (getNextMealType()) { + BigTrayData.COFFEE -> return if (day == SUNDAY) "07h30min às 09h00min" else "06h30min às 09h00min" + BigTrayData.LUNCH -> { + if (day == SUNDAY) return "11h30min às 13h30min" + return if (day == SATURDAY) "11h30min às 14h00min" else "10h30min às 14h00min" + } + else -> { + if (day == SUNDAY) return "17h30min às 19h00min" + return if (day == SATURDAY) "17h30min às 19h00min" else "17h30min às 19h30min" + } + } +} + +fun BigTrayData.isOpen(): Boolean { + var amount = -1 + try { + amount = quota.toInt() + } catch (e: Exception) {} + return open && amount != -1 +} + +fun BigTrayData.percentage(): Float { + try { + val amount = quota.toFloat() + val type = getNextMealType() + + return clamp( + amount / when (type) { + BigTrayData.LUNCH -> 1450 + BigTrayData.DINNER -> 490 + else -> 320 + }, + 0f, + 1f + ) * 100 + } catch (e: Exception) { + Timber.d(e.message) + } + return 0.0f +} diff --git a/app/src/main/java/com/forcetower/uefs/core/model/edge/account/ServiceAccountDTO.kt b/app/src/main/java/com/forcetower/uefs/core/model/edge/account/ServiceAccountDTO.kt index ee239e2e7..b53513239 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/edge/account/ServiceAccountDTO.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/edge/account/ServiceAccountDTO.kt @@ -11,4 +11,4 @@ data class ServiceAccountDTO( val email: String? = null, @SerializedName("imageUrl") val imageUrl: String? = null -) \ No newline at end of file +) diff --git a/app/src/main/java/com/forcetower/uefs/core/model/edge/auth/EdgeAccessTokenDTO.kt b/app/src/main/java/com/forcetower/uefs/core/model/edge/auth/EdgeAccessTokenDTO.kt index 0763c29e1..c7631bec4 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/edge/auth/EdgeAccessTokenDTO.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/edge/auth/EdgeAccessTokenDTO.kt @@ -5,4 +5,4 @@ import com.google.gson.annotations.SerializedName data class EdgeAccessTokenDTO( @SerializedName("accessToken") val accessToken: String -) \ No newline at end of file +) diff --git a/app/src/main/java/com/forcetower/uefs/core/model/edge/auth/EdgeLoginBody.kt b/app/src/main/java/com/forcetower/uefs/core/model/edge/auth/EdgeLoginBody.kt index 9676f2810..61dde3317 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/edge/auth/EdgeLoginBody.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/edge/auth/EdgeLoginBody.kt @@ -4,4 +4,4 @@ data class EdgeLoginBody( val username: String, val password: String, val provider: String = "SNOWPIERCER" -) \ No newline at end of file +) diff --git a/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/EvaluationHotTopic.kt b/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/EvaluationHotTopic.kt index 7dab72524..f75887a94 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/EvaluationHotTopic.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/EvaluationHotTopic.kt @@ -13,4 +13,4 @@ data class EvaluationHotTopic( val disciplines: List? = null, @SerializedName("teachers") val teachers: List? = null -) \ No newline at end of file +) diff --git a/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/EvaluationSnapshot.kt b/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/EvaluationSnapshot.kt index 7b0a4c912..4eff4063d 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/EvaluationSnapshot.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/EvaluationSnapshot.kt @@ -7,4 +7,4 @@ data class EvaluationSnapshot( val teachers: List, @SerializedName("disciplines") val disciplines: List -) \ No newline at end of file +) diff --git a/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicDisciplineEvaluationCombinedData.kt b/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicDisciplineEvaluationCombinedData.kt index 261e9c6f7..5fa4dbfd8 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicDisciplineEvaluationCombinedData.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicDisciplineEvaluationCombinedData.kt @@ -52,4 +52,4 @@ data class PublicDisciplineEvaluationData( val failed: Int, @SerializedName("quit") val quit: Int -) \ No newline at end of file +) diff --git a/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicDisciplineSnapshot.kt b/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicDisciplineSnapshot.kt index 0dc9b11b0..e087a6227 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicDisciplineSnapshot.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicDisciplineSnapshot.kt @@ -11,4 +11,4 @@ data class PublicDisciplineSnapshot( val code: String, @SerializedName("department") val department: String -) \ No newline at end of file +) diff --git a/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicHotEvaluationDiscipline.kt b/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicHotEvaluationDiscipline.kt index ea509c1b3..45d51a79d 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicHotEvaluationDiscipline.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicHotEvaluationDiscipline.kt @@ -17,4 +17,4 @@ data class PublicHotEvaluationDiscipline( val mean: Double, @SerializedName("studentCount") val studentCount: Int -) \ No newline at end of file +) diff --git a/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicHotEvaluationTeacher.kt b/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicHotEvaluationTeacher.kt index 3e75883d9..b260f4b84 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicHotEvaluationTeacher.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicHotEvaluationTeacher.kt @@ -11,4 +11,4 @@ data class PublicHotEvaluationTeacher( val mean: Double, @SerializedName("studentCount") val studentCount: Int -) \ No newline at end of file +) diff --git a/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicTeacherEvaluationData.kt b/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicTeacherEvaluationData.kt index d00795788..1d55f1d21 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicTeacherEvaluationData.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicTeacherEvaluationData.kt @@ -46,4 +46,4 @@ data class PublicTeacherEvaluationCombinedData( val participant: Boolean, @SerializedName("disciplines") val disciplines: List -) \ No newline at end of file +) diff --git a/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicTeacherSnapshot.kt b/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicTeacherSnapshot.kt index d3ff59f14..f723bceb0 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicTeacherSnapshot.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/edge/paradox/PublicTeacherSnapshot.kt @@ -7,4 +7,4 @@ data class PublicTeacherSnapshot( val id: String, @SerializedName("name") val name: String -) \ No newline at end of file +) diff --git a/app/src/main/java/com/forcetower/uefs/core/model/edge/sync/PublicDisciplineData.kt b/app/src/main/java/com/forcetower/uefs/core/model/edge/sync/PublicDisciplineData.kt index 3494a06a6..4e3f6794a 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/edge/sync/PublicDisciplineData.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/edge/sync/PublicDisciplineData.kt @@ -55,5 +55,5 @@ data class PublicGrade( @SerializedName("date") val date: String?, @SerializedName("grade") - val grade: Double?, -) \ No newline at end of file + val grade: Double? +) diff --git a/app/src/main/java/com/forcetower/uefs/core/model/edge/sync/PublicPlatformMessage.kt b/app/src/main/java/com/forcetower/uefs/core/model/edge/sync/PublicPlatformMessage.kt index bc7d1520e..2b690cdb2 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/edge/sync/PublicPlatformMessage.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/edge/sync/PublicPlatformMessage.kt @@ -27,5 +27,5 @@ data class PublicPlatformMessage( @SerializedName("attachmentName") val attachmentName: String?, @SerializedName("attachmentLink") - val attachmentLink: String?, -) \ No newline at end of file + val attachmentLink: String? +) diff --git a/app/src/main/java/com/forcetower/uefs/core/model/siecomp/Session.kt b/app/src/main/java/com/forcetower/uefs/core/model/siecomp/Session.kt index 75521d71c..e5336eda6 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/siecomp/Session.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/siecomp/Session.kt @@ -56,8 +56,10 @@ data class Session( @Ignore val year = startTime.year + @Ignore val duration = endTime.toInstant().toEpochMilli() - startTime.toInstant().toEpochMilli() + @Ignore val sessionType = when (type) { 0 -> SessionType.SPEAK @@ -80,9 +82,10 @@ data class Session( override fun compareTo(other: Session): Int { val value = startTime.compareTo(other.startTime) - return if (value != 0) + return if (value != 0) { value - else + } else { duration.compareTo(other.duration) + } } } diff --git a/app/src/main/java/com/forcetower/uefs/core/model/siecomp/SessionType.kt b/app/src/main/java/com/forcetower/uefs/core/model/siecomp/SessionType.kt index 3c5d16156..be36ffbfc 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/siecomp/SessionType.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/siecomp/SessionType.kt @@ -21,5 +21,11 @@ package com.forcetower.uefs.core.model.siecomp enum class SessionType { - WORKSHOP, SPEAK, TALK, PROJECT, DEBATE, CONCLUSION, UNKNOWN + WORKSHOP, + SPEAK, + TALK, + PROJECT, + DEBATE, + CONCLUSION, + UNKNOWN } diff --git a/app/src/main/java/com/forcetower/uefs/core/model/ui/edge/EmailLinkComplete.kt b/app/src/main/java/com/forcetower/uefs/core/model/ui/edge/EmailLinkComplete.kt index 20f363b82..88315178d 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/ui/edge/EmailLinkComplete.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/ui/edge/EmailLinkComplete.kt @@ -6,4 +6,4 @@ sealed interface EmailLinkComplete { data object EmailTaken : EmailLinkComplete data object TooManyTries : EmailLinkComplete data object ConnectionError : EmailLinkComplete -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/core/model/ui/edge/EmailLinkStart.kt b/app/src/main/java/com/forcetower/uefs/core/model/ui/edge/EmailLinkStart.kt index 37b325fa7..6d8d657ec 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/ui/edge/EmailLinkStart.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/ui/edge/EmailLinkStart.kt @@ -1,7 +1,7 @@ package com.forcetower.uefs.core.model.ui.edge sealed interface EmailLinkStart { - data class CodeSent(val securityCode: String): EmailLinkStart + data class CodeSent(val securityCode: String) : EmailLinkStart data object InvalidInfo : EmailLinkStart data object ConnectionError : EmailLinkStart -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/core/model/unes/ClassGroupTeacher.kt b/app/src/main/java/com/forcetower/uefs/core/model/unes/ClassGroupTeacher.kt index eb2fc1bc5..3761f9877 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/unes/ClassGroupTeacher.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/unes/ClassGroupTeacher.kt @@ -5,17 +5,20 @@ import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey -@Entity(indices = [ - Index(value = ["classGroupId", "teacherId"], unique = true), - Index(value = ["teacherId"], unique = false), - Index(value = ["classGroupId"], unique = false), -], foreignKeys = [ - ForeignKey(entity = ClassGroup::class, parentColumns = ["uid"], childColumns = ["classGroupId"], onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE), - ForeignKey(entity = Teacher::class, parentColumns = ["uid"], childColumns = ["teacherId"], onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE) -]) +@Entity( + indices = [ + Index(value = ["classGroupId", "teacherId"], unique = true), + Index(value = ["teacherId"], unique = false), + Index(value = ["classGroupId"], unique = false) + ], + foreignKeys = [ + ForeignKey(entity = ClassGroup::class, parentColumns = ["uid"], childColumns = ["classGroupId"], onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE), + ForeignKey(entity = Teacher::class, parentColumns = ["uid"], childColumns = ["teacherId"], onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE) + ] +) data class ClassGroupTeacher( @PrimaryKey(autoGenerate = true) val uid: Int, val classGroupId: Long, val teacherId: Long -) \ No newline at end of file +) diff --git a/app/src/main/java/com/forcetower/uefs/core/model/unes/EdgeAccessToken.kt b/app/src/main/java/com/forcetower/uefs/core/model/unes/EdgeAccessToken.kt index 4937f140b..d50e11eb5 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/unes/EdgeAccessToken.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/unes/EdgeAccessToken.kt @@ -8,4 +8,4 @@ data class EdgeAccessToken( val accessToken: String, @PrimaryKey(autoGenerate = false) val id: Int = 1 -) \ No newline at end of file +) diff --git a/app/src/main/java/com/forcetower/uefs/core/model/unes/EdgeParadoxSearchableItem.kt b/app/src/main/java/com/forcetower/uefs/core/model/unes/EdgeParadoxSearchableItem.kt index 6b520c1ed..0ffc47bda 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/unes/EdgeParadoxSearchableItem.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/unes/EdgeParadoxSearchableItem.kt @@ -4,9 +4,11 @@ import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey -@Entity(indices = [ - Index(value = ["serviceId", "type"]) -]) +@Entity( + indices = [ + Index(value = ["serviceId", "type"]) + ] +) data class EdgeParadoxSearchableItem( @PrimaryKey(autoGenerate = true) val id: Long = 0, @@ -16,10 +18,10 @@ data class EdgeParadoxSearchableItem( val displayImage: String?, val type: Int, val searchable: String?, - val optionalReference: String?, + val optionalReference: String? ) { companion object { const val TEACHER_TYPE = 0 const val DISCIPLINE_TYPE = 1 } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/core/model/unes/GithubContributor.kt b/app/src/main/java/com/forcetower/uefs/core/model/unes/GithubContributor.kt index 728d77d84..f6b9a69a7 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/unes/GithubContributor.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/unes/GithubContributor.kt @@ -34,7 +34,7 @@ data class GithubContributor( @SerializedName("html_url") val htmlUrl: String, @SerializedName("url") - val url: String, + val url: String ) data class GithubUser( diff --git a/app/src/main/java/com/forcetower/uefs/core/model/unes/Semester.kt b/app/src/main/java/com/forcetower/uefs/core/model/unes/Semester.kt index a9f447e2b..af46f509d 100644 --- a/app/src/main/java/com/forcetower/uefs/core/model/unes/Semester.kt +++ b/app/src/main/java/com/forcetower/uefs/core/model/unes/Semester.kt @@ -50,8 +50,11 @@ data class Semester( val str2 = Integer.parseInt(o2.substring(0, 5)) if (str1 == str2) { - if (o1.length > 5) -1 - else 1 + if (o1.length > 5) { + -1 + } else { + 1 + } } else { str1.compareTo(str2) * -1 } diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/cookies/CachedCookiePersistor.kt b/app/src/main/java/com/forcetower/uefs/core/storage/cookies/CachedCookiePersistor.kt index a5199c225..5079c19d8 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/cookies/CachedCookiePersistor.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/cookies/CachedCookiePersistor.kt @@ -24,8 +24,8 @@ import android.content.Context import android.content.SharedPreferences import com.forcetower.sagres.cookies.CookiePersistor import com.forcetower.sagres.cookies.SerializableCookie -import okhttp3.Cookie import java.util.ArrayList +import okhttp3.Cookie class CachedCookiePersistor(context: Context) : CookiePersistor { private val sharedPreferences: SharedPreferences = diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/cookies/PrefsCookiePersistor.kt b/app/src/main/java/com/forcetower/uefs/core/storage/cookies/PrefsCookiePersistor.kt index 97b43ef99..a31f1ce0d 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/cookies/PrefsCookiePersistor.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/cookies/PrefsCookiePersistor.kt @@ -24,8 +24,8 @@ import android.content.Context import android.content.SharedPreferences import com.forcetower.sagres.cookies.CookiePersistor import com.forcetower.sagres.cookies.SerializableCookie -import okhttp3.Cookie import java.util.ArrayList +import okhttp3.Cookie class PrefsCookiePersistor(context: Context) : CookiePersistor { private val sharedPreferences: SharedPreferences = diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/database/UDatabase.kt b/app/src/main/java/com/forcetower/uefs/core/storage/database/UDatabase.kt index f8fb4af75..2d325b648 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/database/UDatabase.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/database/UDatabase.kt @@ -1,201 +1,201 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.database - -import androidx.room.AutoMigration -import androidx.room.Database -import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import com.forcetower.uefs.core.model.unes.Access -import com.forcetower.uefs.core.model.unes.AccessToken -import com.forcetower.uefs.core.model.unes.Account -import com.forcetower.uefs.core.model.unes.AffinityQuestion -import com.forcetower.uefs.core.model.unes.AffinityQuestionAlternative -import com.forcetower.uefs.core.model.unes.CalendarItem -import com.forcetower.uefs.core.model.unes.Class -import com.forcetower.uefs.core.model.unes.ClassAbsence -import com.forcetower.uefs.core.model.unes.ClassGroup -import com.forcetower.uefs.core.model.unes.ClassGroupTeacher -import com.forcetower.uefs.core.model.unes.ClassItem -import com.forcetower.uefs.core.model.unes.ClassLocation -import com.forcetower.uefs.core.model.unes.ClassMaterial -import com.forcetower.uefs.core.model.unes.Contributor -import com.forcetower.uefs.core.model.unes.Course -import com.forcetower.uefs.core.model.unes.Discipline -import com.forcetower.uefs.core.model.unes.EdgeAccessToken -import com.forcetower.uefs.core.model.unes.EdgeParadoxSearchableItem -import com.forcetower.uefs.core.model.unes.EdgeServiceAccount -import com.forcetower.uefs.core.model.unes.EvaluationEntity -import com.forcetower.uefs.core.model.unes.Event -import com.forcetower.uefs.core.model.unes.Flowchart -import com.forcetower.uefs.core.model.unes.FlowchartDiscipline -import com.forcetower.uefs.core.model.unes.FlowchartRequirement -import com.forcetower.uefs.core.model.unes.FlowchartSemester -import com.forcetower.uefs.core.model.unes.Grade -import com.forcetower.uefs.core.model.unes.Message -import com.forcetower.uefs.core.model.unes.Profile -import com.forcetower.uefs.core.model.unes.ProfileStatement -import com.forcetower.uefs.core.model.unes.SDemandOffer -import com.forcetower.uefs.core.model.unes.SDiscipline -import com.forcetower.uefs.core.model.unes.SStudent -import com.forcetower.uefs.core.model.unes.STeacher -import com.forcetower.uefs.core.model.unes.SagresDocument -import com.forcetower.uefs.core.model.unes.SagresFlags -import com.forcetower.uefs.core.model.unes.Semester -import com.forcetower.uefs.core.model.unes.ServiceRequest -import com.forcetower.uefs.core.model.unes.SyncRegistry -import com.forcetower.uefs.core.model.unes.Teacher -import com.forcetower.uefs.core.model.unes.UserSession -import com.forcetower.uefs.core.storage.database.dao.AccessDao -import com.forcetower.uefs.core.storage.database.dao.AccessTokenDao -import com.forcetower.uefs.core.storage.database.dao.AccountDao -import com.forcetower.uefs.core.storage.database.dao.AffinityQuestionDao -import com.forcetower.uefs.core.storage.database.dao.CalendarDao -import com.forcetower.uefs.core.storage.database.dao.ClassAbsenceDao -import com.forcetower.uefs.core.storage.database.dao.ClassDao -import com.forcetower.uefs.core.storage.database.dao.ClassGroupDao -import com.forcetower.uefs.core.storage.database.dao.ClassGroupTeacherDao -import com.forcetower.uefs.core.storage.database.dao.ClassItemDao -import com.forcetower.uefs.core.storage.database.dao.ClassLocationDao -import com.forcetower.uefs.core.storage.database.dao.ClassMaterialDao -import com.forcetower.uefs.core.storage.database.dao.ContributorDao -import com.forcetower.uefs.core.storage.database.dao.CourseDao -import com.forcetower.uefs.core.storage.database.dao.DemandOfferDao -import com.forcetower.uefs.core.storage.database.dao.DisciplineDao -import com.forcetower.uefs.core.storage.database.dao.DisciplineServiceDao -import com.forcetower.uefs.core.storage.database.dao.DocumentDao -import com.forcetower.uefs.core.storage.database.dao.EdgeAccessTokenDao -import com.forcetower.uefs.core.storage.database.dao.EdgeParadoxSearchableItemDao -import com.forcetower.uefs.core.storage.database.dao.EdgeServiceAccountDao -import com.forcetower.uefs.core.storage.database.dao.EvaluationEntitiesDao -import com.forcetower.uefs.core.storage.database.dao.EventDao -import com.forcetower.uefs.core.storage.database.dao.FlagsDao -import com.forcetower.uefs.core.storage.database.dao.FlowchartDao -import com.forcetower.uefs.core.storage.database.dao.FlowchartDisciplineDao -import com.forcetower.uefs.core.storage.database.dao.FlowchartRequirementDao -import com.forcetower.uefs.core.storage.database.dao.FlowchartSemesterDao -import com.forcetower.uefs.core.storage.database.dao.GradeDao -import com.forcetower.uefs.core.storage.database.dao.MessageDao -import com.forcetower.uefs.core.storage.database.dao.ProfileDao -import com.forcetower.uefs.core.storage.database.dao.ProfileStatementDao -import com.forcetower.uefs.core.storage.database.dao.SemesterDao -import com.forcetower.uefs.core.storage.database.dao.ServiceRequestDao -import com.forcetower.uefs.core.storage.database.dao.StudentServiceDao -import com.forcetower.uefs.core.storage.database.dao.SyncRegistryDao -import com.forcetower.uefs.core.storage.database.dao.TeacherDao -import com.forcetower.uefs.core.storage.database.dao.TeacherServiceDao -import com.forcetower.uefs.core.storage.database.dao.UserSessionDao -import com.forcetower.uefs.core.util.Converters - -@Database( - entities = [ - AccessToken::class, - Access::class, - Profile::class, - Semester::class, - Message::class, - CalendarItem::class, - Discipline::class, - Class::class, - ClassGroup::class, - ClassAbsence::class, - ClassLocation::class, - ClassItem::class, - ClassMaterial::class, - Grade::class, - Course::class, - SagresDocument::class, - SyncRegistry::class, - Teacher::class, - SDemandOffer::class, - SagresFlags::class, - Contributor::class, - ServiceRequest::class, - Account::class, - STeacher::class, - SDiscipline::class, - SStudent::class, - EvaluationEntity::class, - Flowchart::class, - FlowchartSemester::class, - FlowchartDiscipline::class, - FlowchartRequirement::class, - ProfileStatement::class, - UserSession::class, - AffinityQuestion::class, - AffinityQuestionAlternative::class, - Event::class, - ClassGroupTeacher::class, - EdgeAccessToken::class, - EdgeServiceAccount::class, - EdgeParadoxSearchableItem::class - ], - version = 56, - exportSchema = true, - autoMigrations = [ - AutoMigration(from = 53, to = 54), - AutoMigration(from = 54, to = 55), - AutoMigration(from = 55, to = 56), - ] -) -@TypeConverters(value = [Converters::class]) -abstract class UDatabase : RoomDatabase() { - abstract fun accessDao(): AccessDao - abstract fun accessTokenDao(): AccessTokenDao - abstract fun profileDao(): ProfileDao - abstract fun messageDao(): MessageDao - abstract fun semesterDao(): SemesterDao - abstract fun calendarDao(): CalendarDao - abstract fun disciplineDao(): DisciplineDao - abstract fun classDao(): ClassDao - abstract fun teacherDao(): TeacherDao - abstract fun classGroupDao(): ClassGroupDao - abstract fun classAbsenceDao(): ClassAbsenceDao - abstract fun classLocationDao(): ClassLocationDao - abstract fun gradesDao(): GradeDao - abstract fun courseDao(): CourseDao - abstract fun documentDao(): DocumentDao - abstract fun syncRegistryDao(): SyncRegistryDao - abstract fun classMaterialDao(): ClassMaterialDao - abstract fun classItemDao(): ClassItemDao - abstract fun demandOfferDao(): DemandOfferDao - abstract fun flagsDao(): FlagsDao - abstract fun contributorDao(): ContributorDao - abstract fun serviceRequestDao(): ServiceRequestDao - abstract fun accountDao(): AccountDao - abstract fun disciplineServiceDao(): DisciplineServiceDao - abstract fun teacherServiceDao(): TeacherServiceDao - abstract fun studentServiceDao(): StudentServiceDao - abstract fun evaluationEntitiesDao(): EvaluationEntitiesDao - abstract fun flowchartDao(): FlowchartDao - abstract fun flowchartSemesterDao(): FlowchartSemesterDao - abstract fun flowchartDisciplineDao(): FlowchartDisciplineDao - abstract fun flowchartRequirementDao(): FlowchartRequirementDao - abstract fun statementDao(): ProfileStatementDao - abstract fun userSessionDao(): UserSessionDao - abstract fun affinityQuestion(): AffinityQuestionDao - abstract fun eventDao(): EventDao - abstract fun classGroupTeacher(): ClassGroupTeacherDao - - abstract val edgeAccessToken: EdgeAccessTokenDao - abstract val edgeServiceAccount: EdgeServiceAccountDao - abstract val edgeParadoxSearchableItem: EdgeParadoxSearchableItemDao -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.database + +import androidx.room.AutoMigration +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.forcetower.uefs.core.model.unes.Access +import com.forcetower.uefs.core.model.unes.AccessToken +import com.forcetower.uefs.core.model.unes.Account +import com.forcetower.uefs.core.model.unes.AffinityQuestion +import com.forcetower.uefs.core.model.unes.AffinityQuestionAlternative +import com.forcetower.uefs.core.model.unes.CalendarItem +import com.forcetower.uefs.core.model.unes.Class +import com.forcetower.uefs.core.model.unes.ClassAbsence +import com.forcetower.uefs.core.model.unes.ClassGroup +import com.forcetower.uefs.core.model.unes.ClassGroupTeacher +import com.forcetower.uefs.core.model.unes.ClassItem +import com.forcetower.uefs.core.model.unes.ClassLocation +import com.forcetower.uefs.core.model.unes.ClassMaterial +import com.forcetower.uefs.core.model.unes.Contributor +import com.forcetower.uefs.core.model.unes.Course +import com.forcetower.uefs.core.model.unes.Discipline +import com.forcetower.uefs.core.model.unes.EdgeAccessToken +import com.forcetower.uefs.core.model.unes.EdgeParadoxSearchableItem +import com.forcetower.uefs.core.model.unes.EdgeServiceAccount +import com.forcetower.uefs.core.model.unes.EvaluationEntity +import com.forcetower.uefs.core.model.unes.Event +import com.forcetower.uefs.core.model.unes.Flowchart +import com.forcetower.uefs.core.model.unes.FlowchartDiscipline +import com.forcetower.uefs.core.model.unes.FlowchartRequirement +import com.forcetower.uefs.core.model.unes.FlowchartSemester +import com.forcetower.uefs.core.model.unes.Grade +import com.forcetower.uefs.core.model.unes.Message +import com.forcetower.uefs.core.model.unes.Profile +import com.forcetower.uefs.core.model.unes.ProfileStatement +import com.forcetower.uefs.core.model.unes.SDemandOffer +import com.forcetower.uefs.core.model.unes.SDiscipline +import com.forcetower.uefs.core.model.unes.SStudent +import com.forcetower.uefs.core.model.unes.STeacher +import com.forcetower.uefs.core.model.unes.SagresDocument +import com.forcetower.uefs.core.model.unes.SagresFlags +import com.forcetower.uefs.core.model.unes.Semester +import com.forcetower.uefs.core.model.unes.ServiceRequest +import com.forcetower.uefs.core.model.unes.SyncRegistry +import com.forcetower.uefs.core.model.unes.Teacher +import com.forcetower.uefs.core.model.unes.UserSession +import com.forcetower.uefs.core.storage.database.dao.AccessDao +import com.forcetower.uefs.core.storage.database.dao.AccessTokenDao +import com.forcetower.uefs.core.storage.database.dao.AccountDao +import com.forcetower.uefs.core.storage.database.dao.AffinityQuestionDao +import com.forcetower.uefs.core.storage.database.dao.CalendarDao +import com.forcetower.uefs.core.storage.database.dao.ClassAbsenceDao +import com.forcetower.uefs.core.storage.database.dao.ClassDao +import com.forcetower.uefs.core.storage.database.dao.ClassGroupDao +import com.forcetower.uefs.core.storage.database.dao.ClassGroupTeacherDao +import com.forcetower.uefs.core.storage.database.dao.ClassItemDao +import com.forcetower.uefs.core.storage.database.dao.ClassLocationDao +import com.forcetower.uefs.core.storage.database.dao.ClassMaterialDao +import com.forcetower.uefs.core.storage.database.dao.ContributorDao +import com.forcetower.uefs.core.storage.database.dao.CourseDao +import com.forcetower.uefs.core.storage.database.dao.DemandOfferDao +import com.forcetower.uefs.core.storage.database.dao.DisciplineDao +import com.forcetower.uefs.core.storage.database.dao.DisciplineServiceDao +import com.forcetower.uefs.core.storage.database.dao.DocumentDao +import com.forcetower.uefs.core.storage.database.dao.EdgeAccessTokenDao +import com.forcetower.uefs.core.storage.database.dao.EdgeParadoxSearchableItemDao +import com.forcetower.uefs.core.storage.database.dao.EdgeServiceAccountDao +import com.forcetower.uefs.core.storage.database.dao.EvaluationEntitiesDao +import com.forcetower.uefs.core.storage.database.dao.EventDao +import com.forcetower.uefs.core.storage.database.dao.FlagsDao +import com.forcetower.uefs.core.storage.database.dao.FlowchartDao +import com.forcetower.uefs.core.storage.database.dao.FlowchartDisciplineDao +import com.forcetower.uefs.core.storage.database.dao.FlowchartRequirementDao +import com.forcetower.uefs.core.storage.database.dao.FlowchartSemesterDao +import com.forcetower.uefs.core.storage.database.dao.GradeDao +import com.forcetower.uefs.core.storage.database.dao.MessageDao +import com.forcetower.uefs.core.storage.database.dao.ProfileDao +import com.forcetower.uefs.core.storage.database.dao.ProfileStatementDao +import com.forcetower.uefs.core.storage.database.dao.SemesterDao +import com.forcetower.uefs.core.storage.database.dao.ServiceRequestDao +import com.forcetower.uefs.core.storage.database.dao.StudentServiceDao +import com.forcetower.uefs.core.storage.database.dao.SyncRegistryDao +import com.forcetower.uefs.core.storage.database.dao.TeacherDao +import com.forcetower.uefs.core.storage.database.dao.TeacherServiceDao +import com.forcetower.uefs.core.storage.database.dao.UserSessionDao +import com.forcetower.uefs.core.util.Converters + +@Database( + entities = [ + AccessToken::class, + Access::class, + Profile::class, + Semester::class, + Message::class, + CalendarItem::class, + Discipline::class, + Class::class, + ClassGroup::class, + ClassAbsence::class, + ClassLocation::class, + ClassItem::class, + ClassMaterial::class, + Grade::class, + Course::class, + SagresDocument::class, + SyncRegistry::class, + Teacher::class, + SDemandOffer::class, + SagresFlags::class, + Contributor::class, + ServiceRequest::class, + Account::class, + STeacher::class, + SDiscipline::class, + SStudent::class, + EvaluationEntity::class, + Flowchart::class, + FlowchartSemester::class, + FlowchartDiscipline::class, + FlowchartRequirement::class, + ProfileStatement::class, + UserSession::class, + AffinityQuestion::class, + AffinityQuestionAlternative::class, + Event::class, + ClassGroupTeacher::class, + EdgeAccessToken::class, + EdgeServiceAccount::class, + EdgeParadoxSearchableItem::class + ], + version = 56, + exportSchema = true, + autoMigrations = [ + AutoMigration(from = 53, to = 54), + AutoMigration(from = 54, to = 55), + AutoMigration(from = 55, to = 56) + ] +) +@TypeConverters(value = [Converters::class]) +abstract class UDatabase : RoomDatabase() { + abstract fun accessDao(): AccessDao + abstract fun accessTokenDao(): AccessTokenDao + abstract fun profileDao(): ProfileDao + abstract fun messageDao(): MessageDao + abstract fun semesterDao(): SemesterDao + abstract fun calendarDao(): CalendarDao + abstract fun disciplineDao(): DisciplineDao + abstract fun classDao(): ClassDao + abstract fun teacherDao(): TeacherDao + abstract fun classGroupDao(): ClassGroupDao + abstract fun classAbsenceDao(): ClassAbsenceDao + abstract fun classLocationDao(): ClassLocationDao + abstract fun gradesDao(): GradeDao + abstract fun courseDao(): CourseDao + abstract fun documentDao(): DocumentDao + abstract fun syncRegistryDao(): SyncRegistryDao + abstract fun classMaterialDao(): ClassMaterialDao + abstract fun classItemDao(): ClassItemDao + abstract fun demandOfferDao(): DemandOfferDao + abstract fun flagsDao(): FlagsDao + abstract fun contributorDao(): ContributorDao + abstract fun serviceRequestDao(): ServiceRequestDao + abstract fun accountDao(): AccountDao + abstract fun disciplineServiceDao(): DisciplineServiceDao + abstract fun teacherServiceDao(): TeacherServiceDao + abstract fun studentServiceDao(): StudentServiceDao + abstract fun evaluationEntitiesDao(): EvaluationEntitiesDao + abstract fun flowchartDao(): FlowchartDao + abstract fun flowchartSemesterDao(): FlowchartSemesterDao + abstract fun flowchartDisciplineDao(): FlowchartDisciplineDao + abstract fun flowchartRequirementDao(): FlowchartRequirementDao + abstract fun statementDao(): ProfileStatementDao + abstract fun userSessionDao(): UserSessionDao + abstract fun affinityQuestion(): AffinityQuestionDao + abstract fun eventDao(): EventDao + abstract fun classGroupTeacher(): ClassGroupTeacherDao + + abstract val edgeAccessToken: EdgeAccessTokenDao + abstract val edgeServiceAccount: EdgeServiceAccountDao + abstract val edgeParadoxSearchableItem: EdgeParadoxSearchableItemDao +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/database/aggregation/ClassGroupWithTeachers.kt b/app/src/main/java/com/forcetower/uefs/core/storage/database/aggregation/ClassGroupWithTeachers.kt index 009347ef6..6f102b16a 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/database/aggregation/ClassGroupWithTeachers.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/database/aggregation/ClassGroupWithTeachers.kt @@ -17,4 +17,4 @@ data class ClassGroupWithTeachers( associateBy = Junction(value = AffinityQuestionAlternative::class, parentColumn = "classGroupId", entityColumn = "teacherId") ) val teachers: List -) \ No newline at end of file +) diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/ClassGroupTeacherDao.kt b/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/ClassGroupTeacherDao.kt index e851badf5..1f0d762f6 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/ClassGroupTeacherDao.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/ClassGroupTeacherDao.kt @@ -9,4 +9,4 @@ import com.forcetower.uefs.core.model.unes.ClassGroupTeacher abstract class ClassGroupTeacherDao : BaseDao() { @Query("DELETE FROM ClassGroupTeacher WHERE classGroupId = :classGroupId") abstract suspend fun deleteAllFromClassGroup(classGroupId: Long) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/EdgeAccessTokenDao.kt b/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/EdgeAccessTokenDao.kt index 82f67c8bb..764c30cd0 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/EdgeAccessTokenDao.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/EdgeAccessTokenDao.kt @@ -16,4 +16,4 @@ abstract class EdgeAccessTokenDao : BaseDao() { @Query("DELETE FROM EdgeAccessToken") abstract suspend fun deleteAll() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/EdgeParadoxSearchableItemDao.kt b/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/EdgeParadoxSearchableItemDao.kt index 70f1bcdf0..804705e46 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/EdgeParadoxSearchableItemDao.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/EdgeParadoxSearchableItemDao.kt @@ -68,4 +68,4 @@ abstract class EdgeParadoxSearchableItemDao : BaseDao @Query("SELECT * FROM EdgeParadoxSearchableItem WHERE 0") abstract fun doQueryEmpty(): PagingSource -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/FlowchartDao.kt b/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/FlowchartDao.kt index 94989f1e2..900c30031 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/FlowchartDao.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/FlowchartDao.kt @@ -32,8 +32,8 @@ import com.forcetower.uefs.core.model.unes.Flowchart import com.forcetower.uefs.core.model.unes.FlowchartDiscipline import com.forcetower.uefs.core.model.unes.FlowchartRequirement import com.forcetower.uefs.core.model.unes.FlowchartSemester -import timber.log.Timber import java.util.Calendar +import timber.log.Timber @Dao abstract class FlowchartDao { diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/FlowchartDisciplineDao.kt b/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/FlowchartDisciplineDao.kt index e9a691399..0f9a31c10 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/FlowchartDisciplineDao.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/FlowchartDisciplineDao.kt @@ -30,8 +30,10 @@ import com.forcetower.uefs.core.model.unes.FlowchartSemester interface FlowchartDisciplineDao { @Query("SELECT fd.id AS id, fd.type AS type, fd.mandatory AS mandatory, fd.completed AS completed, fd.participating AS participating, d.name AS name, d.code AS code, d.credits AS credits, d.department AS department, d.resume AS program FROM FlowchartDiscipline fd INNER JOIN Discipline d ON d.uid = fd.disciplineId WHERE fd.semesterId = :semesterId") fun getDecoratedList(semesterId: Long): LiveData> + @Query("SELECT fd.id AS id, fd.type AS type, fd.mandatory AS mandatory, fd.completed AS completed, fd.participating AS participating, d.name AS name, d.code AS code, d.credits AS credits, d.department AS department, d.resume AS program FROM FlowchartDiscipline fd INNER JOIN Discipline d ON d.uid = fd.disciplineId WHERE fd.id = :disciplineId") fun getDecorated(disciplineId: Long): LiveData + @Query("SELECT s.* FROM FlowchartSemester s INNER JOIN FlowchartDiscipline d ON d.semesterId = s.id WHERE d.id = :disciplineId GROUP BY s.name LIMIT 1") fun getSemesterFromDiscipline(disciplineId: Long): LiveData } diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/GradeDao.kt b/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/GradeDao.kt index 4d78c7af5..f167c7ae1 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/GradeDao.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/GradeDao.kt @@ -36,8 +36,8 @@ import com.forcetower.uefs.core.model.unes.Profile import com.forcetower.uefs.core.model.unes.Semester import com.forcetower.uefs.core.storage.database.aggregation.GradeWithClassStudent import dev.forcetower.breaker.model.ClassEvaluation -import timber.log.Timber import java.time.ZonedDateTime +import timber.log.Timber @Dao abstract class GradeDao { @@ -131,10 +131,12 @@ abstract class GradeDao { clazz.uid = id } if (clazz.uid > 0) { - if (score != null) + if (score != null) { updateClassScore(clazz.uid, score) - if (partialScore != null) + } + if (partialScore != null) { updateClassPartialScore(clazz.uid, partialScore) + } prepareInsertion(clazz, it, notify) } @@ -164,9 +166,13 @@ abstract class GradeDao { if (grade == null) { grade = g } else { - if (g.hasGrade()) grade = g - else if (g.hasDate() && grade.hasDate() && g.date != grade.date) grade = g - else Timber.d("This grade was ignored ${g.name}_${g.grade}") + if (g.hasGrade()) { + grade = g + } else if (g.hasDate() && grade.hasDate() && g.date != grade.date) { + grade = g + } else { + Timber.d("This grade was ignored ${g.name}_${g.grade}") + } } values["${g.grouping}<><>${g.name}"] = grade } diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/MessageDao.kt b/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/MessageDao.kt index 7a51062c1..0d6782e0e 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/MessageDao.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/MessageDao.kt @@ -1,189 +1,192 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.database.dao - -import androidx.lifecycle.LiveData -import androidx.paging.PagingSource -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import androidx.room.Transaction -import com.forcetower.uefs.core.model.unes.Message -import timber.log.Timber -import java.util.Locale - -@Dao -abstract class MessageDao { - @Query("DELETE FROM Message") - abstract suspend fun deleteAllSuspend() - - @Transaction - open fun insertIgnoring(messages: List) { - updateOldMessages() - for (message in messages) { - val direct = if (message.html) { - getMessageByHashDirect(message.hashMessage) - } else { - getMessageDirect(message.sagresId) - } - if (direct != null) { - if (message.senderName != null) { - if (direct.senderName.isNullOrBlank()) { - updateSenderName(message.sagresId, message.senderName) - } - } - - // mark message as edited? - if (!message.html && message.content.isNotBlank()) { - updateContent(message.sagresId, message.content) - } - - if (message.discipline != null) { - updateDisciplineName(message.sagresId, message.discipline) - } - - if (message.codeDiscipline != null) { - updateDisciplineCode(direct.sagresId, message.codeDiscipline) - } - - if (message.attachmentLink != null) { - updateAttachmentLink(message.sagresId, message.attachmentLink) - } - - if (message.attachmentName != null) { - updateAttachmentName(message.sagresId, message.attachmentName) - } - - if (message.html && direct.html) { - updateDateString(message.sagresId, message.dateString) - } - - if (direct.html && !message.html) { - Timber.d("Is this really happening?") - updateTimestamp(direct.sagresId, message.timestamp) - updateSenderProfile(direct.sagresId, message.senderProfile) - updateHtmlParseStatus(direct.sagresId, false) - - if (message.senderName != null) - updateSenderName(direct.sagresId, message.senderName) - if (message.discipline != null) - updateDisciplineName(direct.sagresId, message.discipline) - } - } - val resume = message.disciplineResume?.trim() - val code = message.codeDiscipline?.trim() - if (!resume.isNullOrBlank() && !code.isNullOrBlank()) { - updateDisciplineResume(code, resume) - } - } - - insertIgnore(messages) - } - - @Query("SELECT * FROM Message WHERE hash_message = :hashMessage") - protected abstract fun getMessageByHashDirect(hashMessage: Long?): Message? - - private fun updateOldMessages() { - val messages = getAllUndefinedMessages() - messages.forEach { message -> - val hash = message.content.lowercase(Locale.getDefault()).trim().hashCode().toLong() - val existing = getMessageByHashDirect(hash) - if (existing == null) setMessageHash(message.uid, hash) - else { - deleteMessage(message.uid) - Timber.e("Collision of messages ${existing.senderName} and ${message.codeDiscipline}") - } - } - } - - @Query("DELETE FROM Message WHERE uid = :uid") - protected abstract fun deleteMessage(uid: Long) - - @Query("UPDATE Message SET hash_message = :hash WHERE uid = :uid") - protected abstract fun setMessageHash(uid: Long, hash: Long) - - @Query("SELECT * FROM Message WHERE hash_message IS NULL") - protected abstract fun getAllUndefinedMessages(): List - - @Query("UPDATE Message SET date_string = :dateString WHERE sagres_id = :sagresId") - protected abstract fun updateDateString(sagresId: Long, dateString: String?) - - @Query("UPDATE Discipline SET resume = :resume WHERE LOWER(code) = LOWER(:code)") - protected abstract fun updateDisciplineResume(code: String, resume: String) - - @Query("UPDATE Message SET discipline = :discipline WHERE sagres_id = :sagresId") - protected abstract fun updateDisciplineName(sagresId: Long, discipline: String) - - @Query("UPDATE Message SET sender_name = :senderName WHERE sagres_id = :sagresId") - protected abstract fun updateSenderName(sagresId: Long, senderName: String) - - @Query("UPDATE Message SET attachmentLink = :attachmentLink WHERE sagres_id = :sagresId") - protected abstract fun updateAttachmentLink(sagresId: Long, attachmentLink: String) - - @Query("UPDATE Message SET attachmentName = :attachmentName WHERE sagres_id = :sagresId") - protected abstract fun updateAttachmentName(sagresId: Long, attachmentName: String) - - @Query("UPDATE Message SET code_discipline = :codeDiscipline WHERE sagres_id = :sagresId") - protected abstract fun updateDisciplineCode(sagresId: Long, codeDiscipline: String) - - @Query("UPDATE Message SET html = :html WHERE sagres_id = :sagresId") - protected abstract fun updateHtmlParseStatus(sagresId: Long, html: Boolean) - - @Query("UPDATE Message SET sender_profile = :senderProfile WHERE sagres_id = :sagresId") - protected abstract fun updateSenderProfile(sagresId: Long, senderProfile: Int) - - @Query("UPDATE Message SET timestamp = :timestamp WHERE sagres_id = :sagresId") - protected abstract fun updateTimestamp(sagresId: Long, timestamp: Long) - - @Query("UPDATE Message SET content = :content WHERE sagres_id = :sagresId") - protected abstract fun updateContent(sagresId: Long, content: String) - - @Insert(onConflict = OnConflictStrategy.IGNORE) - protected abstract fun insertIgnore(messages: List) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - protected abstract fun insertReplace(messages: List) - - @Query("SELECT * FROM Message WHERE sagres_id = :sagresId") - abstract fun getMessageDirect(sagresId: Long): Message? - - @Query("SELECT * FROM Message ORDER BY timestamp DESC") - abstract fun getAllMessages(): LiveData> - - @Query("SELECT * FROM Message ORDER BY timestamp DESC LIMIT 1") - abstract fun getLastMessage(): LiveData - - @Query("SELECT * FROM Message ORDER BY timestamp DESC") - abstract fun getAllMessagesPaged(): PagingSource - - @Query("SELECT * FROM Message WHERE notified = 0") - abstract fun getNewMessages(): List - - @Query("UPDATE Message SET notified = 1") - abstract fun setAllNotified() - - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insert(message: Message): Long - - @Query("DELETE FROM Message") - abstract fun deleteAll() -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.database.dao + +import androidx.lifecycle.LiveData +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import com.forcetower.uefs.core.model.unes.Message +import java.util.Locale +import timber.log.Timber + +@Dao +abstract class MessageDao { + @Query("DELETE FROM Message") + abstract suspend fun deleteAllSuspend() + + @Transaction + open fun insertIgnoring(messages: List) { + updateOldMessages() + for (message in messages) { + val direct = if (message.html) { + getMessageByHashDirect(message.hashMessage) + } else { + getMessageDirect(message.sagresId) + } + if (direct != null) { + if (message.senderName != null) { + if (direct.senderName.isNullOrBlank()) { + updateSenderName(message.sagresId, message.senderName) + } + } + + // mark message as edited? + if (!message.html && message.content.isNotBlank()) { + updateContent(message.sagresId, message.content) + } + + if (message.discipline != null) { + updateDisciplineName(message.sagresId, message.discipline) + } + + if (message.codeDiscipline != null) { + updateDisciplineCode(direct.sagresId, message.codeDiscipline) + } + + if (message.attachmentLink != null) { + updateAttachmentLink(message.sagresId, message.attachmentLink) + } + + if (message.attachmentName != null) { + updateAttachmentName(message.sagresId, message.attachmentName) + } + + if (message.html && direct.html) { + updateDateString(message.sagresId, message.dateString) + } + + if (direct.html && !message.html) { + Timber.d("Is this really happening?") + updateTimestamp(direct.sagresId, message.timestamp) + updateSenderProfile(direct.sagresId, message.senderProfile) + updateHtmlParseStatus(direct.sagresId, false) + + if (message.senderName != null) { + updateSenderName(direct.sagresId, message.senderName) + } + if (message.discipline != null) { + updateDisciplineName(direct.sagresId, message.discipline) + } + } + } + val resume = message.disciplineResume?.trim() + val code = message.codeDiscipline?.trim() + if (!resume.isNullOrBlank() && !code.isNullOrBlank()) { + updateDisciplineResume(code, resume) + } + } + + insertIgnore(messages) + } + + @Query("SELECT * FROM Message WHERE hash_message = :hashMessage") + protected abstract fun getMessageByHashDirect(hashMessage: Long?): Message? + + private fun updateOldMessages() { + val messages = getAllUndefinedMessages() + messages.forEach { message -> + val hash = message.content.lowercase(Locale.getDefault()).trim().hashCode().toLong() + val existing = getMessageByHashDirect(hash) + if (existing == null) { + setMessageHash(message.uid, hash) + } else { + deleteMessage(message.uid) + Timber.e("Collision of messages ${existing.senderName} and ${message.codeDiscipline}") + } + } + } + + @Query("DELETE FROM Message WHERE uid = :uid") + protected abstract fun deleteMessage(uid: Long) + + @Query("UPDATE Message SET hash_message = :hash WHERE uid = :uid") + protected abstract fun setMessageHash(uid: Long, hash: Long) + + @Query("SELECT * FROM Message WHERE hash_message IS NULL") + protected abstract fun getAllUndefinedMessages(): List + + @Query("UPDATE Message SET date_string = :dateString WHERE sagres_id = :sagresId") + protected abstract fun updateDateString(sagresId: Long, dateString: String?) + + @Query("UPDATE Discipline SET resume = :resume WHERE LOWER(code) = LOWER(:code)") + protected abstract fun updateDisciplineResume(code: String, resume: String) + + @Query("UPDATE Message SET discipline = :discipline WHERE sagres_id = :sagresId") + protected abstract fun updateDisciplineName(sagresId: Long, discipline: String) + + @Query("UPDATE Message SET sender_name = :senderName WHERE sagres_id = :sagresId") + protected abstract fun updateSenderName(sagresId: Long, senderName: String) + + @Query("UPDATE Message SET attachmentLink = :attachmentLink WHERE sagres_id = :sagresId") + protected abstract fun updateAttachmentLink(sagresId: Long, attachmentLink: String) + + @Query("UPDATE Message SET attachmentName = :attachmentName WHERE sagres_id = :sagresId") + protected abstract fun updateAttachmentName(sagresId: Long, attachmentName: String) + + @Query("UPDATE Message SET code_discipline = :codeDiscipline WHERE sagres_id = :sagresId") + protected abstract fun updateDisciplineCode(sagresId: Long, codeDiscipline: String) + + @Query("UPDATE Message SET html = :html WHERE sagres_id = :sagresId") + protected abstract fun updateHtmlParseStatus(sagresId: Long, html: Boolean) + + @Query("UPDATE Message SET sender_profile = :senderProfile WHERE sagres_id = :sagresId") + protected abstract fun updateSenderProfile(sagresId: Long, senderProfile: Int) + + @Query("UPDATE Message SET timestamp = :timestamp WHERE sagres_id = :sagresId") + protected abstract fun updateTimestamp(sagresId: Long, timestamp: Long) + + @Query("UPDATE Message SET content = :content WHERE sagres_id = :sagresId") + protected abstract fun updateContent(sagresId: Long, content: String) + + @Insert(onConflict = OnConflictStrategy.IGNORE) + protected abstract fun insertIgnore(messages: List) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + protected abstract fun insertReplace(messages: List) + + @Query("SELECT * FROM Message WHERE sagres_id = :sagresId") + abstract fun getMessageDirect(sagresId: Long): Message? + + @Query("SELECT * FROM Message ORDER BY timestamp DESC") + abstract fun getAllMessages(): LiveData> + + @Query("SELECT * FROM Message ORDER BY timestamp DESC LIMIT 1") + abstract fun getLastMessage(): LiveData + + @Query("SELECT * FROM Message ORDER BY timestamp DESC") + abstract fun getAllMessagesPaged(): PagingSource + + @Query("SELECT * FROM Message WHERE notified = 0") + abstract fun getNewMessages(): List + + @Query("UPDATE Message SET notified = 1") + abstract fun setAllNotified() + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun insert(message: Message): Long + + @Query("DELETE FROM Message") + abstract fun deleteAll() +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/ProfileDao.kt b/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/ProfileDao.kt index 3b0fd4f31..23748023d 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/ProfileDao.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/ProfileDao.kt @@ -1,126 +1,126 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.database.dao - -import androidx.lifecycle.LiveData -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import androidx.room.Transaction -import com.forcetower.core.utils.WordUtils -import com.forcetower.sagres.database.model.SagresPerson -import com.forcetower.uefs.core.model.unes.Profile -import dev.forcetower.breaker.model.Person -import kotlinx.coroutines.flow.Flow -import timber.log.Timber -import java.util.Locale - -@Dao -abstract class ProfileDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insert(profile: Profile): Long - - @Query("SELECT * FROM Profile WHERE me = 1 LIMIT 1") - abstract fun selectMeDirect(): Profile? - - @Query("SELECT * FROM Profile WHERE me = 1 LIMIT 1") - abstract fun selectMe(): LiveData - - @Query("SELECT * FROM Profile WHERE me = 1 LIMIT 1") - abstract fun me(): Flow - - @Transaction - open fun insert(person: SagresPerson, score: Double = -1.0) { - val name = WordUtils.toTitleCase(person.name?.trim()) ?: "" - var profile = selectMeDirect() - if (profile != null) { - updateProfileName(name) - updateProfileMockStatus(person.isMocked) - if (!person.isMocked) { - updateProfileEmail(person.email?.trim() ?: "") - Timber.d("Updating profile sagres id to ${person.id} ${person.sagresId}") - updateProfileSagresId(person.id) - } - if (score >= 0) updateScore(score) - } else { - profile = Profile(name = name, email = person.email?.trim(), sagresId = person.id, me = true, score = score) - insert(profile) - } - } - - @Transaction - open suspend fun insert(person: Person): Long { - val name = WordUtils.toTitleCase(person.name) ?: "" - val me = selectMeDirect() - if (me != null) { - updateProfileName(name) - updateProfileMockStatus(false) - updateProfileSagresId(person.id) - person.email?.lowercase(Locale.getDefault())?.let { - updateProfileEmail(it) - } - return me.uid - } else { - return insert( - Profile( - name = name, - email = person.email, - sagresId = person.id, - me = true - ) - ) - } - } - - @Query("SELECT c.name FROM Profile p, Course c WHERE p.course IS NOT NULL AND p.course = c.id LIMIT 1") - abstract fun getProfileCourse(): LiveData - - @Query("SELECT c.name FROM Profile p, Course c WHERE p.course IS NOT NULL AND p.course = c.id LIMIT 1") - abstract fun getProfileCourseDirect(): String? - - @Query("UPDATE Profile SET score = :score") - abstract fun updateScore(score: Double) - - @Query("UPDATE Profile SET mocked = :mocked") - abstract fun updateProfileMockStatus(mocked: Boolean) - - @Query("UPDATE Profile SET name = :name") - abstract fun updateProfileName(name: String) - - @Query("UPDATE Profile SET email = :email") - abstract fun updateProfileEmail(email: String) - - @Query("UPDATE Profile SET sagres_id = :sagresId") - abstract fun updateProfileSagresId(sagresId: Long) - - @Query("DELETE FROM Profile WHERE me = 1") - abstract fun deleteMe() - - @Query("SELECT * FROM Profile WHERE uuid = :profileUUID LIMIT 1") - abstract fun selectProfileByUUID(profileUUID: String): LiveData - - @Query("UPDATE Profile SET course = :courseId WHERE me = 1") - abstract fun updateCourse(courseId: Long) - - @Query("UPDATE Profile SET calc_score = :score WHERE me = 1") - abstract fun updateCalculatedScore(score: Double) -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.database.dao + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import com.forcetower.core.utils.WordUtils +import com.forcetower.sagres.database.model.SagresPerson +import com.forcetower.uefs.core.model.unes.Profile +import dev.forcetower.breaker.model.Person +import java.util.Locale +import kotlinx.coroutines.flow.Flow +import timber.log.Timber + +@Dao +abstract class ProfileDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun insert(profile: Profile): Long + + @Query("SELECT * FROM Profile WHERE me = 1 LIMIT 1") + abstract fun selectMeDirect(): Profile? + + @Query("SELECT * FROM Profile WHERE me = 1 LIMIT 1") + abstract fun selectMe(): LiveData + + @Query("SELECT * FROM Profile WHERE me = 1 LIMIT 1") + abstract fun me(): Flow + + @Transaction + open fun insert(person: SagresPerson, score: Double = -1.0) { + val name = WordUtils.toTitleCase(person.name?.trim()) ?: "" + var profile = selectMeDirect() + if (profile != null) { + updateProfileName(name) + updateProfileMockStatus(person.isMocked) + if (!person.isMocked) { + updateProfileEmail(person.email?.trim() ?: "") + Timber.d("Updating profile sagres id to ${person.id} ${person.sagresId}") + updateProfileSagresId(person.id) + } + if (score >= 0) updateScore(score) + } else { + profile = Profile(name = name, email = person.email?.trim(), sagresId = person.id, me = true, score = score) + insert(profile) + } + } + + @Transaction + open suspend fun insert(person: Person): Long { + val name = WordUtils.toTitleCase(person.name) ?: "" + val me = selectMeDirect() + if (me != null) { + updateProfileName(name) + updateProfileMockStatus(false) + updateProfileSagresId(person.id) + person.email?.lowercase(Locale.getDefault())?.let { + updateProfileEmail(it) + } + return me.uid + } else { + return insert( + Profile( + name = name, + email = person.email, + sagresId = person.id, + me = true + ) + ) + } + } + + @Query("SELECT c.name FROM Profile p, Course c WHERE p.course IS NOT NULL AND p.course = c.id LIMIT 1") + abstract fun getProfileCourse(): LiveData + + @Query("SELECT c.name FROM Profile p, Course c WHERE p.course IS NOT NULL AND p.course = c.id LIMIT 1") + abstract fun getProfileCourseDirect(): String? + + @Query("UPDATE Profile SET score = :score") + abstract fun updateScore(score: Double) + + @Query("UPDATE Profile SET mocked = :mocked") + abstract fun updateProfileMockStatus(mocked: Boolean) + + @Query("UPDATE Profile SET name = :name") + abstract fun updateProfileName(name: String) + + @Query("UPDATE Profile SET email = :email") + abstract fun updateProfileEmail(email: String) + + @Query("UPDATE Profile SET sagres_id = :sagresId") + abstract fun updateProfileSagresId(sagresId: Long) + + @Query("DELETE FROM Profile WHERE me = 1") + abstract fun deleteMe() + + @Query("SELECT * FROM Profile WHERE uuid = :profileUUID LIMIT 1") + abstract fun selectProfileByUUID(profileUUID: String): LiveData + + @Query("UPDATE Profile SET course = :courseId WHERE me = 1") + abstract fun updateCourse(courseId: Long) + + @Query("UPDATE Profile SET calc_score = :score WHERE me = 1") + abstract fun updateCalculatedScore(score: Double) +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/SemesterDao.kt b/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/SemesterDao.kt index d2b83f350..d8bebfd76 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/SemesterDao.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/database/dao/SemesterDao.kt @@ -1,102 +1,107 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.database.dao - -import androidx.lifecycle.LiveData -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import androidx.room.Transaction -import com.forcetower.uefs.core.model.unes.Semester - -@Dao -abstract class SemesterDao { - @Transaction - open fun insertIgnoring(semesters: List) { - val newSemesters = semesters.filter { semester -> - val current = getSemesterDirect(semester.sagresId) - if (current != null) { - if (current.start != semester.start && semester.start != null) - updateStart(current.sagresId, semester.start) - - if (current.end != semester.end && semester.end != null) - updateEnd(current.sagresId, semester.end) - - if (current.startClass != semester.startClass && semester.startClass != null) - updateStartClass(current.sagresId, semester.startClass) - - if (current.endClass != semester.endClass && semester.endClass != null) - updateEndClass(current.sagresId, semester.endClass) - - if (current.name != semester.name) - updateName(current.sagresId, semester.name) - } - - // keep only the new semesters - // don't bother try inserting if we just updated above - current == null - } - internalInsertIgnoring(newSemesters) - } - - @Query("UPDATE Semester SET start = :start WHERE sagres_id = :sagresId") - protected abstract fun updateStart(sagresId: Long, start: Long) - - @Query("UPDATE Semester SET `end` = :end WHERE sagres_id = :sagresId") - protected abstract fun updateEnd(sagresId: Long, end: Long) - - @Query("UPDATE Semester SET start_class = :startClass WHERE sagres_id = :sagresId") - protected abstract fun updateStartClass(sagresId: Long, startClass: Long) - - @Query("UPDATE Semester SET end_class = :endClass WHERE sagres_id = :sagresId") - protected abstract fun updateEndClass(sagresId: Long, endClass: Long) - - @Query("UPDATE Semester SET name = :name WHERE sagres_id = :sagresId") - protected abstract fun updateName(sagresId: Long, name: String) - - @Query("SELECT * FROM Semester WHERE sagres_id = :sagresId") - abstract fun getSemesterDirect(sagresId: Long): Semester? - - @Query("SELECT * FROM Semester WHERE sagres_id = :sagresId") - abstract suspend fun getSemesterDirectSuspend(sagresId: Long): Semester? - - @Insert(onConflict = OnConflictStrategy.IGNORE) - protected abstract fun internalInsertIgnoring(semesters: List) - - @Insert(onConflict = OnConflictStrategy.IGNORE) - abstract fun insertIgnoring(semester: Semester) - - @Query("SELECT * FROM Semester ORDER BY sagres_id DESC") - abstract fun getParticipatingSemesters(): LiveData> - - @Query("SELECT * FROM Semester ORDER BY sagres_id DESC") - abstract suspend fun getParticipatingSemestersDirect(): List - - @Query("DELETE FROM Semester") - abstract fun deleteAll() - - @Query("SELECT * FROM Semester ORDER BY sagres_id DESC") - abstract fun getSemestersDirect(): List - - @Insert(onConflict = OnConflictStrategy.IGNORE) - abstract suspend fun insertIgnoreSuspend(semester: Semester) -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.database.dao + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import com.forcetower.uefs.core.model.unes.Semester + +@Dao +abstract class SemesterDao { + @Transaction + open fun insertIgnoring(semesters: List) { + val newSemesters = semesters.filter { semester -> + val current = getSemesterDirect(semester.sagresId) + if (current != null) { + if (current.start != semester.start && semester.start != null) { + updateStart(current.sagresId, semester.start) + } + + if (current.end != semester.end && semester.end != null) { + updateEnd(current.sagresId, semester.end) + } + + if (current.startClass != semester.startClass && semester.startClass != null) { + updateStartClass(current.sagresId, semester.startClass) + } + + if (current.endClass != semester.endClass && semester.endClass != null) { + updateEndClass(current.sagresId, semester.endClass) + } + + if (current.name != semester.name) { + updateName(current.sagresId, semester.name) + } + } + + // keep only the new semesters + // don't bother try inserting if we just updated above + current == null + } + internalInsertIgnoring(newSemesters) + } + + @Query("UPDATE Semester SET start = :start WHERE sagres_id = :sagresId") + protected abstract fun updateStart(sagresId: Long, start: Long) + + @Query("UPDATE Semester SET `end` = :end WHERE sagres_id = :sagresId") + protected abstract fun updateEnd(sagresId: Long, end: Long) + + @Query("UPDATE Semester SET start_class = :startClass WHERE sagres_id = :sagresId") + protected abstract fun updateStartClass(sagresId: Long, startClass: Long) + + @Query("UPDATE Semester SET end_class = :endClass WHERE sagres_id = :sagresId") + protected abstract fun updateEndClass(sagresId: Long, endClass: Long) + + @Query("UPDATE Semester SET name = :name WHERE sagres_id = :sagresId") + protected abstract fun updateName(sagresId: Long, name: String) + + @Query("SELECT * FROM Semester WHERE sagres_id = :sagresId") + abstract fun getSemesterDirect(sagresId: Long): Semester? + + @Query("SELECT * FROM Semester WHERE sagres_id = :sagresId") + abstract suspend fun getSemesterDirectSuspend(sagresId: Long): Semester? + + @Insert(onConflict = OnConflictStrategy.IGNORE) + protected abstract fun internalInsertIgnoring(semesters: List) + + @Insert(onConflict = OnConflictStrategy.IGNORE) + abstract fun insertIgnoring(semester: Semester) + + @Query("SELECT * FROM Semester ORDER BY sagres_id DESC") + abstract fun getParticipatingSemesters(): LiveData> + + @Query("SELECT * FROM Semester ORDER BY sagres_id DESC") + abstract suspend fun getParticipatingSemestersDirect(): List + + @Query("DELETE FROM Semester") + abstract fun deleteAll() + + @Query("SELECT * FROM Semester ORDER BY sagres_id DESC") + abstract fun getSemestersDirect(): List + + @Insert(onConflict = OnConflictStrategy.IGNORE) + abstract suspend fun insertIgnoreSuspend(semester: Semester) +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/eventdatabase/accessors/SessionSpeakerTalker.kt b/app/src/main/java/com/forcetower/uefs/core/storage/eventdatabase/accessors/SessionSpeakerTalker.kt index c90d4c3aa..3665f5eee 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/eventdatabase/accessors/SessionSpeakerTalker.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/eventdatabase/accessors/SessionSpeakerTalker.kt @@ -28,6 +28,7 @@ import com.forcetower.uefs.core.model.siecomp.Speaker class SessionSpeakerTalker { @Embedded lateinit var data: SessionSpeaker + @Relation(entityColumn = "uid", parentColumn = "speaker_id") lateinit var speakers: List diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/eventdatabase/accessors/SessionTagged.kt b/app/src/main/java/com/forcetower/uefs/core/storage/eventdatabase/accessors/SessionTagged.kt index 608e390a6..939715caf 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/eventdatabase/accessors/SessionTagged.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/eventdatabase/accessors/SessionTagged.kt @@ -28,6 +28,7 @@ import com.forcetower.uefs.core.model.siecomp.Tag class SessionTagged { @Embedded lateinit var data: SessionTag + @Relation(entityColumn = "uid", parentColumn = "tag_id") lateinit var tag: List diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/eventdatabase/accessors/SessionWithData.kt b/app/src/main/java/com/forcetower/uefs/core/storage/eventdatabase/accessors/SessionWithData.kt index 05becbc50..3ea9e8c61 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/eventdatabase/accessors/SessionWithData.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/eventdatabase/accessors/SessionWithData.kt @@ -1,47 +1,50 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.eventdatabase.accessors - -import androidx.room.Embedded -import androidx.room.Relation -import com.forcetower.uefs.core.model.siecomp.Session -import com.forcetower.uefs.core.model.siecomp.SessionSpeaker -import com.forcetower.uefs.core.model.siecomp.SessionStar -import com.forcetower.uefs.core.model.siecomp.SessionTag - -class SessionWithData : Comparable { - @Embedded - lateinit var session: Session - @Relation(entityColumn = "session_id", parentColumn = "uid", entity = SessionSpeaker::class) - lateinit var speakersRel: List - @Relation(entityColumn = "session_id", parentColumn = "uid", entity = SessionTag::class) - lateinit var displayTags: List - @Relation(entityColumn = "session_id", parentColumn = "uid") - lateinit var stars: List - - fun tags() = displayTags.map { it.singleTag() }.filter { !it.internal } - fun speakers() = speakersRel.map { it.singleSpeaker() } - fun isStarred() = stars.isNotEmpty() - - override fun compareTo(other: SessionWithData): Int { - return session.compareTo(other.session) - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.eventdatabase.accessors + +import androidx.room.Embedded +import androidx.room.Relation +import com.forcetower.uefs.core.model.siecomp.Session +import com.forcetower.uefs.core.model.siecomp.SessionSpeaker +import com.forcetower.uefs.core.model.siecomp.SessionStar +import com.forcetower.uefs.core.model.siecomp.SessionTag + +class SessionWithData : Comparable { + @Embedded + lateinit var session: Session + + @Relation(entityColumn = "session_id", parentColumn = "uid", entity = SessionSpeaker::class) + lateinit var speakersRel: List + + @Relation(entityColumn = "session_id", parentColumn = "uid", entity = SessionTag::class) + lateinit var displayTags: List + + @Relation(entityColumn = "session_id", parentColumn = "uid") + lateinit var stars: List + + fun tags() = displayTags.map { it.singleTag() }.filter { !it.internal } + fun speakers() = speakersRel.map { it.singleSpeaker() } + fun isStarred() = stars.isNotEmpty() + + override fun compareTo(other: SessionWithData): Int { + return session.compareTo(other.session) + } +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/imgur/ImageUploader.kt b/app/src/main/java/com/forcetower/uefs/core/storage/imgur/ImageUploader.kt index 651749dfb..5c177d827 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/imgur/ImageUploader.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/imgur/ImageUploader.kt @@ -1,60 +1,60 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.imgur - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.net.Uri -import android.util.Base64 -import com.forcetower.uefs.core.model.api.ImgurUpload -import com.forcetower.uefs.core.util.ImgurUploader -import okhttp3.OkHttpClient -import timber.log.Timber -import java.io.ByteArrayOutputStream -import java.io.InputStream -import java.util.UUID - -object ImageUploader { - fun uploadToImGur( - uri: Uri, - context: Context, - client: OkHttpClient, - name: String = UUID.randomUUID().toString() - ): ImgurUpload? { - val resolver = context.applicationContext.contentResolver - val stream: InputStream - try { - stream = resolver.openInputStream(uri) ?: throw Exception("Failed to load stream") - } catch (exception: Throwable) { - Timber.e(exception, "Error uploading image...") - return null - } - - val bitmap = BitmapFactory.decodeStream(stream) - - val baos = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos) - val data = baos.toByteArray() - val encoded = Base64.encodeToString(data, Base64.DEFAULT) - return ImgurUploader.upload(client, encoded, name) - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.imgur + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.util.Base64 +import com.forcetower.uefs.core.model.api.ImgurUpload +import com.forcetower.uefs.core.util.ImgurUploader +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.util.UUID +import okhttp3.OkHttpClient +import timber.log.Timber + +object ImageUploader { + fun uploadToImGur( + uri: Uri, + context: Context, + client: OkHttpClient, + name: String = UUID.randomUUID().toString() + ): ImgurUpload? { + val resolver = context.applicationContext.contentResolver + val stream: InputStream + try { + stream = resolver.openInputStream(uri) ?: throw Exception("Failed to load stream") + } catch (exception: Throwable) { + Timber.e(exception, "Error uploading image...") + return null + } + + val bitmap = BitmapFactory.decodeStream(stream) + + val baos = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos) + val data = baos.toByteArray() + val encoded = Base64.encodeToString(data, Base64.DEFAULT) + return ImgurUploader.upload(client, encoded, name) + } +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/network/EdgeService.kt b/app/src/main/java/com/forcetower/uefs/core/storage/network/EdgeService.kt index f16746e68..3c8be08e3 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/network/EdgeService.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/network/EdgeService.kt @@ -1,7 +1,10 @@ package com.forcetower.uefs.core.storage.network -import com.forcetower.uefs.core.model.edge.auth.AssertionData +import com.forcetower.uefs.core.model.edge.ServiceResponseWrapper import com.forcetower.uefs.core.model.edge.account.ChangePictureDTO +import com.forcetower.uefs.core.model.edge.account.SendMessagingTokenDTO +import com.forcetower.uefs.core.model.edge.account.ServiceAccountDTO +import com.forcetower.uefs.core.model.edge.auth.AssertionData import com.forcetower.uefs.core.model.edge.auth.CompleteAssertionData import com.forcetower.uefs.core.model.edge.auth.EdgeAccessTokenDTO import com.forcetower.uefs.core.model.edge.auth.EdgeLoginBody @@ -10,9 +13,6 @@ import com.forcetower.uefs.core.model.edge.auth.EmailLinkConfirmDTO import com.forcetower.uefs.core.model.edge.auth.LinkEmailResponseDTO import com.forcetower.uefs.core.model.edge.auth.RegisterPasskeyCredential import com.forcetower.uefs.core.model.edge.auth.RegisterPasskeyStart -import com.forcetower.uefs.core.model.edge.account.SendMessagingTokenDTO -import com.forcetower.uefs.core.model.edge.account.ServiceAccountDTO -import com.forcetower.uefs.core.model.edge.ServiceResponseWrapper import com.forcetower.uefs.core.model.edge.sync.PublicDisciplineData import com.forcetower.uefs.core.model.edge.sync.PublicPlatformMessage import retrofit2.Response diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/network/ParadoxService.kt b/app/src/main/java/com/forcetower/uefs/core/storage/network/ParadoxService.kt index 9d39541b5..c8d3becae 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/network/ParadoxService.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/network/ParadoxService.kt @@ -20,4 +20,4 @@ interface ParadoxService { @GET("evaluation/discipline") suspend fun discipline(@Query("id") id: String): ServiceResponseWrapper -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/network/UService.kt b/app/src/main/java/com/forcetower/uefs/core/storage/network/UService.kt index 7310af36f..9eb57c7c4 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/network/UService.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/network/UService.kt @@ -1,275 +1,275 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.network - -import com.forcetower.sagres.SagresNavigator -import com.forcetower.uefs.core.constants.Constants -import com.forcetower.uefs.core.model.api.DarkInvite -import com.forcetower.uefs.core.model.api.DarkUnlock -import com.forcetower.uefs.core.model.api.EverythingSnippet -import com.forcetower.uefs.core.model.api.ImgurUpload -import com.forcetower.uefs.core.model.api.UResponse -import com.forcetower.uefs.core.model.service.Achievement -import com.forcetower.uefs.core.model.service.AffinityQuestionAnswer -import com.forcetower.uefs.core.model.service.AffinityQuestionDTO -import com.forcetower.uefs.core.model.service.EvaluationDiscipline -import com.forcetower.uefs.core.model.service.EvaluationHomeTopic -import com.forcetower.uefs.core.model.service.EvaluationTeacher -import com.forcetower.uefs.core.model.service.FlowchartDTO -import com.forcetower.uefs.core.model.service.SavedCookie -import com.forcetower.uefs.core.model.service.UNESUpdate -import com.forcetower.uefs.core.model.service.UserSessionDTO -import com.forcetower.uefs.core.model.service.discipline.DisciplineDetailsData -import com.forcetower.uefs.core.model.siecomp.ServerSession -import com.forcetower.uefs.core.model.siecomp.Speaker -import com.forcetower.uefs.core.model.unes.Access -import com.forcetower.uefs.core.model.unes.AccessToken -import com.forcetower.uefs.core.model.unes.Account -import com.forcetower.uefs.core.model.unes.Course -import com.forcetower.uefs.core.model.unes.CreateStatementParams -import com.forcetower.uefs.core.model.unes.Event -import com.forcetower.uefs.core.model.unes.Flowchart -import com.forcetower.uefs.core.model.unes.Profile -import com.forcetower.uefs.core.model.unes.ProfileStatement -import com.forcetower.uefs.core.model.unes.Question -import com.forcetower.uefs.core.model.unes.SStudentDTO -import retrofit2.Call -import retrofit2.Response -import retrofit2.http.Body -import retrofit2.http.Field -import retrofit2.http.FormUrlEncoded -import retrofit2.http.GET -import retrofit2.http.POST -import retrofit2.http.Query -import java.util.Locale - -interface UService { - @POST("oauth/token") - @FormUrlEncoded - fun loginWithSagres( - @Field("username") username: String, - @Field("password") password: String, - @Field("grant_type") grant: String = "sagres", - @Field("client_id") client: String = Constants.SERVICE_CLIENT_ID, - @Field("client_secret") secret: String = Constants.SERVICE_CLIENT_SECRET, - @Field("institution") institution: String = SagresNavigator.instance.getSelectedInstitution().lowercase(Locale.ROOT) - ): Call - - @POST("oauth/token") - @FormUrlEncoded - fun login( - @Field("username") username: String, - @Field("password") password: String, - @Field("grant_type") grant: String = "password", - @Field("client_id") client: String = Constants.SERVICE_CLIENT_ID, - @Field("client_secret") secret: String = Constants.SERVICE_CLIENT_SECRET - ): Call - - @POST("oauth/token") - @FormUrlEncoded - fun loginWithBiscuit( - @Field("username") username: String, - @Field("password") password: String, - @Field("auth") auth: String, - @Field("session_id") session: String, - @Field("grant_type") grant: String = "biscuit", - @Field("client_id") client: String = Constants.SERVICE_CLIENT_ID, - @Field("client_secret") secret: String = Constants.SERVICE_CLIENT_SECRET, - @Field("institution") institution: String = SagresNavigator.instance.getSelectedInstitution().lowercase(Locale.ROOT) - ): Call - - @POST("oauth/token") - @FormUrlEncoded - suspend fun loginWithSagresSuspend( - @Field("username") username: String, - @Field("password") password: String, - @Field("grant_type") grant: String = "sagres", - @Field("client_id") client: String = Constants.SERVICE_CLIENT_ID, - @Field("client_secret") secret: String = Constants.SERVICE_CLIENT_SECRET, - @Field("institution") institution: String = SagresNavigator.instance.getSelectedInstitution().lowercase(Locale.ROOT) - ): AccessToken - - @POST("oauth/token") - @FormUrlEncoded - suspend fun loginSuspend( - @Field("username") username: String, - @Field("password") password: String, - @Field("grant_type") grant: String = "password", - @Field("client_id") client: String = Constants.SERVICE_CLIENT_ID, - @Field("client_secret") secret: String = Constants.SERVICE_CLIENT_SECRET - ): AccessToken - - @POST("oauth/token") - @FormUrlEncoded - suspend fun loginWithBiscuitSuspend( - @Field("username") username: String, - @Field("password") password: String, - @Field("auth") auth: String, - @Field("session_id") session: String, - @Field("grant_type") grant: String = "biscuit", - @Field("client_id") client: String = Constants.SERVICE_CLIENT_ID, - @Field("client_secret") secret: String = Constants.SERVICE_CLIENT_SECRET, - @Field("institution") institution: String = SagresNavigator.instance.getSelectedInstitution().lowercase(Locale.ROOT) - ): AccessToken - - @POST("account/credentials") - fun setupAccount(@Body access: Access): Call> - - @POST("account/profile") - fun setupProfile(@Body profile: Profile): Call> - - @POST("account/update_fcm") - fun sendToken(@Body data: Map): Call> - - @POST("account/update_fcm") - suspend fun sendTokenSuspend(@Body data: Map): UResponse - - @GET("account") - fun getAccount(): Call - - @POST("account/image") - fun updateProfileImage(@Body data: ImgurUpload): Call> - - @POST("account/darktheme") - fun requestDarkThemeUnlock(@Body invites: DarkUnlock): Call> - - @POST("account/darktheme/invite") - fun requestDarkSendTo(@Body invite: DarkInvite): Call> - - @GET("account/statements") - fun getStatements(@Query("profile_id") studentId: Long): Call>> - - @POST("account/statements/create") - fun sendStatement(@Body params: CreateStatementParams): Call - - @POST("account/statements/approve") - fun acceptStatement(@Body body: Map): Call - - @POST("account/statements/refuse") - fun refuseStatement(@Body body: Map): Call - - @POST("account/statements/delete") - fun deleteStatement(@Body body: Map): Call - - @POST("account/save_sessions") - fun saveSessions(@Body session: UserSessionDTO): Call - - @GET("courses") - suspend fun getCourses(): List - - @GET("synchronization") - fun getUpdate(): Call - - @POST("grades") - fun sendGrades(@Body grades: DisciplineDetailsData): Call> - - // -------- Evaluation --------- - @GET("evaluation/hot") - fun getEvaluationTopics(): Call> - - @GET("evaluation/discipline") - fun getEvaluationDiscipline(@Query("department") department: String, @Query("code") code: String): Call - - @GET("evaluation/teacher") - fun getTeacherById(@Query("id") teacherId: Long): Call - - @GET("evaluation/teacher") - fun getTeacherByName(@Query("name") teacherName: String): Call - - @GET("evaluation/question/teacher") - fun getQuestionsForTeachers(@Query("teacher_id") teacherId: Long): Call> - - @GET("evaluation/question/discipline") - fun getQuestionsForDisciplines(@Query("code") code: String, @Query("department") department: String): Call> - - @POST("evaluation/question/answer") - fun answerQuestion(@Body data: MutableMap): Call> - - @GET("evaluation/everythingship") - fun getEvaluationSnippetData(): Call - - // --------- Flowchart --------- - - @GET("flowchart") - fun getFlowcharts(): Call>> - - @GET("flowchart") - fun getFlowchart(@Query("course_id") course: Long): Call> - - // --------- Social ------------- - - @GET("student") - fun getStudent(@Query("student_id") studentId: Long): Call> - - @GET("student/me") - fun getMeStudent(): Call> - - // --------- Achievements --------- - @GET("adventure/achievement") - fun getServerAchievements(): Call>> - - // ----------- SIECOMP ------------ - - @GET("siecomp/list_sessions") - fun siecompSessions(): Call> - - @POST("siecomp/speaker") - fun createSpeaker(@Body speaker: Speaker): Call - - @POST("siecomp/edit_speaker") - fun updateSpeaker(@Body speaker: Speaker): Call - - // ---------- Affinity ----------- - @GET("affinity/questions") - fun affinityQuestions(): Call>> - - @POST("affinity/answer") - fun answerAffinity(@Body answer: AffinityQuestionAnswer): Call> - - // --------- General Events --------- - @GET("events") - suspend fun events(): UResponse> - - @POST("events/create") - suspend fun sendEvent(@Body event: Event): UResponse - - @FormUrlEncoded - @POST("events/approve") - suspend fun approveEvent(@Field("id") id: Long): UResponse - - @FormUrlEncoded - @POST("events/delete") - suspend fun deleteEvent(@Field("id") id: Long): UResponse - - // ---------- Cookies for everyone ----------- - @POST("biscuit/save") - suspend fun prepareSession(@Body cookie: SavedCookie): UResponse - - @GET("biscuit/retrieve") - suspend fun getSession(): UResponse - - @POST("biscuit/invalidate") - suspend fun invalidateSession(): UResponse - - // ------------ ping -------------- - @GET("hi") - suspend fun hi(): Response> -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.network + +import com.forcetower.sagres.SagresNavigator +import com.forcetower.uefs.core.constants.Constants +import com.forcetower.uefs.core.model.api.DarkInvite +import com.forcetower.uefs.core.model.api.DarkUnlock +import com.forcetower.uefs.core.model.api.EverythingSnippet +import com.forcetower.uefs.core.model.api.ImgurUpload +import com.forcetower.uefs.core.model.api.UResponse +import com.forcetower.uefs.core.model.service.Achievement +import com.forcetower.uefs.core.model.service.AffinityQuestionAnswer +import com.forcetower.uefs.core.model.service.AffinityQuestionDTO +import com.forcetower.uefs.core.model.service.EvaluationDiscipline +import com.forcetower.uefs.core.model.service.EvaluationHomeTopic +import com.forcetower.uefs.core.model.service.EvaluationTeacher +import com.forcetower.uefs.core.model.service.FlowchartDTO +import com.forcetower.uefs.core.model.service.SavedCookie +import com.forcetower.uefs.core.model.service.UNESUpdate +import com.forcetower.uefs.core.model.service.UserSessionDTO +import com.forcetower.uefs.core.model.service.discipline.DisciplineDetailsData +import com.forcetower.uefs.core.model.siecomp.ServerSession +import com.forcetower.uefs.core.model.siecomp.Speaker +import com.forcetower.uefs.core.model.unes.Access +import com.forcetower.uefs.core.model.unes.AccessToken +import com.forcetower.uefs.core.model.unes.Account +import com.forcetower.uefs.core.model.unes.Course +import com.forcetower.uefs.core.model.unes.CreateStatementParams +import com.forcetower.uefs.core.model.unes.Event +import com.forcetower.uefs.core.model.unes.Flowchart +import com.forcetower.uefs.core.model.unes.Profile +import com.forcetower.uefs.core.model.unes.ProfileStatement +import com.forcetower.uefs.core.model.unes.Question +import com.forcetower.uefs.core.model.unes.SStudentDTO +import java.util.Locale +import retrofit2.Call +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.Field +import retrofit2.http.FormUrlEncoded +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Query + +interface UService { + @POST("oauth/token") + @FormUrlEncoded + fun loginWithSagres( + @Field("username") username: String, + @Field("password") password: String, + @Field("grant_type") grant: String = "sagres", + @Field("client_id") client: String = Constants.SERVICE_CLIENT_ID, + @Field("client_secret") secret: String = Constants.SERVICE_CLIENT_SECRET, + @Field("institution") institution: String = SagresNavigator.instance.getSelectedInstitution().lowercase(Locale.ROOT) + ): Call + + @POST("oauth/token") + @FormUrlEncoded + fun login( + @Field("username") username: String, + @Field("password") password: String, + @Field("grant_type") grant: String = "password", + @Field("client_id") client: String = Constants.SERVICE_CLIENT_ID, + @Field("client_secret") secret: String = Constants.SERVICE_CLIENT_SECRET + ): Call + + @POST("oauth/token") + @FormUrlEncoded + fun loginWithBiscuit( + @Field("username") username: String, + @Field("password") password: String, + @Field("auth") auth: String, + @Field("session_id") session: String, + @Field("grant_type") grant: String = "biscuit", + @Field("client_id") client: String = Constants.SERVICE_CLIENT_ID, + @Field("client_secret") secret: String = Constants.SERVICE_CLIENT_SECRET, + @Field("institution") institution: String = SagresNavigator.instance.getSelectedInstitution().lowercase(Locale.ROOT) + ): Call + + @POST("oauth/token") + @FormUrlEncoded + suspend fun loginWithSagresSuspend( + @Field("username") username: String, + @Field("password") password: String, + @Field("grant_type") grant: String = "sagres", + @Field("client_id") client: String = Constants.SERVICE_CLIENT_ID, + @Field("client_secret") secret: String = Constants.SERVICE_CLIENT_SECRET, + @Field("institution") institution: String = SagresNavigator.instance.getSelectedInstitution().lowercase(Locale.ROOT) + ): AccessToken + + @POST("oauth/token") + @FormUrlEncoded + suspend fun loginSuspend( + @Field("username") username: String, + @Field("password") password: String, + @Field("grant_type") grant: String = "password", + @Field("client_id") client: String = Constants.SERVICE_CLIENT_ID, + @Field("client_secret") secret: String = Constants.SERVICE_CLIENT_SECRET + ): AccessToken + + @POST("oauth/token") + @FormUrlEncoded + suspend fun loginWithBiscuitSuspend( + @Field("username") username: String, + @Field("password") password: String, + @Field("auth") auth: String, + @Field("session_id") session: String, + @Field("grant_type") grant: String = "biscuit", + @Field("client_id") client: String = Constants.SERVICE_CLIENT_ID, + @Field("client_secret") secret: String = Constants.SERVICE_CLIENT_SECRET, + @Field("institution") institution: String = SagresNavigator.instance.getSelectedInstitution().lowercase(Locale.ROOT) + ): AccessToken + + @POST("account/credentials") + fun setupAccount(@Body access: Access): Call> + + @POST("account/profile") + fun setupProfile(@Body profile: Profile): Call> + + @POST("account/update_fcm") + fun sendToken(@Body data: Map): Call> + + @POST("account/update_fcm") + suspend fun sendTokenSuspend(@Body data: Map): UResponse + + @GET("account") + fun getAccount(): Call + + @POST("account/image") + fun updateProfileImage(@Body data: ImgurUpload): Call> + + @POST("account/darktheme") + fun requestDarkThemeUnlock(@Body invites: DarkUnlock): Call> + + @POST("account/darktheme/invite") + fun requestDarkSendTo(@Body invite: DarkInvite): Call> + + @GET("account/statements") + fun getStatements(@Query("profile_id") studentId: Long): Call>> + + @POST("account/statements/create") + fun sendStatement(@Body params: CreateStatementParams): Call + + @POST("account/statements/approve") + fun acceptStatement(@Body body: Map): Call + + @POST("account/statements/refuse") + fun refuseStatement(@Body body: Map): Call + + @POST("account/statements/delete") + fun deleteStatement(@Body body: Map): Call + + @POST("account/save_sessions") + fun saveSessions(@Body session: UserSessionDTO): Call + + @GET("courses") + suspend fun getCourses(): List + + @GET("synchronization") + fun getUpdate(): Call + + @POST("grades") + fun sendGrades(@Body grades: DisciplineDetailsData): Call> + + // -------- Evaluation --------- + @GET("evaluation/hot") + fun getEvaluationTopics(): Call> + + @GET("evaluation/discipline") + fun getEvaluationDiscipline(@Query("department") department: String, @Query("code") code: String): Call + + @GET("evaluation/teacher") + fun getTeacherById(@Query("id") teacherId: Long): Call + + @GET("evaluation/teacher") + fun getTeacherByName(@Query("name") teacherName: String): Call + + @GET("evaluation/question/teacher") + fun getQuestionsForTeachers(@Query("teacher_id") teacherId: Long): Call> + + @GET("evaluation/question/discipline") + fun getQuestionsForDisciplines(@Query("code") code: String, @Query("department") department: String): Call> + + @POST("evaluation/question/answer") + fun answerQuestion(@Body data: MutableMap): Call> + + @GET("evaluation/everythingship") + fun getEvaluationSnippetData(): Call + + // --------- Flowchart --------- + + @GET("flowchart") + fun getFlowcharts(): Call>> + + @GET("flowchart") + fun getFlowchart(@Query("course_id") course: Long): Call> + + // --------- Social ------------- + + @GET("student") + fun getStudent(@Query("student_id") studentId: Long): Call> + + @GET("student/me") + fun getMeStudent(): Call> + + // --------- Achievements --------- + @GET("adventure/achievement") + fun getServerAchievements(): Call>> + + // ----------- SIECOMP ------------ + + @GET("siecomp/list_sessions") + fun siecompSessions(): Call> + + @POST("siecomp/speaker") + fun createSpeaker(@Body speaker: Speaker): Call + + @POST("siecomp/edit_speaker") + fun updateSpeaker(@Body speaker: Speaker): Call + + // ---------- Affinity ----------- + @GET("affinity/questions") + fun affinityQuestions(): Call>> + + @POST("affinity/answer") + fun answerAffinity(@Body answer: AffinityQuestionAnswer): Call> + + // --------- General Events --------- + @GET("events") + suspend fun events(): UResponse> + + @POST("events/create") + suspend fun sendEvent(@Body event: Event): UResponse + + @FormUrlEncoded + @POST("events/approve") + suspend fun approveEvent(@Field("id") id: Long): UResponse + + @FormUrlEncoded + @POST("events/delete") + suspend fun deleteEvent(@Field("id") id: Long): UResponse + + // ---------- Cookies for everyone ----------- + @POST("biscuit/save") + suspend fun prepareSession(@Body cookie: SavedCookie): UResponse + + @GET("biscuit/retrieve") + suspend fun getSession(): UResponse + + @POST("biscuit/invalidate") + suspend fun invalidateSession(): UResponse + + // ------------ ping -------------- + @GET("hi") + suspend fun hi(): Response> +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/network/adapter/LiveDataCallAdapter.kt b/app/src/main/java/com/forcetower/uefs/core/storage/network/adapter/LiveDataCallAdapter.kt index a17af4d42..eb85c6f2a 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/network/adapter/LiveDataCallAdapter.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/network/adapter/LiveDataCallAdapter.kt @@ -20,10 +20,10 @@ package com.forcetower.uefs.core.storage.network.adapter import androidx.lifecycle.LiveData +import java.util.concurrent.atomic.AtomicBoolean import retrofit2.Call import retrofit2.Callback import retrofit2.Response -import java.util.concurrent.atomic.AtomicBoolean /** * A Retrofit adapter that converts the Call into a LiveData of ApiResponse. diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/AccountRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/AccountRepository.kt index d747b3595..aa395d590 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/AccountRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/AccountRepository.kt @@ -29,8 +29,8 @@ import com.forcetower.uefs.core.storage.network.adapter.ApiResponse import com.forcetower.uefs.core.storage.network.adapter.asLiveData import com.forcetower.uefs.core.storage.resource.NetworkBoundResource import com.forcetower.uefs.core.storage.resource.Resource -import timber.log.Timber import javax.inject.Inject +import timber.log.Timber class AccountRepository @Inject constructor( private val database: UDatabase, diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/AdventureRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/AdventureRepository.kt index 467c8c2d4..a13c9139a 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/AdventureRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/AdventureRepository.kt @@ -1,313 +1,326 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.repository - -import android.annotation.SuppressLint -import android.content.SharedPreferences -import android.location.Location -import androidx.annotation.AnyThread -import androidx.annotation.WorkerThread -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import com.forcetower.uefs.AppExecutors -import com.forcetower.uefs.R -import com.forcetower.uefs.core.constants.Constants -import com.forcetower.uefs.core.model.service.AchDistance -import com.forcetower.uefs.core.model.service.Achievement -import com.forcetower.uefs.core.model.unes.ClassLocation -import com.forcetower.uefs.core.model.unes.Semester -import com.forcetower.uefs.core.storage.database.UDatabase -import com.forcetower.uefs.core.storage.network.UService -import com.forcetower.uefs.feature.shared.extensions.generateCalendarFromHour -import java.util.Calendar -import java.util.Locale -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import kotlin.collections.set - -class AdventureRepository @Inject constructor( - private val database: UDatabase, - private val executors: AppExecutors, - private val preferences: SharedPreferences, - private val locations: AchLocationsRepository, - private val service: UService -) { - - @AnyThread - fun checkServerAchievements(): LiveData> { - val data = MutableLiveData>() - executors.networkIO().execute { - try { - val response = service.getServerAchievements().execute() - if (response.isSuccessful) { - val value = response.body()?.data ?: emptyList() - data.postValue(value) - } else { - data.postValue(emptyList()) - } - } catch (error: Throwable) { - data.postValue(emptyList()) - } - } - return data - } - - @AnyThread - fun matchesAnyAchievement(location: Location?): List { - return locations.onReceiveLocation(location) - } - - @AnyThread - fun checkAchievements(): LiveData> { - val data = MutableLiveData>() - executors.diskIO().execute { - val list = internalCheckAchievements() - data.postValue(list) - } - return data - } - - @AnyThread - fun justCheckAchievements(): LiveData> { - val data = MutableLiveData>() - executors.diskIO().execute { - val map = HashMap() - performCheckAchievements(map) - data.postValue(map) - } - return data - } - - @SuppressLint("UseSparseArrays") - @WorkerThread - private fun internalCheckAchievements(): Map { - val data = HashMap() - performCheckAchievements(data) - return data - } - - @WorkerThread - fun performCheckAchievements(data: HashMap) { - val semesters = database.semesterDao().getSemestersDirect() - unlockSemesterBased(semesters, data) - val schedule = database.classLocationDao().getCurrentScheduleDirect() - unlockScheduleBased(schedule, data) - - val darkTheme = preferences.getBoolean("ach_night_mode_enabled", false) - if (darkTheme) { - data[R.string.achievement_escuridao] = -1 - } - - data[R.string.achievement_atualizado] = -1 - } - - private fun unlockSemesterBased(semesters: List, data: HashMap) { - val profile = database.profileDao().selectMeDirect() - val score = profile?.score ?: profile?.calcScore ?: -1.0 - - if (semesters.size > 5 && score >= 7) - data[R.string.achievement_sobrevivente] = -1 - - if (semesters.size > 4) data[R.string.achievement_veterano] = -1 - if (semesters.size > 7) data[R.string.achievement_e_ai_forma_quando] = -1 - if (semesters.size > 1) { - data[R.string.achievement_pseudoveterano] = -1 - - val sorted = if (semesters.all { it.start != null }) { - semesters.sortedBy { it.start } - } else { - semesters.sortedBy { it.sagresId } - }.subList(0, semesters.size - 1) - - var noFinalCount = 0 - var introduction = 0 - - // Only previous semesters - sorted.forEach { semester -> - val start = semester.start - val end = semester.end - if (start != null && end != null) { - val diff = end - start - val days = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS) - if (days <= 180) data[R.string.achievement_semestre_de_6_meses] = -1 - - val calendarStart = Calendar.getInstance().apply { timeInMillis = start }.get(Calendar.YEAR) - val calendarEnd = Calendar.getInstance().apply { timeInMillis = end }.get(Calendar.YEAR) - - if (calendarStart != calendarEnd) - data[R.string.achievement_semestre_da_virada] = -1 - } - - var final = false - var mechanics = true - var hours = 0 - val classes = database.classDao().getClassesWithGradesFromSemesterDirect(semester.uid) - var valid = false - classes.forEach { clazz -> - val points = clazz.clazz.finalScore ?: 0.0 - val credits = clazz.discipline.credits - - if (points < 7 && points >= 0) final = true - if (points == 10.0) data[R.string.achievement_mdia_10] = -1 - if (points >= 5 && points < 7) data[R.string.achievement_luta_at_o_fim] = -1 - if (points == 5.0) data[R.string.achievement_quase] = -1 - if (points in 9.5..9.9) data[R.string.achievement_to_perto_mas_to_longe] = -1 - if (points < 8) mechanics = false - - val teacher = Constants.HARD_DISCIPLINES[clazz.discipline.code.uppercase(Locale.getDefault())] - if (teacher != null && points >= 5) { - if (teacher == "__ANY__") { - data[R.string.achievement_vale_das_sombras] = -1 - } else { - clazz.groups.forEach { group -> - val current = group.teacher - if (current != null && teacher.equals(current, ignoreCase = true)) { - data[R.string.achievement_vale_das_sombras] = -1 - } - } - } - } - - clazz.grades.forEach { grade -> - valid = true - val number = grade.gradeDouble() ?: -1.0 - if (number == 7.0) data[R.string.achievement_medocre] = -1 - if (number == 10.0) data[R.string.achievement_achei_fcil] = -1 - } - - if (clazz.grades.size == 3) { - val one = clazz.grades[0].gradeDouble() - val two = clazz.grades[1].gradeDouble() - val thr = clazz.grades[2].gradeDouble() - - if (one != null && two != null && thr != null) { - if (one < 5 && two > 8.5 && thr > 8.5) - data[R.string.achievement_agora_todas_as_peas_se_encaixaram] = -1 - if (one == 7.0 && two == 7.0 && thr == 7.0) - data[R.string.achievement_jackpot] - } - } - - val absences = database.classAbsenceDao().getAbsenceFromClassDirect(clazz.clazz.uid) - if (absences.isEmpty()) data[R.string.achievement_eu_estou_sempre_l] = -1 - if (clazz.clazz.missedClasses >= credits / 4) data[R.string.achievement_nunca_nem_vi] = -1 - - val name = clazz.discipline.name - if (name.matches("(?i)(.*)introdu([cç])([aã])o(.*)".toRegex())) { - introduction++ - } else if (name.matches("(?i)(.*)int(r)?\\.(.*)".toRegex())) { - introduction++ - } - - hours += credits - } - - if (!final && valid) { - data[R.string.achievement_semestre_limpo] = -1 - noFinalCount++ - } else { - noFinalCount = 0 - } - - if (noFinalCount == 3) data[R.string.achievement_killing_spree] = -1 - - mechanics = mechanics and (classes.size >= 4) - if (mechanics) data[R.string.achievement_mecanizou_todo] = -1 - - if (hours >= 480) data[R.string.achievement_me_empresta_o_seu_viratempo] = -1 - else if (hours <= 275) data[R.string.achievement_engatinhando] = -1 - } - - data[R.string.achievement_introduo_a_introdues] = introduction - } - - // Takes only the current semester - val current = if (semesters.all { it.start != null }) { - semesters.maxByOrNull { it.start!! } - } else { - semesters.maxByOrNull { it.sagresId } - } - - if (current != null) { - var hours = 0 - val classes = database.classDao().getClassesWithGradesFromSemesterDirect(current.uid) - classes.forEach { clazz -> - val points = clazz.clazz.finalScore ?: -1.0 - val credits = clazz.discipline.credits - - hours += credits - - if (points == 10.0) data[R.string.achievement_mdia_10] = -1 - if (points >= 5 && points < 7) data[R.string.achievement_luta_at_o_fim] = -1 - if (points == 5.0) data[R.string.achievement_quase] = -1 - if (points in 9.5..9.9) data[R.string.achievement_to_perto_mas_to_longe] = -1 - - clazz.grades.forEach { grade -> - val number = grade.gradeDouble() ?: -1.0 - if (number == 7.0) data[R.string.achievement_medocre] = -1 - if (number == 10.0) data[R.string.achievement_achei_fcil] = -1 - } - - if (clazz.grades.size == 3) { - val one = clazz.grades[0].gradeDouble() - val two = clazz.grades[1].gradeDouble() - val thr = clazz.grades[2].gradeDouble() - - if (one != null && two != null && thr != null) { - if (one < 5 && two > 8.5 && thr > 8.5) - data[R.string.achievement_agora_todas_as_peas_se_encaixaram] = -1 - if (one == 7.0 && two == 7.0 && thr == 7.0) - data[R.string.achievement_jackpot] - } - } - } - - if (hours >= 480) data[R.string.achievement_me_empresta_o_seu_viratempo] = -1 - else if (hours <= 275) data[R.string.achievement_engatinhando] = -1 - } - } - - private fun unlockScheduleBased(schedule: List, data: HashMap) { - val day = schedule.groupBy { it.day } - day.entries.forEach { group -> - var minutes = 0 - group.value.forEach { location -> - val start = location.startsAt.generateCalendarFromHour()?.timeInMillis - val end = location.endsAt.generateCalendarFromHour()?.timeInMillis - if (start != null && end != null) { - val diff = end - start - minutes += TimeUnit.MINUTES.convert(diff, TimeUnit.MILLISECONDS).toInt() - } - } - if (minutes >= 480) data[R.string.achievement_maratonista] = -1 - } - - var mod1 = false - var mod7 = false - schedule.forEach { location -> - val mod = location.modulo ?: "unknown" - if (!mod1) mod1 = mod.equals("Módulo 1", ignoreCase = true) || mod.equals("Modulo 1", ignoreCase = true) - if (!mod7) mod7 = mod.equals("Módulo 7", ignoreCase = true) || mod.equals("Modulo 7", ignoreCase = true) - } - - if (mod1 && mod7) data[R.string.achievement_dora_a_exploradora] = -1 - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.repository + +import android.annotation.SuppressLint +import android.content.SharedPreferences +import android.location.Location +import androidx.annotation.AnyThread +import androidx.annotation.WorkerThread +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.forcetower.core.extensions.nearlyEquals +import com.forcetower.uefs.AppExecutors +import com.forcetower.uefs.R +import com.forcetower.uefs.core.constants.Constants +import com.forcetower.uefs.core.model.service.AchDistance +import com.forcetower.uefs.core.model.service.Achievement +import com.forcetower.uefs.core.model.unes.ClassLocation +import com.forcetower.uefs.core.model.unes.Semester +import com.forcetower.uefs.core.storage.database.UDatabase +import com.forcetower.uefs.core.storage.network.UService +import com.forcetower.uefs.feature.shared.extensions.generateCalendarFromHour +import java.util.Calendar +import java.util.Locale +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import kotlin.collections.set + +class AdventureRepository @Inject constructor( + private val database: UDatabase, + private val executors: AppExecutors, + private val preferences: SharedPreferences, + private val locations: AchLocationsRepository, + private val service: UService +) { + + @AnyThread + fun checkServerAchievements(): LiveData> { + val data = MutableLiveData>() + executors.networkIO().execute { + try { + val response = service.getServerAchievements().execute() + if (response.isSuccessful) { + val value = response.body()?.data ?: emptyList() + data.postValue(value) + } else { + data.postValue(emptyList()) + } + } catch (error: Throwable) { + data.postValue(emptyList()) + } + } + return data + } + + @AnyThread + fun matchesAnyAchievement(location: Location?): List { + return locations.onReceiveLocation(location) + } + + @AnyThread + fun checkAchievements(): LiveData> { + val data = MutableLiveData>() + executors.diskIO().execute { + val list = internalCheckAchievements() + data.postValue(list) + } + return data + } + + @AnyThread + fun justCheckAchievements(): LiveData> { + val data = MutableLiveData>() + executors.diskIO().execute { + val map = HashMap() + performCheckAchievements(map) + data.postValue(map) + } + return data + } + + @SuppressLint("UseSparseArrays") + @WorkerThread + private fun internalCheckAchievements(): Map { + val data = HashMap() + performCheckAchievements(data) + return data + } + + @WorkerThread + fun performCheckAchievements(data: HashMap) { + val semesters = database.semesterDao().getSemestersDirect() + unlockSemesterBased(semesters, data) + val schedule = database.classLocationDao().getCurrentScheduleDirect() + unlockScheduleBased(schedule, data) + + val darkTheme = preferences.getBoolean("ach_night_mode_enabled", false) + if (darkTheme) { + data[R.string.achievement_escuridao] = -1 + } + + data[R.string.achievement_atualizado] = -1 + } + + private fun unlockSemesterBased(semesters: List, data: HashMap) { + val profile = database.profileDao().selectMeDirect() + val score = profile?.score ?: profile?.calcScore ?: -1.0 + + if (semesters.size > 5 && score >= 7) { + data[R.string.achievement_sobrevivente] = -1 + } + + if (semesters.size > 4) data[R.string.achievement_veterano] = -1 + if (semesters.size > 7) data[R.string.achievement_e_ai_forma_quando] = -1 + if (semesters.size > 1) { + data[R.string.achievement_pseudoveterano] = -1 + + val sorted = if (semesters.all { it.start != null }) { + semesters.sortedBy { it.start } + } else { + semesters.sortedBy { it.sagresId } + }.subList(0, semesters.size - 1) + + var noFinalCount = 0 + var introduction = 0 + + // Only previous semesters + sorted.forEach { semester -> + val start = semester.start + val end = semester.end + if (start != null && end != null) { + val diff = end - start + val days = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS) + if (days <= 180) data[R.string.achievement_semestre_de_6_meses] = -1 + + val calendarStart = Calendar.getInstance().apply { timeInMillis = start }.get(Calendar.YEAR) + val calendarEnd = Calendar.getInstance().apply { timeInMillis = end }.get(Calendar.YEAR) + + if (calendarStart != calendarEnd) { + data[R.string.achievement_semestre_da_virada] = -1 + } + } + + var final = false + var mechanics = true + var hours = 0 + val classes = database.classDao().getClassesWithGradesFromSemesterDirect(semester.uid) + var valid = false + classes.forEach { clazz -> + val points = clazz.clazz.finalScore ?: 0.0 + val credits = clazz.discipline.credits + + if (points < 7 && points >= 0) final = true + if (points.nearlyEquals(10.0)) data[R.string.achievement_mdia_10] = -1 + if (points >= 5 && points < 7) data[R.string.achievement_luta_at_o_fim] = -1 + if (points.nearlyEquals(5.0)) data[R.string.achievement_quase] = -1 + if (points in 9.5..9.9) data[R.string.achievement_to_perto_mas_to_longe] = -1 + if (points < 8) mechanics = false + + val teacher = Constants.HARD_DISCIPLINES[clazz.discipline.code.uppercase(Locale.getDefault())] + if (teacher != null && points >= 5) { + if (teacher == "__ANY__") { + data[R.string.achievement_vale_das_sombras] = -1 + } else { + clazz.groups.forEach { group -> + val current = group.teacher + if (current != null && teacher.equals(current, ignoreCase = true)) { + data[R.string.achievement_vale_das_sombras] = -1 + } + } + } + } + + clazz.grades.forEach { grade -> + valid = true + val number = grade.gradeDouble() ?: -1.0 + if (number.nearlyEquals(7.0)) data[R.string.achievement_medocre] = -1 + if (number.nearlyEquals(10.0)) data[R.string.achievement_achei_fcil] = -1 + } + + if (clazz.grades.size == 3) { + val one = clazz.grades[0].gradeDouble() + val two = clazz.grades[1].gradeDouble() + val thr = clazz.grades[2].gradeDouble() + + if (one != null && two != null && thr != null) { + if (one < 5 && two > 8.5 && thr > 8.5) { + data[R.string.achievement_agora_todas_as_peas_se_encaixaram] = -1 + } + if (one.nearlyEquals(7.0) && two.nearlyEquals(7.0) && thr.nearlyEquals(7.0)) { + data[R.string.achievement_jackpot] + } + } + } + + val absences = database.classAbsenceDao().getAbsenceFromClassDirect(clazz.clazz.uid) + if (absences.isEmpty()) data[R.string.achievement_eu_estou_sempre_l] = -1 + if (clazz.clazz.missedClasses >= credits / 4) data[R.string.achievement_nunca_nem_vi] = -1 + + val name = clazz.discipline.name + if (name.matches("(?i)(.*)introdu([cç])([aã])o(.*)".toRegex())) { + introduction++ + } else if (name.matches("(?i)(.*)int(r)?\\.(.*)".toRegex())) { + introduction++ + } + + hours += credits + } + + if (!final && valid) { + data[R.string.achievement_semestre_limpo] = -1 + noFinalCount++ + } else { + noFinalCount = 0 + } + + if (noFinalCount == 3) data[R.string.achievement_killing_spree] = -1 + + mechanics = mechanics and (classes.size >= 4) + if (mechanics) data[R.string.achievement_mecanizou_todo] = -1 + + if (hours >= 480) { + data[R.string.achievement_me_empresta_o_seu_viratempo] = -1 + } else if (hours <= 275) { + data[R.string.achievement_engatinhando] = -1 + } + } + + data[R.string.achievement_introduo_a_introdues] = introduction + } + + // Takes only the current semester + val current = if (semesters.all { it.start != null }) { + semesters.maxByOrNull { it.start!! } + } else { + semesters.maxByOrNull { it.sagresId } + } + + if (current != null) { + var hours = 0 + val classes = database.classDao().getClassesWithGradesFromSemesterDirect(current.uid) + classes.forEach { clazz -> + val points = clazz.clazz.finalScore ?: -1.0 + val credits = clazz.discipline.credits + + hours += credits + + if (points.nearlyEquals(10.0)) data[R.string.achievement_mdia_10] = -1 + if (points >= 5 && points < 7) data[R.string.achievement_luta_at_o_fim] = -1 + if (points.nearlyEquals(5.0)) data[R.string.achievement_quase] = -1 + if (points in 9.5..9.9) data[R.string.achievement_to_perto_mas_to_longe] = -1 + + clazz.grades.forEach { grade -> + val number = grade.gradeDouble() ?: -1.0 + if (number.nearlyEquals(7.0)) data[R.string.achievement_medocre] = -1 + if (number.nearlyEquals(10.0)) data[R.string.achievement_achei_fcil] = -1 + } + + if (clazz.grades.size == 3) { + val one = clazz.grades[0].gradeDouble() + val two = clazz.grades[1].gradeDouble() + val thr = clazz.grades[2].gradeDouble() + + if (one != null && two != null && thr != null) { + if (one < 5 && two > 8.5 && thr > 8.5) { + data[R.string.achievement_agora_todas_as_peas_se_encaixaram] = -1 + } + if (one.nearlyEquals(7.0) && two.nearlyEquals(7.0) && thr.nearlyEquals(7.0)) { + data[R.string.achievement_jackpot] + } + } + } + } + + if (hours >= 480) { + data[R.string.achievement_me_empresta_o_seu_viratempo] = -1 + } else if (hours <= 275) { + data[R.string.achievement_engatinhando] = -1 + } + } + } + + private fun unlockScheduleBased(schedule: List, data: HashMap) { + val day = schedule.groupBy { it.day } + day.entries.forEach { group -> + var minutes = 0 + group.value.forEach { location -> + val start = location.startsAt.generateCalendarFromHour()?.timeInMillis + val end = location.endsAt.generateCalendarFromHour()?.timeInMillis + if (start != null && end != null) { + val diff = end - start + minutes += TimeUnit.MINUTES.convert(diff, TimeUnit.MILLISECONDS).toInt() + } + } + if (minutes >= 480) data[R.string.achievement_maratonista] = -1 + } + + var mod1 = false + var mod7 = false + schedule.forEach { location -> + val mod = location.modulo ?: "unknown" + if (!mod1) mod1 = mod.equals("Módulo 1", ignoreCase = true) || mod.equals("Modulo 1", ignoreCase = true) + if (!mod7) mod7 = mod.equals("Módulo 7", ignoreCase = true) || mod.equals("Modulo 7", ignoreCase = true) + } + + if (mod1 && mod7) data[R.string.achievement_dora_a_exploradora] = -1 + } +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/ContributorRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/ContributorRepository.kt index 0f1655e7b..343ee3a4c 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/ContributorRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/ContributorRepository.kt @@ -31,9 +31,9 @@ import com.forcetower.uefs.core.storage.network.adapter.asLiveData import com.forcetower.uefs.core.storage.network.github.GithubService import com.forcetower.uefs.core.storage.resource.NetworkBoundResource import com.forcetower.uefs.core.storage.resource.Resource -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +import timber.log.Timber @Singleton class ContributorRepository @Inject constructor( diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/CookieSessionRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/CookieSessionRepository.kt index f3aedfe89..2f0c85ebe 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/CookieSessionRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/CookieSessionRepository.kt @@ -26,10 +26,10 @@ import com.forcetower.uefs.core.model.service.SavedCookie import com.forcetower.uefs.core.storage.cookies.CachedCookiePersistor import com.forcetower.uefs.core.storage.network.UService import com.forcetower.uefs.service.NotificationCreator -import okhttp3.Cookie -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +import okhttp3.Cookie +import timber.log.Timber @Singleton class CookieSessionRepository @Inject constructor( @@ -101,8 +101,11 @@ class CookieSessionRepository @Inject constructor( return INJECT_SUCCESS } // if status is 0 user is ducked, otherwise it's a networking error - return if (status == 0) INJECT_ERROR_NO_VALUE - else INJECT_ERROR_NETWORK + return if (status == 0) { + INJECT_ERROR_NO_VALUE + } else { + INJECT_ERROR_NETWORK + } } suspend fun invalidateCookies() { diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/CourseRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/CourseRepository.kt index 5654e0919..7de97575f 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/CourseRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/CourseRepository.kt @@ -27,14 +27,14 @@ import com.forcetower.uefs.core.storage.network.UService import com.forcetower.uefs.core.task.UCaseResult import com.google.gson.Gson import com.google.gson.reflect.TypeToken +import java.nio.charset.Charset +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import timber.log.Timber -import java.nio.charset.Charset -import javax.inject.Inject -import javax.inject.Singleton @Singleton class CourseRepository @Inject constructor( diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/DemandRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/DemandRepository.kt index 30c52d0df..436d3ccc7 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/DemandRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/DemandRepository.kt @@ -37,9 +37,9 @@ import com.forcetower.uefs.core.work.demand.CreateDemandWorker import com.forcetower.uefs.service.NotificationCreator import com.google.firebase.analytics.FirebaseAnalytics import io.reactivex.BackpressureStrategy -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +import timber.log.Timber @Singleton class DemandRepository @Inject constructor( diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/DisciplineDetailsRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/DisciplineDetailsRepository.kt index 485bed135..6eaaafc58 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/DisciplineDetailsRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/DisciplineDetailsRepository.kt @@ -1,216 +1,216 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.repository - -import android.content.SharedPreferences -import androidx.annotation.AnyThread -import androidx.annotation.MainThread -import androidx.annotation.WorkerThread -import androidx.lifecycle.LiveData -import com.forcetower.sagres.Constants -import com.forcetower.sagres.SagresNavigator -import com.forcetower.sagres.database.model.SagresDiscipline -import com.forcetower.sagres.database.model.SagresDisciplineGroup -import com.forcetower.sagres.operation.Status -import com.forcetower.sagres.operation.disciplines.FastDisciplinesCallback -import com.forcetower.uefs.AppExecutors -import com.forcetower.uefs.core.model.service.discipline.DisciplineDetailsData -import com.forcetower.uefs.core.model.service.discipline.transformToNewStyle -import com.forcetower.uefs.core.model.unes.Discipline -import com.forcetower.uefs.core.model.unes.Semester -import com.forcetower.uefs.core.storage.database.UDatabase -import com.forcetower.uefs.core.storage.network.UService -import com.forcetower.uefs.core.storage.resource.discipline.LoadDisciplineDetailsResource -import com.forcetower.uefs.core.util.isStudentFromUEFS -import com.google.firebase.remoteconfig.FirebaseRemoteConfig -import kotlinx.coroutines.runBlocking -import okhttp3.OkHttpClient -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Named -import javax.inject.Singleton - -@Singleton -class DisciplineDetailsRepository @Inject constructor( - private val database: UDatabase, - private val executors: AppExecutors, - private val gradesRepository: SagresGradesRepository, - private val cookieSessionRepository: CookieSessionRepository, - private val preferences: SharedPreferences, - private val service: UService, - private val remoteConfig: FirebaseRemoteConfig, - private val client: OkHttpClient, - @Named("webViewUA") private val agent: String, - @Named("flagSnowpiercerEnabled") private val snowpiercerEnabled: Boolean, -) { - - /** - * This is a funny function - * It will load the details of any discipline that matches the requirements, if all the parameters - * are null, it will load the details of every single discipline. - * - * This function may take several minutes to complete, it's better to be called within a service - * or something that shows to the user that this long operation is running. - * - * Funny fact: It only saves the data once it has completed downloading everything, a network - * connection fail during the process will generate a catastrophic fail. - * - * Note to myself: save the data when it is ready, don't wait until everything completes - * - * Second note: add a "don't load this discipline", kind of a blacklist so the process ignore the - * discipline and load everything faster - * - * Third note: add a "simple load" and a "full load". Simple load will only fetch the teacher - * name, which is the common use for this function. - */ - @MainThread - fun loadDisciplineDetails(semester: String? = null, code: String? = null, group: String? = null, partialLoad: Boolean = true, discover: Boolean = false): LiveData { - return object : LoadDisciplineDetailsResource( - executors, - database, - semester, - code, - group, - partialLoad, - discover, - snowpiercerEnabled, - agent, - client - ) { - @WorkerThread - override fun saveResults(callback: FastDisciplinesCallback) { - defineSemesters(callback.getSemesters()) - defineDisciplines(callback.getDisciplines()) - defineDisciplineGroups(callback.getGroups()) - } - - @WorkerThread - override fun loadGrades() { - val semesters = database.semesterDao().getSemestersDirect() - semesters.forEach { - // TODO this is just nasty - runBlocking { - gradesRepository.getGrades(it.sagresId, false) - } - } - } - }.asLiveData() - } - - @AnyThread - fun contribute() { - executors.diskIO().execute { - sendDisciplineDetails() - } - } - - @AnyThread - fun contributeCurrent() { - executors.diskIO().execute { - val currentOnly = remoteConfig.getBoolean("contribute_only_current") - sendDisciplineDetails(currentOnly) - } - } - - @WorkerThread - fun sendDisciplineDetails(current: Boolean = false) { - val semesters = database.semesterDao().getSemestersDirect() - val currentSemester = semesters.maxByOrNull { it.sagresId }?.sagresId - - val stats = if (current) { - currentSemester ?: return - database.classGroupDao().getClassStatsWithAllDirect(currentSemester) - } else { - database.classGroupDao().getClassStatsWithAllDirect() - } - - val profile = database.profileDao().selectMeDirect() ?: return - - val treated = stats.transformToNewStyle() - - val amountSemesters = semesters.size - val score = if (profile.score != -1.0) profile.score else profile.calcScore - - val data = DisciplineDetailsData(amountSemesters, score, profile.course, treated) - try { - val response = service.sendGrades(data).execute() - if (response.isSuccessful) { - Timber.d("Success Response") - val result = response.body()!! - Timber.d("Result ${result.success}") - } else { - Timber.d("Failed Response Code: ${response.code()}") - } - } catch (exception: Exception) { - Timber.e(exception) - } - } - - @WorkerThread - suspend fun loadDisciplineDetailsSync(partialLoad: Boolean = true, notify: Boolean = false) { - experimentalDisciplines(partialLoad, notify) - val semesters = database.semesterDao().getSemestersDirect() - semesters.forEach { gradesRepository.getGrades(it.sagresId, false) } - sendDisciplineDetails() - } - - @WorkerThread - suspend fun experimentalDisciplines(partialLoad: Boolean = false, notify: Boolean = true) { - val access = database.accessDao().getAccessDirect() - if (access != null) { - if (preferences.isStudentFromUEFS()) { - cookieSessionRepository.injectGoodCookiesOnClient() - } else if (Constants.getParameter("REQUIRES_CAPTCHA") != "true") { - SagresNavigator.instance.login(access.username, access.password) - } - val experimental = SagresNavigator.instance.disciplinesExperimental( - discover = false, - partialLoad = partialLoad - ) - if (experimental.status == Status.COMPLETED) { - defineSemesters(experimental.getSemesters()) - defineDisciplines(experimental.getDisciplines()) - defineDisciplineGroups(experimental.getGroups(), notify) - } - database.classMaterialDao().markAllNotified() - } - } - - @WorkerThread - private fun defineSemesters(semesters: List>) { - semesters.forEach { - val semester = Semester(sagresId = it.first, name = it.second, codename = it.second) - database.semesterDao().insertIgnoring(semester) - } - } - - @WorkerThread - private fun defineDisciplines(disciplines: List) { - val values = disciplines.map { Discipline.fromSagres(it) } - database.disciplineDao().insert(values) - disciplines.forEach { database.classDao().insert(it, true) } - } - - @WorkerThread - private fun defineDisciplineGroups(groups: List, notify: Boolean = true) { - database.classGroupDao().defineGroups(groups, notify) - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.repository + +import android.content.SharedPreferences +import androidx.annotation.AnyThread +import androidx.annotation.MainThread +import androidx.annotation.WorkerThread +import androidx.lifecycle.LiveData +import com.forcetower.sagres.Constants +import com.forcetower.sagres.SagresNavigator +import com.forcetower.sagres.database.model.SagresDiscipline +import com.forcetower.sagres.database.model.SagresDisciplineGroup +import com.forcetower.sagres.operation.Status +import com.forcetower.sagres.operation.disciplines.FastDisciplinesCallback +import com.forcetower.uefs.AppExecutors +import com.forcetower.uefs.core.model.service.discipline.DisciplineDetailsData +import com.forcetower.uefs.core.model.service.discipline.transformToNewStyle +import com.forcetower.uefs.core.model.unes.Discipline +import com.forcetower.uefs.core.model.unes.Semester +import com.forcetower.uefs.core.storage.database.UDatabase +import com.forcetower.uefs.core.storage.network.UService +import com.forcetower.uefs.core.storage.resource.discipline.LoadDisciplineDetailsResource +import com.forcetower.uefs.core.util.isStudentFromUEFS +import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton +import kotlinx.coroutines.runBlocking +import okhttp3.OkHttpClient +import timber.log.Timber + +@Singleton +class DisciplineDetailsRepository @Inject constructor( + private val database: UDatabase, + private val executors: AppExecutors, + private val gradesRepository: SagresGradesRepository, + private val cookieSessionRepository: CookieSessionRepository, + private val preferences: SharedPreferences, + private val service: UService, + private val remoteConfig: FirebaseRemoteConfig, + private val client: OkHttpClient, + @Named("webViewUA") private val agent: String, + @Named("flagSnowpiercerEnabled") private val snowpiercerEnabled: Boolean +) { + + /** + * This is a funny function + * It will load the details of any discipline that matches the requirements, if all the parameters + * are null, it will load the details of every single discipline. + * + * This function may take several minutes to complete, it's better to be called within a service + * or something that shows to the user that this long operation is running. + * + * Funny fact: It only saves the data once it has completed downloading everything, a network + * connection fail during the process will generate a catastrophic fail. + * + * Note to myself: save the data when it is ready, don't wait until everything completes + * + * Second note: add a "don't load this discipline", kind of a blacklist so the process ignore the + * discipline and load everything faster + * + * Third note: add a "simple load" and a "full load". Simple load will only fetch the teacher + * name, which is the common use for this function. + */ + @MainThread + fun loadDisciplineDetails(semester: String? = null, code: String? = null, group: String? = null, partialLoad: Boolean = true, discover: Boolean = false): LiveData { + return object : LoadDisciplineDetailsResource( + executors, + database, + semester, + code, + group, + partialLoad, + discover, + snowpiercerEnabled, + agent, + client + ) { + @WorkerThread + override fun saveResults(callback: FastDisciplinesCallback) { + defineSemesters(callback.getSemesters()) + defineDisciplines(callback.getDisciplines()) + defineDisciplineGroups(callback.getGroups()) + } + + @WorkerThread + override fun loadGrades() { + val semesters = database.semesterDao().getSemestersDirect() + semesters.forEach { + // TODO this is just nasty + runBlocking { + gradesRepository.getGrades(it.sagresId, false) + } + } + } + }.asLiveData() + } + + @AnyThread + fun contribute() { + executors.diskIO().execute { + sendDisciplineDetails() + } + } + + @AnyThread + fun contributeCurrent() { + executors.diskIO().execute { + val currentOnly = remoteConfig.getBoolean("contribute_only_current") + sendDisciplineDetails(currentOnly) + } + } + + @WorkerThread + fun sendDisciplineDetails(current: Boolean = false) { + val semesters = database.semesterDao().getSemestersDirect() + val currentSemester = semesters.maxByOrNull { it.sagresId }?.sagresId + + val stats = if (current) { + currentSemester ?: return + database.classGroupDao().getClassStatsWithAllDirect(currentSemester) + } else { + database.classGroupDao().getClassStatsWithAllDirect() + } + + val profile = database.profileDao().selectMeDirect() ?: return + + val treated = stats.transformToNewStyle() + + val amountSemesters = semesters.size + val score = if (profile.score != -1.0) profile.score else profile.calcScore + + val data = DisciplineDetailsData(amountSemesters, score, profile.course, treated) + try { + val response = service.sendGrades(data).execute() + if (response.isSuccessful) { + Timber.d("Success Response") + val result = response.body()!! + Timber.d("Result ${result.success}") + } else { + Timber.d("Failed Response Code: ${response.code()}") + } + } catch (exception: Exception) { + Timber.e(exception) + } + } + + @WorkerThread + suspend fun loadDisciplineDetailsSync(partialLoad: Boolean = true, notify: Boolean = false) { + experimentalDisciplines(partialLoad, notify) + val semesters = database.semesterDao().getSemestersDirect() + semesters.forEach { gradesRepository.getGrades(it.sagresId, false) } + sendDisciplineDetails() + } + + @WorkerThread + suspend fun experimentalDisciplines(partialLoad: Boolean = false, notify: Boolean = true) { + val access = database.accessDao().getAccessDirect() + if (access != null) { + if (preferences.isStudentFromUEFS()) { + cookieSessionRepository.injectGoodCookiesOnClient() + } else if (Constants.getParameter("REQUIRES_CAPTCHA") != "true") { + SagresNavigator.instance.login(access.username, access.password) + } + val experimental = SagresNavigator.instance.disciplinesExperimental( + discover = false, + partialLoad = partialLoad + ) + if (experimental.status == Status.COMPLETED) { + defineSemesters(experimental.getSemesters()) + defineDisciplines(experimental.getDisciplines()) + defineDisciplineGroups(experimental.getGroups(), notify) + } + database.classMaterialDao().markAllNotified() + } + } + + @WorkerThread + private fun defineSemesters(semesters: List>) { + semesters.forEach { + val semester = Semester(sagresId = it.first, name = it.second, codename = it.second) + database.semesterDao().insertIgnoring(semester) + } + } + + @WorkerThread + private fun defineDisciplines(disciplines: List) { + val values = disciplines.map { Discipline.fromSagres(it) } + database.disciplineDao().insert(values) + disciplines.forEach { database.classDao().insert(it, true) } + } + + @WorkerThread + private fun defineDisciplineGroups(groups: List, notify: Boolean = true) { + database.classGroupDao().defineGroups(groups, notify) + } +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/DisciplinesRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/DisciplinesRepository.kt index 4d769a360..67c7d16c1 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/DisciplinesRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/DisciplinesRepository.kt @@ -1,267 +1,268 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.repository - -import android.content.Context -import android.content.SharedPreferences -import androidx.annotation.AnyThread -import androidx.lifecycle.LiveData -import com.forcetower.sagres.Constants -import com.forcetower.sagres.SagresNavigator -import com.forcetower.sagres.operation.Status -import com.forcetower.uefs.AppExecutors -import com.forcetower.uefs.core.model.ui.disciplines.DisciplineHelperData -import com.forcetower.uefs.core.model.ui.disciplines.DisciplinesDataUI -import com.forcetower.uefs.core.model.ui.disciplines.DisciplinesIndexed -import com.forcetower.uefs.core.model.unes.Class -import com.forcetower.uefs.core.model.unes.ClassAbsence -import com.forcetower.uefs.core.model.unes.ClassItem -import com.forcetower.uefs.core.model.unes.ClassLocation -import com.forcetower.uefs.core.model.unes.ClassMaterial -import com.forcetower.uefs.core.model.unes.Semester -import com.forcetower.uefs.core.storage.database.UDatabase -import com.forcetower.uefs.core.storage.database.aggregation.ClassFullWithGroup -import com.forcetower.uefs.core.storage.database.aggregation.ClassGroupWithData -import com.forcetower.uefs.core.storage.database.aggregation.ClassLocationWithData -import com.forcetower.uefs.core.task.definers.LectureProcessor -import com.forcetower.uefs.core.task.definers.MissedLectureProcessor -import com.forcetower.uefs.core.util.isStudentFromUEFS -import dagger.Reusable -import dev.forcetower.breaker.Orchestra -import dev.forcetower.breaker.model.Authorization -import dev.forcetower.breaker.result.Outcome -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.map -import okhttp3.OkHttpClient -import timber.log.Timber -import java.util.Calendar -import javax.inject.Inject -import javax.inject.Named - -@Reusable -class DisciplinesRepository @Inject constructor( - private val context: Context, - private val client: OkHttpClient, - private val database: UDatabase, - private val executors: AppExecutors, - private val cookieSessionRepository: CookieSessionRepository, - @Named("webViewUA") private val agent: String, - @Named("flagSnowpiercerEnabled") private val snowpiercerEnabled: Boolean, - private val preferences: SharedPreferences -) { - fun getAllDisciplinesData(): Flow { - return database.classDao().getClassesWithGradesFromAllSemesters().map { classes -> - val databaseSemesters = database.semesterDao().getParticipatingSemestersDirect() - val classesSemester = classes.map { it.semester }.distinct() - val semesters = (databaseSemesters + classesSemester).distinct().run { - if (snowpiercerEnabled && all { it.start != null }) { - sortedByDescending { it.start } - } else if (preferences.getBoolean("stg_semester_deterministic_ordering", true)) { - sortedByDescending { it.sagresId } - } else { - sorted() - } - } - val elements = transformClassesIntoUiElements(semesters, classes) - val indexes = DisciplinesIndexed.from(semesters, elements) - DisciplinesDataUI(elements, indexes, semesters) - } - } - - private fun transformClassesIntoUiElements( - semesters: List, - classes: List - ): List { - val completedMap = classes - .groupBy { it.semester } - .mapValues { entry -> - val disciplines = entry.value - val result = mutableListOf() - disciplines.sortedBy { it.discipline.name }.forEachIndexed { index, clazz -> - if (index != 0) - result += DisciplineHelperData.Divider - - result += DisciplineHelperData.Header(clazz) - - val groupings = clazz.grades.groupBy { it.grouping } - if (groupings.keys.size <= 1) { - clazz.grades.sortedBy { it.name }.forEach { grade -> - result += DisciplineHelperData.Score(clazz, grade) - } - } else { - groupings.entries.sortedBy { it.key }.forEach { (_, value) -> - if (value.isNotEmpty()) { - val sample = value[0] - result += DisciplineHelperData.GroupingName(clazz, sample.groupingName) - value.sortedBy { it.name }.forEach { grade -> - result += DisciplineHelperData.Score(clazz, grade) - } - } - } - } - - if (clazz.clazz.isInFinal()) { - result += DisciplineHelperData.Final(clazz) - } - result += DisciplineHelperData.Mean(clazz) - } - result - } - - return semesters.map { completedMap[it] ?: listOf(DisciplineHelperData.EmptySemester(it)) }.flatten() - } - - fun getParticipatingSemesters(): LiveData> { - return database.semesterDao().getParticipatingSemesters() - } - - fun getClassesWithGradesFromSemester(semesterId: Long): LiveData> { - return database.classDao().getClassesWithGradesFromSemester(semesterId) - } - - fun getClassGroup(classGroupId: Long): LiveData { - return database.classGroupDao().getWithRelations(classGroupId) - } - - fun getClassFull(classId: Long): LiveData { - return database.classDao().getClass(classId) - } - - fun getMyAbsencesFromClass(classId: Long): LiveData> { - return database.classAbsenceDao().getMyAbsenceFromClass(classId) - } - - fun getAbsencesAmount(classId: Long): LiveData { - return database.classAbsenceDao().getMissedClassesAmount(classId) - } - - fun getMaterialsFromGroup(classGroupId: Long): LiveData> { - return database.classMaterialDao().getMaterialsFromGroup(classGroupId) - } - - fun getClassItemsFromGroup(classGroupId: Long): LiveData> { - return database.classItemDao().getClassItemsFromGroup(classGroupId) - } - - fun getLocationsFromClass(classId: Long): LiveData> { - return database.classLocationDao().getLocationsOfClass(classId) - } - - suspend fun getCurrentClass(): ClassLocationWithData? { - val calendar = Calendar.getInstance() - val dayInt = calendar.get(Calendar.DAY_OF_WEEK) - val currentTimeInt = calendar.get(Calendar.HOUR_OF_DAY) * 60 + calendar.get(Calendar.MINUTE) - return database.classLocationDao().getCurrentClassDirect(dayInt, currentTimeInt) - } - - fun loadClassDetailsSnowflake(groupId: Long) = flow { - emit(true) - val access = database.accessDao().getAccessDirect() - val profile = database.profileDao().selectMeDirect() - - if (access == null || profile == null) { - emit(false) - return@flow - } - - val orchestra = Orchestra.Builder() - .userAgent(agent) - .client(client) - .build() - - orchestra.setAuthorization(Authorization(access.username, access.password)) - - val sagresGroupId = database.classGroupDao().getGroupDirect(groupId)?.sagresId - if (sagresGroupId != null) { - val lectures = orchestra.lectures(sagresGroupId, 0, 0) - if (lectures is Outcome.Success) { - LectureProcessor(context, database, groupId, lectures.value, false).execute() - } else if (lectures is Outcome.Error) { - Timber.e(lectures.error, "Error during lectures. Code ${lectures.code}") - } - - val absences = orchestra.absences(profile.sagresId, sagresGroupId, 0, 0) - if (absences is Outcome.Success) { - MissedLectureProcessor(context, database, profile.uid, groupId, absences.value, false).execute() - } else if (absences is Outcome.Error) { - Timber.e(absences.error, "Error during absences. Code ${absences.code}") - } - } - emit(false) - } - - @AnyThread - fun loadClassDetails(groupId: Long) = flow { - emit(true) - Timber.d("Group id for load is $groupId") - val access = database.accessDao().getAccessDirect() - val value = database.classGroupDao().getWithRelationsDirect(groupId) - if (value == null || access == null) { - Timber.d("Class Group with ID: $groupId was not found") - emit(false) - } else { - val clazz = value.classData - val semester = clazz.semester.name - val code = clazz.discipline.code - val group = value.group.group - - Timber.d("Code: $code. Semester: $semester. Group: $group") - - if (preferences.isStudentFromUEFS()) { - cookieSessionRepository.injectGoodCookiesOnClient() - } else if (Constants.getParameter("REQUIRES_CAPTCHA") != "true") { - SagresNavigator.instance.login(access.username, access.password) - } - val callback = SagresNavigator.instance.disciplinesExperimental(semester, code, group) - if (callback.status == Status.COMPLETED) { - val groups = callback.getGroups() - database.classGroupDao().defineGroups(groups) - } else { - Timber.d("Load group has failed along the way") - } - emit(false) - } - } - - @AnyThread - fun resetGroups(clazz: Class) { - executors.diskIO().execute { - val uid = clazz.uid - val groups = database.classGroupDao().getGroupsFromClassDirect(uid) - groups.forEach { - val id = it.uid - database.classItemDao().clearFromGroup(id) - database.classMaterialDao().clearFromGroup(id) - } - } - } - - fun getMaterialsFromClassItem(classItemId: Long): LiveData> { - return database.classMaterialDao().getMaterialsFromClassItem(classItemId) - } - - fun updateLocationVisibilityAsync(location: Long, status: Boolean) { - executors.diskIO().execute { - database.classLocationDao().setClassHiddenHidden(status, location) - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.repository + +import android.content.Context +import android.content.SharedPreferences +import androidx.annotation.AnyThread +import androidx.lifecycle.LiveData +import com.forcetower.sagres.Constants +import com.forcetower.sagres.SagresNavigator +import com.forcetower.sagres.operation.Status +import com.forcetower.uefs.AppExecutors +import com.forcetower.uefs.core.model.ui.disciplines.DisciplineHelperData +import com.forcetower.uefs.core.model.ui.disciplines.DisciplinesDataUI +import com.forcetower.uefs.core.model.ui.disciplines.DisciplinesIndexed +import com.forcetower.uefs.core.model.unes.Class +import com.forcetower.uefs.core.model.unes.ClassAbsence +import com.forcetower.uefs.core.model.unes.ClassItem +import com.forcetower.uefs.core.model.unes.ClassLocation +import com.forcetower.uefs.core.model.unes.ClassMaterial +import com.forcetower.uefs.core.model.unes.Semester +import com.forcetower.uefs.core.storage.database.UDatabase +import com.forcetower.uefs.core.storage.database.aggregation.ClassFullWithGroup +import com.forcetower.uefs.core.storage.database.aggregation.ClassGroupWithData +import com.forcetower.uefs.core.storage.database.aggregation.ClassLocationWithData +import com.forcetower.uefs.core.task.definers.LectureProcessor +import com.forcetower.uefs.core.task.definers.MissedLectureProcessor +import com.forcetower.uefs.core.util.isStudentFromUEFS +import dagger.Reusable +import dev.forcetower.breaker.Orchestra +import dev.forcetower.breaker.model.Authorization +import dev.forcetower.breaker.result.Outcome +import java.util.Calendar +import javax.inject.Inject +import javax.inject.Named +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import okhttp3.OkHttpClient +import timber.log.Timber + +@Reusable +class DisciplinesRepository @Inject constructor( + private val context: Context, + private val client: OkHttpClient, + private val database: UDatabase, + private val executors: AppExecutors, + private val cookieSessionRepository: CookieSessionRepository, + @Named("webViewUA") private val agent: String, + @Named("flagSnowpiercerEnabled") private val snowpiercerEnabled: Boolean, + private val preferences: SharedPreferences +) { + fun getAllDisciplinesData(): Flow { + return database.classDao().getClassesWithGradesFromAllSemesters().map { classes -> + val databaseSemesters = database.semesterDao().getParticipatingSemestersDirect() + val classesSemester = classes.map { it.semester }.distinct() + val semesters = (databaseSemesters + classesSemester).distinct().run { + if (snowpiercerEnabled && all { it.start != null }) { + sortedByDescending { it.start } + } else if (preferences.getBoolean("stg_semester_deterministic_ordering", true)) { + sortedByDescending { it.sagresId } + } else { + sorted() + } + } + val elements = transformClassesIntoUiElements(semesters, classes) + val indexes = DisciplinesIndexed.from(semesters, elements) + DisciplinesDataUI(elements, indexes, semesters) + } + } + + private fun transformClassesIntoUiElements( + semesters: List, + classes: List + ): List { + val completedMap = classes + .groupBy { it.semester } + .mapValues { entry -> + val disciplines = entry.value + val result = mutableListOf() + disciplines.sortedBy { it.discipline.name }.forEachIndexed { index, clazz -> + if (index != 0) { + result += DisciplineHelperData.Divider + } + + result += DisciplineHelperData.Header(clazz) + + val groupings = clazz.grades.groupBy { it.grouping } + if (groupings.keys.size <= 1) { + clazz.grades.sortedBy { it.name }.forEach { grade -> + result += DisciplineHelperData.Score(clazz, grade) + } + } else { + groupings.entries.sortedBy { it.key }.forEach { (_, value) -> + if (value.isNotEmpty()) { + val sample = value[0] + result += DisciplineHelperData.GroupingName(clazz, sample.groupingName) + value.sortedBy { it.name }.forEach { grade -> + result += DisciplineHelperData.Score(clazz, grade) + } + } + } + } + + if (clazz.clazz.isInFinal()) { + result += DisciplineHelperData.Final(clazz) + } + result += DisciplineHelperData.Mean(clazz) + } + result + } + + return semesters.map { completedMap[it] ?: listOf(DisciplineHelperData.EmptySemester(it)) }.flatten() + } + + fun getParticipatingSemesters(): LiveData> { + return database.semesterDao().getParticipatingSemesters() + } + + fun getClassesWithGradesFromSemester(semesterId: Long): LiveData> { + return database.classDao().getClassesWithGradesFromSemester(semesterId) + } + + fun getClassGroup(classGroupId: Long): LiveData { + return database.classGroupDao().getWithRelations(classGroupId) + } + + fun getClassFull(classId: Long): LiveData { + return database.classDao().getClass(classId) + } + + fun getMyAbsencesFromClass(classId: Long): LiveData> { + return database.classAbsenceDao().getMyAbsenceFromClass(classId) + } + + fun getAbsencesAmount(classId: Long): LiveData { + return database.classAbsenceDao().getMissedClassesAmount(classId) + } + + fun getMaterialsFromGroup(classGroupId: Long): LiveData> { + return database.classMaterialDao().getMaterialsFromGroup(classGroupId) + } + + fun getClassItemsFromGroup(classGroupId: Long): LiveData> { + return database.classItemDao().getClassItemsFromGroup(classGroupId) + } + + fun getLocationsFromClass(classId: Long): LiveData> { + return database.classLocationDao().getLocationsOfClass(classId) + } + + suspend fun getCurrentClass(): ClassLocationWithData? { + val calendar = Calendar.getInstance() + val dayInt = calendar.get(Calendar.DAY_OF_WEEK) + val currentTimeInt = calendar.get(Calendar.HOUR_OF_DAY) * 60 + calendar.get(Calendar.MINUTE) + return database.classLocationDao().getCurrentClassDirect(dayInt, currentTimeInt) + } + + fun loadClassDetailsSnowflake(groupId: Long) = flow { + emit(true) + val access = database.accessDao().getAccessDirect() + val profile = database.profileDao().selectMeDirect() + + if (access == null || profile == null) { + emit(false) + return@flow + } + + val orchestra = Orchestra.Builder() + .userAgent(agent) + .client(client) + .build() + + orchestra.setAuthorization(Authorization(access.username, access.password)) + + val sagresGroupId = database.classGroupDao().getGroupDirect(groupId)?.sagresId + if (sagresGroupId != null) { + val lectures = orchestra.lectures(sagresGroupId, 0, 0) + if (lectures is Outcome.Success) { + LectureProcessor(context, database, groupId, lectures.value, false).execute() + } else if (lectures is Outcome.Error) { + Timber.e(lectures.error, "Error during lectures. Code ${lectures.code}") + } + + val absences = orchestra.absences(profile.sagresId, sagresGroupId, 0, 0) + if (absences is Outcome.Success) { + MissedLectureProcessor(context, database, profile.uid, groupId, absences.value, false).execute() + } else if (absences is Outcome.Error) { + Timber.e(absences.error, "Error during absences. Code ${absences.code}") + } + } + emit(false) + } + + @AnyThread + fun loadClassDetails(groupId: Long) = flow { + emit(true) + Timber.d("Group id for load is $groupId") + val access = database.accessDao().getAccessDirect() + val value = database.classGroupDao().getWithRelationsDirect(groupId) + if (value == null || access == null) { + Timber.d("Class Group with ID: $groupId was not found") + emit(false) + } else { + val clazz = value.classData + val semester = clazz.semester.name + val code = clazz.discipline.code + val group = value.group.group + + Timber.d("Code: $code. Semester: $semester. Group: $group") + + if (preferences.isStudentFromUEFS()) { + cookieSessionRepository.injectGoodCookiesOnClient() + } else if (Constants.getParameter("REQUIRES_CAPTCHA") != "true") { + SagresNavigator.instance.login(access.username, access.password) + } + val callback = SagresNavigator.instance.disciplinesExperimental(semester, code, group) + if (callback.status == Status.COMPLETED) { + val groups = callback.getGroups() + database.classGroupDao().defineGroups(groups) + } else { + Timber.d("Load group has failed along the way") + } + emit(false) + } + } + + @AnyThread + fun resetGroups(clazz: Class) { + executors.diskIO().execute { + val uid = clazz.uid + val groups = database.classGroupDao().getGroupsFromClassDirect(uid) + groups.forEach { + val id = it.uid + database.classItemDao().clearFromGroup(id) + database.classMaterialDao().clearFromGroup(id) + } + } + } + + fun getMaterialsFromClassItem(classItemId: Long): LiveData> { + return database.classMaterialDao().getMaterialsFromClassItem(classItemId) + } + + fun updateLocationVisibilityAsync(location: Long, status: Boolean) { + executors.diskIO().execute { + database.classLocationDao().setClassHiddenHidden(status, location) + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/DocumentsRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/DocumentsRepository.kt index cbc7c1181..d1597de8e 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/DocumentsRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/DocumentsRepository.kt @@ -32,10 +32,10 @@ import com.forcetower.uefs.core.model.unes.Document import com.forcetower.uefs.core.model.unes.SagresDocument import com.forcetower.uefs.core.storage.database.UDatabase import com.forcetower.uefs.core.storage.resource.Resource -import timber.log.Timber import java.io.File import javax.inject.Inject import javax.inject.Singleton +import timber.log.Timber @Singleton class DocumentsRepository @Inject constructor( diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/EvaluationRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/EvaluationRepository.kt index 930897edf..b267fb71f 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/EvaluationRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/EvaluationRepository.kt @@ -21,35 +21,21 @@ package com.forcetower.uefs.core.storage.repository import android.content.SharedPreferences -import androidx.annotation.AnyThread -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData -import com.forcetower.uefs.AppExecutors -import com.forcetower.uefs.core.model.service.EvaluationDiscipline -import com.forcetower.uefs.core.model.service.EvaluationHomeTopic -import com.forcetower.uefs.core.model.service.EvaluationTeacher import com.forcetower.uefs.core.model.unes.EdgeParadoxSearchableItem -import com.forcetower.uefs.core.model.unes.EvaluationEntity -import com.forcetower.uefs.core.model.unes.Question import com.forcetower.uefs.core.storage.database.UDatabase -import com.forcetower.uefs.core.storage.network.EdgeService import com.forcetower.uefs.core.storage.network.ParadoxService -import com.forcetower.uefs.core.storage.network.UService -import com.forcetower.uefs.core.storage.network.adapter.asLiveData -import com.forcetower.uefs.core.storage.resource.NetworkOnlyResource -import com.forcetower.uefs.core.storage.resource.Resource import com.forcetower.uefs.domain.model.paradox.DisciplineCombinedData import com.forcetower.uefs.domain.model.paradox.SemesterMean import com.forcetower.uefs.domain.model.paradox.TeacherMean -import kotlinx.coroutines.flow.Flow -import timber.log.Timber import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.util.Calendar import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import timber.log.Timber class EvaluationRepository @Inject constructor( private val database: UDatabase, @@ -67,9 +53,11 @@ class EvaluationRepository @Inject constructor( val semester = entry.value val studentCountWeighted = semester.sumOf { it.studentCountWeighted } - val mean = if (studentCountWeighted > 0) + val mean = if (studentCountWeighted > 0) { semester.sumOf { it.mean * it.studentCountWeighted } / studentCountWeighted - else 0.0 + } else { + 0.0 + } val first = semester.first() val startedAt = ZonedDateTime.parse(first.semesterStart, DateTimeFormatter.ISO_ZONED_DATE_TIME) @@ -88,9 +76,11 @@ class EvaluationRepository @Inject constructor( val studentCount = values.sumOf { it.studentsCount } val studentCountWeighted = values.sumOf { it.studentCountWeighted } - val mean = if (studentCountWeighted > 0) + val mean = if (studentCountWeighted > 0) { values.sumOf { it.mean * it.studentCountWeighted } / studentCountWeighted - else 0.0 + } else { + 0.0 + } val appearTime = ZonedDateTime.parse(appear.semesterStart, DateTimeFormatter.ISO_ZONED_DATE_TIME) diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/FirebaseMessageRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/FirebaseMessageRepository.kt index 1ed2736eb..7787c81ca 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/FirebaseMessageRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/FirebaseMessageRepository.kt @@ -1,317 +1,318 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.repository - -import android.content.Context -import android.content.SharedPreferences -import androidx.work.WorkManager -import com.forcetower.sagres.SagresNavigator -import com.forcetower.uefs.AppExecutors -import com.forcetower.uefs.core.model.edge.account.SendMessagingTokenDTO -import com.forcetower.uefs.core.model.unes.Message -import com.forcetower.uefs.core.notification.StatementNotificationProcessor -import com.forcetower.uefs.core.storage.database.UDatabase -import com.forcetower.uefs.core.storage.network.EdgeService -import com.forcetower.uefs.core.storage.network.UService -import com.forcetower.uefs.core.work.hourglass.HourglassContributeWorker -import com.forcetower.uefs.core.work.sync.SyncLinkedWorker -import com.forcetower.uefs.core.work.sync.SyncMainWorker -import com.forcetower.uefs.feature.shared.extensions.toBooleanOrNull -import com.forcetower.uefs.service.NotificationCreator -import com.google.android.gms.tasks.Tasks -import com.google.firebase.messaging.FirebaseMessaging -import com.google.firebase.messaging.RemoteMessage -import kotlinx.coroutines.tasks.await -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class FirebaseMessageRepository @Inject constructor( - private val service: UService, - private val edgeService: EdgeService, - private val database: UDatabase, - private val preferences: SharedPreferences, - private val context: Context, - private val syncRepository: SagresSyncRepository, - private val firebaseMessaging: FirebaseMessaging, - private val executors: AppExecutors -) { - suspend fun onMessageReceived(message: RemoteMessage) { - val data = message.data - when { - data.keys.isNotEmpty() -> onDataMessageReceived(data) - message.notification != null -> onSimpleMessageReceived(message) - else -> Timber.d("An invalid message was received") - } - } - - private suspend fun onDataMessageReceived(data: Map) { - Timber.d("Data message received") - when (data["identifier"]) { - "event" -> eventNotification(data) - "teacher" -> teacherNotification(data) - "remote_database" -> promoteDatabase(data) - "service" -> serviceNotificationExtractor(data) - "synchronize" -> universalSync() - "reschedule_sync" -> rescheduleSync(data) - "hourglass_initiator" -> hourglassRunner() - "worker_cancel" -> cancelWorker(data) - "remote_preferences" -> promotePreferences(data) - null -> Timber.e("Invalid notification received. No Identifier.") - } - } - - private fun serviceNotificationExtractor(data: Map) { - val typed = data["service_typed"] - // Legacy notifications... - if (typed == null) { - serviceNotification(data) - } else { - when (typed) { - "statement_received", "statement_received_hidden" -> statementReceivedBackbone(data) - } - } - } - - private fun statementReceivedBackbone(data: Map) { - StatementNotificationProcessor.onStatementReceived(context, data) - } - - private fun promotePreferences(data: Map) { - val type = data["type"] - val key = data["key"] - val value = data["value"] - - type ?: return - key ?: return - value ?: return - - val editor = preferences.edit() - when (type) { - "int" -> { - val integer = value.toIntOrNull() - if (integer != null) editor.putInt(key, integer) - } - "float" -> { - val float = value.toFloatOrNull() - if (float != null) editor.putFloat(key, float) - } - "boolean" -> { - val bool = value.toBooleanOrNull() - if (bool != null) editor.putBoolean(key, bool) - } - "long" -> { - val long = value.toLongOrNull() - if (long != null) editor.putLong(key, long) - } - "string" -> { - editor.putString(key, value) - } - } - - editor.apply() - } - - private fun cancelWorker(data: Map) { - val tag = data["tag"] - tag ?: return - WorkManager.getInstance(context).cancelAllWorkByTag(tag) - } - - private fun hourglassRunner() { - HourglassContributeWorker.createWorker(context) - } - - private fun rescheduleSync(data: Map) { - val current = preferences.getString("stg_sync_frequency", "60")?.toIntOrNull() ?: 60 - val period = data["period"]?.toIntOrNull() ?: current - val forced = data["forced"]?.toBooleanOrNull() ?: true - - /** - * Se a frequencia atual for maior que a recomendada e a sincronização não for forçada, - * nada precisa ser feito. - * - * Note que não é maior ou igual, este método se torna conveniente para que o remetente - * possa enviar uma mensagem somente com o identificador reschedule_sync e o aparelho irá - * fazer o reschedule mesmo que nenhum parâmetro seja passado. - **/ - if (current > period && !forced) { - Timber.d("No action needed") - } else { - when (preferences.getString("stg_sync_worker_type", "0")?.toIntOrNull() ?: 0) { - 0 -> SyncMainWorker.createWorker(context, period, true) - 1 -> { - SyncLinkedWorker.stopWorker(context) - SyncLinkedWorker.createWorker(context, period, true) - } - } - preferences.edit().putString("stg_sync_frequency", period.toString()).apply() - Timber.d("Target rescheduled") - } - } - - private suspend fun universalSync() { - syncRepository.performSync("Universal") - } - - private fun teacherNotification(data: Map) { - val message = data["message"] - val teacher = data["teacher"] - val discipline = data["discipline"] - val timestamp = data["timestamp"] - - if (message == null || teacher == null || timestamp == null || discipline == null) { - Timber.e("Invalid notification received. No message, teacher or timestamp") - return - } - - val sent = timestamp.toLongOrNull() - if (sent == null) { - Timber.e("Invalid notification received. Send time is invalid. Teacher: $teacher, $message") - return - } - - val default = Message(content = message, sagresId = System.currentTimeMillis(), notified = true, senderName = teacher, senderProfile = -2, timestamp = sent, discipline = discipline) - val uid = database.messageDao().insert(default) - NotificationCreator.showSagresMessageNotification(default, context, uid) - } - - private fun serviceNotification(data: Map) { - val title = data["title"] - val message = data["message"]?.replace("\\n", "\n") - val image = data["image"] - val institution = data["institution"] - val course = data["course"]?.toLongOrNull() - - if (title == null || message == null) { - Timber.e("Bad notification created. It was ignored") - return - } - - if (course != null) { - val profile = database.profileDao().selectMeDirect() - if (profile != null && profile.course != course) { - return - } - } - - if (institution == null || institution == SagresNavigator.instance.getSelectedInstitution()) { - NotificationCreator.showServiceMessageNotification(context, message.hashCode().toLong(), title, message, image) - } - } - - private fun eventNotification(data: Map) { - val id = data["eventId"] - val title = data["title"] - val description = data["description"] - val image = data["image"] - - if (id == null || title == null || description == null) { - Timber.e("Bad notification created. It was ignored") - return - } - - NotificationCreator.showEventNotification(context, id, title, description, image) - } - - // This call is by far the most dangerous call that the hole code may have - // Call allows the server to perform an update in the database at any moment - // It will also be extremely helpful when something wrong happens and the server will be able to fix everyone at once - private fun promoteDatabase(data: Map) { - val query = data["query"] ?: return - val unique = data["unique"] - - if (unique != null) { - val executed = preferences.getBoolean(unique, false) - if (executed) { - Timber.d("Promotion dismissed") - return - } - } - - try { - database.openHelper.writableDatabase.execSQL(query) - if (unique != null) - preferences.edit().putBoolean(unique, true).apply() - } catch (t: Throwable) { - Timber.d("Failed executing database promotion. ${t.message}") - Timber.e(t) - } - } - - private fun onSimpleMessageReceived(message: RemoteMessage) { - Timber.d("Simple notification received") - val notification = message.notification - if (notification == null) { - Timber.e("Invalidation of notification happened really quickly") - return - } - - val content = notification.body - val title = notification.title - - if (content == null || title == null) { - Timber.e("Bad notification created. It was ignored") - return - } - - NotificationCreator.showSimpleNotification(context, title, content) - } - - suspend fun onNewToken(token: String) { - val auth = database.accessTokenDao().getAccessTokenDirectSuspend() - if (auth != null) { - runCatching { service.sendTokenSuspend(mapOf("token" to token)) } - } else { - Timber.d("Disconnected Base UNES") - } - - val auth2 = database.edgeAccessToken.require() - if (auth2 != null) { - runCatching { edgeService.fcm(SendMessagingTokenDTO(token)) } - } - - preferences.edit().putString("current_firebase_token", token).apply() - } - - fun subscribe(topics: Array) { - executors.networkIO().execute { - topics.map { firebaseMessaging.subscribeToTopic(it) }.forEach { task -> - try { - Tasks.await(task) - } catch (t: Throwable) { - Timber.e(t) - } - } - } - } - - suspend fun sendNewTokenOrNot() { - try { - val task = FirebaseMessaging.getInstance().token - val value = task.await() - onNewToken(value) - } catch (e: Throwable) { - Timber.e(e, "Failed to update fcm token") - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.repository + +import android.content.Context +import android.content.SharedPreferences +import androidx.work.WorkManager +import com.forcetower.sagres.SagresNavigator +import com.forcetower.uefs.AppExecutors +import com.forcetower.uefs.core.model.edge.account.SendMessagingTokenDTO +import com.forcetower.uefs.core.model.unes.Message +import com.forcetower.uefs.core.notification.StatementNotificationProcessor +import com.forcetower.uefs.core.storage.database.UDatabase +import com.forcetower.uefs.core.storage.network.EdgeService +import com.forcetower.uefs.core.storage.network.UService +import com.forcetower.uefs.core.work.hourglass.HourglassContributeWorker +import com.forcetower.uefs.core.work.sync.SyncLinkedWorker +import com.forcetower.uefs.core.work.sync.SyncMainWorker +import com.forcetower.uefs.feature.shared.extensions.toBooleanOrNull +import com.forcetower.uefs.service.NotificationCreator +import com.google.android.gms.tasks.Tasks +import com.google.firebase.messaging.FirebaseMessaging +import com.google.firebase.messaging.RemoteMessage +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.tasks.await +import timber.log.Timber + +@Singleton +class FirebaseMessageRepository @Inject constructor( + private val service: UService, + private val edgeService: EdgeService, + private val database: UDatabase, + private val preferences: SharedPreferences, + private val context: Context, + private val syncRepository: SagresSyncRepository, + private val firebaseMessaging: FirebaseMessaging, + private val executors: AppExecutors +) { + suspend fun onMessageReceived(message: RemoteMessage) { + val data = message.data + when { + data.keys.isNotEmpty() -> onDataMessageReceived(data) + message.notification != null -> onSimpleMessageReceived(message) + else -> Timber.d("An invalid message was received") + } + } + + private suspend fun onDataMessageReceived(data: Map) { + Timber.d("Data message received") + when (data["identifier"]) { + "event" -> eventNotification(data) + "teacher" -> teacherNotification(data) + "remote_database" -> promoteDatabase(data) + "service" -> serviceNotificationExtractor(data) + "synchronize" -> universalSync() + "reschedule_sync" -> rescheduleSync(data) + "hourglass_initiator" -> hourglassRunner() + "worker_cancel" -> cancelWorker(data) + "remote_preferences" -> promotePreferences(data) + null -> Timber.e("Invalid notification received. No Identifier.") + } + } + + private fun serviceNotificationExtractor(data: Map) { + val typed = data["service_typed"] + // Legacy notifications... + if (typed == null) { + serviceNotification(data) + } else { + when (typed) { + "statement_received", "statement_received_hidden" -> statementReceivedBackbone(data) + } + } + } + + private fun statementReceivedBackbone(data: Map) { + StatementNotificationProcessor.onStatementReceived(context, data) + } + + private fun promotePreferences(data: Map) { + val type = data["type"] + val key = data["key"] + val value = data["value"] + + type ?: return + key ?: return + value ?: return + + val editor = preferences.edit() + when (type) { + "int" -> { + val integer = value.toIntOrNull() + if (integer != null) editor.putInt(key, integer) + } + "float" -> { + val float = value.toFloatOrNull() + if (float != null) editor.putFloat(key, float) + } + "boolean" -> { + val bool = value.toBooleanOrNull() + if (bool != null) editor.putBoolean(key, bool) + } + "long" -> { + val long = value.toLongOrNull() + if (long != null) editor.putLong(key, long) + } + "string" -> { + editor.putString(key, value) + } + } + + editor.apply() + } + + private fun cancelWorker(data: Map) { + val tag = data["tag"] + tag ?: return + WorkManager.getInstance(context).cancelAllWorkByTag(tag) + } + + private fun hourglassRunner() { + HourglassContributeWorker.createWorker(context) + } + + private fun rescheduleSync(data: Map) { + val current = preferences.getString("stg_sync_frequency", "60")?.toIntOrNull() ?: 60 + val period = data["period"]?.toIntOrNull() ?: current + val forced = data["forced"]?.toBooleanOrNull() ?: true + + /** + * Se a frequencia atual for maior que a recomendada e a sincronização não for forçada, + * nada precisa ser feito. + * + * Note que não é maior ou igual, este método se torna conveniente para que o remetente + * possa enviar uma mensagem somente com o identificador reschedule_sync e o aparelho irá + * fazer o reschedule mesmo que nenhum parâmetro seja passado. + **/ + if (current > period && !forced) { + Timber.d("No action needed") + } else { + when (preferences.getString("stg_sync_worker_type", "0")?.toIntOrNull() ?: 0) { + 0 -> SyncMainWorker.createWorker(context, period, true) + 1 -> { + SyncLinkedWorker.stopWorker(context) + SyncLinkedWorker.createWorker(context, period, true) + } + } + preferences.edit().putString("stg_sync_frequency", period.toString()).apply() + Timber.d("Target rescheduled") + } + } + + private suspend fun universalSync() { + syncRepository.performSync("Universal") + } + + private fun teacherNotification(data: Map) { + val message = data["message"] + val teacher = data["teacher"] + val discipline = data["discipline"] + val timestamp = data["timestamp"] + + if (message == null || teacher == null || timestamp == null || discipline == null) { + Timber.e("Invalid notification received. No message, teacher or timestamp") + return + } + + val sent = timestamp.toLongOrNull() + if (sent == null) { + Timber.e("Invalid notification received. Send time is invalid. Teacher: $teacher, $message") + return + } + + val default = Message(content = message, sagresId = System.currentTimeMillis(), notified = true, senderName = teacher, senderProfile = -2, timestamp = sent, discipline = discipline) + val uid = database.messageDao().insert(default) + NotificationCreator.showSagresMessageNotification(default, context, uid) + } + + private fun serviceNotification(data: Map) { + val title = data["title"] + val message = data["message"]?.replace("\\n", "\n") + val image = data["image"] + val institution = data["institution"] + val course = data["course"]?.toLongOrNull() + + if (title == null || message == null) { + Timber.e("Bad notification created. It was ignored") + return + } + + if (course != null) { + val profile = database.profileDao().selectMeDirect() + if (profile != null && profile.course != course) { + return + } + } + + if (institution == null || institution == SagresNavigator.instance.getSelectedInstitution()) { + NotificationCreator.showServiceMessageNotification(context, message.hashCode().toLong(), title, message, image) + } + } + + private fun eventNotification(data: Map) { + val id = data["eventId"] + val title = data["title"] + val description = data["description"] + val image = data["image"] + + if (id == null || title == null || description == null) { + Timber.e("Bad notification created. It was ignored") + return + } + + NotificationCreator.showEventNotification(context, id, title, description, image) + } + + // This call is by far the most dangerous call that the hole code may have + // Call allows the server to perform an update in the database at any moment + // It will also be extremely helpful when something wrong happens and the server will be able to fix everyone at once + private fun promoteDatabase(data: Map) { + val query = data["query"] ?: return + val unique = data["unique"] + + if (unique != null) { + val executed = preferences.getBoolean(unique, false) + if (executed) { + Timber.d("Promotion dismissed") + return + } + } + + try { + database.openHelper.writableDatabase.execSQL(query) + if (unique != null) { + preferences.edit().putBoolean(unique, true).apply() + } + } catch (t: Throwable) { + Timber.d("Failed executing database promotion. ${t.message}") + Timber.e(t) + } + } + + private fun onSimpleMessageReceived(message: RemoteMessage) { + Timber.d("Simple notification received") + val notification = message.notification + if (notification == null) { + Timber.e("Invalidation of notification happened really quickly") + return + } + + val content = notification.body + val title = notification.title + + if (content == null || title == null) { + Timber.e("Bad notification created. It was ignored") + return + } + + NotificationCreator.showSimpleNotification(context, title, content) + } + + suspend fun onNewToken(token: String) { + val auth = database.accessTokenDao().getAccessTokenDirectSuspend() + if (auth != null) { + runCatching { service.sendTokenSuspend(mapOf("token" to token)) } + } else { + Timber.d("Disconnected Base UNES") + } + + val auth2 = database.edgeAccessToken.require() + if (auth2 != null) { + runCatching { edgeService.fcm(SendMessagingTokenDTO(token)) } + } + + preferences.edit().putString("current_firebase_token", token).apply() + } + + fun subscribe(topics: Array) { + executors.networkIO().execute { + topics.map { firebaseMessaging.subscribeToTopic(it) }.forEach { task -> + try { + Tasks.await(task) + } catch (t: Throwable) { + Timber.e(t) + } + } + } + } + + suspend fun sendNewTokenOrNot() { + try { + val task = FirebaseMessaging.getInstance().token + val value = task.await() + onNewToken(value) + } catch (e: Throwable) { + Timber.e(e, "Failed to update fcm token") + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/FlowchartRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/FlowchartRepository.kt index 2389db6fd..4c4f7eab7 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/FlowchartRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/FlowchartRepository.kt @@ -35,8 +35,8 @@ import com.forcetower.uefs.core.storage.network.UService import com.forcetower.uefs.core.storage.network.adapter.asLiveData import com.forcetower.uefs.core.storage.resource.NetworkBoundResource import com.forcetower.uefs.core.storage.resource.Resource -import timber.log.Timber import javax.inject.Inject +import timber.log.Timber class FlowchartRepository @Inject constructor( private val database: UDatabase, diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/FormsRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/FormsRepository.kt index 25f2d8a5a..d81b39b43 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/FormsRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/FormsRepository.kt @@ -23,11 +23,11 @@ package com.forcetower.uefs.core.storage.repository import android.content.SharedPreferences import com.forcetower.uefs.AppExecutors import com.forcetower.uefs.core.storage.database.UDatabase +import javax.inject.Inject import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.Request import timber.log.Timber -import javax.inject.Inject class FormsRepository @Inject constructor( private val client: OkHttpClient, diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/LoginSagresRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/LoginSagresRepository.kt index e33546d34..2a94efad7 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/LoginSagresRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/LoginSagresRepository.kt @@ -1,478 +1,481 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.repository - -import android.content.Context -import android.content.SharedPreferences -import androidx.annotation.MainThread -import androidx.annotation.StringRes -import androidx.annotation.WorkerThread -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData -import com.forcetower.sagres.SagresNavigator -import com.forcetower.sagres.database.model.SagresCalendar -import com.forcetower.sagres.database.model.SagresCredential -import com.forcetower.sagres.database.model.SagresDiscipline -import com.forcetower.sagres.database.model.SagresDisciplineClassLocation -import com.forcetower.sagres.database.model.SagresDisciplineGroup -import com.forcetower.sagres.database.model.SagresDisciplineMissedClass -import com.forcetower.sagres.database.model.SagresGrade -import com.forcetower.sagres.database.model.SagresMessage -import com.forcetower.sagres.database.model.SagresPerson -import com.forcetower.sagres.database.model.SagresRequestedService -import com.forcetower.sagres.operation.Callback -import com.forcetower.sagres.operation.Status -import com.forcetower.sagres.parsers.SagresBasicParser -import com.forcetower.uefs.AppExecutors -import com.forcetower.uefs.R -import com.forcetower.uefs.core.model.unes.Access -import com.forcetower.uefs.core.model.unes.CalendarItem -import com.forcetower.uefs.core.model.unes.Discipline -import com.forcetower.uefs.core.model.unes.Message -import com.forcetower.uefs.core.model.unes.Semester -import com.forcetower.uefs.core.model.unes.ServiceRequest -import com.forcetower.uefs.core.storage.database.UDatabase -import com.forcetower.uefs.core.storage.repository.cloud.AuthRepository -import com.forcetower.uefs.core.util.LocationShrinker -import com.forcetower.uefs.core.util.isStudentFromUEFS -import com.forcetower.uefs.core.util.toLiveData -import com.forcetower.uefs.core.work.grades.GradesSagresWorker -import com.forcetower.uefs.core.work.hourglass.HourglassContributeWorker -import kotlinx.coroutines.runBlocking -import org.jsoup.nodes.Document -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class LoginSagresRepository @Inject constructor( - private val executor: AppExecutors, - private val database: UDatabase, - private val preferences: SharedPreferences, - private val authRepository: AuthRepository, - private val sessionRepository: CookieSessionRepository, - private val context: Context -) { - val currentStep: MutableLiveData = MutableLiveData() - - fun getAccess(): LiveData = database.accessDao().getAccess() - - fun stopCurrentLogin() { - SagresNavigator.instance.stopTags("aLogin") - } - - fun getProfileMe() = database.profileDao().selectMe() - - @MainThread - fun login(username: String, password: String, captcha: String?, deleteDatabase: Boolean = false, skipLogin: Boolean = false): LiveData { - val signIn = MediatorLiveData() - resetSteps() - if (deleteDatabase) { - currentStep.value = createStep(R.string.step_delete_database) - executor.diskIO().execute { - database.accessDao().deleteAll() - database.messageDao().deleteAll() - database.accessTokenDao().deleteAll() - database.calendarDao().delete() - database.profileDao().deleteMe() - database.semesterDao().deleteAll() - executor.mainThread().execute { - login(signIn, username, password, captcha, skipLogin) - } - } - } else { - incSteps() - login(signIn, username, password, captcha, skipLogin) - } - return signIn - } - - @MainThread - private fun login(data: MediatorLiveData, username: String, password: String, captcha: String?, skipLogin: Boolean) { - SagresNavigator.instance.putCredentials(SagresCredential(username, password, SagresNavigator.instance.getSelectedInstitution())) - - val source = if (!skipLogin) { - currentStep.value = createStep(R.string.step_logging_in) - SagresNavigator.instance.aLogin(username, password, captcha).toLiveData() - } else { - currentStep.value = createStep(R.string.step_login_bypassed) - SagresNavigator.instance.aStartPage().toLiveData() - } - data.addSource(source) { l -> - if (l.status == Status.SUCCESS) { - data.removeSource(source) - val score = SagresBasicParser.getScore(l.document) - Timber.d("Login Completed. Score parsed: $score") - executor.diskIO().execute { - database.accessDao().insert(username, password) - if (preferences.isStudentFromUEFS()) { - val baked = sessionRepository.getSavedBiscuit() - authRepository.syncLogin(username, password, baked?.auth, baked?.sessionId) - // TODO Remove this SHAMELESS call - runBlocking { - sessionRepository.findAndSaveCookies() - } - } - } - me(data, score, Access(username = username, password = password), l.document!!) - } else { - SagresNavigator.instance.putCredentials(null) - data.value = Callback.Builder(l.status) - .code(l.code) - .message(l.message) - .throwable(l.throwable) - .document(l.document) - .build() - } - } - } - - private fun me(data: MediatorLiveData, score: Double, access: Access, document: Document) { - currentStep.value = createStep(R.string.step_finding_profile) - val username = access.username - - if (username.contains("@") || !preferences.isStudentFromUEFS()) { - continueUsingHtml(document, username, score, access, data) - } else { - val me = SagresNavigator.instance.aMe().toLiveData() - data.addSource(me) { m -> - Timber.d("Me status ${m.status} ${m.code} ${m.throwable}") - if (m.status == Status.SUCCESS) { - data.removeSource(me) - Timber.d("Me Completed. You are ${m.person?.name} and your CPF is ${m.person?.getCpf()}") - val person = m.person - if (person != null) { - executor.diskIO().execute { database.profileDao().insert(person, score) } - messages(data, person.id) - } else { - Timber.d("SPerson is null") - } - } else if (m.status == Status.RESPONSE_FAILED || m.status == Status.NETWORK_ERROR) { - m.throwable?.printStackTrace() - continueUsingHtml(document, username, score, access, data) - } else { - Timber.d("The status ${m.status}") - data.value = Callback.Builder(m.status) - .code(m.code) - .message(m.message) - .throwable(m.throwable) - .document(m.document) - .build() - } - } - } - } - - private fun continueUsingHtml(document: Document, username: String, score: Double, access: Access, data: MediatorLiveData) { - val name = SagresBasicParser.getName(document) ?: username - val person = SagresPerson(username.hashCode().toLong(), name, name, "00000000000", username) - executor.diskIO().execute { database.profileDao().insert(person, score) } - messages(data, null) - } - - private fun messages(data: MediatorLiveData, userId: Long?) { - val messages = if (userId != null) - SagresNavigator.instance.aMessages(userId).toLiveData() - else - SagresNavigator.instance.aMessagesHtml().toLiveData() - - currentStep.value = createStep(R.string.step_fetching_messages) - data.addSource(messages) { m -> - if (m.status == Status.SUCCESS) { - data.removeSource(messages) - val values = m.messages!!.map { Message.fromMessage(it, true) } - executor.diskIO().execute { database.messageDao().insertIgnoring(values) } - Timber.d("Messages Completed") - Timber.d("You got: ${m.messages}") - if (userId != null) - semesters(data, userId) - else - disciplinesExperimental(data) - } else { - data.value = Callback.Builder(m.status) - .code(m.code) - .message(m.message) - .throwable(m.throwable) - .document(m.document) - .build() - } - } - } - - private fun semesters(data: MediatorLiveData, userId: Long) { - val semesters = SagresNavigator.instance.aSemesters(userId).toLiveData() - currentStep.value = createStep(R.string.step_fetching_semesters) - data.addSource(semesters) { s -> - if (s.status == Status.SUCCESS) { - data.removeSource(semesters) - val values = s.getSemesters().map { Semester.fromSagres(it) } - executor.diskIO().execute { database.semesterDao().insertIgnoring(values) } - Timber.d("Semesters Completed") - Timber.d("You got: ${s.getSemesters()}") - disciplinesExperimental(data) - } else { - data.value = Callback.Builder(s.status) - .code(s.code) - .message(s.message) - .throwable(s.throwable) - .document(s.document) - .build() - } - } - } - - private fun disciplinesExperimental(data: MediatorLiveData) { - Timber.d("Disciplines Experimental") - val experimental = SagresNavigator.instance.aDisciplinesExperimental().toLiveData() - currentStep.value = createStep(R.string.step_discipline_experimental) - data.addSource(experimental) { e -> - Timber.d("Experimental Status: ${e.status}") - if (e.status == Status.COMPLETED) { - data.removeSource(experimental) - executor.diskIO().execute { - defineSemesters(e.getSemesters()) - defineDisciplines(e.getDisciplines()) - defineDisciplineGroups(e.getGroups()) - } - defineExperimentalWorkers() - startPage(data) - } - } - } - - private fun defineExperimentalWorkers() { - if (preferences.isStudentFromUEFS()) - HourglassContributeWorker.createWorker(context) - preferences.edit().putBoolean("sent_hourglass_testing_data_0.0.0", true).apply() - } - - private fun startPage(data: MediatorLiveData) { - val start = SagresNavigator.instance.aStartPage().toLiveData() - currentStep.value = createStep(R.string.step_moving_to_start_page) - data.addSource(start) { s -> - if (s.status == Status.SUCCESS) { - data.removeSource(start) - - executor.diskIO().execute { - defineMessages(s.messages) - defineCalendar(s.calendar) - defineDisciplines(s.disciplines) - defineDisciplineGroups(s.groups) - defineSchedule(s.locations) - } - - Timber.d("Start Page completed") - Timber.d("Semesters: ${s.semesters}") - Timber.d("Disciplines: ${s.disciplines}") - Timber.d("Calendar: ${s.calendar}") - grades(data) - } else { - data.value = Callback.Builder(s.status) - .code(s.code) - .message(s.message) - .throwable(s.throwable) - .document(s.document) - .build() - } - } - } - - private fun grades(data: MediatorLiveData) { - Timber.d("Grades Fetch!") - val grades = SagresNavigator.instance.aGetCurrentGrades().toLiveData() - currentStep.value = createStep(R.string.step_fetching_grades) - data.addSource(grades) { g -> - when (g.status) { - Status.SUCCESS -> { - data.removeSource(grades) - - Timber.d("Grades received: ${g.grades}") - Timber.d("Frequency: ${g.frequency}") - Timber.d("Semesters: ${g.semesters}") - - executor.diskIO().execute { - defineSemesters(g.semesters) - defineGrades(g.grades) - defineFrequency(g.frequency) - database.gradesDao().markAllNotified() - Timber.d("Execute default") - val semesters = g.semesters?.map { pair -> pair.first } ?: emptyList() - defineGradesWorkers(semesters) - } - - services(data) - } - Status.LOADING -> { - data.value = Callback.Builder(g.status) - .code(g.code) - .message(g.message) - .throwable(g.throwable) - .document(g.document) - .build() - } - else -> { - Timber.d("Data status: ${g.status} ${g.code} ${g.throwable?.message}") - data.value = Callback.Builder(Status.GRADES_FAILED) - .code(g.code) - .message(g.message) - .throwable(g.throwable) - .document(g.document) - .build() - } - } - } - } - - @MainThread - private fun services(data: MediatorLiveData) { - Timber.d("Services fetch") - val services = SagresNavigator.instance.aGetRequestedServices().toLiveData() - data.addSource(services) { s -> - when (s.status) { - Status.SUCCESS -> { - data.removeSource(services) - executor.diskIO().execute { - defineServices(s.services) - database.serviceRequestDao().markAllNotified() - } - data.value = Callback.Builder(s.status).document(s.document).build() - } - Status.LOADING -> { - data.value = Callback.Builder(s.status) - .code(s.code) - .message(s.message) - .throwable(s.throwable) - .document(s.document) - .build() - } - else -> { - Timber.d("ANOTHER ONE!") - executor.networkIO().execute { - // TODO Remove this other shameless call - runBlocking { - sessionRepository.findAndSaveCookies() - } - } - data.value = Callback.Builder(Status.COMPLETED) - .code(s.code) - .message(s.message) - .throwable(s.throwable) - .document(s.document) - .build() - } - } - } - } - - private fun defineServices(services: List) { - val list = services.map { ServiceRequest.fromSagres(it) } - database.serviceRequestDao().insertList(list) - } - - private fun defineGradesWorkers(semesters: List) { - semesters.forEach { - GradesSagresWorker.createWorker(context, it) - } - } - - @WorkerThread - private fun defineFrequency(frequency: List?) { - if (frequency == null) return - database.classAbsenceDao().putAbsences(frequency) - } - - @WorkerThread - private fun defineGrades(grades: List?) { - grades?.run { - database.gradesDao().putGrades(grades) - } - } - - @WorkerThread - private fun defineSemesters(semesters: List>?) { - semesters?.forEach { - val semester = Semester(sagresId = it.first, name = it.second, codename = it.second) - database.semesterDao().insertIgnoring(semester) - } - } - - @WorkerThread - private fun defineSchedule(locations: List?) { - if (locations == null) return - val ordering = preferences.getBoolean("stg_semester_deterministic_ordering", true) - val shrinkSchedule = preferences.getBoolean("stg_schedule_shrinking", true) - if (shrinkSchedule) { - val shrink = LocationShrinker.shrink(locations) - database.classLocationDao().putSchedule(shrink, ordering) - } else { - database.classLocationDao().putSchedule(locations, ordering) - } - } - - @WorkerThread - private fun defineDisciplineGroups(groups: List?) { - groups ?: return - database.classGroupDao().defineGroups(groups) - } - - @WorkerThread - private fun defineDisciplines(disciplines: List?) { - disciplines ?: return - val values = disciplines.map { Discipline.fromSagres(it) } - database.disciplineDao().insert(values) - disciplines.forEach { database.classDao().insert(it, true) } - } - - @WorkerThread - private fun defineCalendar(calendar: List?) { - val values = calendar?.map { CalendarItem.fromSagres(it) } - database.calendarDao().deleteAndInsert(values) - } - - private fun defineMessages(messages: List?) { - messages ?: return - messages.reversed().forEachIndexed { index, message -> - message.processingTime = System.currentTimeMillis() + index - } - val values = messages.map { Message.fromMessage(it, true) } - database.messageDao().insertIgnoring(values) - } - - companion object { - private var currentStep = 0 - private const val stepCount = 7 - - fun resetSteps() { - currentStep = 0 - } - - fun incSteps() { - currentStep++ - } - - fun createStep(@StringRes desc: Int): Step = Step(currentStep++, desc) - } - - data class Step(val step: Int, @StringRes val res: Int) { - val count = stepCount - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.repository + +import android.content.Context +import android.content.SharedPreferences +import androidx.annotation.MainThread +import androidx.annotation.StringRes +import androidx.annotation.WorkerThread +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import com.forcetower.sagres.SagresNavigator +import com.forcetower.sagres.database.model.SagresCalendar +import com.forcetower.sagres.database.model.SagresCredential +import com.forcetower.sagres.database.model.SagresDiscipline +import com.forcetower.sagres.database.model.SagresDisciplineClassLocation +import com.forcetower.sagres.database.model.SagresDisciplineGroup +import com.forcetower.sagres.database.model.SagresDisciplineMissedClass +import com.forcetower.sagres.database.model.SagresGrade +import com.forcetower.sagres.database.model.SagresMessage +import com.forcetower.sagres.database.model.SagresPerson +import com.forcetower.sagres.database.model.SagresRequestedService +import com.forcetower.sagres.operation.Callback +import com.forcetower.sagres.operation.Status +import com.forcetower.sagres.parsers.SagresBasicParser +import com.forcetower.uefs.AppExecutors +import com.forcetower.uefs.R +import com.forcetower.uefs.core.model.unes.Access +import com.forcetower.uefs.core.model.unes.CalendarItem +import com.forcetower.uefs.core.model.unes.Discipline +import com.forcetower.uefs.core.model.unes.Message +import com.forcetower.uefs.core.model.unes.Semester +import com.forcetower.uefs.core.model.unes.ServiceRequest +import com.forcetower.uefs.core.storage.database.UDatabase +import com.forcetower.uefs.core.storage.repository.cloud.AuthRepository +import com.forcetower.uefs.core.util.LocationShrinker +import com.forcetower.uefs.core.util.isStudentFromUEFS +import com.forcetower.uefs.core.util.toLiveData +import com.forcetower.uefs.core.work.grades.GradesSagresWorker +import com.forcetower.uefs.core.work.hourglass.HourglassContributeWorker +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.runBlocking +import org.jsoup.nodes.Document +import timber.log.Timber + +@Singleton +class LoginSagresRepository @Inject constructor( + private val executor: AppExecutors, + private val database: UDatabase, + private val preferences: SharedPreferences, + private val authRepository: AuthRepository, + private val sessionRepository: CookieSessionRepository, + private val context: Context +) { + val currentStep: MutableLiveData = MutableLiveData() + + fun getAccess(): LiveData = database.accessDao().getAccess() + + fun stopCurrentLogin() { + SagresNavigator.instance.stopTags("aLogin") + } + + fun getProfileMe() = database.profileDao().selectMe() + + @MainThread + fun login(username: String, password: String, captcha: String?, deleteDatabase: Boolean = false, skipLogin: Boolean = false): LiveData { + val signIn = MediatorLiveData() + resetSteps() + if (deleteDatabase) { + currentStep.value = createStep(R.string.step_delete_database) + executor.diskIO().execute { + database.accessDao().deleteAll() + database.messageDao().deleteAll() + database.accessTokenDao().deleteAll() + database.calendarDao().delete() + database.profileDao().deleteMe() + database.semesterDao().deleteAll() + executor.mainThread().execute { + login(signIn, username, password, captcha, skipLogin) + } + } + } else { + incSteps() + login(signIn, username, password, captcha, skipLogin) + } + return signIn + } + + @MainThread + private fun login(data: MediatorLiveData, username: String, password: String, captcha: String?, skipLogin: Boolean) { + SagresNavigator.instance.putCredentials(SagresCredential(username, password, SagresNavigator.instance.getSelectedInstitution())) + + val source = if (!skipLogin) { + currentStep.value = createStep(R.string.step_logging_in) + SagresNavigator.instance.aLogin(username, password, captcha).toLiveData() + } else { + currentStep.value = createStep(R.string.step_login_bypassed) + SagresNavigator.instance.aStartPage().toLiveData() + } + data.addSource(source) { l -> + if (l.status == Status.SUCCESS) { + data.removeSource(source) + val score = SagresBasicParser.getScore(l.document) + Timber.d("Login Completed. Score parsed: $score") + executor.diskIO().execute { + database.accessDao().insert(username, password) + if (preferences.isStudentFromUEFS()) { + val baked = sessionRepository.getSavedBiscuit() + authRepository.syncLogin(username, password, baked?.auth, baked?.sessionId) + // TODO Remove this SHAMELESS call + runBlocking { + sessionRepository.findAndSaveCookies() + } + } + } + me(data, score, Access(username = username, password = password), l.document!!) + } else { + SagresNavigator.instance.putCredentials(null) + data.value = Callback.Builder(l.status) + .code(l.code) + .message(l.message) + .throwable(l.throwable) + .document(l.document) + .build() + } + } + } + + private fun me(data: MediatorLiveData, score: Double, access: Access, document: Document) { + currentStep.value = createStep(R.string.step_finding_profile) + val username = access.username + + if (username.contains("@") || !preferences.isStudentFromUEFS()) { + continueUsingHtml(document, username, score, access, data) + } else { + val me = SagresNavigator.instance.aMe().toLiveData() + data.addSource(me) { m -> + Timber.d("Me status ${m.status} ${m.code} ${m.throwable}") + if (m.status == Status.SUCCESS) { + data.removeSource(me) + Timber.d("Me Completed. You are ${m.person?.name} and your CPF is ${m.person?.getCpf()}") + val person = m.person + if (person != null) { + executor.diskIO().execute { database.profileDao().insert(person, score) } + messages(data, person.id) + } else { + Timber.d("SPerson is null") + } + } else if (m.status == Status.RESPONSE_FAILED || m.status == Status.NETWORK_ERROR) { + m.throwable?.printStackTrace() + continueUsingHtml(document, username, score, access, data) + } else { + Timber.d("The status ${m.status}") + data.value = Callback.Builder(m.status) + .code(m.code) + .message(m.message) + .throwable(m.throwable) + .document(m.document) + .build() + } + } + } + } + + private fun continueUsingHtml(document: Document, username: String, score: Double, access: Access, data: MediatorLiveData) { + val name = SagresBasicParser.getName(document) ?: username + val person = SagresPerson(username.hashCode().toLong(), name, name, "00000000000", username) + executor.diskIO().execute { database.profileDao().insert(person, score) } + messages(data, null) + } + + private fun messages(data: MediatorLiveData, userId: Long?) { + val messages = if (userId != null) { + SagresNavigator.instance.aMessages(userId).toLiveData() + } else { + SagresNavigator.instance.aMessagesHtml().toLiveData() + } + + currentStep.value = createStep(R.string.step_fetching_messages) + data.addSource(messages) { m -> + if (m.status == Status.SUCCESS) { + data.removeSource(messages) + val values = m.messages!!.map { Message.fromMessage(it, true) } + executor.diskIO().execute { database.messageDao().insertIgnoring(values) } + Timber.d("Messages Completed") + Timber.d("You got: ${m.messages}") + if (userId != null) { + semesters(data, userId) + } else { + disciplinesExperimental(data) + } + } else { + data.value = Callback.Builder(m.status) + .code(m.code) + .message(m.message) + .throwable(m.throwable) + .document(m.document) + .build() + } + } + } + + private fun semesters(data: MediatorLiveData, userId: Long) { + val semesters = SagresNavigator.instance.aSemesters(userId).toLiveData() + currentStep.value = createStep(R.string.step_fetching_semesters) + data.addSource(semesters) { s -> + if (s.status == Status.SUCCESS) { + data.removeSource(semesters) + val values = s.getSemesters().map { Semester.fromSagres(it) } + executor.diskIO().execute { database.semesterDao().insertIgnoring(values) } + Timber.d("Semesters Completed") + Timber.d("You got: ${s.getSemesters()}") + disciplinesExperimental(data) + } else { + data.value = Callback.Builder(s.status) + .code(s.code) + .message(s.message) + .throwable(s.throwable) + .document(s.document) + .build() + } + } + } + + private fun disciplinesExperimental(data: MediatorLiveData) { + Timber.d("Disciplines Experimental") + val experimental = SagresNavigator.instance.aDisciplinesExperimental().toLiveData() + currentStep.value = createStep(R.string.step_discipline_experimental) + data.addSource(experimental) { e -> + Timber.d("Experimental Status: ${e.status}") + if (e.status == Status.COMPLETED) { + data.removeSource(experimental) + executor.diskIO().execute { + defineSemesters(e.getSemesters()) + defineDisciplines(e.getDisciplines()) + defineDisciplineGroups(e.getGroups()) + } + defineExperimentalWorkers() + startPage(data) + } + } + } + + private fun defineExperimentalWorkers() { + if (preferences.isStudentFromUEFS()) { + HourglassContributeWorker.createWorker(context) + } + preferences.edit().putBoolean("sent_hourglass_testing_data_0.0.0", true).apply() + } + + private fun startPage(data: MediatorLiveData) { + val start = SagresNavigator.instance.aStartPage().toLiveData() + currentStep.value = createStep(R.string.step_moving_to_start_page) + data.addSource(start) { s -> + if (s.status == Status.SUCCESS) { + data.removeSource(start) + + executor.diskIO().execute { + defineMessages(s.messages) + defineCalendar(s.calendar) + defineDisciplines(s.disciplines) + defineDisciplineGroups(s.groups) + defineSchedule(s.locations) + } + + Timber.d("Start Page completed") + Timber.d("Semesters: ${s.semesters}") + Timber.d("Disciplines: ${s.disciplines}") + Timber.d("Calendar: ${s.calendar}") + grades(data) + } else { + data.value = Callback.Builder(s.status) + .code(s.code) + .message(s.message) + .throwable(s.throwable) + .document(s.document) + .build() + } + } + } + + private fun grades(data: MediatorLiveData) { + Timber.d("Grades Fetch!") + val grades = SagresNavigator.instance.aGetCurrentGrades().toLiveData() + currentStep.value = createStep(R.string.step_fetching_grades) + data.addSource(grades) { g -> + when (g.status) { + Status.SUCCESS -> { + data.removeSource(grades) + + Timber.d("Grades received: ${g.grades}") + Timber.d("Frequency: ${g.frequency}") + Timber.d("Semesters: ${g.semesters}") + + executor.diskIO().execute { + defineSemesters(g.semesters) + defineGrades(g.grades) + defineFrequency(g.frequency) + database.gradesDao().markAllNotified() + Timber.d("Execute default") + val semesters = g.semesters?.map { pair -> pair.first } ?: emptyList() + defineGradesWorkers(semesters) + } + + services(data) + } + Status.LOADING -> { + data.value = Callback.Builder(g.status) + .code(g.code) + .message(g.message) + .throwable(g.throwable) + .document(g.document) + .build() + } + else -> { + Timber.d("Data status: ${g.status} ${g.code} ${g.throwable?.message}") + data.value = Callback.Builder(Status.GRADES_FAILED) + .code(g.code) + .message(g.message) + .throwable(g.throwable) + .document(g.document) + .build() + } + } + } + } + + @MainThread + private fun services(data: MediatorLiveData) { + Timber.d("Services fetch") + val services = SagresNavigator.instance.aGetRequestedServices().toLiveData() + data.addSource(services) { s -> + when (s.status) { + Status.SUCCESS -> { + data.removeSource(services) + executor.diskIO().execute { + defineServices(s.services) + database.serviceRequestDao().markAllNotified() + } + data.value = Callback.Builder(s.status).document(s.document).build() + } + Status.LOADING -> { + data.value = Callback.Builder(s.status) + .code(s.code) + .message(s.message) + .throwable(s.throwable) + .document(s.document) + .build() + } + else -> { + Timber.d("ANOTHER ONE!") + executor.networkIO().execute { + // TODO Remove this other shameless call + runBlocking { + sessionRepository.findAndSaveCookies() + } + } + data.value = Callback.Builder(Status.COMPLETED) + .code(s.code) + .message(s.message) + .throwable(s.throwable) + .document(s.document) + .build() + } + } + } + } + + private fun defineServices(services: List) { + val list = services.map { ServiceRequest.fromSagres(it) } + database.serviceRequestDao().insertList(list) + } + + private fun defineGradesWorkers(semesters: List) { + semesters.forEach { + GradesSagresWorker.createWorker(context, it) + } + } + + @WorkerThread + private fun defineFrequency(frequency: List?) { + if (frequency == null) return + database.classAbsenceDao().putAbsences(frequency) + } + + @WorkerThread + private fun defineGrades(grades: List?) { + grades?.run { + database.gradesDao().putGrades(grades) + } + } + + @WorkerThread + private fun defineSemesters(semesters: List>?) { + semesters?.forEach { + val semester = Semester(sagresId = it.first, name = it.second, codename = it.second) + database.semesterDao().insertIgnoring(semester) + } + } + + @WorkerThread + private fun defineSchedule(locations: List?) { + if (locations == null) return + val ordering = preferences.getBoolean("stg_semester_deterministic_ordering", true) + val shrinkSchedule = preferences.getBoolean("stg_schedule_shrinking", true) + if (shrinkSchedule) { + val shrink = LocationShrinker.shrink(locations) + database.classLocationDao().putSchedule(shrink, ordering) + } else { + database.classLocationDao().putSchedule(locations, ordering) + } + } + + @WorkerThread + private fun defineDisciplineGroups(groups: List?) { + groups ?: return + database.classGroupDao().defineGroups(groups) + } + + @WorkerThread + private fun defineDisciplines(disciplines: List?) { + disciplines ?: return + val values = disciplines.map { Discipline.fromSagres(it) } + database.disciplineDao().insert(values) + disciplines.forEach { database.classDao().insert(it, true) } + } + + @WorkerThread + private fun defineCalendar(calendar: List?) { + val values = calendar?.map { CalendarItem.fromSagres(it) } + database.calendarDao().deleteAndInsert(values) + } + + private fun defineMessages(messages: List?) { + messages ?: return + messages.reversed().forEachIndexed { index, message -> + message.processingTime = System.currentTimeMillis() + index + } + val values = messages.map { Message.fromMessage(it, true) } + database.messageDao().insertIgnoring(values) + } + + companion object { + private var currentStep = 0 + private const val stepCount = 7 + + fun resetSteps() { + currentStep = 0 + } + + fun incSteps() { + currentStep++ + } + + fun createStep(@StringRes desc: Int): Step = Step(currentStep++, desc) + } + + data class Step(val step: Int, @StringRes val res: Int) { + val count = stepCount + } +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/MessagesRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/MessagesRepository.kt index 903e98f9f..99e5e95ff 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/MessagesRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/MessagesRepository.kt @@ -1,161 +1,161 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.repository - -import android.content.Context -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.asLiveData -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData -import com.forcetower.sagres.SagresNavigator -import com.forcetower.sagres.database.model.SagresCredential -import com.forcetower.sagres.operation.Status -import com.forcetower.uefs.core.model.service.UMessage -import com.forcetower.uefs.core.model.unes.Message -import com.forcetower.uefs.core.model.unes.defineInDatabase -import com.forcetower.uefs.core.storage.database.UDatabase -import com.forcetower.uefs.core.storage.repository.CookieSessionRepository.Companion.INJECT_ERROR_NO_VALUE -import com.forcetower.uefs.core.storage.repository.CookieSessionRepository.Companion.INJECT_SUCCESS -import com.forcetower.uefs.core.task.definers.MessagesProcessor -import com.google.firebase.firestore.CollectionReference -import com.google.firebase.firestore.Query -import dev.forcetower.breaker.Orchestra -import dev.forcetower.breaker.model.Authorization -import dev.forcetower.breaker.result.Outcome -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import okhttp3.OkHttpClient -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Named -import javax.inject.Singleton - -@Singleton -class MessagesRepository @Inject constructor( - private val context: Context, - private val client: OkHttpClient, - private val database: UDatabase, - private val cookieSessionRepository: CookieSessionRepository, - @Named(UMessage.COLLECTION) private val collection: CollectionReference, - @Named("flagSnowpiercerEnabled") private val snowpiercerEnabled: Boolean, - @Named("webViewUA") private val agent: String -) { - fun getMessages(): Flow> = Pager( - config = PagingConfig( - pageSize = 20, - enablePlaceholders = false - ), - pagingSourceFactory = { database.messageDao().getAllMessagesPaged() } - ).flow - - fun fetchMessages(fetchAll: Boolean = false): LiveData { - return if (snowpiercerEnabled) { - fetchMessagesSnowflake(fetchAll).asLiveData(Dispatchers.IO) - } else { - fetchMessagesCase(fetchAll).asLiveData(Dispatchers.IO) - } - } - - private fun fetchMessagesSnowflake(fetchAll: Boolean) = flow { - val access = database.accessDao().getAccessDirect() - val profile = database.profileDao().selectMeDirect() - if (access == null || profile == null) { - emit(false) - } else { - val orchestra = Orchestra.Builder().client(client).userAgent(agent).build() - orchestra.setAuthorization(Authorization(access.username, access.password)) - - val outcome = orchestra.messages(profile.sagresId, amount = if (fetchAll) 0 else 10) - if (outcome is Outcome.Success) { - MessagesProcessor(outcome.value, database, context, true).execute() - emit(true) - } else { - emit(false) - } - } - } - - private fun fetchMessagesCase(all: Boolean = false) = flow { - val profile = database.profileDao().selectMeDirect() - val access = database.accessDao().getAccessDirect() - if (profile != null && access != null) { - SagresNavigator.instance.putCredentials(SagresCredential(access.username, access.password, SagresNavigator.instance.getSelectedInstitution())) - val messages = if (!profile.mocked) { - SagresNavigator.instance.messages(profile.sagresId, all) - } else { - val me = SagresNavigator.instance.me() - val person = me.person - if (person != null) { - database.profileDao().insert(person) - SagresNavigator.instance.messages(person.id, all) - } else { - val injected = cookieSessionRepository.injectGoodCookiesOnClient() - if (injected == INJECT_SUCCESS) { - SagresNavigator.instance.messagesHtml() - } else { - if (injected == INJECT_ERROR_NO_VALUE) { - database.accessDao().setAccessValidationSuspend(false) - Timber.d("User didn't have a injectable cookie... Logout actually :D") - } - emit(false) - return@flow - } - } - } - - Timber.d("Profile mocked: ${profile.mocked}, ${profile.sagresId}, $all") - - if (messages.status == Status.SUCCESS) { - Timber.d("${messages.messages}") - messages.messages.defineInDatabase(database, true) - emit(true) - } else { - Timber.d("${messages.status}") - emit(false) - } - } else { - emit(false) - } - } - - fun getUnesMessages(): LiveData> { - val result = MutableLiveData>() - val institution = SagresNavigator.instance.getSelectedInstitution() - collection.orderBy("createdAt", Query.Direction.DESCENDING).addSnapshotListener { snapshot, exception -> - if (exception != null) { - Timber.e(exception) - } else if (snapshot != null) { - val list = snapshot.documents.mapNotNull { - it.toObject(UMessage::class.java)?.apply { - id = it.id - val replaced = message.replace("\\n", "\n") - message = replaced - } - }.filter { it.institution == null || it.institution == institution } - result.postValue(list) - } - } - return result - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.repository + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.asLiveData +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import com.forcetower.sagres.SagresNavigator +import com.forcetower.sagres.database.model.SagresCredential +import com.forcetower.sagres.operation.Status +import com.forcetower.uefs.core.model.service.UMessage +import com.forcetower.uefs.core.model.unes.Message +import com.forcetower.uefs.core.model.unes.defineInDatabase +import com.forcetower.uefs.core.storage.database.UDatabase +import com.forcetower.uefs.core.storage.repository.CookieSessionRepository.Companion.INJECT_ERROR_NO_VALUE +import com.forcetower.uefs.core.storage.repository.CookieSessionRepository.Companion.INJECT_SUCCESS +import com.forcetower.uefs.core.task.definers.MessagesProcessor +import com.google.firebase.firestore.CollectionReference +import com.google.firebase.firestore.Query +import dev.forcetower.breaker.Orchestra +import dev.forcetower.breaker.model.Authorization +import dev.forcetower.breaker.result.Outcome +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import okhttp3.OkHttpClient +import timber.log.Timber + +@Singleton +class MessagesRepository @Inject constructor( + private val context: Context, + private val client: OkHttpClient, + private val database: UDatabase, + private val cookieSessionRepository: CookieSessionRepository, + @Named(UMessage.COLLECTION) private val collection: CollectionReference, + @Named("flagSnowpiercerEnabled") private val snowpiercerEnabled: Boolean, + @Named("webViewUA") private val agent: String +) { + fun getMessages(): Flow> = Pager( + config = PagingConfig( + pageSize = 20, + enablePlaceholders = false + ), + pagingSourceFactory = { database.messageDao().getAllMessagesPaged() } + ).flow + + fun fetchMessages(fetchAll: Boolean = false): LiveData { + return if (snowpiercerEnabled) { + fetchMessagesSnowflake(fetchAll).asLiveData(Dispatchers.IO) + } else { + fetchMessagesCase(fetchAll).asLiveData(Dispatchers.IO) + } + } + + private fun fetchMessagesSnowflake(fetchAll: Boolean) = flow { + val access = database.accessDao().getAccessDirect() + val profile = database.profileDao().selectMeDirect() + if (access == null || profile == null) { + emit(false) + } else { + val orchestra = Orchestra.Builder().client(client).userAgent(agent).build() + orchestra.setAuthorization(Authorization(access.username, access.password)) + + val outcome = orchestra.messages(profile.sagresId, amount = if (fetchAll) 0 else 10) + if (outcome is Outcome.Success) { + MessagesProcessor(outcome.value, database, context, true).execute() + emit(true) + } else { + emit(false) + } + } + } + + private fun fetchMessagesCase(all: Boolean = false) = flow { + val profile = database.profileDao().selectMeDirect() + val access = database.accessDao().getAccessDirect() + if (profile != null && access != null) { + SagresNavigator.instance.putCredentials(SagresCredential(access.username, access.password, SagresNavigator.instance.getSelectedInstitution())) + val messages = if (!profile.mocked) { + SagresNavigator.instance.messages(profile.sagresId, all) + } else { + val me = SagresNavigator.instance.me() + val person = me.person + if (person != null) { + database.profileDao().insert(person) + SagresNavigator.instance.messages(person.id, all) + } else { + val injected = cookieSessionRepository.injectGoodCookiesOnClient() + if (injected == INJECT_SUCCESS) { + SagresNavigator.instance.messagesHtml() + } else { + if (injected == INJECT_ERROR_NO_VALUE) { + database.accessDao().setAccessValidationSuspend(false) + Timber.d("User didn't have a injectable cookie... Logout actually :D") + } + emit(false) + return@flow + } + } + } + + Timber.d("Profile mocked: ${profile.mocked}, ${profile.sagresId}, $all") + + if (messages.status == Status.SUCCESS) { + Timber.d("${messages.messages}") + messages.messages.defineInDatabase(database, true) + emit(true) + } else { + Timber.d("${messages.status}") + emit(false) + } + } else { + emit(false) + } + } + + fun getUnesMessages(): LiveData> { + val result = MutableLiveData>() + val institution = SagresNavigator.instance.getSelectedInstitution() + collection.orderBy("createdAt", Query.Direction.DESCENDING).addSnapshotListener { snapshot, exception -> + if (exception != null) { + Timber.e(exception) + } else if (snapshot != null) { + val list = snapshot.documents.mapNotNull { + it.toObject(UMessage::class.java)?.apply { + id = it.id + val replaced = message.replace("\\n", "\n") + message = replaced + } + }.filter { it.institution == null || it.institution == institution } + result.postValue(list) + } + } + return result + } +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/MicroSyncRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/MicroSyncRepository.kt index 64f72930d..e27955e1c 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/MicroSyncRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/MicroSyncRepository.kt @@ -22,9 +22,9 @@ package com.forcetower.uefs.core.storage.repository import com.forcetower.sagres.SagresNavigator import com.forcetower.uefs.core.storage.database.UDatabase +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import javax.inject.Inject class MicroSyncRepository @Inject constructor( database: UDatabase diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/ProfileRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/ProfileRepository.kt index 530da6ace..6e6521282 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/ProfileRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/ProfileRepository.kt @@ -38,9 +38,9 @@ import com.forcetower.uefs.core.storage.network.adapter.asLiveData import com.forcetower.uefs.core.storage.resource.NetworkBoundResource import com.forcetower.uefs.core.storage.resource.Resource import com.forcetower.uefs.core.work.statement.ProfileStatementWorker -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +import timber.log.Timber @Singleton class ProfileRepository @Inject constructor( @@ -117,6 +117,7 @@ class ProfileRepository @Inject constructor( return object : NetworkBoundResource, UResponse>>(executors) { override fun loadFromDb() = database.statementDao().getStatements(userId) override fun shouldFetch(it: List?) = true + // As of this version, the API uses the student id for loading the statements // This was a poor design on my part :/ override fun createCall() = service.getStatements(studentId).asLiveData() @@ -169,7 +170,9 @@ class ProfileRepository @Inject constructor( return if (response.isSuccessful) { database.statementDao().markStatementAccepted(statementId) 0 - } else 1 + } else { + 1 + } } @WorkerThread @@ -178,7 +181,9 @@ class ProfileRepository @Inject constructor( return if (response.isSuccessful) { database.statementDao().markStatementRefused(statementId) 0 - } else 1 + } else { + 1 + } } @WorkerThread @@ -187,6 +192,8 @@ class ProfileRepository @Inject constructor( return if (response.isSuccessful) { database.statementDao().markStatementDeleted(statementId) 0 - } else 1 + } else { + 1 + } } } diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/RemindersRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/RemindersRepository.kt index 6d6fd53cf..02eec51df 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/RemindersRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/RemindersRepository.kt @@ -34,14 +34,11 @@ class RemindersRepository @Inject constructor() { } fun createReminder(reminder: Reminder) { - } fun updateReminderCompleteStatus(reminder: Reminder, next: Boolean) { - } fun deleteReminder(reminder: Reminder) { - } } diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/SIECOMPRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/SIECOMPRepository.kt index 5b0e070dc..43ffbe710 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/SIECOMPRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/SIECOMPRepository.kt @@ -1,133 +1,133 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.repository - -import android.content.Context -import android.net.Uri -import androidx.lifecycle.LiveData -import com.forcetower.uefs.AppExecutors -import com.forcetower.uefs.core.model.siecomp.ServerSession -import com.forcetower.uefs.core.model.siecomp.Speaker -import com.forcetower.uefs.core.model.unes.AccessToken -import com.forcetower.uefs.core.storage.eventdatabase.EventDatabase -import com.forcetower.uefs.core.storage.eventdatabase.accessors.SessionWithData -import com.forcetower.uefs.core.storage.imgur.ImageUploader -import com.forcetower.uefs.core.storage.network.UService -import com.forcetower.uefs.core.storage.network.adapter.asLiveData -import com.forcetower.uefs.core.storage.resource.NetworkBoundResource -import com.forcetower.uefs.service.NotificationCreator -import okhttp3.OkHttpClient -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class SIECOMPRepository @Inject constructor( - private val database: EventDatabase, - private val executors: AppExecutors, - private val service: UService, - private val context: Context, - private val client: OkHttpClient -) { - fun getSessionsFromDayLocal(day: Int) = database.eventDao().getSessionsFromDay(day) - - fun getAllSessions() = - object : NetworkBoundResource, List>(executors) { - override fun loadFromDb() = database.eventDao().getAllSessions() - override fun shouldFetch(it: List?) = true - override fun createCall() = service.siecompSessions().asLiveData() - override fun saveCallResult(value: List) { - database.eventDao().insertServerSessions(value) - } - }.asLiveData() - - fun getSessionDetails(id: Long): LiveData { - return database.eventDao().getSessionWithId(id) - } - - fun getSpeaker(speakerId: Long): LiveData { - return database.eventDao().getSpeakerWithId(speakerId) - } - - fun markSessionStar(sessionId: Long, star: Boolean) { - executors.diskIO().execute { - database.eventDao().markSessionStar(sessionId, star) - } - } - - fun loginToService(username: String, password: String) { - executors.networkIO().execute { - try { - val response = service.login(username, password).execute() - if (response.isSuccessful) { - val token = response.body()!! - Timber.d("Token: $token") - database.accessTokenDao().deleteAll() - database.accessTokenDao().insert(token) - NotificationCreator.showSimpleNotification(context, "Login Concluido", "Você agr tem acesso a funções exclusivas") - } else { - NotificationCreator.showSimpleNotification(context, "Login falhou", "O login retornou com o código ${response.code()}") - Timber.e(response.message()) - } - } catch (t: Throwable) { - Timber.e("Exception@${t.message}") - } - } - } - - fun sendSpeaker(speaker: Speaker, create: Boolean) { - executors.networkIO().execute { - try { - Timber.d("Speaker $speaker") - val image = speaker.image - if (image != null && !image.contains("imgur.com")) { - Timber.d("Uploading image") - val uri = Uri.parse(image) - val link = ImageUploader.uploadToImGur(uri, context, client)?.link - if (link == null) { - Timber.d("Image not uploaded... unsetting...") - } else { - Timber.d("Image uploaded... setting") - } - speaker.image = link - } - - val response = if (create) { - service.createSpeaker(speaker).execute() - } else { - service.updateSpeaker(speaker).execute() - } - if (response.isSuccessful) { - NotificationCreator.showSimpleNotification(context, "Operação concluida", "A requisição concluiu com sucesso") - } else { - NotificationCreator.showSimpleNotification(context, "Operação falhou", "A operação falhou com o código ${response.code()}") - Timber.e(response.message()) - } - } catch (t: Throwable) { - Timber.e("Connection failed") - } - } - } - - fun getAccess(): LiveData { - return database.accessTokenDao().getAccess() - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.repository + +import android.content.Context +import android.net.Uri +import androidx.lifecycle.LiveData +import com.forcetower.uefs.AppExecutors +import com.forcetower.uefs.core.model.siecomp.ServerSession +import com.forcetower.uefs.core.model.siecomp.Speaker +import com.forcetower.uefs.core.model.unes.AccessToken +import com.forcetower.uefs.core.storage.eventdatabase.EventDatabase +import com.forcetower.uefs.core.storage.eventdatabase.accessors.SessionWithData +import com.forcetower.uefs.core.storage.imgur.ImageUploader +import com.forcetower.uefs.core.storage.network.UService +import com.forcetower.uefs.core.storage.network.adapter.asLiveData +import com.forcetower.uefs.core.storage.resource.NetworkBoundResource +import com.forcetower.uefs.service.NotificationCreator +import javax.inject.Inject +import javax.inject.Singleton +import okhttp3.OkHttpClient +import timber.log.Timber + +@Singleton +class SIECOMPRepository @Inject constructor( + private val database: EventDatabase, + private val executors: AppExecutors, + private val service: UService, + private val context: Context, + private val client: OkHttpClient +) { + fun getSessionsFromDayLocal(day: Int) = database.eventDao().getSessionsFromDay(day) + + fun getAllSessions() = + object : NetworkBoundResource, List>(executors) { + override fun loadFromDb() = database.eventDao().getAllSessions() + override fun shouldFetch(it: List?) = true + override fun createCall() = service.siecompSessions().asLiveData() + override fun saveCallResult(value: List) { + database.eventDao().insertServerSessions(value) + } + }.asLiveData() + + fun getSessionDetails(id: Long): LiveData { + return database.eventDao().getSessionWithId(id) + } + + fun getSpeaker(speakerId: Long): LiveData { + return database.eventDao().getSpeakerWithId(speakerId) + } + + fun markSessionStar(sessionId: Long, star: Boolean) { + executors.diskIO().execute { + database.eventDao().markSessionStar(sessionId, star) + } + } + + fun loginToService(username: String, password: String) { + executors.networkIO().execute { + try { + val response = service.login(username, password).execute() + if (response.isSuccessful) { + val token = response.body()!! + Timber.d("Token: $token") + database.accessTokenDao().deleteAll() + database.accessTokenDao().insert(token) + NotificationCreator.showSimpleNotification(context, "Login Concluido", "Você agr tem acesso a funções exclusivas") + } else { + NotificationCreator.showSimpleNotification(context, "Login falhou", "O login retornou com o código ${response.code()}") + Timber.e(response.message()) + } + } catch (t: Throwable) { + Timber.e("Exception@${t.message}") + } + } + } + + fun sendSpeaker(speaker: Speaker, create: Boolean) { + executors.networkIO().execute { + try { + Timber.d("Speaker $speaker") + val image = speaker.image + if (image != null && !image.contains("imgur.com")) { + Timber.d("Uploading image") + val uri = Uri.parse(image) + val link = ImageUploader.uploadToImGur(uri, context, client)?.link + if (link == null) { + Timber.d("Image not uploaded... unsetting...") + } else { + Timber.d("Image uploaded... setting") + } + speaker.image = link + } + + val response = if (create) { + service.createSpeaker(speaker).execute() + } else { + service.updateSpeaker(speaker).execute() + } + if (response.isSuccessful) { + NotificationCreator.showSimpleNotification(context, "Operação concluida", "A requisição concluiu com sucesso") + } else { + NotificationCreator.showSimpleNotification(context, "Operação falhou", "A operação falhou com o código ${response.code()}") + Timber.e(response.message()) + } + } catch (t: Throwable) { + Timber.e("Connection failed") + } + } + } + + fun getAccess(): LiveData { + return database.accessTokenDao().getAccess() + } +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/SagresDataRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/SagresDataRepository.kt index 1145e5a32..ee95b3343 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/SagresDataRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/SagresDataRepository.kt @@ -1,197 +1,197 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.repository - -import android.content.SharedPreferences -import androidx.lifecycle.LiveData -import androidx.lifecycle.liveData -import com.forcetower.sagres.SagresNavigator -import com.forcetower.sagres.operation.Status -import com.forcetower.uefs.AppExecutors -import com.forcetower.uefs.core.model.service.SavedCookie -import com.forcetower.uefs.core.model.unes.Access -import com.forcetower.uefs.core.storage.database.UDatabase -import com.forcetower.uefs.core.storage.network.UService -import com.forcetower.uefs.core.storage.resource.Resource -import com.forcetower.uefs.core.util.round -import dev.forcetower.breaker.Orchestra -import dev.forcetower.breaker.model.Authorization -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Named -import javax.inject.Singleton - -@Singleton -class SagresDataRepository @Inject constructor( - private val database: UDatabase, - private val executor: AppExecutors, - private val preferences: SharedPreferences, - private val client: OkHttpClient, - @Named("webViewUA") private val agent: String, - private val cookies: CookieSessionRepository, - private val service: UService -) { - fun getMessages() = database.messageDao().getAllMessages() - - fun logout() { - executor.diskIO().execute { - preferences.edit() - .remove("hourglass_status") - .apply() - database.accessDao().deleteAll() - database.accessTokenDao().deleteAll() - database.accountDao().deleteAll() - database.profileDao().deleteMe() - database.classDao().deleteAll() - database.semesterDao().deleteAll() - database.messageDao().deleteAll() - database.demandOfferDao().deleteAll() - database.serviceRequestDao().deleteAll() - SagresNavigator.instance.logout() - SagresNavigator.instance.putCredentials(null) - } - } - - suspend fun logoutSuspend() { - database.edgeAccessToken.deleteAll() - database.edgeServiceAccount.deleteAll() - database.studentServiceDao().markNoOneAsMe() - } - - fun getFlags() = database.flagsDao().getFlags() - fun getSemesters() = database.semesterDao().getParticipatingSemesters() - fun getCourse() = database.profileDao().getProfileCourse() - - fun lightweightCalcScore() { - executor.diskIO().execute { - val classes = database.classDao().getAllDirect().filter { it.clazz.finalScore != null } - val hours = classes.sumOf { it.discipline.credits } - val mean = classes.sumOf { - val zeroValue = it.clazz.missedClasses > (it.discipline.credits / 4) - val finalScore = if (zeroValue) 0.0 else it.clazz.finalScore!! - it.discipline.credits * finalScore - } - if (hours > 0) { - val score = (mean / hours).round(1) - Timber.d("Score is $score") - database.profileDao().updateCalculatedScore(score) - } - } - } - - fun changeAccessValidation(valid: Boolean) { - executor.diskIO().execute { - database.accessDao().setAccessValidation(valid) - } - } - - fun attemptLoginWithNewPassword(password: String, token: String?): LiveData> { - return liveData(Dispatchers.IO) { - val access = database.accessDao().getAccessDirect() - if (access == null) { - emit(Resource.error("Sem acesso", false)) - } else { - emit(Resource.loading(false)) - val callback = SagresNavigator.instance.login(access.username, access.password, token) - if (callback.status == Status.INVALID_LOGIN) { - emit(Resource.success(false)) - } else { - val cooked = cookies.getSavedBiscuit() - if (ensureServiceConnected(access, cooked)) { - cookies.findAndSaveCookies() - database.accessDao().run { - setAccessValidationSuspend(true) - updateAccessPasswordSuspend(password) - } - emit(Resource.success(true)) - } else { - emit(Resource.error("Não consegue entrar no unesverso (provavelmente) :D", false)) - } - } - } - } - } - - private suspend fun ensureServiceConnected(access: Access, cooked: SavedCookie?): Boolean { - cooked ?: return false - val token = database.accessTokenDao().getAccessTokenDirectSuspend() - try { - if (token == null) { - reconnectToService(access, cooked) - return true - } else { - val response = service.hi() - return when { - response.isSuccessful -> true - response.code() == 401 -> { - reconnectToService(access, cooked) - true - } - else -> { - Timber.tag("reconnect").e("User fails to reconnect with code ${response.code()}") - false - } - } - } - } catch (error: Throwable) { - Timber.tag("reconnect").e(error, "Failed to reconnect to server...") - return false - } - } - - private suspend fun reconnectToService(access: Access, cooked: SavedCookie) { - val token = service.loginWithBiscuitSuspend( - access.username, - access.password, - cooked.auth, - cooked.sessionId - ) - database.accessTokenDao().insertSuspend(token) - } - - suspend fun loginWithNewPasswordSuspend(password: String): Boolean = withContext(Dispatchers.IO) { - val access = database.accessDao().getAccessDirectSuspend() ?: return@withContext false - val orchestra = Orchestra.Builder().client(client).userAgent(agent).build() - orchestra.setAuthorization(Authorization(access.username, password)) - try { - val outcome = orchestra.login() - - if (outcome.isSuccess) { - database.accessDao().run { - setAccessValidationSuspend(true) - updateAccessPasswordSuspend(password) - } - } - - outcome.isSuccess - } catch (error: Throwable) { - Timber.e(error, "Error during password change") - false - } - } - - fun getScheduleHideCount(): LiveData { - return database.classLocationDao().getHiddenClassesCount() - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.repository + +import android.content.SharedPreferences +import androidx.lifecycle.LiveData +import androidx.lifecycle.liveData +import com.forcetower.sagres.SagresNavigator +import com.forcetower.sagres.operation.Status +import com.forcetower.uefs.AppExecutors +import com.forcetower.uefs.core.model.service.SavedCookie +import com.forcetower.uefs.core.model.unes.Access +import com.forcetower.uefs.core.storage.database.UDatabase +import com.forcetower.uefs.core.storage.network.UService +import com.forcetower.uefs.core.storage.resource.Resource +import com.forcetower.uefs.core.util.round +import dev.forcetower.breaker.Orchestra +import dev.forcetower.breaker.model.Authorization +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import timber.log.Timber + +@Singleton +class SagresDataRepository @Inject constructor( + private val database: UDatabase, + private val executor: AppExecutors, + private val preferences: SharedPreferences, + private val client: OkHttpClient, + @Named("webViewUA") private val agent: String, + private val cookies: CookieSessionRepository, + private val service: UService +) { + fun getMessages() = database.messageDao().getAllMessages() + + fun logout() { + executor.diskIO().execute { + preferences.edit() + .remove("hourglass_status") + .apply() + database.accessDao().deleteAll() + database.accessTokenDao().deleteAll() + database.accountDao().deleteAll() + database.profileDao().deleteMe() + database.classDao().deleteAll() + database.semesterDao().deleteAll() + database.messageDao().deleteAll() + database.demandOfferDao().deleteAll() + database.serviceRequestDao().deleteAll() + SagresNavigator.instance.logout() + SagresNavigator.instance.putCredentials(null) + } + } + + suspend fun logoutSuspend() { + database.edgeAccessToken.deleteAll() + database.edgeServiceAccount.deleteAll() + database.studentServiceDao().markNoOneAsMe() + } + + fun getFlags() = database.flagsDao().getFlags() + fun getSemesters() = database.semesterDao().getParticipatingSemesters() + fun getCourse() = database.profileDao().getProfileCourse() + + fun lightweightCalcScore() { + executor.diskIO().execute { + val classes = database.classDao().getAllDirect().filter { it.clazz.finalScore != null } + val hours = classes.sumOf { it.discipline.credits } + val mean = classes.sumOf { + val zeroValue = it.clazz.missedClasses > (it.discipline.credits / 4) + val finalScore = if (zeroValue) 0.0 else it.clazz.finalScore!! + it.discipline.credits * finalScore + } + if (hours > 0) { + val score = (mean / hours).round(1) + Timber.d("Score is $score") + database.profileDao().updateCalculatedScore(score) + } + } + } + + fun changeAccessValidation(valid: Boolean) { + executor.diskIO().execute { + database.accessDao().setAccessValidation(valid) + } + } + + fun attemptLoginWithNewPassword(password: String, token: String?): LiveData> { + return liveData(Dispatchers.IO) { + val access = database.accessDao().getAccessDirect() + if (access == null) { + emit(Resource.error("Sem acesso", false)) + } else { + emit(Resource.loading(false)) + val callback = SagresNavigator.instance.login(access.username, access.password, token) + if (callback.status == Status.INVALID_LOGIN) { + emit(Resource.success(false)) + } else { + val cooked = cookies.getSavedBiscuit() + if (ensureServiceConnected(access, cooked)) { + cookies.findAndSaveCookies() + database.accessDao().run { + setAccessValidationSuspend(true) + updateAccessPasswordSuspend(password) + } + emit(Resource.success(true)) + } else { + emit(Resource.error("Não consegue entrar no unesverso (provavelmente) :D", false)) + } + } + } + } + } + + private suspend fun ensureServiceConnected(access: Access, cooked: SavedCookie?): Boolean { + cooked ?: return false + val token = database.accessTokenDao().getAccessTokenDirectSuspend() + try { + if (token == null) { + reconnectToService(access, cooked) + return true + } else { + val response = service.hi() + return when { + response.isSuccessful -> true + response.code() == 401 -> { + reconnectToService(access, cooked) + true + } + else -> { + Timber.tag("reconnect").e("User fails to reconnect with code ${response.code()}") + false + } + } + } + } catch (error: Throwable) { + Timber.tag("reconnect").e(error, "Failed to reconnect to server...") + return false + } + } + + private suspend fun reconnectToService(access: Access, cooked: SavedCookie) { + val token = service.loginWithBiscuitSuspend( + access.username, + access.password, + cooked.auth, + cooked.sessionId + ) + database.accessTokenDao().insertSuspend(token) + } + + suspend fun loginWithNewPasswordSuspend(password: String): Boolean = withContext(Dispatchers.IO) { + val access = database.accessDao().getAccessDirectSuspend() ?: return@withContext false + val orchestra = Orchestra.Builder().client(client).userAgent(agent).build() + orchestra.setAuthorization(Authorization(access.username, password)) + try { + val outcome = orchestra.login() + + if (outcome.isSuccess) { + database.accessDao().run { + setAccessValidationSuspend(true) + updateAccessPasswordSuspend(password) + } + } + + outcome.isSuccess + } catch (error: Throwable) { + Timber.e(error, "Error during password change") + false + } + } + + fun getScheduleHideCount(): LiveData { + return database.classLocationDao().getHiddenClassesCount() + } +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/SagresGradesRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/SagresGradesRepository.kt index ad028f5fc..c77d6d9a6 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/SagresGradesRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/SagresGradesRepository.kt @@ -1,154 +1,154 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.repository - -import android.content.Context -import android.content.SharedPreferences -import androidx.annotation.WorkerThread -import com.forcetower.sagres.Constants -import com.forcetower.sagres.SagresNavigator -import com.forcetower.sagres.database.model.SagresDisciplineMissedClass -import com.forcetower.sagres.database.model.SagresGrade -import com.forcetower.sagres.operation.Status -import com.forcetower.uefs.core.model.unes.Semester -import com.forcetower.uefs.core.storage.database.UDatabase -import com.forcetower.uefs.core.task.definers.DisciplinesProcessor -import com.forcetower.uefs.core.util.isStudentFromUEFS -import dev.forcetower.breaker.Orchestra -import dev.forcetower.breaker.model.Authorization -import dev.forcetower.breaker.result.Outcome -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Named - -class SagresGradesRepository @Inject constructor( - private val context: Context, - private val database: UDatabase, - private val client: OkHttpClient, - private val cookieSessionRepository: CookieSessionRepository, - private val preferences: SharedPreferences, - @Named("webViewUA") private val agent: String, - @Named("flagSnowpiercerEnabled") private val snowpiercer: Boolean -) { - suspend fun getGradesAsync(semesterId: Long, needLogin: Boolean): Int = withContext(Dispatchers.IO) { - if (snowpiercer) { - getGradesSnowflake(semesterId) - } else { - getGrades(semesterId, needLogin) - } - } - - private suspend fun getGradesSnowflake(semesterId: Long): Int { - val access = database.accessDao().getAccessDirect() - val profile = database.profileDao().selectMeDirect() - return if (access == null || profile == null) { - NO_ACCESS - } else { - val orchestra = Orchestra.Builder().client(client).userAgent(agent).build() - orchestra.setAuthorization(Authorization(access.username, access.password)) - val outcome = orchestra.grades(profile.sagresId, semesterId) - if (outcome is Outcome.Success) { - val currentSemester = database.semesterDao().getSemesterDirect(semesterId) - DisciplinesProcessor(context, database, outcome.value, currentSemester!!.uid, profile.uid, false).execute() - SUCCESS - } else { - CURRENT_GRADES_FAILED - } - } - } - - @WorkerThread - suspend fun getGrades(semesterSagresId: Long, needLogin: Boolean = true): Int { - val access = database.accessDao().getAccessDirect() ?: return NO_ACCESS - - return if (needLogin) { - if (preferences.isStudentFromUEFS()) { - cookieSessionRepository.injectGoodCookiesOnClient() - proceed(semesterSagresId) - } else if (Constants.getParameter("REQUIRES_CAPTCHA") != "true") { - val login = SagresNavigator.instance.login(access.username, access.password) - if (login.status == Status.SUCCESS && login.document != null) { - Timber.d("[$semesterSagresId] Login Completed Correctly") - proceed(semesterSagresId) - } else { - INVALID_ACCESS - } - } else { - INVALID_ACCESS - } - } else { - proceed(semesterSagresId) - } - } - - @WorkerThread - private fun proceed(semesterSagresId: Long): Int { - val grades = SagresNavigator.instance.getCurrentGrades() - return if (grades.status == Status.SUCCESS && grades.document != null) { - Timber.d("[$semesterSagresId] Grades Part 01/02 Completed!") - val semesterGrades = SagresNavigator.instance.getGradesFromSemester(semesterSagresId, grades.document!!) - if (semesterGrades.status == Status.SUCCESS) { - defineSemesters(semesterGrades.semesters) - defineGrades(semesterGrades.grades) - defineFrequency(semesterGrades.frequency) - Timber.d("[$semesterSagresId] Grades Part 02/02 Completed!") - Timber.d("[$semesterSagresId] Grades: ${semesterGrades.grades}") - SUCCESS - } else { - ACTUAL_GRADES_CALL_FAILED - } - } else { - Timber.d("Current Grades Status Failed") - CURRENT_GRADES_FAILED - } - } - - @WorkerThread - private fun defineFrequency(frequency: List?) { - if (frequency == null) return - database.classAbsenceDao().putAbsences(frequency) - } - - @WorkerThread - private fun defineGrades(grades: List?) { - grades ?: return - database.gradesDao().putGrades(grades, notify = false) - } - - @WorkerThread - private fun defineSemesters(semesters: List>?) { - semesters?.forEach { - val semester = Semester(sagresId = it.first, name = it.second, codename = it.second) - database.semesterDao().insertIgnoring(semester) - } - } - - companion object { - const val SUCCESS = 0 - const val NO_ACCESS = -1 - const val INVALID_ACCESS = -2 - const val CURRENT_GRADES_FAILED = -3 - const val ACTUAL_GRADES_CALL_FAILED = -4 - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.repository + +import android.content.Context +import android.content.SharedPreferences +import androidx.annotation.WorkerThread +import com.forcetower.sagres.Constants +import com.forcetower.sagres.SagresNavigator +import com.forcetower.sagres.database.model.SagresDisciplineMissedClass +import com.forcetower.sagres.database.model.SagresGrade +import com.forcetower.sagres.operation.Status +import com.forcetower.uefs.core.model.unes.Semester +import com.forcetower.uefs.core.storage.database.UDatabase +import com.forcetower.uefs.core.task.definers.DisciplinesProcessor +import com.forcetower.uefs.core.util.isStudentFromUEFS +import dev.forcetower.breaker.Orchestra +import dev.forcetower.breaker.model.Authorization +import dev.forcetower.breaker.result.Outcome +import javax.inject.Inject +import javax.inject.Named +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import timber.log.Timber + +class SagresGradesRepository @Inject constructor( + private val context: Context, + private val database: UDatabase, + private val client: OkHttpClient, + private val cookieSessionRepository: CookieSessionRepository, + private val preferences: SharedPreferences, + @Named("webViewUA") private val agent: String, + @Named("flagSnowpiercerEnabled") private val snowpiercer: Boolean +) { + suspend fun getGradesAsync(semesterId: Long, needLogin: Boolean): Int = withContext(Dispatchers.IO) { + if (snowpiercer) { + getGradesSnowflake(semesterId) + } else { + getGrades(semesterId, needLogin) + } + } + + private suspend fun getGradesSnowflake(semesterId: Long): Int { + val access = database.accessDao().getAccessDirect() + val profile = database.profileDao().selectMeDirect() + return if (access == null || profile == null) { + NO_ACCESS + } else { + val orchestra = Orchestra.Builder().client(client).userAgent(agent).build() + orchestra.setAuthorization(Authorization(access.username, access.password)) + val outcome = orchestra.grades(profile.sagresId, semesterId) + if (outcome is Outcome.Success) { + val currentSemester = database.semesterDao().getSemesterDirect(semesterId) + DisciplinesProcessor(context, database, outcome.value, currentSemester!!.uid, profile.uid, false).execute() + SUCCESS + } else { + CURRENT_GRADES_FAILED + } + } + } + + @WorkerThread + suspend fun getGrades(semesterSagresId: Long, needLogin: Boolean = true): Int { + val access = database.accessDao().getAccessDirect() ?: return NO_ACCESS + + return if (needLogin) { + if (preferences.isStudentFromUEFS()) { + cookieSessionRepository.injectGoodCookiesOnClient() + proceed(semesterSagresId) + } else if (Constants.getParameter("REQUIRES_CAPTCHA") != "true") { + val login = SagresNavigator.instance.login(access.username, access.password) + if (login.status == Status.SUCCESS && login.document != null) { + Timber.d("[$semesterSagresId] Login Completed Correctly") + proceed(semesterSagresId) + } else { + INVALID_ACCESS + } + } else { + INVALID_ACCESS + } + } else { + proceed(semesterSagresId) + } + } + + @WorkerThread + private fun proceed(semesterSagresId: Long): Int { + val grades = SagresNavigator.instance.getCurrentGrades() + return if (grades.status == Status.SUCCESS && grades.document != null) { + Timber.d("[$semesterSagresId] Grades Part 01/02 Completed!") + val semesterGrades = SagresNavigator.instance.getGradesFromSemester(semesterSagresId, grades.document!!) + if (semesterGrades.status == Status.SUCCESS) { + defineSemesters(semesterGrades.semesters) + defineGrades(semesterGrades.grades) + defineFrequency(semesterGrades.frequency) + Timber.d("[$semesterSagresId] Grades Part 02/02 Completed!") + Timber.d("[$semesterSagresId] Grades: ${semesterGrades.grades}") + SUCCESS + } else { + ACTUAL_GRADES_CALL_FAILED + } + } else { + Timber.d("Current Grades Status Failed") + CURRENT_GRADES_FAILED + } + } + + @WorkerThread + private fun defineFrequency(frequency: List?) { + if (frequency == null) return + database.classAbsenceDao().putAbsences(frequency) + } + + @WorkerThread + private fun defineGrades(grades: List?) { + grades ?: return + database.gradesDao().putGrades(grades, notify = false) + } + + @WorkerThread + private fun defineSemesters(semesters: List>?) { + semesters?.forEach { + val semester = Semester(sagresId = it.first, name = it.second, codename = it.second) + database.semesterDao().insertIgnoring(semester) + } + } + + companion object { + const val SUCCESS = 0 + const val NO_ACCESS = -1 + const val INVALID_ACCESS = -2 + const val CURRENT_GRADES_FAILED = -3 + const val ACTUAL_GRADES_CALL_FAILED = -4 + } +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/SagresSyncRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/SagresSyncRepository.kt index 77ad81753..c8496b147 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/SagresSyncRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/SagresSyncRepository.kt @@ -1,765 +1,769 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.repository - -import android.content.Context -import android.content.SharedPreferences -import android.net.ConnectivityManager -import android.net.NetworkCapabilities -import android.net.wifi.WifiInfo -import android.net.wifi.WifiManager -import android.os.Build -import android.telephony.TelephonyManager -import androidx.annotation.WorkerThread -import androidx.core.content.ContextCompat -import com.forcetower.core.getDynamicDataSourceFactory -import com.forcetower.sagres.SagresNavigator -import com.forcetower.sagres.database.model.SagresCalendar -import com.forcetower.sagres.database.model.SagresCredential -import com.forcetower.sagres.database.model.SagresDiscipline -import com.forcetower.sagres.database.model.SagresDisciplineClassLocation -import com.forcetower.sagres.database.model.SagresDisciplineGroup -import com.forcetower.sagres.database.model.SagresDisciplineMissedClass -import com.forcetower.sagres.database.model.SagresGrade -import com.forcetower.sagres.database.model.SagresMessage -import com.forcetower.sagres.database.model.SagresPerson -import com.forcetower.sagres.database.model.SagresRequestedService -import com.forcetower.sagres.operation.BaseCallback -import com.forcetower.sagres.operation.Status -import com.forcetower.sagres.parsers.SagresBasicParser -import com.forcetower.sagres.parsers.SagresMessageParser -import com.forcetower.sagres.parsers.SagresScheduleParser -import com.forcetower.sagres.utils.ConnectedStates -import com.forcetower.uefs.AppExecutors -import com.forcetower.uefs.BuildConfig -import com.forcetower.uefs.R -import com.forcetower.uefs.core.constants.Constants -import com.forcetower.uefs.core.model.unes.Access -import com.forcetower.uefs.core.model.unes.CalendarItem -import com.forcetower.uefs.core.model.unes.Discipline -import com.forcetower.uefs.core.model.unes.Message -import com.forcetower.uefs.core.model.unes.NetworkType -import com.forcetower.uefs.core.model.unes.SagresFlags -import com.forcetower.uefs.core.model.unes.Semester -import com.forcetower.uefs.core.model.unes.ServiceRequest -import com.forcetower.uefs.core.model.unes.SyncRegistry -import com.forcetower.uefs.core.model.unes.notify -import com.forcetower.uefs.core.storage.database.UDatabase -import com.forcetower.uefs.core.storage.network.UService -import com.forcetower.uefs.core.storage.repository.cloud.AuthRepository -import com.forcetower.uefs.core.util.LocationShrinker -import com.forcetower.uefs.core.util.VersionUtils -import com.forcetower.uefs.core.util.isStudentFromUEFS -import com.forcetower.uefs.core.work.discipline.DisciplinesDetailsWorker -import com.forcetower.uefs.core.work.hourglass.HourglassContributeWorker -import com.forcetower.uefs.service.NotificationCreator -import com.google.firebase.crashlytics.FirebaseCrashlytics -import com.google.firebase.messaging.FirebaseMessaging -import com.google.firebase.remoteconfig.FirebaseRemoteConfig -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.tasks.await -import kotlinx.coroutines.withContext -import org.jsoup.nodes.Document -import timber.log.Timber -import java.util.Calendar -import java.util.Locale -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class SagresSyncRepository @Inject constructor( - private val context: Context, - private val database: UDatabase, - private val executors: AppExecutors, - private val authRepository: AuthRepository, - private val adventureRepository: AdventureRepository, - private val cookieSessionRepository: CookieSessionRepository, - private val service: UService, - private val remoteConfig: FirebaseRemoteConfig, - private val preferences: SharedPreferences -) { - private val mutex = Mutex() - - private suspend fun findAndMatch() { - val aeri = getDynamicDataSourceFactory(context, "com.forcetower.uefs.aeri.domain.AERIDataSourceFactoryProvider") - aeri?.create()?.run { - update() - getNotifyMessages().forEach { - NotificationCreator.showSimpleNotification(context, it.title, it.content) - } - } - } - - @WorkerThread - suspend fun performSync(executor: String, gToken: String? = null) = withContext(Dispatchers.IO) { - val registry = createRegistry(executor) - val access = database.accessDao().getAccessDirect() - access ?: Timber.d("Access is null, sync will not continue") - if (access != null) { - FirebaseCrashlytics.getInstance().setUserId(access.username) - // Only one sync may be active at a time - mutex.withLock { - execute(access, registry, executor, gToken) - } - } else { - registry.completed = true - registry.error = -1 - registry.success = false - registry.message = "Credenciais de acesso inválidas" - registry.end = System.currentTimeMillis() - database.syncRegistryDao().insert(registry) - } - } - - private fun createRegistry(executor: String): SyncRegistry { - val connectivity = ContextCompat.getSystemService(context, ConnectivityManager::class.java)!! - - if (VersionUtils.isMarshmallow()) { - val capabilities = connectivity.getNetworkCapabilities(connectivity.activeNetwork) - return if (capabilities != null) { - val wifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) - val network = if (wifi) { - val manager = context.getSystemService(WifiManager::class.java) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - (capabilities.transportInfo as? WifiInfo)?.ssid ?: "Unknown" - } else { - @Suppress("DEPRECATION") - manager?.connectionInfo?.ssid ?: "Unknown" - } - } else { - val manager = context.getSystemService(TelephonyManager::class.java) - manager?.simOperatorName ?: "Operator" - } - Timber.d("Is on Wifi? $wifi. Network name: $network") - SyncRegistry( - executor = executor, - network = network, - networkType = if (wifi) NetworkType.WIFI.ordinal else NetworkType.CELLULAR.ordinal - ) - } else { - SyncRegistry(executor = executor, network = "Invalid", networkType = NetworkType.OTHER.ordinal) - } - } else { - val manager = ContextCompat.getSystemService(context, WifiManager::class.java) - @Suppress("DEPRECATION") - val info = manager?.connectionInfo - return if (info == null) { - val phone = ContextCompat.getSystemService(context, TelephonyManager::class.java) - val operatorName = phone?.simOperatorName - SyncRegistry(executor = executor, network = operatorName ?: "invalid", networkType = NetworkType.CELLULAR.ordinal) - } else { - SyncRegistry(executor = executor, network = info.ssid, networkType = NetworkType.WIFI.ordinal) - } - } - } - - @WorkerThread - private suspend fun execute(access: Access, registry: SyncRegistry, executor: String, gToken: String?) { - val uid = database.syncRegistryDao().insert(registry) - val calendar = Calendar.getInstance() - val today = calendar.get(Calendar.DAY_OF_MONTH) - registry.uid = uid - SagresNavigator.instance.putCredentials( - SagresCredential( - access.username, - access.password, - SagresNavigator.instance.getSelectedInstitution() - ) - ) - - // Internal checks for canceling auto sync - // this was useful to avoid unes from ddos'ing the website. - // they said unes was doing it anyways, so here is a deleted useless piece of code - // you welcome - if (!Constants.EXECUTOR_WHITELIST.contains(executor.lowercase(Locale.getDefault()))) { - Timber.d("There was a time where this would cause sync to be aborted if server.. But i don't care anymore") - } - - runCatching { findAndMatch() } - - val isStudentFromUEFS = preferences.isStudentFromUEFS() - val isCaptchaInjectionNeeded = isStudentFromUEFS && com.forcetower.sagres.Constants.getParameter("REQUIRES_CAPTCHA") == "true" - if (isCaptchaInjectionNeeded) { - val injected = cookieSessionRepository.injectGoodCookiesOnClient() - if (injected == CookieSessionRepository.INJECT_ERROR_NO_VALUE) { - Timber.i("The cookie that is good no one wants to give!") - NotificationCreator.showSimpleNotification( - context, - "Então...", - "Você não tem sessão criada, entra de novo" - ) - - registry.completed = true - registry.error = 1 shl 11 - registry.success = false - registry.skipped = 0 - registry.message = "Deve-se consultar as flags de erro" - registry.end = System.currentTimeMillis() - database.syncRegistryDao().update(registry) - - markSessionExpired() - return - } else if (injected == CookieSessionRepository.INJECT_ERROR_NETWORK) { - Timber.i("Failed to connect to UNES Services") - registry.completed = true - registry.error = 1 shl 10 - registry.success = false - registry.skipped = 0 - registry.message = "Deve-se consultar as flags de erro" - registry.end = System.currentTimeMillis() - database.syncRegistryDao().update(registry) - return - } - } - - database.gradesDao().markAllNotified() - database.messageDao().setAllNotified() - database.classMaterialDao().markAllNotified() - - val homeDoc = when { - isStudentFromUEFS && gToken == null && isCaptchaInjectionNeeded -> initialPage() - isStudentFromUEFS && !isCaptchaInjectionNeeded -> login(access, null) - !isStudentFromUEFS -> login(access, gToken) - else -> null - } - - if (isStudentFromUEFS) { - val connection = SagresBasicParser.isConnected(homeDoc) - if (connection == ConnectedStates.INVALID) { - cookieSessionRepository.invalidateCookies() - NotificationCreator.showSimpleNotification( - context, - "Vish...", - "Sua sessão expirou, entra de novo" - ) - markSessionExpired() - return - } - } - - val score = SagresBasicParser.getScore(homeDoc) - Timber.d("Login Completed. Score Parsed: $score") - - // Since stuff is just broken.... - if (homeDoc == null) { - registry.completed = true - registry.error = -2 - registry.success = false - registry.message = "Login falhou" - } else { - defineSchedule(SagresScheduleParser.getSchedule(homeDoc)) - defineMessages(SagresMessageParser.getMessages(homeDoc)) - } - - val person = me(score, access) - Timber.d("The person from me is ${person?.name} ${person?.isMocked}") - if (person == null) { - registry.completed = true - registry.error = -3 - registry.success = false - registry.message = "The dream is over" - registry.end = System.currentTimeMillis() - database.syncRegistryDao().update(registry) - return - } - - var result = 0 - var skipped = 0 - - if (!person.isMocked) { - Timber.d("I guess the person is not a mocked version on it") - if (!messages(person.id)) { - if (homeDoc != null && !messages(null)) result += 1 shl 1 - } - if (!semesters(person.id)) result += 1 shl 2 - if (homeDoc == null) { - registry.completed = true - registry.error = 10 - registry.success = true - registry.message = "Partial sync" - registry.executor = "${registry.executor}-Parc" - registry.end = System.currentTimeMillis() - database.syncRegistryDao().update(registry) - return - } - } else { - if (!messages(null)) result += 1 shl 1 - } - - val dailyDisciplines = - preferences.getString("stg_daily_discipline_sync", "2")?.toIntOrNull() ?: 2 - val currentDaily = preferences.getInt("daily_discipline_count", 0) - val currentDayDiscipline = preferences.getInt("daily_discipline_day", -1) - val lastDailyHour = preferences.getInt("daily_discipline_hour", 0) - val isNewDaily = currentDayDiscipline != today || dailyDisciplines == -1 - val currentDailyHour = calendar.get(Calendar.HOUR_OF_DAY) - - val (actualDailyCount, nextHour) = if (isNewDaily) - 0 to -1 - else - currentDaily to if (lastDailyHour < 8) 10 else lastDailyHour + 4 - - val shouldDisciplineSync = - ((actualDailyCount < dailyDisciplines) || (dailyDisciplines == -1)) && - (currentDailyHour >= nextHour) - - if (shouldDisciplineSync) { - if (!disciplinesExperimental()) result += 1 shl 6 - else { - preferences.edit() - .putInt("daily_discipline_count", actualDailyCount + 1) - .putInt("daily_discipline_day", today) - .putInt("daily_discipline_hour", currentDailyHour) - .apply() - } - } else { - skipped += 1 shl 1 - } - - if (!startPage()) result += 1 shl 3 - if (!grades()) result += 1 shl 4 - if (!servicesRequest()) result += 1 shl 5 - - if (isStudentFromUEFS) { - serviceLogin() - } - - if (preferences.getBoolean("primary_fetch", true)) { - DisciplinesDetailsWorker.createWorker(context) - preferences.edit().putBoolean("primary_fetch", false).apply() - } - - if (isStudentFromUEFS) { - if (!preferences.getBoolean("sent_hourglass_testing_data_0.0.2", false) && - authRepository.getAccessTokenDirect() != null - ) { - HourglassContributeWorker.createWorker(context) - preferences.edit().putBoolean("sent_hourglass_testing_data_0.0.2", true).apply() - } - } - - try { - val day = preferences.getInt("sync_daily_update", -1) - if (day != today) { - adventureRepository.performCheckAchievements(HashMap()) - - val task = FirebaseMessaging.getInstance().token - val value = task.await() - onNewToken(value) - - preferences.edit().putInt("sync_daily_update", today).apply() - } - createNewVersionNotification() - } catch (t: Throwable) { - Timber.e(t) - } - - registry.completed = true - registry.error = result - registry.success = result == 0 - registry.skipped = skipped - registry.message = "Deve-se consultar as flags de erro" - registry.end = System.currentTimeMillis() - database.syncRegistryDao().update(registry) - } - - private fun defineMessages(values: List) { - values.reversed().forEachIndexed { index, message -> - message.processingTime = System.currentTimeMillis() + index - } - database.messageDao().insertIgnoring(values.map { Message.fromMessage(it, false) }) - } - - private fun onNewToken(token: String) { - val auth = database.accessTokenDao().getAccessTokenDirect() - if (auth != null) { - runCatching { - service.sendToken(mapOf("token" to token)).execute() - } - } else { - Timber.d("Disconnected") - } - preferences.edit().putString("current_firebase_token", token).apply() - } - - private fun serviceLogin() { - val token = authRepository.getAccessTokenDirect() - if (token == null || !preferences.getBoolean("__reconnect_account_for_name_update__", false)) { - executors.networkIO().execute { - authRepository.performAccountSyncState() - } - preferences.edit().putBoolean("__reconnect_account_for_name_update__", true).apply() - } - } - - private fun createNewVersionNotification() { - val currentVersion = remoteConfig.getLong("version_current") - val notified = preferences.getBoolean("version_ntf_key_$currentVersion", false) - if (currentVersion > BuildConfig.VERSION_CODE && !notified) { - val notes = remoteConfig.getString("version_notes") - val version = remoteConfig.getString("version_name") - val title = context.getString(R.string.new_version_ntf_title_format, version) - NotificationCreator.showSimpleNotification(context, title, notes) - preferences.edit().putBoolean("version_ntf_key_$currentVersion", true).apply() - } - } - - private fun initialPage(): Document? { - val document = SagresNavigator.instance.startPage() - when (document.status) { - Status.SUCCESS -> return document.document - else -> produceErrorMessage(document) - } - return null - } - - fun login(access: Access, gToken: String?): Document? { - val login = SagresNavigator.instance.login(access.username, access.password, gToken) - when (login.status) { - Status.SUCCESS -> { - return login.document - } - Status.INVALID_LOGIN -> { - if (login.code == 401) { - onInvalidLogin() - } - } - else -> produceErrorMessage(login) - } - return null - } - - private fun markSessionExpired() { - val access = database.accessDao().getAccessDirect() - if (access != null && access.valid) { - database.accessDao().setAccessValidation(false) - } - } - - private fun onInvalidLogin() { - val access = database.accessDao().getAccessDirect() - if ( - access != null && - access.valid && - com.forcetower.sagres.Constants.getParameter("REQUIRES_CAPTCHA") != "true" - ) { - database.accessDao().setAccessValidation(false) - NotificationCreator.showInvalidAccessNotification(context) - } - } - - private fun me(score: Double, access: Access): SagresPerson? { - val username = access.username - if (username.contains("@")) { - return continueWithHtml(username, score) - } else { - val me = SagresNavigator.instance.me() - Timber.d("Me response: ${me.status}") - when (me.status) { - Status.SUCCESS -> { - val person = me.person - if (person != null) { - database.profileDao().insert(person, score) - Timber.d("Me completed. Person name: ${person.name}") - return person - } else { - Timber.e("Page loaded but API returned invalid types") - } - } - Status.RESPONSE_FAILED, Status.NETWORK_ERROR -> { - return continueWithHtml(username, score) - } - else -> produceErrorMessage(me) - } - } - return null - } - - private fun continueWithHtml(username: String, score: Double): SagresPerson? { - val start = SagresNavigator.instance.startPage().document ?: return null - val name = SagresBasicParser.getName(start) ?: username - val person = SagresPerson(username.hashCode().toLong(), name, name, "00000000000", username).apply { isMocked = true } - database.profileDao().insert(person, score) - return person - } - - @WorkerThread - private fun messages(userId: Long?): Boolean { - Timber.d("Messages was invoked using $userId") - val messages = if (userId != null) - SagresNavigator.instance.messages(userId) - else - SagresNavigator.instance.messagesHtml() - - Timber.d("Did receive a valid list? ${messages.messages != null}, ${messages.status}") - - return when (messages.status) { - Status.SUCCESS -> { - val values = messages.messages?.map { Message.fromMessage(it, false) } ?: emptyList() - Timber.d("Messages mapped: ${values.size}") - database.messageDao().insertIgnoring(values) - messagesNotifications() - Timber.d("Messages completed. Messages size is ${values.size}") - true - } - else -> { - produceErrorMessage(messages) - false - } - } - } - - @WorkerThread - private fun semesters(userId: Long): Boolean { - val semesters = SagresNavigator.instance.semesters(userId) - return when (semesters.status) { - Status.SUCCESS -> { - val values = semesters.getSemesters().map { Semester.fromSagres(it) } - database.semesterDao().insertIgnoring(values) - Timber.d("Semesters Completed with: ${semesters.getSemesters()}") - true - } - else -> { - produceErrorMessage(semesters) - false - } - } - } - - @WorkerThread - private fun startPage(): Boolean { - val start = SagresNavigator.instance.startPage() - return when (start.status) { - Status.SUCCESS -> { - defineCalendar(start.calendar) - defineDisciplines(start.disciplines) - defineDisciplineGroups(start.groups) - defineSchedule(start.locations) - defineDemand(start.isDemandOpen) - - Timber.d("Semesters: ${start.semesters}") - Timber.d("Disciplines: ${start.disciplines}") - Timber.d("Groups: ${start.groups}") - Timber.d("Calendar: ${start.calendar}") - true - } - else -> { - produceErrorMessage(start) - false - } - } - } - - @WorkerThread - private fun disciplinesExperimental(): Boolean { - Timber.d("Experimental Experimental Start") - val experimental = SagresNavigator.instance.disciplinesExperimental() - return when (experimental.status) { - Status.COMPLETED -> { - Timber.d("Experimental Completed") - defineSemesters(experimental.getSemesters()) - defineDisciplines(experimental.getDisciplines()) - defineDisciplineGroups(experimental.getGroups()) - - materialsNotifications() - - Timber.d("Semesters: ${experimental.getSemesters()}") - Timber.d("Disciplines: ${experimental.getDisciplines()}") - Timber.d("Groups: ${experimental.getGroups()}") - true - } - else -> { - Timber.d("Experimental Failed") - produceErrorMessage(experimental) - false - } - } - } - - @WorkerThread - private fun materialsNotifications() { - database.classMaterialDao().run { - getAllUnnotified().forEach { - NotificationCreator.showMaterialPostedNotification(context, it) - } - markAllNotified() - } - } - - @WorkerThread - private fun grades(): Boolean { - val grades = SagresNavigator.instance.getCurrentGrades() - return when (grades.status) { - Status.SUCCESS -> { - defineSemesters(grades.semesters) - defineGrades(grades.grades) - defineFrequency(grades.frequency) - - Timber.d("Grades received: ${grades.grades}") - Timber.d("Frequency: ${grades.frequency}") - Timber.d("Semesters: ${grades.semesters}") - - gradesNotifications() - - Timber.d("Completed!") - true - } - else -> { - produceErrorMessage(grades) - false - } - } - } - - @WorkerThread - private fun servicesRequest(): Boolean { - val services = SagresNavigator.instance.getRequestedServices() - return when (services.status) { - Status.SUCCESS -> { - defineServices(services.services) - Timber.d("Services Requested: ${services.services}") - servicesNotifications() - true - } - else -> { - produceErrorMessage(services) - false - } - } - } - - private fun servicesNotifications() { - database.serviceRequestDao().run { - val created = getCreatedDirect() - val updated = getStatusChangedDirect() - - created.forEach { NotificationCreator.createServiceRequestNotification(context, it, false) } - updated.forEach { NotificationCreator.createServiceRequestNotification(context, it, true) } - - markAllNotified() - } - } - - private fun defineServices(services: List?) { - services ?: return - val list = services.map { ServiceRequest.fromSagres(it) } - database.serviceRequestDao().insertList(list) - } - - private fun gradesNotifications() { - database.gradesDao().run { - val posted = getPostedGradesDirect() - val create = getCreatedGradesDirect() - val change = getChangedGradesDirect() - val date = getDateChangedGradesDirect() - - markAllNotified() - - posted.forEach { NotificationCreator.showSagresPostedGradesNotification(it, context) } - create.forEach { NotificationCreator.showSagresCreateGradesNotification(it, context) } - change.forEach { NotificationCreator.showSagresChangeGradesNotification(it, context) } - date.forEach { NotificationCreator.showSagresDateGradesNotification(it, context) } - } - } - - @WorkerThread - private fun messagesNotifications() { - val messages = database.messageDao().getNewMessages() - database.messageDao().setAllNotified() - messages.forEach { it.notify(context) } - } - - @WorkerThread - private fun defineFrequency(frequency: List?) { - frequency ?: return - database.classAbsenceDao().putAbsences(frequency) - } - - @WorkerThread - private fun defineGrades(grades: List?) { - grades ?: return - database.gradesDao().putGrades(grades) - } - - @WorkerThread - private fun defineSemesters(semesters: List>?) { - semesters?.forEach { - val semester = Semester(sagresId = it.first, name = it.second, codename = it.second) - database.semesterDao().insertIgnoring(semester) - } - } - - @WorkerThread - private fun defineSchedule(locations: List?) { - locations ?: return - val ordering = preferences.getBoolean("stg_semester_deterministic_ordering", true) - val shrinkSchedule = preferences.getBoolean("stg_schedule_shrinking", true) - if (shrinkSchedule) { - val shrink = LocationShrinker.shrink(locations) - database.classLocationDao().putSchedule(shrink, ordering) - } else { - database.classLocationDao().putSchedule(locations, ordering) - } - } - - @WorkerThread - private fun defineDisciplineGroups(groups: List?) { - groups ?: return - database.classGroupDao().defineGroups(groups) - } - - @WorkerThread - private fun defineDisciplines(disciplines: List?) { - disciplines ?: return - val values = disciplines.map { Discipline.fromSagres(it) } - database.disciplineDao().insert(values) - disciplines.forEach { database.classDao().insert(it, true) } - } - - @WorkerThread - private fun defineCalendar(calendar: List?) { - val values = calendar?.map { CalendarItem.fromSagres(it) } - database.calendarDao().deleteAndInsert(values) - } - - private fun defineDemand(demandOpen: Boolean) { - val flags = database.flagsDao().getFlagsDirect() - if (flags == null) database.flagsDao().insertFlags(SagresFlags()) - - database.flagsDao().updateDemand(demandOpen) - - if ((flags?.demandOpen == false || flags?.demandOpen == null) && demandOpen) { - NotificationCreator.showDemandOpenNotification(context) - } - } - - private fun produceErrorMessage(callback: BaseCallback<*>) { - Timber.d("Is throwable invalid? ${callback.throwable == null}") - callback.throwable?.printStackTrace() - Timber.e("Failed executing with status ${callback.status} and throwable message [${callback.throwable?.message}]") - } - - suspend fun asyncSync(gToken: String?) { - performSync("Manual", gToken) - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.repository + +import android.content.Context +import android.content.SharedPreferences +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.net.wifi.WifiInfo +import android.net.wifi.WifiManager +import android.os.Build +import android.telephony.TelephonyManager +import androidx.annotation.WorkerThread +import androidx.core.content.ContextCompat +import com.forcetower.core.getDynamicDataSourceFactory +import com.forcetower.sagres.SagresNavigator +import com.forcetower.sagres.database.model.SagresCalendar +import com.forcetower.sagres.database.model.SagresCredential +import com.forcetower.sagres.database.model.SagresDiscipline +import com.forcetower.sagres.database.model.SagresDisciplineClassLocation +import com.forcetower.sagres.database.model.SagresDisciplineGroup +import com.forcetower.sagres.database.model.SagresDisciplineMissedClass +import com.forcetower.sagres.database.model.SagresGrade +import com.forcetower.sagres.database.model.SagresMessage +import com.forcetower.sagres.database.model.SagresPerson +import com.forcetower.sagres.database.model.SagresRequestedService +import com.forcetower.sagres.operation.BaseCallback +import com.forcetower.sagres.operation.Status +import com.forcetower.sagres.parsers.SagresBasicParser +import com.forcetower.sagres.parsers.SagresMessageParser +import com.forcetower.sagres.parsers.SagresScheduleParser +import com.forcetower.sagres.utils.ConnectedStates +import com.forcetower.uefs.AppExecutors +import com.forcetower.uefs.BuildConfig +import com.forcetower.uefs.R +import com.forcetower.uefs.core.constants.Constants +import com.forcetower.uefs.core.model.unes.Access +import com.forcetower.uefs.core.model.unes.CalendarItem +import com.forcetower.uefs.core.model.unes.Discipline +import com.forcetower.uefs.core.model.unes.Message +import com.forcetower.uefs.core.model.unes.NetworkType +import com.forcetower.uefs.core.model.unes.SagresFlags +import com.forcetower.uefs.core.model.unes.Semester +import com.forcetower.uefs.core.model.unes.ServiceRequest +import com.forcetower.uefs.core.model.unes.SyncRegistry +import com.forcetower.uefs.core.model.unes.notify +import com.forcetower.uefs.core.storage.database.UDatabase +import com.forcetower.uefs.core.storage.network.UService +import com.forcetower.uefs.core.storage.repository.cloud.AuthRepository +import com.forcetower.uefs.core.util.LocationShrinker +import com.forcetower.uefs.core.util.VersionUtils +import com.forcetower.uefs.core.util.isStudentFromUEFS +import com.forcetower.uefs.core.work.discipline.DisciplinesDetailsWorker +import com.forcetower.uefs.core.work.hourglass.HourglassContributeWorker +import com.forcetower.uefs.service.NotificationCreator +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.google.firebase.messaging.FirebaseMessaging +import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import java.util.Calendar +import java.util.Locale +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.tasks.await +import kotlinx.coroutines.withContext +import org.jsoup.nodes.Document +import timber.log.Timber + +@Singleton +class SagresSyncRepository @Inject constructor( + private val context: Context, + private val database: UDatabase, + private val executors: AppExecutors, + private val authRepository: AuthRepository, + private val adventureRepository: AdventureRepository, + private val cookieSessionRepository: CookieSessionRepository, + private val service: UService, + private val remoteConfig: FirebaseRemoteConfig, + private val preferences: SharedPreferences +) { + private val mutex = Mutex() + + private suspend fun findAndMatch() { + val aeri = getDynamicDataSourceFactory(context, "com.forcetower.uefs.aeri.domain.AERIDataSourceFactoryProvider") + aeri?.create()?.run { + update() + getNotifyMessages().forEach { + NotificationCreator.showSimpleNotification(context, it.title, it.content) + } + } + } + + @WorkerThread + suspend fun performSync(executor: String, gToken: String? = null) = withContext(Dispatchers.IO) { + val registry = createRegistry(executor) + val access = database.accessDao().getAccessDirect() + access ?: Timber.d("Access is null, sync will not continue") + if (access != null) { + FirebaseCrashlytics.getInstance().setUserId(access.username) + // Only one sync may be active at a time + mutex.withLock { + execute(access, registry, executor, gToken) + } + } else { + registry.completed = true + registry.error = -1 + registry.success = false + registry.message = "Credenciais de acesso inválidas" + registry.end = System.currentTimeMillis() + database.syncRegistryDao().insert(registry) + } + } + + private fun createRegistry(executor: String): SyncRegistry { + val connectivity = ContextCompat.getSystemService(context, ConnectivityManager::class.java)!! + + if (VersionUtils.isMarshmallow()) { + val capabilities = connectivity.getNetworkCapabilities(connectivity.activeNetwork) + return if (capabilities != null) { + val wifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + val network = if (wifi) { + val manager = context.getSystemService(WifiManager::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + (capabilities.transportInfo as? WifiInfo)?.ssid ?: "Unknown" + } else { + @Suppress("DEPRECATION") + manager?.connectionInfo?.ssid ?: "Unknown" + } + } else { + val manager = context.getSystemService(TelephonyManager::class.java) + manager?.simOperatorName ?: "Operator" + } + Timber.d("Is on Wifi? $wifi. Network name: $network") + SyncRegistry( + executor = executor, + network = network, + networkType = if (wifi) NetworkType.WIFI.ordinal else NetworkType.CELLULAR.ordinal + ) + } else { + SyncRegistry(executor = executor, network = "Invalid", networkType = NetworkType.OTHER.ordinal) + } + } else { + val manager = ContextCompat.getSystemService(context, WifiManager::class.java) + + @Suppress("DEPRECATION") + val info = manager?.connectionInfo + return if (info == null) { + val phone = ContextCompat.getSystemService(context, TelephonyManager::class.java) + val operatorName = phone?.simOperatorName + SyncRegistry(executor = executor, network = operatorName ?: "invalid", networkType = NetworkType.CELLULAR.ordinal) + } else { + SyncRegistry(executor = executor, network = info.ssid, networkType = NetworkType.WIFI.ordinal) + } + } + } + + @WorkerThread + private suspend fun execute(access: Access, registry: SyncRegistry, executor: String, gToken: String?) { + val uid = database.syncRegistryDao().insert(registry) + val calendar = Calendar.getInstance() + val today = calendar.get(Calendar.DAY_OF_MONTH) + registry.uid = uid + SagresNavigator.instance.putCredentials( + SagresCredential( + access.username, + access.password, + SagresNavigator.instance.getSelectedInstitution() + ) + ) + + // Internal checks for canceling auto sync + // this was useful to avoid unes from ddos'ing the website. + // they said unes was doing it anyways, so here is a deleted useless piece of code + // you welcome + if (!Constants.EXECUTOR_WHITELIST.contains(executor.lowercase(Locale.getDefault()))) { + Timber.d("There was a time where this would cause sync to be aborted if server.. But i don't care anymore") + } + + runCatching { findAndMatch() } + + val isStudentFromUEFS = preferences.isStudentFromUEFS() + val isCaptchaInjectionNeeded = isStudentFromUEFS && com.forcetower.sagres.Constants.getParameter("REQUIRES_CAPTCHA") == "true" + if (isCaptchaInjectionNeeded) { + val injected = cookieSessionRepository.injectGoodCookiesOnClient() + if (injected == CookieSessionRepository.INJECT_ERROR_NO_VALUE) { + Timber.i("The cookie that is good no one wants to give!") + NotificationCreator.showSimpleNotification( + context, + "Então...", + "Você não tem sessão criada, entra de novo" + ) + + registry.completed = true + registry.error = 1 shl 11 + registry.success = false + registry.skipped = 0 + registry.message = "Deve-se consultar as flags de erro" + registry.end = System.currentTimeMillis() + database.syncRegistryDao().update(registry) + + markSessionExpired() + return + } else if (injected == CookieSessionRepository.INJECT_ERROR_NETWORK) { + Timber.i("Failed to connect to UNES Services") + registry.completed = true + registry.error = 1 shl 10 + registry.success = false + registry.skipped = 0 + registry.message = "Deve-se consultar as flags de erro" + registry.end = System.currentTimeMillis() + database.syncRegistryDao().update(registry) + return + } + } + + database.gradesDao().markAllNotified() + database.messageDao().setAllNotified() + database.classMaterialDao().markAllNotified() + + val homeDoc = when { + isStudentFromUEFS && gToken == null && isCaptchaInjectionNeeded -> initialPage() + isStudentFromUEFS && !isCaptchaInjectionNeeded -> login(access, null) + !isStudentFromUEFS -> login(access, gToken) + else -> null + } + + if (isStudentFromUEFS) { + val connection = SagresBasicParser.isConnected(homeDoc) + if (connection == ConnectedStates.INVALID) { + cookieSessionRepository.invalidateCookies() + NotificationCreator.showSimpleNotification( + context, + "Vish...", + "Sua sessão expirou, entra de novo" + ) + markSessionExpired() + return + } + } + + val score = SagresBasicParser.getScore(homeDoc) + Timber.d("Login Completed. Score Parsed: $score") + + // Since stuff is just broken.... + if (homeDoc == null) { + registry.completed = true + registry.error = -2 + registry.success = false + registry.message = "Login falhou" + } else { + defineSchedule(SagresScheduleParser.getSchedule(homeDoc)) + defineMessages(SagresMessageParser.getMessages(homeDoc)) + } + + val person = me(score, access) + Timber.d("The person from me is ${person?.name} ${person?.isMocked}") + if (person == null) { + registry.completed = true + registry.error = -3 + registry.success = false + registry.message = "The dream is over" + registry.end = System.currentTimeMillis() + database.syncRegistryDao().update(registry) + return + } + + var result = 0 + var skipped = 0 + + if (!person.isMocked) { + Timber.d("I guess the person is not a mocked version on it") + if (!messages(person.id)) { + if (homeDoc != null && !messages(null)) result += 1 shl 1 + } + if (!semesters(person.id)) result += 1 shl 2 + if (homeDoc == null) { + registry.completed = true + registry.error = 10 + registry.success = true + registry.message = "Partial sync" + registry.executor = "${registry.executor}-Parc" + registry.end = System.currentTimeMillis() + database.syncRegistryDao().update(registry) + return + } + } else { + if (!messages(null)) result += 1 shl 1 + } + + val dailyDisciplines = + preferences.getString("stg_daily_discipline_sync", "2")?.toIntOrNull() ?: 2 + val currentDaily = preferences.getInt("daily_discipline_count", 0) + val currentDayDiscipline = preferences.getInt("daily_discipline_day", -1) + val lastDailyHour = preferences.getInt("daily_discipline_hour", 0) + val isNewDaily = currentDayDiscipline != today || dailyDisciplines == -1 + val currentDailyHour = calendar.get(Calendar.HOUR_OF_DAY) + + val (actualDailyCount, nextHour) = if (isNewDaily) { + 0 to -1 + } else { + currentDaily to if (lastDailyHour < 8) 10 else lastDailyHour + 4 + } + + val shouldDisciplineSync = + ((actualDailyCount < dailyDisciplines) || (dailyDisciplines == -1)) && + (currentDailyHour >= nextHour) + + if (shouldDisciplineSync) { + if (!disciplinesExperimental()) { + result += 1 shl 6 + } else { + preferences.edit() + .putInt("daily_discipline_count", actualDailyCount + 1) + .putInt("daily_discipline_day", today) + .putInt("daily_discipline_hour", currentDailyHour) + .apply() + } + } else { + skipped += 1 shl 1 + } + + if (!startPage()) result += 1 shl 3 + if (!grades()) result += 1 shl 4 + if (!servicesRequest()) result += 1 shl 5 + + if (isStudentFromUEFS) { + serviceLogin() + } + + if (preferences.getBoolean("primary_fetch", true)) { + DisciplinesDetailsWorker.createWorker(context) + preferences.edit().putBoolean("primary_fetch", false).apply() + } + + if (isStudentFromUEFS) { + if (!preferences.getBoolean("sent_hourglass_testing_data_0.0.2", false) && + authRepository.getAccessTokenDirect() != null + ) { + HourglassContributeWorker.createWorker(context) + preferences.edit().putBoolean("sent_hourglass_testing_data_0.0.2", true).apply() + } + } + + try { + val day = preferences.getInt("sync_daily_update", -1) + if (day != today) { + adventureRepository.performCheckAchievements(HashMap()) + + val task = FirebaseMessaging.getInstance().token + val value = task.await() + onNewToken(value) + + preferences.edit().putInt("sync_daily_update", today).apply() + } + createNewVersionNotification() + } catch (t: Throwable) { + Timber.e(t) + } + + registry.completed = true + registry.error = result + registry.success = result == 0 + registry.skipped = skipped + registry.message = "Deve-se consultar as flags de erro" + registry.end = System.currentTimeMillis() + database.syncRegistryDao().update(registry) + } + + private fun defineMessages(values: List) { + values.reversed().forEachIndexed { index, message -> + message.processingTime = System.currentTimeMillis() + index + } + database.messageDao().insertIgnoring(values.map { Message.fromMessage(it, false) }) + } + + private fun onNewToken(token: String) { + val auth = database.accessTokenDao().getAccessTokenDirect() + if (auth != null) { + runCatching { + service.sendToken(mapOf("token" to token)).execute() + } + } else { + Timber.d("Disconnected") + } + preferences.edit().putString("current_firebase_token", token).apply() + } + + private fun serviceLogin() { + val token = authRepository.getAccessTokenDirect() + if (token == null || !preferences.getBoolean("__reconnect_account_for_name_update__", false)) { + executors.networkIO().execute { + authRepository.performAccountSyncState() + } + preferences.edit().putBoolean("__reconnect_account_for_name_update__", true).apply() + } + } + + private fun createNewVersionNotification() { + val currentVersion = remoteConfig.getLong("version_current") + val notified = preferences.getBoolean("version_ntf_key_$currentVersion", false) + if (currentVersion > BuildConfig.VERSION_CODE && !notified) { + val notes = remoteConfig.getString("version_notes") + val version = remoteConfig.getString("version_name") + val title = context.getString(R.string.new_version_ntf_title_format, version) + NotificationCreator.showSimpleNotification(context, title, notes) + preferences.edit().putBoolean("version_ntf_key_$currentVersion", true).apply() + } + } + + private fun initialPage(): Document? { + val document = SagresNavigator.instance.startPage() + when (document.status) { + Status.SUCCESS -> return document.document + else -> produceErrorMessage(document) + } + return null + } + + fun login(access: Access, gToken: String?): Document? { + val login = SagresNavigator.instance.login(access.username, access.password, gToken) + when (login.status) { + Status.SUCCESS -> { + return login.document + } + Status.INVALID_LOGIN -> { + if (login.code == 401) { + onInvalidLogin() + } + } + else -> produceErrorMessage(login) + } + return null + } + + private fun markSessionExpired() { + val access = database.accessDao().getAccessDirect() + if (access != null && access.valid) { + database.accessDao().setAccessValidation(false) + } + } + + private fun onInvalidLogin() { + val access = database.accessDao().getAccessDirect() + if ( + access != null && + access.valid && + com.forcetower.sagres.Constants.getParameter("REQUIRES_CAPTCHA") != "true" + ) { + database.accessDao().setAccessValidation(false) + NotificationCreator.showInvalidAccessNotification(context) + } + } + + private fun me(score: Double, access: Access): SagresPerson? { + val username = access.username + if (username.contains("@")) { + return continueWithHtml(username, score) + } else { + val me = SagresNavigator.instance.me() + Timber.d("Me response: ${me.status}") + when (me.status) { + Status.SUCCESS -> { + val person = me.person + if (person != null) { + database.profileDao().insert(person, score) + Timber.d("Me completed. Person name: ${person.name}") + return person + } else { + Timber.e("Page loaded but API returned invalid types") + } + } + Status.RESPONSE_FAILED, Status.NETWORK_ERROR -> { + return continueWithHtml(username, score) + } + else -> produceErrorMessage(me) + } + } + return null + } + + private fun continueWithHtml(username: String, score: Double): SagresPerson? { + val start = SagresNavigator.instance.startPage().document ?: return null + val name = SagresBasicParser.getName(start) ?: username + val person = SagresPerson(username.hashCode().toLong(), name, name, "00000000000", username).apply { isMocked = true } + database.profileDao().insert(person, score) + return person + } + + @WorkerThread + private fun messages(userId: Long?): Boolean { + Timber.d("Messages was invoked using $userId") + val messages = if (userId != null) { + SagresNavigator.instance.messages(userId) + } else { + SagresNavigator.instance.messagesHtml() + } + + Timber.d("Did receive a valid list? ${messages.messages != null}, ${messages.status}") + + return when (messages.status) { + Status.SUCCESS -> { + val values = messages.messages?.map { Message.fromMessage(it, false) } ?: emptyList() + Timber.d("Messages mapped: ${values.size}") + database.messageDao().insertIgnoring(values) + messagesNotifications() + Timber.d("Messages completed. Messages size is ${values.size}") + true + } + else -> { + produceErrorMessage(messages) + false + } + } + } + + @WorkerThread + private fun semesters(userId: Long): Boolean { + val semesters = SagresNavigator.instance.semesters(userId) + return when (semesters.status) { + Status.SUCCESS -> { + val values = semesters.getSemesters().map { Semester.fromSagres(it) } + database.semesterDao().insertIgnoring(values) + Timber.d("Semesters Completed with: ${semesters.getSemesters()}") + true + } + else -> { + produceErrorMessage(semesters) + false + } + } + } + + @WorkerThread + private fun startPage(): Boolean { + val start = SagresNavigator.instance.startPage() + return when (start.status) { + Status.SUCCESS -> { + defineCalendar(start.calendar) + defineDisciplines(start.disciplines) + defineDisciplineGroups(start.groups) + defineSchedule(start.locations) + defineDemand(start.isDemandOpen) + + Timber.d("Semesters: ${start.semesters}") + Timber.d("Disciplines: ${start.disciplines}") + Timber.d("Groups: ${start.groups}") + Timber.d("Calendar: ${start.calendar}") + true + } + else -> { + produceErrorMessage(start) + false + } + } + } + + @WorkerThread + private fun disciplinesExperimental(): Boolean { + Timber.d("Experimental Experimental Start") + val experimental = SagresNavigator.instance.disciplinesExperimental() + return when (experimental.status) { + Status.COMPLETED -> { + Timber.d("Experimental Completed") + defineSemesters(experimental.getSemesters()) + defineDisciplines(experimental.getDisciplines()) + defineDisciplineGroups(experimental.getGroups()) + + materialsNotifications() + + Timber.d("Semesters: ${experimental.getSemesters()}") + Timber.d("Disciplines: ${experimental.getDisciplines()}") + Timber.d("Groups: ${experimental.getGroups()}") + true + } + else -> { + Timber.d("Experimental Failed") + produceErrorMessage(experimental) + false + } + } + } + + @WorkerThread + private fun materialsNotifications() { + database.classMaterialDao().run { + getAllUnnotified().forEach { + NotificationCreator.showMaterialPostedNotification(context, it) + } + markAllNotified() + } + } + + @WorkerThread + private fun grades(): Boolean { + val grades = SagresNavigator.instance.getCurrentGrades() + return when (grades.status) { + Status.SUCCESS -> { + defineSemesters(grades.semesters) + defineGrades(grades.grades) + defineFrequency(grades.frequency) + + Timber.d("Grades received: ${grades.grades}") + Timber.d("Frequency: ${grades.frequency}") + Timber.d("Semesters: ${grades.semesters}") + + gradesNotifications() + + Timber.d("Completed!") + true + } + else -> { + produceErrorMessage(grades) + false + } + } + } + + @WorkerThread + private fun servicesRequest(): Boolean { + val services = SagresNavigator.instance.getRequestedServices() + return when (services.status) { + Status.SUCCESS -> { + defineServices(services.services) + Timber.d("Services Requested: ${services.services}") + servicesNotifications() + true + } + else -> { + produceErrorMessage(services) + false + } + } + } + + private fun servicesNotifications() { + database.serviceRequestDao().run { + val created = getCreatedDirect() + val updated = getStatusChangedDirect() + + created.forEach { NotificationCreator.createServiceRequestNotification(context, it, false) } + updated.forEach { NotificationCreator.createServiceRequestNotification(context, it, true) } + + markAllNotified() + } + } + + private fun defineServices(services: List?) { + services ?: return + val list = services.map { ServiceRequest.fromSagres(it) } + database.serviceRequestDao().insertList(list) + } + + private fun gradesNotifications() { + database.gradesDao().run { + val posted = getPostedGradesDirect() + val create = getCreatedGradesDirect() + val change = getChangedGradesDirect() + val date = getDateChangedGradesDirect() + + markAllNotified() + + posted.forEach { NotificationCreator.showSagresPostedGradesNotification(it, context) } + create.forEach { NotificationCreator.showSagresCreateGradesNotification(it, context) } + change.forEach { NotificationCreator.showSagresChangeGradesNotification(it, context) } + date.forEach { NotificationCreator.showSagresDateGradesNotification(it, context) } + } + } + + @WorkerThread + private fun messagesNotifications() { + val messages = database.messageDao().getNewMessages() + database.messageDao().setAllNotified() + messages.forEach { it.notify(context) } + } + + @WorkerThread + private fun defineFrequency(frequency: List?) { + frequency ?: return + database.classAbsenceDao().putAbsences(frequency) + } + + @WorkerThread + private fun defineGrades(grades: List?) { + grades ?: return + database.gradesDao().putGrades(grades) + } + + @WorkerThread + private fun defineSemesters(semesters: List>?) { + semesters?.forEach { + val semester = Semester(sagresId = it.first, name = it.second, codename = it.second) + database.semesterDao().insertIgnoring(semester) + } + } + + @WorkerThread + private fun defineSchedule(locations: List?) { + locations ?: return + val ordering = preferences.getBoolean("stg_semester_deterministic_ordering", true) + val shrinkSchedule = preferences.getBoolean("stg_schedule_shrinking", true) + if (shrinkSchedule) { + val shrink = LocationShrinker.shrink(locations) + database.classLocationDao().putSchedule(shrink, ordering) + } else { + database.classLocationDao().putSchedule(locations, ordering) + } + } + + @WorkerThread + private fun defineDisciplineGroups(groups: List?) { + groups ?: return + database.classGroupDao().defineGroups(groups) + } + + @WorkerThread + private fun defineDisciplines(disciplines: List?) { + disciplines ?: return + val values = disciplines.map { Discipline.fromSagres(it) } + database.disciplineDao().insert(values) + disciplines.forEach { database.classDao().insert(it, true) } + } + + @WorkerThread + private fun defineCalendar(calendar: List?) { + val values = calendar?.map { CalendarItem.fromSagres(it) } + database.calendarDao().deleteAndInsert(values) + } + + private fun defineDemand(demandOpen: Boolean) { + val flags = database.flagsDao().getFlagsDirect() + if (flags == null) database.flagsDao().insertFlags(SagresFlags()) + + database.flagsDao().updateDemand(demandOpen) + + if ((flags?.demandOpen == false || flags?.demandOpen == null) && demandOpen) { + NotificationCreator.showDemandOpenNotification(context) + } + } + + private fun produceErrorMessage(callback: BaseCallback<*>) { + Timber.d("Is throwable invalid? ${callback.throwable == null}") + callback.throwable?.printStackTrace() + Timber.e("Failed executing with status ${callback.status} and throwable message [${callback.throwable?.message}]") + } + + suspend fun asyncSync(gToken: String?) { + performSync("Manual", gToken) + } +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/ScheduleRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/ScheduleRepository.kt index e6da9a156..c6ca97b99 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/ScheduleRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/ScheduleRepository.kt @@ -1,96 +1,96 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.repository - -import androidx.annotation.WorkerThread -import androidx.lifecycle.LiveData -import com.forcetower.uefs.core.model.ui.ProcessedClassLocation -import com.forcetower.uefs.core.model.unes.Profile -import com.forcetower.uefs.core.storage.database.UDatabase -import com.google.android.gms.tasks.Tasks -import com.google.firebase.firestore.CollectionReference -import com.google.firebase.firestore.SetOptions -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Named -import javax.inject.Singleton - -@Singleton -class ScheduleRepository @Inject constructor( - @Named(Profile.COLLECTION) - private val collection: CollectionReference, - private val database: UDatabase -) { - - @WorkerThread - fun saveSchedule(userId: String) { - val schedule = database.classLocationDao().getCurrentScheduleDirect() - val semester = database.semesterDao().getSemestersDirect().maxByOrNull { it.sagresId } - if (schedule.isEmpty() || semester == null) { - Timber.d("It's too late to apologize") - return - } - - val reference = collection.document(userId) - .collection("schedule") - .document(semester.sagresId.toString()) - - val mapped = mapOf( - "locations" to schedule - ) - - try { - Tasks.await(reference.set(mapped, SetOptions.merge())) - } catch (t: Throwable) { - Timber.e(t) - } - } - - fun getProcessedSchedule(): Flow>> { - val source = database.classLocationDao().getCurrentVisibleSchedulePerformance() - return source.map { data -> - val timers = data.map { Timed(it.location.startsAtInt, it.location.endsAtInt, it.location.startsAt, it.location.endsAt) }.distinctBy { it.start }.sortedBy { it.start } - data.groupBy { it.location.dayInt }.mapValues { entry -> - val dayList = timers.map { timed -> - val location = entry.value.find { it.location.startsAtInt == timed.start && it.location.endsAtInt == timed.end } - val element = if (location == null) ProcessedClassLocation.EmptySpace() else ProcessedClassLocation.ElementSpace(location) - element - } - dayList - }.toMutableMap().apply { - put(-1, timers.map { ProcessedClassLocation.TimeSpace(it.startString, it.endString, it.start, it.end) }) - }.toMap() - } - } - - fun hasSchedule(): LiveData { - return database.classLocationDao().hasSchedule() - } - - private data class Timed( - val start: Int, - val end: Int, - val startString: String, - val endString: String - ) -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.repository + +import androidx.annotation.WorkerThread +import androidx.lifecycle.LiveData +import com.forcetower.uefs.core.model.ui.ProcessedClassLocation +import com.forcetower.uefs.core.model.unes.Profile +import com.forcetower.uefs.core.storage.database.UDatabase +import com.google.android.gms.tasks.Tasks +import com.google.firebase.firestore.CollectionReference +import com.google.firebase.firestore.SetOptions +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import timber.log.Timber + +@Singleton +class ScheduleRepository @Inject constructor( + @Named(Profile.COLLECTION) + private val collection: CollectionReference, + private val database: UDatabase +) { + + @WorkerThread + fun saveSchedule(userId: String) { + val schedule = database.classLocationDao().getCurrentScheduleDirect() + val semester = database.semesterDao().getSemestersDirect().maxByOrNull { it.sagresId } + if (schedule.isEmpty() || semester == null) { + Timber.d("It's too late to apologize") + return + } + + val reference = collection.document(userId) + .collection("schedule") + .document(semester.sagresId.toString()) + + val mapped = mapOf( + "locations" to schedule + ) + + try { + Tasks.await(reference.set(mapped, SetOptions.merge())) + } catch (t: Throwable) { + Timber.e(t) + } + } + + fun getProcessedSchedule(): Flow>> { + val source = database.classLocationDao().getCurrentVisibleSchedulePerformance() + return source.map { data -> + val timers = data.map { Timed(it.location.startsAtInt, it.location.endsAtInt, it.location.startsAt, it.location.endsAt) }.distinctBy { it.start }.sortedBy { it.start } + data.groupBy { it.location.dayInt }.mapValues { entry -> + val dayList = timers.map { timed -> + val location = entry.value.find { it.location.startsAtInt == timed.start && it.location.endsAtInt == timed.end } + val element = if (location == null) ProcessedClassLocation.EmptySpace() else ProcessedClassLocation.ElementSpace(location) + element + } + dayList + }.toMutableMap().apply { + put(-1, timers.map { ProcessedClassLocation.TimeSpace(it.startString, it.endString, it.start, it.end) }) + }.toMap() + } + } + + fun hasSchedule(): LiveData { + return database.classLocationDao().hasSchedule() + } + + private data class Timed( + val start: Int, + val end: Int, + val startString: String, + val endString: String + ) +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/SettingsRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/SettingsRepository.kt index b739284b2..131022df5 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/SettingsRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/SettingsRepository.kt @@ -1,70 +1,70 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.repository - -import android.content.SharedPreferences -import androidx.annotation.MainThread -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import com.forcetower.uefs.core.storage.database.UDatabase -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class SettingsRepository @Inject constructor( - private val preferences: SharedPreferences, - private val database: UDatabase, - private val gradesRepository: SagresGradesRepository, - private val adventureRepository: AdventureRepository -) { - - @MainThread - fun hasDarkModeEnabled(): LiveData { - val result = MediatorLiveData() - val default = preferences.getBoolean("ach_night_mode_enabled", false) - result.value = default - -// val source = darkThemeRepository.getFirebaseProfile() -// result.addSource(source) { -// val enabled = it?.darkThemeEnabled ?: false -// preferences.edit().putBoolean("ach_night_mode_enabled", enabled).apply() -// result.postValue(enabled) -// } - - return result - } - - suspend fun requestAllGradesAndCalculateScore() = withContext(Dispatchers.IO) { - var loginNeeded = true - val semesters = database.semesterDao().getSemestersDirect() - semesters.forEach { - val result = gradesRepository.getGrades(it.sagresId, loginNeeded) - loginNeeded = false - if (result != 0) { - Timber.d("Failed to run on semester ${it.sagresId} - ${it.codename}: $result") - } - } - adventureRepository.performCheckAchievements(HashMap()) - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.repository + +import android.content.SharedPreferences +import androidx.annotation.MainThread +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import com.forcetower.uefs.core.storage.database.UDatabase +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import timber.log.Timber + +@Singleton +class SettingsRepository @Inject constructor( + private val preferences: SharedPreferences, + private val database: UDatabase, + private val gradesRepository: SagresGradesRepository, + private val adventureRepository: AdventureRepository +) { + + @MainThread + fun hasDarkModeEnabled(): LiveData { + val result = MediatorLiveData() + val default = preferences.getBoolean("ach_night_mode_enabled", false) + result.value = default + +// val source = darkThemeRepository.getFirebaseProfile() +// result.addSource(source) { +// val enabled = it?.darkThemeEnabled ?: false +// preferences.edit().putBoolean("ach_night_mode_enabled", enabled).apply() +// result.postValue(enabled) +// } + + return result + } + + suspend fun requestAllGradesAndCalculateScore() = withContext(Dispatchers.IO) { + var loginNeeded = true + val semesters = database.semesterDao().getSemestersDirect() + semesters.forEach { + val result = gradesRepository.getGrades(it.sagresId, loginNeeded) + loginNeeded = false + if (result != 0) { + Timber.d("Failed to run on semester ${it.sagresId} - ${it.codename}: $result") + } + } + adventureRepository.performCheckAchievements(HashMap()) + } +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/SnowpiercerLoginRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/SnowpiercerLoginRepository.kt index 94732468c..94f34051b 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/SnowpiercerLoginRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/SnowpiercerLoginRepository.kt @@ -22,12 +22,10 @@ package com.forcetower.uefs.core.storage.repository import android.content.Context import androidx.lifecycle.MutableLiveData -import com.forcetower.sagres.database.model.SagresPerson import com.forcetower.sagres.operation.Callback import com.forcetower.sagres.operation.Status import com.forcetower.uefs.AppExecutors import com.forcetower.uefs.R -import com.forcetower.uefs.core.model.unes.Access import com.forcetower.uefs.core.storage.database.UDatabase import com.forcetower.uefs.core.task.definers.DisciplinesProcessor import com.forcetower.uefs.core.task.definers.MessagesProcessor @@ -36,11 +34,11 @@ import dev.forcetower.breaker.Orchestra import dev.forcetower.breaker.model.Authorization import dev.forcetower.breaker.model.Person import dev.forcetower.breaker.result.Outcome +import javax.inject.Inject +import javax.inject.Named import kotlinx.coroutines.flow.flow import okhttp3.OkHttpClient import timber.log.Timber -import javax.inject.Inject -import javax.inject.Named class SnowpiercerLoginRepository @Inject constructor( private val client: OkHttpClient, @@ -70,7 +68,9 @@ class SnowpiercerLoginRepository @Inject constructor( if (login is Outcome.Error) { if (login.code == 401) { emit(Callback.Builder(Status.INVALID_LOGIN).code(401).build()) - } else emit(produceErrorMessage(login)) + } else { + emit(produceErrorMessage(login)) + } return@flow } diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/SnowpiercerSyncRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/SnowpiercerSyncRepository.kt index df66672b7..c8bb8f966 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/SnowpiercerSyncRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/SnowpiercerSyncRepository.kt @@ -47,16 +47,16 @@ import dev.forcetower.breaker.Orchestra import dev.forcetower.breaker.model.Authorization import dev.forcetower.breaker.model.Person import dev.forcetower.breaker.result.Outcome -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient -import timber.log.Timber import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.util.Calendar import javax.inject.Inject import javax.inject.Named import javax.inject.Singleton +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import timber.log.Timber @Singleton class SnowpiercerSyncRepository @Inject constructor( @@ -100,8 +100,11 @@ class SnowpiercerSyncRepository @Inject constructor( val login = orchestra.login() if (login is Outcome.Error) { - if (login.code == 401) onAccessInvalided() - else produceErrorMessage(login) + if (login.code == 401) { + onAccessInvalided() + } else { + produceErrorMessage(login) + } // The first request failed... stop here return } @@ -234,6 +237,7 @@ class SnowpiercerSyncRepository @Inject constructor( } } else { val manager = ContextCompat.getSystemService(context, WifiManager::class.java) + @Suppress("DEPRECATION") val info = manager?.connectionInfo return if (info == null) { @@ -257,10 +261,11 @@ class SnowpiercerSyncRepository @Inject constructor( val isNewDaily = currentDayDiscipline != today || dailyDisciplines == -1 val currentDailyHour = calendar.get(Calendar.HOUR_OF_DAY) - val (actualDailyCount, nextHour) = if (isNewDaily) + val (actualDailyCount, nextHour) = if (isNewDaily) { 0 to -1 - else + } else { currentDaily to if (lastDailyHour < 8) 10 else lastDailyHour + 4 + } val execute = ((actualDailyCount < dailyDisciplines) || (dailyDisciplines == -1)) && (currentDailyHour >= nextHour) diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/SyncFrequencyRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/SyncFrequencyRepository.kt index 603d39516..8ea6ae61e 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/SyncFrequencyRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/SyncFrequencyRepository.kt @@ -1,63 +1,65 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.repository - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import com.forcetower.uefs.core.model.service.SyncFrequency -import com.google.firebase.firestore.CollectionReference -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Named -import javax.inject.Singleton - -@Singleton -class SyncFrequencyRepository @Inject constructor( - @Named(SyncFrequency.COLLECTION) - private val collection: CollectionReference -) { - - fun getFrequencies(): LiveData> { - val result = MutableLiveData>() - collection.addSnapshotListener { snapshot, exception -> - when { - snapshot != null -> { - val frequencies = snapshot.documents - .map { it.toObject(SyncFrequency::class.java)!! } - .sortedBy { it.value } - .toMutableList() - if (frequencies.isEmpty()) { frequencies += SyncFrequency() } - result.postValue(frequencies) - } - exception != null -> { - Timber.d("Exception: ${exception.message}") - Timber.e(exception) - result.postValue(listOf(SyncFrequency())) - } - else -> { - Timber.e("Something really odd happened") - result.postValue(listOf(SyncFrequency())) - } - } - } - return result - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.repository + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.forcetower.uefs.core.model.service.SyncFrequency +import com.google.firebase.firestore.CollectionReference +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton +import timber.log.Timber + +@Singleton +class SyncFrequencyRepository @Inject constructor( + @Named(SyncFrequency.COLLECTION) + private val collection: CollectionReference +) { + + fun getFrequencies(): LiveData> { + val result = MutableLiveData>() + collection.addSnapshotListener { snapshot, exception -> + when { + snapshot != null -> { + val frequencies = snapshot.documents + .map { it.toObject(SyncFrequency::class.java)!! } + .sortedBy { it.value } + .toMutableList() + if (frequencies.isEmpty()) { + frequencies += SyncFrequency() + } + result.postValue(frequencies) + } + exception != null -> { + Timber.d("Exception: ${exception.message}") + Timber.e(exception) + result.postValue(listOf(SyncFrequency())) + } + else -> { + Timber.e("Something really odd happened") + result.postValue(listOf(SyncFrequency())) + } + } + } + return result + } +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/SyncRegistryRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/SyncRegistryRepository.kt index ecf2379d1..9d8a3363a 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/SyncRegistryRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/SyncRegistryRepository.kt @@ -25,9 +25,9 @@ import androidx.paging.PagingConfig import androidx.paging.PagingData import com.forcetower.uefs.core.model.unes.SyncRegistry import com.forcetower.uefs.core.storage.database.UDatabase -import kotlinx.coroutines.flow.Flow import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.flow.Flow @Singleton class SyncRegistryRepository @Inject constructor( diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/UpgradeRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/UpgradeRepository.kt index ba2fc0fdb..292390727 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/UpgradeRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/UpgradeRepository.kt @@ -25,9 +25,9 @@ import com.forcetower.uefs.core.storage.database.UDatabase import com.forcetower.uefs.core.storage.network.UService import com.google.android.gms.tasks.Tasks import com.google.firebase.messaging.FirebaseMessaging -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +import timber.log.Timber @Singleton class UpgradeRepository @Inject constructor( diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/UserSessionRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/UserSessionRepository.kt index 9de55ca8b..6792b4f35 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/UserSessionRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/UserSessionRepository.kt @@ -1,142 +1,142 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.repository - -import androidx.annotation.AnyThread -import androidx.annotation.WorkerThread -import com.forcetower.uefs.core.model.service.UserSessionDTO -import com.forcetower.uefs.core.model.unes.UserSession -import com.forcetower.uefs.core.storage.database.UDatabase -import com.forcetower.uefs.core.storage.network.UService -import com.forcetower.uefs.core.storage.repository.cloud.AuthRepository -import timber.log.Timber -import java.util.Calendar -import java.util.UUID -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class UserSessionRepository @Inject constructor( - private val database: UDatabase, - private val service: UService, -// private val executors: AppExecutors, -// private val preferences: SharedPreferences, - private val authRepository: AuthRepository -) { - - @WorkerThread - fun onSessionStarted(): UserSession { - val uuid = UUID.randomUUID().toString() - val now = Calendar.getInstance().timeInMillis - val session = UserSession(uuid, now) - database.userSessionDao().insert(session) - return session - } - - @WorkerThread - fun onUserInteraction() { - val now = Calendar.getInstance().timeInMillis - val session = database.userSessionDao().getLatestSession() ?: onSessionStarted() - val lastInteraction = session.lastInteraction ?: 0 - val difference = now - lastInteraction - if (difference >= 45000 && session.lastInteraction != null) { - onSessionStarted() - } else { - database.userSessionDao().updateLastInteraction(session.uid, now) - } - } - - @WorkerThread - fun onUserClickedAd() { - val session = database.userSessionDao().getLatestSession() ?: onSessionStarted() - database.userSessionDao().updateClickedAd(session.uid, 1) - } - - @WorkerThread - fun onUserAdImpression() { - val session = database.userSessionDao().getLatestSession() ?: onSessionStarted() - database.userSessionDao().updateAdImpression(session.uid, 1) - } - - @WorkerThread - fun syncSessions() { - Timber.d("Started sync session...") - val sessions = database.userSessionDao().getUnsyncedSessions() - if (sessions.isEmpty()) { - Timber.d("All sessions in sync...") - return - } - - Timber.d("Session sync will be performed") - - val start = sessions.map { it.started }.minOrNull() ?: 0 - val end = sessions.map { it.started }.maxOrNull() ?: 0 - val dto = UserSessionDTO(start, end, sessions) - try { - val response = service.saveSessions(dto).execute() - if (response.isSuccessful) { - sessions.forEach { database.userSessionDao().markSyncedSession(it.uid) } - Timber.d("Sessions sync completed") - database.userSessionDao().removeSyncedSessions() - } else { - Timber.d("Response failed with ${response.code()}") - // User is not authorized... - // Reconnect... - if (response.code() == 401) { - Timber.d("User needs to reconnect...") - database.accessTokenDao().deleteAll() - authRepository.performAccountSyncState() - } - } - } catch (error: Throwable) { - Timber.e(error, "It seems that the sync failed") - } - } - - @AnyThread - fun onUserInteractionAsync() { -// if (!preferences.isStudentFromUEFS()) return -// executors.diskIO().execute { onUserInteraction() } - } - - @AnyThread - fun onSessionStartedAsync() { -// if (!preferences.isStudentFromUEFS()) return -// executors.diskIO().execute { onSessionStarted() } - } - - @AnyThread - fun onUserClickedAdAsync() { -// if (!preferences.isStudentFromUEFS()) return -// executors.diskIO().execute { onUserClickedAd() } - } - - @AnyThread - fun onUserAdImpressionAsync() { -// if (!preferences.isStudentFromUEFS()) return -// executors.diskIO().execute { onUserAdImpression() } - } - - @AnyThread - fun onSyncSessionsAsync() { - // executors.networkIO().execute { syncSessions() } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.repository + +import androidx.annotation.AnyThread +import androidx.annotation.WorkerThread +import com.forcetower.uefs.core.model.service.UserSessionDTO +import com.forcetower.uefs.core.model.unes.UserSession +import com.forcetower.uefs.core.storage.database.UDatabase +import com.forcetower.uefs.core.storage.network.UService +import com.forcetower.uefs.core.storage.repository.cloud.AuthRepository +import java.util.Calendar +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton +import timber.log.Timber + +@Singleton +class UserSessionRepository @Inject constructor( + private val database: UDatabase, + private val service: UService, +// private val executors: AppExecutors, +// private val preferences: SharedPreferences, + private val authRepository: AuthRepository +) { + + @WorkerThread + fun onSessionStarted(): UserSession { + val uuid = UUID.randomUUID().toString() + val now = Calendar.getInstance().timeInMillis + val session = UserSession(uuid, now) + database.userSessionDao().insert(session) + return session + } + + @WorkerThread + fun onUserInteraction() { + val now = Calendar.getInstance().timeInMillis + val session = database.userSessionDao().getLatestSession() ?: onSessionStarted() + val lastInteraction = session.lastInteraction ?: 0 + val difference = now - lastInteraction + if (difference >= 45000 && session.lastInteraction != null) { + onSessionStarted() + } else { + database.userSessionDao().updateLastInteraction(session.uid, now) + } + } + + @WorkerThread + fun onUserClickedAd() { + val session = database.userSessionDao().getLatestSession() ?: onSessionStarted() + database.userSessionDao().updateClickedAd(session.uid, 1) + } + + @WorkerThread + fun onUserAdImpression() { + val session = database.userSessionDao().getLatestSession() ?: onSessionStarted() + database.userSessionDao().updateAdImpression(session.uid, 1) + } + + @WorkerThread + fun syncSessions() { + Timber.d("Started sync session...") + val sessions = database.userSessionDao().getUnsyncedSessions() + if (sessions.isEmpty()) { + Timber.d("All sessions in sync...") + return + } + + Timber.d("Session sync will be performed") + + val start = sessions.map { it.started }.minOrNull() ?: 0 + val end = sessions.map { it.started }.maxOrNull() ?: 0 + val dto = UserSessionDTO(start, end, sessions) + try { + val response = service.saveSessions(dto).execute() + if (response.isSuccessful) { + sessions.forEach { database.userSessionDao().markSyncedSession(it.uid) } + Timber.d("Sessions sync completed") + database.userSessionDao().removeSyncedSessions() + } else { + Timber.d("Response failed with ${response.code()}") + // User is not authorized... + // Reconnect... + if (response.code() == 401) { + Timber.d("User needs to reconnect...") + database.accessTokenDao().deleteAll() + authRepository.performAccountSyncState() + } + } + } catch (error: Throwable) { + Timber.e(error, "It seems that the sync failed") + } + } + + @AnyThread + fun onUserInteractionAsync() { +// if (!preferences.isStudentFromUEFS()) return +// executors.diskIO().execute { onUserInteraction() } + } + + @AnyThread + fun onSessionStartedAsync() { +// if (!preferences.isStudentFromUEFS()) return +// executors.diskIO().execute { onSessionStarted() } + } + + @AnyThread + fun onUserClickedAdAsync() { +// if (!preferences.isStudentFromUEFS()) return +// executors.diskIO().execute { onUserClickedAd() } + } + + @AnyThread + fun onUserAdImpressionAsync() { +// if (!preferences.isStudentFromUEFS()) return +// executors.diskIO().execute { onUserAdImpression() } + } + + @AnyThread + fun onSyncSessionsAsync() { + // executors.networkIO().execute { syncSessions() } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/cloud/AffinityQuestionRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/cloud/AffinityQuestionRepository.kt index 6f31226b2..a8fef2198 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/cloud/AffinityQuestionRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/cloud/AffinityQuestionRepository.kt @@ -26,9 +26,9 @@ import com.forcetower.uefs.AppExecutors import com.forcetower.uefs.core.model.service.AffinityQuestionAnswer import com.forcetower.uefs.core.storage.database.UDatabase import com.forcetower.uefs.core.storage.network.UService -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +import timber.log.Timber @Singleton class AffinityQuestionRepository @Inject constructor( diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/cloud/AuthRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/cloud/AuthRepository.kt index 52a394eed..3bfc303e9 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/cloud/AuthRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/cloud/AuthRepository.kt @@ -35,12 +35,12 @@ import com.forcetower.uefs.core.storage.network.adapter.ApiResponse import com.forcetower.uefs.core.storage.network.adapter.asLiveData import com.forcetower.uefs.core.storage.resource.NetworkBoundResource import com.forcetower.uefs.core.storage.resource.Resource +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import retrofit2.Response import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton @Singleton class AuthRepository @Inject constructor( @@ -64,7 +64,9 @@ class AuthRepository @Inject constructor( else -> service.loginWithSagres(username, password).asLiveData() } } - override fun saveCallResult(value: AccessToken) { database.accessTokenDao().insert(value) } + override fun saveCallResult(value: AccessToken) { + database.accessTokenDao().insert(value) + } }.asLiveData() } @@ -94,8 +96,11 @@ class AuthRepository @Inject constructor( fun performAccountSyncStateIfNeededAsync() { executors.networkIO().execute { val tk = database.accessTokenDao().getAccessTokenDirect() - if (tk == null) performAccountSyncState() - else updateAccount() + if (tk == null) { + performAccountSyncState() + } else { + updateAccount() + } } } diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/cloud/EdgeAccountRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/cloud/EdgeAccountRepository.kt index b7b216903..02b181f3f 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/cloud/EdgeAccountRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/cloud/EdgeAccountRepository.kt @@ -5,8 +5,8 @@ import com.forcetower.uefs.core.model.unes.EdgeServiceAccount import com.forcetower.uefs.core.storage.database.UDatabase import com.forcetower.uefs.core.storage.network.EdgeService import dagger.Reusable -import timber.log.Timber import javax.inject.Inject +import timber.log.Timber @Reusable class EdgeAccountRepository @Inject constructor( @@ -34,4 +34,4 @@ class EdgeAccountRepository @Inject constructor( service.uploadPicture(ChangePictureDTO(base64)) runCatching { fetchAccountIfNeeded() } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/cloud/EdgeAuthRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/cloud/EdgeAuthRepository.kt index b142b4ef9..c29d2fe9e 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/cloud/EdgeAuthRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/cloud/EdgeAuthRepository.kt @@ -13,9 +13,9 @@ import com.forcetower.uefs.core.model.unes.EdgeAccessToken import com.forcetower.uefs.core.model.unes.EdgeServiceAccount import com.forcetower.uefs.core.storage.database.UDatabase import com.forcetower.uefs.core.storage.network.EdgeService -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +import timber.log.Timber @Singleton class EdgeAuthRepository @Inject constructor( @@ -64,8 +64,9 @@ class EdgeAuthRepository @Inject constructor( try { val response = service.linkEmailStart(EmailLinkBodyDTO(email)) val body = response.body() - if (body != null && response.isSuccessful) + if (body != null && response.isSuccessful) { return EmailLinkStart.CodeSent(body.data.securityToken) + } return EmailLinkStart.InvalidInfo } catch (error: Throwable) { diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/repository/cloud/EdgeSyncRepository.kt b/app/src/main/java/com/forcetower/uefs/core/storage/repository/cloud/EdgeSyncRepository.kt index b83199b9e..9b929516a 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/repository/cloud/EdgeSyncRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/repository/cloud/EdgeSyncRepository.kt @@ -9,10 +9,10 @@ import com.forcetower.uefs.core.storage.database.UDatabase import com.forcetower.uefs.core.storage.network.EdgeService import com.forcetower.uefs.feature.shared.extensions.toTitleCase import dagger.Reusable +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber -import javax.inject.Inject @Reusable class EdgeSyncRepository @Inject constructor( @@ -87,4 +87,4 @@ class EdgeSyncRepository @Inject constructor( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/resource/NetworkBoundResource.kt b/app/src/main/java/com/forcetower/uefs/core/storage/resource/NetworkBoundResource.kt index 86f503bcd..62bed51d2 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/resource/NetworkBoundResource.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/resource/NetworkBoundResource.kt @@ -106,12 +106,16 @@ abstract class NetworkBoundResource @MainThread abstract fun loadFromDb(): LiveData + @MainThread abstract fun shouldFetch(it: ResultType?): Boolean + @MainThread abstract fun createCall(): LiveData> + @WorkerThread abstract fun saveCallResult(value: RequestType) + @WorkerThread open fun onErrorCallback() = Unit } diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/resource/NetworkOnlyResource.kt b/app/src/main/java/com/forcetower/uefs/core/storage/resource/NetworkOnlyResource.kt index d31f64318..ba9d7f319 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/resource/NetworkOnlyResource.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/resource/NetworkOnlyResource.kt @@ -79,6 +79,7 @@ abstract class NetworkOnlyResource @MainThread abstract fun createCall(): LiveData> + @WorkerThread abstract fun saveCallResult(value: RequestType) } diff --git a/app/src/main/java/com/forcetower/uefs/core/storage/resource/discipline/LoadDisciplineDetailsResource.kt b/app/src/main/java/com/forcetower/uefs/core/storage/resource/discipline/LoadDisciplineDetailsResource.kt index 8227c09c5..06245b20c 100644 --- a/app/src/main/java/com/forcetower/uefs/core/storage/resource/discipline/LoadDisciplineDetailsResource.kt +++ b/app/src/main/java/com/forcetower/uefs/core/storage/resource/discipline/LoadDisciplineDetailsResource.kt @@ -1,132 +1,133 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.storage.resource.discipline - -import androidx.annotation.MainThread -import androidx.annotation.WorkerThread -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.liveData -import com.forcetower.sagres.SagresNavigator -import com.forcetower.sagres.operation.Status -import com.forcetower.sagres.operation.disciplines.FastDisciplinesCallback -import com.forcetower.uefs.AppExecutors -import com.forcetower.uefs.core.storage.database.UDatabase -import com.forcetower.uefs.core.util.toLiveData -import dev.forcetower.breaker.Orchestra -import dev.forcetower.breaker.model.Authorization -import kotlinx.coroutines.delay -import okhttp3.OkHttpClient -import timber.log.Timber - -abstract class LoadDisciplineDetailsResource @MainThread constructor( - private val executors: AppExecutors, - private val database: UDatabase, - private val semester: String? = null, - private val code: String? = null, - private val group: String? = null, - private val partialLoad: Boolean = false, - private val discover: Boolean = false, - private val snowpiercer: Boolean, - userAgent: String, - client: OkHttpClient -) { - private val result = MediatorLiveData() - private val orchestra = Orchestra.Builder() - .client(client) - .userAgent(userAgent) - .build() - - init { - if (!snowpiercer) - loginToSagres() - else - syncAllSnowpiercer() - } - - private fun syncAllSnowpiercer() { - val data = liveData { - val access = database.accessDao().getAccessDirectSuspend() - if (access == null || !access.valid) { - emit(FastDisciplinesCallback(Status.INVALID_LOGIN).flags(FastDisciplinesCallback.LOGIN)) - return@liveData - } - orchestra.setAuthorization(Authorization(access.username, access.password)) - emit(FastDisciplinesCallback(Status.STARTED).flags(FastDisciplinesCallback.INITIAL)) - delay(4000) - emit(FastDisciplinesCallback(Status.LOADING).flags(FastDisciplinesCallback.SAVING)) - } - - result.addSource(data) { - result.value = it - } - } - - @MainThread - private fun loginToSagres() { - val access = database.accessDao().getAccess() - result.addSource(access) { data -> - result.removeSource(access) - if (data == null) { - result.value = FastDisciplinesCallback(Status.INVALID_LOGIN).flags(FastDisciplinesCallback.LOGIN) - } else { - val login = SagresNavigator.instance.aLogin(data.username, data.password).toLiveData() - result.addSource(login) { - result.removeSource(login) - loadFromSagres() - } - } - } - } - - @MainThread - private fun loadFromSagres() { - val loader = SagresNavigator.instance.aDisciplinesExperimental(semester, code, group, partialLoad, discover).toLiveData() - result.addSource(loader) { callback -> - Timber.d("Current Status: ${callback.status}") - when (callback.status) { - Status.COMPLETED -> { - result.removeSource(loader) - startSaveResults(callback) - } - else -> result.value = callback - } - } - } - - @MainThread - private fun startSaveResults(callback: FastDisciplinesCallback) { - result.postValue(FastDisciplinesCallback(Status.LOADING).flags(FastDisciplinesCallback.SAVING)) - executors.diskIO().execute { - saveResults(callback) - result.postValue(FastDisciplinesCallback(Status.LOADING).flags(FastDisciplinesCallback.GRADES)) - loadGrades() - result.postValue(callback) - } - } - - @WorkerThread - abstract fun loadGrades() - - @WorkerThread - abstract fun saveResults(callback: FastDisciplinesCallback) - - fun asLiveData() = result -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.storage.resource.discipline + +import androidx.annotation.MainThread +import androidx.annotation.WorkerThread +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.liveData +import com.forcetower.sagres.SagresNavigator +import com.forcetower.sagres.operation.Status +import com.forcetower.sagres.operation.disciplines.FastDisciplinesCallback +import com.forcetower.uefs.AppExecutors +import com.forcetower.uefs.core.storage.database.UDatabase +import com.forcetower.uefs.core.util.toLiveData +import dev.forcetower.breaker.Orchestra +import dev.forcetower.breaker.model.Authorization +import kotlinx.coroutines.delay +import okhttp3.OkHttpClient +import timber.log.Timber + +abstract class LoadDisciplineDetailsResource @MainThread constructor( + private val executors: AppExecutors, + private val database: UDatabase, + private val semester: String? = null, + private val code: String? = null, + private val group: String? = null, + private val partialLoad: Boolean = false, + private val discover: Boolean = false, + private val snowpiercer: Boolean, + userAgent: String, + client: OkHttpClient +) { + private val result = MediatorLiveData() + private val orchestra = Orchestra.Builder() + .client(client) + .userAgent(userAgent) + .build() + + init { + if (!snowpiercer) { + loginToSagres() + } else { + syncAllSnowpiercer() + } + } + + private fun syncAllSnowpiercer() { + val data = liveData { + val access = database.accessDao().getAccessDirectSuspend() + if (access == null || !access.valid) { + emit(FastDisciplinesCallback(Status.INVALID_LOGIN).flags(FastDisciplinesCallback.LOGIN)) + return@liveData + } + orchestra.setAuthorization(Authorization(access.username, access.password)) + emit(FastDisciplinesCallback(Status.STARTED).flags(FastDisciplinesCallback.INITIAL)) + delay(4000) + emit(FastDisciplinesCallback(Status.LOADING).flags(FastDisciplinesCallback.SAVING)) + } + + result.addSource(data) { + result.value = it + } + } + + @MainThread + private fun loginToSagres() { + val access = database.accessDao().getAccess() + result.addSource(access) { data -> + result.removeSource(access) + if (data == null) { + result.value = FastDisciplinesCallback(Status.INVALID_LOGIN).flags(FastDisciplinesCallback.LOGIN) + } else { + val login = SagresNavigator.instance.aLogin(data.username, data.password).toLiveData() + result.addSource(login) { + result.removeSource(login) + loadFromSagres() + } + } + } + } + + @MainThread + private fun loadFromSagres() { + val loader = SagresNavigator.instance.aDisciplinesExperimental(semester, code, group, partialLoad, discover).toLiveData() + result.addSource(loader) { callback -> + Timber.d("Current Status: ${callback.status}") + when (callback.status) { + Status.COMPLETED -> { + result.removeSource(loader) + startSaveResults(callback) + } + else -> result.value = callback + } + } + } + + @MainThread + private fun startSaveResults(callback: FastDisciplinesCallback) { + result.postValue(FastDisciplinesCallback(Status.LOADING).flags(FastDisciplinesCallback.SAVING)) + executors.diskIO().execute { + saveResults(callback) + result.postValue(FastDisciplinesCallback(Status.LOADING).flags(FastDisciplinesCallback.GRADES)) + loadGrades() + result.postValue(callback) + } + } + + @WorkerThread + abstract fun loadGrades() + + @WorkerThread + abstract fun saveResults(callback: FastDisciplinesCallback) + + fun asLiveData() = result +} diff --git a/app/src/main/java/com/forcetower/uefs/core/task/FetchMissingSemestersUseCase.kt b/app/src/main/java/com/forcetower/uefs/core/task/FetchMissingSemestersUseCase.kt index 556213620..56880f844 100644 --- a/app/src/main/java/com/forcetower/uefs/core/task/FetchMissingSemestersUseCase.kt +++ b/app/src/main/java/com/forcetower/uefs/core/task/FetchMissingSemestersUseCase.kt @@ -12,12 +12,12 @@ import dagger.hilt.android.qualifiers.ApplicationContext import dev.forcetower.breaker.Orchestra import dev.forcetower.breaker.model.Authorization import dev.forcetower.breaker.model.Semester +import javax.inject.Inject +import javax.inject.Named import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull import okhttp3.OkHttpClient import timber.log.Timber -import javax.inject.Inject -import javax.inject.Named class FetchMissingSemestersUseCase @Inject constructor( @ApplicationContext private val context: Context, @@ -79,7 +79,7 @@ class FetchMissingSemestersUseCase @Inject constructor( Semester(1000000439, "20131", "2013.1", "2013-03-11T00:00:00-03:00", "2013-08-15T00:00:00-03:00"), Semester(1000000403, "20122", "2012.2", "2012-09-05T00:00:00-03:00", "2013-01-23T00:00:00-02:00"), Semester(1000000372, "20121", "2012.1", "2012-04-09T00:00:00-03:00", "2012-08-27T00:00:00-03:00"), - Semester(1000000340, "20112", "2011.2", "2011-09-29T00:00:00-03:00", "2012-03-19T00:00:00-03:00"), + Semester(1000000340, "20112", "2011.2", "2011-09-29T00:00:00-03:00", "2012-03-19T00:00:00-03:00") ) } } diff --git a/app/src/main/java/com/forcetower/uefs/core/task/definers/DisciplinesProcessor.kt b/app/src/main/java/com/forcetower/uefs/core/task/definers/DisciplinesProcessor.kt index 12c409777..01febf05f 100644 --- a/app/src/main/java/com/forcetower/uefs/core/task/definers/DisciplinesProcessor.kt +++ b/app/src/main/java/com/forcetower/uefs/core/task/definers/DisciplinesProcessor.kt @@ -37,8 +37,8 @@ import com.forcetower.uefs.feature.shared.extensions.toWeekDay import com.forcetower.uefs.service.NotificationCreator import dev.forcetower.breaker.model.DisciplineData import dev.forcetower.breaker.model.Person -import timber.log.Timber import java.util.UUID +import timber.log.Timber class DisciplinesProcessor( private val context: Context, @@ -85,8 +85,9 @@ class DisciplinesProcessor( ) val groupId = database.classGroupDao().insertNewWay(group) - if (clazz.teachers.isNotEmpty()) + if (clazz.teachers.isNotEmpty()) { database.classGroupTeacher().deleteAllFromClassGroup(groupId) + } clazz.teachers.forEach { teacher -> val id = insertTeacher(teacher, it.department) diff --git a/app/src/main/java/com/forcetower/uefs/core/task/usecase/message/FetchAllMessagesSnowpiercerUseCase.kt b/app/src/main/java/com/forcetower/uefs/core/task/usecase/message/FetchAllMessagesSnowpiercerUseCase.kt index 72e7f030a..5ed9891ff 100644 --- a/app/src/main/java/com/forcetower/uefs/core/task/usecase/message/FetchAllMessagesSnowpiercerUseCase.kt +++ b/app/src/main/java/com/forcetower/uefs/core/task/usecase/message/FetchAllMessagesSnowpiercerUseCase.kt @@ -26,11 +26,11 @@ import com.forcetower.uefs.core.task.UseCase import dev.forcetower.breaker.Orchestra import dev.forcetower.breaker.model.Authorization import dev.forcetower.breaker.result.Outcome +import javax.inject.Inject +import javax.inject.Named import kotlinx.coroutines.Dispatchers import okhttp3.OkHttpClient import timber.log.Timber -import javax.inject.Inject -import javax.inject.Named class FetchAllMessagesSnowpiercerUseCase @Inject constructor( private val database: UDatabase, diff --git a/app/src/main/java/com/forcetower/uefs/core/util/ImgurUploader.kt b/app/src/main/java/com/forcetower/uefs/core/util/ImgurUploader.kt index da7b4f161..4e3eb360f 100644 --- a/app/src/main/java/com/forcetower/uefs/core/util/ImgurUploader.kt +++ b/app/src/main/java/com/forcetower/uefs/core/util/ImgurUploader.kt @@ -24,11 +24,11 @@ import androidx.annotation.WorkerThread import com.forcetower.uefs.core.model.api.ImgurUpload import com.forcetower.uefs.core.model.api.UploadResponse import com.google.gson.Gson +import java.io.IOException import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.Request import timber.log.Timber -import java.io.IOException object ImgurUploader { @WorkerThread diff --git a/app/src/main/java/com/forcetower/uefs/core/util/LinkTouchMovementMethod.kt b/app/src/main/java/com/forcetower/uefs/core/util/LinkTouchMovementMethod.kt index 826de6254..8b45c7e5c 100644 --- a/app/src/main/java/com/forcetower/uefs/core/util/LinkTouchMovementMethod.kt +++ b/app/src/main/java/com/forcetower/uefs/core/util/LinkTouchMovementMethod.kt @@ -20,13 +20,13 @@ package com.forcetower.uefs.core.util -import `in`.uncod.android.bypass.style.TouchableUrlSpan import android.text.Selection import android.text.Spannable import android.text.method.LinkMovementMethod import android.text.method.MovementMethod import android.view.MotionEvent import android.widget.TextView +import `in`.uncod.android.bypass.style.TouchableUrlSpan class LinkTouchMovementMethod : LinkMovementMethod() { private var pressedSpan: TouchableUrlSpan? = null @@ -64,7 +64,6 @@ class LinkTouchMovementMethod : LinkMovementMethod() { } private fun getPressedSpan(textView: TextView, spannable: Spannable, event: MotionEvent): TouchableUrlSpan? { - var x = event.x.toInt() var y = event.y.toInt() @@ -91,8 +90,9 @@ class LinkTouchMovementMethod : LinkMovementMethod() { private var instance: LinkTouchMovementMethod? = null fun getInstance(): MovementMethod { - if (instance == null) + if (instance == null) { instance = LinkTouchMovementMethod() + } return instance!! } diff --git a/app/src/main/java/com/forcetower/uefs/core/vm/BillingViewModel.kt b/app/src/main/java/com/forcetower/uefs/core/vm/BillingViewModel.kt index df0e4fa9f..55229fe48 100644 --- a/app/src/main/java/com/forcetower/uefs/core/vm/BillingViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/core/vm/BillingViewModel.kt @@ -1,241 +1,241 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.vm - -import android.app.Activity -import android.content.Context -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.switchMap -import androidx.lifecycle.viewModelScope -import com.android.billingclient.api.BillingClient -import com.android.billingclient.api.BillingClientStateListener -import com.android.billingclient.api.BillingFlowParams -import com.android.billingclient.api.BillingResult -import com.android.billingclient.api.ConsumeParams -import com.android.billingclient.api.ProductDetails -import com.android.billingclient.api.Purchase -import com.android.billingclient.api.PurchasesUpdatedListener -import com.android.billingclient.api.QueryProductDetailsParams -import com.android.billingclient.api.QueryPurchasesParams -import com.forcetower.core.lifecycle.Event -import com.forcetower.uefs.R -import com.forcetower.uefs.core.billing.SkuDetailsResult -import com.forcetower.uefs.core.storage.repository.BillingRepository -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import timber.log.Timber -import javax.inject.Inject -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException - -@HiltViewModel -class BillingViewModel @Inject constructor( - context: Context, - private val repository: BillingRepository -) : ViewModel(), PurchasesUpdatedListener, BillingClientStateListener { - private val _selectSku = MutableLiveData>() - val selectSku: LiveData> - get() = _selectSku - - private val _snack = MutableLiveData>() - val snack: LiveData> - get() = _snack - - private val billingClient = BillingClient.newBuilder(context.applicationContext) - .enablePendingPurchases() - .setListener(this) - .build() - - init { - if (!billingClient.isReady) { - billingClient.startConnection(this) - } - } - - private suspend fun queryGoldMonkey(): Boolean { - try { - val purchases = billingClient.suspendQueryPurchases(BillingClient.ProductType.SUBS) - if (purchases.isEmpty()) { - repository.cancelSubscriptions() - } else { - repository.handlePurchases(purchases) - } - } catch (error: Throwable) { - Timber.i(error, "Error during purchase update") - } - return repository.isGoldMonkey() - } - - val currentUsername: LiveData - get() = repository.getUsername() - - fun getSkus() = repository.getManagedSkus() - - val subscriptions = getSkus().switchMap { skuList -> getSubscriptions(skuList) } - val inAppProducts = getSkus().switchMap { skuList -> getInAppProducts(skuList) } - - fun selectSku(skuDetails: ProductDetails?) { - skuDetails ?: return - _selectSku.value = Event(skuDetails) - } - - private fun getSubscriptions(list: List): LiveData { - val subscriptions = MutableLiveData() - val products = list.map { - QueryProductDetailsParams.Product.newBuilder() - .setProductType(BillingClient.ProductType.SUBS) - .setProductId(it) - .build() - } - - val request = QueryProductDetailsParams.newBuilder() - .setProductList(products) - .build() - - billingClient.queryProductDetailsAsync(request) { response, details -> - subscriptions.value = SkuDetailsResult(response.responseCode, details) - } - return subscriptions - } - - private fun getInAppProducts(list: List): LiveData { - val inAppProducts = MutableLiveData() - val products = list.map { - QueryProductDetailsParams.Product.newBuilder() - .setProductType(BillingClient.ProductType.INAPP) - .setProductId(it) - .build() - } - - val request = QueryProductDetailsParams.newBuilder() - .setProductList(products) - .build() - - billingClient.queryProductDetailsAsync(request) { response, details -> - inAppProducts.value = SkuDetailsResult(response.responseCode, details) - } - return inAppProducts - } - - override fun onPurchasesUpdated(result: BillingResult, purchases: List?) { - when (result.responseCode) { - BillingClient.BillingResponseCode.OK -> { - if (purchases != null) { - handlePurchases(purchases) - } - } - BillingClient.BillingResponseCode.USER_CANCELED -> { - Timber.d("User canceled purchase") - } - BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> { - _snack.postValue(Event(R.string.purchase_item_already_owned)) - } - else -> { - _snack.postValue(Event(R.string.purchase_update_error)) - Timber.e("Error purchase: ${result.responseCode}") - } - } - } - - fun launchBillingFlow(activity: Activity, details: ProductDetails, username: String) { - val productParams = BillingFlowParams.ProductDetailsParams.newBuilder() - .setProductDetails(details) - .build() - - val params = BillingFlowParams.newBuilder() - .setProductDetailsParamsList(listOf(productParams)) - .setObfuscatedAccountId(username) - .build() - - billingClient.launchBillingFlow(activity, params) - } - - private suspend fun updatePurchases() { - val result = try { - billingClient.suspendQueryPurchases(BillingClient.ProductType.INAPP) - } catch (error: Throwable) { - null - } - - if (result == null) { - Timber.d("Update purchase: Null purchase list") - } else { - handlePurchases(result) - } - } - - private fun handlePurchases(purchases: List) { - repository.handlePurchases(purchases) - purchases.forEach { - if (!it.isAutoRenewing) { - consume(it) - } else { - _snack.postValue(Event(R.string.purchase_subscription_started)) - } - } - } - - override fun onBillingServiceDisconnected() = Unit - - override fun onBillingSetupFinished(result: BillingResult) { - if (result.responseCode == BillingClient.BillingResponseCode.OK) { - viewModelScope.launch { - updatePurchases() - } - } - } - - override fun onCleared() { - super.onCleared() - billingClient.endConnection() - } - - private fun consume(purchase: Purchase) { - val params = ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build() - billingClient.consumeAsync(params) { response, token -> - Timber.d("Attempt to consume $token finished with code ${response.responseCode}") - if (response.responseCode == BillingClient.BillingResponseCode.OK) { - _snack.postValue(Event(R.string.purchase_you_bought_a_consumable_item)) - } else { - Timber.e("Failed to consume ${purchase.products}") - } - } - } - - private suspend fun BillingClient.suspendQueryPurchases( - @BillingClient.ProductType type: String - ) = suspendCancellableCoroutine> { continuation -> - val params = QueryPurchasesParams.newBuilder() - .setProductType(type) - .build() - - queryPurchasesAsync(params) { result, purchases -> - if (result.responseCode == BillingClient.BillingResponseCode.OK) { - continuation.resume(purchases) - } else { - continuation.resumeWithException(IllegalStateException("Response code was ${result.responseCode}")) - } - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.vm + +import android.app.Activity +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.switchMap +import androidx.lifecycle.viewModelScope +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.BillingClientStateListener +import com.android.billingclient.api.BillingFlowParams +import com.android.billingclient.api.BillingResult +import com.android.billingclient.api.ConsumeParams +import com.android.billingclient.api.ProductDetails +import com.android.billingclient.api.Purchase +import com.android.billingclient.api.PurchasesUpdatedListener +import com.android.billingclient.api.QueryProductDetailsParams +import com.android.billingclient.api.QueryPurchasesParams +import com.forcetower.core.lifecycle.Event +import com.forcetower.uefs.R +import com.forcetower.uefs.core.billing.SkuDetailsResult +import com.forcetower.uefs.core.storage.repository.BillingRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import timber.log.Timber + +@HiltViewModel +class BillingViewModel @Inject constructor( + context: Context, + private val repository: BillingRepository +) : ViewModel(), PurchasesUpdatedListener, BillingClientStateListener { + private val _selectSku = MutableLiveData>() + val selectSku: LiveData> + get() = _selectSku + + private val _snack = MutableLiveData>() + val snack: LiveData> + get() = _snack + + private val billingClient = BillingClient.newBuilder(context.applicationContext) + .enablePendingPurchases() + .setListener(this) + .build() + + init { + if (!billingClient.isReady) { + billingClient.startConnection(this) + } + } + + private suspend fun queryGoldMonkey(): Boolean { + try { + val purchases = billingClient.suspendQueryPurchases(BillingClient.ProductType.SUBS) + if (purchases.isEmpty()) { + repository.cancelSubscriptions() + } else { + repository.handlePurchases(purchases) + } + } catch (error: Throwable) { + Timber.i(error, "Error during purchase update") + } + return repository.isGoldMonkey() + } + + val currentUsername: LiveData + get() = repository.getUsername() + + fun getSkus() = repository.getManagedSkus() + + val subscriptions = getSkus().switchMap { skuList -> getSubscriptions(skuList) } + val inAppProducts = getSkus().switchMap { skuList -> getInAppProducts(skuList) } + + fun selectSku(skuDetails: ProductDetails?) { + skuDetails ?: return + _selectSku.value = Event(skuDetails) + } + + private fun getSubscriptions(list: List): LiveData { + val subscriptions = MutableLiveData() + val products = list.map { + QueryProductDetailsParams.Product.newBuilder() + .setProductType(BillingClient.ProductType.SUBS) + .setProductId(it) + .build() + } + + val request = QueryProductDetailsParams.newBuilder() + .setProductList(products) + .build() + + billingClient.queryProductDetailsAsync(request) { response, details -> + subscriptions.value = SkuDetailsResult(response.responseCode, details) + } + return subscriptions + } + + private fun getInAppProducts(list: List): LiveData { + val inAppProducts = MutableLiveData() + val products = list.map { + QueryProductDetailsParams.Product.newBuilder() + .setProductType(BillingClient.ProductType.INAPP) + .setProductId(it) + .build() + } + + val request = QueryProductDetailsParams.newBuilder() + .setProductList(products) + .build() + + billingClient.queryProductDetailsAsync(request) { response, details -> + inAppProducts.value = SkuDetailsResult(response.responseCode, details) + } + return inAppProducts + } + + override fun onPurchasesUpdated(result: BillingResult, purchases: List?) { + when (result.responseCode) { + BillingClient.BillingResponseCode.OK -> { + if (purchases != null) { + handlePurchases(purchases) + } + } + BillingClient.BillingResponseCode.USER_CANCELED -> { + Timber.d("User canceled purchase") + } + BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> { + _snack.postValue(Event(R.string.purchase_item_already_owned)) + } + else -> { + _snack.postValue(Event(R.string.purchase_update_error)) + Timber.e("Error purchase: ${result.responseCode}") + } + } + } + + fun launchBillingFlow(activity: Activity, details: ProductDetails, username: String) { + val productParams = BillingFlowParams.ProductDetailsParams.newBuilder() + .setProductDetails(details) + .build() + + val params = BillingFlowParams.newBuilder() + .setProductDetailsParamsList(listOf(productParams)) + .setObfuscatedAccountId(username) + .build() + + billingClient.launchBillingFlow(activity, params) + } + + private suspend fun updatePurchases() { + val result = try { + billingClient.suspendQueryPurchases(BillingClient.ProductType.INAPP) + } catch (error: Throwable) { + null + } + + if (result == null) { + Timber.d("Update purchase: Null purchase list") + } else { + handlePurchases(result) + } + } + + private fun handlePurchases(purchases: List) { + repository.handlePurchases(purchases) + purchases.forEach { + if (!it.isAutoRenewing) { + consume(it) + } else { + _snack.postValue(Event(R.string.purchase_subscription_started)) + } + } + } + + override fun onBillingServiceDisconnected() = Unit + + override fun onBillingSetupFinished(result: BillingResult) { + if (result.responseCode == BillingClient.BillingResponseCode.OK) { + viewModelScope.launch { + updatePurchases() + } + } + } + + override fun onCleared() { + super.onCleared() + billingClient.endConnection() + } + + private fun consume(purchase: Purchase) { + val params = ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build() + billingClient.consumeAsync(params) { response, token -> + Timber.d("Attempt to consume $token finished with code ${response.responseCode}") + if (response.responseCode == BillingClient.BillingResponseCode.OK) { + _snack.postValue(Event(R.string.purchase_you_bought_a_consumable_item)) + } else { + Timber.e("Failed to consume ${purchase.products}") + } + } + } + + private suspend fun BillingClient.suspendQueryPurchases( + @BillingClient.ProductType type: String + ) = suspendCancellableCoroutine> { continuation -> + val params = QueryPurchasesParams.newBuilder() + .setProductType(type) + .build() + + queryPurchasesAsync(params) { result, purchases -> + if (result.responseCode == BillingClient.BillingResponseCode.OK) { + continuation.resume(purchases) + } else { + continuation.resumeWithException(IllegalStateException("Response code was ${result.responseCode}")) + } + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/core/vm/CourseViewModel.kt b/app/src/main/java/com/forcetower/uefs/core/vm/CourseViewModel.kt index 7d1635c93..7e82c4250 100644 --- a/app/src/main/java/com/forcetower/uefs/core/vm/CourseViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/core/vm/CourseViewModel.kt @@ -28,10 +28,10 @@ import com.forcetower.uefs.core.task.UCaseResult import com.forcetower.uefs.core.task.data import dagger.hilt.android.lifecycle.HiltViewModel import dev.forcetower.unes.usecases.courses.LoadCoursesUseCase +import javax.inject.Inject import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map -import javax.inject.Inject @HiltViewModel class CourseViewModel @Inject constructor( diff --git a/app/src/main/java/com/forcetower/uefs/core/vm/UnesverseViewModel.kt b/app/src/main/java/com/forcetower/uefs/core/vm/UnesverseViewModel.kt index 36af64c07..71712b9c3 100644 --- a/app/src/main/java/com/forcetower/uefs/core/vm/UnesverseViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/core/vm/UnesverseViewModel.kt @@ -35,8 +35,8 @@ import com.forcetower.uefs.core.storage.repository.cloud.EdgeAuthRepository import com.forcetower.uefs.core.storage.resource.Resource import com.forcetower.uefs.feature.shared.extensions.setValueIfNew import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.launch @HiltViewModel class UnesverseViewModel @Inject constructor( diff --git a/app/src/main/java/com/forcetower/uefs/core/work/WorkExtensions.kt b/app/src/main/java/com/forcetower/uefs/core/work/WorkExtensions.kt index 0b4b7d9e9..062778371 100644 --- a/app/src/main/java/com/forcetower/uefs/core/work/WorkExtensions.kt +++ b/app/src/main/java/com/forcetower/uefs/core/work/WorkExtensions.kt @@ -30,10 +30,11 @@ import androidx.work.WorkManager fun OneTimeWorkRequest.enqueueUnique(context: Context, name: String, replace: Boolean = true) { WorkManager.getInstance(context).beginUniqueWork( name, - if (replace) + if (replace) { ExistingWorkPolicy.REPLACE - else - ExistingWorkPolicy.KEEP, + } else { + ExistingWorkPolicy.KEEP + }, this ).enqueue() } @@ -45,10 +46,11 @@ fun OneTimeWorkRequest.enqueue(context: Context) { fun PeriodicWorkRequest.enqueueUnique(context: Context, name: String, replace: Boolean = true) { WorkManager.getInstance(context).enqueueUniquePeriodicWork( name, - if (replace) + if (replace) { ExistingPeriodicWorkPolicy.UPDATE - else - ExistingPeriodicWorkPolicy.KEEP, + } else { + ExistingPeriodicWorkPolicy.KEEP + }, this ) } diff --git a/app/src/main/java/com/forcetower/uefs/core/work/affinity/AnswerAffinityWorker.kt b/app/src/main/java/com/forcetower/uefs/core/work/affinity/AnswerAffinityWorker.kt index 73852b42a..3a0760014 100644 --- a/app/src/main/java/com/forcetower/uefs/core/work/affinity/AnswerAffinityWorker.kt +++ b/app/src/main/java/com/forcetower/uefs/core/work/affinity/AnswerAffinityWorker.kt @@ -33,8 +33,8 @@ import com.forcetower.uefs.core.storage.repository.cloud.AffinityQuestionReposit import com.forcetower.uefs.core.work.enqueue import dagger.assisted.Assisted import dagger.assisted.AssistedInject -import timber.log.Timber import java.util.concurrent.TimeUnit +import timber.log.Timber @HiltWorker class AnswerAffinityWorker @AssistedInject constructor( diff --git a/app/src/main/java/com/forcetower/uefs/core/work/image/UploadImageToStorage.kt b/app/src/main/java/com/forcetower/uefs/core/work/image/UploadImageToStorage.kt index f1a281cfd..d585b724d 100644 --- a/app/src/main/java/com/forcetower/uefs/core/work/image/UploadImageToStorage.kt +++ b/app/src/main/java/com/forcetower/uefs/core/work/image/UploadImageToStorage.kt @@ -1,125 +1,125 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.work.image - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.media.ThumbnailUtils -import android.net.Uri -import android.util.Base64 -import androidx.annotation.WorkerThread -import androidx.hilt.work.HiltWorker -import androidx.work.Constraints -import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.Worker -import androidx.work.WorkerParameters -import androidx.work.workDataOf -import com.forcetower.uefs.core.storage.database.UDatabase -import com.forcetower.uefs.core.storage.network.UService -import com.forcetower.uefs.core.util.ImgurUploader -import com.forcetower.uefs.core.work.enqueue -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import okhttp3.OkHttpClient -import timber.log.Timber -import java.io.ByteArrayOutputStream -import java.io.InputStream -import java.util.UUID - -@HiltWorker -class UploadImageToStorage @AssistedInject constructor( - @Assisted context: Context, - @Assisted params: WorkerParameters, - private val client: OkHttpClient, - private val database: UDatabase, - private val service: UService -) : Worker(context, params) { - - @SuppressLint("WrongThread") - @WorkerThread - override fun doWork(): Result { - Timber.d("Started picture upload") - val tUri = inputData.getString(URI) ?: return Result.failure() - - val uri = Uri.parse(tUri) - - val resolver = applicationContext.contentResolver - val stream: InputStream - try { - stream = resolver.openInputStream(uri) ?: return Result.failure() - } catch (exception: Throwable) { - return Result.failure() - } - - val image = BitmapFactory.decodeStream(stream) - image ?: return Result.failure() - - val bitmap = ThumbnailUtils.extractThumbnail(image, 1080, 1080) - - val baos = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos) - val data = baos.toByteArray() - - val name = database.accessDao().getAccessDirect()?.username ?: "user-unes-${UUID.randomUUID().toString().substring(0, 4)}" - val encoded = Base64.encodeToString(data, Base64.DEFAULT) - val upload = ImgurUploader.upload(client, encoded, name) ?: return Result.retry() - - return try { - val response = service.updateProfileImage(upload).execute() - if (response.isSuccessful) { - Timber.d("Success setting on account!!") - try { - val acc = service.getAccount().execute() - acc.body()?.run { - database.accountDao().insert(this) - } - } catch (e: Throwable) { } - Result.success() - } else { - Timber.d("Unsucessful response ${response.code()}") - Result.retry() - } - } catch (t: Throwable) { - Timber.e(t, "Error") - Result.retry() - } - } - - companion object { - private const val URI = "image_uri" - private const val TAG = "upload_profile_image" - - fun createWorker(context: Context, uri: Uri) { - val data = workDataOf(URI to uri.toString()) - val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build() - - OneTimeWorkRequestBuilder() - .setInputData(data) - .addTag(TAG) - .setConstraints(constraints) - .build() - .enqueue(context) - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.work.image + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.media.ThumbnailUtils +import android.net.Uri +import android.util.Base64 +import androidx.annotation.WorkerThread +import androidx.hilt.work.HiltWorker +import androidx.work.Constraints +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.Worker +import androidx.work.WorkerParameters +import androidx.work.workDataOf +import com.forcetower.uefs.core.storage.database.UDatabase +import com.forcetower.uefs.core.storage.network.UService +import com.forcetower.uefs.core.util.ImgurUploader +import com.forcetower.uefs.core.work.enqueue +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.util.UUID +import okhttp3.OkHttpClient +import timber.log.Timber + +@HiltWorker +class UploadImageToStorage @AssistedInject constructor( + @Assisted context: Context, + @Assisted params: WorkerParameters, + private val client: OkHttpClient, + private val database: UDatabase, + private val service: UService +) : Worker(context, params) { + + @SuppressLint("WrongThread") + @WorkerThread + override fun doWork(): Result { + Timber.d("Started picture upload") + val tUri = inputData.getString(URI) ?: return Result.failure() + + val uri = Uri.parse(tUri) + + val resolver = applicationContext.contentResolver + val stream: InputStream + try { + stream = resolver.openInputStream(uri) ?: return Result.failure() + } catch (exception: Throwable) { + return Result.failure() + } + + val image = BitmapFactory.decodeStream(stream) + image ?: return Result.failure() + + val bitmap = ThumbnailUtils.extractThumbnail(image, 1080, 1080) + + val baos = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos) + val data = baos.toByteArray() + + val name = database.accessDao().getAccessDirect()?.username ?: "user-unes-${UUID.randomUUID().toString().substring(0, 4)}" + val encoded = Base64.encodeToString(data, Base64.DEFAULT) + val upload = ImgurUploader.upload(client, encoded, name) ?: return Result.retry() + + return try { + val response = service.updateProfileImage(upload).execute() + if (response.isSuccessful) { + Timber.d("Success setting on account!!") + try { + val acc = service.getAccount().execute() + acc.body()?.run { + database.accountDao().insert(this) + } + } catch (e: Throwable) { } + Result.success() + } else { + Timber.d("Unsucessful response ${response.code()}") + Result.retry() + } + } catch (t: Throwable) { + Timber.e(t, "Error") + Result.retry() + } + } + + companion object { + private const val URI = "image_uri" + private const val TAG = "upload_profile_image" + + fun createWorker(context: Context, uri: Uri) { + val data = workDataOf(URI to uri.toString()) + val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build() + + OneTimeWorkRequestBuilder() + .setInputData(data) + .addTag(TAG) + .setConstraints(constraints) + .build() + .enqueue(context) + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/core/work/sync/SyncLinkedWorker.kt b/app/src/main/java/com/forcetower/uefs/core/work/sync/SyncLinkedWorker.kt index 2e395fced..b3146fada 100644 --- a/app/src/main/java/com/forcetower/uefs/core/work/sync/SyncLinkedWorker.kt +++ b/app/src/main/java/com/forcetower/uefs/core/work/sync/SyncLinkedWorker.kt @@ -1,93 +1,93 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.work.sync - -import android.content.Context -import androidx.annotation.IntRange -import androidx.hilt.work.HiltWorker -import androidx.work.BackoffPolicy -import androidx.work.Constraints -import androidx.work.CoroutineWorker -import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkManager -import androidx.work.WorkerParameters -import androidx.work.workDataOf -import com.forcetower.uefs.core.storage.repository.SagresSyncRepository -import com.forcetower.uefs.core.work.enqueueUnique -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import timber.log.Timber -import java.util.concurrent.TimeUnit - -@HiltWorker -class SyncLinkedWorker @AssistedInject constructor( - @Assisted context: Context, - @Assisted params: WorkerParameters, - private val repository: SagresSyncRepository -) : CoroutineWorker(context, params) { - - override suspend fun doWork(): Result { - try { - repository.performSync("Linked") - } catch (t: Throwable) { - Timber.d("Worker ignored the error so it may continue") - } - val period = inputData.getInt(PERIOD, 60) - val count = inputData.getInt(COUNT, 0) - val other = if (count == 2) 1 else 2 - createWorker(applicationContext, period, true, other) - return Result.success() - } - - companion object { - private const val PERIOD = "linked_work_period" - private const val COUNT = "linked_work_count" - - private const val TAG = "linked_sagres_sync_worker" - private const val NAME = "worker_sagres_linked" - - fun createWorker(context: Context, @IntRange(from = 1, to = 9000) period: Int, replace: Boolean = true, count: Int = 0) { - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - - val data = workDataOf(PERIOD to period, COUNT to count) - - val request = OneTimeWorkRequestBuilder() - .setInputData(data) - .addTag(TAG) - .setInitialDelay(period.toLong(), TimeUnit.MINUTES) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) - .setConstraints(constraints) - .build() - - request.enqueueUnique(context, "${NAME}_$count", replace) - if (replace) { - Timber.d("Scheduled linked worker on a $period period") - } - } - - fun stopWorker(context: Context) { - WorkManager.getInstance(context).cancelAllWorkByTag(TAG).result.get() - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.work.sync + +import android.content.Context +import androidx.annotation.IntRange +import androidx.hilt.work.HiltWorker +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import androidx.work.workDataOf +import com.forcetower.uefs.core.storage.repository.SagresSyncRepository +import com.forcetower.uefs.core.work.enqueueUnique +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import java.util.concurrent.TimeUnit +import timber.log.Timber + +@HiltWorker +class SyncLinkedWorker @AssistedInject constructor( + @Assisted context: Context, + @Assisted params: WorkerParameters, + private val repository: SagresSyncRepository +) : CoroutineWorker(context, params) { + + override suspend fun doWork(): Result { + try { + repository.performSync("Linked") + } catch (t: Throwable) { + Timber.d("Worker ignored the error so it may continue") + } + val period = inputData.getInt(PERIOD, 60) + val count = inputData.getInt(COUNT, 0) + val other = if (count == 2) 1 else 2 + createWorker(applicationContext, period, true, other) + return Result.success() + } + + companion object { + private const val PERIOD = "linked_work_period" + private const val COUNT = "linked_work_count" + + private const val TAG = "linked_sagres_sync_worker" + private const val NAME = "worker_sagres_linked" + + fun createWorker(context: Context, @IntRange(from = 1, to = 9000) period: Int, replace: Boolean = true, count: Int = 0) { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val data = workDataOf(PERIOD to period, COUNT to count) + + val request = OneTimeWorkRequestBuilder() + .setInputData(data) + .addTag(TAG) + .setInitialDelay(period.toLong(), TimeUnit.MINUTES) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) + .setConstraints(constraints) + .build() + + request.enqueueUnique(context, "${NAME}_$count", replace) + if (replace) { + Timber.d("Scheduled linked worker on a $period period") + } + } + + fun stopWorker(context: Context) { + WorkManager.getInstance(context).cancelAllWorkByTag(TAG).result.get() + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/core/work/sync/SyncMainWorker.kt b/app/src/main/java/com/forcetower/uefs/core/work/sync/SyncMainWorker.kt index 862d9b8ff..de5ba41bf 100644 --- a/app/src/main/java/com/forcetower/uefs/core/work/sync/SyncMainWorker.kt +++ b/app/src/main/java/com/forcetower/uefs/core/work/sync/SyncMainWorker.kt @@ -1,99 +1,99 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.core.work.sync - -import android.content.Context -import androidx.annotation.IntRange -import androidx.hilt.work.HiltWorker -import androidx.preference.PreferenceManager -import androidx.work.BackoffPolicy -import androidx.work.Constraints -import androidx.work.CoroutineWorker -import androidx.work.NetworkType -import androidx.work.PeriodicWorkRequestBuilder -import androidx.work.WorkManager -import androidx.work.WorkerParameters -import com.forcetower.uefs.core.constants.PreferenceConstants -import com.forcetower.uefs.core.storage.repository.SagresSyncRepository -import com.forcetower.uefs.core.storage.repository.SnowpiercerSyncRepository -import com.forcetower.uefs.core.work.enqueueUnique -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import timber.log.Timber -import java.util.concurrent.TimeUnit -import javax.inject.Named - -@HiltWorker -class SyncMainWorker @AssistedInject constructor( - @Assisted context: Context, - @Assisted params: WorkerParameters, - private val repository: SagresSyncRepository, - private val snowpiercer: SnowpiercerSyncRepository, - @Named("flagSnowpiercerEnabled") private val snowpiercerEnabled: Boolean, -) : CoroutineWorker(context, params) { - override suspend fun doWork(): Result { - try { - Timber.d("Main Worker started") - if (snowpiercerEnabled) { - snowpiercer.performSync("Snowpiercer") - } else { - repository.performSync("Principal") - } - Timber.d("Main Worker completed") - } catch (t: Throwable) { - Timber.d(t, "Worker ignored the error so it may continue") - } - return Result.success() - } - - companion object { - private const val TAG = "main_sagres_sync_worker" - private const val NAME = "worker_sagres_sync" - - // Function that creates a Sagres Sync Worker - fun createWorker(ctx: Context, @IntRange(from = 15, to = 9000) period: Int, forcedReplace: Boolean = false) { - val preferences = PreferenceManager.getDefaultSharedPreferences(ctx) - // We need to observe the frequency to know if we need to replace the current worker with a new one - val current = preferences.getInt(PreferenceConstants.SYNC_FREQUENCY, 60) - val replace = current != period || forcedReplace - - // The Sync Worker requires internet connection - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - - // This worker is periodic - val request = PeriodicWorkRequestBuilder(period.toLong(), TimeUnit.MINUTES) - .addTag(TAG) - .setConstraints(constraints) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) - .build() - - request.enqueueUnique(ctx, NAME, replace) - if (replace) preferences.edit().putInt(PreferenceConstants.SYNC_FREQUENCY, period).apply() - Timber.d("Main Sync Work Scheduled") - } - - fun stopWorker(ctx: Context) { - WorkManager.getInstance(ctx).cancelAllWorkByTag(TAG) - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.core.work.sync + +import android.content.Context +import androidx.annotation.IntRange +import androidx.hilt.work.HiltWorker +import androidx.preference.PreferenceManager +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import com.forcetower.uefs.core.constants.PreferenceConstants +import com.forcetower.uefs.core.storage.repository.SagresSyncRepository +import com.forcetower.uefs.core.storage.repository.SnowpiercerSyncRepository +import com.forcetower.uefs.core.work.enqueueUnique +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import java.util.concurrent.TimeUnit +import javax.inject.Named +import timber.log.Timber + +@HiltWorker +class SyncMainWorker @AssistedInject constructor( + @Assisted context: Context, + @Assisted params: WorkerParameters, + private val repository: SagresSyncRepository, + private val snowpiercer: SnowpiercerSyncRepository, + @Named("flagSnowpiercerEnabled") private val snowpiercerEnabled: Boolean +) : CoroutineWorker(context, params) { + override suspend fun doWork(): Result { + try { + Timber.d("Main Worker started") + if (snowpiercerEnabled) { + snowpiercer.performSync("Snowpiercer") + } else { + repository.performSync("Principal") + } + Timber.d("Main Worker completed") + } catch (t: Throwable) { + Timber.d(t, "Worker ignored the error so it may continue") + } + return Result.success() + } + + companion object { + private const val TAG = "main_sagres_sync_worker" + private const val NAME = "worker_sagres_sync" + + // Function that creates a Sagres Sync Worker + fun createWorker(ctx: Context, @IntRange(from = 15, to = 9000) period: Int, forcedReplace: Boolean = false) { + val preferences = PreferenceManager.getDefaultSharedPreferences(ctx) + // We need to observe the frequency to know if we need to replace the current worker with a new one + val current = preferences.getInt(PreferenceConstants.SYNC_FREQUENCY, 60) + val replace = current != period || forcedReplace + + // The Sync Worker requires internet connection + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + // This worker is periodic + val request = PeriodicWorkRequestBuilder(period.toLong(), TimeUnit.MINUTES) + .addTag(TAG) + .setConstraints(constraints) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) + .build() + + request.enqueueUnique(ctx, NAME, replace) + if (replace) preferences.edit().putInt(PreferenceConstants.SYNC_FREQUENCY, period).apply() + Timber.d("Main Sync Work Scheduled") + } + + fun stopWorker(ctx: Context) { + WorkManager.getInstance(ctx).cancelAllWorkByTag(TAG) + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/domain/model/paradox/DisciplineEvaluation.kt b/app/src/main/java/com/forcetower/uefs/domain/model/paradox/DisciplineEvaluation.kt index b2e05e1b0..d7d1b8941 100644 --- a/app/src/main/java/com/forcetower/uefs/domain/model/paradox/DisciplineEvaluation.kt +++ b/app/src/main/java/com/forcetower/uefs/domain/model/paradox/DisciplineEvaluation.kt @@ -27,4 +27,4 @@ data class TeacherMean( val studentCountWeighted: Int, val semesterStart: ZonedDateTime, val values: List -) \ No newline at end of file +) diff --git a/app/src/main/java/com/forcetower/uefs/domain/usecase/account/ChangeProfilePictureUseCase.kt b/app/src/main/java/com/forcetower/uefs/domain/usecase/account/ChangeProfilePictureUseCase.kt index 1fbf9be28..596c4609c 100644 --- a/app/src/main/java/com/forcetower/uefs/domain/usecase/account/ChangeProfilePictureUseCase.kt +++ b/app/src/main/java/com/forcetower/uefs/domain/usecase/account/ChangeProfilePictureUseCase.kt @@ -9,10 +9,10 @@ import android.util.Base64 import com.forcetower.uefs.core.storage.repository.cloud.EdgeAccountRepository import dagger.Reusable import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import java.io.ByteArrayOutputStream import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext @Reusable class ChangeProfilePictureUseCase @Inject constructor( @@ -36,4 +36,4 @@ class ChangeProfilePictureUseCase @Inject constructor( repository.uploadPicture(encoded) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/domain/usecase/account/GetEdgeServiceAccountUseCase.kt b/app/src/main/java/com/forcetower/uefs/domain/usecase/account/GetEdgeServiceAccountUseCase.kt index 39753662b..3c7bdaa4a 100644 --- a/app/src/main/java/com/forcetower/uefs/domain/usecase/account/GetEdgeServiceAccountUseCase.kt +++ b/app/src/main/java/com/forcetower/uefs/domain/usecase/account/GetEdgeServiceAccountUseCase.kt @@ -10,4 +10,4 @@ class GetEdgeServiceAccountUseCase @Inject constructor( ) { operator fun invoke() = repository.getAccount() suspend fun update() = repository.fetchAccountIfNeeded() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/domain/usecase/auth/CompleteAssertionUseCase.kt b/app/src/main/java/com/forcetower/uefs/domain/usecase/auth/CompleteAssertionUseCase.kt index 4a3c2c4c1..c26b91241 100644 --- a/app/src/main/java/com/forcetower/uefs/domain/usecase/auth/CompleteAssertionUseCase.kt +++ b/app/src/main/java/com/forcetower/uefs/domain/usecase/auth/CompleteAssertionUseCase.kt @@ -3,8 +3,8 @@ package com.forcetower.uefs.domain.usecase.auth import com.forcetower.uefs.core.model.unes.EdgeServiceAccount import com.forcetower.uefs.core.storage.repository.cloud.EdgeAuthRepository import dagger.Reusable -import timber.log.Timber import javax.inject.Inject +import timber.log.Timber @Reusable class CompleteAssertionUseCase @Inject constructor( diff --git a/app/src/main/java/com/forcetower/uefs/domain/usecase/auth/EdgeAnonymousLoginUseCase.kt b/app/src/main/java/com/forcetower/uefs/domain/usecase/auth/EdgeAnonymousLoginUseCase.kt index 6358ede43..8f4faf0aa 100644 --- a/app/src/main/java/com/forcetower/uefs/domain/usecase/auth/EdgeAnonymousLoginUseCase.kt +++ b/app/src/main/java/com/forcetower/uefs/domain/usecase/auth/EdgeAnonymousLoginUseCase.kt @@ -20,4 +20,4 @@ class EdgeAnonymousLoginUseCase @Inject constructor( suspend fun invoke(username: String, password: String) { repository.anonymous(username, password) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/domain/usecase/auth/LinkEmailUseCase.kt b/app/src/main/java/com/forcetower/uefs/domain/usecase/auth/LinkEmailUseCase.kt index 2e703a67d..1bc20e198 100644 --- a/app/src/main/java/com/forcetower/uefs/domain/usecase/auth/LinkEmailUseCase.kt +++ b/app/src/main/java/com/forcetower/uefs/domain/usecase/auth/LinkEmailUseCase.kt @@ -17,4 +17,4 @@ class LinkEmailUseCase @Inject constructor( suspend fun finish(code: String, securityToken: String): EmailLinkComplete { return repository.emailLinkFinish(code, securityToken) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/domain/usecase/profile/GetProfileUseCase.kt b/app/src/main/java/com/forcetower/uefs/domain/usecase/profile/GetProfileUseCase.kt index 459d32113..ed299e3e0 100644 --- a/app/src/main/java/com/forcetower/uefs/domain/usecase/profile/GetProfileUseCase.kt +++ b/app/src/main/java/com/forcetower/uefs/domain/usecase/profile/GetProfileUseCase.kt @@ -9,4 +9,4 @@ class GetProfileUseCase @Inject constructor( private val repository: ProfileRepository ) { operator fun invoke() = repository.me() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/easter/darktheme/DarkThemeRepository.kt b/app/src/main/java/com/forcetower/uefs/easter/darktheme/DarkThemeRepository.kt index 141ad2cde..7599e54cc 100644 --- a/app/src/main/java/com/forcetower/uefs/easter/darktheme/DarkThemeRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/easter/darktheme/DarkThemeRepository.kt @@ -1,158 +1,158 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.easter.darktheme - -import android.content.Context -import android.content.SharedPreferences -import androidx.annotation.MainThread -import androidx.annotation.WorkerThread -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import com.forcetower.uefs.AppExecutors -import com.forcetower.uefs.R -import com.forcetower.uefs.core.model.api.DarkInvite -import com.forcetower.uefs.core.model.api.DarkUnlock -import com.forcetower.uefs.core.storage.database.UDatabase -import com.forcetower.uefs.core.storage.network.UService -import com.forcetower.uefs.core.storage.resource.Resource -import com.forcetower.uefs.easter.twofoureight.tools.ScoreKeeper -import com.forcetower.uefs.feature.shared.extensions.generateCalendarFromHour -import timber.log.Timber -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class DarkThemeRepository @Inject constructor( - private val preferences: SharedPreferences, - private val context: Context, - private val executors: AppExecutors, - private val database: UDatabase, - private val service: UService -) { - - fun getPreconditions(): LiveData> { - val result = MutableLiveData>() - executors.diskIO().execute { - val precondition1 = create2048Precondition() - val precondition2 = createLocationPrecondition() - val precondition3 = createHoursPrecondition() - val list = listOf(precondition1, precondition2, precondition3) - executors.others().execute { - sendInfoToServer(list) - } - result.postValue(list) - } - return result - } - - @WorkerThread - private fun sendInfoToServer(list: List) { - val completed = list.filter { it.completed } - val completedSize = completed.size - val account = database.accountDao().getAccountDirect() - if (completed.isEmpty()) return - - if (account != null) { - try { - service.requestDarkThemeUnlock(DarkUnlock(completedSize)).execute() - } catch (throwable: Throwable) { - Timber.e(throwable) - } - } - } - - private fun create2048Precondition(): Precondition { - val the2048score = context.getSharedPreferences(ScoreKeeper.PREFERENCES, Context.MODE_PRIVATE) - .getLong(ScoreKeeper.HIGH_SCORE, 0) - Timber.d("2048 score: $the2048score") - return Precondition(context.getString(R.string.precondition_1), context.getString(R.string.precondition_1_desc), the2048score >= 50000) - } - - @WorkerThread - private fun createLocationPrecondition(): Precondition { - val bigTray = preferences.getBoolean("ach_dora_big_tray", false) - val library = preferences.getBoolean("ach_dora_library", false) - val zoology = preferences.getBoolean("ach_dora_zoology", false) - val hogwarts = preferences.getBoolean("ach_dora_hogwarts", false) - val module1 = preferences.getBoolean("ach_dora_mod1", false) - val module7 = preferences.getBoolean("ach_dora_mod7", false) - val management = preferences.getBoolean("ach_dora_management", false) - val all = bigTray and library and zoology and hogwarts and module1 and module7 and management - - val schedule = database.classLocationDao().getCurrentScheduleDirect() - var eightHours = false - val day = schedule.groupBy { it.day } - day.entries.forEach { group -> - var minutes = 0 - group.value.forEach { location -> - val start = location.startsAt.generateCalendarFromHour()?.timeInMillis - val end = location.endsAt.generateCalendarFromHour()?.timeInMillis - if (start != null && end != null) { - val diff = end - start - minutes += TimeUnit.MINUTES.convert(diff, TimeUnit.MILLISECONDS).toInt() - } - } - if (minutes >= 480) eightHours = true - } - - return Precondition(context.getString(R.string.precondition_2), context.getString(R.string.precondition_2_desc), all && eightHours) - } - - @WorkerThread - private fun createHoursPrecondition(): Precondition { - val list = database.classDao().getAllDirect() - val credits = list.asSequence().map { it.discipline.credits }.sum() - Timber.d("Credits: $credits") - return Precondition(context.getString(R.string.precondition_3), context.getString(R.string.precondition_3_desc, credits), credits >= 2200) - } - - @MainThread - fun sendDarkThemeTo(username: String?): LiveData> { - val result = MutableLiveData>() - result.value = Resource.loading(false) - - executors.networkIO().execute { - try { - val response = service.requestDarkSendTo(DarkInvite(username)).execute() - val code = response.code() - Timber.d("Response code $code") - if (code == 200) { - try { - val accResponse = service.getAccount().execute() - if (accResponse.isSuccessful) { - val item = accResponse.body() - if (item != null) { - database.accountDao().insert(item) - } - } - } catch (ignored: Throwable) { } - result.postValue(Resource.success(true)) - } else { - result.postValue(Resource.error("Invalid request", code, Exception("Nothing special"))) - } - } catch (t: Throwable) { - result.postValue(Resource.error("Invalid for all", 500, t)) - } - } - return result - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.easter.darktheme + +import android.content.Context +import android.content.SharedPreferences +import androidx.annotation.MainThread +import androidx.annotation.WorkerThread +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.forcetower.uefs.AppExecutors +import com.forcetower.uefs.R +import com.forcetower.uefs.core.model.api.DarkInvite +import com.forcetower.uefs.core.model.api.DarkUnlock +import com.forcetower.uefs.core.storage.database.UDatabase +import com.forcetower.uefs.core.storage.network.UService +import com.forcetower.uefs.core.storage.resource.Resource +import com.forcetower.uefs.easter.twofoureight.tools.ScoreKeeper +import com.forcetower.uefs.feature.shared.extensions.generateCalendarFromHour +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton +import timber.log.Timber + +@Singleton +class DarkThemeRepository @Inject constructor( + private val preferences: SharedPreferences, + private val context: Context, + private val executors: AppExecutors, + private val database: UDatabase, + private val service: UService +) { + + fun getPreconditions(): LiveData> { + val result = MutableLiveData>() + executors.diskIO().execute { + val precondition1 = create2048Precondition() + val precondition2 = createLocationPrecondition() + val precondition3 = createHoursPrecondition() + val list = listOf(precondition1, precondition2, precondition3) + executors.others().execute { + sendInfoToServer(list) + } + result.postValue(list) + } + return result + } + + @WorkerThread + private fun sendInfoToServer(list: List) { + val completed = list.filter { it.completed } + val completedSize = completed.size + val account = database.accountDao().getAccountDirect() + if (completed.isEmpty()) return + + if (account != null) { + try { + service.requestDarkThemeUnlock(DarkUnlock(completedSize)).execute() + } catch (throwable: Throwable) { + Timber.e(throwable) + } + } + } + + private fun create2048Precondition(): Precondition { + val the2048score = context.getSharedPreferences(ScoreKeeper.PREFERENCES, Context.MODE_PRIVATE) + .getLong(ScoreKeeper.HIGH_SCORE, 0) + Timber.d("2048 score: $the2048score") + return Precondition(context.getString(R.string.precondition_1), context.getString(R.string.precondition_1_desc), the2048score >= 50000) + } + + @WorkerThread + private fun createLocationPrecondition(): Precondition { + val bigTray = preferences.getBoolean("ach_dora_big_tray", false) + val library = preferences.getBoolean("ach_dora_library", false) + val zoology = preferences.getBoolean("ach_dora_zoology", false) + val hogwarts = preferences.getBoolean("ach_dora_hogwarts", false) + val module1 = preferences.getBoolean("ach_dora_mod1", false) + val module7 = preferences.getBoolean("ach_dora_mod7", false) + val management = preferences.getBoolean("ach_dora_management", false) + val all = bigTray and library and zoology and hogwarts and module1 and module7 and management + + val schedule = database.classLocationDao().getCurrentScheduleDirect() + var eightHours = false + val day = schedule.groupBy { it.day } + day.entries.forEach { group -> + var minutes = 0 + group.value.forEach { location -> + val start = location.startsAt.generateCalendarFromHour()?.timeInMillis + val end = location.endsAt.generateCalendarFromHour()?.timeInMillis + if (start != null && end != null) { + val diff = end - start + minutes += TimeUnit.MINUTES.convert(diff, TimeUnit.MILLISECONDS).toInt() + } + } + if (minutes >= 480) eightHours = true + } + + return Precondition(context.getString(R.string.precondition_2), context.getString(R.string.precondition_2_desc), all && eightHours) + } + + @WorkerThread + private fun createHoursPrecondition(): Precondition { + val list = database.classDao().getAllDirect() + val credits = list.asSequence().map { it.discipline.credits }.sum() + Timber.d("Credits: $credits") + return Precondition(context.getString(R.string.precondition_3), context.getString(R.string.precondition_3_desc, credits), credits >= 2200) + } + + @MainThread + fun sendDarkThemeTo(username: String?): LiveData> { + val result = MutableLiveData>() + result.value = Resource.loading(false) + + executors.networkIO().execute { + try { + val response = service.requestDarkSendTo(DarkInvite(username)).execute() + val code = response.code() + Timber.d("Response code $code") + if (code == 200) { + try { + val accResponse = service.getAccount().execute() + if (accResponse.isSuccessful) { + val item = accResponse.body() + if (item != null) { + database.accountDao().insert(item) + } + } + } catch (ignored: Throwable) { } + result.postValue(Resource.success(true)) + } else { + result.postValue(Resource.error("Invalid request", code, Exception("Nothing special"))) + } + } catch (t: Throwable) { + result.postValue(Resource.error("Invalid for all", 500, t)) + } + } + return result + } +} diff --git a/app/src/main/java/com/forcetower/uefs/easter/twofoureight/Game2048Activity.kt b/app/src/main/java/com/forcetower/uefs/easter/twofoureight/Game2048Activity.kt index cebd66940..b4371b060 100644 --- a/app/src/main/java/com/forcetower/uefs/easter/twofoureight/Game2048Activity.kt +++ b/app/src/main/java/com/forcetower/uefs/easter/twofoureight/Game2048Activity.kt @@ -1,111 +1,110 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.easter.twofoureight - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.view.KeyEvent -import androidx.activity.viewModels -import androidx.core.app.ActivityCompat -import androidx.core.view.WindowCompat -import com.forcetower.uefs.R -import com.forcetower.uefs.core.vm.UserSessionViewModel -import com.forcetower.uefs.easter.twofoureight.tools.KeyListener -import com.forcetower.uefs.feature.shared.UGameActivity -import com.forcetower.uefs.feature.shared.extensions.config -import com.google.android.material.snackbar.Snackbar -import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber - -@AndroidEntryPoint -class Game2048Activity : UGameActivity() { - private val sessionViewModel: UserSessionViewModel by viewModels() - - public override fun onCreate(savedInstanceState: Bundle?) { - overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_game2048) - - WindowCompat.setDecorFitsSystemWindows(window, false) - - if (savedInstanceState == null) { - supportFragmentManager.beginTransaction() - .add(R.id.container, Game2048Fragment(), "Game Fragment") - .commit() - } - - unlockAchievement(R.string.achievement_voc_me_achou) - revealAchievement(R.string.achievement_voc__bom) - revealAchievement(R.string.achievement_o_campeo_de_2048_no_unes) - revealAchievement(R.string.achievement_eu_tentei) - revealAchievement(R.string.achievement_a_prtica_leva__perfeio) - } - - override fun showSnack(string: String, duration: Int) { - getSnackInstance(string, duration).show() - } - - override fun getSnackInstance(string: String, duration: Int): Snackbar { - val snack = Snackbar.make(findViewById(R.id.container), string, duration) - snack.config() - return snack - } - - override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { - Timber.d("Action: $keyCode event: $event") - if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { - val current = supportFragmentManager.findFragmentByTag("Game Fragment") - if (current is KeyListener) { - return (current as KeyListener).onKeyDown(keyCode, event) - } - } - return super.onKeyDown(keyCode, event) - } - - override fun onUserInteraction() { - super.onUserInteraction() - sessionViewModel.onUserInteraction() - } - - override fun onPause() { - super.onPause() - sessionViewModel.onUserInteraction() - } - - override fun onResume() { - super.onResume() - sessionViewModel.onUserInteraction() - } - - override fun onDestroy() { - super.onDestroy() - sessionViewModel.onUserInteraction() - } - - companion object { - - fun startActivity(context: Context) { - val intent = Intent(context, Game2048Activity::class.java) - context.startActivity(intent) - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.easter.twofoureight + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.KeyEvent +import androidx.activity.viewModels +import androidx.core.view.WindowCompat +import com.forcetower.uefs.R +import com.forcetower.uefs.core.vm.UserSessionViewModel +import com.forcetower.uefs.easter.twofoureight.tools.KeyListener +import com.forcetower.uefs.feature.shared.UGameActivity +import com.forcetower.uefs.feature.shared.extensions.config +import com.google.android.material.snackbar.Snackbar +import dagger.hilt.android.AndroidEntryPoint +import timber.log.Timber + +@AndroidEntryPoint +class Game2048Activity : UGameActivity() { + private val sessionViewModel: UserSessionViewModel by viewModels() + + public override fun onCreate(savedInstanceState: Bundle?) { + overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_game2048) + + WindowCompat.setDecorFitsSystemWindows(window, false) + + if (savedInstanceState == null) { + supportFragmentManager.beginTransaction() + .add(R.id.container, Game2048Fragment(), "Game Fragment") + .commit() + } + + unlockAchievement(R.string.achievement_voc_me_achou) + revealAchievement(R.string.achievement_voc__bom) + revealAchievement(R.string.achievement_o_campeo_de_2048_no_unes) + revealAchievement(R.string.achievement_eu_tentei) + revealAchievement(R.string.achievement_a_prtica_leva__perfeio) + } + + override fun showSnack(string: String, duration: Int) { + getSnackInstance(string, duration).show() + } + + override fun getSnackInstance(string: String, duration: Int): Snackbar { + val snack = Snackbar.make(findViewById(R.id.container), string, duration) + snack.config() + return snack + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + Timber.d("Action: $keyCode event: $event") + if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + val current = supportFragmentManager.findFragmentByTag("Game Fragment") + if (current is KeyListener) { + return (current as KeyListener).onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onUserInteraction() { + super.onUserInteraction() + sessionViewModel.onUserInteraction() + } + + override fun onPause() { + super.onPause() + sessionViewModel.onUserInteraction() + } + + override fun onResume() { + super.onResume() + sessionViewModel.onUserInteraction() + } + + override fun onDestroy() { + super.onDestroy() + sessionViewModel.onUserInteraction() + } + + companion object { + + fun startActivity(context: Context) { + val intent = Intent(context, Game2048Activity::class.java) + context.startActivity(intent) + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/easter/twofoureight/Game2048Fragment.kt b/app/src/main/java/com/forcetower/uefs/easter/twofoureight/Game2048Fragment.kt index 1a9fde1fe..af0426d8b 100644 --- a/app/src/main/java/com/forcetower/uefs/easter/twofoureight/Game2048Fragment.kt +++ b/app/src/main/java/com/forcetower/uefs/easter/twofoureight/Game2048Fragment.kt @@ -1,350 +1,351 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.easter.twofoureight - -import android.annotation.SuppressLint -import android.content.Context -import android.content.SharedPreferences -import android.os.Bundle -import android.view.KeyEvent -import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.View -import android.view.View.GONE -import android.view.View.VISIBLE -import android.view.ViewGroup -import android.widget.Toast -import androidx.core.text.HtmlCompat -import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY -import androidx.databinding.DataBindingUtil -import androidx.preference.PreferenceManager -import com.forcetower.uefs.R -import com.forcetower.uefs.databinding.GameFragment2048Binding -import com.forcetower.uefs.easter.darktheme.DarkThemeRepository -import com.forcetower.uefs.easter.twofoureight.tools.InputListener -import com.forcetower.uefs.easter.twofoureight.tools.KeyListener -import com.forcetower.uefs.easter.twofoureight.tools.ScoreKeeper -import com.forcetower.uefs.easter.twofoureight.view.Game -import com.forcetower.uefs.easter.twofoureight.view.Tile -import com.forcetower.uefs.feature.shared.UFragment -import com.forcetower.uefs.feature.shared.UGameActivity -import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber -import javax.inject.Inject -import kotlin.math.abs - -/** - * Created by João Paulo on 02/06/2018. - */ -@AndroidEntryPoint -class Game2048Fragment : UFragment(), KeyListener, Game.GameStateListener, View.OnTouchListener { - - @Inject lateinit var preferences: SharedPreferences - @Inject lateinit var darkRepository: DarkThemeRepository - - private var downX: Float = 0.toFloat() - private var downY: Float = 0.toFloat() - private var upX: Float = 0.toFloat() - private var upY: Float = 0.toFloat() - - private lateinit var mGame: Game - private lateinit var binding: GameFragment2048Binding - private var activity: UGameActivity? = null - - override fun onAttach(context: Context) { - super.onAttach(context) - activity = context as? UGameActivity - activity ?: Timber.e("Adventure Fragment must be attached to a UGameActivity for it to work") - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - binding = DataBindingUtil.inflate(inflater, R.layout.game_fragment_2048, container, false) - binding.gamePad.setOnTouchListener(this) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - val mScoreKeeper = ScoreKeeper(requireActivity()) - - mScoreKeeper.setViews(binding.tvScore, binding.tvHighscore) - mScoreKeeper.setScoreListener( - object : Game.ScoreListener { - override fun onNewScore(score: Long) { - if (score >= 50000) { - unlockDarkTheme() - } - } - } - ) - - mGame = Game() - mGame.setup(binding.gameview) - mGame.setScoreListener(mScoreKeeper) - mGame.setGameStateListener(this) - mGame.newGame() - val input = InputListener() - input.setView(binding.gameview) - input.setGame(mGame) - - binding.tvTitle.setOnClickListener { - if (!mGame.isEndlessMode) { - mGame.setEndlessMode() - binding.tvTitle.text = HtmlCompat.fromHtml("∞", FROM_HTML_MODE_LEGACY) - } - } - - binding.ibReset.setOnClickListener { - mGame.newGame() - binding.tvTitle.text = HtmlCompat.fromHtml("2048", FROM_HTML_MODE_LEGACY) - } - - binding.ibUndo.setOnClickListener { - mGame.revertUndoState() - if (mGame.gameState === Game.State.ENDLESS || mGame.gameState === Game.State.ENDLESS_WON) { - binding.tvTitle.text = HtmlCompat.fromHtml("∞", FROM_HTML_MODE_LEGACY) - } else { - binding.tvTitle.text = HtmlCompat.fromHtml("2048", FROM_HTML_MODE_LEGACY) - } - } - - binding.ibReset.setOnLongClickListener { - Toast.makeText(activity, getString(R.string.start_new_game), Toast.LENGTH_SHORT).show() - true - } - } - - private fun unlockDarkTheme() { - val enabled = preferences.getBoolean("ach_night_mode_enabled", false) - if (!enabled) { - preferences.edit().putBoolean("ach_night_mode_enabled", true).apply() - darkRepository.getPreconditions() - } - } - - override fun onPause() { - save() - super.onPause() - } - - override fun onResume() { - load() - if (mGame.gameState === Game.State.ENDLESS || mGame.gameState === Game.State.ENDLESS_WON) { - binding.tvTitle.text = HtmlCompat.fromHtml("∞", FROM_HTML_MODE_LEGACY) - } - super.onResume() - } - - override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { - when (keyCode) { - KeyEvent.KEYCODE_DPAD_DOWN -> { - mGame.move(Game.DIRECTION_DOWN) - return true - } - KeyEvent.KEYCODE_DPAD_UP -> { - mGame.move(Game.DIRECTION_UP) - return true - } - KeyEvent.KEYCODE_DPAD_LEFT -> { - mGame.move(Game.DIRECTION_LEFT) - return true - } - KeyEvent.KEYCODE_DPAD_RIGHT -> { - mGame.move(Game.DIRECTION_RIGHT) - return true - } - else -> return false - } - } - - override fun onGameStateChanged(state: Game.State?) { - Timber.d("Game state changed to: %s", state!!) - if (state == Game.State.WON || state == Game.State.ENDLESS_WON) { - binding.tvEndgameOverlay.visibility = VISIBLE - binding.tvEndgameOverlay.setText(R.string.you_win) - - activity?.unlockAchievement(R.string.achievement_voc__bom) - activity?.incrementAchievementProgress(R.string.achievement_o_campeo_de_2048_no_unes, 1) - } else if (state == Game.State.LOST) { - binding.tvEndgameOverlay.visibility = VISIBLE - binding.tvEndgameOverlay.setText(R.string.game_over) - activity?.unlockAchievement(R.string.achievement_eu_tentei) - activity?.incrementAchievementProgress(R.string.achievement_a_prtica_leva__perfeio, 1) - } else { - binding.tvEndgameOverlay.visibility = GONE - } - } - - private fun save() { - val settings = PreferenceManager.getDefaultSharedPreferences(requireContext()) - val editor = settings.edit() - val field = mGame.gameGrid!!.grid - val undoField = mGame.gameGrid!!.undoGrid - val uuid = mGame.uuid - for (xx in field.indices) { - for (yy in field[0].indices) { - if (field[xx][yy] != null) { - editor.putInt("$xx $yy", field[xx][yy].value) - } else { - editor.putInt("$xx $yy", 0) - } - - if (undoField[xx][yy] != null) { - editor.putInt("$UNDO_GRID$xx $yy", undoField[xx][yy].value) - } else { - editor.putInt("$UNDO_GRID$xx $yy", 0) - } - } - } - editor.putLong(SCORE, mGame.score) - editor.putLong(UNDO_SCORE, mGame.lastScore) - editor.putBoolean(CAN_UNDO, mGame.isCanUndo) - editor.putString(GAME_STATE, mGame.gameState!!.name) - editor.putString(UNDO_GAME_STATE, mGame.lastGameState!!.name) - editor.putString(GAME_UUID, uuid) - editor.apply() - } - - private fun load() { - // Stopping all animations - binding.gameview.cancelAnimations() - val settings = PreferenceManager.getDefaultSharedPreferences(requireContext()) - for (xx in mGame.gameGrid!!.grid.indices) { - for (yy in mGame.gameGrid!!.grid[0].indices) { - val value = settings.getInt("$xx $yy", -1) - if (value > 0) { - mGame.gameGrid!!.grid[xx][yy] = Tile(xx, yy, value) - } else if (value == 0) { - mGame.gameGrid!!.grid[xx][yy] = null - } - - val undoValue = settings.getInt("$UNDO_GRID$xx $yy", -1) - if (undoValue > 0) { - mGame.gameGrid!!.undoGrid[xx][yy] = Tile(xx, yy, undoValue) - } else if (value == 0) { - mGame.gameGrid!!.undoGrid[xx][yy] = null - } - } - } - - mGame.score = settings.getLong(SCORE, 0) - mGame.lastScore = settings.getLong(UNDO_SCORE, 0) - mGame.isCanUndo = settings.getBoolean(CAN_UNDO, mGame.isCanUndo) - mGame.uuid = settings.getString(GAME_UUID, mGame.uuid) ?: mGame.uuid - try { - mGame.updateGameState(Game.State.valueOf(settings.getString(GAME_STATE, Game.State.NORMAL.name)!!)) - } catch (e: Exception) { - mGame.updateGameState(Game.State.NORMAL) - } - - try { - mGame.lastGameState = Game.State.valueOf(settings.getString(UNDO_GAME_STATE, Game.State.NORMAL.name)!!) - } catch (e: Exception) { - mGame.lastGameState = Game.State.NORMAL - } - - mGame.updateUI() - } - - private fun onLeftSwipe() { - mGame.move(Game.DIRECTION_LEFT) - Timber.d("Left Swipe") - } - - private fun onRightSwipe() { - mGame.move(Game.DIRECTION_RIGHT) - Timber.d("Right Swipe") - } - - private fun onDownSwipe() { - mGame.move(Game.DIRECTION_DOWN) - Timber.d("Down Swipe") - } - - private fun onUpSwipe() { - mGame.move(Game.DIRECTION_UP) - Timber.d("Up Swipe") - } - - @SuppressLint("ClickableViewAccessibility") - override fun onTouch(v: View, event: MotionEvent): Boolean { - when (event.action) { - MotionEvent.ACTION_DOWN -> { - downX = event.x - downY = event.y - return true - } - MotionEvent.ACTION_UP -> { - upX = event.x - upY = event.y - - val deltaX = downX - upX - val deltaY = downY - upY - - // swipe horizontal? - if (abs(deltaX) > abs(deltaY)) { - if (abs(deltaX) > MIN_DISTANCE) { - // left or right - if (deltaX > 0) { - this.onLeftSwipe() - return true - } - if (deltaX < 0) { - this.onRightSwipe() - return true - } - } else { - return false // We don't consume the event - } - } else { - if (abs(deltaY) > MIN_DISTANCE) { - // top or down - if (deltaY < 0) { - this.onDownSwipe() - return true - } - if (deltaY > 0) { - this.onUpSwipe() - return true - } - } else { - return false // We don't consume the event - } - } // swipe vertical? - - return true - } - } - return false - } - - companion object { - internal const val MIN_DISTANCE = 70 - - private const val SCORE = "savegame.score" - private const val UNDO_SCORE = "savegame.undoscore" - private const val CAN_UNDO = "savegame.canundo" - private const val UNDO_GRID = "savegame.undo" - private const val GAME_STATE = "savegame.gamestate" - private const val UNDO_GAME_STATE = "savegame.undogamestate" - private const val GAME_UUID = "savegame.uuid" - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.easter.twofoureight + +import android.annotation.SuppressLint +import android.content.Context +import android.content.SharedPreferences +import android.os.Bundle +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.text.HtmlCompat +import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY +import androidx.databinding.DataBindingUtil +import androidx.preference.PreferenceManager +import com.forcetower.uefs.R +import com.forcetower.uefs.databinding.GameFragment2048Binding +import com.forcetower.uefs.easter.darktheme.DarkThemeRepository +import com.forcetower.uefs.easter.twofoureight.tools.InputListener +import com.forcetower.uefs.easter.twofoureight.tools.KeyListener +import com.forcetower.uefs.easter.twofoureight.tools.ScoreKeeper +import com.forcetower.uefs.easter.twofoureight.view.Game +import com.forcetower.uefs.easter.twofoureight.view.Tile +import com.forcetower.uefs.feature.shared.UFragment +import com.forcetower.uefs.feature.shared.UGameActivity +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlin.math.abs +import timber.log.Timber + +/** + * Created by João Paulo on 02/06/2018. + */ +@AndroidEntryPoint +class Game2048Fragment : UFragment(), KeyListener, Game.GameStateListener, View.OnTouchListener { + + @Inject lateinit var preferences: SharedPreferences + + @Inject lateinit var darkRepository: DarkThemeRepository + + private var downX: Float = 0.toFloat() + private var downY: Float = 0.toFloat() + private var upX: Float = 0.toFloat() + private var upY: Float = 0.toFloat() + + private lateinit var mGame: Game + private lateinit var binding: GameFragment2048Binding + private var activity: UGameActivity? = null + + override fun onAttach(context: Context) { + super.onAttach(context) + activity = context as? UGameActivity + activity ?: Timber.e("Adventure Fragment must be attached to a UGameActivity for it to work") + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = DataBindingUtil.inflate(inflater, R.layout.game_fragment_2048, container, false) + binding.gamePad.setOnTouchListener(this) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val mScoreKeeper = ScoreKeeper(requireActivity()) + + mScoreKeeper.setViews(binding.tvScore, binding.tvHighscore) + mScoreKeeper.setScoreListener( + object : Game.ScoreListener { + override fun onNewScore(score: Long) { + if (score >= 50000) { + unlockDarkTheme() + } + } + } + ) + + mGame = Game() + mGame.setup(binding.gameview) + mGame.setScoreListener(mScoreKeeper) + mGame.setGameStateListener(this) + mGame.newGame() + val input = InputListener() + input.setView(binding.gameview) + input.setGame(mGame) + + binding.tvTitle.setOnClickListener { + if (!mGame.isEndlessMode) { + mGame.setEndlessMode() + binding.tvTitle.text = HtmlCompat.fromHtml("∞", FROM_HTML_MODE_LEGACY) + } + } + + binding.ibReset.setOnClickListener { + mGame.newGame() + binding.tvTitle.text = HtmlCompat.fromHtml("2048", FROM_HTML_MODE_LEGACY) + } + + binding.ibUndo.setOnClickListener { + mGame.revertUndoState() + if (mGame.gameState === Game.State.ENDLESS || mGame.gameState === Game.State.ENDLESS_WON) { + binding.tvTitle.text = HtmlCompat.fromHtml("∞", FROM_HTML_MODE_LEGACY) + } else { + binding.tvTitle.text = HtmlCompat.fromHtml("2048", FROM_HTML_MODE_LEGACY) + } + } + + binding.ibReset.setOnLongClickListener { + Toast.makeText(activity, getString(R.string.start_new_game), Toast.LENGTH_SHORT).show() + true + } + } + + private fun unlockDarkTheme() { + val enabled = preferences.getBoolean("ach_night_mode_enabled", false) + if (!enabled) { + preferences.edit().putBoolean("ach_night_mode_enabled", true).apply() + darkRepository.getPreconditions() + } + } + + override fun onPause() { + save() + super.onPause() + } + + override fun onResume() { + load() + if (mGame.gameState === Game.State.ENDLESS || mGame.gameState === Game.State.ENDLESS_WON) { + binding.tvTitle.text = HtmlCompat.fromHtml("∞", FROM_HTML_MODE_LEGACY) + } + super.onResume() + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + when (keyCode) { + KeyEvent.KEYCODE_DPAD_DOWN -> { + mGame.move(Game.DIRECTION_DOWN) + return true + } + KeyEvent.KEYCODE_DPAD_UP -> { + mGame.move(Game.DIRECTION_UP) + return true + } + KeyEvent.KEYCODE_DPAD_LEFT -> { + mGame.move(Game.DIRECTION_LEFT) + return true + } + KeyEvent.KEYCODE_DPAD_RIGHT -> { + mGame.move(Game.DIRECTION_RIGHT) + return true + } + else -> return false + } + } + + override fun onGameStateChanged(state: Game.State?) { + Timber.d("Game state changed to: %s", state!!) + if (state == Game.State.WON || state == Game.State.ENDLESS_WON) { + binding.tvEndgameOverlay.visibility = VISIBLE + binding.tvEndgameOverlay.setText(R.string.you_win) + + activity?.unlockAchievement(R.string.achievement_voc__bom) + activity?.incrementAchievementProgress(R.string.achievement_o_campeo_de_2048_no_unes, 1) + } else if (state == Game.State.LOST) { + binding.tvEndgameOverlay.visibility = VISIBLE + binding.tvEndgameOverlay.setText(R.string.game_over) + activity?.unlockAchievement(R.string.achievement_eu_tentei) + activity?.incrementAchievementProgress(R.string.achievement_a_prtica_leva__perfeio, 1) + } else { + binding.tvEndgameOverlay.visibility = GONE + } + } + + private fun save() { + val settings = PreferenceManager.getDefaultSharedPreferences(requireContext()) + val editor = settings.edit() + val field = mGame.gameGrid!!.grid + val undoField = mGame.gameGrid!!.undoGrid + val uuid = mGame.uuid + for (xx in field.indices) { + for (yy in field[0].indices) { + if (field[xx][yy] != null) { + editor.putInt("$xx $yy", field[xx][yy].value) + } else { + editor.putInt("$xx $yy", 0) + } + + if (undoField[xx][yy] != null) { + editor.putInt("$UNDO_GRID$xx $yy", undoField[xx][yy].value) + } else { + editor.putInt("$UNDO_GRID$xx $yy", 0) + } + } + } + editor.putLong(SCORE, mGame.score) + editor.putLong(UNDO_SCORE, mGame.lastScore) + editor.putBoolean(CAN_UNDO, mGame.isCanUndo) + editor.putString(GAME_STATE, mGame.gameState!!.name) + editor.putString(UNDO_GAME_STATE, mGame.lastGameState!!.name) + editor.putString(GAME_UUID, uuid) + editor.apply() + } + + private fun load() { + // Stopping all animations + binding.gameview.cancelAnimations() + val settings = PreferenceManager.getDefaultSharedPreferences(requireContext()) + for (xx in mGame.gameGrid!!.grid.indices) { + for (yy in mGame.gameGrid!!.grid[0].indices) { + val value = settings.getInt("$xx $yy", -1) + if (value > 0) { + mGame.gameGrid!!.grid[xx][yy] = Tile(xx, yy, value) + } else if (value == 0) { + mGame.gameGrid!!.grid[xx][yy] = null + } + + val undoValue = settings.getInt("$UNDO_GRID$xx $yy", -1) + if (undoValue > 0) { + mGame.gameGrid!!.undoGrid[xx][yy] = Tile(xx, yy, undoValue) + } else if (value == 0) { + mGame.gameGrid!!.undoGrid[xx][yy] = null + } + } + } + + mGame.score = settings.getLong(SCORE, 0) + mGame.lastScore = settings.getLong(UNDO_SCORE, 0) + mGame.isCanUndo = settings.getBoolean(CAN_UNDO, mGame.isCanUndo) + mGame.uuid = settings.getString(GAME_UUID, mGame.uuid) ?: mGame.uuid + try { + mGame.updateGameState(Game.State.valueOf(settings.getString(GAME_STATE, Game.State.NORMAL.name)!!)) + } catch (e: Exception) { + mGame.updateGameState(Game.State.NORMAL) + } + + try { + mGame.lastGameState = Game.State.valueOf(settings.getString(UNDO_GAME_STATE, Game.State.NORMAL.name)!!) + } catch (e: Exception) { + mGame.lastGameState = Game.State.NORMAL + } + + mGame.updateUI() + } + + private fun onLeftSwipe() { + mGame.move(Game.DIRECTION_LEFT) + Timber.d("Left Swipe") + } + + private fun onRightSwipe() { + mGame.move(Game.DIRECTION_RIGHT) + Timber.d("Right Swipe") + } + + private fun onDownSwipe() { + mGame.move(Game.DIRECTION_DOWN) + Timber.d("Down Swipe") + } + + private fun onUpSwipe() { + mGame.move(Game.DIRECTION_UP) + Timber.d("Up Swipe") + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(v: View, event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + downX = event.x + downY = event.y + return true + } + MotionEvent.ACTION_UP -> { + upX = event.x + upY = event.y + + val deltaX = downX - upX + val deltaY = downY - upY + + // swipe horizontal? + if (abs(deltaX) > abs(deltaY)) { + if (abs(deltaX) > MIN_DISTANCE) { + // left or right + if (deltaX > 0) { + this.onLeftSwipe() + return true + } + if (deltaX < 0) { + this.onRightSwipe() + return true + } + } else { + return false // We don't consume the event + } + } else { + if (abs(deltaY) > MIN_DISTANCE) { + // top or down + if (deltaY < 0) { + this.onDownSwipe() + return true + } + if (deltaY > 0) { + this.onUpSwipe() + return true + } + } else { + return false // We don't consume the event + } + } // swipe vertical? + + return true + } + } + return false + } + + companion object { + internal const val MIN_DISTANCE = 70 + + private const val SCORE = "savegame.score" + private const val UNDO_SCORE = "savegame.undoscore" + private const val CAN_UNDO = "savegame.canundo" + private const val UNDO_GRID = "savegame.undo" + private const val GAME_STATE = "savegame.gamestate" + private const val UNDO_GAME_STATE = "savegame.undogamestate" + private const val GAME_UUID = "savegame.uuid" + } +} diff --git a/app/src/main/java/com/forcetower/uefs/easter/twofoureight/tools/InputListener.kt b/app/src/main/java/com/forcetower/uefs/easter/twofoureight/tools/InputListener.kt index 1010bf833..ed1964908 100644 --- a/app/src/main/java/com/forcetower/uefs/easter/twofoureight/tools/InputListener.kt +++ b/app/src/main/java/com/forcetower/uefs/easter/twofoureight/tools/InputListener.kt @@ -55,7 +55,6 @@ class InputListener : View.OnTouchListener { @SuppressLint("ClickableViewAccessibility") override fun onTouch(view: View, event: MotionEvent): Boolean { when (event.action) { - MotionEvent.ACTION_DOWN -> { x = event.x y = event.y diff --git a/app/src/main/java/com/forcetower/uefs/easter/twofoureight/view/Game.kt b/app/src/main/java/com/forcetower/uefs/easter/twofoureight/view/Game.kt index e83bb7aed..41cdca070 100644 --- a/app/src/main/java/com/forcetower/uefs/easter/twofoureight/view/Game.kt +++ b/app/src/main/java/com/forcetower/uefs/easter/twofoureight/view/Game.kt @@ -1,365 +1,371 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.easter.twofoureight.view - -import timber.log.Timber -import java.util.UUID -import kotlin.math.pow -import kotlin.random.Random - -/** - * Created by João Paulo on 02/06/2018. - */ -class Game { - private var endingMaxValue: Int = 0 - var gameGrid: GameGrid? = null - private set - var uuid: String = UUID.randomUUID().toString() - private val mPositionsX = DEFAULT_HEIGHT_X - private val mPositionsY = DEFAULT_WIDTH_Y - private val mTileTypes = DEFAULT_TILE_TYPES - private val mStartingTiles = DEFAULT_STARTING_TILES - var isCanUndo: Boolean = false - var lastGameState: State? = null - private var mBufferGameState: State? = null - var gameState: State? = State.NORMAL - private set - private var mView: GameView? = null - private var mScoreListener: ScoreListener? = null - var score: Long = 0 - var lastScore: Long = 0 - private var mBufferScore: Long = 0 - private var mGameStateListener: GameStateListener? = null - - private val isGameWon: Boolean - get() = gameState == State.WON || gameState == State.ENDLESS_WON - - val isGameOnGoing: Boolean - get() = gameState != State.WON && gameState != State.LOST && gameState != State.ENDLESS_WON - - val isEndlessMode: Boolean - get() = gameState == State.ENDLESS || gameState == State.ENDLESS_WON - - private val isMovePossible: Boolean - get() = gameGrid!!.isCellsAvailable || tileMatchesAvailable() - - enum class State { - NORMAL, WON, LOST, ENDLESS, ENDLESS_WON - } - - interface ScoreListener { - fun onNewScore(score: Long) - } - - interface GameStateListener { - fun onGameStateChanged(state: State?) - } - - fun setGameStateListener(listener: GameStateListener) { - this.mGameStateListener = listener - } - - fun setScoreListener(listener: ScoreListener) { - mScoreListener = listener - } - - fun setup(view: GameView) { - mView = view - } - - private fun updateScore(score: Long) { - this.score = score - if (mScoreListener != null) - mScoreListener!!.onNewScore(this.score) - } - - fun newGame() { - uuid = UUID.randomUUID().toString() - if (gameGrid == null) { - gameGrid = GameGrid(mPositionsX, mPositionsY) - } else { - prepareUndoState() - saveUndoState() - gameGrid!!.clearGrid() - } - endingMaxValue = 2.0.pow((mTileTypes - 1).toDouble()).toInt() - mView!!.updateGrid(gameGrid!!) - - updateScore(0) - updateGameState(State.NORMAL) - mView!!.setGameState(gameState!!) - addStartTiles() - mView!!.setRefreshLastTime(true) - mView!!.reSyncTime() - mView!!.invalidate() - } - - private fun addStartTiles() { - for (xx in 0 until mStartingTiles) { - addRandomTile() - } - } - - private fun addRandomTile() { - if (gameGrid!!.isCellsAvailable) { - val value = if (Random.nextDouble() < 0.9) 2 else 4 - val tile = Tile(gameGrid!!.randomAvailableCell()!!, value) - spawnTile(tile) - } - } - - private fun spawnTile(tile: Tile) { - gameGrid!!.insertTile(tile) - mView!!.spawnTile(tile) - } - - private fun prepareTiles() { - for (array in gameGrid!!.grid) { - for (tile in array) { - if (gameGrid!!.isCellOccupied(tile)) { - tile.mergedFrom = null - } - } - } - } - - private fun moveTile(tile: Tile, cell: Position) { - gameGrid!!.grid[tile.x][tile.y] = null - gameGrid!!.grid[cell.x][cell.y] = tile - tile.updatePosition(cell) - } - - private fun saveUndoState() { - gameGrid!!.saveTiles() - isCanUndo = true - lastScore = mBufferScore - lastGameState = mBufferGameState - } - - private fun prepareUndoState() { - gameGrid!!.prepareSaveTiles() - mBufferScore = score - mBufferGameState = gameState - } - - fun revertUndoState() { - if (gameState != State.WON) { - if (isCanUndo) { - isCanUndo = false - mView!!.cancelAnimations() - gameGrid!!.revertTiles() - updateScore(lastScore) - updateGameState(lastGameState) - mView!!.setGameState(gameState!!) - mView!!.setRefreshLastTime(true) - mView!!.invalidate() - } - } else { - Timber.d("Can't revert a won game") - } - } - - fun updateUI() { - updateScore(score) - mView!!.setGameState(gameState!!) - mView!!.setRefreshLastTime(true) - mView!!.invalidate() - } - - fun move(direction: Int) { - mView!!.cancelAnimations() - if (!isGameOnGoing) { - Timber.d("Game is not happening") - return - } - - Timber.d("Game will process your move") - prepareUndoState() - val vector = Position.getVector(direction) - val traversalsX = buildTraversalsX(vector) - val traversalsY = buildTraversalsY(vector) - var moved = false - - prepareTiles() - - for (xx in traversalsX) { - for (yy in traversalsY) { - val cell = Position(xx, yy) - val tile = gameGrid!!.getTile(cell) - - if (tile != null) { - val positions = findFarthestPosition(cell, vector) - val next = gameGrid!!.getTile(positions[1]) - - if (next != null && next.value == tile.value && next.mergedFrom == null) { - val merged = Tile(positions[1], tile.value * 2) - val temp = arrayOf(tile, next) - merged.mergedFrom = temp - - gameGrid!!.insertTile(merged) - gameGrid!!.removeTile(tile) - - // Converge the two tiles' positions - tile.updatePosition(positions[1]) - - val extras = intArrayOf(xx, yy) - // Direction: 0 = MOVING MERGED - mView!!.moveTile(merged.x, merged.y, extras) - mView!!.mergeTile(merged.x, merged.y) - - updateScore(score + merged.value) - - // The mighty 2048 tile - if (merged.value >= winValue() && !isGameWon) { - when (gameState) { - State.ENDLESS -> updateGameState(State.ENDLESS_WON) - State.NORMAL -> updateGameState(State.WON) - else -> throw RuntimeException("Can't move into win state") - } - mView!!.setGameState(gameState!!) - endGame() - } - } else { - moveTile(tile, positions[0]) - val extras = intArrayOf(xx, yy, 0) - // Direction: 1 = MOVING NO MERGE - mView!!.moveTile(positions[0].x, positions[0].y, extras) - } - - if (!Position.equal(cell, tile)) { - moved = true - } - } - } - } - mView!!.updateGrid(gameGrid!!) - if (moved) { - saveUndoState() - addRandomTile() - checkLose() - } - mView!!.reSyncTime() - mView!!.invalidate() - } - - private fun findFarthestPosition(cell: Position, vector: Position): Array { - var previous: Position - var nextCell = Position(cell.x, cell.y) - do { - previous = nextCell - nextCell = Position( - previous.x + vector.x, - previous.y + vector.y - ) - } while (gameGrid!!.isCellWithinBounds(nextCell) && gameGrid!!.isCellAvailable(nextCell)) - return arrayOf(previous, nextCell) - } - - fun updateGameState(state: State?) { - gameState = state - if (mGameStateListener != null) - mGameStateListener!!.onGameStateChanged(gameState) - } - - private fun checkLose() { - if (!isMovePossible && !isGameWon) { - updateGameState(State.LOST) - mView!!.setGameState(gameState!!) - endGame() - } - } - - private fun endGame() { - mView!!.endGame() - updateScore(score) - } - - private fun buildTraversalsX(vector: Position): List { - val traversals = ArrayList() - for (xx in 0 until mPositionsX) { - traversals.add(xx) - } - if (vector.x == 1) { - traversals.reverse() - } - return traversals - } - - private fun buildTraversalsY(vector: Position): List { - val traversals = ArrayList() - for (xx in 0 until mPositionsY) { - traversals.add(xx) - } - if (vector.y == 1) { - traversals.reverse() - } - return traversals - } - - private fun tileMatchesAvailable(): Boolean { - var tile: Tile? - for (xx in 0 until mPositionsX) { - for (yy in 0 until mPositionsY) { - tile = gameGrid!!.getTile(Position(xx, yy)) - if (tile != null) { - for (direction in 0..3) { - val vector = Position.getVector(direction) - val cell = Position(xx + vector.x, yy + vector.y) - val other = gameGrid!!.getTile(cell) - if (other != null && other.value == tile.value) { - return true - } - } - } - } - } - - return false - } - - private fun winValue(): Int { - return if (isEndlessMode) { - endingMaxValue - } else { - startingMaxValue - } - } - - fun setEndlessMode() { - updateGameState(State.ENDLESS) - mView!!.setGameState(gameState!!) - mView!!.invalidate() - mView!!.setRefreshLastTime(true) - } - - companion object { - private const val startingMaxValue = 2048 - internal const val DEFAULT_HEIGHT_X = 4 - internal const val DEFAULT_WIDTH_Y = 4 - internal const val DEFAULT_TILE_TYPES = 24 - private const val DEFAULT_STARTING_TILES = 2 - - const val DIRECTION_UP = 0 - const val DIRECTION_RIGHT = 1 - const val DIRECTION_DOWN = 2 - const val DIRECTION_LEFT = 3 - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.easter.twofoureight.view + +import java.util.UUID +import kotlin.math.pow +import kotlin.random.Random +import timber.log.Timber + +/** + * Created by João Paulo on 02/06/2018. + */ +class Game { + private var endingMaxValue: Int = 0 + var gameGrid: GameGrid? = null + private set + var uuid: String = UUID.randomUUID().toString() + private val mPositionsX = DEFAULT_HEIGHT_X + private val mPositionsY = DEFAULT_WIDTH_Y + private val mTileTypes = DEFAULT_TILE_TYPES + private val mStartingTiles = DEFAULT_STARTING_TILES + var isCanUndo: Boolean = false + var lastGameState: State? = null + private var mBufferGameState: State? = null + var gameState: State? = State.NORMAL + private set + private var mView: GameView? = null + private var mScoreListener: ScoreListener? = null + var score: Long = 0 + var lastScore: Long = 0 + private var mBufferScore: Long = 0 + private var mGameStateListener: GameStateListener? = null + + private val isGameWon: Boolean + get() = gameState == State.WON || gameState == State.ENDLESS_WON + + val isGameOnGoing: Boolean + get() = gameState != State.WON && gameState != State.LOST && gameState != State.ENDLESS_WON + + val isEndlessMode: Boolean + get() = gameState == State.ENDLESS || gameState == State.ENDLESS_WON + + private val isMovePossible: Boolean + get() = gameGrid!!.isCellsAvailable || tileMatchesAvailable() + + enum class State { + NORMAL, + WON, + LOST, + ENDLESS, + ENDLESS_WON + } + + interface ScoreListener { + fun onNewScore(score: Long) + } + + interface GameStateListener { + fun onGameStateChanged(state: State?) + } + + fun setGameStateListener(listener: GameStateListener) { + this.mGameStateListener = listener + } + + fun setScoreListener(listener: ScoreListener) { + mScoreListener = listener + } + + fun setup(view: GameView) { + mView = view + } + + private fun updateScore(score: Long) { + this.score = score + if (mScoreListener != null) { + mScoreListener!!.onNewScore(this.score) + } + } + + fun newGame() { + uuid = UUID.randomUUID().toString() + if (gameGrid == null) { + gameGrid = GameGrid(mPositionsX, mPositionsY) + } else { + prepareUndoState() + saveUndoState() + gameGrid!!.clearGrid() + } + endingMaxValue = 2.0.pow((mTileTypes - 1).toDouble()).toInt() + mView!!.updateGrid(gameGrid!!) + + updateScore(0) + updateGameState(State.NORMAL) + mView!!.setGameState(gameState!!) + addStartTiles() + mView!!.setRefreshLastTime(true) + mView!!.reSyncTime() + mView!!.invalidate() + } + + private fun addStartTiles() { + for (xx in 0 until mStartingTiles) { + addRandomTile() + } + } + + private fun addRandomTile() { + if (gameGrid!!.isCellsAvailable) { + val value = if (Random.nextDouble() < 0.9) 2 else 4 + val tile = Tile(gameGrid!!.randomAvailableCell()!!, value) + spawnTile(tile) + } + } + + private fun spawnTile(tile: Tile) { + gameGrid!!.insertTile(tile) + mView!!.spawnTile(tile) + } + + private fun prepareTiles() { + for (array in gameGrid!!.grid) { + for (tile in array) { + if (gameGrid!!.isCellOccupied(tile)) { + tile.mergedFrom = null + } + } + } + } + + private fun moveTile(tile: Tile, cell: Position) { + gameGrid!!.grid[tile.x][tile.y] = null + gameGrid!!.grid[cell.x][cell.y] = tile + tile.updatePosition(cell) + } + + private fun saveUndoState() { + gameGrid!!.saveTiles() + isCanUndo = true + lastScore = mBufferScore + lastGameState = mBufferGameState + } + + private fun prepareUndoState() { + gameGrid!!.prepareSaveTiles() + mBufferScore = score + mBufferGameState = gameState + } + + fun revertUndoState() { + if (gameState != State.WON) { + if (isCanUndo) { + isCanUndo = false + mView!!.cancelAnimations() + gameGrid!!.revertTiles() + updateScore(lastScore) + updateGameState(lastGameState) + mView!!.setGameState(gameState!!) + mView!!.setRefreshLastTime(true) + mView!!.invalidate() + } + } else { + Timber.d("Can't revert a won game") + } + } + + fun updateUI() { + updateScore(score) + mView!!.setGameState(gameState!!) + mView!!.setRefreshLastTime(true) + mView!!.invalidate() + } + + fun move(direction: Int) { + mView!!.cancelAnimations() + if (!isGameOnGoing) { + Timber.d("Game is not happening") + return + } + + Timber.d("Game will process your move") + prepareUndoState() + val vector = Position.getVector(direction) + val traversalsX = buildTraversalsX(vector) + val traversalsY = buildTraversalsY(vector) + var moved = false + + prepareTiles() + + for (xx in traversalsX) { + for (yy in traversalsY) { + val cell = Position(xx, yy) + val tile = gameGrid!!.getTile(cell) + + if (tile != null) { + val positions = findFarthestPosition(cell, vector) + val next = gameGrid!!.getTile(positions[1]) + + if (next != null && next.value == tile.value && next.mergedFrom == null) { + val merged = Tile(positions[1], tile.value * 2) + val temp = arrayOf(tile, next) + merged.mergedFrom = temp + + gameGrid!!.insertTile(merged) + gameGrid!!.removeTile(tile) + + // Converge the two tiles' positions + tile.updatePosition(positions[1]) + + val extras = intArrayOf(xx, yy) + // Direction: 0 = MOVING MERGED + mView!!.moveTile(merged.x, merged.y, extras) + mView!!.mergeTile(merged.x, merged.y) + + updateScore(score + merged.value) + + // The mighty 2048 tile + if (merged.value >= winValue() && !isGameWon) { + when (gameState) { + State.ENDLESS -> updateGameState(State.ENDLESS_WON) + State.NORMAL -> updateGameState(State.WON) + else -> throw RuntimeException("Can't move into win state") + } + mView!!.setGameState(gameState!!) + endGame() + } + } else { + moveTile(tile, positions[0]) + val extras = intArrayOf(xx, yy, 0) + // Direction: 1 = MOVING NO MERGE + mView!!.moveTile(positions[0].x, positions[0].y, extras) + } + + if (!Position.equal(cell, tile)) { + moved = true + } + } + } + } + mView!!.updateGrid(gameGrid!!) + if (moved) { + saveUndoState() + addRandomTile() + checkLose() + } + mView!!.reSyncTime() + mView!!.invalidate() + } + + private fun findFarthestPosition(cell: Position, vector: Position): Array { + var previous: Position + var nextCell = Position(cell.x, cell.y) + do { + previous = nextCell + nextCell = Position( + previous.x + vector.x, + previous.y + vector.y + ) + } while (gameGrid!!.isCellWithinBounds(nextCell) && gameGrid!!.isCellAvailable(nextCell)) + return arrayOf(previous, nextCell) + } + + fun updateGameState(state: State?) { + gameState = state + if (mGameStateListener != null) { + mGameStateListener!!.onGameStateChanged(gameState) + } + } + + private fun checkLose() { + if (!isMovePossible && !isGameWon) { + updateGameState(State.LOST) + mView!!.setGameState(gameState!!) + endGame() + } + } + + private fun endGame() { + mView!!.endGame() + updateScore(score) + } + + private fun buildTraversalsX(vector: Position): List { + val traversals = ArrayList() + for (xx in 0 until mPositionsX) { + traversals.add(xx) + } + if (vector.x == 1) { + traversals.reverse() + } + return traversals + } + + private fun buildTraversalsY(vector: Position): List { + val traversals = ArrayList() + for (xx in 0 until mPositionsY) { + traversals.add(xx) + } + if (vector.y == 1) { + traversals.reverse() + } + return traversals + } + + private fun tileMatchesAvailable(): Boolean { + var tile: Tile? + for (xx in 0 until mPositionsX) { + for (yy in 0 until mPositionsY) { + tile = gameGrid!!.getTile(Position(xx, yy)) + if (tile != null) { + for (direction in 0..3) { + val vector = Position.getVector(direction) + val cell = Position(xx + vector.x, yy + vector.y) + val other = gameGrid!!.getTile(cell) + if (other != null && other.value == tile.value) { + return true + } + } + } + } + } + + return false + } + + private fun winValue(): Int { + return if (isEndlessMode) { + endingMaxValue + } else { + startingMaxValue + } + } + + fun setEndlessMode() { + updateGameState(State.ENDLESS) + mView!!.setGameState(gameState!!) + mView!!.invalidate() + mView!!.setRefreshLastTime(true) + } + + companion object { + private const val startingMaxValue = 2048 + internal const val DEFAULT_HEIGHT_X = 4 + internal const val DEFAULT_WIDTH_Y = 4 + internal const val DEFAULT_TILE_TYPES = 24 + private const val DEFAULT_STARTING_TILES = 2 + + const val DIRECTION_UP = 0 + const val DIRECTION_RIGHT = 1 + const val DIRECTION_DOWN = 2 + const val DIRECTION_LEFT = 3 + } +} diff --git a/app/src/main/java/com/forcetower/uefs/easter/twofoureight/view/GameView.kt b/app/src/main/java/com/forcetower/uefs/easter/twofoureight/view/GameView.kt index a1d12d749..3e039ae81 100644 --- a/app/src/main/java/com/forcetower/uefs/easter/twofoureight/view/GameView.kt +++ b/app/src/main/java/com/forcetower/uefs/easter/twofoureight/view/GameView.kt @@ -1,453 +1,453 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.easter.twofoureight.view - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.Drawable -import android.util.AttributeSet -import android.view.View -import androidx.core.content.ContextCompat -import com.forcetower.uefs.R -import timber.log.Timber -import kotlin.math.max -import kotlin.math.min -import kotlin.math.pow - -/** - * Created by João Paulo on 02/06/2018. - */ -class GameView : View { - - private val mPaint = Paint() - - // Layout variables - private var mCellSize = 0 - private var mTextSize = 0f - private var mCellTextSize = 0f - private var mGridWidth = 0 - private var mStartingX: Int = 0 - private var mStartingY: Int = 0 - private var mEndingX: Int = 0 - private var mEndingY: Int = 0 - - // Assets - private var mBackgroundRectangle: Drawable? = null - private lateinit var mCellRectangle: Array - private lateinit var mBitmapCell: Array - private var mLightUpRectangle: Drawable? = null - private var mFadeRectangle: Drawable? = null - - private var mLastFPSTime = System.nanoTime() - private var mCurrentTime = System.nanoTime() - - private var mGameOverTextSize: Float = 0.toFloat() - - private var mRefreshLastTime = true - private var mNumberOfSquaresX: Int = 0 - private var mNumberOfSquaresY: Int = 0 - private var mGameState: Game.State? = null - private var mAnimationGrid = AnimationGrid(4, 4) - private var mBackground: Bitmap? = null - private var mGameGrid: GameGrid? = null - - constructor(context: Context) : super(context) { - init() - } - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { - init() - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { - init() - } - - private fun init() { - try { - setSquareCount(Game.DEFAULT_HEIGHT_X, Game.DEFAULT_WIDTH_Y) - mCellRectangle = arrayOfNulls(Game.DEFAULT_TILE_TYPES) - mBitmapCell = arrayOfNulls(Game.DEFAULT_TILE_TYPES) - - updateGrid(GameGrid(4, 4)) - // Getting assets - mBackgroundRectangle = ContextCompat.getDrawable(context, R.drawable.background_rectangle) - mCellRectangle[0] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle) - mCellRectangle[1] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_2) - mCellRectangle[2] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_4) - mCellRectangle[3] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_8) - mCellRectangle[4] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_16) - mCellRectangle[5] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_32) - mCellRectangle[6] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_64) - mCellRectangle[7] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_128) - mCellRectangle[8] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_256) - mCellRectangle[9] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_512) - mCellRectangle[10] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_1024) - mCellRectangle[11] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_2048) - mCellRectangle[12] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_4096) - mCellRectangle[13] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_8192) - mCellRectangle[14] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_16384) - mCellRectangle[15] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_32768) - mCellRectangle[16] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_65536) - mCellRectangle[17] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_131072) - mCellRectangle[18] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_262144) - mCellRectangle[19] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_524288) - for (xx in 20 until mCellRectangle.size) { - mCellRectangle[xx] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_524288) - } - - mLightUpRectangle = ContextCompat.getDrawable(context, R.drawable.light_up_rectangle) - mFadeRectangle = ContextCompat.getDrawable(context, R.drawable.fade_rectangle) - mPaint.isAntiAlias = true - } catch (e: Exception) { - Timber.e("Failed loading assets") - } - } - - fun updateGrid(grid: GameGrid) { - mGameGrid = grid - } - - fun setGameState(state: Game.State) { - mGameState = state - } - - private fun setSquareCount(x: Int, y: Int) { - mNumberOfSquaresX = x - mNumberOfSquaresY = y - mAnimationGrid = AnimationGrid(mNumberOfSquaresX, mNumberOfSquaresY) - } - - override fun onSizeChanged(width: Int, height: Int, oldw: Int, oldh: Int) { - super.onSizeChanged(width, height, oldw, oldh) - getLayout(width, height) - createBackgroundBitmap(width, height) - createBitmapCells() - } - - public override fun onDraw(canvas: Canvas) { - // Reset the transparency of the screen - canvas.drawBitmap(mBackground!!, 0f, 0f, mPaint) - drawTiles(canvas) - - // Refresh the screen if there is still an animation running - if (mAnimationGrid.isAnimationActive) { - // invalidate(mStartingX, mStartingY, mEndingX, mEndingY) - invalidate() - tick() - // Refresh one last time on game end. - } else if (!(mGameState != Game.State.WON && mGameState != Game.State.LOST) && mRefreshLastTime) { - invalidate() - mRefreshLastTime = false - } - } - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - val widthMode = MeasureSpec.getMode(widthMeasureSpec) - val widthSize = MeasureSpec.getSize(widthMeasureSpec) - val heightMode = MeasureSpec.getMode(heightMeasureSpec) - val heightSize = MeasureSpec.getSize(heightMeasureSpec) - - val size: Int - size = if (widthMode == MeasureSpec.EXACTLY && widthSize > 0) { - widthSize - } else if (heightMode == MeasureSpec.EXACTLY && heightSize > 0) { - heightSize - } else { - if (widthSize < heightSize) widthSize else heightSize - } - val finalMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY) - super.onMeasure(finalMeasureSpec, finalMeasureSpec) - } - - private fun getLayout(width: Int, height: Int) { - mCellSize = min(width / (mNumberOfSquaresX + 1), height / (mNumberOfSquaresY + 1)) - mGridWidth = mCellSize / 5 - val boardMiddleX = width / 2 - val boardMiddleY = height / 2 - - mPaint.textAlign = Paint.Align.CENTER - mPaint.textSize = mCellSize.toFloat() - mTextSize = mCellSize * mCellSize / max(mCellSize.toFloat(), mPaint.measureText("0000")) - mCellTextSize = mTextSize - - mGameOverTextSize = mTextSize * 2 - - // Grid Dimensions - val halfNumSquaresX = mNumberOfSquaresX / 2.0 - val halfNumSquaresY = mNumberOfSquaresY / 2.0 - - mStartingX = - (boardMiddleX.toDouble() - (mCellSize + mGridWidth) * halfNumSquaresX - (mGridWidth / 2).toDouble()).toInt() - mEndingX = - (boardMiddleX.toDouble() + (mCellSize + mGridWidth) * halfNumSquaresX + (mGridWidth / 2).toDouble()).toInt() - mStartingY = - (boardMiddleY.toDouble() - (mCellSize + mGridWidth) * halfNumSquaresY - (mGridWidth / 2).toDouble()).toInt() - mEndingY = - (boardMiddleY.toDouble() + (mCellSize + mGridWidth) * halfNumSquaresY + (mGridWidth / 2).toDouble()).toInt() - reSyncTime() - } - - private fun drawDrawable( - canvas: Canvas, - draw: Drawable, - startingX: Int, - startingY: Int, - endingX: Int, - endingY: Int - ) { - draw.setBounds(startingX, startingY, endingX, endingY) - draw.draw(canvas) - } - - private fun createBackgroundBitmap(width: Int, height: Int) { - mBackground = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) - val canvas = Canvas(mBackground!!) - drawTileBackground(canvas) - drawEmptyTiles(canvas) - } - - private fun drawTileBackground(canvas: Canvas) { - drawDrawable(canvas, mBackgroundRectangle!!, mStartingX, mStartingY, mEndingX, mEndingY) - } - - private fun drawEmptyTiles(canvas: Canvas) { - // Outputting the game mGameGrid - for (xx in 0 until mNumberOfSquaresX) { - for (yy in 0 until mNumberOfSquaresY) { - val sX = mStartingX + mGridWidth + (mCellSize + mGridWidth) * xx - val eX = sX + mCellSize - val sY = mStartingY + mGridWidth + (mCellSize + mGridWidth) * yy - val eY = sY + mCellSize - drawDrawable(canvas, mCellRectangle[0]!!, sX, sY, eX, eY) - } - } - } - - private fun drawTiles(canvas: Canvas) { - mPaint.textSize = mTextSize - mPaint.textAlign = Paint.Align.CENTER - // Outputting the individual cells - for (xx in 0 until mNumberOfSquaresX) { - for (yy in 0 until mNumberOfSquaresY) { - val sX = mStartingX + mGridWidth + (mCellSize + mGridWidth) * xx - val eX = sX + mCellSize - val sY = mStartingY + mGridWidth + (mCellSize + mGridWidth) * yy - val eY = sY + mCellSize - - val currentTile = mGameGrid!!.getCellContent(xx, yy) - if (currentTile != null) { - // Get and represent the value of the tile - val value = currentTile.value - val index = log2(value) - - // Check for any active animations - val aArray = mAnimationGrid.getAnimationCell(xx, yy) - var animated = false - for (i in aArray.indices.reversed()) { - val aCell = aArray[i] - // If this animation is not active, skip it - if (aCell.animationType == SPAWN_ANIMATION) { - animated = true - } - if (!aCell.isActive) { - continue - } - - when (aCell.animationType) { - SPAWN_ANIMATION -> { // Spawning animation - val percentDone = aCell.percentageDone - val textScaleSize = percentDone.toFloat() - mPaint.textSize = mTextSize * textScaleSize - - val cellScaleSize = mCellSize / 2 * (1 - textScaleSize) - mBitmapCell[index]!!.setBounds( - (sX + cellScaleSize).toInt(), - (sY + cellScaleSize).toInt(), - (eX - cellScaleSize).toInt(), - (eY - cellScaleSize).toInt() - ) - mBitmapCell[index]!!.draw(canvas) - } - MERGE_ANIMATION -> { // Merging Animation - val percentDone = aCell.percentageDone - val textScaleSize = ( - 1.0 + INITIAL_VELOCITY * percentDone + - MERGING_ACCELERATION.toDouble() * percentDone * percentDone / 2 - ).toFloat() - mPaint.textSize = mTextSize * textScaleSize - - val cellScaleSize = mCellSize / 2 * (1 - textScaleSize) - mBitmapCell[index]!!.setBounds( - (sX + cellScaleSize).toInt(), - (sY + cellScaleSize).toInt(), - (eX - cellScaleSize).toInt(), - (eY - cellScaleSize).toInt() - ) - mBitmapCell[index]!!.draw(canvas) - } - MOVE_ANIMATION -> { // Moving animation - val percentDone = aCell.percentageDone - var tempIndex = index - if (aArray.size >= 2) { - tempIndex -= 1 - } - val previousX = aCell.extras!![0] - val previousY = aCell.extras[1] - val currentX = currentTile.x - val currentY = currentTile.y - val dX = - ((currentX - previousX).toDouble() * (mCellSize + mGridWidth).toDouble() * (percentDone - 1) * 1.0).toInt() - val dY = - ((currentY - previousY).toDouble() * (mCellSize + mGridWidth).toDouble() * (percentDone - 1) * 1.0).toInt() - mBitmapCell[tempIndex]!!.setBounds(sX + dX, sY + dY, eX + dX, eY + dY) - mBitmapCell[tempIndex]!!.draw(canvas) - } - } - animated = true - } - - // No active animations? Just draw the cell - if (!animated) { - mBitmapCell[index]!!.setBounds(sX, sY, eX, eY) - mBitmapCell[index]!!.draw(canvas) - } - } - } - } - } - - private fun createBitmapCells() { - mPaint.textAlign = Paint.Align.CENTER - for (xx in mBitmapCell.indices) { - val value = 2.0.pow(xx.toDouble()).toInt() - mPaint.textSize = mCellTextSize - val tempTextSize = mCellTextSize * mCellSize.toFloat() * 0.9f / max( - mCellSize * 0.9f, - mPaint.measureText(value.toString()) - ) - mPaint.textSize = tempTextSize - val bitmap = Bitmap.createBitmap(mCellSize, mCellSize, Bitmap.Config.ARGB_8888) - val canvas = Canvas(bitmap) - drawDrawable(canvas, mCellRectangle[xx]!!, 0, 0, mCellSize, mCellSize) - drawTileText(canvas, value, 0, 0) - mBitmapCell[xx] = BitmapDrawable(resources, bitmap) - } - } - - private fun drawTileText(canvas: Canvas, value: Int, sX: Int, sY: Int) { - val textShiftY = centerText() - if (value == 2) { - mPaint.color = ContextCompat.getColor(context, R.color.text_shadow) - mPaint.setShadowLayer(2.0f, 0f, 0f, ContextCompat.getColor(context, R.color.text_white)) - } else { - mPaint.color = ContextCompat.getColor(context, R.color.text_white) - mPaint.setShadowLayer(2.0f, 0f, 0f, ContextCompat.getColor(context, R.color.text_shadow)) - } - canvas.drawText("" + value, (sX + mCellSize / 2).toFloat(), (sY + mCellSize / 2 - textShiftY).toFloat(), mPaint) - } - - private fun tick() { - mCurrentTime = System.nanoTime() - mAnimationGrid.tickAll(mCurrentTime - mLastFPSTime) - mLastFPSTime = mCurrentTime - } - - override fun performClick(): Boolean { - Timber.d("Performed a click in the game") - return super.performClick() - } - - fun reSyncTime() { - mLastFPSTime = System.nanoTime() - } - - private fun centerText(): Int { - return ((mPaint.descent() + mPaint.ascent()) / 2).toInt() - } - - fun spawnTile(tile: Tile) { - mAnimationGrid.startAnimation( - tile.x, - tile.y, - SPAWN_ANIMATION, - SPAWN_ANIMATION_TIME, - MOVE_ANIMATION_TIME, - null - ) // Direction: -1 = EXPANDING - } - - fun cancelAnimations() { - mAnimationGrid.cancelAnimations() - } - - fun moveTile(x: Int, y: Int, extras: IntArray) { - mAnimationGrid.startAnimation(x, y, MOVE_ANIMATION, MOVE_ANIMATION_TIME, 0, extras) - } - - fun mergeTile(x: Int, y: Int) { - mAnimationGrid.startAnimation( - x, - y, - MERGE_ANIMATION, - SPAWN_ANIMATION_TIME, - MOVE_ANIMATION_TIME, - null - ) - } - - fun endGame() { - mAnimationGrid.startAnimation( - -1, - -1, - FADE_GLOBAL_ANIMATION, - NOTIFICATION_ANIMATION_TIME, - NOTIFICATION_DELAY_TIME, - null - ) - } - - fun setRefreshLastTime(refreshLastTime: Boolean) { - mRefreshLastTime = refreshLastTime - } - - companion object { - private const val BASE_ANIMATION_TIME = 100000000 - private const val MERGING_ACCELERATION = (-0.5).toFloat() - private const val INITIAL_VELOCITY = (1 - MERGING_ACCELERATION) / 4 - private const val SPAWN_ANIMATION = -1 - private const val MOVE_ANIMATION = 0 - private const val MERGE_ANIMATION = 1 - private const val FADE_GLOBAL_ANIMATION = 0 - private const val MOVE_ANIMATION_TIME = BASE_ANIMATION_TIME.toLong() - private const val SPAWN_ANIMATION_TIME = BASE_ANIMATION_TIME.toLong() - private const val NOTIFICATION_ANIMATION_TIME = (BASE_ANIMATION_TIME * 5).toLong() - private const val NOTIFICATION_DELAY_TIME = MOVE_ANIMATION_TIME + SPAWN_ANIMATION_TIME - - private fun log2(n: Int): Int { - require(n > 0) - return 31 - Integer.numberOfLeadingZeros(n) - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.easter.twofoureight.view + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.View +import androidx.core.content.ContextCompat +import com.forcetower.uefs.R +import kotlin.math.max +import kotlin.math.min +import kotlin.math.pow +import timber.log.Timber + +/** + * Created by João Paulo on 02/06/2018. + */ +class GameView : View { + + private val mPaint = Paint() + + // Layout variables + private var mCellSize = 0 + private var mTextSize = 0f + private var mCellTextSize = 0f + private var mGridWidth = 0 + private var mStartingX: Int = 0 + private var mStartingY: Int = 0 + private var mEndingX: Int = 0 + private var mEndingY: Int = 0 + + // Assets + private var mBackgroundRectangle: Drawable? = null + private lateinit var mCellRectangle: Array + private lateinit var mBitmapCell: Array + private var mLightUpRectangle: Drawable? = null + private var mFadeRectangle: Drawable? = null + + private var mLastFPSTime = System.nanoTime() + private var mCurrentTime = System.nanoTime() + + private var mGameOverTextSize: Float = 0.toFloat() + + private var mRefreshLastTime = true + private var mNumberOfSquaresX: Int = 0 + private var mNumberOfSquaresY: Int = 0 + private var mGameState: Game.State? = null + private var mAnimationGrid = AnimationGrid(4, 4) + private var mBackground: Bitmap? = null + private var mGameGrid: GameGrid? = null + + constructor(context: Context) : super(context) { + init() + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + init() + } + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + init() + } + + private fun init() { + try { + setSquareCount(Game.DEFAULT_HEIGHT_X, Game.DEFAULT_WIDTH_Y) + mCellRectangle = arrayOfNulls(Game.DEFAULT_TILE_TYPES) + mBitmapCell = arrayOfNulls(Game.DEFAULT_TILE_TYPES) + + updateGrid(GameGrid(4, 4)) + // Getting assets + mBackgroundRectangle = ContextCompat.getDrawable(context, R.drawable.background_rectangle) + mCellRectangle[0] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle) + mCellRectangle[1] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_2) + mCellRectangle[2] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_4) + mCellRectangle[3] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_8) + mCellRectangle[4] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_16) + mCellRectangle[5] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_32) + mCellRectangle[6] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_64) + mCellRectangle[7] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_128) + mCellRectangle[8] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_256) + mCellRectangle[9] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_512) + mCellRectangle[10] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_1024) + mCellRectangle[11] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_2048) + mCellRectangle[12] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_4096) + mCellRectangle[13] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_8192) + mCellRectangle[14] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_16384) + mCellRectangle[15] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_32768) + mCellRectangle[16] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_65536) + mCellRectangle[17] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_131072) + mCellRectangle[18] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_262144) + mCellRectangle[19] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_524288) + for (xx in 20 until mCellRectangle.size) { + mCellRectangle[xx] = ContextCompat.getDrawable(context, R.drawable.cell_rectangle_524288) + } + + mLightUpRectangle = ContextCompat.getDrawable(context, R.drawable.light_up_rectangle) + mFadeRectangle = ContextCompat.getDrawable(context, R.drawable.fade_rectangle) + mPaint.isAntiAlias = true + } catch (e: Exception) { + Timber.e("Failed loading assets") + } + } + + fun updateGrid(grid: GameGrid) { + mGameGrid = grid + } + + fun setGameState(state: Game.State) { + mGameState = state + } + + private fun setSquareCount(x: Int, y: Int) { + mNumberOfSquaresX = x + mNumberOfSquaresY = y + mAnimationGrid = AnimationGrid(mNumberOfSquaresX, mNumberOfSquaresY) + } + + override fun onSizeChanged(width: Int, height: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(width, height, oldw, oldh) + getLayout(width, height) + createBackgroundBitmap(width, height) + createBitmapCells() + } + + public override fun onDraw(canvas: Canvas) { + // Reset the transparency of the screen + canvas.drawBitmap(mBackground!!, 0f, 0f, mPaint) + drawTiles(canvas) + + // Refresh the screen if there is still an animation running + if (mAnimationGrid.isAnimationActive) { + // invalidate(mStartingX, mStartingY, mEndingX, mEndingY) + invalidate() + tick() + // Refresh one last time on game end. + } else if (!(mGameState != Game.State.WON && mGameState != Game.State.LOST) && mRefreshLastTime) { + invalidate() + mRefreshLastTime = false + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + + val size: Int + size = if (widthMode == MeasureSpec.EXACTLY && widthSize > 0) { + widthSize + } else if (heightMode == MeasureSpec.EXACTLY && heightSize > 0) { + heightSize + } else { + if (widthSize < heightSize) widthSize else heightSize + } + val finalMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY) + super.onMeasure(finalMeasureSpec, finalMeasureSpec) + } + + private fun getLayout(width: Int, height: Int) { + mCellSize = min(width / (mNumberOfSquaresX + 1), height / (mNumberOfSquaresY + 1)) + mGridWidth = mCellSize / 5 + val boardMiddleX = width / 2 + val boardMiddleY = height / 2 + + mPaint.textAlign = Paint.Align.CENTER + mPaint.textSize = mCellSize.toFloat() + mTextSize = mCellSize * mCellSize / max(mCellSize.toFloat(), mPaint.measureText("0000")) + mCellTextSize = mTextSize + + mGameOverTextSize = mTextSize * 2 + + // Grid Dimensions + val halfNumSquaresX = mNumberOfSquaresX / 2.0 + val halfNumSquaresY = mNumberOfSquaresY / 2.0 + + mStartingX = + (boardMiddleX.toDouble() - (mCellSize + mGridWidth) * halfNumSquaresX - (mGridWidth / 2).toDouble()).toInt() + mEndingX = + (boardMiddleX.toDouble() + (mCellSize + mGridWidth) * halfNumSquaresX + (mGridWidth / 2).toDouble()).toInt() + mStartingY = + (boardMiddleY.toDouble() - (mCellSize + mGridWidth) * halfNumSquaresY - (mGridWidth / 2).toDouble()).toInt() + mEndingY = + (boardMiddleY.toDouble() + (mCellSize + mGridWidth) * halfNumSquaresY + (mGridWidth / 2).toDouble()).toInt() + reSyncTime() + } + + private fun drawDrawable( + canvas: Canvas, + draw: Drawable, + startingX: Int, + startingY: Int, + endingX: Int, + endingY: Int + ) { + draw.setBounds(startingX, startingY, endingX, endingY) + draw.draw(canvas) + } + + private fun createBackgroundBitmap(width: Int, height: Int) { + mBackground = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(mBackground!!) + drawTileBackground(canvas) + drawEmptyTiles(canvas) + } + + private fun drawTileBackground(canvas: Canvas) { + drawDrawable(canvas, mBackgroundRectangle!!, mStartingX, mStartingY, mEndingX, mEndingY) + } + + private fun drawEmptyTiles(canvas: Canvas) { + // Outputting the game mGameGrid + for (xx in 0 until mNumberOfSquaresX) { + for (yy in 0 until mNumberOfSquaresY) { + val sX = mStartingX + mGridWidth + (mCellSize + mGridWidth) * xx + val eX = sX + mCellSize + val sY = mStartingY + mGridWidth + (mCellSize + mGridWidth) * yy + val eY = sY + mCellSize + drawDrawable(canvas, mCellRectangle[0]!!, sX, sY, eX, eY) + } + } + } + + private fun drawTiles(canvas: Canvas) { + mPaint.textSize = mTextSize + mPaint.textAlign = Paint.Align.CENTER + // Outputting the individual cells + for (xx in 0 until mNumberOfSquaresX) { + for (yy in 0 until mNumberOfSquaresY) { + val sX = mStartingX + mGridWidth + (mCellSize + mGridWidth) * xx + val eX = sX + mCellSize + val sY = mStartingY + mGridWidth + (mCellSize + mGridWidth) * yy + val eY = sY + mCellSize + + val currentTile = mGameGrid!!.getCellContent(xx, yy) + if (currentTile != null) { + // Get and represent the value of the tile + val value = currentTile.value + val index = log2(value) + + // Check for any active animations + val aArray = mAnimationGrid.getAnimationCell(xx, yy) + var animated = false + for (i in aArray.indices.reversed()) { + val aCell = aArray[i] + // If this animation is not active, skip it + if (aCell.animationType == SPAWN_ANIMATION) { + animated = true + } + if (!aCell.isActive) { + continue + } + + when (aCell.animationType) { + SPAWN_ANIMATION -> { // Spawning animation + val percentDone = aCell.percentageDone + val textScaleSize = percentDone.toFloat() + mPaint.textSize = mTextSize * textScaleSize + + val cellScaleSize = mCellSize / 2 * (1 - textScaleSize) + mBitmapCell[index]!!.setBounds( + (sX + cellScaleSize).toInt(), + (sY + cellScaleSize).toInt(), + (eX - cellScaleSize).toInt(), + (eY - cellScaleSize).toInt() + ) + mBitmapCell[index]!!.draw(canvas) + } + MERGE_ANIMATION -> { // Merging Animation + val percentDone = aCell.percentageDone + val textScaleSize = ( + 1.0 + INITIAL_VELOCITY * percentDone + + MERGING_ACCELERATION.toDouble() * percentDone * percentDone / 2 + ).toFloat() + mPaint.textSize = mTextSize * textScaleSize + + val cellScaleSize = mCellSize / 2 * (1 - textScaleSize) + mBitmapCell[index]!!.setBounds( + (sX + cellScaleSize).toInt(), + (sY + cellScaleSize).toInt(), + (eX - cellScaleSize).toInt(), + (eY - cellScaleSize).toInt() + ) + mBitmapCell[index]!!.draw(canvas) + } + MOVE_ANIMATION -> { // Moving animation + val percentDone = aCell.percentageDone + var tempIndex = index + if (aArray.size >= 2) { + tempIndex -= 1 + } + val previousX = aCell.extras!![0] + val previousY = aCell.extras[1] + val currentX = currentTile.x + val currentY = currentTile.y + val dX = + ((currentX - previousX).toDouble() * (mCellSize + mGridWidth).toDouble() * (percentDone - 1) * 1.0).toInt() + val dY = + ((currentY - previousY).toDouble() * (mCellSize + mGridWidth).toDouble() * (percentDone - 1) * 1.0).toInt() + mBitmapCell[tempIndex]!!.setBounds(sX + dX, sY + dY, eX + dX, eY + dY) + mBitmapCell[tempIndex]!!.draw(canvas) + } + } + animated = true + } + + // No active animations? Just draw the cell + if (!animated) { + mBitmapCell[index]!!.setBounds(sX, sY, eX, eY) + mBitmapCell[index]!!.draw(canvas) + } + } + } + } + } + + private fun createBitmapCells() { + mPaint.textAlign = Paint.Align.CENTER + for (xx in mBitmapCell.indices) { + val value = 2.0.pow(xx.toDouble()).toInt() + mPaint.textSize = mCellTextSize + val tempTextSize = mCellTextSize * mCellSize.toFloat() * 0.9f / max( + mCellSize * 0.9f, + mPaint.measureText(value.toString()) + ) + mPaint.textSize = tempTextSize + val bitmap = Bitmap.createBitmap(mCellSize, mCellSize, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + drawDrawable(canvas, mCellRectangle[xx]!!, 0, 0, mCellSize, mCellSize) + drawTileText(canvas, value, 0, 0) + mBitmapCell[xx] = BitmapDrawable(resources, bitmap) + } + } + + private fun drawTileText(canvas: Canvas, value: Int, sX: Int, sY: Int) { + val textShiftY = centerText() + if (value == 2) { + mPaint.color = ContextCompat.getColor(context, R.color.text_shadow) + mPaint.setShadowLayer(2.0f, 0f, 0f, ContextCompat.getColor(context, R.color.text_white)) + } else { + mPaint.color = ContextCompat.getColor(context, R.color.text_white) + mPaint.setShadowLayer(2.0f, 0f, 0f, ContextCompat.getColor(context, R.color.text_shadow)) + } + canvas.drawText("" + value, (sX + mCellSize / 2).toFloat(), (sY + mCellSize / 2 - textShiftY).toFloat(), mPaint) + } + + private fun tick() { + mCurrentTime = System.nanoTime() + mAnimationGrid.tickAll(mCurrentTime - mLastFPSTime) + mLastFPSTime = mCurrentTime + } + + override fun performClick(): Boolean { + Timber.d("Performed a click in the game") + return super.performClick() + } + + fun reSyncTime() { + mLastFPSTime = System.nanoTime() + } + + private fun centerText(): Int { + return ((mPaint.descent() + mPaint.ascent()) / 2).toInt() + } + + fun spawnTile(tile: Tile) { + mAnimationGrid.startAnimation( + tile.x, + tile.y, + SPAWN_ANIMATION, + SPAWN_ANIMATION_TIME, + MOVE_ANIMATION_TIME, + null + ) // Direction: -1 = EXPANDING + } + + fun cancelAnimations() { + mAnimationGrid.cancelAnimations() + } + + fun moveTile(x: Int, y: Int, extras: IntArray) { + mAnimationGrid.startAnimation(x, y, MOVE_ANIMATION, MOVE_ANIMATION_TIME, 0, extras) + } + + fun mergeTile(x: Int, y: Int) { + mAnimationGrid.startAnimation( + x, + y, + MERGE_ANIMATION, + SPAWN_ANIMATION_TIME, + MOVE_ANIMATION_TIME, + null + ) + } + + fun endGame() { + mAnimationGrid.startAnimation( + -1, + -1, + FADE_GLOBAL_ANIMATION, + NOTIFICATION_ANIMATION_TIME, + NOTIFICATION_DELAY_TIME, + null + ) + } + + fun setRefreshLastTime(refreshLastTime: Boolean) { + mRefreshLastTime = refreshLastTime + } + + companion object { + private const val BASE_ANIMATION_TIME = 100000000 + private const val MERGING_ACCELERATION = (-0.5).toFloat() + private const val INITIAL_VELOCITY = (1 - MERGING_ACCELERATION) / 4 + private const val SPAWN_ANIMATION = -1 + private const val MOVE_ANIMATION = 0 + private const val MERGE_ANIMATION = 1 + private const val FADE_GLOBAL_ANIMATION = 0 + private const val MOVE_ANIMATION_TIME = BASE_ANIMATION_TIME.toLong() + private const val SPAWN_ANIMATION_TIME = BASE_ANIMATION_TIME.toLong() + private const val NOTIFICATION_ANIMATION_TIME = (BASE_ANIMATION_TIME * 5).toLong() + private const val NOTIFICATION_DELAY_TIME = MOVE_ANIMATION_TIME + SPAWN_ANIMATION_TIME + + private fun log2(n: Int): Int { + require(n > 0) + return 31 - Integer.numberOfLeadingZeros(n) + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/about/AboutMeFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/about/AboutMeFragment.kt index 7b2203458..9c27b31c6 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/about/AboutMeFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/about/AboutMeFragment.kt @@ -20,7 +20,6 @@ package com.forcetower.uefs.feature.about -import `in`.uncod.android.bypass.Bypass import android.os.Bundle import android.text.Layout import android.text.SpannableString @@ -38,6 +37,7 @@ import com.forcetower.uefs.core.util.HtmlUtils import com.forcetower.uefs.databinding.FragmentAboutMeBinding import com.forcetower.uefs.feature.shared.UFragment import dagger.hilt.android.AndroidEntryPoint +import `in`.uncod.android.bypass.Bypass @AndroidEntryPoint class AboutMeFragment : UFragment() { diff --git a/app/src/main/java/com/forcetower/uefs/feature/adventure/AdventureFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/adventure/AdventureFragment.kt index 6f3a03013..cd544581c 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/adventure/AdventureFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/adventure/AdventureFragment.kt @@ -1,271 +1,271 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.adventure - -import android.Manifest -import android.content.Context -import android.content.SharedPreferences -import android.content.pm.PackageManager -import android.location.Location -import android.os.Bundle -import android.os.Looper -import android.view.LayoutInflater -import android.view.View -import android.view.View.GONE -import android.view.View.VISIBLE -import android.view.ViewGroup -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat -import androidx.fragment.app.DialogFragment -import androidx.fragment.app.activityViewModels -import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope -import com.forcetower.core.lifecycle.EventObserver -import com.forcetower.uefs.GameConnectionStatus -import com.forcetower.uefs.R -import com.forcetower.uefs.REQUEST_CHECK_SETTINGS -import com.forcetower.uefs.core.model.service.AchDistance -import com.forcetower.uefs.databinding.FragmentAdventureBeginsBinding -import com.forcetower.uefs.feature.profile.ProfileViewModel -import com.forcetower.uefs.feature.shared.UFragment -import com.forcetower.uefs.feature.shared.UGameActivity -import com.google.android.gms.common.api.ResolvableApiException -import com.google.android.gms.location.FusedLocationProviderClient -import com.google.android.gms.location.LocationCallback -import com.google.android.gms.location.LocationRequest -import com.google.android.gms.location.LocationResult -import com.google.android.gms.location.LocationServices -import com.google.android.gms.location.LocationSettingsRequest -import com.google.android.gms.location.Priority -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -@AndroidEntryPoint -class AdventureFragment : UFragment() { - @Inject lateinit var preferences: SharedPreferences - - private val viewModel: AdventureViewModel by activityViewModels() - private val profileViewModel: ProfileViewModel by viewModels() - private var activity: UGameActivity? = null - private lateinit var binding: FragmentAdventureBeginsBinding - - private val distanceAdapter by lazy { DistanceAdapter() } - - private lateinit var fusedLocationClient: FusedLocationProviderClient - private lateinit var locationCallback: LocationCallback - private lateinit var mLocationRequest: LocationRequest - private var showedLocationMessage: Boolean = false - private var requestingLocationUpdates = false - - private var currentList: List? = null - - private val requestPermissions = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> - val allGranted = result.size > 1 && result.entries.all { it.value } - if (allGranted) startRequesting() - } - - override fun onAttach(context: Context) { - super.onAttach(context) - activity = context as? UGameActivity - activity ?: Timber.e("Adventure Fragment must be attached to a UGameActivity for it to work") - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - locationSettings() - return FragmentAdventureBeginsBinding.inflate(inflater, container, false).also { - binding = it - }.apply { - interactor = viewModel - profile = profileViewModel - lifecycleOwner = this@AdventureFragment - executePendingBindings() - }.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - binding.adventureAchievements.distanceRecycler.run { - adapter = distanceAdapter - } - - profileViewModel.getMeProfile().observe( - viewLifecycleOwner - ) { - if (it != null) { - profileViewModel.setProfileId(it.data?.id) - } - } - - viewModel.run { - achievements.observe(viewLifecycleOwner, EventObserver { onOpenAchievements() }) - start.observe(viewLifecycleOwner, EventObserver { activity?.signIn() }) - locations.observe(viewLifecycleOwner) { requestLocations(it) } - } - - lifecycleScope.launch { - if (activity?.isConnectedToPlayGames() == false && savedInstanceState == null) { - openStartupDialog() - } - } - - activity?.mGamesInstance?.connectionStatus?.observe( - viewLifecycleOwner, - EventObserver { - when (it) { - GameConnectionStatus.DISCONNECTED -> openStartupDialog() - GameConnectionStatus.CONNECTED -> { - val fragment = childFragmentManager.findFragmentByTag("adventure_sign_in") - (fragment as? DialogFragment)?.dismiss() - showSnack(getString(R.string.connected_to_play_games)) - } - GameConnectionStatus.LOADING -> Unit - } - } - ) - } - - private fun onOpenAchievements() { - lifecycleScope.launch { - activity?.openAchievements() - } - } - - override fun onPause() { - super.onPause() - stopUpdates() - } - - override fun onResume() { - super.onResume() - if (requestingLocationUpdates) startUpdates() - } - - private fun openStartupDialog() { - val dialog = AdventureSignInDialog() - dialog.show(childFragmentManager, "adventure_sign_in") - } - - private fun requestLocations(request: Boolean) { - if (request) { - startRequesting() - } else { - stopUpdates() - } - } - - private fun startRequesting() { - val perms = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) - val permissionsGranted = perms.all { - ContextCompat.checkSelfPermission(requireContext(), it) == PackageManager.PERMISSION_GRANTED - } - - if (permissionsGranted) { - startLocationsUpdate() - } else { - requestPermissions.launch(perms) - } - } - - private fun startLocationsUpdate() { - mLocationRequest = LocationRequest.Builder(7000) - .setWaitForAccurateLocation(true) - .setMinUpdateIntervalMillis(5000) - .setPriority(Priority.PRIORITY_HIGH_ACCURACY) - .build() - - if (currentList == null) { - onReceiveLocation(null) - } - - val builder = LocationSettingsRequest.Builder().addLocationRequest(mLocationRequest) - val client = LocationServices.getSettingsClient(requireContext()) - val task = client.checkLocationSettings(builder.build()) - - task.addOnCompleteListener(requireActivity()) { - Timber.d("Can make location request") - - try { - startUpdates() - } catch (e: SecurityException) { - Timber.e("What??? How did this happen?") - } - }.addOnFailureListener(requireActivity()) { fail -> - if (fail is ResolvableApiException) { - try { - fail.startResolutionForResult(requireActivity(), REQUEST_CHECK_SETTINGS) - } catch (e: Exception) { - Timber.d("Ignored exception") - e.printStackTrace() - showSnack(getString(R.string.cant_receive_location)) - } - } else { - Timber.d("Unresolvable Exception") - fail.printStackTrace() - showSnack(getString(R.string.cant_receive_location)) - } - } - } - - private fun startUpdates() { - try { - if (!::mLocationRequest.isInitialized) startLocationsUpdate() - fusedLocationClient.requestLocationUpdates(mLocationRequest, locationCallback, Looper.getMainLooper()) - binding.adventureAchievements.distanceRecycler.visibility = VISIBLE - } catch (e: SecurityException) { - Timber.d("Method could not be called") - } - } - - private fun stopUpdates() { - fusedLocationClient.removeLocationUpdates(locationCallback) - binding.adventureAchievements.distanceRecycler.visibility = GONE - } - - private fun locationSettings() { - fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext()) - locationCallback = object : LocationCallback() { - override fun onLocationResult(result: LocationResult) { - result.locations.forEach { location -> - if (location.accuracy >= 100) { - if (!showedLocationMessage) { - if (context != null) { - showSnack(getString(R.string.adventure_not_accurate)) - showedLocationMessage = true - } - } - } else { - onReceiveLocation(location) - } - } - } - } - } - - private fun onReceiveLocation(location: Location?) { - val value = viewModel.onReceiveLocation(location) - currentList = value - distanceAdapter.submitList(value) - value.mapNotNull { it.id }.forEach { - activity?.unlockAchievement(it) - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.adventure + +import android.Manifest +import android.content.Context +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.location.Location +import android.os.Bundle +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import com.forcetower.core.lifecycle.EventObserver +import com.forcetower.uefs.GameConnectionStatus +import com.forcetower.uefs.R +import com.forcetower.uefs.REQUEST_CHECK_SETTINGS +import com.forcetower.uefs.core.model.service.AchDistance +import com.forcetower.uefs.databinding.FragmentAdventureBeginsBinding +import com.forcetower.uefs.feature.profile.ProfileViewModel +import com.forcetower.uefs.feature.shared.UFragment +import com.forcetower.uefs.feature.shared.UGameActivity +import com.google.android.gms.common.api.ResolvableApiException +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationCallback +import com.google.android.gms.location.LocationRequest +import com.google.android.gms.location.LocationResult +import com.google.android.gms.location.LocationServices +import com.google.android.gms.location.LocationSettingsRequest +import com.google.android.gms.location.Priority +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.launch +import timber.log.Timber + +@AndroidEntryPoint +class AdventureFragment : UFragment() { + @Inject lateinit var preferences: SharedPreferences + + private val viewModel: AdventureViewModel by activityViewModels() + private val profileViewModel: ProfileViewModel by viewModels() + private var activity: UGameActivity? = null + private lateinit var binding: FragmentAdventureBeginsBinding + + private val distanceAdapter by lazy { DistanceAdapter() } + + private lateinit var fusedLocationClient: FusedLocationProviderClient + private lateinit var locationCallback: LocationCallback + private lateinit var mLocationRequest: LocationRequest + private var showedLocationMessage: Boolean = false + private var requestingLocationUpdates = false + + private var currentList: List? = null + + private val requestPermissions = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> + val allGranted = result.size > 1 && result.entries.all { it.value } + if (allGranted) startRequesting() + } + + override fun onAttach(context: Context) { + super.onAttach(context) + activity = context as? UGameActivity + activity ?: Timber.e("Adventure Fragment must be attached to a UGameActivity for it to work") + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + locationSettings() + return FragmentAdventureBeginsBinding.inflate(inflater, container, false).also { + binding = it + }.apply { + interactor = viewModel + profile = profileViewModel + lifecycleOwner = this@AdventureFragment + executePendingBindings() + }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding.adventureAchievements.distanceRecycler.run { + adapter = distanceAdapter + } + + profileViewModel.getMeProfile().observe( + viewLifecycleOwner + ) { + if (it != null) { + profileViewModel.setProfileId(it.data?.id) + } + } + + viewModel.run { + achievements.observe(viewLifecycleOwner, EventObserver { onOpenAchievements() }) + start.observe(viewLifecycleOwner, EventObserver { activity?.signIn() }) + locations.observe(viewLifecycleOwner) { requestLocations(it) } + } + + lifecycleScope.launch { + if (activity?.isConnectedToPlayGames() == false && savedInstanceState == null) { + openStartupDialog() + } + } + + activity?.mGamesInstance?.connectionStatus?.observe( + viewLifecycleOwner, + EventObserver { + when (it) { + GameConnectionStatus.DISCONNECTED -> openStartupDialog() + GameConnectionStatus.CONNECTED -> { + val fragment = childFragmentManager.findFragmentByTag("adventure_sign_in") + (fragment as? DialogFragment)?.dismiss() + showSnack(getString(R.string.connected_to_play_games)) + } + GameConnectionStatus.LOADING -> Unit + } + } + ) + } + + private fun onOpenAchievements() { + lifecycleScope.launch { + activity?.openAchievements() + } + } + + override fun onPause() { + super.onPause() + stopUpdates() + } + + override fun onResume() { + super.onResume() + if (requestingLocationUpdates) startUpdates() + } + + private fun openStartupDialog() { + val dialog = AdventureSignInDialog() + dialog.show(childFragmentManager, "adventure_sign_in") + } + + private fun requestLocations(request: Boolean) { + if (request) { + startRequesting() + } else { + stopUpdates() + } + } + + private fun startRequesting() { + val perms = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) + val permissionsGranted = perms.all { + ContextCompat.checkSelfPermission(requireContext(), it) == PackageManager.PERMISSION_GRANTED + } + + if (permissionsGranted) { + startLocationsUpdate() + } else { + requestPermissions.launch(perms) + } + } + + private fun startLocationsUpdate() { + mLocationRequest = LocationRequest.Builder(7000) + .setWaitForAccurateLocation(true) + .setMinUpdateIntervalMillis(5000) + .setPriority(Priority.PRIORITY_HIGH_ACCURACY) + .build() + + if (currentList == null) { + onReceiveLocation(null) + } + + val builder = LocationSettingsRequest.Builder().addLocationRequest(mLocationRequest) + val client = LocationServices.getSettingsClient(requireContext()) + val task = client.checkLocationSettings(builder.build()) + + task.addOnCompleteListener(requireActivity()) { + Timber.d("Can make location request") + + try { + startUpdates() + } catch (e: SecurityException) { + Timber.e("What??? How did this happen?") + } + }.addOnFailureListener(requireActivity()) { fail -> + if (fail is ResolvableApiException) { + try { + fail.startResolutionForResult(requireActivity(), REQUEST_CHECK_SETTINGS) + } catch (e: Exception) { + Timber.d("Ignored exception") + e.printStackTrace() + showSnack(getString(R.string.cant_receive_location)) + } + } else { + Timber.d("Unresolvable Exception") + fail.printStackTrace() + showSnack(getString(R.string.cant_receive_location)) + } + } + } + + private fun startUpdates() { + try { + if (!::mLocationRequest.isInitialized) startLocationsUpdate() + fusedLocationClient.requestLocationUpdates(mLocationRequest, locationCallback, Looper.getMainLooper()) + binding.adventureAchievements.distanceRecycler.visibility = VISIBLE + } catch (e: SecurityException) { + Timber.d("Method could not be called") + } + } + + private fun stopUpdates() { + fusedLocationClient.removeLocationUpdates(locationCallback) + binding.adventureAchievements.distanceRecycler.visibility = GONE + } + + private fun locationSettings() { + fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext()) + locationCallback = object : LocationCallback() { + override fun onLocationResult(result: LocationResult) { + result.locations.forEach { location -> + if (location.accuracy >= 100) { + if (!showedLocationMessage) { + if (context != null) { + showSnack(getString(R.string.adventure_not_accurate)) + showedLocationMessage = true + } + } + } else { + onReceiveLocation(location) + } + } + } + } + } + + private fun onReceiveLocation(location: Location?) { + val value = viewModel.onReceiveLocation(location) + currentList = value + distanceAdapter.submitList(value) + value.mapNotNull { it.id }.forEach { + activity?.unlockAchievement(it) + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/allownotification/AllowNotificationActivity.kt b/app/src/main/java/com/forcetower/uefs/feature/allownotification/AllowNotificationActivity.kt index 90d7abadf..779f4e98b 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/allownotification/AllowNotificationActivity.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/allownotification/AllowNotificationActivity.kt @@ -27,8 +27,11 @@ class AllowNotificationActivity : UActivity() { private val requestPostNotificationPermission = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { granted -> - if (granted) finish() - else onPermissionsDenied() + if (granted) { + finish() + } else { + onPermissionsDenied() + } } override fun onCreate(savedInstanceState: Bundle?) { @@ -50,8 +53,9 @@ class AllowNotificationActivity : UActivity() { } private fun checkNotificationPermission() { - if (ContextCompat.checkSelfPermission(this, PERMISSION) == PackageManager.PERMISSION_GRANTED) + if (ContextCompat.checkSelfPermission(this, PERMISSION) == PackageManager.PERMISSION_GRANTED) { finish() + } } private fun requestNotificationPermission() { diff --git a/app/src/main/java/com/forcetower/uefs/feature/baddevice/BadDeviceFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/baddevice/BadDeviceFragment.kt index e4ebcc97c..cdbd3af16 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/baddevice/BadDeviceFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/baddevice/BadDeviceFragment.kt @@ -37,8 +37,8 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber import javax.inject.Inject +import timber.log.Timber @AndroidEntryPoint class BadDeviceFragment : BottomSheetDialogFragment() { diff --git a/app/src/main/java/com/forcetower/uefs/feature/bigtray/BigTrayRepository.kt b/app/src/main/java/com/forcetower/uefs/feature/bigtray/BigTrayRepository.kt index a623dc9f4..adf56dadc 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/bigtray/BigTrayRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/bigtray/BigTrayRepository.kt @@ -26,10 +26,10 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.forcetower.uefs.AppExecutors import com.forcetower.uefs.core.model.bigtray.BigTrayData +import javax.inject.Inject import okhttp3.OkHttpClient import okhttp3.Request import timber.log.Timber -import javax.inject.Inject class BigTrayRepository @Inject constructor( private val client: OkHttpClient, diff --git a/app/src/main/java/com/forcetower/uefs/feature/demand/DemandActivity.kt b/app/src/main/java/com/forcetower/uefs/feature/demand/DemandActivity.kt index 03283f548..d7c4320f7 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/demand/DemandActivity.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/demand/DemandActivity.kt @@ -1,75 +1,75 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.demand - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.activity.viewModels -import androidx.databinding.DataBindingUtil -import com.forcetower.core.lifecycle.EventObserver -import com.forcetower.uefs.R -import com.forcetower.uefs.databinding.ActivityDemandBinding -import com.forcetower.uefs.feature.shared.UActivity -import com.forcetower.uefs.feature.shared.extensions.config -import com.forcetower.uefs.feature.shared.extensions.inTransaction -import com.google.android.material.snackbar.Snackbar -import com.google.firebase.analytics.FirebaseAnalytics -import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber -import javax.inject.Inject - -@AndroidEntryPoint -class DemandActivity : UActivity() { - @Inject - lateinit var analytics: FirebaseAnalytics - - private lateinit var binding: ActivityDemandBinding - private val viewModel: DemandViewModel by viewModels() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = DataBindingUtil.setContentView(this, R.layout.activity_demand) - - if (savedInstanceState == null) { - supportFragmentManager.inTransaction { - val fragment = DemandOffersFragment() - add(R.id.fragment_container, fragment) - } - analytics.logEvent("demand_entered_screen", null) - } - - viewModel.snackbarMessage.observe(this, EventObserver { showSnack(it) }) - } - - override fun showSnack(string: String, duration: Int) { - Timber.d("Show snack called on activity") - val snack = Snackbar.make(binding.snack, string, duration) - snack.config(pxElevation = 8) - snack.show() - } - - companion object { - fun startIntent(context: Context): Intent { - return Intent(context, DemandActivity::class.java) - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.demand + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.databinding.DataBindingUtil +import com.forcetower.core.lifecycle.EventObserver +import com.forcetower.uefs.R +import com.forcetower.uefs.databinding.ActivityDemandBinding +import com.forcetower.uefs.feature.shared.UActivity +import com.forcetower.uefs.feature.shared.extensions.config +import com.forcetower.uefs.feature.shared.extensions.inTransaction +import com.google.android.material.snackbar.Snackbar +import com.google.firebase.analytics.FirebaseAnalytics +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import timber.log.Timber + +@AndroidEntryPoint +class DemandActivity : UActivity() { + @Inject + lateinit var analytics: FirebaseAnalytics + + private lateinit var binding: ActivityDemandBinding + private val viewModel: DemandViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_demand) + + if (savedInstanceState == null) { + supportFragmentManager.inTransaction { + val fragment = DemandOffersFragment() + add(R.id.fragment_container, fragment) + } + analytics.logEvent("demand_entered_screen", null) + } + + viewModel.snackbarMessage.observe(this, EventObserver { showSnack(it) }) + } + + override fun showSnack(string: String, duration: Int) { + Timber.d("Show snack called on activity") + val snack = Snackbar.make(binding.snack, string, duration) + snack.config(pxElevation = 8) + snack.show() + } + + companion object { + fun startIntent(context: Context): Intent { + return Intent(context, DemandActivity::class.java) + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/demand/DemandViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/demand/DemandViewModel.kt index e9b81f502..c02376ce9 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/demand/DemandViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/demand/DemandViewModel.kt @@ -34,8 +34,8 @@ import com.forcetower.uefs.core.storage.resource.Resource import com.forcetower.uefs.core.storage.resource.Status import com.google.firebase.analytics.FirebaseAnalytics import dagger.hilt.android.lifecycle.HiltViewModel -import timber.log.Timber import javax.inject.Inject +import timber.log.Timber @HiltViewModel class DemandViewModel @Inject constructor( @@ -56,7 +56,9 @@ class DemandViewModel @Inject constructor( private val _offers = MediatorLiveData>>() val offers: LiveData>> get() { - if (!loaded) { initLoad() } + if (!loaded) { + initLoad() + } return _offers } diff --git a/app/src/main/java/com/forcetower/uefs/feature/disciplines/DisciplineFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/disciplines/DisciplineFragment.kt index 45c96939c..14bc85d62 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/disciplines/DisciplineFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/disciplines/DisciplineFragment.kt @@ -50,12 +50,13 @@ import com.google.android.material.snackbar.Snackbar import com.google.android.material.tabs.TabLayout import com.google.firebase.remoteconfig.FirebaseRemoteConfig import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber import javax.inject.Inject +import timber.log.Timber @AndroidEntryPoint class DisciplineFragment : UFragment() { @Inject lateinit var preferences: SharedPreferences + @Inject lateinit var remoteConfig: FirebaseRemoteConfig private val viewModel: DisciplineViewModel by activityViewModels() diff --git a/app/src/main/java/com/forcetower/uefs/feature/disciplines/DisciplinePerformanceAdapter.kt b/app/src/main/java/com/forcetower/uefs/feature/disciplines/DisciplinePerformanceAdapter.kt index 0855e783a..ec65323f2 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/disciplines/DisciplinePerformanceAdapter.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/disciplines/DisciplinePerformanceAdapter.kt @@ -50,8 +50,9 @@ class DisciplinePerformanceAdapter( private fun buildMergedList(classes: List): List { val list = mutableListOf() classes.sortedBy { it.discipline.name }.forEachIndexed { index, clazz -> - if (index != 0) + if (index != 0) { list += Divider + } list += Header(clazz) @@ -152,35 +153,45 @@ class DisciplinePerformanceAdapter( val binding: ItemDisciplineStatusGroupingNameOldBinding, listener: DisciplineActions ) : DisciplineHolder(binding.root) { - init { binding.listener = listener } + init { + binding.listener = listener + } } class MeanHolder( val binding: ItemDisciplineStatusMeanOldBinding, listener: DisciplineActions ) : DisciplineHolder(binding.root) { - init { binding.listener = listener } + init { + binding.listener = listener + } } class FinalsHolder( val binding: ItemDisciplineStatusFinalsOldBinding, listener: DisciplineActions ) : DisciplineHolder(binding.root) { - init { binding.listener = listener } + init { + binding.listener = listener + } } class GradeHolder( val binding: ItemGradeOldBinding, listener: DisciplineActions ) : DisciplineHolder(binding.root) { - init { binding.listener = listener } + init { + binding.listener = listener + } } class HeaderHolder( val binding: ItemDisciplineStatusNameResumedOldBinding, listener: DisciplineActions ) : DisciplineHolder(binding.root) { - init { binding.listener = listener } + init { + binding.listener = listener + } } class DividerHolder( diff --git a/app/src/main/java/com/forcetower/uefs/feature/disciplines/DisciplineSemesterFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/disciplines/DisciplineSemesterFragment.kt index a0abd7bef..009a8e1f1 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/disciplines/DisciplineSemesterFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/disciplines/DisciplineSemesterFragment.kt @@ -1,121 +1,125 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.disciplines - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.os.bundleOf -import androidx.fragment.app.activityViewModels -import androidx.fragment.app.viewModels -import androidx.recyclerview.widget.DefaultItemAnimator -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.forcetower.core.widget.CustomSwipeRefreshLayout -import com.forcetower.uefs.core.model.unes.Semester -import com.forcetower.uefs.core.storage.database.aggregation.ClassFullWithGroup -import com.forcetower.uefs.databinding.FragmentDisciplineSemesterBinding -import com.forcetower.uefs.feature.shared.UFragment -import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber - -@AndroidEntryPoint -class DisciplineSemesterFragment : UFragment() { - private val viewModel: DisciplineViewModel by activityViewModels() - private val localDisciplineVM: DisciplineViewModel by viewModels() - private lateinit var recyclerView: RecyclerView - private lateinit var adapterPerformance: DisciplinePerformanceAdapter - private lateinit var swipeRefreshLayout: CustomSwipeRefreshLayout - private lateinit var binding: FragmentDisciplineSemesterBinding - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - binding = FragmentDisciplineSemesterBinding.inflate(inflater, container, false).apply { - viewModel = this@DisciplineSemesterFragment.viewModel - }.also { - recyclerView = it.disciplinesRecycler - swipeRefreshLayout = it.swipeRefresh - } - - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - try { binding.lifecycleOwner = viewLifecycleOwner } catch (t: Throwable) { Timber.e(t) } - - adapterPerformance = DisciplinePerformanceAdapter(viewModel) - recyclerView.adapter = adapterPerformance - - recyclerView.apply { - (layoutManager as LinearLayoutManager).recycleChildrenOnDetach = true - (itemAnimator as DefaultItemAnimator).run { - supportsChangeAnimations = false - addDuration = 160L - moveDuration = 160L - changeDuration = 160L - removeDuration = 120L - } - setRecycledViewPool( - RecyclerView.RecycledViewPool().apply { - setMaxRecycledViews(4, 7) - setMaxRecycledViews(8, 15) - } - ) - } - swipeRefreshLayout.setOnRefreshListener { - localDisciplineVM.updateGradesFromSemester(requireArguments().getLong(SEMESTER_SAGRES_ID)) - } - - binding.downloadBtn.setOnClickListener { - localDisciplineVM.updateGradesFromSemester(requireArguments().getLong(SEMESTER_SAGRES_ID)) - } - - localDisciplineVM.refreshing.observe( - viewLifecycleOwner, - { - swipeRefreshLayout.isRefreshing = it - binding.loading = it - } - ) - - viewModel.classes(requireArguments().getLong(SEMESTER_DATABASE_ID)).observe( - viewLifecycleOwner, - { - populateInterface(it) - binding.hasData = it.isNotEmpty() - } - ) - } - - private fun populateInterface(classes: List) { - adapterPerformance.classes = classes - } - - companion object { - const val SEMESTER_SAGRES_ID = "unes_sagres_id" - const val SEMESTER_DATABASE_ID = "unes_database_id" - - fun newInstance(semester: Semester): DisciplineSemesterFragment { - val args = bundleOf(SEMESTER_SAGRES_ID to semester.sagresId, SEMESTER_DATABASE_ID to semester.uid) - return DisciplineSemesterFragment().apply { arguments = args } - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.disciplines + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.DefaultItemAnimator +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.forcetower.core.widget.CustomSwipeRefreshLayout +import com.forcetower.uefs.core.model.unes.Semester +import com.forcetower.uefs.core.storage.database.aggregation.ClassFullWithGroup +import com.forcetower.uefs.databinding.FragmentDisciplineSemesterBinding +import com.forcetower.uefs.feature.shared.UFragment +import dagger.hilt.android.AndroidEntryPoint +import timber.log.Timber + +@AndroidEntryPoint +class DisciplineSemesterFragment : UFragment() { + private val viewModel: DisciplineViewModel by activityViewModels() + private val localDisciplineVM: DisciplineViewModel by viewModels() + private lateinit var recyclerView: RecyclerView + private lateinit var adapterPerformance: DisciplinePerformanceAdapter + private lateinit var swipeRefreshLayout: CustomSwipeRefreshLayout + private lateinit var binding: FragmentDisciplineSemesterBinding + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = FragmentDisciplineSemesterBinding.inflate(inflater, container, false).apply { + viewModel = this@DisciplineSemesterFragment.viewModel + }.also { + recyclerView = it.disciplinesRecycler + swipeRefreshLayout = it.swipeRefresh + } + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + try { + binding.lifecycleOwner = viewLifecycleOwner + } catch (t: Throwable) { + Timber.e(t) + } + + adapterPerformance = DisciplinePerformanceAdapter(viewModel) + recyclerView.adapter = adapterPerformance + + recyclerView.apply { + (layoutManager as LinearLayoutManager).recycleChildrenOnDetach = true + (itemAnimator as DefaultItemAnimator).run { + supportsChangeAnimations = false + addDuration = 160L + moveDuration = 160L + changeDuration = 160L + removeDuration = 120L + } + setRecycledViewPool( + RecyclerView.RecycledViewPool().apply { + setMaxRecycledViews(4, 7) + setMaxRecycledViews(8, 15) + } + ) + } + swipeRefreshLayout.setOnRefreshListener { + localDisciplineVM.updateGradesFromSemester(requireArguments().getLong(SEMESTER_SAGRES_ID)) + } + + binding.downloadBtn.setOnClickListener { + localDisciplineVM.updateGradesFromSemester(requireArguments().getLong(SEMESTER_SAGRES_ID)) + } + + localDisciplineVM.refreshing.observe( + viewLifecycleOwner, + { + swipeRefreshLayout.isRefreshing = it + binding.loading = it + } + ) + + viewModel.classes(requireArguments().getLong(SEMESTER_DATABASE_ID)).observe( + viewLifecycleOwner, + { + populateInterface(it) + binding.hasData = it.isNotEmpty() + } + ) + } + + private fun populateInterface(classes: List) { + adapterPerformance.classes = classes + } + + companion object { + const val SEMESTER_SAGRES_ID = "unes_sagres_id" + const val SEMESTER_DATABASE_ID = "unes_database_id" + + fun newInstance(semester: Semester): DisciplineSemesterFragment { + val args = bundleOf(SEMESTER_SAGRES_ID to semester.sagresId, SEMESTER_DATABASE_ID to semester.uid) + return DisciplineSemesterFragment().apply { arguments = args } + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/disciplines/DisciplineViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/disciplines/DisciplineViewModel.kt index 064147717..b853039d8 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/disciplines/DisciplineViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/disciplines/DisciplineViewModel.kt @@ -1,288 +1,288 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.disciplines - -import android.view.View -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.asLiveData -import androidx.lifecycle.viewModelScope -import com.forcetower.core.lifecycle.Event -import com.forcetower.uefs.architecture.service.discipline.DisciplineDetailsLoaderService -import com.forcetower.uefs.core.model.unes.Class -import com.forcetower.uefs.core.model.unes.ClassAbsence -import com.forcetower.uefs.core.model.unes.ClassGroup -import com.forcetower.uefs.core.model.unes.ClassItem -import com.forcetower.uefs.core.model.unes.ClassLocation -import com.forcetower.uefs.core.model.unes.ClassMaterial -import com.forcetower.uefs.core.storage.database.aggregation.ClassFullWithGroup -import com.forcetower.uefs.core.storage.database.aggregation.ClassGroupWithTeachers -import com.forcetower.uefs.core.storage.repository.DisciplineDetailsRepository -import com.forcetower.uefs.core.storage.repository.DisciplinesRepository -import com.forcetower.uefs.core.storage.repository.SagresGradesRepository -import com.forcetower.uefs.feature.common.DisciplineActions -import com.forcetower.uefs.feature.disciplines.disciplinedetail.classes.ClassesActions -import com.forcetower.uefs.feature.shared.extensions.setValueIfNew -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Named - -@HiltViewModel -class DisciplineViewModel @Inject constructor( - private val repository: DisciplinesRepository, - private val grades: SagresGradesRepository, - private val detailsRepository: DisciplineDetailsRepository, - @Named("flagSnowpiercerEnabled") private val snowpiercerEnabled: Boolean -) : ViewModel(), DisciplineActions, MaterialActions, ClassesActions { - - val semesters by lazy { repository.getParticipatingSemesters() } - fun classes(semesterId: Long) = repository.getClassesWithGradesFromSemester(semesterId) - - private val classGroupId = MutableLiveData() - private val classId = MutableLiveData() - - private val _classFull = MediatorLiveData() - val clazz: LiveData - get() = _classFull - - private val _group = MediatorLiveData() - val group: LiveData - get() = _group - - private val _absences = MediatorLiveData>() - val absences: LiveData> - get() = _absences - - private val _absencesAmount = MediatorLiveData() - val absencesAmount: LiveData - get() = _absencesAmount - - private val _materials = MediatorLiveData>() - val materials: LiveData> - get() = _materials - - private val _classItems = MediatorLiveData>() - val classItems: LiveData> - get() = _classItems - - private val _schedule = MediatorLiveData>() - val schedule: LiveData> - get() = _schedule - - private val _loadClassDetails = MediatorLiveData() - val loadClassDetails: LiveData - get() = _loadClassDetails - - private val _navigateToDisciplineAction = MutableLiveData>() - val navigateToDisciplineAction: LiveData> - get() = _navigateToDisciplineAction - - private val _navigateToGroupAction = MutableLiveData>() - val navigateToGroupAction: LiveData> - get() = _navigateToGroupAction - - private val _navigateToTeacherAction = MutableLiveData>() - val navigateToTeacherAction: LiveData> - get() = _navigateToTeacherAction - - private val _refreshing = MediatorLiveData() - val refreshing: LiveData - get() = _refreshing - - private val _materialClick = MutableLiveData>() - val materialClick: LiveData> - get() = _materialClick - - private val _classItemClick = MutableLiveData>() - val classItemClick: LiveData> - get() = _classItemClick - - init { - _classFull.addSource(classId) { - refreshClassStudent(it) - } - _absences.addSource(classId) { - refreshAbsences(it) - } - _absencesAmount.addSource(classId) { - refreshAbsencesAmount(it) - } - _materials.addSource(classGroupId) { - refreshMaterials(it) - } - _classItems.addSource(classGroupId) { - refreshClassItems(it) - } - _schedule.addSource(classId) { - refreshSchedule(it) - } - - _loadClassDetails.addSource(classGroupId) { - if (it != null) { - val src = if (snowpiercerEnabled) { - repository.loadClassDetailsSnowflake(it).asLiveData(Dispatchers.IO) - } else { - repository.loadClassDetails(it).asLiveData(Dispatchers.IO) - } - _loadClassDetails.addSource(src) { loading -> - _loadClassDetails.value = loading - } - } - } - _group.addSource(classGroupId) { - if (it != null) { - val source = repository.getClassGroup(it) - _group.addSource(source) { value -> - _group.value = value?.let { el -> ClassGroupWithTeachers(el.group, el.teachers) } - } - } - } - } - - private fun refreshSchedule(classId: Long?) { - classId ?: return - val source = repository.getLocationsFromClass(classId) - _schedule.addSource(source) { value -> - _schedule.value = value - } - } - - private fun refreshClassItems(classGroupId: Long?) { - if (classGroupId != null) { - val source = repository.getClassItemsFromGroup(classGroupId) - _classItems.addSource(source) { value -> - _classItems.value = value - } - } - } - - private fun refreshMaterials(classGroupId: Long?) { - if (classGroupId != null) { - val source = repository.getMaterialsFromGroup(classGroupId) - _materials.addSource(source) { value -> - _materials.value = value - } - } - } - - private fun refreshAbsences(classId: Long?) { - if (classId != null) { - val source = repository.getMyAbsencesFromClass(classId) - _absences.addSource(source) { value -> - _absences.value = value - } - } - } - - private fun refreshAbsencesAmount(classId: Long?) { - if (classId != null) { - val source = repository.getAbsencesAmount(classId) - _absencesAmount.addSource(source) { value -> - _absencesAmount.value = value - } - } - } - - private fun refreshClassStudent(classId: Long?) { - if (classId != null) { - val source = repository.getClassFull(classId) - _classFull.addSource(source) { value -> - _classFull.value = value - } - } - } - - fun setClassGroupId(classGroupId: Long?) { - this.classGroupId.setValueIfNew(classGroupId) - } - - fun setClassId(classId: Long?) { - this.classId.setValueIfNew(classId) - } - - override fun classClicked(clazz: ClassFullWithGroup) { - _navigateToDisciplineAction.value = Event(clazz) - } - - override fun groupSelected(clazz: ClassGroup) { - _navigateToGroupAction.value = Event(clazz) - } - - fun onTeacherNameClick(name: String) { - Timber.d("Name clicked $name") - _navigateToTeacherAction.value = Event(name) - } - - fun updateGradesFromSemester(semesterId: Long) { - Timber.d("Started refresh") - if (_refreshing.value != true) { - Timber.d("Something will actually happen") - _refreshing.value = true - viewModelScope.launch { - val result = grades.getGradesAsync(semesterId, false) - if (result == SagresGradesRepository.SUCCESS) { - Timber.d("Completed!") - } - _refreshing.value = false - } - } - } - - fun resetGroups(clazz: Class?): Boolean { - clazz ?: return true - repository.resetGroups(clazz) - return true - } - - fun loadAllDisciplines(view: View): Boolean { - val ctx = view.context - DisciplineDetailsLoaderService.startService(ctx, true) - return true - } - - fun getMaterialsFromClassItem(classItemId: Long): LiveData> { - return repository.getMaterialsFromClassItem(classItemId) - } - - override fun onMaterialClick(material: ClassMaterial?) { - material ?: return - _materialClick.value = Event(material) - } - - override fun onClassItemClicked(classItem: ClassItem?) { - classItem ?: return - _classItemClick.value = Event(classItem) - } - - fun updateLocationVisibility(location: ClassLocation) { - val hideStatus = !location.hiddenOnSchedule - repository.updateLocationVisibilityAsync(location.uid, hideStatus) - } - - fun prepareAndSendStats() { - detailsRepository.contributeCurrent() - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.disciplines + +import android.view.View +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import com.forcetower.core.lifecycle.Event +import com.forcetower.uefs.architecture.service.discipline.DisciplineDetailsLoaderService +import com.forcetower.uefs.core.model.unes.Class +import com.forcetower.uefs.core.model.unes.ClassAbsence +import com.forcetower.uefs.core.model.unes.ClassGroup +import com.forcetower.uefs.core.model.unes.ClassItem +import com.forcetower.uefs.core.model.unes.ClassLocation +import com.forcetower.uefs.core.model.unes.ClassMaterial +import com.forcetower.uefs.core.storage.database.aggregation.ClassFullWithGroup +import com.forcetower.uefs.core.storage.database.aggregation.ClassGroupWithTeachers +import com.forcetower.uefs.core.storage.repository.DisciplineDetailsRepository +import com.forcetower.uefs.core.storage.repository.DisciplinesRepository +import com.forcetower.uefs.core.storage.repository.SagresGradesRepository +import com.forcetower.uefs.feature.common.DisciplineActions +import com.forcetower.uefs.feature.disciplines.disciplinedetail.classes.ClassesActions +import com.forcetower.uefs.feature.shared.extensions.setValueIfNew +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import javax.inject.Named +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import timber.log.Timber + +@HiltViewModel +class DisciplineViewModel @Inject constructor( + private val repository: DisciplinesRepository, + private val grades: SagresGradesRepository, + private val detailsRepository: DisciplineDetailsRepository, + @Named("flagSnowpiercerEnabled") private val snowpiercerEnabled: Boolean +) : ViewModel(), DisciplineActions, MaterialActions, ClassesActions { + + val semesters by lazy { repository.getParticipatingSemesters() } + fun classes(semesterId: Long) = repository.getClassesWithGradesFromSemester(semesterId) + + private val classGroupId = MutableLiveData() + private val classId = MutableLiveData() + + private val _classFull = MediatorLiveData() + val clazz: LiveData + get() = _classFull + + private val _group = MediatorLiveData() + val group: LiveData + get() = _group + + private val _absences = MediatorLiveData>() + val absences: LiveData> + get() = _absences + + private val _absencesAmount = MediatorLiveData() + val absencesAmount: LiveData + get() = _absencesAmount + + private val _materials = MediatorLiveData>() + val materials: LiveData> + get() = _materials + + private val _classItems = MediatorLiveData>() + val classItems: LiveData> + get() = _classItems + + private val _schedule = MediatorLiveData>() + val schedule: LiveData> + get() = _schedule + + private val _loadClassDetails = MediatorLiveData() + val loadClassDetails: LiveData + get() = _loadClassDetails + + private val _navigateToDisciplineAction = MutableLiveData>() + val navigateToDisciplineAction: LiveData> + get() = _navigateToDisciplineAction + + private val _navigateToGroupAction = MutableLiveData>() + val navigateToGroupAction: LiveData> + get() = _navigateToGroupAction + + private val _navigateToTeacherAction = MutableLiveData>() + val navigateToTeacherAction: LiveData> + get() = _navigateToTeacherAction + + private val _refreshing = MediatorLiveData() + val refreshing: LiveData + get() = _refreshing + + private val _materialClick = MutableLiveData>() + val materialClick: LiveData> + get() = _materialClick + + private val _classItemClick = MutableLiveData>() + val classItemClick: LiveData> + get() = _classItemClick + + init { + _classFull.addSource(classId) { + refreshClassStudent(it) + } + _absences.addSource(classId) { + refreshAbsences(it) + } + _absencesAmount.addSource(classId) { + refreshAbsencesAmount(it) + } + _materials.addSource(classGroupId) { + refreshMaterials(it) + } + _classItems.addSource(classGroupId) { + refreshClassItems(it) + } + _schedule.addSource(classId) { + refreshSchedule(it) + } + + _loadClassDetails.addSource(classGroupId) { + if (it != null) { + val src = if (snowpiercerEnabled) { + repository.loadClassDetailsSnowflake(it).asLiveData(Dispatchers.IO) + } else { + repository.loadClassDetails(it).asLiveData(Dispatchers.IO) + } + _loadClassDetails.addSource(src) { loading -> + _loadClassDetails.value = loading + } + } + } + _group.addSource(classGroupId) { + if (it != null) { + val source = repository.getClassGroup(it) + _group.addSource(source) { value -> + _group.value = value?.let { el -> ClassGroupWithTeachers(el.group, el.teachers) } + } + } + } + } + + private fun refreshSchedule(classId: Long?) { + classId ?: return + val source = repository.getLocationsFromClass(classId) + _schedule.addSource(source) { value -> + _schedule.value = value + } + } + + private fun refreshClassItems(classGroupId: Long?) { + if (classGroupId != null) { + val source = repository.getClassItemsFromGroup(classGroupId) + _classItems.addSource(source) { value -> + _classItems.value = value + } + } + } + + private fun refreshMaterials(classGroupId: Long?) { + if (classGroupId != null) { + val source = repository.getMaterialsFromGroup(classGroupId) + _materials.addSource(source) { value -> + _materials.value = value + } + } + } + + private fun refreshAbsences(classId: Long?) { + if (classId != null) { + val source = repository.getMyAbsencesFromClass(classId) + _absences.addSource(source) { value -> + _absences.value = value + } + } + } + + private fun refreshAbsencesAmount(classId: Long?) { + if (classId != null) { + val source = repository.getAbsencesAmount(classId) + _absencesAmount.addSource(source) { value -> + _absencesAmount.value = value + } + } + } + + private fun refreshClassStudent(classId: Long?) { + if (classId != null) { + val source = repository.getClassFull(classId) + _classFull.addSource(source) { value -> + _classFull.value = value + } + } + } + + fun setClassGroupId(classGroupId: Long?) { + this.classGroupId.setValueIfNew(classGroupId) + } + + fun setClassId(classId: Long?) { + this.classId.setValueIfNew(classId) + } + + override fun classClicked(clazz: ClassFullWithGroup) { + _navigateToDisciplineAction.value = Event(clazz) + } + + override fun groupSelected(clazz: ClassGroup) { + _navigateToGroupAction.value = Event(clazz) + } + + fun onTeacherNameClick(name: String) { + Timber.d("Name clicked $name") + _navigateToTeacherAction.value = Event(name) + } + + fun updateGradesFromSemester(semesterId: Long) { + Timber.d("Started refresh") + if (_refreshing.value != true) { + Timber.d("Something will actually happen") + _refreshing.value = true + viewModelScope.launch { + val result = grades.getGradesAsync(semesterId, false) + if (result == SagresGradesRepository.SUCCESS) { + Timber.d("Completed!") + } + _refreshing.value = false + } + } + } + + fun resetGroups(clazz: Class?): Boolean { + clazz ?: return true + repository.resetGroups(clazz) + return true + } + + fun loadAllDisciplines(view: View): Boolean { + val ctx = view.context + DisciplineDetailsLoaderService.startService(ctx, true) + return true + } + + fun getMaterialsFromClassItem(classItemId: Long): LiveData> { + return repository.getMaterialsFromClassItem(classItemId) + } + + override fun onMaterialClick(material: ClassMaterial?) { + material ?: return + _materialClick.value = Event(material) + } + + override fun onClassItemClicked(classItem: ClassItem?) { + classItem ?: return + _classItemClick.value = Event(classItem) + } + + fun updateLocationVisibility(location: ClassLocation) { + val hideStatus = !location.hiddenOnSchedule + repository.updateLocationVisibilityAsync(location.uid, hideStatus) + } + + fun prepareAndSendStats() { + detailsRepository.contributeCurrent() + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/disciplines/DisciplinesBindingAdapter.kt b/app/src/main/java/com/forcetower/uefs/feature/disciplines/DisciplinesBindingAdapter.kt index 13e03e27f..7d9d3fd97 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/disciplines/DisciplinesBindingAdapter.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/disciplines/DisciplinesBindingAdapter.kt @@ -1,204 +1,207 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.disciplines - -import android.graphics.Paint -import android.widget.TextView -import androidx.databinding.BindingAdapter -import androidx.recyclerview.widget.RecyclerView -import com.forcetower.uefs.R -import com.forcetower.uefs.core.model.unes.Grade -import com.forcetower.uefs.core.storage.database.aggregation.ClassFullWithGroup -import com.forcetower.uefs.core.util.round -import com.forcetower.uefs.feature.common.DisciplineActions -import com.forcetower.uefs.feature.grades.ClassGroupGradesAdapter -import com.forcetower.uefs.widget.CircleProgressBar -import timber.log.Timber -import java.time.OffsetDateTime -import java.time.format.DateTimeFormatter -import kotlin.math.max - -@BindingAdapter(value = ["disciplineGroupsGrades", "disciplineListener"], requireAll = false) -fun disciplineGroupsGrades(recycler: RecyclerView, classes: List?, listener: DisciplineActions?) { - val sort = classes?.sortedWith { one, two -> - when { - one.name.trim().equals("prova final", ignoreCase = true) -> 1 - two.name.trim().equals("prova final", ignoreCase = true) -> -1 - else -> one.name.compareTo(two.name) - } - } - - val adapter: ClassGroupGradesAdapter - if (recycler.adapter == null) { - adapter = ClassGroupGradesAdapter(listener) - recycler.adapter = adapter - } else { - adapter = recycler.adapter as ClassGroupGradesAdapter - } - - adapter.submitList(sort) -} - -@BindingAdapter("classStudentGrade") -fun classStudentGrade(cpb: CircleProgressBar, clazz: ClassFullWithGroup?) { - val value = clazz?.clazz?.finalScore - if (value == null) { - cpb.setProgress(0.0f) - } else { - cpb.setProgressWithAnimation(value.toFloat() * 10) - } -} - -@BindingAdapter("gradeFormat") -fun gradeFormat(tv: TextView, value: Grade?) { - val grade = value?.gradeDouble() - if (grade == null) { - tv.text = tv.context.getString(R.string.grade_not_published) - } else { - tv.text = tv.context.getString(R.string.grade_format, grade.toFloat()) - } -} - -@BindingAdapter("evaluationDate") -fun evaluationDate(tv: TextView, value: Grade?) { - val date = value?.date - if (date == null) { - tv.text = tv.context.getString(R.string.grade_date_unknown) - } else { - try { - tv.text = OffsetDateTime.parse(date).format(DateTimeFormatter.ofPattern("dd/MM/YYYY")) - } catch (error: Throwable) { - tv.text = date - } - } -} - -@BindingAdapter("classStudentGrade") -fun classStudentGrade(tv: TextView, clazz: ClassFullWithGroup?) { - val value = clazz?.clazz?.finalScore - if (value == null) { - tv.text = "??" - } else { - tv.text = value.toString() - } -} - -@BindingAdapter("gradeNeededInFinal") -fun gradeNeededInFinal(tv: TextView, clazz: ClassFullWithGroup?) { - val value = clazz?.clazz?.partialScore - if (value == null) { - tv.text = "??" - } else { - val needed = (12.5 - (1.5 * value)).round() - tv.text = tv.context.getString(R.string.grade_format, needed) - } -} - -fun getClassWithGroupsGrade(clazz: ClassFullWithGroup): Double? { - if (clazz.groups.isNotEmpty()) { - return clazz.clazz.finalScore - } - return null -} - -@BindingAdapter(value = ["missedDescription", "missedDate"], requireAll = true) -fun classAbsence(tv: TextView, desc: String?, date: String?) { - tv.text = tv.context.getString(R.string.discipline_absence_item_format, desc ?: tv.context.getString(R.string.not_registed), date ?: tv.context.getString(R.string.not_registed)) -} - -@BindingAdapter(value = ["absences", "absencesListSize", "credits"], requireAll = true) -fun totalAbsence(tv: TextView, absences: Int?, absencesListSize: Int?, credits: Int?) { - val context = tv.context - val absenceCount = max(absences ?: 0, absencesListSize ?: 0) - if ((absences == null && absencesListSize == null) || credits == null || credits == 0) { - tv.text = context.getString(R.string.discipline_credits_undefined) - } else { - Timber.d("Credits: $credits __ Absence: $absences") - val left = (credits / 4) - absenceCount - when { - left > 0 -> tv.text = context.getString(R.string.discipline_absence_left, left) - left == 0 -> tv.text = context.getString(R.string.you_cant_miss_a_class) - else -> tv.text = context.getString(R.string.you_missed_to_many_classes) - } - } -} - -@BindingAdapter(value = ["absenceCount", "absenceAmount"], requireAll = true) -fun absenceCount(tv: TextView, absenceCount: Int?, absenceAmount: Int?) { - val context = tv.context - val count = max(absenceCount ?: 0, absenceAmount ?: 0) - if (absenceCount == null && absenceAmount == null) { - tv.text = context.getString(R.string.discipline_credits_undefined) - } else { - tv.text = context.getString(R.string.integer_format, count) - } -} - -@BindingAdapter(value = ["disciplineCredits"]) -fun credits(tv: TextView, credits: Int?) { - tv.text = credits?.toString()?.plus("h") ?: "??h" -} - -@BindingAdapter(value = ["somethingOrQuestions"]) -fun somethingOrQuestions(tv: TextView, something: String?) { - val text = something ?: "????" - tv.text = text -} - -@BindingAdapter(value = ["classSubject", "classSituation"], requireAll = true) -fun classSubject(tv: TextView, subject: String?, situation: String?) { - val text = subject ?: "????" - tv.text = text - - val strike = situation?.trim()?.equals("realizada", ignoreCase = true) - - if (strike == true) tv.paintFlags = tv.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG - else tv.paintFlags = tv.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() -} - -@BindingAdapter(value = ["absenceSequence", "absenceDate"], requireAll = true) -fun disciplineAbsence(tv: TextView, sequence: Int?, date: String?) { - val ctx = tv.context - val seq = sequence ?: 0 - val dat = date ?: "??/??/????" - - val dated = try { - OffsetDateTime.parse(dat).format(DateTimeFormatter.ofPattern("dd/MM/YYYY")) - } catch (error: Throwable) { - dat - } - - val text = ctx.getString(R.string.discipline_absence_date_format, seq, dated) - tv.text = text -} - -@BindingAdapter(value = ["absenceDescription"]) -fun absenceDescription(tv: TextView, description: String?) { - val desc = description ?: "CL 2 - ????" - val text = desc.substring(desc.indexOf("-") + 1).trim() - tv.text = text -} - -@BindingAdapter(value = ["disciplineStartsAtText", "disciplineEndsAtText"]) -fun disciplineStartEndGenerator(tv: TextView, startsAt: String?, endsAt: String?) { - val context = tv.context - tv.text = context.getString(R.string.discipline_start_end_format, startsAt, endsAt) -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.disciplines + +import android.graphics.Paint +import android.widget.TextView +import androidx.databinding.BindingAdapter +import androidx.recyclerview.widget.RecyclerView +import com.forcetower.uefs.R +import com.forcetower.uefs.core.model.unes.Grade +import com.forcetower.uefs.core.storage.database.aggregation.ClassFullWithGroup +import com.forcetower.uefs.core.util.round +import com.forcetower.uefs.feature.common.DisciplineActions +import com.forcetower.uefs.feature.grades.ClassGroupGradesAdapter +import com.forcetower.uefs.widget.CircleProgressBar +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter +import kotlin.math.max +import timber.log.Timber + +@BindingAdapter(value = ["disciplineGroupsGrades", "disciplineListener"], requireAll = false) +fun disciplineGroupsGrades(recycler: RecyclerView, classes: List?, listener: DisciplineActions?) { + val sort = classes?.sortedWith { one, two -> + when { + one.name.trim().equals("prova final", ignoreCase = true) -> 1 + two.name.trim().equals("prova final", ignoreCase = true) -> -1 + else -> one.name.compareTo(two.name) + } + } + + val adapter: ClassGroupGradesAdapter + if (recycler.adapter == null) { + adapter = ClassGroupGradesAdapter(listener) + recycler.adapter = adapter + } else { + adapter = recycler.adapter as ClassGroupGradesAdapter + } + + adapter.submitList(sort) +} + +@BindingAdapter("classStudentGrade") +fun classStudentGrade(cpb: CircleProgressBar, clazz: ClassFullWithGroup?) { + val value = clazz?.clazz?.finalScore + if (value == null) { + cpb.setProgress(0.0f) + } else { + cpb.setProgressWithAnimation(value.toFloat() * 10) + } +} + +@BindingAdapter("gradeFormat") +fun gradeFormat(tv: TextView, value: Grade?) { + val grade = value?.gradeDouble() + if (grade == null) { + tv.text = tv.context.getString(R.string.grade_not_published) + } else { + tv.text = tv.context.getString(R.string.grade_format, grade.toFloat()) + } +} + +@BindingAdapter("evaluationDate") +fun evaluationDate(tv: TextView, value: Grade?) { + val date = value?.date + if (date == null) { + tv.text = tv.context.getString(R.string.grade_date_unknown) + } else { + try { + tv.text = OffsetDateTime.parse(date).format(DateTimeFormatter.ofPattern("dd/MM/YYYY")) + } catch (error: Throwable) { + tv.text = date + } + } +} + +@BindingAdapter("classStudentGrade") +fun classStudentGrade(tv: TextView, clazz: ClassFullWithGroup?) { + val value = clazz?.clazz?.finalScore + if (value == null) { + tv.text = "??" + } else { + tv.text = value.toString() + } +} + +@BindingAdapter("gradeNeededInFinal") +fun gradeNeededInFinal(tv: TextView, clazz: ClassFullWithGroup?) { + val value = clazz?.clazz?.partialScore + if (value == null) { + tv.text = "??" + } else { + val needed = (12.5 - (1.5 * value)).round() + tv.text = tv.context.getString(R.string.grade_format, needed) + } +} + +fun getClassWithGroupsGrade(clazz: ClassFullWithGroup): Double? { + if (clazz.groups.isNotEmpty()) { + return clazz.clazz.finalScore + } + return null +} + +@BindingAdapter(value = ["missedDescription", "missedDate"], requireAll = true) +fun classAbsence(tv: TextView, desc: String?, date: String?) { + tv.text = tv.context.getString(R.string.discipline_absence_item_format, desc ?: tv.context.getString(R.string.not_registed), date ?: tv.context.getString(R.string.not_registed)) +} + +@BindingAdapter(value = ["absences", "absencesListSize", "credits"], requireAll = true) +fun totalAbsence(tv: TextView, absences: Int?, absencesListSize: Int?, credits: Int?) { + val context = tv.context + val absenceCount = max(absences ?: 0, absencesListSize ?: 0) + if ((absences == null && absencesListSize == null) || credits == null || credits == 0) { + tv.text = context.getString(R.string.discipline_credits_undefined) + } else { + Timber.d("Credits: $credits __ Absence: $absences") + val left = (credits / 4) - absenceCount + when { + left > 0 -> tv.text = context.getString(R.string.discipline_absence_left, left) + left == 0 -> tv.text = context.getString(R.string.you_cant_miss_a_class) + else -> tv.text = context.getString(R.string.you_missed_to_many_classes) + } + } +} + +@BindingAdapter(value = ["absenceCount", "absenceAmount"], requireAll = true) +fun absenceCount(tv: TextView, absenceCount: Int?, absenceAmount: Int?) { + val context = tv.context + val count = max(absenceCount ?: 0, absenceAmount ?: 0) + if (absenceCount == null && absenceAmount == null) { + tv.text = context.getString(R.string.discipline_credits_undefined) + } else { + tv.text = context.getString(R.string.integer_format, count) + } +} + +@BindingAdapter(value = ["disciplineCredits"]) +fun credits(tv: TextView, credits: Int?) { + tv.text = credits?.toString()?.plus("h") ?: "??h" +} + +@BindingAdapter(value = ["somethingOrQuestions"]) +fun somethingOrQuestions(tv: TextView, something: String?) { + val text = something ?: "????" + tv.text = text +} + +@BindingAdapter(value = ["classSubject", "classSituation"], requireAll = true) +fun classSubject(tv: TextView, subject: String?, situation: String?) { + val text = subject ?: "????" + tv.text = text + + val strike = situation?.trim()?.equals("realizada", ignoreCase = true) + + if (strike == true) { + tv.paintFlags = tv.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG + } else { + tv.paintFlags = tv.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() + } +} + +@BindingAdapter(value = ["absenceSequence", "absenceDate"], requireAll = true) +fun disciplineAbsence(tv: TextView, sequence: Int?, date: String?) { + val ctx = tv.context + val seq = sequence ?: 0 + val dat = date ?: "??/??/????" + + val dated = try { + OffsetDateTime.parse(dat).format(DateTimeFormatter.ofPattern("dd/MM/YYYY")) + } catch (error: Throwable) { + dat + } + + val text = ctx.getString(R.string.discipline_absence_date_format, seq, dated) + tv.text = text +} + +@BindingAdapter(value = ["absenceDescription"]) +fun absenceDescription(tv: TextView, description: String?) { + val desc = description ?: "CL 2 - ????" + val text = desc.substring(desc.indexOf("-") + 1).trim() + tv.text = text +} + +@BindingAdapter(value = ["disciplineStartsAtText", "disciplineEndsAtText"]) +fun disciplineStartEndGenerator(tv: TextView, startsAt: String?, endsAt: String?) { + val context = tv.context + tv.text = context.getString(R.string.discipline_start_end_format, startsAt, endsAt) +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/disciplines/disciplinedetail/DisciplineDetailsFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/disciplines/disciplinedetail/DisciplineDetailsFragment.kt index 08d6121a7..4ec492fe9 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/disciplines/disciplinedetail/DisciplineDetailsFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/disciplines/disciplinedetail/DisciplineDetailsFragment.kt @@ -1,173 +1,174 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.disciplines.disciplinedetail - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.content.ContextCompat -import androidx.core.os.bundleOf -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentPagerAdapter -import androidx.fragment.app.activityViewModels -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.RecyclerView -import androidx.viewpager.widget.ViewPager -import com.forcetower.core.lifecycle.EventObserver -import com.forcetower.uefs.R -import com.forcetower.uefs.core.model.unes.Discipline -import com.forcetower.uefs.databinding.ExtItemDisciplineHoursBinding -import com.forcetower.uefs.databinding.ExtItemMissedClassesBinding -import com.forcetower.uefs.databinding.FragmentDisciplineDetailsBinding -import com.forcetower.uefs.feature.disciplines.DisciplineViewModel -import com.forcetower.uefs.feature.disciplines.disciplinedetail.absences.AbsencesFragment -import com.forcetower.uefs.feature.disciplines.disciplinedetail.classes.ClassesFragment -import com.forcetower.uefs.feature.disciplines.disciplinedetail.materials.MaterialsFragment -import com.forcetower.uefs.feature.disciplines.disciplinedetail.overview.OverviewFragment -import com.forcetower.uefs.feature.evaluation.EvaluationActivity -import com.forcetower.uefs.feature.shared.UFragment -import com.forcetower.uefs.feature.shared.extensions.openURL -import com.forcetower.uefs.feature.shared.inflate -import com.forcetower.uefs.widget.DividerItemDecorator -import com.google.android.material.tabs.TabLayout -import com.google.firebase.firestore.CollectionReference -import javax.inject.Inject -import javax.inject.Named - -class DisciplineDetailsFragment : UFragment() { - @Inject @Named(Discipline.COLLECTION) - lateinit var firestore: CollectionReference - - private val viewModel: DisciplineViewModel by activityViewModels() - private lateinit var binding: FragmentDisciplineDetailsBinding - private lateinit var tabs: TabLayout - private lateinit var viewPager: ViewPager - private lateinit var adapter: DetailsAdapter - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - binding = FragmentDisciplineDetailsBinding.inflate(inflater, container, false).also { - viewPager = it.viewPager - tabs = it.tabs - } - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - adapter = DetailsAdapter(childFragmentManager) - createFragments() - viewPager.adapter = adapter - tabs.setupWithViewPager(viewPager) - viewPager.offscreenPageLimit = 4 - - val menuAdapter = ItemsDisciplineAdapter() - binding.recyclerDisciplineItems.apply { - adapter = menuAdapter - addItemDecoration(DividerItemDecorator(ContextCompat.getDrawable(context, R.drawable.divider)!!, DividerItemDecoration.HORIZONTAL)) - } - - binding.up.setOnClickListener { - activity?.finishAfterTransition() - } - - viewModel.materialClick.observe(viewLifecycleOwner, EventObserver { requireContext().openURL(it.link) }) - - viewModel.setClassId(requireNotNull(arguments).getLong(DisciplineDetailsActivity.CLASS_ID)) - viewModel.setClassGroupId(requireNotNull(arguments).getLong(DisciplineDetailsActivity.CLASS_GROUP_ID)) - viewModel.navigateToTeacherAction.observe( - viewLifecycleOwner, - EventObserver { - startActivity(EvaluationActivity.startIntentForTeacher(requireContext(), it)) - } - ) - binding.apply { - viewModel = this@DisciplineDetailsFragment.viewModel - lifecycleOwner = this@DisciplineDetailsFragment - } - } - - private fun createFragments() { - val group = requireNotNull(arguments).getLong(DisciplineDetailsActivity.CLASS_GROUP_ID) - val overview = getString(R.string.discipline_details_overview) to OverviewFragment.newInstance(group) - val classes = getString(R.string.discipline_details_classes) to ClassesFragment.newInstance(group) - val materials = getString(R.string.discipline_details_materials) to MaterialsFragment.newInstance(group) - val absences = getString(R.string.discipline_details_absences) to AbsencesFragment.newInstance(group) - val list = listOf>(overview, classes, materials, absences) - adapter.submitList(list) - } - - private class DetailsAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { - private val tabs = mutableListOf>() - override fun getItem(position: Int) = tabs[position].second - override fun getCount() = tabs.size - override fun getPageTitle(position: Int) = tabs[position].first - fun submitList(pairs: List>) { - tabs.clear() - tabs.addAll(pairs) - notifyDataSetChanged() - } - } - - companion object { - fun newInstance(classId: Long, classGroupId: Long): DisciplineDetailsFragment { - return DisciplineDetailsFragment().apply { - arguments = bundleOf( - DisciplineDetailsActivity.CLASS_GROUP_ID to classGroupId, - DisciplineDetailsActivity.CLASS_ID to classId - ) - } - } - } - - private inner class ItemsDisciplineAdapter : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder { - return when (viewType) { - 0 -> ItemHolder.HoursHolder(parent.inflate(R.layout.ext_item_discipline_hours)) - 1 -> ItemHolder.MissedHolder(parent.inflate(R.layout.ext_item_missed_classes)) - else -> throw IllegalStateException("Invalid State of views") - } - } - - override fun onBindViewHolder(holder: ItemHolder, position: Int) { - when (holder) { - is ItemHolder.HoursHolder -> holder.binding.apply { - viewModel = this@DisciplineDetailsFragment.viewModel - lifecycleOwner = this@DisciplineDetailsFragment - executePendingBindings() - } - is ItemHolder.MissedHolder -> holder.binding.apply { - viewModel = this@DisciplineDetailsFragment.viewModel - lifecycleOwner = this@DisciplineDetailsFragment - executePendingBindings() - } - } - } - - override fun getItemCount() = 2 - override fun getItemViewType(position: Int) = position - } - - private sealed class ItemHolder(item: View) : RecyclerView.ViewHolder(item) { - class HoursHolder(val binding: ExtItemDisciplineHoursBinding) : ItemHolder(binding.root) - class MissedHolder(val binding: ExtItemMissedClassesBinding) : ItemHolder(binding.root) - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.disciplines.disciplinedetail + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentPagerAdapter +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager.widget.ViewPager +import com.forcetower.core.lifecycle.EventObserver +import com.forcetower.uefs.R +import com.forcetower.uefs.core.model.unes.Discipline +import com.forcetower.uefs.databinding.ExtItemDisciplineHoursBinding +import com.forcetower.uefs.databinding.ExtItemMissedClassesBinding +import com.forcetower.uefs.databinding.FragmentDisciplineDetailsBinding +import com.forcetower.uefs.feature.disciplines.DisciplineViewModel +import com.forcetower.uefs.feature.disciplines.disciplinedetail.absences.AbsencesFragment +import com.forcetower.uefs.feature.disciplines.disciplinedetail.classes.ClassesFragment +import com.forcetower.uefs.feature.disciplines.disciplinedetail.materials.MaterialsFragment +import com.forcetower.uefs.feature.disciplines.disciplinedetail.overview.OverviewFragment +import com.forcetower.uefs.feature.evaluation.EvaluationActivity +import com.forcetower.uefs.feature.shared.UFragment +import com.forcetower.uefs.feature.shared.extensions.openURL +import com.forcetower.uefs.feature.shared.inflate +import com.forcetower.uefs.widget.DividerItemDecorator +import com.google.android.material.tabs.TabLayout +import com.google.firebase.firestore.CollectionReference +import javax.inject.Inject +import javax.inject.Named + +class DisciplineDetailsFragment : UFragment() { + @Inject + @Named(Discipline.COLLECTION) + lateinit var firestore: CollectionReference + + private val viewModel: DisciplineViewModel by activityViewModels() + private lateinit var binding: FragmentDisciplineDetailsBinding + private lateinit var tabs: TabLayout + private lateinit var viewPager: ViewPager + private lateinit var adapter: DetailsAdapter + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + binding = FragmentDisciplineDetailsBinding.inflate(inflater, container, false).also { + viewPager = it.viewPager + tabs = it.tabs + } + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + adapter = DetailsAdapter(childFragmentManager) + createFragments() + viewPager.adapter = adapter + tabs.setupWithViewPager(viewPager) + viewPager.offscreenPageLimit = 4 + + val menuAdapter = ItemsDisciplineAdapter() + binding.recyclerDisciplineItems.apply { + adapter = menuAdapter + addItemDecoration(DividerItemDecorator(ContextCompat.getDrawable(context, R.drawable.divider)!!, DividerItemDecoration.HORIZONTAL)) + } + + binding.up.setOnClickListener { + activity?.finishAfterTransition() + } + + viewModel.materialClick.observe(viewLifecycleOwner, EventObserver { requireContext().openURL(it.link) }) + + viewModel.setClassId(requireNotNull(arguments).getLong(DisciplineDetailsActivity.CLASS_ID)) + viewModel.setClassGroupId(requireNotNull(arguments).getLong(DisciplineDetailsActivity.CLASS_GROUP_ID)) + viewModel.navigateToTeacherAction.observe( + viewLifecycleOwner, + EventObserver { + startActivity(EvaluationActivity.startIntentForTeacher(requireContext(), it)) + } + ) + binding.apply { + viewModel = this@DisciplineDetailsFragment.viewModel + lifecycleOwner = this@DisciplineDetailsFragment + } + } + + private fun createFragments() { + val group = requireNotNull(arguments).getLong(DisciplineDetailsActivity.CLASS_GROUP_ID) + val overview = getString(R.string.discipline_details_overview) to OverviewFragment.newInstance(group) + val classes = getString(R.string.discipline_details_classes) to ClassesFragment.newInstance(group) + val materials = getString(R.string.discipline_details_materials) to MaterialsFragment.newInstance(group) + val absences = getString(R.string.discipline_details_absences) to AbsencesFragment.newInstance(group) + val list = listOf>(overview, classes, materials, absences) + adapter.submitList(list) + } + + private class DetailsAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + private val tabs = mutableListOf>() + override fun getItem(position: Int) = tabs[position].second + override fun getCount() = tabs.size + override fun getPageTitle(position: Int) = tabs[position].first + fun submitList(pairs: List>) { + tabs.clear() + tabs.addAll(pairs) + notifyDataSetChanged() + } + } + + companion object { + fun newInstance(classId: Long, classGroupId: Long): DisciplineDetailsFragment { + return DisciplineDetailsFragment().apply { + arguments = bundleOf( + DisciplineDetailsActivity.CLASS_GROUP_ID to classGroupId, + DisciplineDetailsActivity.CLASS_ID to classId + ) + } + } + } + + private inner class ItemsDisciplineAdapter : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder { + return when (viewType) { + 0 -> ItemHolder.HoursHolder(parent.inflate(R.layout.ext_item_discipline_hours)) + 1 -> ItemHolder.MissedHolder(parent.inflate(R.layout.ext_item_missed_classes)) + else -> throw IllegalStateException("Invalid State of views") + } + } + + override fun onBindViewHolder(holder: ItemHolder, position: Int) { + when (holder) { + is ItemHolder.HoursHolder -> holder.binding.apply { + viewModel = this@DisciplineDetailsFragment.viewModel + lifecycleOwner = this@DisciplineDetailsFragment + executePendingBindings() + } + is ItemHolder.MissedHolder -> holder.binding.apply { + viewModel = this@DisciplineDetailsFragment.viewModel + lifecycleOwner = this@DisciplineDetailsFragment + executePendingBindings() + } + } + } + + override fun getItemCount() = 2 + override fun getItemViewType(position: Int) = position + } + + private sealed class ItemHolder(item: View) : RecyclerView.ViewHolder(item) { + class HoursHolder(val binding: ExtItemDisciplineHoursBinding) : ItemHolder(binding.root) + class MissedHolder(val binding: ExtItemMissedClassesBinding) : ItemHolder(binding.root) + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/disciplines/disciplinedetail/overview/OverviewAdapter.kt b/app/src/main/java/com/forcetower/uefs/feature/disciplines/disciplinedetail/overview/OverviewAdapter.kt index 4e225fae5..bdb9c0a47 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/disciplines/disciplinedetail/overview/OverviewAdapter.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/disciplines/disciplinedetail/overview/OverviewAdapter.kt @@ -27,9 +27,7 @@ import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.forcetower.uefs.R -import com.forcetower.uefs.core.model.unes.ClassGroup import com.forcetower.uefs.core.model.unes.ClassLocation -import com.forcetower.uefs.core.model.unes.Teacher import com.forcetower.uefs.core.storage.database.aggregation.ClassFullWithGroup import com.forcetower.uefs.core.storage.database.aggregation.ClassGroupWithTeachers import com.forcetower.uefs.databinding.ItemDisciplineGoalsBinding diff --git a/app/src/main/java/com/forcetower/uefs/feature/disciplines/disciplinedetail/overview/OverviewFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/disciplines/disciplinedetail/overview/OverviewFragment.kt index ad98f53d7..fd60abefb 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/disciplines/disciplinedetail/overview/OverviewFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/disciplines/disciplinedetail/overview/OverviewFragment.kt @@ -1,71 +1,70 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.disciplines.disciplinedetail.overview - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.os.bundleOf -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Observer -import com.forcetower.uefs.databinding.FragmentDisciplineOverviewBinding -import com.forcetower.uefs.feature.disciplines.DisciplineViewModel -import com.forcetower.uefs.feature.disciplines.disciplinedetail.DisciplineDetailsActivity.Companion.CLASS_GROUP_ID -import com.forcetower.uefs.feature.shared.UFragment -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class OverviewFragment : UFragment() { - private lateinit var binding: FragmentDisciplineOverviewBinding - private val viewModel: DisciplineViewModel by activityViewModels() - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return FragmentDisciplineOverviewBinding.inflate(inflater, container, false).also { - binding = it - }.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val overviewAdapter = OverviewAdapter(this, viewModel) - binding.recyclerOverview.apply { - adapter = overviewAdapter - itemAnimator?.run { - addDuration = 120L - moveDuration = 120L - changeDuration = 120L - removeDuration = 100L - } - } - - viewModel.clazz.observe(viewLifecycleOwner) { overviewAdapter.currentClazz = it } - viewModel.group.observe(viewLifecycleOwner) { overviewAdapter.currentGroup = it } - viewModel.schedule.observe(viewLifecycleOwner) { overviewAdapter.currentSchedule = it } - } - - companion object { - fun newInstance(classId: Long): OverviewFragment { - return OverviewFragment().apply { - arguments = bundleOf(CLASS_GROUP_ID to classId) - } - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.disciplines.disciplinedetail.overview + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.activityViewModels +import com.forcetower.uefs.databinding.FragmentDisciplineOverviewBinding +import com.forcetower.uefs.feature.disciplines.DisciplineViewModel +import com.forcetower.uefs.feature.disciplines.disciplinedetail.DisciplineDetailsActivity.Companion.CLASS_GROUP_ID +import com.forcetower.uefs.feature.shared.UFragment +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class OverviewFragment : UFragment() { + private lateinit var binding: FragmentDisciplineOverviewBinding + private val viewModel: DisciplineViewModel by activityViewModels() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return FragmentDisciplineOverviewBinding.inflate(inflater, container, false).also { + binding = it + }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val overviewAdapter = OverviewAdapter(this, viewModel) + binding.recyclerOverview.apply { + adapter = overviewAdapter + itemAnimator?.run { + addDuration = 120L + moveDuration = 120L + changeDuration = 120L + removeDuration = 100L + } + } + + viewModel.clazz.observe(viewLifecycleOwner) { overviewAdapter.currentClazz = it } + viewModel.group.observe(viewLifecycleOwner) { overviewAdapter.currentGroup = it } + viewModel.schedule.observe(viewLifecycleOwner) { overviewAdapter.currentSchedule = it } + } + + companion object { + fun newInstance(classId: Long): OverviewFragment { + return OverviewFragment().apply { + arguments = bundleOf(CLASS_GROUP_ID to classId) + } + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/document/DocumentsFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/document/DocumentsFragment.kt index 0210f3e03..4e20358c9 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/document/DocumentsFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/document/DocumentsFragment.kt @@ -1,93 +1,93 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.document - -import android.content.ActivityNotFoundException -import android.content.Intent -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.content.FileProvider -import androidx.fragment.app.activityViewModels -import com.forcetower.core.lifecycle.EventObserver -import com.forcetower.uefs.BuildConfig -import com.forcetower.uefs.R -import com.forcetower.uefs.core.model.unes.SagresDocument -import com.forcetower.uefs.databinding.FragmentDocumentsBinding -import com.forcetower.uefs.feature.captcha.CaptchaResolverFragment -import com.forcetower.uefs.feature.shared.UFragment -import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber -import java.io.File - -@AndroidEntryPoint -class DocumentsFragment : UFragment() { - private lateinit var binding: FragmentDocumentsBinding - private val viewModel: DocumentsViewModel by activityViewModels() - private lateinit var adapter: DocumentsAdapter - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return FragmentDocumentsBinding.inflate(inflater, container, false).also { - binding = it - }.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - adapter = DocumentsAdapter(this@DocumentsFragment, viewModel) - binding.apply { - recyclerDocuments.adapter = adapter - incToolbar.textToolbarTitle.text = getString(R.string.label_documents) - } - - viewModel.documents.observe(viewLifecycleOwner, { adapter.documents = it ?: emptyList() }) - viewModel.openDocumentAction.observe(viewLifecycleOwner, EventObserver { openDocument(it) }) - viewModel.snackMessages.observe(viewLifecycleOwner, EventObserver { showSnack(it) }) - viewModel.onRequestDownload.observe(viewLifecycleOwner, EventObserver { requestCaptchaForDocument(it) }) - } - - private fun requestCaptchaForDocument(document: SagresDocument) { - val fragment = CaptchaResolverFragment() - fragment.setCallback( - object : CaptchaResolverFragment.CaptchaResolvedCallback { - override fun onCaptchaResolved(token: String) { - Timber.d("Token received $token") - viewModel.onDownload(document, token) - } - } - ) - fragment.show(childFragmentManager, "captcha_resolver") - } - - private fun openDocument(document: File) { - val intent = Intent(Intent.ACTION_VIEW) - val uri = FileProvider.getUriForFile(requireContext(), BuildConfig.APPLICATION_ID + ".fileprovider", document) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - intent.setDataAndType(uri, "application/pdf") - intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) - val choose = Intent.createChooser(intent, getString(R.string.open_file)) - try { - startActivity(choose) - } catch (e: ActivityNotFoundException) { - showSnack(getString(R.string.no_pdf_reader)) - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.document + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.FileProvider +import androidx.fragment.app.activityViewModels +import com.forcetower.core.lifecycle.EventObserver +import com.forcetower.uefs.BuildConfig +import com.forcetower.uefs.R +import com.forcetower.uefs.core.model.unes.SagresDocument +import com.forcetower.uefs.databinding.FragmentDocumentsBinding +import com.forcetower.uefs.feature.captcha.CaptchaResolverFragment +import com.forcetower.uefs.feature.shared.UFragment +import dagger.hilt.android.AndroidEntryPoint +import java.io.File +import timber.log.Timber + +@AndroidEntryPoint +class DocumentsFragment : UFragment() { + private lateinit var binding: FragmentDocumentsBinding + private val viewModel: DocumentsViewModel by activityViewModels() + private lateinit var adapter: DocumentsAdapter + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return FragmentDocumentsBinding.inflate(inflater, container, false).also { + binding = it + }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + adapter = DocumentsAdapter(this@DocumentsFragment, viewModel) + binding.apply { + recyclerDocuments.adapter = adapter + incToolbar.textToolbarTitle.text = getString(R.string.label_documents) + } + + viewModel.documents.observe(viewLifecycleOwner, { adapter.documents = it ?: emptyList() }) + viewModel.openDocumentAction.observe(viewLifecycleOwner, EventObserver { openDocument(it) }) + viewModel.snackMessages.observe(viewLifecycleOwner, EventObserver { showSnack(it) }) + viewModel.onRequestDownload.observe(viewLifecycleOwner, EventObserver { requestCaptchaForDocument(it) }) + } + + private fun requestCaptchaForDocument(document: SagresDocument) { + val fragment = CaptchaResolverFragment() + fragment.setCallback( + object : CaptchaResolverFragment.CaptchaResolvedCallback { + override fun onCaptchaResolved(token: String) { + Timber.d("Token received $token") + viewModel.onDownload(document, token) + } + } + ) + fragment.show(childFragmentManager, "captcha_resolver") + } + + private fun openDocument(document: File) { + val intent = Intent(Intent.ACTION_VIEW) + val uri = FileProvider.getUriForFile(requireContext(), BuildConfig.APPLICATION_ID + ".fileprovider", document) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.setDataAndType(uri, "application/pdf") + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) + val choose = Intent.createChooser(intent, getString(R.string.open_file)) + try { + startActivity(choose) + } catch (e: ActivityNotFoundException) { + showSnack(getString(R.string.no_pdf_reader)) + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/evaluation/EvaluationActivity.kt b/app/src/main/java/com/forcetower/uefs/feature/evaluation/EvaluationActivity.kt index 450dbf594..2a46219de 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/evaluation/EvaluationActivity.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/evaluation/EvaluationActivity.kt @@ -26,7 +26,6 @@ import android.os.Bundle import androidx.activity.viewModels import androidx.databinding.DataBindingUtil import androidx.navigation.findNavController -import com.forcetower.uefs.EvalNavGraphDirections import com.forcetower.uefs.R import com.forcetower.uefs.core.vm.UserSessionViewModel import com.forcetower.uefs.databinding.ActivityEvaluationBinding diff --git a/app/src/main/java/com/forcetower/uefs/feature/evaluation/EvaluationBindingAdapters.kt b/app/src/main/java/com/forcetower/uefs/feature/evaluation/EvaluationBindingAdapters.kt index 36e25ca8f..1949b68aa 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/evaluation/EvaluationBindingAdapters.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/evaluation/EvaluationBindingAdapters.kt @@ -29,7 +29,6 @@ import com.forcetower.uefs.R import com.forcetower.uefs.core.model.unes.EdgeParadoxSearchableItem import com.forcetower.uefs.core.model.unes.EdgeParadoxSearchableItem.Companion.DISCIPLINE_TYPE import com.forcetower.uefs.core.model.unes.EdgeParadoxSearchableItem.Companion.TEACHER_TYPE -import com.forcetower.uefs.core.model.unes.EvaluationEntity import com.forcetower.uefs.feature.evaluation.discipline.SemesterMean import com.github.mikephil.charting.charts.LineChart import com.github.mikephil.charting.components.AxisBase @@ -71,8 +70,9 @@ fun formatSemesterGradeChart(chart: LineChart, list: List?) { val formatter = object : ValueFormatter() { override fun getAxisLabel(value: Float, axis: AxisBase?): String { val converted = value.toInt() - if (converted < 0 || converted >= pair.first.size) + if (converted < 0 || converted >= pair.first.size) { return "" + } return pair.first[converted] } } diff --git a/app/src/main/java/com/forcetower/uefs/feature/evaluation/EvaluationViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/evaluation/EvaluationViewModel.kt index 7015c20d2..5b7e57fd7 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/evaluation/EvaluationViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/evaluation/EvaluationViewModel.kt @@ -22,7 +22,6 @@ package com.forcetower.uefs.feature.evaluation import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import androidx.paging.cachedIn @@ -33,17 +32,10 @@ import com.forcetower.uefs.core.model.edge.paradox.PublicHotEvaluationDiscipline import com.forcetower.uefs.core.model.edge.paradox.PublicHotEvaluationTeacher import com.forcetower.uefs.core.model.edge.paradox.PublicTeacherEvaluationCombinedData import com.forcetower.uefs.core.model.edge.paradox.PublicTeacherEvaluationData -import com.forcetower.uefs.core.model.service.EvaluationDiscipline -import com.forcetower.uefs.core.model.service.EvaluationHomeTopic -import com.forcetower.uefs.core.model.service.EvaluationTeacher import com.forcetower.uefs.core.model.unes.EdgeParadoxSearchableItem -import com.forcetower.uefs.core.model.unes.EvaluationEntity -import com.forcetower.uefs.core.storage.repository.AccountRepository import com.forcetower.uefs.core.storage.repository.EvaluationRepository -import com.forcetower.uefs.core.storage.repository.cloud.AuthRepository import com.forcetower.uefs.core.storage.repository.cloud.EdgeAccountRepository import com.forcetower.uefs.core.storage.repository.cloud.EdgeAuthRepository -import com.forcetower.uefs.core.storage.resource.Resource import com.forcetower.uefs.core.util.TextTransformUtils import com.forcetower.uefs.domain.model.paradox.DisciplineCombinedData import com.forcetower.uefs.feature.evaluation.discipline.DisciplineInteractor @@ -51,8 +43,8 @@ import com.forcetower.uefs.feature.evaluation.discipline.TeacherInt import com.forcetower.uefs.feature.evaluation.home.HomeInteractor import com.forcetower.uefs.feature.evaluation.search.EntitySelector import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.launch @HiltViewModel class EvaluationViewModel @Inject constructor( diff --git a/app/src/main/java/com/forcetower/uefs/feature/evaluation/InitialFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/evaluation/InitialFragment.kt index 485ab5859..9d1ec6714 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/evaluation/InitialFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/evaluation/InitialFragment.kt @@ -30,8 +30,8 @@ import androidx.navigation.fragment.findNavController import com.forcetower.uefs.R import com.forcetower.uefs.feature.shared.UFragment import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber import javax.inject.Inject +import timber.log.Timber @AndroidEntryPoint class InitialFragment : UFragment() { diff --git a/app/src/main/java/com/forcetower/uefs/feature/evaluation/discipline/DisciplineEvaluationFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/evaluation/discipline/DisciplineEvaluationFragment.kt index 680181c7f..6bb0ceba1 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/evaluation/discipline/DisciplineEvaluationFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/evaluation/discipline/DisciplineEvaluationFragment.kt @@ -31,16 +31,12 @@ import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.forcetower.core.lifecycle.EventObserver -import com.forcetower.uefs.core.model.service.EvaluationDiscipline -import com.forcetower.uefs.core.storage.resource.Resource -import com.forcetower.uefs.core.storage.resource.Status import com.forcetower.uefs.databinding.FragmentEvaluationDisciplineBinding import com.forcetower.uefs.domain.model.paradox.DisciplineCombinedData import com.forcetower.uefs.feature.evaluation.EvaluationState import com.forcetower.uefs.feature.evaluation.EvaluationViewModel import com.forcetower.uefs.feature.shared.UFragment import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber @AndroidEntryPoint class DisciplineEvaluationFragment : UFragment() { diff --git a/app/src/main/java/com/forcetower/uefs/feature/evaluation/home/EvaluationTopicAdapter.kt b/app/src/main/java/com/forcetower/uefs/feature/evaluation/home/EvaluationTopicAdapter.kt index 8e1c0914f..f3b1f8e2e 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/evaluation/home/EvaluationTopicAdapter.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/evaluation/home/EvaluationTopicAdapter.kt @@ -113,10 +113,14 @@ private data class DisciplineWrapper(val discipline: PublicHotEvaluationDiscipli sealed class EvaluationHolder(view: View) : RecyclerView.ViewHolder(view) { class EvaluationHeader(val binding: ItemEvaluationHeaderBinding) : EvaluationHolder(binding.root) class EvaluationDiscipline(val binding: ItemEvaluateDisciplineHomeBinding, interactor: HomeInteractor) : EvaluationHolder(binding.root) { - init { binding.interactor = interactor } + init { + binding.interactor = interactor + } } class EvaluationTeacher(val binding: ItemEvaluateTeacherHomeBinding, interactor: HomeInteractor) : EvaluationHolder(binding.root) { - init { binding.interactor = interactor } + init { + binding.interactor = interactor + } } } diff --git a/app/src/main/java/com/forcetower/uefs/feature/evaluation/home/HomeInteractor.kt b/app/src/main/java/com/forcetower/uefs/feature/evaluation/home/HomeInteractor.kt index 1db1872e3..48e6a0819 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/evaluation/home/HomeInteractor.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/evaluation/home/HomeInteractor.kt @@ -23,8 +23,6 @@ package com.forcetower.uefs.feature.evaluation.home import com.forcetower.uefs.core.model.edge.paradox.PublicHotEvaluationDiscipline import com.forcetower.uefs.core.model.edge.paradox.PublicHotEvaluationTeacher import com.forcetower.uefs.core.model.edge.paradox.PublicTeacherEvaluationData -import com.forcetower.uefs.core.model.service.EvaluationDiscipline -import com.forcetower.uefs.core.model.service.EvaluationTeacher interface HomeInteractor { fun onClickDiscipline(discipline: PublicHotEvaluationDiscipline) diff --git a/app/src/main/java/com/forcetower/uefs/feature/evaluation/rating/EvaluationRatingViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/evaluation/rating/EvaluationRatingViewModel.kt index ce0c6701f..29be5c4da 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/evaluation/rating/EvaluationRatingViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/evaluation/rating/EvaluationRatingViewModel.kt @@ -24,9 +24,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.forcetower.core.lifecycle.Event -import com.forcetower.uefs.core.model.unes.Question import com.forcetower.uefs.core.storage.repository.EvaluationRepository -import com.forcetower.uefs.core.storage.resource.Resource import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject diff --git a/app/src/main/java/com/forcetower/uefs/feature/evaluation/rating/RatingActivity.kt b/app/src/main/java/com/forcetower/uefs/feature/evaluation/rating/RatingActivity.kt index 4d1d2a8a8..81e82d74e 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/evaluation/rating/RatingActivity.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/evaluation/rating/RatingActivity.kt @@ -1,99 +1,98 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.evaluation.rating - -import android.os.Bundle -import android.view.View.GONE -import android.view.View.VISIBLE -import androidx.activity.viewModels -import androidx.databinding.DataBindingUtil -import androidx.lifecycle.Observer -import androidx.navigation.navArgs -import com.forcetower.core.lifecycle.EventObserver -import com.forcetower.uefs.R -import com.forcetower.uefs.core.model.unes.Question -import com.forcetower.uefs.core.storage.resource.Resource -import com.forcetower.uefs.databinding.ActivityEvaluationRatingBinding -import com.forcetower.uefs.feature.shared.FragmentAdapter -import com.forcetower.uefs.feature.shared.UActivity -import com.forcetower.uefs.feature.themeswitcher.ThemeOverlayUtils -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class RatingActivity : UActivity() { - private val viewModel: EvaluationRatingViewModel by viewModels() - private lateinit var binding: ActivityEvaluationRatingBinding - private lateinit var adapter: FragmentAdapter - private val args by navArgs() - private var currentData: List? = null - - override fun onCreate(savedInstanceState: Bundle?) { - ThemeOverlayUtils.applyThemeOverlays(this, intArrayOf(R.id.theme_feature_background_color)) - super.onCreate(savedInstanceState) - binding = DataBindingUtil.setContentView(this, R.layout.activity_evaluation_rating) - adapter = FragmentAdapter(supportFragmentManager) - binding.viewPager.adapter = adapter - - if (args.isTeacher) { - viewModel.initForTeacher(args.teacherId) -// viewModel.getQuestionsForTeacher(args.teacherId).observe(this, Observer { useResponse(it) }) - } else { - val code = args.code ?: "0" - val department = args.department ?: "0" - viewModel.initForDiscipline(code, department) -// viewModel.getQuestionsForDiscipline(code, department).observe(this, Observer { useResponse(it) }) - } - - viewModel.nextQuestion.observe( - this, - EventObserver { - val position = binding.viewPager.currentItem - val size = currentData?.size ?: 0 - if (position + 1 >= size) { - finish() - } else { - binding.viewPager.setCurrentItem(position + 1, true) - } - } - ) - } - - private fun useResponse(resource: Resource>) { - val data = resource.data - if (data != null) { - val additional = data.toMutableList().apply { add(Question(-2, "", "", last = true, teacher = false, discipline = false)) } - currentData = additional - binding.groupLoading.visibility = GONE - binding.viewPager.visibility = VISIBLE - createFragmentsList(additional) - } else { - binding.groupLoading.visibility = VISIBLE - binding.viewPager.visibility = GONE - } - } - - private fun createFragmentsList(data: List) { - val fragments = data.map { InternalQuestionFragment.newInstance(it) } - adapter.setItems(fragments) - } - - override fun shouldApplyThemeOverlay() = false -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.evaluation.rating + +import android.os.Bundle +import android.view.View.GONE +import android.view.View.VISIBLE +import androidx.activity.viewModels +import androidx.databinding.DataBindingUtil +import androidx.navigation.navArgs +import com.forcetower.core.lifecycle.EventObserver +import com.forcetower.uefs.R +import com.forcetower.uefs.core.model.unes.Question +import com.forcetower.uefs.core.storage.resource.Resource +import com.forcetower.uefs.databinding.ActivityEvaluationRatingBinding +import com.forcetower.uefs.feature.shared.FragmentAdapter +import com.forcetower.uefs.feature.shared.UActivity +import com.forcetower.uefs.feature.themeswitcher.ThemeOverlayUtils +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class RatingActivity : UActivity() { + private val viewModel: EvaluationRatingViewModel by viewModels() + private lateinit var binding: ActivityEvaluationRatingBinding + private lateinit var adapter: FragmentAdapter + private val args by navArgs() + private var currentData: List? = null + + override fun onCreate(savedInstanceState: Bundle?) { + ThemeOverlayUtils.applyThemeOverlays(this, intArrayOf(R.id.theme_feature_background_color)) + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_evaluation_rating) + adapter = FragmentAdapter(supportFragmentManager) + binding.viewPager.adapter = adapter + + if (args.isTeacher) { + viewModel.initForTeacher(args.teacherId) +// viewModel.getQuestionsForTeacher(args.teacherId).observe(this, Observer { useResponse(it) }) + } else { + val code = args.code ?: "0" + val department = args.department ?: "0" + viewModel.initForDiscipline(code, department) +// viewModel.getQuestionsForDiscipline(code, department).observe(this, Observer { useResponse(it) }) + } + + viewModel.nextQuestion.observe( + this, + EventObserver { + val position = binding.viewPager.currentItem + val size = currentData?.size ?: 0 + if (position + 1 >= size) { + finish() + } else { + binding.viewPager.setCurrentItem(position + 1, true) + } + } + ) + } + + private fun useResponse(resource: Resource>) { + val data = resource.data + if (data != null) { + val additional = data.toMutableList().apply { add(Question(-2, "", "", last = true, teacher = false, discipline = false)) } + currentData = additional + binding.groupLoading.visibility = GONE + binding.viewPager.visibility = VISIBLE + createFragmentsList(additional) + } else { + binding.groupLoading.visibility = VISIBLE + binding.viewPager.visibility = GONE + } + } + + private fun createFragmentsList(data: List) { + val fragments = data.map { InternalQuestionFragment.newInstance(it) } + adapter.setItems(fragments) + } + + override fun shouldApplyThemeOverlay() = false +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/evaluation/search/EvaluationEntityAdapter.kt b/app/src/main/java/com/forcetower/uefs/feature/evaluation/search/EvaluationEntityAdapter.kt index 77362d33f..a21f61538 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/evaluation/search/EvaluationEntityAdapter.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/evaluation/search/EvaluationEntityAdapter.kt @@ -26,7 +26,6 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.forcetower.uefs.R import com.forcetower.uefs.core.model.unes.EdgeParadoxSearchableItem -import com.forcetower.uefs.core.model.unes.EvaluationEntity import com.forcetower.uefs.databinding.ItemEvaluationSimpleEntityBinding import com.forcetower.uefs.feature.shared.inflate diff --git a/app/src/main/java/com/forcetower/uefs/feature/evaluation/search/SearchFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/evaluation/search/SearchFragment.kt index 2d5294522..666d26805 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/evaluation/search/SearchFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/evaluation/search/SearchFragment.kt @@ -37,9 +37,6 @@ import com.forcetower.uefs.R import com.forcetower.uefs.core.model.unes.EdgeParadoxSearchableItem import com.forcetower.uefs.core.model.unes.EdgeParadoxSearchableItem.Companion.DISCIPLINE_TYPE import com.forcetower.uefs.core.model.unes.EdgeParadoxSearchableItem.Companion.TEACHER_TYPE -import com.forcetower.uefs.core.model.unes.EvaluationEntity -import com.forcetower.uefs.core.storage.resource.Resource -import com.forcetower.uefs.core.storage.resource.Status import com.forcetower.uefs.databinding.FragmentEvaluationSearchBinding import com.forcetower.uefs.feature.evaluation.EvaluationState import com.forcetower.uefs.feature.evaluation.EvaluationViewModel diff --git a/app/src/main/java/com/forcetower/uefs/feature/evaluation/teacher/TeacherAdapter.kt b/app/src/main/java/com/forcetower/uefs/feature/evaluation/teacher/TeacherAdapter.kt index 6842dc871..25b847425 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/evaluation/teacher/TeacherAdapter.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/evaluation/teacher/TeacherAdapter.kt @@ -26,12 +26,8 @@ import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.forcetower.uefs.R -import com.forcetower.uefs.core.model.edge.paradox.PublicHotEvaluationDiscipline import com.forcetower.uefs.core.model.edge.paradox.PublicTeacherEvaluationCombinedData import com.forcetower.uefs.core.model.edge.paradox.PublicTeacherEvaluationData -import com.forcetower.uefs.core.model.service.EvaluationDiscipline -import com.forcetower.uefs.core.model.service.EvaluationTeacher -import com.forcetower.uefs.databinding.ItemEvaluateDisciplineHomeBinding import com.forcetower.uefs.databinding.ItemEvaluateDisciplineTeacherBinding import com.forcetower.uefs.databinding.ItemEvaluationTeacherGraphicsBinding import com.forcetower.uefs.databinding.ItemEvaluationTeacherMeanBinding diff --git a/app/src/main/java/com/forcetower/uefs/feature/evaluation/teacher/TeacherFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/evaluation/teacher/TeacherFragment.kt index 8b09d0482..65584b7bb 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/evaluation/teacher/TeacherFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/evaluation/teacher/TeacherFragment.kt @@ -25,14 +25,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.viewModels -import androidx.lifecycle.Observer import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.forcetower.core.lifecycle.EventObserver import com.forcetower.uefs.core.model.edge.paradox.PublicTeacherEvaluationCombinedData -import com.forcetower.uefs.core.model.service.EvaluationTeacher -import com.forcetower.uefs.core.storage.resource.Resource -import com.forcetower.uefs.core.storage.resource.Status import com.forcetower.uefs.databinding.FragmentEvaluateTeacherBinding import com.forcetower.uefs.feature.evaluation.EvaluationState import com.forcetower.uefs.feature.evaluation.EvaluationViewModel diff --git a/app/src/main/java/com/forcetower/uefs/feature/feedback/FeedbackViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/feedback/FeedbackViewModel.kt index 8584bbfcf..7c6255362 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/feedback/FeedbackViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/feedback/FeedbackViewModel.kt @@ -1,51 +1,48 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.feedback - -import android.content.Context -import androidx.annotation.MainThread -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import com.forcetower.core.lifecycle.Event -import com.forcetower.uefs.R -import com.forcetower.uefs.core.storage.repository.FeedbackRepository -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject - -@HiltViewModel -class FeedbackViewModel @Inject constructor( - private val repository: FeedbackRepository, -) : ViewModel() { - private val _sendFeedback = MediatorLiveData>() - val sendFeedback: LiveData> - get() = _sendFeedback - - private val _textError = MutableLiveData>() - val textError: LiveData> - get() = _textError - - @MainThread - fun onSendFeedback(text: String?) { - - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.feedback + +import androidx.annotation.MainThread +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.forcetower.core.lifecycle.Event +import com.forcetower.uefs.core.storage.repository.FeedbackRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class FeedbackViewModel @Inject constructor( + private val repository: FeedbackRepository +) : ViewModel() { + private val _sendFeedback = MediatorLiveData>() + val sendFeedback: LiveData> + get() = _sendFeedback + + private val _textError = MutableLiveData>() + val textError: LiveData> + get() = _textError + + @MainThread + fun onSendFeedback(text: String?) { + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/flowchart/CourseAdapter.kt b/app/src/main/java/com/forcetower/uefs/feature/flowchart/CourseAdapter.kt index f29ce944b..0f9ddb0cf 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/flowchart/CourseAdapter.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/flowchart/CourseAdapter.kt @@ -46,7 +46,9 @@ class CourseAdapter( val binding: ItemFlowchartCourseBinding, interactor: FlowchartInteractor ) : RecyclerView.ViewHolder(binding.root) { - init { binding.interactor = interactor } + init { + binding.interactor = interactor + } } private object DiffCallback : DiffUtil.ItemCallback() { diff --git a/app/src/main/java/com/forcetower/uefs/feature/flowchart/discipline/DisciplineDetailsAdapter.kt b/app/src/main/java/com/forcetower/uefs/feature/flowchart/discipline/DisciplineDetailsAdapter.kt index 60f1e67a2..0e37f3aff 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/flowchart/discipline/DisciplineDetailsAdapter.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/flowchart/discipline/DisciplineDetailsAdapter.kt @@ -123,8 +123,9 @@ class DisciplineDetailsAdapter( if (item != null) { result += Header(item, semester) - if (item.program != null) + if (item.program != null) { result += Resume(item) + } } val mapped = default.groupBy { it.type } @@ -144,7 +145,9 @@ class DisciplineDetailsAdapter( class ResumeHolder(val binding: ItemFlowchartDisciplineProgramBinding) : DisciplineDetailsHolder(binding.root) class CategoryHolder(val binding: ItemFlowchartDisciplineGroupingBinding) : DisciplineDetailsHolder(binding.root) class DisciplineHolder(val binding: ItemFlowchartDisciplineMinifiedBinding, interactor: DisciplineInteractor) : DisciplineDetailsHolder(binding.root) { - init { binding.interactor = interactor } + init { + binding.interactor = interactor + } } } diff --git a/app/src/main/java/com/forcetower/uefs/feature/flowchart/discipline/DisciplineFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/flowchart/discipline/DisciplineFragment.kt index 7a75cd8ed..2c38eb40c 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/flowchart/discipline/DisciplineFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/flowchart/discipline/DisciplineFragment.kt @@ -79,10 +79,11 @@ class DisciplineFragment : UFragment() { private fun onRequirementSelected(requirement: FlowchartRequirementUI) { if (requirement.requiredDisciplineId != null) { - val id = if (requirement.type == getString(R.string.flowchart_recursive_unlock)) + val id = if (requirement.type == getString(R.string.flowchart_recursive_unlock)) { requirement.disciplineId - else + } else { requirement.requiredDisciplineId + } val direction = DisciplineFragmentDirections.actionDisciplineSelf(id) findNavController().navigate(direction) } diff --git a/app/src/main/java/com/forcetower/uefs/feature/flowchart/semester/DisciplinesAdapter.kt b/app/src/main/java/com/forcetower/uefs/feature/flowchart/semester/DisciplinesAdapter.kt index 79321933e..b1cddc0a2 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/flowchart/semester/DisciplinesAdapter.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/flowchart/semester/DisciplinesAdapter.kt @@ -45,7 +45,9 @@ class DisciplinesAdapter( } class DisciplineHolder(val binding: ItemFlowchartDisciplineBinding, interactor: DisciplineInteractor) : RecyclerView.ViewHolder(binding.root) { - init { binding.interactor = interactor } + init { + binding.interactor = interactor + } } private object DiffCallback : DiffUtil.ItemCallback() { diff --git a/app/src/main/java/com/forcetower/uefs/feature/forms/FormsViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/forms/FormsViewModel.kt index dce4acff7..3fc3e8fe9 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/forms/FormsViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/forms/FormsViewModel.kt @@ -26,8 +26,8 @@ import androidx.lifecycle.ViewModel import com.forcetower.core.lifecycle.Event import com.forcetower.uefs.core.storage.repository.FormsRepository import dagger.hilt.android.lifecycle.HiltViewModel -import timber.log.Timber import javax.inject.Inject +import timber.log.Timber @HiltViewModel class FormsViewModel @Inject constructor( diff --git a/app/src/main/java/com/forcetower/uefs/feature/home/HomeActivity.kt b/app/src/main/java/com/forcetower/uefs/feature/home/HomeActivity.kt index 6d479510d..0cb0847c1 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/home/HomeActivity.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/home/HomeActivity.kt @@ -76,16 +76,19 @@ import com.google.android.play.core.review.ReviewManagerFactory import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.remoteconfig.FirebaseRemoteConfig import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject @AndroidEntryPoint class HomeActivity : UGameActivity() { @Inject lateinit var preferences: SharedPreferences + @Inject lateinit var analytics: FirebaseAnalytics + @Inject lateinit var remoteConfig: FirebaseRemoteConfig + @Inject lateinit var executors: AppExecutors private lateinit var reviewManager: ReviewManager @@ -262,10 +265,9 @@ class HomeActivity : UGameActivity() { viewModel.accessToken.observe(this) { onAccessTokenUpdate(it) } viewModel.snackbarMessage.observe(this, EventObserver { showSnack(it) }) dynamicDFMViewModel.snackbarMessage.observe(this, EventObserver { showSnack(it) }) - viewModel.sendToken() if (preferences.isStudentFromUEFS()) { // Update and unlock achievements for participating in a class with the creator - viewModel.connectToServiceIfNeeded() + viewModel.performServerSync() // viewModel.goodCookies() disciplineViewModel.prepareAndSendStats() viewModel.getMeProfile() @@ -344,10 +346,11 @@ class HomeActivity : UGameActivity() { override fun checkAchievements() { adventureViewModel.checkAchievements().observe(this) { it.entries.forEach { achievement -> - if (achievement.value == -1) + if (achievement.value == -1) { unlockAchievement(achievement.key) - else + } else { updateAchievementProgress(achievement.key, achievement.value) + } } } checkServerAchievements() @@ -368,7 +371,9 @@ class HomeActivity : UGameActivity() { } else { unlockAchievement(achievement.identifier) } - } catch (error: Throwable) { Timber.e(error, "Failed to unlock achievement ${achievement.identifier}") } + } catch (error: Throwable) { + Timber.e(error, "Failed to unlock achievement ${achievement.identifier}") + } } } } diff --git a/app/src/main/java/com/forcetower/uefs/feature/home/HomeBottomFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/home/HomeBottomFragment.kt index b67a0c455..6a51f7612 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/home/HomeBottomFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/home/HomeBottomFragment.kt @@ -1,235 +1,235 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.home - -import android.content.SharedPreferences -import android.net.Uri -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.IdRes -import androidx.core.content.ContextCompat -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.ui.NavigationUI -import com.bumptech.glide.Glide -import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions -import com.canhub.cropper.CropImageContract -import com.canhub.cropper.CropImageContractOptions -import com.canhub.cropper.CropImageOptions -import com.canhub.cropper.CropImageView -import com.forcetower.core.utils.ColorUtils -import com.forcetower.uefs.BuildConfig -import com.forcetower.uefs.R -import com.forcetower.uefs.core.model.unes.Account -import com.forcetower.uefs.core.model.unes.Course -import com.forcetower.uefs.core.model.unes.EdgeServiceAccount -import com.forcetower.uefs.core.util.isStudentFromUEFS -import com.forcetower.uefs.databinding.HomeBottomBinding -import com.forcetower.uefs.feature.about.AboutActivity -import com.forcetower.uefs.feature.feedback.SendFeedbackFragment -import com.forcetower.uefs.feature.settings.SettingsActivity -import com.forcetower.uefs.feature.setup.CourseSelectionCallback -import com.forcetower.uefs.feature.setup.SelectCourseDialog -import com.forcetower.uefs.feature.shared.UFragment -import com.forcetower.uefs.feature.shared.getPixelsFromDp -import com.google.firebase.remoteconfig.FirebaseRemoteConfig -import com.mikepenz.aboutlibraries.LibsBuilder -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -@AndroidEntryPoint -class HomeBottomFragment : UFragment() { - @Inject lateinit var remoteConfig: FirebaseRemoteConfig - @Inject lateinit var preferences: SharedPreferences - - private val pickImageContract = registerForActivityResult(ActivityResultContracts.GetContent()) { - onContentSelected(it) - } - - private val cropImage = registerForActivityResult(CropImageContract()) { - onCropResults(it) - } - - private lateinit var binding: HomeBottomBinding - private val viewModel: HomeViewModel by activityViewModels() - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return HomeBottomBinding.inflate(inflater, container, false).also { - binding = it - }.apply { - lifecycleOwner = this@HomeBottomFragment - viewModel = this@HomeBottomFragment.viewModel - executePendingBindings() - textUserName.setOnClickListener { editCourse() } - textScore.setOnClickListener { editCourse() } - }.root - } - - private fun editCourse() { - if (!preferences.isStudentFromUEFS()) return - val dialog = SelectCourseDialog() - dialog.setCallback( - object : CourseSelectionCallback { - override fun onSelected(course: Course) { - viewModel.setSelectedCourse(course) - } - } - ) - dialog.show(childFragmentManager, "dialog_course") - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setupNavigation() - featureFlags() - viewModel.databaseAccount.observe(viewLifecycleOwner) { handleAccount(it) } - } - - private fun handleAccount(account: EdgeServiceAccount?) { - binding.account = account - } - - private fun featureFlags() { - // This feature is broken, i think - // toggleItem(R.id.demand, false) - - val uefsStudent = preferences.isStudentFromUEFS() - - val documentsFlag = remoteConfig.getBoolean("feature_flag_documents") || BuildConfig.DEBUG - toggleItem(R.id.documents, documentsFlag) - - val storeFlag = remoteConfig.getBoolean("feature_flag_store") - toggleItem(R.id.purchases, storeFlag) - - val hourglass = remoteConfig.getBoolean("feature_flag_evaluation") && uefsStudent - toggleItem(R.id.evaluation, hourglass) - - val bigTray = remoteConfig.getBoolean("feature_flag_big_tray") && uefsStudent - toggleItem(R.id.big_tray, bigTray) - - val themeSwitcher = remoteConfig.getBoolean("feature_flag_theme_switcher") - toggleItem(R.id.theme_switcher, themeSwitcher) - - val campusMap = (remoteConfig.getBoolean("feature_flag_campus_map") || BuildConfig.VERSION_NAME.contains("-beta")) && uefsStudent - val campusPreference = preferences.getBoolean("stg_advanced_maps_install", true) - toggleItem(R.id.campus_map, campusMap && campusPreference) - - toggleItem(R.id.adventure, uefsStudent) - toggleItem(R.id.events, uefsStudent) - toggleItem(R.id.flowchart, uefsStudent) - } - - private fun toggleItem(@IdRes id: Int, visible: Boolean) { - val item = binding.navigationView.menu.findItem(id) - item?.isVisible = visible - } - - private fun setupNavigation() { - binding.navigationView.setNavigationItemSelectedListener { item -> - when (item.itemId) { - R.id.about -> { - AboutActivity.startActivity(requireActivity()) - true - } - R.id.logout -> { - val fragment = LogoutConfirmationFragment() - fragment.show(childFragmentManager, "logout_modal") - true - } - R.id.open_source -> { - LibsBuilder() - .withEdgeToEdge(true) - .withAboutIconShown(true) - .withAboutVersionShown(true) - .withAboutDescription(getString(R.string.about_description)) - .start(requireContext()) - true - } - R.id.settings -> { - startActivity(SettingsActivity.startIntent(requireContext())) - true - } - R.id.bug_report -> { - val fragment = SendFeedbackFragment() - fragment.show(childFragmentManager, "feedback_modal") - true - } - R.id.campus_map -> { - findNavController().navigate(R.id.campus_map) - true - } - else -> { - NavigationUI.onNavDestinationSelected(item, findNavController()) - } - } - } - } - - private fun onImagePicked(uri: Uri) { - viewModel.setSelectedImage(uri) - Glide.with(requireContext()) - .load(uri) - .fallback(com.forcetower.core.R.mipmap.ic_unes_large_image_512) - .placeholder(com.forcetower.core.R.mipmap.ic_unes_large_image_512) - .circleCrop() - .transition(DrawableTransitionOptions.withCrossFade()) - .into(binding.imageUserPicture) - - viewModel.uploadImageToStorage() - } - - private fun pickImage() { - pickImageContract.launch("image/*") - } - - private fun onContentSelected(uri: Uri?) { - uri ?: return - val bg = ColorUtils.modifyAlpha(ContextCompat.getColor(requireContext(), R.color.colorPrimary), 120) - val ac = ContextCompat.getColor(requireContext(), R.color.colorAccent) - - val options = CropImageContractOptions( - uri, - CropImageOptions( - fixAspectRatio = true, - aspectRatioX = 1, - aspectRatioY = 1, - cropShape = CropImageView.CropShape.OVAL, - backgroundColor = bg, - borderLineColor = ac, - borderCornerColor = ac, - activityMenuIconColor = ac, - borderLineThickness = getPixelsFromDp(requireContext(), 2), - activityTitle = getString(R.string.cut_profile_image), - guidelines = CropImageView.Guidelines.OFF - ) - ) - - cropImage.launch(options) - } - - private fun onCropResults(result: CropImageView.CropResult) { - val imageUri = result.uriContent ?: return - onImagePicked(imageUri) - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.home + +import android.content.SharedPreferences +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.IdRes +import androidx.core.content.ContextCompat +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import androidx.navigation.ui.NavigationUI +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions +import com.canhub.cropper.CropImageContract +import com.canhub.cropper.CropImageContractOptions +import com.canhub.cropper.CropImageOptions +import com.canhub.cropper.CropImageView +import com.forcetower.core.utils.ColorUtils +import com.forcetower.uefs.BuildConfig +import com.forcetower.uefs.R +import com.forcetower.uefs.core.model.unes.Course +import com.forcetower.uefs.core.model.unes.EdgeServiceAccount +import com.forcetower.uefs.core.util.isStudentFromUEFS +import com.forcetower.uefs.databinding.HomeBottomBinding +import com.forcetower.uefs.feature.about.AboutActivity +import com.forcetower.uefs.feature.feedback.SendFeedbackFragment +import com.forcetower.uefs.feature.settings.SettingsActivity +import com.forcetower.uefs.feature.setup.CourseSelectionCallback +import com.forcetower.uefs.feature.setup.SelectCourseDialog +import com.forcetower.uefs.feature.shared.UFragment +import com.forcetower.uefs.feature.shared.getPixelsFromDp +import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import com.mikepenz.aboutlibraries.LibsBuilder +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class HomeBottomFragment : UFragment() { + @Inject lateinit var remoteConfig: FirebaseRemoteConfig + + @Inject lateinit var preferences: SharedPreferences + + private val pickImageContract = registerForActivityResult(ActivityResultContracts.GetContent()) { + onContentSelected(it) + } + + private val cropImage = registerForActivityResult(CropImageContract()) { + onCropResults(it) + } + + private lateinit var binding: HomeBottomBinding + private val viewModel: HomeViewModel by activityViewModels() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return HomeBottomBinding.inflate(inflater, container, false).also { + binding = it + }.apply { + lifecycleOwner = this@HomeBottomFragment + viewModel = this@HomeBottomFragment.viewModel + executePendingBindings() + textUserName.setOnClickListener { editCourse() } + textScore.setOnClickListener { editCourse() } + }.root + } + + private fun editCourse() { + if (!preferences.isStudentFromUEFS()) return + val dialog = SelectCourseDialog() + dialog.setCallback( + object : CourseSelectionCallback { + override fun onSelected(course: Course) { + viewModel.setSelectedCourse(course) + } + } + ) + dialog.show(childFragmentManager, "dialog_course") + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupNavigation() + featureFlags() + viewModel.databaseAccount.observe(viewLifecycleOwner) { handleAccount(it) } + } + + private fun handleAccount(account: EdgeServiceAccount?) { + binding.account = account + } + + private fun featureFlags() { + // This feature is broken, i think + // toggleItem(R.id.demand, false) + + val uefsStudent = preferences.isStudentFromUEFS() + + val documentsFlag = remoteConfig.getBoolean("feature_flag_documents") || BuildConfig.DEBUG + toggleItem(R.id.documents, documentsFlag) + + val storeFlag = remoteConfig.getBoolean("feature_flag_store") + toggleItem(R.id.purchases, storeFlag) + + val hourglass = remoteConfig.getBoolean("feature_flag_evaluation") && uefsStudent + toggleItem(R.id.evaluation, hourglass) + + val bigTray = remoteConfig.getBoolean("feature_flag_big_tray") && uefsStudent + toggleItem(R.id.big_tray, bigTray) + + val themeSwitcher = remoteConfig.getBoolean("feature_flag_theme_switcher") + toggleItem(R.id.theme_switcher, themeSwitcher) + + val campusMap = (remoteConfig.getBoolean("feature_flag_campus_map") || BuildConfig.VERSION_NAME.contains("-beta")) && uefsStudent + val campusPreference = preferences.getBoolean("stg_advanced_maps_install", true) + toggleItem(R.id.campus_map, campusMap && campusPreference) + + toggleItem(R.id.adventure, uefsStudent) + toggleItem(R.id.events, uefsStudent) + toggleItem(R.id.flowchart, uefsStudent) + } + + private fun toggleItem(@IdRes id: Int, visible: Boolean) { + val item = binding.navigationView.menu.findItem(id) + item?.isVisible = visible + } + + private fun setupNavigation() { + binding.navigationView.setNavigationItemSelectedListener { item -> + when (item.itemId) { + R.id.about -> { + AboutActivity.startActivity(requireActivity()) + true + } + R.id.logout -> { + val fragment = LogoutConfirmationFragment() + fragment.show(childFragmentManager, "logout_modal") + true + } + R.id.open_source -> { + LibsBuilder() + .withEdgeToEdge(true) + .withAboutIconShown(true) + .withAboutVersionShown(true) + .withAboutDescription(getString(R.string.about_description)) + .start(requireContext()) + true + } + R.id.settings -> { + startActivity(SettingsActivity.startIntent(requireContext())) + true + } + R.id.bug_report -> { + val fragment = SendFeedbackFragment() + fragment.show(childFragmentManager, "feedback_modal") + true + } + R.id.campus_map -> { + findNavController().navigate(R.id.campus_map) + true + } + else -> { + NavigationUI.onNavDestinationSelected(item, findNavController()) + } + } + } + } + + private fun onImagePicked(uri: Uri) { + viewModel.setSelectedImage(uri) + Glide.with(requireContext()) + .load(uri) + .fallback(com.forcetower.core.R.mipmap.ic_unes_large_image_512) + .placeholder(com.forcetower.core.R.mipmap.ic_unes_large_image_512) + .circleCrop() + .transition(DrawableTransitionOptions.withCrossFade()) + .into(binding.imageUserPicture) + + viewModel.uploadImageToStorage() + } + + private fun pickImage() { + pickImageContract.launch("image/*") + } + + private fun onContentSelected(uri: Uri?) { + uri ?: return + val bg = ColorUtils.modifyAlpha(ContextCompat.getColor(requireContext(), R.color.colorPrimary), 120) + val ac = ContextCompat.getColor(requireContext(), R.color.colorAccent) + + val options = CropImageContractOptions( + uri, + CropImageOptions( + fixAspectRatio = true, + aspectRatioX = 1, + aspectRatioY = 1, + cropShape = CropImageView.CropShape.OVAL, + backgroundColor = bg, + borderLineColor = ac, + borderCornerColor = ac, + activityMenuIconColor = ac, + borderLineThickness = getPixelsFromDp(requireContext(), 2), + activityTitle = getString(R.string.cut_profile_image), + guidelines = CropImageView.Guidelines.OFF + ) + ) + + cropImage.launch(options) + } + + private fun onCropResults(result: CropImageView.CropResult) { + val imageUri = result.uriContent ?: return + onImagePicked(imageUri) + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/home/HomeViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/home/HomeViewModel.kt index 52c513f63..b2441fe6f 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/home/HomeViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/home/HomeViewModel.kt @@ -1,234 +1,233 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.home - -import android.app.Application -import android.net.Uri -import androidx.annotation.MainThread -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.asLiveData -import androidx.lifecycle.viewModelScope -import com.forcetower.core.lifecycle.Event -import com.forcetower.uefs.core.model.unes.Access -import com.forcetower.uefs.core.model.unes.AccessToken -import com.forcetower.uefs.core.model.unes.Account -import com.forcetower.uefs.core.model.unes.Course -import com.forcetower.uefs.core.model.unes.Message -import com.forcetower.uefs.core.model.unes.Profile -import com.forcetower.uefs.core.model.unes.SagresFlags -import com.forcetower.uefs.core.model.unes.Semester -import com.forcetower.uefs.core.storage.repository.AccountRepository -import com.forcetower.uefs.core.storage.repository.FirebaseMessageRepository -import com.forcetower.uefs.core.storage.repository.LoginSagresRepository -import com.forcetower.uefs.core.storage.repository.ProfileRepository -import com.forcetower.uefs.core.storage.repository.SagresDataRepository -import com.forcetower.uefs.core.storage.repository.UserSessionRepository -import com.forcetower.uefs.core.storage.repository.cloud.AffinityQuestionRepository -import com.forcetower.uefs.core.storage.repository.cloud.AuthRepository -import com.forcetower.uefs.core.storage.repository.cloud.EdgeAccountRepository -import com.forcetower.uefs.core.storage.repository.cloud.EdgeSyncRepository -import com.forcetower.uefs.core.storage.resource.Resource -import com.forcetower.uefs.core.storage.resource.Status -import com.forcetower.uefs.core.task.FetchMissingSemestersUseCase -import com.forcetower.uefs.core.work.image.UploadImageToStorage -import com.forcetower.uefs.domain.usecase.auth.EdgeAnonymousLoginUseCase -import com.forcetower.uefs.easter.darktheme.DarkThemeRepository -import com.google.android.play.core.install.model.AppUpdateType -import com.google.android.play.core.install.model.InstallStatus -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Named - -@HiltViewModel -class HomeViewModel @Inject constructor( - private val loginSagresRepository: LoginSagresRepository, - private val dataRepository: SagresDataRepository, - private val firebaseMessageRepository: FirebaseMessageRepository, - private val darkThemeRepository: DarkThemeRepository, - private val authRepository: AuthRepository, - private val profileRepository: ProfileRepository, - application: Application, - private val sessionRepository: UserSessionRepository, - private val accountRepository: AccountRepository, - private val edgeAccountRepository: EdgeAccountRepository, - private val affinityRepository: AffinityQuestionRepository, - private val fetchMissingSemesters: FetchMissingSemestersUseCase, - @Named("flagSnowpiercerEnabled") - private val snowpiercerEnabled: Boolean, - private val anonymousLoginUseCase: EdgeAnonymousLoginUseCase, - private val edgeSyncRepository: EdgeSyncRepository -) : AndroidViewModel(application) { - private var selectImageUri: Uri? = null - - @AppUpdateType - var updateType: Int? = null - - private val _snackbar = MutableLiveData>() - val snackbarMessage: LiveData> - get() = _snackbar - - private val _passwordChangeProcess = MediatorLiveData>>() - val passwordChangeProcess: LiveData>> - get() = _passwordChangeProcess - - private val _inAppUpdateStatus = MutableLiveData() - val inAppUpdateStatus: LiveData - get() = _inAppUpdateStatus - - private val _onMoveToSchedule = MutableLiveData>() - val onMoveToSchedule: LiveData> - get() = _onMoveToSchedule - - val access: LiveData by lazy { loginSagresRepository.getAccess() } - val accessToken: LiveData by lazy { accountRepository.getAccessToken() } - val profile: LiveData by lazy { loginSagresRepository.getProfileMe() } - val messages: LiveData> by lazy { dataRepository.getMessages() } - val semesters: LiveData> by lazy { dataRepository.getSemesters() } - val course: LiveData by lazy { dataRepository.getCourse() } - val account: LiveData> = accountRepository.getAccount() - val databaseAccount = edgeAccountRepository.getAccount().asLiveData() - val flags: LiveData by lazy { dataRepository.getFlags() } - val scheduleHideCount: LiveData = dataRepository.getScheduleHideCount() - - fun uploadImageToStorage() { - val uri = selectImageUri - uri ?: return - UploadImageToStorage.createWorker(getApplication(), uri) - } - - fun setSelectedImage(uri: Uri) { - selectImageUri = uri - } - - fun logout() { - dataRepository.logout() - viewModelScope.launch { - dataRepository.logoutSuspend() - } - } - - fun showSnack(message: String) { - _snackbar.value = Event(message) - } - - fun subscribeToTopics(vararg topics: String) { - firebaseMessageRepository.subscribe(topics) - } - - fun verifyDarkTheme() = darkThemeRepository.getPreconditions() - - fun lightWeightCalcScore() = dataRepository.lightweightCalcScore() - - fun changeAccessValidation(valid: Boolean) = dataRepository.changeAccessValidation(valid) - - fun attemptNewPasswordLogin(password: String, token: String? = null) { - if (!snowpiercerEnabled) { - val source = dataRepository.attemptLoginWithNewPassword(password, token) - _passwordChangeProcess.addSource(source) { - if (it.status == Status.SUCCESS) { - _passwordChangeProcess.removeSource(source) - } - _passwordChangeProcess.value = Event(it) - } - } else { - viewModelScope.launch { - _passwordChangeProcess.value = Event(Resource.loading(null)) - val result = dataRepository.loginWithNewPasswordSuspend(password) - _passwordChangeProcess.value = Event(Resource.success(result)) - } - } - } - - fun connectToServiceIfNeeded() { - authRepository.performAccountSyncStateIfNeededAsync() - viewModelScope.launch { - runCatching { - anonymousLoginUseCase.prepareAndLogin() - edgeAccountRepository.fetchAccountIfNeeded() - edgeSyncRepository.syncDataIfNeeded() - }.onFailure { - Timber.e(it, "Failed to authenticate or fetch account.") - } - } - } - - fun sendToken() { - viewModelScope.launch { - firebaseMessageRepository.sendNewTokenOrNot() - } - } - - fun setSelectedCourse(course: Course) { - profileRepository.updateUserCourse(course) - } - - fun onSessionStarted() { - sessionRepository.onSessionStartedAsync() - } - - fun onUserInteraction() { - sessionRepository.onUserInteractionAsync() - } - - fun onUserClickedAd() { - sessionRepository.onUserClickedAdAsync() - } - - fun onUserAdImpression() { - sessionRepository.onUserAdImpressionAsync() - } - - fun getAccountSync(): Account? { - return accountRepository.getAccountSync() - } - - fun onSyncSessions() { - sessionRepository.onSyncSessionsAsync() - } - - fun setCurrentUpdateState(@InstallStatus installStatus: Int) { - _inAppUpdateStatus.value = installStatus - } - - fun getMeProfile() { - profileRepository.getMeProfileAsync() - } - - fun onMoveToSchedule() { - _onMoveToSchedule.value = Event(Unit) - } - - fun getAffinityQuestions() { - affinityRepository.getAffinityQuestionsAsync() - } - - fun checkMissingSemesters() { - if (!snowpiercerEnabled) return - viewModelScope.launch { - fetchMissingSemesters(Unit) - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.home + +import android.app.Application +import android.net.Uri +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import com.forcetower.core.lifecycle.Event +import com.forcetower.uefs.core.model.unes.Access +import com.forcetower.uefs.core.model.unes.AccessToken +import com.forcetower.uefs.core.model.unes.Account +import com.forcetower.uefs.core.model.unes.Course +import com.forcetower.uefs.core.model.unes.Message +import com.forcetower.uefs.core.model.unes.Profile +import com.forcetower.uefs.core.model.unes.SagresFlags +import com.forcetower.uefs.core.model.unes.Semester +import com.forcetower.uefs.core.storage.repository.AccountRepository +import com.forcetower.uefs.core.storage.repository.FirebaseMessageRepository +import com.forcetower.uefs.core.storage.repository.LoginSagresRepository +import com.forcetower.uefs.core.storage.repository.ProfileRepository +import com.forcetower.uefs.core.storage.repository.SagresDataRepository +import com.forcetower.uefs.core.storage.repository.UserSessionRepository +import com.forcetower.uefs.core.storage.repository.cloud.AffinityQuestionRepository +import com.forcetower.uefs.core.storage.repository.cloud.AuthRepository +import com.forcetower.uefs.core.storage.repository.cloud.EdgeAccountRepository +import com.forcetower.uefs.core.storage.repository.cloud.EdgeSyncRepository +import com.forcetower.uefs.core.storage.resource.Resource +import com.forcetower.uefs.core.storage.resource.Status +import com.forcetower.uefs.core.task.FetchMissingSemestersUseCase +import com.forcetower.uefs.core.work.image.UploadImageToStorage +import com.forcetower.uefs.domain.usecase.auth.EdgeAnonymousLoginUseCase +import com.forcetower.uefs.easter.darktheme.DarkThemeRepository +import com.google.android.play.core.install.model.AppUpdateType +import com.google.android.play.core.install.model.InstallStatus +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import javax.inject.Named +import kotlinx.coroutines.launch +import timber.log.Timber + +@HiltViewModel +class HomeViewModel @Inject constructor( + private val loginSagresRepository: LoginSagresRepository, + private val dataRepository: SagresDataRepository, + private val firebaseMessageRepository: FirebaseMessageRepository, + private val darkThemeRepository: DarkThemeRepository, + private val authRepository: AuthRepository, + private val profileRepository: ProfileRepository, + application: Application, + private val sessionRepository: UserSessionRepository, + private val accountRepository: AccountRepository, + private val edgeAccountRepository: EdgeAccountRepository, + private val affinityRepository: AffinityQuestionRepository, + private val fetchMissingSemesters: FetchMissingSemestersUseCase, + @Named("flagSnowpiercerEnabled") + private val snowpiercerEnabled: Boolean, + private val anonymousLoginUseCase: EdgeAnonymousLoginUseCase, + private val edgeSyncRepository: EdgeSyncRepository +) : AndroidViewModel(application) { + private var selectImageUri: Uri? = null + + @AppUpdateType + var updateType: Int? = null + + private val _snackbar = MutableLiveData>() + val snackbarMessage: LiveData> + get() = _snackbar + + private val _passwordChangeProcess = MediatorLiveData>>() + val passwordChangeProcess: LiveData>> + get() = _passwordChangeProcess + + private val _inAppUpdateStatus = MutableLiveData() + val inAppUpdateStatus: LiveData + get() = _inAppUpdateStatus + + private val _onMoveToSchedule = MutableLiveData>() + val onMoveToSchedule: LiveData> + get() = _onMoveToSchedule + + val access: LiveData by lazy { loginSagresRepository.getAccess() } + val accessToken: LiveData by lazy { accountRepository.getAccessToken() } + val profile: LiveData by lazy { loginSagresRepository.getProfileMe() } + val messages: LiveData> by lazy { dataRepository.getMessages() } + val semesters: LiveData> by lazy { dataRepository.getSemesters() } + val course: LiveData by lazy { dataRepository.getCourse() } + val account: LiveData> = accountRepository.getAccount() + val databaseAccount = edgeAccountRepository.getAccount().asLiveData() + val flags: LiveData by lazy { dataRepository.getFlags() } + val scheduleHideCount: LiveData = dataRepository.getScheduleHideCount() + + fun uploadImageToStorage() { + val uri = selectImageUri + uri ?: return + UploadImageToStorage.createWorker(getApplication(), uri) + } + + fun setSelectedImage(uri: Uri) { + selectImageUri = uri + } + + fun logout() { + dataRepository.logout() + viewModelScope.launch { + dataRepository.logoutSuspend() + } + } + + fun showSnack(message: String) { + _snackbar.value = Event(message) + } + + fun subscribeToTopics(vararg topics: String) { + firebaseMessageRepository.subscribe(topics) + } + + fun verifyDarkTheme() = darkThemeRepository.getPreconditions() + + fun lightWeightCalcScore() = dataRepository.lightweightCalcScore() + + fun changeAccessValidation(valid: Boolean) = dataRepository.changeAccessValidation(valid) + + fun attemptNewPasswordLogin(password: String, token: String? = null) { + if (!snowpiercerEnabled) { + val source = dataRepository.attemptLoginWithNewPassword(password, token) + _passwordChangeProcess.addSource(source) { + if (it.status == Status.SUCCESS) { + _passwordChangeProcess.removeSource(source) + } + _passwordChangeProcess.value = Event(it) + } + } else { + viewModelScope.launch { + _passwordChangeProcess.value = Event(Resource.loading(null)) + val result = dataRepository.loginWithNewPasswordSuspend(password) + _passwordChangeProcess.value = Event(Resource.success(result)) + } + } + } + + fun performServerSync() { + authRepository.performAccountSyncStateIfNeededAsync() + viewModelScope.launch { + runCatching { + anonymousLoginUseCase.prepareAndLogin() + edgeAccountRepository.fetchAccountIfNeeded() + edgeSyncRepository.syncDataIfNeeded() + }.onFailure { + Timber.e(it, "Failed to authenticate or fetch account.") + } + + runCatching { + firebaseMessageRepository.sendNewTokenOrNot() + }.onFailure { + Timber.e(it, "Failed to send FCM token") + } + } + } + + fun setSelectedCourse(course: Course) { + profileRepository.updateUserCourse(course) + } + + fun onSessionStarted() { + sessionRepository.onSessionStartedAsync() + } + + fun onUserInteraction() { + sessionRepository.onUserInteractionAsync() + } + + fun onUserClickedAd() { + sessionRepository.onUserClickedAdAsync() + } + + fun onUserAdImpression() { + sessionRepository.onUserAdImpressionAsync() + } + + fun getAccountSync(): Account? { + return accountRepository.getAccountSync() + } + + fun onSyncSessions() { + sessionRepository.onSyncSessionsAsync() + } + + fun setCurrentUpdateState(@InstallStatus installStatus: Int) { + _inAppUpdateStatus.value = installStatus + } + + fun getMeProfile() { + profileRepository.getMeProfileAsync() + } + + fun onMoveToSchedule() { + _onMoveToSchedule.value = Event(Unit) + } + + fun getAffinityQuestions() { + affinityRepository.getAffinityQuestionsAsync() + } + + fun checkMissingSemesters() { + if (!snowpiercerEnabled) return + viewModelScope.launch { + fetchMissingSemesters(Unit) + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/home/InvalidAccessDialog.kt b/app/src/main/java/com/forcetower/uefs/feature/home/InvalidAccessDialog.kt index 45bb908ec..2374a44e5 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/home/InvalidAccessDialog.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/home/InvalidAccessDialog.kt @@ -1,171 +1,172 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.home - -import android.app.Dialog -import android.content.SharedPreferences -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.View.INVISIBLE -import android.view.View.VISIBLE -import android.view.ViewGroup -import android.widget.FrameLayout -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.lifecycleScope -import com.forcetower.core.lifecycle.EventObserver -import com.forcetower.sagres.Constants -import com.forcetower.uefs.R -import com.forcetower.uefs.core.storage.resource.Status -import com.forcetower.uefs.core.util.isStudentFromUEFS -import com.forcetower.uefs.databinding.DialogInvalidAccessBinding -import com.forcetower.uefs.feature.captcha.CaptchaResolverFragment -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import com.google.firebase.remoteconfig.FirebaseRemoteConfig -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import timber.log.Timber -import javax.inject.Inject - -@AndroidEntryPoint -class InvalidAccessDialog : BottomSheetDialogFragment() { - @Inject lateinit var preferences: SharedPreferences - @Inject lateinit var remoteConfig: FirebaseRemoteConfig - private lateinit var binding: DialogInvalidAccessBinding - private val viewModel: HomeViewModel by activityViewModels() - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog - - try { - dialog.setOnShowListener { - val bottomSheet = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet)!! - val behavior = BottomSheetBehavior.from(bottomSheet) - behavior.skipCollapsed = true - behavior.state = BottomSheetBehavior.STATE_EXPANDED - } - } catch (t: Throwable) { - Timber.d(t, "Hum...") - } - return dialog - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return DialogInvalidAccessBinding.inflate(inflater, container, false).also { - binding = it - it.btnCancel.setOnClickListener { - viewModel.changeAccessValidation(true) - dismiss() - } - it.btnChange.setOnClickListener { - internalChecks() - } - }.root - } - - private fun internalChecks() { - val password = binding.etPassword.text.toString() - if (password.length < 3) { - binding.etPassword.run { - error = getString(R.string.error_too_small) - requestFocus() - } - return - } - val studentFromUEFS = preferences.isStudentFromUEFS() - val snowpiercer = remoteConfig.getBoolean("feature_flag_use_snowpiercer") && studentFromUEFS - if (Constants.getParameter("REQUIRES_CAPTCHA") != "true" || snowpiercer) { - preparePassword(password) - } else { - requestCaptchaToken(password) - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - viewModel.passwordChangeProcess.observe( - viewLifecycleOwner, - EventObserver { - if (it.status == Status.SUCCESS) { - binding.run { - btnChange.isEnabled = true - btnCancel.isEnabled = true - pbOperation.visibility = INVISIBLE - } - - if (it.data == true) { - viewModel.showSnack(getString(R.string.invalid_access_password_change)) - dismiss() - } else if (it.data == false) { - binding.etPassword.run { - error = getString(R.string.invalid_access_new_password_is_incorrect) - requestFocus() - } - } - } else if (it.status == Status.LOADING) { - binding.run { - btnChange.isEnabled = false - btnCancel.isEnabled = false - pbOperation.visibility = VISIBLE - } - } else { - binding.run { - btnChange.isEnabled = true - btnCancel.isEnabled = true - pbOperation.visibility = INVISIBLE - - etPassword.run { - val message = it.message ?: getString(R.string.login_access_reconnect_not_specified) - error = message - requestFocus() - } - } - } - } - ) - } - - private fun requestCaptchaToken(password: String) { - val fragment = CaptchaResolverFragment() - fragment.setCallback( - object : CaptchaResolverFragment.CaptchaResolvedCallback { - override fun onCaptchaResolved(token: String) { - // This here is not called on the actual "main" thread - Timber.d("Token received $token") - lifecycleScope.launchWhenCreated { - withContext(Dispatchers.Main) { - viewModel.attemptNewPasswordLogin(password, token) - } - } - } - } - ) - - fragment.show(childFragmentManager, "captcha_resolver") - } - - private fun preparePassword(password: String) { - viewModel.attemptNewPasswordLogin(password) - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.home + +import android.app.Dialog +import android.content.SharedPreferences +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.INVISIBLE +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import com.forcetower.core.lifecycle.EventObserver +import com.forcetower.sagres.Constants +import com.forcetower.uefs.R +import com.forcetower.uefs.core.storage.resource.Status +import com.forcetower.uefs.core.util.isStudentFromUEFS +import com.forcetower.uefs.databinding.DialogInvalidAccessBinding +import com.forcetower.uefs.feature.captcha.CaptchaResolverFragment +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import timber.log.Timber + +@AndroidEntryPoint +class InvalidAccessDialog : BottomSheetDialogFragment() { + @Inject lateinit var preferences: SharedPreferences + + @Inject lateinit var remoteConfig: FirebaseRemoteConfig + private lateinit var binding: DialogInvalidAccessBinding + private val viewModel: HomeViewModel by activityViewModels() + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog + + try { + dialog.setOnShowListener { + val bottomSheet = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet)!! + val behavior = BottomSheetBehavior.from(bottomSheet) + behavior.skipCollapsed = true + behavior.state = BottomSheetBehavior.STATE_EXPANDED + } + } catch (t: Throwable) { + Timber.d(t, "Hum...") + } + return dialog + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return DialogInvalidAccessBinding.inflate(inflater, container, false).also { + binding = it + it.btnCancel.setOnClickListener { + viewModel.changeAccessValidation(true) + dismiss() + } + it.btnChange.setOnClickListener { + internalChecks() + } + }.root + } + + private fun internalChecks() { + val password = binding.etPassword.text.toString() + if (password.length < 3) { + binding.etPassword.run { + error = getString(R.string.error_too_small) + requestFocus() + } + return + } + val studentFromUEFS = preferences.isStudentFromUEFS() + val snowpiercer = remoteConfig.getBoolean("feature_flag_use_snowpiercer") && studentFromUEFS + if (Constants.getParameter("REQUIRES_CAPTCHA") != "true" || snowpiercer) { + preparePassword(password) + } else { + requestCaptchaToken(password) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.passwordChangeProcess.observe( + viewLifecycleOwner, + EventObserver { + if (it.status == Status.SUCCESS) { + binding.run { + btnChange.isEnabled = true + btnCancel.isEnabled = true + pbOperation.visibility = INVISIBLE + } + + if (it.data == true) { + viewModel.showSnack(getString(R.string.invalid_access_password_change)) + dismiss() + } else if (it.data == false) { + binding.etPassword.run { + error = getString(R.string.invalid_access_new_password_is_incorrect) + requestFocus() + } + } + } else if (it.status == Status.LOADING) { + binding.run { + btnChange.isEnabled = false + btnCancel.isEnabled = false + pbOperation.visibility = VISIBLE + } + } else { + binding.run { + btnChange.isEnabled = true + btnCancel.isEnabled = true + pbOperation.visibility = INVISIBLE + + etPassword.run { + val message = it.message ?: getString(R.string.login_access_reconnect_not_specified) + error = message + requestFocus() + } + } + } + } + ) + } + + private fun requestCaptchaToken(password: String) { + val fragment = CaptchaResolverFragment() + fragment.setCallback( + object : CaptchaResolverFragment.CaptchaResolvedCallback { + override fun onCaptchaResolved(token: String) { + // This here is not called on the actual "main" thread + Timber.d("Token received $token") + lifecycleScope.launchWhenCreated { + withContext(Dispatchers.Main) { + viewModel.attemptNewPasswordLogin(password, token) + } + } + } + } + ) + + fragment.show(childFragmentManager, "captcha_resolver") + } + + private fun preparePassword(password: String) { + viewModel.attemptNewPasswordLogin(password) + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/home/LogoutConfirmationFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/home/LogoutConfirmationFragment.kt index 913cbc4ac..a75d6ee2d 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/home/LogoutConfirmationFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/home/LogoutConfirmationFragment.kt @@ -45,7 +45,10 @@ class LogoutConfirmationFragment : BottomSheetDialogFragment() { binding = it }.apply { btnCancel.setOnClickListener { dismiss() } - btnConfirm.setOnClickListener { viewModel.logout(); dismiss() } + btnConfirm.setOnClickListener { + viewModel.logout() + dismiss() + } }.root } diff --git a/app/src/main/java/com/forcetower/uefs/feature/information/InformationDialog.kt b/app/src/main/java/com/forcetower/uefs/feature/information/InformationDialog.kt index 78d4d092f..a8e5e97f2 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/information/InformationDialog.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/information/InformationDialog.kt @@ -32,6 +32,7 @@ class InformationDialog : RoundedDialog() { private lateinit var binding: DialogInformationBaseBinding lateinit var title: String lateinit var description: String + @Suppress("MemberVisibilityCanBePrivate") var buttonText: String? = null diff --git a/app/src/main/java/com/forcetower/uefs/feature/login/LoadingFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/login/LoadingFragment.kt index 41a789fde..5fab0b334 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/login/LoadingFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/login/LoadingFragment.kt @@ -1,150 +1,150 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.login - -import `in`.uncod.android.bypass.Bypass -import android.app.ActivityManager -import android.content.Intent -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.text.Layout -import android.text.SpannableString -import android.text.Spanned -import android.text.TextUtils -import android.text.style.AlignmentSpan -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.app.AlertDialog -import androidx.core.content.ContextCompat -import androidx.core.os.bundleOf -import androidx.core.os.postDelayed -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.navigation.fragment.findNavController -import com.forcetower.uefs.R -import com.forcetower.uefs.core.model.unes.Access -import com.forcetower.uefs.core.util.HtmlUtils -import com.forcetower.uefs.databinding.FragmentLoadingBinding -import com.forcetower.uefs.feature.home.HomeActivity -import com.forcetower.uefs.feature.shared.UFragment -import com.forcetower.uefs.feature.shared.fadeIn -import com.forcetower.uefs.feature.shared.fadeOut -import com.forcetower.uefs.service.NotificationCreator -import com.google.firebase.analytics.FirebaseAnalytics -import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber -import javax.inject.Inject - -@AndroidEntryPoint -class LoadingFragment : UFragment() { - @Inject - lateinit var analytics: FirebaseAnalytics - private val viewModel: LoginViewModel by viewModels() - private lateinit var binding: FragmentLoadingBinding - private lateinit var markdown: Bypass - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return try { - FragmentLoadingBinding.inflate(inflater, container, false).also { - binding = it - binding.btnFirstSteps.setOnClickListener { - onMoveToNextScreen() - } - markdown = Bypass(requireContext(), Bypass.Options()) - setupTermsText() - }.root - } catch (error: Exception) { - Timber.e(error, "Failed inflating initial layout") - showInitializationError() - null - } - } - - private fun onMoveToNextScreen() { - try { - findNavController().navigate(R.id.action_login_loading_to_login_form) - } catch (error: IllegalArgumentException) { - showSnack(getString(R.string.why_are_you_so_clicky)) - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - viewModel.getAccess().observe(viewLifecycleOwner, { onReceiveToken(it) }) - } - - private fun showInitializationError() { - try { - AlertDialog.Builder(requireContext()) - .setTitle(R.string.start_up_failed) - .setMessage(R.string.start_up_failed_description) - .setPositiveButton(R.string.start_up_failed_positive_btn) { dialog, _ -> - ContextCompat.getSystemService(requireContext(), ActivityManager::class.java)?.clearApplicationUserData() - dialog.dismiss() - } - .setCancelable(false) - .create() - .show() - } catch (error: Throwable) { - try { - NotificationCreator.showSimpleNotification(requireContext(), getString(R.string.start_up_failed_resumed), getString(R.string.start_up_failed_resumed_desc)) - analytics.logEvent("start_up_failed", bundleOf("error_type" to "text_init_error")) - } catch (_: Throwable) { - analytics.logEvent("start_up_ntf_failed", bundleOf("error_type" to "ntf_show_error")) - } - - Handler(Looper.getMainLooper()).postDelayed(3000) { - if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { - ContextCompat.getSystemService(requireContext(), ActivityManager::class.java)?.clearApplicationUserData() - // Actually, you may crash :D - throw error - } - } - } - } - - private fun onReceiveToken(access: Access?) { - if (access == null) { - binding.btnFirstSteps.fadeIn() - binding.contentLoading.fadeOut() - } else { - Timber.d("Connected already") - if (viewModel.isConnected()) return - - viewModel.setConnected() - val intent = Intent(context, HomeActivity::class.java) - startActivity(intent) - activity?.finish() - } - } - - private fun setupTermsText() { - val sequence1 = SpannableString(resources.getString(R.string.label_terms_and_conditions_p1)) - sequence1.setSpan(AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), 0, sequence1.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - val sequence2 = SpannableString(markdown.markdownToSpannable(resources.getString(R.string.label_terms_and_conditions_p2), binding.textTermsAndPrivacy, null)) - sequence2.setSpan(AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), 0, sequence2.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - - val sequence = TextUtils.concat(sequence1, "\n", sequence2) - HtmlUtils.setTextWithNiceLinks(binding.textTermsAndPrivacy, sequence) - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.login + +import android.app.ActivityManager +import android.content.Intent +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.text.Layout +import android.text.SpannableString +import android.text.Spanned +import android.text.TextUtils +import android.text.style.AlignmentSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.core.os.bundleOf +import androidx.core.os.postDelayed +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.navigation.fragment.findNavController +import com.forcetower.uefs.R +import com.forcetower.uefs.core.model.unes.Access +import com.forcetower.uefs.core.util.HtmlUtils +import com.forcetower.uefs.databinding.FragmentLoadingBinding +import com.forcetower.uefs.feature.home.HomeActivity +import com.forcetower.uefs.feature.shared.UFragment +import com.forcetower.uefs.feature.shared.fadeIn +import com.forcetower.uefs.feature.shared.fadeOut +import com.forcetower.uefs.service.NotificationCreator +import com.google.firebase.analytics.FirebaseAnalytics +import dagger.hilt.android.AndroidEntryPoint +import `in`.uncod.android.bypass.Bypass +import javax.inject.Inject +import timber.log.Timber + +@AndroidEntryPoint +class LoadingFragment : UFragment() { + @Inject + lateinit var analytics: FirebaseAnalytics + private val viewModel: LoginViewModel by viewModels() + private lateinit var binding: FragmentLoadingBinding + private lateinit var markdown: Bypass + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return try { + FragmentLoadingBinding.inflate(inflater, container, false).also { + binding = it + binding.btnFirstSteps.setOnClickListener { + onMoveToNextScreen() + } + markdown = Bypass(requireContext(), Bypass.Options()) + setupTermsText() + }.root + } catch (error: Exception) { + Timber.e(error, "Failed inflating initial layout") + showInitializationError() + null + } + } + + private fun onMoveToNextScreen() { + try { + findNavController().navigate(R.id.action_login_loading_to_login_form) + } catch (error: IllegalArgumentException) { + showSnack(getString(R.string.why_are_you_so_clicky)) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.getAccess().observe(viewLifecycleOwner, { onReceiveToken(it) }) + } + + private fun showInitializationError() { + try { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.start_up_failed) + .setMessage(R.string.start_up_failed_description) + .setPositiveButton(R.string.start_up_failed_positive_btn) { dialog, _ -> + ContextCompat.getSystemService(requireContext(), ActivityManager::class.java)?.clearApplicationUserData() + dialog.dismiss() + } + .setCancelable(false) + .create() + .show() + } catch (error: Throwable) { + try { + NotificationCreator.showSimpleNotification(requireContext(), getString(R.string.start_up_failed_resumed), getString(R.string.start_up_failed_resumed_desc)) + analytics.logEvent("start_up_failed", bundleOf("error_type" to "text_init_error")) + } catch (_: Throwable) { + analytics.logEvent("start_up_ntf_failed", bundleOf("error_type" to "ntf_show_error")) + } + + Handler(Looper.getMainLooper()).postDelayed(3000) { + if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { + ContextCompat.getSystemService(requireContext(), ActivityManager::class.java)?.clearApplicationUserData() + // Actually, you may crash :D + throw error + } + } + } + } + + private fun onReceiveToken(access: Access?) { + if (access == null) { + binding.btnFirstSteps.fadeIn() + binding.contentLoading.fadeOut() + } else { + Timber.d("Connected already") + if (viewModel.isConnected()) return + + viewModel.setConnected() + val intent = Intent(context, HomeActivity::class.java) + startActivity(intent) + activity?.finish() + } + } + + private fun setupTermsText() { + val sequence1 = SpannableString(resources.getString(R.string.label_terms_and_conditions_p1)) + sequence1.setSpan(AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), 0, sequence1.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + val sequence2 = SpannableString(markdown.markdownToSpannable(resources.getString(R.string.label_terms_and_conditions_p2), binding.textTermsAndPrivacy, null)) + sequence2.setSpan(AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), 0, sequence2.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + + val sequence = TextUtils.concat(sequence1, "\n", sequence2) + HtmlUtils.setTextWithNiceLinks(binding.textTermsAndPrivacy, sequence) + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/login/LoginFormViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/login/LoginFormViewModel.kt index 9e9903dee..a73b8ba54 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/login/LoginFormViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/login/LoginFormViewModel.kt @@ -3,15 +3,15 @@ package com.forcetower.uefs.feature.login import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.forcetower.core.lifecycle.SingleLiveEvent import com.forcetower.uefs.core.model.edge.auth.RegisterPasskeyStart import com.forcetower.uefs.domain.usecase.auth.CompleteAssertionUseCase import com.forcetower.uefs.domain.usecase.auth.RegisterPasskeyUseCase import com.forcetower.uefs.domain.usecase.auth.StartAssertionUseCase -import com.forcetower.core.lifecycle.SingleLiveEvent import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject @HiltViewModel class LoginFormViewModel @Inject constructor( diff --git a/app/src/main/java/com/forcetower/uefs/feature/login/LoginFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/login/LoginFragment.kt index 318c43b26..040d3826c 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/login/LoginFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/login/LoginFragment.kt @@ -1,238 +1,239 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.login - -import android.content.SharedPreferences -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.app.ActivityOptionsCompat -import androidx.core.os.bundleOf -import androidx.credentials.CreateCredentialResponse -import androidx.credentials.CreatePublicKeyCredentialRequest -import androidx.credentials.CreatePublicKeyCredentialResponse -import androidx.credentials.CredentialManager -import androidx.credentials.GetCredentialRequest -import androidx.credentials.GetCredentialResponse -import androidx.credentials.GetPasswordOption -import androidx.credentials.GetPublicKeyCredentialOption -import androidx.credentials.PasswordCredential -import androidx.credentials.PublicKeyCredential -import androidx.credentials.exceptions.CreateCredentialException -import androidx.credentials.exceptions.GetCredentialException -import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope -import androidx.navigation.ActivityNavigator -import androidx.navigation.fragment.findNavController -import com.forcetower.sagres.Constants -import com.forcetower.sagres.SagresNavigator -import com.forcetower.uefs.R -import com.forcetower.uefs.core.model.edge.auth.RegisterPasskeyStart -import com.forcetower.uefs.core.util.isStudentFromUEFS -import com.forcetower.uefs.databinding.FragmentLoginFormBinding -import com.forcetower.uefs.feature.shared.UFragment -import com.google.firebase.crashlytics.FirebaseCrashlytics -import com.google.firebase.remoteconfig.FirebaseRemoteConfig -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -@AndroidEntryPoint -class LoginFragment : UFragment() { - @Inject lateinit var remoteConfig: FirebaseRemoteConfig - @Inject lateinit var preferences: SharedPreferences - - private val viewModel by viewModels() - private lateinit var binding: FragmentLoginFormBinding - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - allowOnlyUEFS() - return FragmentLoginFormBinding.inflate(inflater, container, false).also { - binding = it - binding.btnInstitution.setOnClickListener { - showInstitutionSelector() - } - binding.btnConnect.setOnClickListener { - prepareLogin() - } - binding.btnAboutUnes.setOnClickListener { - toAbout() - } - }.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewModel.challenge.observe(viewLifecycleOwner) { - startCredentialsManager(it) - } - - viewModel.register.observe(viewLifecycleOwner) { - runCatching { - startPasskeyRegister(it) - } - } - } - - private fun startPasskeyRegister(start: RegisterPasskeyStart) { - val manager = CredentialManager.create(requireContext()) - - Timber.d("Register challenge: ${start.create}") - - val request = CreatePublicKeyCredentialRequest( - requestJson = start.create, - preferImmediatelyAvailableCredentials = false, - ) - - lifecycleScope.launch { - try { - val result = manager.createCredential( - context = requireActivity(), - request = request, - ) - handlePasskeyRegistrationResult(result, start) - } catch (e: CreateCredentialException) { - Timber.e(e, "Failed to create passkey") - showSnack("Não foi possível recuperar credenciais.") - } - } - } - - private fun handlePasskeyRegistrationResult( - result: CreateCredentialResponse, - start: RegisterPasskeyStart - ) { - if (result is CreatePublicKeyCredentialResponse) { - Timber.d("Register Response: ${result.registrationResponseJson}") - viewModel.finishRegister(start.flowId, result.registrationResponseJson) - } else { - Timber.e("This is not a passkey. lol") - } - } - - private fun startCredentialsManager(challenge: String) { - val manager = CredentialManager.create(requireContext()) - val passwordOption = GetPasswordOption() - - val publicKeyCredential = GetPublicKeyCredentialOption( - requestJson = challenge - ) - - val request = GetCredentialRequest(listOf(passwordOption, publicKeyCredential)) - - lifecycleScope.launch { - try { - val result = manager.getCredential( - context = requireActivity(), - request = request - ) - onCredentialSignInCompleted(result) - } catch (e: GetCredentialException) { - Timber.e(e, "Failed to get credentials") - showSnack("Não foi possível recuperar credenciais.") - } - } - } - - private fun onCredentialSignInCompleted(result: GetCredentialResponse) { - when (val credential = result.credential) { - is PublicKeyCredential -> { - val responseJson = credential.authenticationResponseJson - viewModel.completeAssertion(responseJson) - } - - is PasswordCredential -> { - val username = credential.id - val password = credential.password - doLogin(username, password) - } - } - } - - private fun showInstitutionSelector() { - val dialog = InstitutionSelectDialog() - dialog.show(childFragmentManager, "dialog_institution_selector") - } - - private fun prepareLogin() { - val username = binding.editUser.text.toString() - val password = binding.editPass.text.toString() - var error = false - - if (username.isBlank() || username.length < 3) { - binding.editUser.error = getString(R.string.error_too_small) - binding.editUser.requestFocus() - error = true - } - - if (password.isBlank() || password.length < 2) { - binding.editPass.error = getString(R.string.error_too_small) - binding.editPass.requestFocus() - error = true - } - - if (error) return - - doLogin(username, password) - } - - private fun doLogin(username: String, password: String) { - val snowpiercer = - preferences.isStudentFromUEFS() && remoteConfig.getBoolean("feature_flag_use_snowpiercer") - FirebaseCrashlytics.getInstance().setCustomKey("snowpiercer_user", snowpiercer) - val info = bundleOf( - "username" to username, - "password" to password, - "snowpiercer" to snowpiercer - ) - - if (Constants.getParameter("REQUIRES_CAPTCHA") != "true" || snowpiercer) { - findNavController().navigate(R.id.action_login_form_to_signing_in, info) - } else { - val directions = - LoginFragmentDirections.actionLoginFormToTechNopeCaptchaStuff(username, password) - findNavController().navigate(directions) - } - } - - private fun loginWithPasskey() { - viewModel.startAssertion() - } - - private fun toAbout() { - val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity()) - val extras = ActivityNavigator.Extras.Builder() - .setActivityOptions(bundle) - .build() - findNavController().navigate(R.id.action_login_open_about, null, null, extras) - } - - private fun allowOnlyUEFS() { - preferences.edit() - .putString(com.forcetower.uefs.core.constants.Constants.SELECTED_INSTITUTION_KEY, "UEFS") - .apply() - - SagresNavigator.instance.setSelectedInstitution("UEFS") - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.login + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.app.ActivityOptionsCompat +import androidx.core.os.bundleOf +import androidx.credentials.CreateCredentialResponse +import androidx.credentials.CreatePublicKeyCredentialRequest +import androidx.credentials.CreatePublicKeyCredentialResponse +import androidx.credentials.CredentialManager +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetCredentialResponse +import androidx.credentials.GetPasswordOption +import androidx.credentials.GetPublicKeyCredentialOption +import androidx.credentials.PasswordCredential +import androidx.credentials.PublicKeyCredential +import androidx.credentials.exceptions.CreateCredentialException +import androidx.credentials.exceptions.GetCredentialException +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.ActivityNavigator +import androidx.navigation.fragment.findNavController +import com.forcetower.sagres.Constants +import com.forcetower.sagres.SagresNavigator +import com.forcetower.uefs.R +import com.forcetower.uefs.core.model.edge.auth.RegisterPasskeyStart +import com.forcetower.uefs.core.util.isStudentFromUEFS +import com.forcetower.uefs.databinding.FragmentLoginFormBinding +import com.forcetower.uefs.feature.shared.UFragment +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.launch +import timber.log.Timber + +@AndroidEntryPoint +class LoginFragment : UFragment() { + @Inject lateinit var remoteConfig: FirebaseRemoteConfig + + @Inject lateinit var preferences: SharedPreferences + + private val viewModel by viewModels() + private lateinit var binding: FragmentLoginFormBinding + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + allowOnlyUEFS() + return FragmentLoginFormBinding.inflate(inflater, container, false).also { + binding = it + binding.btnInstitution.setOnClickListener { + showInstitutionSelector() + } + binding.btnConnect.setOnClickListener { + prepareLogin() + } + binding.btnAboutUnes.setOnClickListener { + toAbout() + } + }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.challenge.observe(viewLifecycleOwner) { + startCredentialsManager(it) + } + + viewModel.register.observe(viewLifecycleOwner) { + runCatching { + startPasskeyRegister(it) + } + } + } + + private fun startPasskeyRegister(start: RegisterPasskeyStart) { + val manager = CredentialManager.create(requireContext()) + + Timber.d("Register challenge: ${start.create}") + + val request = CreatePublicKeyCredentialRequest( + requestJson = start.create, + preferImmediatelyAvailableCredentials = false + ) + + lifecycleScope.launch { + try { + val result = manager.createCredential( + context = requireActivity(), + request = request + ) + handlePasskeyRegistrationResult(result, start) + } catch (e: CreateCredentialException) { + Timber.e(e, "Failed to create passkey") + showSnack("Não foi possível recuperar credenciais.") + } + } + } + + private fun handlePasskeyRegistrationResult( + result: CreateCredentialResponse, + start: RegisterPasskeyStart + ) { + if (result is CreatePublicKeyCredentialResponse) { + Timber.d("Register Response: ${result.registrationResponseJson}") + viewModel.finishRegister(start.flowId, result.registrationResponseJson) + } else { + Timber.e("This is not a passkey. lol") + } + } + + private fun startCredentialsManager(challenge: String) { + val manager = CredentialManager.create(requireContext()) + val passwordOption = GetPasswordOption() + + val publicKeyCredential = GetPublicKeyCredentialOption( + requestJson = challenge + ) + + val request = GetCredentialRequest(listOf(passwordOption, publicKeyCredential)) + + lifecycleScope.launch { + try { + val result = manager.getCredential( + context = requireActivity(), + request = request + ) + onCredentialSignInCompleted(result) + } catch (e: GetCredentialException) { + Timber.e(e, "Failed to get credentials") + showSnack("Não foi possível recuperar credenciais.") + } + } + } + + private fun onCredentialSignInCompleted(result: GetCredentialResponse) { + when (val credential = result.credential) { + is PublicKeyCredential -> { + val responseJson = credential.authenticationResponseJson + viewModel.completeAssertion(responseJson) + } + + is PasswordCredential -> { + val username = credential.id + val password = credential.password + doLogin(username, password) + } + } + } + + private fun showInstitutionSelector() { + val dialog = InstitutionSelectDialog() + dialog.show(childFragmentManager, "dialog_institution_selector") + } + + private fun prepareLogin() { + val username = binding.editUser.text.toString() + val password = binding.editPass.text.toString() + var error = false + + if (username.isBlank() || username.length < 3) { + binding.editUser.error = getString(R.string.error_too_small) + binding.editUser.requestFocus() + error = true + } + + if (password.isBlank() || password.length < 2) { + binding.editPass.error = getString(R.string.error_too_small) + binding.editPass.requestFocus() + error = true + } + + if (error) return + + doLogin(username, password) + } + + private fun doLogin(username: String, password: String) { + val snowpiercer = + preferences.isStudentFromUEFS() && remoteConfig.getBoolean("feature_flag_use_snowpiercer") + FirebaseCrashlytics.getInstance().setCustomKey("snowpiercer_user", snowpiercer) + val info = bundleOf( + "username" to username, + "password" to password, + "snowpiercer" to snowpiercer + ) + + if (Constants.getParameter("REQUIRES_CAPTCHA") != "true" || snowpiercer) { + findNavController().navigate(R.id.action_login_form_to_signing_in, info) + } else { + val directions = + LoginFragmentDirections.actionLoginFormToTechNopeCaptchaStuff(username, password) + findNavController().navigate(directions) + } + } + + private fun loginWithPasskey() { + viewModel.startAssertion() + } + + private fun toAbout() { + val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity()) + val extras = ActivityNavigator.Extras.Builder() + .setActivityOptions(bundle) + .build() + findNavController().navigate(R.id.action_login_open_about, null, null, extras) + } + + private fun allowOnlyUEFS() { + preferences.edit() + .putString(com.forcetower.uefs.core.constants.Constants.SELECTED_INSTITUTION_KEY, "UEFS") + .apply() + + SagresNavigator.instance.setSelectedInstitution("UEFS") + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/login/LoginViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/login/LoginViewModel.kt index fbd2baf94..8c9276356 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/login/LoginViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/login/LoginViewModel.kt @@ -31,9 +31,9 @@ import com.forcetower.uefs.core.storage.repository.LoginSagresRepository import com.forcetower.uefs.core.storage.repository.SnowpiercerLoginRepository import dagger.hilt.android.lifecycle.HiltViewModel import dev.forcetower.unes.usecases.courses.InitialCourseLoadUseCase +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import javax.inject.Inject @HiltViewModel class LoginViewModel @Inject constructor( diff --git a/app/src/main/java/com/forcetower/uefs/feature/login/SigningInFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/login/SigningInFragment.kt index bb2b820ac..609e63ed5 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/login/SigningInFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/login/SigningInFragment.kt @@ -1,244 +1,245 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.login - -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.util.TypedValue -import android.view.Gravity.CENTER -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.core.app.ActivityOptionsCompat -import androidx.core.content.ContextCompat -import androidx.core.content.res.ResourcesCompat -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.Observer -import androidx.navigation.ActivityNavigator -import androidx.navigation.findNavController -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import com.forcetower.sagres.operation.Callback -import com.forcetower.sagres.operation.Status -import com.forcetower.uefs.R -import com.forcetower.uefs.core.model.unes.Profile -import com.forcetower.uefs.core.storage.repository.LoginSagresRepository -import com.forcetower.uefs.core.util.fromJson -import com.forcetower.uefs.databinding.FragmentSigningInBinding -import com.forcetower.uefs.feature.shared.UFragment -import com.forcetower.uefs.feature.shared.fadeIn -import com.forcetower.uefs.feature.shared.fadeOut -import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber -import java.nio.charset.Charset - -@AndroidEntryPoint -class SigningInFragment : UFragment() { - private lateinit var binding: FragmentSigningInBinding - private val viewModel: LoginViewModel by viewModels() - private lateinit var messages: Array - private val possibleMessages: ArrayList = arrayListOf() - - private val args by navArgs() - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return FragmentSigningInBinding.inflate(inflater, container, false).also { - binding = it - prepareSwitcher() - prepareMessages() - }.root - } - - private fun prepareMessages() { - try { - val stream = context?.assets?.open("login_messages.json") - if (stream != null) { - val size = stream.available() - val buffer = ByteArray(size) - stream.read(buffer) - stream.close() - val json = String(buffer, Charset.forName("UTF-8")) - messages = json.fromJson() - } - } catch (e: Exception) { - messages = arrayOf("Hum... Algo de estranho aconteceu", "O aplicativo não tem mensagens...", "Corre!!!", "Me avisa que isso aconteceu!") - } - } - - private fun prepareSwitcher() { - val font = ResourcesCompat.getFont(requireContext(), R.font.product_sans_regular) - - binding.textStatus.setFactory { - val textView = TextView(requireContext()).apply { - textSize = 16f - gravity = CENTER - typeface = font - } - - val typedValue = TypedValue() - val theme = requireContext().theme - theme.resolveAttribute(com.google.android.material.R.attr.colorOnSurface, typedValue, true) - val colorOnSurface = typedValue.data - textView.setTextColor(colorOnSurface) - textView - } - - binding.textTips.setFactory { - val textView = TextView(requireContext()).apply { - textSize = 13f - gravity = CENTER - typeface = font - setTextColor(ContextCompat.getColor(requireContext(), R.color.red)) - } - textView - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - viewModel.getLogin().observe(viewLifecycleOwner) { onLoginProgress(it) } - viewModel.getProfile().observe(viewLifecycleOwner, Observer(this::onProfileUpdate)) - viewModel.getStep(args.snowpiercer).observe(viewLifecycleOwner, Observer(this::onStep)) - doLogin() - } - - override fun onStart() { - super.onStart() - displayRandomText() - } - - private fun displayRandomText() { - if (possibleMessages.isEmpty()) { - possibleMessages.addAll(messages) - possibleMessages.shuffle() - } - binding.textStatus.setText(possibleMessages.removeFirst()) - Handler(Looper.getMainLooper()).postDelayed( - { - if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) - displayRandomText() - }, - 3000 - ) - } - - private fun doLogin() { - val username = args.username - val password = args.password - val captcha = args.captchaToken - val snowpiercer = args.snowpiercer - - if (username.isBlank() || password.isBlank()) { - showSnack(getString(R.string.error_invalid_credentials)) - view?.findNavController()?.popBackStack() - } else { - viewModel.login(username, password, captcha, snowpiercer, true) - if (username.contains("@")) { - binding.textTips.setText(getString(R.string.enter_using_username_instead)) - binding.textTips.fadeIn() - } else { - binding.textTips.fadeOut() - } - } - } - - private fun onStep(step: LoginSagresRepository.Step) { - binding.contentLoading.setProgressWithAnimation(step.step.toFloat() * 100 / step.count) - } - - private fun onLoginProgress(callback: Callback) { - Timber.d("${callback.status}, ${callback.message}, ${callback.code}") - when (callback.status) { - Status.STARTED -> Timber.d("Status: Started") - Status.LOADING -> Timber.d("Status: Loading") - Status.INVALID_LOGIN -> { - val code = callback.code - if (code != 401) { - Timber.tag("connect_fail").e("User did not connect due to code $code") - snackAndBack(getString(R.string.error_invalid_connect_error_with_code, code)) - } else { - snackAndBack(getString(R.string.error_invalid_credentials)) - } - } - Status.APPROVING -> Timber.d("Status: Approving") - Status.NETWORK_ERROR -> snackAndBack(getString(R.string.error_network_error)) - Status.RESPONSE_FAILED -> snackAndBack(getString(R.string.error_unexpected_response_joke)) - Status.SUCCESS -> completeLogin() - Status.APPROVAL_ERROR -> snackAndBack(getString(R.string.error_network_error)) - Status.GRADES_FAILED -> completeLogin() - Status.UNKNOWN_FAILURE -> snackAndBack(getString(R.string.unknown_error)) - Status.COMPLETED -> completeLogin() - } - -// val document = callback.document -// if (document != null) { -// val html = document.html() -// binding.testingWebview.run { -// loadDataWithBaseURL("", html, "text/html", "ISO8859-1", "") -// } -// } - } - - private fun onProfileUpdate(profile: Profile?) { - if (profile != null) { - val username = arguments?.getString("username") - if (username != null && username.contains("@")) { - binding.textHelloUser.text = getString(R.string.attention_user_login_with_email, profile.name) - } else { - binding.textHelloUser.text = getString(R.string.login_hello_user, profile.name) - } - binding.textHelloUser.fadeIn() - } - } - - private fun completeLogin() { - if (viewModel.isConnected()) return - viewModel.setConnected() - val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), binding.imageCenter, getString(R.string.user_image_transition)) - val extras = ActivityNavigator.Extras.Builder() - .setActivityOptions(options) - .build() - - binding.textHelloUser.fadeOut() - - Handler(Looper.getMainLooper()).postDelayed( - { - if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { - binding.textHelloUser.text = "" - findNavController().navigate(R.id.action_login_to_setup, null, null, extras) - activity?.finishAfterTransition() - } - }, - 1000 - ) - } - - private fun snackAndBack(string: String) { - showSnack(string) - binding.textHelloUser.text = "" - binding.textHelloUser.fadeOut() - binding.textTips.fadeOut() - findNavController().popBackStack(R.id.login_form, false) - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.login + +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.TypedValue +import android.view.Gravity.CENTER +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.app.ActivityOptionsCompat +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.Observer +import androidx.navigation.ActivityNavigator +import androidx.navigation.findNavController +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import com.forcetower.sagres.operation.Callback +import com.forcetower.sagres.operation.Status +import com.forcetower.uefs.R +import com.forcetower.uefs.core.model.unes.Profile +import com.forcetower.uefs.core.storage.repository.LoginSagresRepository +import com.forcetower.uefs.core.util.fromJson +import com.forcetower.uefs.databinding.FragmentSigningInBinding +import com.forcetower.uefs.feature.shared.UFragment +import com.forcetower.uefs.feature.shared.fadeIn +import com.forcetower.uefs.feature.shared.fadeOut +import dagger.hilt.android.AndroidEntryPoint +import java.nio.charset.Charset +import timber.log.Timber + +@AndroidEntryPoint +class SigningInFragment : UFragment() { + private lateinit var binding: FragmentSigningInBinding + private val viewModel: LoginViewModel by viewModels() + private lateinit var messages: Array + private val possibleMessages: ArrayList = arrayListOf() + + private val args by navArgs() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return FragmentSigningInBinding.inflate(inflater, container, false).also { + binding = it + prepareSwitcher() + prepareMessages() + }.root + } + + private fun prepareMessages() { + try { + val stream = context?.assets?.open("login_messages.json") + if (stream != null) { + val size = stream.available() + val buffer = ByteArray(size) + stream.read(buffer) + stream.close() + val json = String(buffer, Charset.forName("UTF-8")) + messages = json.fromJson() + } + } catch (e: Exception) { + messages = arrayOf("Hum... Algo de estranho aconteceu", "O aplicativo não tem mensagens...", "Corre!!!", "Me avisa que isso aconteceu!") + } + } + + private fun prepareSwitcher() { + val font = ResourcesCompat.getFont(requireContext(), R.font.product_sans_regular) + + binding.textStatus.setFactory { + val textView = TextView(requireContext()).apply { + textSize = 16f + gravity = CENTER + typeface = font + } + + val typedValue = TypedValue() + val theme = requireContext().theme + theme.resolveAttribute(com.google.android.material.R.attr.colorOnSurface, typedValue, true) + val colorOnSurface = typedValue.data + textView.setTextColor(colorOnSurface) + textView + } + + binding.textTips.setFactory { + val textView = TextView(requireContext()).apply { + textSize = 13f + gravity = CENTER + typeface = font + setTextColor(ContextCompat.getColor(requireContext(), R.color.red)) + } + textView + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.getLogin().observe(viewLifecycleOwner) { onLoginProgress(it) } + viewModel.getProfile().observe(viewLifecycleOwner, Observer(this::onProfileUpdate)) + viewModel.getStep(args.snowpiercer).observe(viewLifecycleOwner, Observer(this::onStep)) + doLogin() + } + + override fun onStart() { + super.onStart() + displayRandomText() + } + + private fun displayRandomText() { + if (possibleMessages.isEmpty()) { + possibleMessages.addAll(messages) + possibleMessages.shuffle() + } + binding.textStatus.setText(possibleMessages.removeFirst()) + Handler(Looper.getMainLooper()).postDelayed( + { + if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { + displayRandomText() + } + }, + 3000 + ) + } + + private fun doLogin() { + val username = args.username + val password = args.password + val captcha = args.captchaToken + val snowpiercer = args.snowpiercer + + if (username.isBlank() || password.isBlank()) { + showSnack(getString(R.string.error_invalid_credentials)) + view?.findNavController()?.popBackStack() + } else { + viewModel.login(username, password, captcha, snowpiercer, true) + if (username.contains("@")) { + binding.textTips.setText(getString(R.string.enter_using_username_instead)) + binding.textTips.fadeIn() + } else { + binding.textTips.fadeOut() + } + } + } + + private fun onStep(step: LoginSagresRepository.Step) { + binding.contentLoading.setProgressWithAnimation(step.step.toFloat() * 100 / step.count) + } + + private fun onLoginProgress(callback: Callback) { + Timber.d("${callback.status}, ${callback.message}, ${callback.code}") + when (callback.status) { + Status.STARTED -> Timber.d("Status: Started") + Status.LOADING -> Timber.d("Status: Loading") + Status.INVALID_LOGIN -> { + val code = callback.code + if (code != 401) { + Timber.tag("connect_fail").e("User did not connect due to code $code") + snackAndBack(getString(R.string.error_invalid_connect_error_with_code, code)) + } else { + snackAndBack(getString(R.string.error_invalid_credentials)) + } + } + Status.APPROVING -> Timber.d("Status: Approving") + Status.NETWORK_ERROR -> snackAndBack(getString(R.string.error_network_error)) + Status.RESPONSE_FAILED -> snackAndBack(getString(R.string.error_unexpected_response_joke)) + Status.SUCCESS -> completeLogin() + Status.APPROVAL_ERROR -> snackAndBack(getString(R.string.error_network_error)) + Status.GRADES_FAILED -> completeLogin() + Status.UNKNOWN_FAILURE -> snackAndBack(getString(R.string.unknown_error)) + Status.COMPLETED -> completeLogin() + } + +// val document = callback.document +// if (document != null) { +// val html = document.html() +// binding.testingWebview.run { +// loadDataWithBaseURL("", html, "text/html", "ISO8859-1", "") +// } +// } + } + + private fun onProfileUpdate(profile: Profile?) { + if (profile != null) { + val username = arguments?.getString("username") + if (username != null && username.contains("@")) { + binding.textHelloUser.text = getString(R.string.attention_user_login_with_email, profile.name) + } else { + binding.textHelloUser.text = getString(R.string.login_hello_user, profile.name) + } + binding.textHelloUser.fadeIn() + } + } + + private fun completeLogin() { + if (viewModel.isConnected()) return + viewModel.setConnected() + val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), binding.imageCenter, getString(R.string.user_image_transition)) + val extras = ActivityNavigator.Extras.Builder() + .setActivityOptions(options) + .build() + + binding.textHelloUser.fadeOut() + + Handler(Looper.getMainLooper()).postDelayed( + { + if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { + binding.textHelloUser.text = "" + findNavController().navigate(R.id.action_login_to_setup, null, null, extras) + activity?.finishAfterTransition() + } + }, + 1000 + ) + } + + private fun snackAndBack(string: String) { + showSnack(string) + binding.textHelloUser.text = "" + binding.textHelloUser.fadeOut() + binding.textTips.fadeOut() + findNavController().popBackStack(R.id.login_form, false) + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/mechcalculator/MechCalcRepository.kt b/app/src/main/java/com/forcetower/uefs/feature/mechcalculator/MechCalcRepository.kt index 5de04bb60..56ab2ced3 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/mechcalculator/MechCalcRepository.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/mechcalculator/MechCalcRepository.kt @@ -1,121 +1,121 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.mechcalculator - -import androidx.annotation.AnyThread -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import com.forcetower.uefs.AppExecutors -import com.forcetower.uefs.core.util.round -import com.forcetower.uefs.core.util.truncate -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.math.min - -@Singleton -class MechCalcRepository @Inject constructor( - private val executors: AppExecutors -) { - private var desiredMean = 7.0 - - private val _result = MutableLiveData() - val result: LiveData - get() = _result - - private val values = mutableListOf() - private val _mechanics = MutableLiveData>() - val mechanics: LiveData> - get() = _mechanics - - @AnyThread - fun onAddValue(value: MechValue) { - values.add(value) - _mechanics.postValue(values.toMutableList()) - calculate() - } - - @AnyThread - fun onDeleteValue(value: MechValue) { - values.remove(value) - _mechanics.postValue(values.toMutableList()) - calculate() - } - - @AnyThread - fun calculate() { - executors.others().execute { - Timber.d("Values: $values") - var wildcardWeight = 0.0 - var gradesSum = 0.0 - var weightSum = 0.0 - for (value in values) { - val grade = value.grade - if (grade == null) { - wildcardWeight += value.weight - } else { - gradesSum += (grade * value.weight).truncate() - weightSum += value.weight - } - } - - val rightEquation = ((weightSum + wildcardWeight) * desiredMean).truncate() - if (wildcardWeight == 0.0) { - val mean = (gradesSum / weightSum).truncate() - if (gradesSum >= rightEquation) { - _result.postValue(MechResult(mean)) - } else { - val mech = onFinals(mean, false) - _result.postValue(mech) - } - } else { - val newRight = rightEquation - gradesSum - val wildcard = (newRight / wildcardWeight).truncate() - val normWildcard = min(wildcard, 10.0) - - val additional = values.filter { it.grade == null }.sumOf { (normWildcard * it.weight).truncate() } - val finalGrade = gradesSum + additional - val finalWeight = weightSum + wildcardWeight - val theMean = (finalGrade / finalWeight).truncate() - if (wildcard > 10) { - val mech = onFinals(theMean, true) - _result.postValue(mech) - } else { - _result.postValue(MechResult(theMean, wildcard, null, final = false, lost = false)) - } - } - } - } - - private fun onFinals(mean: Double, needsWildcard: Boolean): MechResult { - val wildcard = if (needsWildcard) 10.0 else null - return if (mean < 3) { - MechResult(mean, null, null, final = false, lost = true) - } else { - val finalGrade = (12.5 - (1.5 * mean)).round() - if (finalGrade <= 8) { - MechResult(mean, wildcard, finalGrade, final = true, lost = false) - } else { - MechResult(mean, wildcard, finalGrade, final = true, lost = true) - } - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.mechcalculator + +import androidx.annotation.AnyThread +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.forcetower.uefs.AppExecutors +import com.forcetower.uefs.core.util.round +import com.forcetower.uefs.core.util.truncate +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.math.min +import timber.log.Timber + +@Singleton +class MechCalcRepository @Inject constructor( + private val executors: AppExecutors +) { + private var desiredMean = 7.0 + + private val _result = MutableLiveData() + val result: LiveData + get() = _result + + private val values = mutableListOf() + private val _mechanics = MutableLiveData>() + val mechanics: LiveData> + get() = _mechanics + + @AnyThread + fun onAddValue(value: MechValue) { + values.add(value) + _mechanics.postValue(values.toMutableList()) + calculate() + } + + @AnyThread + fun onDeleteValue(value: MechValue) { + values.remove(value) + _mechanics.postValue(values.toMutableList()) + calculate() + } + + @AnyThread + fun calculate() { + executors.others().execute { + Timber.d("Values: $values") + var wildcardWeight = 0.0 + var gradesSum = 0.0 + var weightSum = 0.0 + for (value in values) { + val grade = value.grade + if (grade == null) { + wildcardWeight += value.weight + } else { + gradesSum += (grade * value.weight).truncate() + weightSum += value.weight + } + } + + val rightEquation = ((weightSum + wildcardWeight) * desiredMean).truncate() + if (wildcardWeight == 0.0) { + val mean = (gradesSum / weightSum).truncate() + if (gradesSum >= rightEquation) { + _result.postValue(MechResult(mean)) + } else { + val mech = onFinals(mean, false) + _result.postValue(mech) + } + } else { + val newRight = rightEquation - gradesSum + val wildcard = (newRight / wildcardWeight).truncate() + val normWildcard = min(wildcard, 10.0) + + val additional = values.filter { it.grade == null }.sumOf { (normWildcard * it.weight).truncate() } + val finalGrade = gradesSum + additional + val finalWeight = weightSum + wildcardWeight + val theMean = (finalGrade / finalWeight).truncate() + if (wildcard > 10) { + val mech = onFinals(theMean, true) + _result.postValue(mech) + } else { + _result.postValue(MechResult(theMean, wildcard, null, final = false, lost = false)) + } + } + } + } + + private fun onFinals(mean: Double, needsWildcard: Boolean): MechResult { + val wildcard = if (needsWildcard) 10.0 else null + return if (mean < 3) { + MechResult(mean, null, null, final = false, lost = true) + } else { + val finalGrade = (12.5 - (1.5 * mean)).round() + if (finalGrade <= 8) { + MechResult(mean, wildcard, finalGrade, final = true, lost = false) + } else { + MechResult(mean, wildcard, finalGrade, final = true, lost = true) + } + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/messages/MessagesDFMViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/messages/MessagesDFMViewModel.kt index 2b2e57d56..0a538e794 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/messages/MessagesDFMViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/messages/MessagesDFMViewModel.kt @@ -37,8 +37,8 @@ import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListene import com.google.android.play.core.splitinstall.model.SplitInstallErrorCode import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus import dagger.hilt.android.lifecycle.HiltViewModel -import timber.log.Timber import javax.inject.Inject +import timber.log.Timber @HiltViewModel class MessagesDFMViewModel @Inject constructor( diff --git a/app/src/main/java/com/forcetower/uefs/feature/messages/MessagesFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/messages/MessagesFragment.kt index 628083166..4960b8eee 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/messages/MessagesFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/messages/MessagesFragment.kt @@ -1,176 +1,177 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.messages - -import android.content.SharedPreferences -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.View.VISIBLE -import android.view.ViewGroup -import android.widget.ArrayAdapter -import androidx.appcompat.app.AlertDialog -import androidx.core.os.bundleOf -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentPagerAdapter -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.lifecycleScope -import com.forcetower.core.lifecycle.EventObserver -import com.forcetower.uefs.R -import com.forcetower.uefs.UApplication -import com.forcetower.uefs.core.storage.database.UDatabase -import com.forcetower.uefs.core.util.getLinks -import com.forcetower.uefs.core.util.isStudentFromUEFS -import com.forcetower.uefs.databinding.FragmentAllMessagesBinding -import com.forcetower.uefs.feature.home.HomeViewModel -import com.forcetower.uefs.feature.messages.dynamic.AERIMessageFragment -import com.forcetower.uefs.feature.profile.ProfileViewModel -import com.forcetower.uefs.feature.shared.UFragment -import com.forcetower.uefs.feature.shared.extensions.openURL -import com.google.android.material.snackbar.Snackbar -import com.google.android.material.tabs.TabLayout -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch -import javax.inject.Inject - -@AndroidEntryPoint -class MessagesFragment : UFragment() { - @Inject - lateinit var preferences: SharedPreferences - @Inject - lateinit var database: UDatabase - - private lateinit var binding: FragmentAllMessagesBinding - private val profileViewModel: ProfileViewModel by activityViewModels() - private val messagesViewModel: MessagesViewModel by activityViewModels() - private val homeViewModel: HomeViewModel by activityViewModels() - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - binding = FragmentAllMessagesBinding.inflate(inflater, container, false).apply { - profileViewModel = this@MessagesFragment.profileViewModel - lifecycleOwner = this@MessagesFragment - } - - preparePager() - return binding.root - } - - private fun preparePager() { - val tabLayout = binding.tabLayout - tabLayout.visibility = VISIBLE - - tabLayout.clearOnTabSelectedListeners() - tabLayout.removeAllTabs() - - tabLayout.setupWithViewPager(binding.pagerMessage) - tabLayout.addOnTabSelectedListener(TabLayout.ViewPagerOnTabSelectedListener(binding.pagerMessage)) - binding.pagerMessage.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabLayout)) - - val fragments = mutableListOf() - fragments += SagresMessagesFragment() - fragments += UnesMessagesFragment() - if (preferences.isStudentFromUEFS() && preferences.getBoolean("stg_advanced_aeri_tab", true)) { - fragments += AERIMessageFragment() - } - - binding.pagerMessage.adapter = SectionFragmentAdapter(childFragmentManager, fragments) - binding.textToolbarTitle.setOnLongClickListener { - lifecycleScope.launch { - database.messageDao().deleteAllSuspend() - } - true - } - binding.textToolbarTitle.setOnClickListener { - (requireContext().applicationContext as UApplication).messageToolbarDevClickCount++ - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val index = arguments?.getInt(EXTRA_MESSAGES_FLAG, 0) ?: 0 - val open = savedInstanceState?.getBoolean(EXTRA_OPEN_MESSAGES_FLAG, true) ?: true - if (index > 0 && open) { - binding.pagerMessage.setCurrentItem(index, true) - } - - messagesViewModel.messageClick.observe(viewLifecycleOwner, EventObserver { openLink(it) }) - messagesViewModel.snackMessage.observe(viewLifecycleOwner, EventObserver { showSnack(getString(it), Snackbar.LENGTH_LONG) }) - } - - private fun openLink(content: String) { - val links = content.getLinks() - if (links.isEmpty()) return - - if (links.size == 1) { - try { - requireContext().openURL(links[0]) - } catch (ignored: Throwable) { - homeViewModel.showSnack(getString(R.string.unable_to_open_url)) - } - } else { - val adapter = ArrayAdapter(requireContext(), android.R.layout.select_dialog_item) - adapter.addAll(links) - - val dialog = AlertDialog.Builder(requireContext()) - .setIcon(R.drawable.ic_http_accent_30dp) - .setTitle(R.string.select_a_link) - .setAdapter(adapter) { dialog, position -> - val url = adapter.getItem(position) - dialog.dismiss() - try { - if (url != null) requireContext().openURL(url) - } catch (ignored: Throwable) { - homeViewModel.showSnack(getString(R.string.unable_to_open_url)) - } - } - .setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() } - .create() - - dialog.show() - } - } - - override fun onStop() { - super.onStop() - messagesViewModel.pushedTimes = 0 - } - - override fun onSaveInstanceState(outState: Bundle) { - outState.putBoolean(EXTRA_OPEN_MESSAGES_FLAG, false) - super.onSaveInstanceState(outState) - } - - private class SectionFragmentAdapter(fm: FragmentManager, val fragments: List) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { - override fun getCount() = fragments.size - override fun getItem(position: Int) = fragments[position] - override fun getPageTitle(position: Int) = fragments[position].displayName - } - - companion object { - const val EXTRA_MESSAGES_FLAG = "unes.messages.is_svc_message" - const val EXTRA_OPEN_MESSAGES_FLAG = "unes.messages.opened_notification" - - fun newInstance(fragmentIndex: Int): MessagesFragment { - return MessagesFragment().apply { - arguments = bundleOf(EXTRA_MESSAGES_FLAG to fragmentIndex) - } - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.messages + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.widget.ArrayAdapter +import androidx.appcompat.app.AlertDialog +import androidx.core.os.bundleOf +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentPagerAdapter +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import com.forcetower.core.lifecycle.EventObserver +import com.forcetower.uefs.R +import com.forcetower.uefs.UApplication +import com.forcetower.uefs.core.storage.database.UDatabase +import com.forcetower.uefs.core.util.getLinks +import com.forcetower.uefs.core.util.isStudentFromUEFS +import com.forcetower.uefs.databinding.FragmentAllMessagesBinding +import com.forcetower.uefs.feature.home.HomeViewModel +import com.forcetower.uefs.feature.messages.dynamic.AERIMessageFragment +import com.forcetower.uefs.feature.profile.ProfileViewModel +import com.forcetower.uefs.feature.shared.UFragment +import com.forcetower.uefs.feature.shared.extensions.openURL +import com.google.android.material.snackbar.Snackbar +import com.google.android.material.tabs.TabLayout +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class MessagesFragment : UFragment() { + @Inject + lateinit var preferences: SharedPreferences + + @Inject + lateinit var database: UDatabase + + private lateinit var binding: FragmentAllMessagesBinding + private val profileViewModel: ProfileViewModel by activityViewModels() + private val messagesViewModel: MessagesViewModel by activityViewModels() + private val homeViewModel: HomeViewModel by activityViewModels() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = FragmentAllMessagesBinding.inflate(inflater, container, false).apply { + profileViewModel = this@MessagesFragment.profileViewModel + lifecycleOwner = this@MessagesFragment + } + + preparePager() + return binding.root + } + + private fun preparePager() { + val tabLayout = binding.tabLayout + tabLayout.visibility = VISIBLE + + tabLayout.clearOnTabSelectedListeners() + tabLayout.removeAllTabs() + + tabLayout.setupWithViewPager(binding.pagerMessage) + tabLayout.addOnTabSelectedListener(TabLayout.ViewPagerOnTabSelectedListener(binding.pagerMessage)) + binding.pagerMessage.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabLayout)) + + val fragments = mutableListOf() + fragments += SagresMessagesFragment() + fragments += UnesMessagesFragment() + if (preferences.isStudentFromUEFS() && preferences.getBoolean("stg_advanced_aeri_tab", true)) { + fragments += AERIMessageFragment() + } + + binding.pagerMessage.adapter = SectionFragmentAdapter(childFragmentManager, fragments) + binding.textToolbarTitle.setOnLongClickListener { + lifecycleScope.launch { + database.messageDao().deleteAllSuspend() + } + true + } + binding.textToolbarTitle.setOnClickListener { + (requireContext().applicationContext as UApplication).messageToolbarDevClickCount++ + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val index = arguments?.getInt(EXTRA_MESSAGES_FLAG, 0) ?: 0 + val open = savedInstanceState?.getBoolean(EXTRA_OPEN_MESSAGES_FLAG, true) ?: true + if (index > 0 && open) { + binding.pagerMessage.setCurrentItem(index, true) + } + + messagesViewModel.messageClick.observe(viewLifecycleOwner, EventObserver { openLink(it) }) + messagesViewModel.snackMessage.observe(viewLifecycleOwner, EventObserver { showSnack(getString(it), Snackbar.LENGTH_LONG) }) + } + + private fun openLink(content: String) { + val links = content.getLinks() + if (links.isEmpty()) return + + if (links.size == 1) { + try { + requireContext().openURL(links[0]) + } catch (ignored: Throwable) { + homeViewModel.showSnack(getString(R.string.unable_to_open_url)) + } + } else { + val adapter = ArrayAdapter(requireContext(), android.R.layout.select_dialog_item) + adapter.addAll(links) + + val dialog = AlertDialog.Builder(requireContext()) + .setIcon(R.drawable.ic_http_accent_30dp) + .setTitle(R.string.select_a_link) + .setAdapter(adapter) { dialog, position -> + val url = adapter.getItem(position) + dialog.dismiss() + try { + if (url != null) requireContext().openURL(url) + } catch (ignored: Throwable) { + homeViewModel.showSnack(getString(R.string.unable_to_open_url)) + } + } + .setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() } + .create() + + dialog.show() + } + } + + override fun onStop() { + super.onStop() + messagesViewModel.pushedTimes = 0 + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putBoolean(EXTRA_OPEN_MESSAGES_FLAG, false) + super.onSaveInstanceState(outState) + } + + private class SectionFragmentAdapter(fm: FragmentManager, val fragments: List) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + override fun getCount() = fragments.size + override fun getItem(position: Int) = fragments[position] + override fun getPageTitle(position: Int) = fragments[position].displayName + } + + companion object { + const val EXTRA_MESSAGES_FLAG = "unes.messages.is_svc_message" + const val EXTRA_OPEN_MESSAGES_FLAG = "unes.messages.opened_notification" + + fun newInstance(fragmentIndex: Int): MessagesFragment { + return MessagesFragment().apply { + arguments = bundleOf(EXTRA_MESSAGES_FLAG to fragmentIndex) + } + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/messages/MessagesViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/messages/MessagesViewModel.kt index c9f24679a..7aa4476d4 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/messages/MessagesViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/messages/MessagesViewModel.kt @@ -1,141 +1,141 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.messages - -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent -import android.view.View -import androidx.core.content.FileProvider -import androidx.core.content.getSystemService -import androidx.core.view.drawToBitmap -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.asLiveData -import androidx.lifecycle.viewModelScope -import androidx.paging.cachedIn -import com.forcetower.core.lifecycle.Event -import com.forcetower.uefs.BuildConfig -import com.forcetower.uefs.R -import com.forcetower.uefs.core.model.service.UMessage -import com.forcetower.uefs.core.model.unes.Message -import com.forcetower.uefs.core.storage.repository.MessagesRepository -import com.forcetower.uefs.core.task.usecase.message.FetchAllMessagesSnowpiercerUseCase -import com.forcetower.uefs.feature.shared.extensions.toFile -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import javax.inject.Inject -import javax.inject.Named - -@HiltViewModel -class MessagesViewModel @Inject constructor( - private val repository: MessagesRepository, - @Named("flagSnowpiercerEnabled") private val snowpiercerEnabled: Boolean, - private val fetchAllMessagesSnowpiercerUseCase: FetchAllMessagesSnowpiercerUseCase -) : ViewModel(), MessagesActions { - val messages = repository.getMessages().cachedIn(viewModelScope).asLiveData() - val unesMessages by lazy { repository.getUnesMessages() } - var pushedTimes = 0 - - private val _refreshing = MediatorLiveData() - val refreshing: LiveData - get() = _refreshing - - private val _messageClick = MutableLiveData>() - val messageClick: LiveData> - get() = _messageClick - - private val _portalMessageClick = MutableLiveData>() - val portalMessageClick: LiveData> - get() = _portalMessageClick - - private val _snackMessage = MutableLiveData>() - val snackMessage: LiveData> - get() = _snackMessage - - fun onRefresh() { - pushedTimes++ - - if (pushedTimes == 3 && snowpiercerEnabled) { - _snackMessage.value = Event(R.string.download_all_messages) - viewModelScope.launch { - _refreshing.value = true - fetchAllMessagesSnowpiercerUseCase(Unit) - _refreshing.value = false - pushedTimes = 0 - } - } else { - val fetchMessages = repository.fetchMessages(pushedTimes == 3) - _refreshing.value = true - _refreshing.addSource(fetchMessages) { - _refreshing.removeSource(fetchMessages) - _refreshing.value = false - } - } - } - - override fun onMessageClick(message: String?) { - message ?: return - _messageClick.value = Event(message) - } - - override fun onPortalMessageClick(message: Message?) { - message ?: return - onMessageClick(message.content) - } - - override fun onUNESMessageLongClick(view: View, message: UMessage?): Boolean { - message ?: return false - val context = view.context - val content = "Mensagem UNES\n\n${message.message}" - return shareMessage(context, content) - } - - override fun onMessageLongClick(view: View, message: Message?): Boolean { - message ?: return false - val context = view.context - val content = "${message.content}\n\nEnviada por ${message.senderName}" - return shareMessage(context, content) - } - - override fun onMessageShare(view: View, pos: Int) { - val context = view.context - val file = view.drawToBitmap().toFile(context) - - val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file) - val intent = Intent(Intent.ACTION_SEND) - intent.putExtra(Intent.EXTRA_STREAM, uri) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - intent.type = "image/jpg" - context.startActivity(intent) - } - - private fun shareMessage(context: Context, content: String): Boolean { - val clipboard: ClipboardManager? = context.getSystemService() - clipboard ?: return false - clipboard.setPrimaryClip(ClipData.newPlainText("unes-message", content)) - _snackMessage.value = Event(R.string.message_copied_to_clipboard) - return true - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.messages + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.view.View +import androidx.core.content.FileProvider +import androidx.core.content.getSystemService +import androidx.core.view.drawToBitmap +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import androidx.paging.cachedIn +import com.forcetower.core.lifecycle.Event +import com.forcetower.uefs.BuildConfig +import com.forcetower.uefs.R +import com.forcetower.uefs.core.model.service.UMessage +import com.forcetower.uefs.core.model.unes.Message +import com.forcetower.uefs.core.storage.repository.MessagesRepository +import com.forcetower.uefs.core.task.usecase.message.FetchAllMessagesSnowpiercerUseCase +import com.forcetower.uefs.feature.shared.extensions.toFile +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import javax.inject.Named +import kotlinx.coroutines.launch + +@HiltViewModel +class MessagesViewModel @Inject constructor( + private val repository: MessagesRepository, + @Named("flagSnowpiercerEnabled") private val snowpiercerEnabled: Boolean, + private val fetchAllMessagesSnowpiercerUseCase: FetchAllMessagesSnowpiercerUseCase +) : ViewModel(), MessagesActions { + val messages = repository.getMessages().cachedIn(viewModelScope).asLiveData() + val unesMessages by lazy { repository.getUnesMessages() } + var pushedTimes = 0 + + private val _refreshing = MediatorLiveData() + val refreshing: LiveData + get() = _refreshing + + private val _messageClick = MutableLiveData>() + val messageClick: LiveData> + get() = _messageClick + + private val _portalMessageClick = MutableLiveData>() + val portalMessageClick: LiveData> + get() = _portalMessageClick + + private val _snackMessage = MutableLiveData>() + val snackMessage: LiveData> + get() = _snackMessage + + fun onRefresh() { + pushedTimes++ + + if (pushedTimes == 3 && snowpiercerEnabled) { + _snackMessage.value = Event(R.string.download_all_messages) + viewModelScope.launch { + _refreshing.value = true + fetchAllMessagesSnowpiercerUseCase(Unit) + _refreshing.value = false + pushedTimes = 0 + } + } else { + val fetchMessages = repository.fetchMessages(pushedTimes == 3) + _refreshing.value = true + _refreshing.addSource(fetchMessages) { + _refreshing.removeSource(fetchMessages) + _refreshing.value = false + } + } + } + + override fun onMessageClick(message: String?) { + message ?: return + _messageClick.value = Event(message) + } + + override fun onPortalMessageClick(message: Message?) { + message ?: return + onMessageClick(message.content) + } + + override fun onUNESMessageLongClick(view: View, message: UMessage?): Boolean { + message ?: return false + val context = view.context + val content = "Mensagem UNES\n\n${message.message}" + return shareMessage(context, content) + } + + override fun onMessageLongClick(view: View, message: Message?): Boolean { + message ?: return false + val context = view.context + val content = "${message.content}\n\nEnviada por ${message.senderName}" + return shareMessage(context, content) + } + + override fun onMessageShare(view: View, pos: Int) { + val context = view.context + val file = view.drawToBitmap().toFile(context) + + val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file) + val intent = Intent(Intent.ACTION_SEND) + intent.putExtra(Intent.EXTRA_STREAM, uri) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.type = "image/jpg" + context.startActivity(intent) + } + + private fun shareMessage(context: Context, content: String): Boolean { + val clipboard: ClipboardManager? = context.getSystemService() + clipboard ?: return false + clipboard.setPrimaryClip(ClipData.newPlainText("unes-message", content)) + _snackMessage.value = Event(R.string.message_copied_to_clipboard) + return true + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/messages/SagresMessagesFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/messages/SagresMessagesFragment.kt index f83725380..0c7d38dd5 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/messages/SagresMessagesFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/messages/SagresMessagesFragment.kt @@ -1,62 +1,64 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.messages - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.activityViewModels -import com.forcetower.uefs.databinding.FragmentSagresMessagesBinding -import com.forcetower.uefs.feature.shared.UFragment -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class SagresMessagesFragment : UFragment() { - init { displayName = "Sagres" } - - private lateinit var binding: FragmentSagresMessagesBinding - private val viewModel: MessagesViewModel by activityViewModels() - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return FragmentSagresMessagesBinding.inflate(inflater, container, false).also { - binding = it - }.apply { - messagesViewModel = viewModel - lifecycleOwner = this@SagresMessagesFragment - }.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val adapter = SagresMessageAdapter(this, viewModel) - binding.apply { - recyclerSagresMessages.adapter = adapter - recyclerSagresMessages.itemAnimator?.run { - addDuration = 120L - moveDuration = 120L - changeDuration = 120L - removeDuration = 100L - } - } - - viewModel.messages.observe(viewLifecycleOwner, { adapter.submitData(lifecycle, it) }) - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.messages + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.activityViewModels +import com.forcetower.uefs.databinding.FragmentSagresMessagesBinding +import com.forcetower.uefs.feature.shared.UFragment +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class SagresMessagesFragment : UFragment() { + init { + displayName = "Sagres" + } + + private lateinit var binding: FragmentSagresMessagesBinding + private val viewModel: MessagesViewModel by activityViewModels() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return FragmentSagresMessagesBinding.inflate(inflater, container, false).also { + binding = it + }.apply { + messagesViewModel = viewModel + lifecycleOwner = this@SagresMessagesFragment + }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val adapter = SagresMessageAdapter(this, viewModel) + binding.apply { + recyclerSagresMessages.adapter = adapter + recyclerSagresMessages.itemAnimator?.run { + addDuration = 120L + moveDuration = 120L + changeDuration = 120L + removeDuration = 100L + } + } + + viewModel.messages.observe(viewLifecycleOwner, { adapter.submitData(lifecycle, it) }) + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/messages/UnesMessagesFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/messages/UnesMessagesFragment.kt index a2e195b4c..e79e5ca06 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/messages/UnesMessagesFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/messages/UnesMessagesFragment.kt @@ -1,87 +1,89 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.messages - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Observer -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.forcetower.uefs.databinding.FragmentUnesMessagesBinding -import com.forcetower.uefs.feature.shared.UFragment -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class UnesMessagesFragment : UFragment() { - init { displayName = "UNES" } - - private lateinit var binding: FragmentUnesMessagesBinding - - private val viewModel: MessagesViewModel by activityViewModels() - private lateinit var messagesAdapter: UnesMessageAdapter - private lateinit var adapterDataObserver: RecyclerView.AdapterDataObserver - - private var initialized = false - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return FragmentUnesMessagesBinding.inflate(inflater, container, false).also { - binding = it - }.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - messagesAdapter = UnesMessageAdapter(this, viewModel) - binding.recyclerSagresMessages.apply { - adapter = messagesAdapter - itemAnimator?.run { - addDuration = 120L - moveDuration = 120L - changeDuration = 120L - removeDuration = 100L - } - } - - val manager = binding.recyclerSagresMessages.layoutManager as LinearLayoutManager - adapterDataObserver = object : RecyclerView.AdapterDataObserver() { - override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { - super.onItemRangeInserted(positionStart, itemCount) - if (positionStart == 0 && initialized) { - manager.smoothScrollToPosition(binding.recyclerSagresMessages, null, 0) - } - initialized = true - } - } - - messagesAdapter.registerAdapterDataObserver(adapterDataObserver) - viewModel.unesMessages.observe(viewLifecycleOwner, Observer { messagesAdapter.submitList(it) }) - } - - override fun onDestroyView() { - super.onDestroyView() - // safe clean-up (prob unnecessary) - if (::adapterDataObserver.isInitialized) { - messagesAdapter.unregisterAdapterDataObserver(adapterDataObserver) - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.messages + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.forcetower.uefs.databinding.FragmentUnesMessagesBinding +import com.forcetower.uefs.feature.shared.UFragment +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class UnesMessagesFragment : UFragment() { + init { + displayName = "UNES" + } + + private lateinit var binding: FragmentUnesMessagesBinding + + private val viewModel: MessagesViewModel by activityViewModels() + private lateinit var messagesAdapter: UnesMessageAdapter + private lateinit var adapterDataObserver: RecyclerView.AdapterDataObserver + + private var initialized = false + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return FragmentUnesMessagesBinding.inflate(inflater, container, false).also { + binding = it + }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + messagesAdapter = UnesMessageAdapter(this, viewModel) + binding.recyclerSagresMessages.apply { + adapter = messagesAdapter + itemAnimator?.run { + addDuration = 120L + moveDuration = 120L + changeDuration = 120L + removeDuration = 100L + } + } + + val manager = binding.recyclerSagresMessages.layoutManager as LinearLayoutManager + adapterDataObserver = object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + super.onItemRangeInserted(positionStart, itemCount) + if (positionStart == 0 && initialized) { + manager.smoothScrollToPosition(binding.recyclerSagresMessages, null, 0) + } + initialized = true + } + } + + messagesAdapter.registerAdapterDataObserver(adapterDataObserver) + viewModel.unesMessages.observe(viewLifecycleOwner, Observer { messagesAdapter.submitList(it) }) + } + + override fun onDestroyView() { + super.onDestroyView() + // safe clean-up (prob unnecessary) + if (::adapterDataObserver.isInitialized) { + messagesAdapter.unregisterAdapterDataObserver(adapterDataObserver) + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/profile/ProfileAdapter.kt b/app/src/main/java/com/forcetower/uefs/feature/profile/ProfileAdapter.kt index 664115164..128bd3371 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/profile/ProfileAdapter.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/profile/ProfileAdapter.kt @@ -118,7 +118,9 @@ class ProfileAdapter( sealed class ProfileHolder(view: View) : RecyclerView.ViewHolder(view) { class Header(val binding: ItemProfileHeaderBinding, interactor: ProfileInteractor?) : ProfileHolder(binding.root) { - init { binding.interactor = interactor } + init { + binding.interactor = interactor + } } class Statement( val binding: ItemProfileStatementBinding, diff --git a/app/src/main/java/com/forcetower/uefs/feature/profile/ProfileBindingAdapters.kt b/app/src/main/java/com/forcetower/uefs/feature/profile/ProfileBindingAdapters.kt index 7861f1f18..f36664ed2 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/profile/ProfileBindingAdapters.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/profile/ProfileBindingAdapters.kt @@ -1,140 +1,141 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.profile - -import android.view.View -import android.widget.ImageView -import android.widget.TextView -import androidx.databinding.BindingAdapter -import androidx.preference.PreferenceManager -import com.bumptech.glide.Glide -import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions -import com.forcetower.uefs.R -import com.forcetower.uefs.core.model.unes.Semester -import java.text.SimpleDateFormat -import java.time.ZoneOffset -import java.time.ZonedDateTime -import java.util.Calendar -import java.util.Date -import java.util.Locale -import java.util.concurrent.TimeUnit -import kotlin.math.min - -@BindingAdapter("profileImage") -fun profileImage(iv: ImageView, url: String?) { - if (url == null) return - - Glide.with(iv.context) - .load(url) - .fallback(com.forcetower.core.R.mipmap.ic_unes_large_image_512) - .placeholder(com.forcetower.core.R.mipmap.ic_unes_large_image_512) - .circleCrop() - .transition(DrawableTransitionOptions.withCrossFade()) - .into(iv) -} - -@BindingAdapter(value = ["profileScoreOptional", "profileScoreCalculated", "semestersList", "profileCourse"], requireAll = true) -fun profileScoreOptional( - tv: TextView, - score: Double?, - calculated: Double?, - semesters: List?, - course: String? -) { - val context = tv.context - val preferences = PreferenceManager.getDefaultSharedPreferences(context) - - val actual = score ?: -1.0 - val calc = calculated ?: -1.0 - - // power up da nota - var currentIncrease = preferences.getFloat("score_increase_value", 0f) - val currentExpire = preferences.getLong("score_increase_expires", -1) - - val now = Calendar.getInstance().timeInMillis - if (currentExpire < now) currentIncrease = 0.0f - - if (preferences.getBoolean("stg_acc_score", true)) { - // caso queira o calculado - if (preferences.getBoolean("stg_choice_score", false)) - when { - calc != -1.0 -> tv.text = context.getString(R.string.label_your_calculated_score, min((calc + currentIncrease), 10.0)) - actual != -1.0 -> tv.text = context.getString(R.string.label_your_score, min((actual + currentIncrease), 10.0)) - } - // por padrão exibe o real que vem do SAGRES - else - when { - actual != -1.0 -> tv.text = context.getString(R.string.label_your_score, min((actual + currentIncrease), 10.0)) - calc != -1.0 -> tv.text = context.getString(R.string.label_your_calculated_score, min((calc + currentIncrease), 10.0)) - } - - // verificando se existe realmente um score - if (calc / actual == 1.0 && calc == -1.0) tv.text = context.getString(R.string.label_score_undefined) - } else if (preferences.getBoolean("stg_acc_semester", true)) { - val filtered = semesters?.filter { !it.name.endsWith("F") } - val number = filtered?.size ?: 1 - tv.text = context.getString(R.string.your_semester_is, number) - } else if (course != null) { - tv.text = course - } else { - tv.text = "" - } -} - -@BindingAdapter(value = ["zonedStatement"]) -fun getZonedTimeStampedDate(view: TextView, zonedDate: ZonedDateTime?) { - if (zonedDate == null) { - view.visibility = View.INVISIBLE - return - } - val time = zonedDate.toLocalDateTime().toInstant(ZoneOffset.ofHours(0)).toEpochMilli() - val context = view.context - val now = System.currentTimeMillis() - val diff = now - time - - val oneDay = TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS) - val oneHor = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS) - val days = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS) - val value = when { - days > 1L -> { - val format = SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.getDefault()) - val str = format.format(Date(time)) - context.getString(R.string.profile_statement_received_date_format, str) - } - days == 1L -> { - val hours = TimeUnit.HOURS.convert(diff - oneDay, TimeUnit.MILLISECONDS) - val str = days.toString() + "d " + hours.toString() + "h" - context.getString(R.string.profile_statement_received_date_ago_format, str) - } - else -> { - val hours = TimeUnit.HOURS.convert(diff, TimeUnit.MILLISECONDS) - val minutes = TimeUnit.MINUTES.convert(diff - (hours * oneHor), TimeUnit.MILLISECONDS) - val str = if (hours > 0) { - hours.toString() + "h " + minutes + "min" - } else { - minutes.toString() + "min" - } - context.getString(R.string.message_received_date_ago_format, str) - } - } - view.text = value - view.visibility = View.VISIBLE -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.profile + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.databinding.BindingAdapter +import androidx.preference.PreferenceManager +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions +import com.forcetower.uefs.R +import com.forcetower.uefs.core.model.unes.Semester +import java.text.SimpleDateFormat +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.util.Calendar +import java.util.Date +import java.util.Locale +import java.util.concurrent.TimeUnit +import kotlin.math.min + +@BindingAdapter("profileImage") +fun profileImage(iv: ImageView, url: String?) { + if (url == null) return + + Glide.with(iv.context) + .load(url) + .fallback(com.forcetower.core.R.mipmap.ic_unes_large_image_512) + .placeholder(com.forcetower.core.R.mipmap.ic_unes_large_image_512) + .circleCrop() + .transition(DrawableTransitionOptions.withCrossFade()) + .into(iv) +} + +@BindingAdapter(value = ["profileScoreOptional", "profileScoreCalculated", "semestersList", "profileCourse"], requireAll = true) +fun profileScoreOptional( + tv: TextView, + score: Double?, + calculated: Double?, + semesters: List?, + course: String? +) { + val context = tv.context + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + + val actual = score ?: -1.0 + val calc = calculated ?: -1.0 + + // power up da nota + var currentIncrease = preferences.getFloat("score_increase_value", 0f) + val currentExpire = preferences.getLong("score_increase_expires", -1) + + val now = Calendar.getInstance().timeInMillis + if (currentExpire < now) currentIncrease = 0.0f + + if (preferences.getBoolean("stg_acc_score", true)) { + // caso queira o calculado + if (preferences.getBoolean("stg_choice_score", false)) { + when { + calc != -1.0 -> tv.text = context.getString(R.string.label_your_calculated_score, min((calc + currentIncrease), 10.0)) + actual != -1.0 -> tv.text = context.getString(R.string.label_your_score, min((actual + currentIncrease), 10.0)) + } + } // por padrão exibe o real que vem do SAGRES + else { + when { + actual != -1.0 -> tv.text = context.getString(R.string.label_your_score, min((actual + currentIncrease), 10.0)) + calc != -1.0 -> tv.text = context.getString(R.string.label_your_calculated_score, min((calc + currentIncrease), 10.0)) + } + } + + // verificando se existe realmente um score + if (calc / actual == 1.0 && calc == -1.0) tv.text = context.getString(R.string.label_score_undefined) + } else if (preferences.getBoolean("stg_acc_semester", true)) { + val filtered = semesters?.filter { !it.name.endsWith("F") } + val number = filtered?.size ?: 1 + tv.text = context.getString(R.string.your_semester_is, number) + } else if (course != null) { + tv.text = course + } else { + tv.text = "" + } +} + +@BindingAdapter(value = ["zonedStatement"]) +fun getZonedTimeStampedDate(view: TextView, zonedDate: ZonedDateTime?) { + if (zonedDate == null) { + view.visibility = View.INVISIBLE + return + } + val time = zonedDate.toLocalDateTime().toInstant(ZoneOffset.ofHours(0)).toEpochMilli() + val context = view.context + val now = System.currentTimeMillis() + val diff = now - time + + val oneDay = TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS) + val oneHor = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS) + val days = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS) + val value = when { + days > 1L -> { + val format = SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.getDefault()) + val str = format.format(Date(time)) + context.getString(R.string.profile_statement_received_date_format, str) + } + days == 1L -> { + val hours = TimeUnit.HOURS.convert(diff - oneDay, TimeUnit.MILLISECONDS) + val str = days.toString() + "d " + hours.toString() + "h" + context.getString(R.string.profile_statement_received_date_ago_format, str) + } + else -> { + val hours = TimeUnit.HOURS.convert(diff, TimeUnit.MILLISECONDS) + val minutes = TimeUnit.MINUTES.convert(diff - (hours * oneHor), TimeUnit.MILLISECONDS) + val str = if (hours > 0) { + hours.toString() + "h " + minutes + "min" + } else { + minutes.toString() + "min" + } + context.getString(R.string.message_received_date_ago_format, str) + } + } + view.text = value + view.visibility = View.VISIBLE +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/profile/ProfileFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/profile/ProfileFragment.kt index ca0c59750..3aae93316 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/profile/ProfileFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/profile/ProfileFragment.kt @@ -1,196 +1,200 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.profile - -import android.graphics.drawable.Drawable -import android.net.Uri -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat -import androidx.core.os.bundleOf -import androidx.core.view.doOnLayout -import androidx.fragment.app.viewModels -import androidx.lifecycle.Observer -import com.canhub.cropper.CropImageContract -import com.canhub.cropper.CropImageContractOptions -import com.canhub.cropper.CropImageOptions -import com.canhub.cropper.CropImageView -import com.forcetower.core.adapters.ImageLoadListener -import com.forcetower.core.utils.ColorUtils -import com.forcetower.uefs.R -import com.forcetower.uefs.databinding.FragmentProfileBinding -import com.forcetower.uefs.feature.profile.ProfileActivity.Companion.EXTRA_USER_ID -import com.forcetower.uefs.feature.setup.SetupViewModel -import com.forcetower.uefs.feature.shared.UFragment -import com.forcetower.uefs.feature.shared.extensions.inTransaction -import com.forcetower.uefs.feature.shared.extensions.postponeEnterTransition -import com.forcetower.uefs.feature.shared.getPixelsFromDp -import com.forcetower.uefs.feature.siecomp.session.PushUpScrollListener -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class ProfileFragment : UFragment() { - - private val pickImageContract = registerForActivityResult(ActivityResultContracts.GetContent()) { - onContentSelected(it) - } - - private val cropImage = registerForActivityResult(CropImageContract()) { - onCropResults(it) - } - - private val viewModel: ProfileViewModel by viewModels() - private val setupViewModel: SetupViewModel by viewModels() - private lateinit var binding: FragmentProfileBinding - private lateinit var adapter: ProfileAdapter - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - activity?.postponeEnterTransition(500L) - val userId = requireArguments().getString(EXTRA_USER_ID) ?: throw IllegalStateException("Welp...") - viewModel.setUserId(userId) - viewModel.profile.observe( - viewLifecycleOwner, - Observer { - it ?: return@Observer - if (it.imageUrl == null) { - activity?.startPostponedEnterTransition() - } - if (it.me) { - binding.writeStatement.hide() - } else { - binding.writeStatement.show() - } - } - ) - - val headLoadListener = object : ImageLoadListener { - override fun onImageLoaded(drawable: Drawable) { activity?.startPostponedEnterTransition() } - override fun onImageLoadFailed() { activity?.startPostponedEnterTransition() } - } - - adapter = ProfileAdapter(viewModel, this, headLoadListener, viewModel) - return FragmentProfileBinding.inflate(inflater, container, false).also { - binding = it - }.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - binding.apply { - lifecycleOwner = this@ProfileFragment - viewModel = this@ProfileFragment.viewModel - executePendingBindings() - profileRecycler.apply { - adapter = this@ProfileFragment.adapter - itemAnimator?.run { - addDuration = 120L - moveDuration = 120L - changeDuration = 120L - removeDuration = 100L - } - doOnLayout { - addOnScrollListener( - PushUpScrollListener( - binding.up, - it, - R.id.student_name, - R.id.student_course - ) - ) - } - } - } - - viewModel.statements.observe( - viewLifecycleOwner - ) { statements -> - adapter.statements = statements.sortedByDescending { it.createdAt } - } - - binding.up.setOnClickListener { - requireActivity().finishAfterTransition() - } - - binding.writeStatement.setOnClickListener { - val userId = requireNotNull(arguments).getLong(EXTRA_USER_ID, 0) - parentFragmentManager.inTransaction { - val fragment = WriteStatementFragment().apply { - arguments = bundleOf( - EXTRA_USER_ID to userId - ) - } - replace(R.id.fragment_container, fragment, "write_statement") - addToBackStack("home") - } - } - } - - fun pickImage() { - pickImageContract.launch("image/*") - } - - private fun onContentSelected(uri: Uri?) { - uri ?: return - val bg = ColorUtils.modifyAlpha(ContextCompat.getColor(requireContext(), R.color.colorPrimary), 120) - val ac = ContextCompat.getColor(requireContext(), R.color.colorAccent) - - val options = CropImageContractOptions( - uri, - CropImageOptions( - fixAspectRatio = true, - aspectRatioX = 1, - aspectRatioY = 1, - cropShape = CropImageView.CropShape.OVAL, - backgroundColor = bg, - borderLineColor = ac, - borderCornerColor = ac, - activityMenuIconColor = ac, - borderLineThickness = getPixelsFromDp(requireContext(), 2), - activityTitle = getString(R.string.cut_profile_image), - guidelines = CropImageView.Guidelines.OFF - ) - ) - - cropImage.launch(options) - } - - private fun onCropResults(result: CropImageView.CropResult) { - val imageUri = result.uriContent ?: return - onImagePicked(imageUri) - } - - private fun onImagePicked(uri: Uri) { - setupViewModel.setSelectedImage(uri) - setupViewModel.uploadImageToStorage() - } - - companion object { - fun newInstance(userId: String): ProfileFragment { - return ProfileFragment().apply { - arguments = bundleOf( - EXTRA_USER_ID to userId - ) - } - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.profile + +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat +import androidx.core.os.bundleOf +import androidx.core.view.doOnLayout +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import com.canhub.cropper.CropImageContract +import com.canhub.cropper.CropImageContractOptions +import com.canhub.cropper.CropImageOptions +import com.canhub.cropper.CropImageView +import com.forcetower.core.adapters.ImageLoadListener +import com.forcetower.core.utils.ColorUtils +import com.forcetower.uefs.R +import com.forcetower.uefs.databinding.FragmentProfileBinding +import com.forcetower.uefs.feature.profile.ProfileActivity.Companion.EXTRA_USER_ID +import com.forcetower.uefs.feature.setup.SetupViewModel +import com.forcetower.uefs.feature.shared.UFragment +import com.forcetower.uefs.feature.shared.extensions.inTransaction +import com.forcetower.uefs.feature.shared.extensions.postponeEnterTransition +import com.forcetower.uefs.feature.shared.getPixelsFromDp +import com.forcetower.uefs.feature.siecomp.session.PushUpScrollListener +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class ProfileFragment : UFragment() { + + private val pickImageContract = registerForActivityResult(ActivityResultContracts.GetContent()) { + onContentSelected(it) + } + + private val cropImage = registerForActivityResult(CropImageContract()) { + onCropResults(it) + } + + private val viewModel: ProfileViewModel by viewModels() + private val setupViewModel: SetupViewModel by viewModels() + private lateinit var binding: FragmentProfileBinding + private lateinit var adapter: ProfileAdapter + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + activity?.postponeEnterTransition(500L) + val userId = requireArguments().getString(EXTRA_USER_ID) ?: throw IllegalStateException("Welp...") + viewModel.setUserId(userId) + viewModel.profile.observe( + viewLifecycleOwner, + Observer { + it ?: return@Observer + if (it.imageUrl == null) { + activity?.startPostponedEnterTransition() + } + if (it.me) { + binding.writeStatement.hide() + } else { + binding.writeStatement.show() + } + } + ) + + val headLoadListener = object : ImageLoadListener { + override fun onImageLoaded(drawable: Drawable) { + activity?.startPostponedEnterTransition() + } + override fun onImageLoadFailed() { + activity?.startPostponedEnterTransition() + } + } + + adapter = ProfileAdapter(viewModel, this, headLoadListener, viewModel) + return FragmentProfileBinding.inflate(inflater, container, false).also { + binding = it + }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding.apply { + lifecycleOwner = this@ProfileFragment + viewModel = this@ProfileFragment.viewModel + executePendingBindings() + profileRecycler.apply { + adapter = this@ProfileFragment.adapter + itemAnimator?.run { + addDuration = 120L + moveDuration = 120L + changeDuration = 120L + removeDuration = 100L + } + doOnLayout { + addOnScrollListener( + PushUpScrollListener( + binding.up, + it, + R.id.student_name, + R.id.student_course + ) + ) + } + } + } + + viewModel.statements.observe( + viewLifecycleOwner + ) { statements -> + adapter.statements = statements.sortedByDescending { it.createdAt } + } + + binding.up.setOnClickListener { + requireActivity().finishAfterTransition() + } + + binding.writeStatement.setOnClickListener { + val userId = requireNotNull(arguments).getLong(EXTRA_USER_ID, 0) + parentFragmentManager.inTransaction { + val fragment = WriteStatementFragment().apply { + arguments = bundleOf( + EXTRA_USER_ID to userId + ) + } + replace(R.id.fragment_container, fragment, "write_statement") + addToBackStack("home") + } + } + } + + fun pickImage() { + pickImageContract.launch("image/*") + } + + private fun onContentSelected(uri: Uri?) { + uri ?: return + val bg = ColorUtils.modifyAlpha(ContextCompat.getColor(requireContext(), R.color.colorPrimary), 120) + val ac = ContextCompat.getColor(requireContext(), R.color.colorAccent) + + val options = CropImageContractOptions( + uri, + CropImageOptions( + fixAspectRatio = true, + aspectRatioX = 1, + aspectRatioY = 1, + cropShape = CropImageView.CropShape.OVAL, + backgroundColor = bg, + borderLineColor = ac, + borderCornerColor = ac, + activityMenuIconColor = ac, + borderLineThickness = getPixelsFromDp(requireContext(), 2), + activityTitle = getString(R.string.cut_profile_image), + guidelines = CropImageView.Guidelines.OFF + ) + ) + + cropImage.launch(options) + } + + private fun onCropResults(result: CropImageView.CropResult) { + val imageUri = result.uriContent ?: return + onImagePicked(imageUri) + } + + private fun onImagePicked(uri: Uri) { + setupViewModel.setSelectedImage(uri) + setupViewModel.uploadImageToStorage() + } + + companion object { + fun newInstance(userId: String): ProfileFragment { + return ProfileFragment().apply { + arguments = bundleOf( + EXTRA_USER_ID to userId + ) + } + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/profile/ProfileViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/profile/ProfileViewModel.kt index 70a62ceff..b1db64b38 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/profile/ProfileViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/profile/ProfileViewModel.kt @@ -1,153 +1,152 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.profile - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import com.forcetower.core.lifecycle.Event -import com.forcetower.uefs.core.model.unes.ProfileStatement -import com.forcetower.uefs.core.model.unes.SStudent -import com.forcetower.uefs.core.storage.repository.ProfileRepository -import com.forcetower.uefs.core.storage.resource.Status -import com.forcetower.uefs.feature.shared.extensions.setValueIfNew -import dagger.hilt.android.lifecycle.HiltViewModel -import timber.log.Timber -import javax.inject.Inject - -@HiltViewModel -class ProfileViewModel @Inject constructor( - private val repository: ProfileRepository -) : ViewModel(), ProfileInteractor { - val commonProfile = repository.getCommonProfile() - private var userId: String? = null - private val profileId = MutableLiveData() - - private val _profile = MediatorLiveData() - val profile: LiveData - get() = _profile - - val account = repository.getAccountDatabase() - - private val _statements = MediatorLiveData>() - val statements: LiveData> - get() = _statements - - private val _sendingStatement = MediatorLiveData() - val sendingStatement: LiveData - get() = _sendingStatement - - private val _messages = MutableLiveData>() - val messages: LiveData> - get() = _messages - - private val _statementSentSignal = MutableLiveData>() - val statementSentSignal: LiveData> - get() = _statementSentSignal - - init { - _profile.addSource(profileId) { - refreshProfile(it) - refreshStatements(userId) - } - } - - private fun refreshStatements(userId: String?) { -// val source = repository.loadStatements(profileId, userId) -// _statements.addSource(source) { -// Timber.d("Resource status ${it.status}") -// val data = it.data ?: emptyList() -// _statements.value = data -// } - } - - private fun refreshProfile(profileId: Long?) { - if (profileId != null) { - val source = repository.loadProfile(profileId) - Timber.d("Fetching profile...") - _profile.addSource(source) { - Timber.d("Profile load update ${it.status}") - val data = it.data - if (data != null) { - _profile.value = data - } - } - } else { - Timber.d("No profile information available") - } - } - - fun getMeProfile() = repository.getMeProfile() - - fun setProfileId(newProfileId: Long?) { - Timber.d("Setting new profile id: $newProfileId") - profileId.setValueIfNew(newProfileId) - } - - /** - * This method should be replaced by a reactive thing together with setProfileId - */ - fun setUserId(userId: String) { - Timber.d("Setting new userId: $userId") - this.userId = userId - } - - fun onSendStatement(statement: String, profileId: Long, hidden: Boolean) { - if (_sendingStatement.value == true) { - Timber.d("Already sending data") - return - } - - _sendingStatement.value = true - val source = repository.sendStatement(statement, profileId, hidden) - _sendingStatement.addSource(source) { - _sendingStatement.removeSource(source) - if (it.message != null) { - _messages.value = Event(it.message) - } - -// if (it.status == Status.SUCCESS) { -// _statementSentSignal.value = Event(true) -// val uid = userId -// if (uid != null) { -// repository.loadStatements(uid) -// } -// } - - _sendingStatement.value = false - } - } - - override fun onPictureClick() = Unit - - override fun onAcceptStatement(statement: ProfileStatement) { - repository.acceptStatementAsync(statement) - } - - override fun onRefuseStatement(statement: ProfileStatement) { - repository.refuseStatementAsync(statement) - } - - override fun onDeleteStatement(statement: ProfileStatement) { - repository.deleteStatementAsync(statement) - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.profile + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.forcetower.core.lifecycle.Event +import com.forcetower.uefs.core.model.unes.ProfileStatement +import com.forcetower.uefs.core.model.unes.SStudent +import com.forcetower.uefs.core.storage.repository.ProfileRepository +import com.forcetower.uefs.feature.shared.extensions.setValueIfNew +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import timber.log.Timber + +@HiltViewModel +class ProfileViewModel @Inject constructor( + private val repository: ProfileRepository +) : ViewModel(), ProfileInteractor { + val commonProfile = repository.getCommonProfile() + private var userId: String? = null + private val profileId = MutableLiveData() + + private val _profile = MediatorLiveData() + val profile: LiveData + get() = _profile + + val account = repository.getAccountDatabase() + + private val _statements = MediatorLiveData>() + val statements: LiveData> + get() = _statements + + private val _sendingStatement = MediatorLiveData() + val sendingStatement: LiveData + get() = _sendingStatement + + private val _messages = MutableLiveData>() + val messages: LiveData> + get() = _messages + + private val _statementSentSignal = MutableLiveData>() + val statementSentSignal: LiveData> + get() = _statementSentSignal + + init { + _profile.addSource(profileId) { + refreshProfile(it) + refreshStatements(userId) + } + } + + private fun refreshStatements(userId: String?) { +// val source = repository.loadStatements(profileId, userId) +// _statements.addSource(source) { +// Timber.d("Resource status ${it.status}") +// val data = it.data ?: emptyList() +// _statements.value = data +// } + } + + private fun refreshProfile(profileId: Long?) { + if (profileId != null) { + val source = repository.loadProfile(profileId) + Timber.d("Fetching profile...") + _profile.addSource(source) { + Timber.d("Profile load update ${it.status}") + val data = it.data + if (data != null) { + _profile.value = data + } + } + } else { + Timber.d("No profile information available") + } + } + + fun getMeProfile() = repository.getMeProfile() + + fun setProfileId(newProfileId: Long?) { + Timber.d("Setting new profile id: $newProfileId") + profileId.setValueIfNew(newProfileId) + } + + /** + * This method should be replaced by a reactive thing together with setProfileId + */ + fun setUserId(userId: String) { + Timber.d("Setting new userId: $userId") + this.userId = userId + } + + fun onSendStatement(statement: String, profileId: Long, hidden: Boolean) { + if (_sendingStatement.value == true) { + Timber.d("Already sending data") + return + } + + _sendingStatement.value = true + val source = repository.sendStatement(statement, profileId, hidden) + _sendingStatement.addSource(source) { + _sendingStatement.removeSource(source) + if (it.message != null) { + _messages.value = Event(it.message) + } + +// if (it.status == Status.SUCCESS) { +// _statementSentSignal.value = Event(true) +// val uid = userId +// if (uid != null) { +// repository.loadStatements(uid) +// } +// } + + _sendingStatement.value = false + } + } + + override fun onPictureClick() = Unit + + override fun onAcceptStatement(statement: ProfileStatement) { + repository.acceptStatementAsync(statement) + } + + override fun onRefuseStatement(statement: ProfileStatement) { + repository.refuseStatementAsync(statement) + } + + override fun onDeleteStatement(statement: ProfileStatement) { + repository.deleteStatementAsync(statement) + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/purchases/PurchasesFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/purchases/PurchasesFragment.kt index f3c4b54d2..3fc7a20c9 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/purchases/PurchasesFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/purchases/PurchasesFragment.kt @@ -1,111 +1,112 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.purchases - -import android.content.SharedPreferences -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.activityViewModels -import com.android.billingclient.api.BillingClient -import com.android.billingclient.api.ProductDetails -import com.forcetower.core.lifecycle.EventObserver -import com.forcetower.uefs.R -import com.forcetower.uefs.core.billing.SkuDetailsResult -import com.forcetower.uefs.core.vm.BillingViewModel -import com.forcetower.uefs.databinding.FragmentPurchasesBinding -import com.forcetower.uefs.feature.shared.UFragment -import com.google.android.material.snackbar.Snackbar -import com.google.firebase.analytics.FirebaseAnalytics -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -@AndroidEntryPoint -class PurchasesFragment : UFragment() { - @Inject lateinit var preferences: SharedPreferences - @Inject lateinit var analytics: FirebaseAnalytics - - private val viewModel: BillingViewModel by activityViewModels() - private lateinit var binding: FragmentPurchasesBinding - private lateinit var skuAdapter: SkuDetailsAdapter - - private val details: MutableList = mutableListOf() - - private var currentUsername: String? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - if (savedInstanceState == null) { - analytics.logEvent("purchases_screen", null) - } - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return FragmentPurchasesBinding.inflate(inflater, container, false).also { - binding = it - }.apply { - imageTop = "https://cdn.dribbble.com/users/1903950/screenshots/4225909/02_main_tr__1.gif" - executePendingBindings() - incToolbar.textToolbarTitle.text = getString(R.string.label_purchases) - }.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - skuAdapter = SkuDetailsAdapter(viewModel) - binding.recyclerSku.apply { - adapter = skuAdapter - } - viewModel.subscriptions.observe(viewLifecycleOwner) { - processDetails(it) - } - viewModel.selectSku.observe( - viewLifecycleOwner, - EventObserver { - purchaseFlow(it) - } - ) - viewModel.currentUsername.observe(viewLifecycleOwner) { - currentUsername = it - } - } - - private fun processDetails(result: SkuDetailsResult) { - if (result.responseCode == BillingClient.BillingResponseCode.OK) { - val values = result.list - details.clear() - if (values != null) details.addAll(values) - skuAdapter.submitList(values) - } else { - showSnack("${getString(R.string.donation_service_response_error)} ${result.responseCode}", Snackbar.LENGTH_LONG) - analytics.logEvent("purchases_failed", null) - } - } - - private fun purchaseFlow(details: ProductDetails) { - val username = currentUsername - if (username != null) { - viewModel.launchBillingFlow(requireActivity(), details, username) - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.purchases + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.activityViewModels +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.ProductDetails +import com.forcetower.core.lifecycle.EventObserver +import com.forcetower.uefs.R +import com.forcetower.uefs.core.billing.SkuDetailsResult +import com.forcetower.uefs.core.vm.BillingViewModel +import com.forcetower.uefs.databinding.FragmentPurchasesBinding +import com.forcetower.uefs.feature.shared.UFragment +import com.google.android.material.snackbar.Snackbar +import com.google.firebase.analytics.FirebaseAnalytics +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class PurchasesFragment : UFragment() { + @Inject lateinit var preferences: SharedPreferences + + @Inject lateinit var analytics: FirebaseAnalytics + + private val viewModel: BillingViewModel by activityViewModels() + private lateinit var binding: FragmentPurchasesBinding + private lateinit var skuAdapter: SkuDetailsAdapter + + private val details: MutableList = mutableListOf() + + private var currentUsername: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (savedInstanceState == null) { + analytics.logEvent("purchases_screen", null) + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return FragmentPurchasesBinding.inflate(inflater, container, false).also { + binding = it + }.apply { + imageTop = "https://cdn.dribbble.com/users/1903950/screenshots/4225909/02_main_tr__1.gif" + executePendingBindings() + incToolbar.textToolbarTitle.text = getString(R.string.label_purchases) + }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + skuAdapter = SkuDetailsAdapter(viewModel) + binding.recyclerSku.apply { + adapter = skuAdapter + } + viewModel.subscriptions.observe(viewLifecycleOwner) { + processDetails(it) + } + viewModel.selectSku.observe( + viewLifecycleOwner, + EventObserver { + purchaseFlow(it) + } + ) + viewModel.currentUsername.observe(viewLifecycleOwner) { + currentUsername = it + } + } + + private fun processDetails(result: SkuDetailsResult) { + if (result.responseCode == BillingClient.BillingResponseCode.OK) { + val values = result.list + details.clear() + if (values != null) details.addAll(values) + skuAdapter.submitList(values) + } else { + showSnack("${getString(R.string.donation_service_response_error)} ${result.responseCode}", Snackbar.LENGTH_LONG) + analytics.logEvent("purchases_failed", null) + } + } + + private fun purchaseFlow(details: ProductDetails) { + val username = currentUsername + if (username != null) { + viewModel.launchBillingFlow(requireActivity(), details, username) + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/reminders/CreateReminderDialog.kt b/app/src/main/java/com/forcetower/uefs/feature/reminders/CreateReminderDialog.kt index ff5784fb9..9d9ea2b04 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/reminders/CreateReminderDialog.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/reminders/CreateReminderDialog.kt @@ -1,107 +1,109 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.reminders - -import android.content.res.ColorStateList -import android.os.Bundle -import android.util.TypedValue -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.viewModels -import com.forcetower.core.utils.ViewUtils -import com.forcetower.uefs.R -import com.forcetower.uefs.databinding.DialogCreateReminderBinding -import com.forcetower.uefs.feature.shared.RoundedDialog -import com.forcetower.uefs.feature.shared.inflate -import com.wdullaer.materialdatetimepicker.date.DatePickerDialog -import dagger.hilt.android.AndroidEntryPoint -import java.util.Calendar - -@AndroidEntryPoint -class CreateReminderDialog : RoundedDialog() { - private val viewModel: RemindersViewModel by viewModels() - private lateinit var binding: DialogCreateReminderBinding - - override fun onChildCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - binding = inflater.inflate(R.layout.dialog_create_reminder) - binding.apply { - btnOk.setOnClickListener { createReminder() } - btnDeadline.setOnClickListener { openCalendar() } - btnCancel.setOnClickListener { dismiss() } - } - return binding.root - } - - private fun createReminder() { - val title = binding.textInputTitle.text?.toString() - var description = binding.textInputDescription.text?.toString() - - if (title == null || title.isBlank()) { - binding.textInputTitle.error = getString(R.string.reminder_title_empty) - binding.textInputTitle.requestFocus() - return - } - - if (description != null && description.isBlank()) - description = null - - viewModel.createReminder(title, description) - dismiss() - } - - private fun openCalendar() { - val calendar = Calendar.getInstance() - - if (viewModel.currentDeadline != null) - calendar.timeInMillis = viewModel.currentDeadline!! - - val color = ViewUtils.attributeColorUtils(requireContext(), androidx.appcompat.R.attr.colorPrimary) - - val picker = DatePickerDialog.newInstance( - { _, y, m, d -> - val next = Calendar.getInstance().apply { - set(Calendar.YEAR, y) - set(Calendar.MONTH, m) - set(Calendar.DAY_OF_MONTH, d) - }.timeInMillis - viewModel.currentDeadline = next - binding.btnDeadline.iconTint = ColorStateList.valueOf(color) - }, - calendar - ) - picker.version = DatePickerDialog.Version.VERSION_2 - - picker.accentColor = color - picker.setOkColor(color) - - val theme = requireContext().theme - val darkThemeValue = TypedValue() - theme.resolveAttribute(R.attr.lightStatusBar, darkThemeValue, true) - picker.isThemeDark = darkThemeValue.data == 0 - - val colorOnSurfaceLight = TypedValue() - theme.resolveAttribute(R.attr.colorOnSurfaceLight, colorOnSurfaceLight, true) - picker.setCancelColor(colorOnSurfaceLight.data) - - picker.show(childFragmentManager, "date_picker_dialog") - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.reminders + +import android.content.res.ColorStateList +import android.os.Bundle +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import com.forcetower.core.utils.ViewUtils +import com.forcetower.uefs.R +import com.forcetower.uefs.databinding.DialogCreateReminderBinding +import com.forcetower.uefs.feature.shared.RoundedDialog +import com.forcetower.uefs.feature.shared.inflate +import com.wdullaer.materialdatetimepicker.date.DatePickerDialog +import dagger.hilt.android.AndroidEntryPoint +import java.util.Calendar + +@AndroidEntryPoint +class CreateReminderDialog : RoundedDialog() { + private val viewModel: RemindersViewModel by viewModels() + private lateinit var binding: DialogCreateReminderBinding + + override fun onChildCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + binding = inflater.inflate(R.layout.dialog_create_reminder) + binding.apply { + btnOk.setOnClickListener { createReminder() } + btnDeadline.setOnClickListener { openCalendar() } + btnCancel.setOnClickListener { dismiss() } + } + return binding.root + } + + private fun createReminder() { + val title = binding.textInputTitle.text?.toString() + var description = binding.textInputDescription.text?.toString() + + if (title == null || title.isBlank()) { + binding.textInputTitle.error = getString(R.string.reminder_title_empty) + binding.textInputTitle.requestFocus() + return + } + + if (description != null && description.isBlank()) { + description = null + } + + viewModel.createReminder(title, description) + dismiss() + } + + private fun openCalendar() { + val calendar = Calendar.getInstance() + + if (viewModel.currentDeadline != null) { + calendar.timeInMillis = viewModel.currentDeadline!! + } + + val color = ViewUtils.attributeColorUtils(requireContext(), androidx.appcompat.R.attr.colorPrimary) + + val picker = DatePickerDialog.newInstance( + { _, y, m, d -> + val next = Calendar.getInstance().apply { + set(Calendar.YEAR, y) + set(Calendar.MONTH, m) + set(Calendar.DAY_OF_MONTH, d) + }.timeInMillis + viewModel.currentDeadline = next + binding.btnDeadline.iconTint = ColorStateList.valueOf(color) + }, + calendar + ) + picker.version = DatePickerDialog.Version.VERSION_2 + + picker.accentColor = color + picker.setOkColor(color) + + val theme = requireContext().theme + val darkThemeValue = TypedValue() + theme.resolveAttribute(R.attr.lightStatusBar, darkThemeValue, true) + picker.isThemeDark = darkThemeValue.data == 0 + + val colorOnSurfaceLight = TypedValue() + theme.resolveAttribute(R.attr.colorOnSurfaceLight, colorOnSurfaceLight, true) + picker.setCancelColor(colorOnSurfaceLight.data) + + picker.show(childFragmentManager, "date_picker_dialog") + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/reminders/ReminderBindingAdapter.kt b/app/src/main/java/com/forcetower/uefs/feature/reminders/ReminderBindingAdapter.kt index 8dcfddb80..e3fd2a4e4 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/reminders/ReminderBindingAdapter.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/reminders/ReminderBindingAdapter.kt @@ -1,38 +1,41 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.reminders - -import android.graphics.Paint -import android.widget.TextView -import androidx.databinding.BindingAdapter -import com.forcetower.uefs.feature.shared.extensions.formatSimpleDay - -@BindingAdapter("strikeText") -fun strikeText(tv: TextView, strike: Boolean) { - if (strike) tv.paintFlags = tv.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG - else tv.paintFlags = tv.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() -} - -@BindingAdapter("reminderDate") -fun reminderDate(tv: TextView, date: Long?) { - if (date == null) return - tv.text = date.formatSimpleDay() -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.reminders + +import android.graphics.Paint +import android.widget.TextView +import androidx.databinding.BindingAdapter +import com.forcetower.uefs.feature.shared.extensions.formatSimpleDay + +@BindingAdapter("strikeText") +fun strikeText(tv: TextView, strike: Boolean) { + if (strike) { + tv.paintFlags = tv.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG + } else { + tv.paintFlags = tv.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() + } +} + +@BindingAdapter("reminderDate") +fun reminderDate(tv: TextView, date: Long?) { + if (date == null) return + tv.text = date.formatSimpleDay() +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/reminders/RemindersViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/reminders/RemindersViewModel.kt index d8f31bfd8..049adfe5c 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/reminders/RemindersViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/reminders/RemindersViewModel.kt @@ -24,8 +24,8 @@ import androidx.lifecycle.ViewModel import com.forcetower.uefs.core.model.service.Reminder import com.forcetower.uefs.core.storage.repository.RemindersRepository import dagger.hilt.android.lifecycle.HiltViewModel -import timber.log.Timber import javax.inject.Inject +import timber.log.Timber @HiltViewModel class RemindersViewModel @Inject constructor( diff --git a/app/src/main/java/com/forcetower/uefs/feature/schedule/SchedulePerformanceFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/schedule/SchedulePerformanceFragment.kt index 27755654c..c144a1db0 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/schedule/SchedulePerformanceFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/schedule/SchedulePerformanceFragment.kt @@ -36,13 +36,14 @@ import com.forcetower.uefs.feature.profile.ProfileViewModel import com.forcetower.uefs.feature.shared.UFragment import com.google.firebase.remoteconfig.FirebaseRemoteConfig import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber import javax.inject.Inject import kotlin.math.max +import timber.log.Timber @AndroidEntryPoint class SchedulePerformanceFragment : UFragment() { @Inject lateinit var preferences: SharedPreferences + @Inject lateinit var remoteConfig: FirebaseRemoteConfig private val viewModel by activityViewModels() private val profileViewModel by activityViewModels() diff --git a/app/src/main/java/com/forcetower/uefs/feature/schedule/ScheduleViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/schedule/ScheduleViewModel.kt index e2de26ab1..adc9453e8 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/schedule/ScheduleViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/schedule/ScheduleViewModel.kt @@ -1,106 +1,106 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.schedule - -import android.view.View -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.asLiveData -import androidx.lifecycle.viewModelScope -import com.forcetower.core.lifecycle.Event -import com.forcetower.uefs.core.model.ui.ProcessedClassLocation -import com.forcetower.uefs.core.storage.database.aggregation.ClassGroupWithData -import com.forcetower.uefs.core.storage.database.aggregation.ClassLocationWithData -import com.forcetower.uefs.core.storage.repository.SagresSyncRepository -import com.forcetower.uefs.core.storage.repository.ScheduleRepository -import com.forcetower.uefs.core.storage.repository.SnowpiercerSyncRepository -import com.forcetower.uefs.easter.twofoureight.Game2048Activity -import com.forcetower.uefs.feature.disciplines.disciplinedetail.DisciplineDetailsActivity -import com.forcetower.uefs.feature.shared.extensions.toLongWeekDay -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class ScheduleViewModel @Inject constructor( - repository: ScheduleRepository, - private val sagresSyncRepository: SagresSyncRepository, - private val snowpiercerSyncRepository: SnowpiercerSyncRepository -) : ViewModel(), ScheduleActions { - - val hasSchedule = repository.hasSchedule() - private val innerScheduleMaster = repository.getProcessedSchedule() - - val schedule = innerScheduleMaster.asLiveData(Dispatchers.IO) - val scheduleLine = innerScheduleMaster.map { buildScheduleLine(it) }.asLiveData(Dispatchers.IO) - - private val _onRefresh = MutableLiveData>() - val onRefresh: LiveData> = _onRefresh - - override fun onLongClick(view: View): Boolean { - Game2048Activity.startActivity(view.context) - return false - } - - override fun onClick(view: View, group: ClassGroupWithData) { - val context = view.context - val intent = DisciplineDetailsActivity.startIntent(context, group.classData.clazz.uid, group.group.uid) - context.startActivity(intent) - } - - override fun onLocationClick(view: View, location: ClassLocationWithData) { - val group = location.groupData - if (group.classData.clazz.scheduleOnly) return - val context = view.context - val intent = DisciplineDetailsActivity.startIntent(context, group.classData.clazz.uid, group.group.uid) - context.startActivity(intent) - } - - fun doRefreshData(gToken: String?, snowpiercer: Boolean) { - if (snowpiercer) { - viewModelScope.launch { - snowpiercerSyncRepository.asyncSync() - } - } else { - viewModelScope.launch { - sagresSyncRepository.asyncSync(gToken) - } - } - } - - override fun refreshData() { - _onRefresh.value = Event(Unit) - } - - private fun buildScheduleLine(value: Map>): List { - return value.filter { it.key != -1 } - .mapValues { entry -> - // Call should not be simplified since the list needs to be of supertype ProcessedClassLocation. - @Suppress("SimplifiableCall") - entry.value.filter { it is ProcessedClassLocation.ElementSpace }.toMutableList().apply { - add(0, ProcessedClassLocation.DaySpace(entry.key.toLongWeekDay(), entry.key)) - } - }.entries.sortedBy { it.key }.flatMap { it.value } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.schedule + +import android.view.View +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import com.forcetower.core.lifecycle.Event +import com.forcetower.uefs.core.model.ui.ProcessedClassLocation +import com.forcetower.uefs.core.storage.database.aggregation.ClassGroupWithData +import com.forcetower.uefs.core.storage.database.aggregation.ClassLocationWithData +import com.forcetower.uefs.core.storage.repository.SagresSyncRepository +import com.forcetower.uefs.core.storage.repository.ScheduleRepository +import com.forcetower.uefs.core.storage.repository.SnowpiercerSyncRepository +import com.forcetower.uefs.easter.twofoureight.Game2048Activity +import com.forcetower.uefs.feature.disciplines.disciplinedetail.DisciplineDetailsActivity +import com.forcetower.uefs.feature.shared.extensions.toLongWeekDay +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +@HiltViewModel +class ScheduleViewModel @Inject constructor( + repository: ScheduleRepository, + private val sagresSyncRepository: SagresSyncRepository, + private val snowpiercerSyncRepository: SnowpiercerSyncRepository +) : ViewModel(), ScheduleActions { + + val hasSchedule = repository.hasSchedule() + private val innerScheduleMaster = repository.getProcessedSchedule() + + val schedule = innerScheduleMaster.asLiveData(Dispatchers.IO) + val scheduleLine = innerScheduleMaster.map { buildScheduleLine(it) }.asLiveData(Dispatchers.IO) + + private val _onRefresh = MutableLiveData>() + val onRefresh: LiveData> = _onRefresh + + override fun onLongClick(view: View): Boolean { + Game2048Activity.startActivity(view.context) + return false + } + + override fun onClick(view: View, group: ClassGroupWithData) { + val context = view.context + val intent = DisciplineDetailsActivity.startIntent(context, group.classData.clazz.uid, group.group.uid) + context.startActivity(intent) + } + + override fun onLocationClick(view: View, location: ClassLocationWithData) { + val group = location.groupData + if (group.classData.clazz.scheduleOnly) return + val context = view.context + val intent = DisciplineDetailsActivity.startIntent(context, group.classData.clazz.uid, group.group.uid) + context.startActivity(intent) + } + + fun doRefreshData(gToken: String?, snowpiercer: Boolean) { + if (snowpiercer) { + viewModelScope.launch { + snowpiercerSyncRepository.asyncSync() + } + } else { + viewModelScope.launch { + sagresSyncRepository.asyncSync(gToken) + } + } + } + + override fun refreshData() { + _onRefresh.value = Event(Unit) + } + + private fun buildScheduleLine(value: Map>): List { + return value.filter { it.key != -1 } + .mapValues { entry -> + // Call should not be simplified since the list needs to be of supertype ProcessedClassLocation. + @Suppress("SimplifiableCall") + entry.value.filter { it is ProcessedClassLocation.ElementSpace }.toMutableList().apply { + add(0, ProcessedClassLocation.DaySpace(entry.key.toLongWeekDay(), entry.key)) + } + }.entries.sortedBy { it.key }.flatMap { it.value } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/settings/AdvancedSettingsFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/settings/AdvancedSettingsFragment.kt index 97ad73b37..326059194 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/settings/AdvancedSettingsFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/settings/AdvancedSettingsFragment.kt @@ -57,14 +57,15 @@ import com.google.android.material.snackbar.Snackbar import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.judemanutd.autostarter.AutoStartPermissionHelper import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber import java.io.File import java.util.Locale import javax.inject.Inject +import timber.log.Timber @AndroidEntryPoint class AdvancedSettingsFragment : PreferenceFragmentCompat() { private val viewModel: SettingsViewModel by activityViewModels() + @Inject lateinit var remoteConfig: FirebaseRemoteConfig diff --git a/app/src/main/java/com/forcetower/uefs/feature/settings/SettingsActivity.kt b/app/src/main/java/com/forcetower/uefs/feature/settings/SettingsActivity.kt index c27eaed8c..5b351f5ef 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/settings/SettingsActivity.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/settings/SettingsActivity.kt @@ -1,98 +1,101 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.settings - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.provider.Settings -import androidx.databinding.DataBindingUtil -import androidx.fragment.app.Fragment -import androidx.preference.Preference -import androidx.preference.PreferenceFragmentCompat -import com.forcetower.uefs.R -import com.forcetower.uefs.core.util.VersionUtils -import com.forcetower.uefs.databinding.ActivitySettingsBinding -import com.forcetower.uefs.feature.shared.UActivity -import com.forcetower.uefs.feature.shared.extensions.inTransaction -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class SettingsActivity : UActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { - private lateinit var binding: ActivitySettingsBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - binding = DataBindingUtil.setContentView(this, R.layout.activity_settings) - binding.btnBack.setOnClickListener { - supportFragmentManager.run { - if (backStackEntryCount == 0) finish() - else popBackStack() - } - } - if (savedInstanceState == null) { - supportFragmentManager.inTransaction { - add(R.id.fragment_container, RootSettingsFragment()) - } - } - - when (intent.getIntExtra("move_to_screen", -1)) { - 0 -> navigateTo(SyncSettingsFragment()) - 2 -> navigateTo(AccountSettingsFragment()) - 3 -> navigateTo(AdvancedSettingsFragment()) - } - } - - override fun onPreferenceStartFragment( - caller: PreferenceFragmentCompat, - pref: Preference - ): Boolean { - when (pref.key) { - "settings_synchronization" -> navigateTo(SyncSettingsFragment()) - "settings_notifications" -> { - if (VersionUtils.isOreo()) { - val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) - intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName) - startActivity(intent) - } else { - navigateTo(NotificationSettingsFragment()) - } - } - "settings_account" -> navigateTo(AccountSettingsFragment()) - "settings_advanced" -> navigateTo(AdvancedSettingsFragment()) - } - return true - } - - private fun navigateTo(fragment: Fragment) { - supportFragmentManager.inTransaction { - replace(R.id.fragment_container, fragment) - addToBackStack(null) - } - } - - companion object { - fun startIntent(context: Context): Intent { - return Intent(context, SettingsActivity::class.java) - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.settings + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.provider.Settings +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import com.forcetower.uefs.R +import com.forcetower.uefs.core.util.VersionUtils +import com.forcetower.uefs.databinding.ActivitySettingsBinding +import com.forcetower.uefs.feature.shared.UActivity +import com.forcetower.uefs.feature.shared.extensions.inTransaction +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class SettingsActivity : UActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { + private lateinit var binding: ActivitySettingsBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = DataBindingUtil.setContentView(this, R.layout.activity_settings) + binding.btnBack.setOnClickListener { + supportFragmentManager.run { + if (backStackEntryCount == 0) { + finish() + } else { + popBackStack() + } + } + } + if (savedInstanceState == null) { + supportFragmentManager.inTransaction { + add(R.id.fragment_container, RootSettingsFragment()) + } + } + + when (intent.getIntExtra("move_to_screen", -1)) { + 0 -> navigateTo(SyncSettingsFragment()) + 2 -> navigateTo(AccountSettingsFragment()) + 3 -> navigateTo(AdvancedSettingsFragment()) + } + } + + override fun onPreferenceStartFragment( + caller: PreferenceFragmentCompat, + pref: Preference + ): Boolean { + when (pref.key) { + "settings_synchronization" -> navigateTo(SyncSettingsFragment()) + "settings_notifications" -> { + if (VersionUtils.isOreo()) { + val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName) + startActivity(intent) + } else { + navigateTo(NotificationSettingsFragment()) + } + } + "settings_account" -> navigateTo(AccountSettingsFragment()) + "settings_advanced" -> navigateTo(AdvancedSettingsFragment()) + } + return true + } + + private fun navigateTo(fragment: Fragment) { + supportFragmentManager.inTransaction { + replace(R.id.fragment_container, fragment) + addToBackStack(null) + } + } + + companion object { + fun startIntent(context: Context): Intent { + return Intent(context, SettingsActivity::class.java) + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/settings/SettingsViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/settings/SettingsViewModel.kt index 005d42754..0796e6e02 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/settings/SettingsViewModel.kt @@ -1,57 +1,57 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.settings - -import android.content.Context -import androidx.lifecycle.LiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.forcetower.uefs.core.storage.repository.SettingsRepository -import com.google.android.play.core.splitinstall.SplitInstallManagerFactory -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class SettingsViewModel @Inject constructor( - private val repository: SettingsRepository, - context: Context -) : ViewModel() { - private val splitInstallManager = SplitInstallManagerFactory.create(context) - private var done: Boolean = false - - val isDarkModeEnabled: LiveData - get() = repository.hasDarkModeEnabled() - - fun getAllTheGrades() { - if (done) return - done = true - viewModelScope.launch { - repository.requestAllGradesAndCalculateScore() - } - } - - fun uninstallModuleIfExists(name: String) { - if (splitInstallManager.installedModules.contains(name)) { - splitInstallManager.deferredUninstall(listOf(name)).addOnCompleteListener {} - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.settings + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.forcetower.uefs.core.storage.repository.SettingsRepository +import com.google.android.play.core.splitinstall.SplitInstallManagerFactory +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch + +@HiltViewModel +class SettingsViewModel @Inject constructor( + private val repository: SettingsRepository, + context: Context +) : ViewModel() { + private val splitInstallManager = SplitInstallManagerFactory.create(context) + private var done: Boolean = false + + val isDarkModeEnabled: LiveData + get() = repository.hasDarkModeEnabled() + + fun getAllTheGrades() { + if (done) return + done = true + viewModelScope.launch { + repository.requestAllGradesAndCalculateScore() + } + } + + fun uninstallModuleIfExists(name: String) { + if (splitInstallManager.installedModules.contains(name)) { + splitInstallManager.deferredUninstall(listOf(name)).addOnCompleteListener {} + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/settings/SyncSettingsFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/settings/SyncSettingsFragment.kt index af57c54ce..ea37384a5 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/settings/SyncSettingsFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/settings/SyncSettingsFragment.kt @@ -33,8 +33,8 @@ import com.forcetower.uefs.core.storage.repository.SyncFrequencyRepository import com.forcetower.uefs.core.work.sync.SyncLinkedWorker import com.forcetower.uefs.core.work.sync.SyncMainWorker import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber import javax.inject.Inject +import timber.log.Timber @AndroidEntryPoint class SyncSettingsFragment : PreferenceFragmentCompat() { diff --git a/app/src/main/java/com/forcetower/uefs/feature/setup/IntroductionFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/setup/IntroductionFragment.kt index 3f6febd6f..bdc2ecc4a 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/setup/IntroductionFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/setup/IntroductionFragment.kt @@ -1,153 +1,154 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.setup - -import android.content.SharedPreferences -import android.net.Uri -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import com.bumptech.glide.Glide -import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions -import com.canhub.cropper.CropImageContract -import com.canhub.cropper.CropImageContractOptions -import com.canhub.cropper.CropImageOptions -import com.canhub.cropper.CropImageView -import com.forcetower.core.utils.ColorUtils -import com.forcetower.uefs.R -import com.forcetower.uefs.core.model.unes.Course -import com.forcetower.uefs.core.storage.repository.SyncFrequencyRepository -import com.forcetower.uefs.core.util.isStudentFromUEFS -import com.forcetower.uefs.databinding.FragmentSetupIntroductionBinding -import com.forcetower.uefs.feature.shared.UFragment -import com.forcetower.uefs.feature.shared.getPixelsFromDp -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -@AndroidEntryPoint -class IntroductionFragment : UFragment() { - @Inject lateinit var repository: SyncFrequencyRepository - @Inject lateinit var preferences: SharedPreferences - - private val pickImageContract = registerForActivityResult(ActivityResultContracts.GetContent()) { - onContentSelected(it) - } - - private val cropImage = registerForActivityResult(CropImageContract()) { - onCropResults(it) - } - - private val viewModel: SetupViewModel by activityViewModels() - private lateinit var binding: FragmentSetupIntroductionBinding - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return FragmentSetupIntroductionBinding.inflate(inflater, container, false).also { - binding = it - }.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val uefsStudent = preferences.isStudentFromUEFS() - if (!uefsStudent) binding.textSelectCourse.visibility = View.INVISIBLE - binding.textSelectCourseInternal.setOnClickListener { - val dialog = SelectCourseDialog() - dialog.setCallback( - object : CourseSelectionCallback { - override fun onSelected(course: Course) { - viewModel.setSelectedCourse(course) - binding.textSelectCourseInternal.setText(course.name) - } - } - ) - dialog.show(childFragmentManager, "dialog_course") - } - - binding.imageUserImage.setOnClickListener { - pickImage() - } - - binding.btnNext.setOnClickListener { - val course = viewModel.getSelectedCourse() - if (course == null && uefsStudent) { - binding.textSelectCourseInternal.error = getString(R.string.error_select_a_course) - } else { - binding.textSelectCourseInternal.error = null - findNavController().navigate(R.id.action_introduction_to_configuration) - } - } - - repository.getFrequencies().observe( - viewLifecycleOwner - ) { - viewModel.syncFrequencies = it - } - } - - private fun pickImage() { - pickImageContract.launch("image/*") - } - - private fun onContentSelected(uri: Uri?) { - uri ?: return - val bg = ColorUtils.modifyAlpha(ContextCompat.getColor(requireContext(), R.color.colorPrimary), 120) - val ac = ContextCompat.getColor(requireContext(), R.color.colorAccent) - - val options = CropImageContractOptions( - uri, - CropImageOptions( - fixAspectRatio = true, - aspectRatioX = 1, - aspectRatioY = 1, - cropShape = CropImageView.CropShape.OVAL, - backgroundColor = bg, - borderLineColor = ac, - borderCornerColor = ac, - activityMenuIconColor = ac, - borderLineThickness = getPixelsFromDp(requireContext(), 2), - activityTitle = getString(R.string.cut_profile_image), - guidelines = CropImageView.Guidelines.OFF - ) - ) - - cropImage.launch(options) - } - - private fun onCropResults(result: CropImageView.CropResult) { - val imageUri = result.uriContent ?: return - onImagePicked(imageUri) - } - - private fun onImagePicked(uri: Uri) { - viewModel.setSelectedImage(uri) - Glide.with(requireContext()) - .load(uri) - .fallback(com.forcetower.core.R.mipmap.ic_unes_large_image_512) - .placeholder(com.forcetower.core.R.mipmap.ic_unes_large_image_512) - .circleCrop() - .transition(DrawableTransitionOptions.withCrossFade()) - .into(binding.imageUserImage) - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.setup + +import android.content.SharedPreferences +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions +import com.canhub.cropper.CropImageContract +import com.canhub.cropper.CropImageContractOptions +import com.canhub.cropper.CropImageOptions +import com.canhub.cropper.CropImageView +import com.forcetower.core.utils.ColorUtils +import com.forcetower.uefs.R +import com.forcetower.uefs.core.model.unes.Course +import com.forcetower.uefs.core.storage.repository.SyncFrequencyRepository +import com.forcetower.uefs.core.util.isStudentFromUEFS +import com.forcetower.uefs.databinding.FragmentSetupIntroductionBinding +import com.forcetower.uefs.feature.shared.UFragment +import com.forcetower.uefs.feature.shared.getPixelsFromDp +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class IntroductionFragment : UFragment() { + @Inject lateinit var repository: SyncFrequencyRepository + + @Inject lateinit var preferences: SharedPreferences + + private val pickImageContract = registerForActivityResult(ActivityResultContracts.GetContent()) { + onContentSelected(it) + } + + private val cropImage = registerForActivityResult(CropImageContract()) { + onCropResults(it) + } + + private val viewModel: SetupViewModel by activityViewModels() + private lateinit var binding: FragmentSetupIntroductionBinding + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return FragmentSetupIntroductionBinding.inflate(inflater, container, false).also { + binding = it + }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val uefsStudent = preferences.isStudentFromUEFS() + if (!uefsStudent) binding.textSelectCourse.visibility = View.INVISIBLE + binding.textSelectCourseInternal.setOnClickListener { + val dialog = SelectCourseDialog() + dialog.setCallback( + object : CourseSelectionCallback { + override fun onSelected(course: Course) { + viewModel.setSelectedCourse(course) + binding.textSelectCourseInternal.setText(course.name) + } + } + ) + dialog.show(childFragmentManager, "dialog_course") + } + + binding.imageUserImage.setOnClickListener { + pickImage() + } + + binding.btnNext.setOnClickListener { + val course = viewModel.getSelectedCourse() + if (course == null && uefsStudent) { + binding.textSelectCourseInternal.error = getString(R.string.error_select_a_course) + } else { + binding.textSelectCourseInternal.error = null + findNavController().navigate(R.id.action_introduction_to_configuration) + } + } + + repository.getFrequencies().observe( + viewLifecycleOwner + ) { + viewModel.syncFrequencies = it + } + } + + private fun pickImage() { + pickImageContract.launch("image/*") + } + + private fun onContentSelected(uri: Uri?) { + uri ?: return + val bg = ColorUtils.modifyAlpha(ContextCompat.getColor(requireContext(), R.color.colorPrimary), 120) + val ac = ContextCompat.getColor(requireContext(), R.color.colorAccent) + + val options = CropImageContractOptions( + uri, + CropImageOptions( + fixAspectRatio = true, + aspectRatioX = 1, + aspectRatioY = 1, + cropShape = CropImageView.CropShape.OVAL, + backgroundColor = bg, + borderLineColor = ac, + borderCornerColor = ac, + activityMenuIconColor = ac, + borderLineThickness = getPixelsFromDp(requireContext(), 2), + activityTitle = getString(R.string.cut_profile_image), + guidelines = CropImageView.Guidelines.OFF + ) + ) + + cropImage.launch(options) + } + + private fun onCropResults(result: CropImageView.CropResult) { + val imageUri = result.uriContent ?: return + onImagePicked(imageUri) + } + + private fun onImagePicked(uri: Uri) { + viewModel.setSelectedImage(uri) + Glide.with(requireContext()) + .load(uri) + .fallback(com.forcetower.core.R.mipmap.ic_unes_large_image_512) + .placeholder(com.forcetower.core.R.mipmap.ic_unes_large_image_512) + .circleCrop() + .transition(DrawableTransitionOptions.withCrossFade()) + .into(binding.imageUserImage) + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/setup/SyncSpecialFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/setup/SyncSpecialFragment.kt index bba30a51f..e05a6e5dd 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/setup/SyncSpecialFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/setup/SyncSpecialFragment.kt @@ -44,9 +44,9 @@ import com.forcetower.uefs.feature.web.CustomTabActivityHelper import com.google.firebase.analytics.FirebaseAnalytics import com.judemanutd.autostarter.AutoStartPermissionHelper import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber import java.util.Locale import javax.inject.Inject +import timber.log.Timber @AndroidEntryPoint class SyncSpecialFragment : UFragment() { @@ -62,7 +62,6 @@ class SyncSpecialFragment : UFragment() { @SuppressLint("BatteryLife") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - binding.btnNext.setOnClickListener { findNavController().navigate(R.id.action_special_to_home) requireActivity().finishAfterTransition() diff --git a/app/src/main/java/com/forcetower/uefs/feature/shared/UGameActivity.kt b/app/src/main/java/com/forcetower/uefs/feature/shared/UGameActivity.kt index 0397d0912..e49fb6b30 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/shared/UGameActivity.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/shared/UGameActivity.kt @@ -1,108 +1,108 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.shared - -import android.os.Bundle -import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.StringRes -import com.forcetower.uefs.GooglePlayGamesInstance -import com.forcetower.uefs.R -import com.google.android.gms.games.PlayGamesSdk -import com.google.android.material.snackbar.Snackbar -import kotlinx.coroutines.tasks.await -import timber.log.Timber -import javax.inject.Inject - -/** - * Disponibiliza código especifico para trabalhar com as ferramentas do Google Play Games. - */ -abstract class UGameActivity : UActivity() { - @Inject - lateinit var mGamesInstance: GooglePlayGamesInstance - - private val showAchievements = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - Timber.d("Received result from show achievements") - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - mGamesInstance.signInClient.isAuthenticated().addOnCompleteListener { task -> - val isAuthenticated = task.isSuccessful && task.result.isAuthenticated - if (isAuthenticated) { - onGooglePlayGamesConnected() - } else { - mGamesInstance.onDisconnected() - } - } - } - - override fun onStart() { - super.onStart() - if (mGamesInstance.isPlayGamesEnabled()) signInSilently() - // else checkNotConnectedAchievements() - } - - private fun signInSilently() { - val client = mGamesInstance.signInClient - Timber.d("Signing in!") - client.signIn() - } - - fun signIn() { - PlayGamesSdk.initialize(applicationContext) - val client = mGamesInstance.signInClient - Timber.d("Signing in!") - client.signIn() - } - - private fun onGooglePlayGamesConnected() { - mGamesInstance.onConnected() - checkAchievements() - } - - open fun checkAchievements() = Unit - open fun checkNotConnectedAchievements() = Unit - - suspend fun isConnectedToPlayGames() = mGamesInstance.isConnected() - fun unlockAchievement(@StringRes id: Int) = mGamesInstance.unlockAchievement(id) - fun unlockAchievement(id: String) = mGamesInstance.unlockAchievement(id) - fun revealAchievement(@StringRes id: Int) = mGamesInstance.revealAchievement(id) - fun incrementAchievementProgress(@StringRes id: Int, step: Int) = mGamesInstance.incrementAchievement(id, step) - fun updateAchievementProgress(@StringRes id: Int, value: Int) = mGamesInstance.updateProgress(id, value) - fun updateAchievementProgress(id: String, value: Int) = mGamesInstance.updateProgress(id, value) - fun signOut() = mGamesInstance.disconnect() - - suspend fun openAchievements() { - if (!mGamesInstance.isConnected()) { - showSnack(getString(R.string.not_connected_to_the_adventure), Snackbar.LENGTH_LONG) - return - } - val client = mGamesInstance.achievementsClient - - try { - val intent = client.achievementsIntent.await() - showAchievements.launch(intent) - } catch (error: Exception) { - Timber.e(error, "Device can't open achievements intent") - showSnack("${getString(R.string.unable_to_open_achievements)} ${error.message}") - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.shared + +import android.os.Bundle +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.StringRes +import com.forcetower.uefs.GooglePlayGamesInstance +import com.forcetower.uefs.R +import com.google.android.gms.games.PlayGamesSdk +import com.google.android.material.snackbar.Snackbar +import javax.inject.Inject +import kotlinx.coroutines.tasks.await +import timber.log.Timber + +/** + * Disponibiliza código especifico para trabalhar com as ferramentas do Google Play Games. + */ +abstract class UGameActivity : UActivity() { + @Inject + lateinit var mGamesInstance: GooglePlayGamesInstance + + private val showAchievements = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + Timber.d("Received result from show achievements") + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + mGamesInstance.signInClient.isAuthenticated().addOnCompleteListener { task -> + val isAuthenticated = task.isSuccessful && task.result.isAuthenticated + if (isAuthenticated) { + onGooglePlayGamesConnected() + } else { + mGamesInstance.onDisconnected() + } + } + } + + override fun onStart() { + super.onStart() + if (mGamesInstance.isPlayGamesEnabled()) signInSilently() + // else checkNotConnectedAchievements() + } + + private fun signInSilently() { + val client = mGamesInstance.signInClient + Timber.d("Signing in!") + client.signIn() + } + + fun signIn() { + PlayGamesSdk.initialize(applicationContext) + val client = mGamesInstance.signInClient + Timber.d("Signing in!") + client.signIn() + } + + private fun onGooglePlayGamesConnected() { + mGamesInstance.onConnected() + checkAchievements() + } + + open fun checkAchievements() = Unit + open fun checkNotConnectedAchievements() = Unit + + suspend fun isConnectedToPlayGames() = mGamesInstance.isConnected() + fun unlockAchievement(@StringRes id: Int) = mGamesInstance.unlockAchievement(id) + fun unlockAchievement(id: String) = mGamesInstance.unlockAchievement(id) + fun revealAchievement(@StringRes id: Int) = mGamesInstance.revealAchievement(id) + fun incrementAchievementProgress(@StringRes id: Int, step: Int) = mGamesInstance.incrementAchievement(id, step) + fun updateAchievementProgress(@StringRes id: Int, value: Int) = mGamesInstance.updateProgress(id, value) + fun updateAchievementProgress(id: String, value: Int) = mGamesInstance.updateProgress(id, value) + fun signOut() = mGamesInstance.disconnect() + + suspend fun openAchievements() { + if (!mGamesInstance.isConnected()) { + showSnack(getString(R.string.not_connected_to_the_adventure), Snackbar.LENGTH_LONG) + return + } + val client = mGamesInstance.achievementsClient + + try { + val intent = client.achievementsIntent.await() + showAchievements.launch(intent) + } catch (error: Exception) { + Timber.e(error, "Device can't open achievements intent") + showSnack("${getString(R.string.unable_to_open_achievements)} ${error.message}") + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/shared/extensions/DateUtils.kt b/app/src/main/java/com/forcetower/uefs/feature/shared/extensions/DateUtils.kt index 6a38997a9..b5846c5ed 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/shared/extensions/DateUtils.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/shared/extensions/DateUtils.kt @@ -1,187 +1,188 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.shared.extensions - -import android.widget.TextView -import androidx.databinding.BindingAdapter -import com.forcetower.uefs.R -import timber.log.Timber -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Date -import java.util.Locale -import java.util.concurrent.TimeUnit - -@BindingAdapter(value = ["timestamped"]) -fun getTimeStampedDate(view: TextView, time: Long) { - val context = view.context - val now = System.currentTimeMillis() - val diff = now - time - - val oneDay = TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS) - val oneHor = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS) - val days = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS) - val value = when { - days > 1L -> { - val format = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) - val str = format.format(Date(time)) - context.getString(R.string.message_received_date_format, str) - } - days == 1L -> { - val hours = TimeUnit.HOURS.convert(diff - oneDay, TimeUnit.MILLISECONDS) - val str = days.toString() + "d " + hours.toString() + "h" - context.getString(R.string.message_received_date_ago_format, str) - } - else -> { - val hours = TimeUnit.HOURS.convert(diff, TimeUnit.MILLISECONDS) - val minutes = TimeUnit.MINUTES.convert(diff - (hours * oneHor), TimeUnit.MILLISECONDS) - val str = hours.toString() + "h " + minutes + "min" - context.getString(R.string.message_received_date_ago_format, str) - } - } - view.text = value -} - -fun Int.toLongWeekDay(): String { - return when (this) { - 1 -> "Domingo" - 2 -> "Segunda" - 3 -> "Terça" - 4 -> "Quarta" - 5 -> "Quinta" - 6 -> "Sexta" - 7 -> "Sábado" - else -> "UNDEFINED" - } -} - -fun Int.toWeekDay(): String { - return when (this) { - 1 -> "DOM" - 2 -> "SEG" - 3 -> "TER" - 4 -> "QUA" - 5 -> "QUI" - 6 -> "SEX" - 7 -> "SAB" - else -> "UNDEFINED" - } -} - -fun String.fromWeekDay(): Int { - return when (this.uppercase(Locale.getDefault())) { - "DOM" -> 1 - "SEG" -> 2 - "TER" -> 3 - "QUA" -> 4 - "QUI" -> 5 - "SEX" -> 6 - "SAB" -> 7 - else -> 0 - } -} - -fun String.createTimeInt(): Int { - return try { - val split = this.split(":") - val hour = split[0].toInt() * 60 - val minute = split[1].toInt() - hour + minute - } catch (t: Throwable) { - Timber.e(t, "Failed to parse $this") - 0 - } -} - -fun Long.toCalendar(): Calendar { - val calendar = Calendar.getInstance() - calendar.timeInMillis = this - return calendar -} - -fun Long.formatDateTime(): String { - return if (this.isToday()) { - this.formatTime() - } else { - this.formatDate() - } -} - -fun Long.isToday(): Boolean { - val dateFormat = SimpleDateFormat("yyyyMMdd", Locale.getDefault()) - val date = dateFormat.format(this) - return date == dateFormat.format(System.currentTimeMillis()) -} - -fun Long.formatDate(): String { - val dateFormat = SimpleDateFormat("dd MMMM", Locale.getDefault()) - return dateFormat.format(this) -} - -fun Long.formatTime(): String { - val dateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) - return dateFormat.format(this) -} - -fun Long.formatTimeWithoutSeconds(): String { - val dateFormat = SimpleDateFormat("HH:mm", Locale.getDefault()) - return dateFormat.format(this) -} - -fun Long.formatFullDate(): String { - val date = Date(this) - val format = SimpleDateFormat("dd/MM/yyyy HH:mm:ss", Locale.getDefault()) - return format.format(date) -} - -fun Long.formatSimpleDay(): String { - val date = Date(this) - val format = SimpleDateFormat("EEE, d MMM yyyy", Locale.getDefault()) - return format.format(date) -} - -fun Long.formatMonthYear(): String { - val date = Date(this) - val format = SimpleDateFormat("MMMM yyyy", Locale.getDefault()) - return format.format(date) -} - -fun String?.generateCalendarFromHour(): Calendar? { - if (this == null) return null - - try { - val calendar = Calendar.getInstance() - val parts = trim { it <= ' ' }.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - if (parts.size != 1) { - calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(parts[0])) - calendar.set(Calendar.MINUTE, Integer.parseInt(parts[1])) - - if (parts.size == 3) - calendar.set(Calendar.SECOND, Integer.parseInt(parts[2])) - - return calendar - } - } catch (e: Exception) { - e.printStackTrace() - } - - return null -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.shared.extensions + +import android.widget.TextView +import androidx.databinding.BindingAdapter +import com.forcetower.uefs.R +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale +import java.util.concurrent.TimeUnit +import timber.log.Timber + +@BindingAdapter(value = ["timestamped"]) +fun getTimeStampedDate(view: TextView, time: Long) { + val context = view.context + val now = System.currentTimeMillis() + val diff = now - time + + val oneDay = TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS) + val oneHor = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS) + val days = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS) + val value = when { + days > 1L -> { + val format = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) + val str = format.format(Date(time)) + context.getString(R.string.message_received_date_format, str) + } + days == 1L -> { + val hours = TimeUnit.HOURS.convert(diff - oneDay, TimeUnit.MILLISECONDS) + val str = days.toString() + "d " + hours.toString() + "h" + context.getString(R.string.message_received_date_ago_format, str) + } + else -> { + val hours = TimeUnit.HOURS.convert(diff, TimeUnit.MILLISECONDS) + val minutes = TimeUnit.MINUTES.convert(diff - (hours * oneHor), TimeUnit.MILLISECONDS) + val str = hours.toString() + "h " + minutes + "min" + context.getString(R.string.message_received_date_ago_format, str) + } + } + view.text = value +} + +fun Int.toLongWeekDay(): String { + return when (this) { + 1 -> "Domingo" + 2 -> "Segunda" + 3 -> "Terça" + 4 -> "Quarta" + 5 -> "Quinta" + 6 -> "Sexta" + 7 -> "Sábado" + else -> "UNDEFINED" + } +} + +fun Int.toWeekDay(): String { + return when (this) { + 1 -> "DOM" + 2 -> "SEG" + 3 -> "TER" + 4 -> "QUA" + 5 -> "QUI" + 6 -> "SEX" + 7 -> "SAB" + else -> "UNDEFINED" + } +} + +fun String.fromWeekDay(): Int { + return when (this.uppercase(Locale.getDefault())) { + "DOM" -> 1 + "SEG" -> 2 + "TER" -> 3 + "QUA" -> 4 + "QUI" -> 5 + "SEX" -> 6 + "SAB" -> 7 + else -> 0 + } +} + +fun String.createTimeInt(): Int { + return try { + val split = this.split(":") + val hour = split[0].toInt() * 60 + val minute = split[1].toInt() + hour + minute + } catch (t: Throwable) { + Timber.e(t, "Failed to parse $this") + 0 + } +} + +fun Long.toCalendar(): Calendar { + val calendar = Calendar.getInstance() + calendar.timeInMillis = this + return calendar +} + +fun Long.formatDateTime(): String { + return if (this.isToday()) { + this.formatTime() + } else { + this.formatDate() + } +} + +fun Long.isToday(): Boolean { + val dateFormat = SimpleDateFormat("yyyyMMdd", Locale.getDefault()) + val date = dateFormat.format(this) + return date == dateFormat.format(System.currentTimeMillis()) +} + +fun Long.formatDate(): String { + val dateFormat = SimpleDateFormat("dd MMMM", Locale.getDefault()) + return dateFormat.format(this) +} + +fun Long.formatTime(): String { + val dateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) + return dateFormat.format(this) +} + +fun Long.formatTimeWithoutSeconds(): String { + val dateFormat = SimpleDateFormat("HH:mm", Locale.getDefault()) + return dateFormat.format(this) +} + +fun Long.formatFullDate(): String { + val date = Date(this) + val format = SimpleDateFormat("dd/MM/yyyy HH:mm:ss", Locale.getDefault()) + return format.format(date) +} + +fun Long.formatSimpleDay(): String { + val date = Date(this) + val format = SimpleDateFormat("EEE, d MMM yyyy", Locale.getDefault()) + return format.format(date) +} + +fun Long.formatMonthYear(): String { + val date = Date(this) + val format = SimpleDateFormat("MMMM yyyy", Locale.getDefault()) + return format.format(date) +} + +fun String?.generateCalendarFromHour(): Calendar? { + if (this == null) return null + + try { + val calendar = Calendar.getInstance() + val parts = trim { it <= ' ' }.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + if (parts.size != 1) { + calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(parts[0])) + calendar.set(Calendar.MINUTE, Integer.parseInt(parts[1])) + + if (parts.size == 3) { + calendar.set(Calendar.SECOND, Integer.parseInt(parts[2])) + } + + return calendar + } + } catch (e: Exception) { + e.printStackTrace() + } + + return null +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/shared/extensions/StringExtensions.kt b/app/src/main/java/com/forcetower/uefs/feature/shared/extensions/StringExtensions.kt index eab9c3711..89ba65b37 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/shared/extensions/StringExtensions.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/shared/extensions/StringExtensions.kt @@ -1,48 +1,51 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.shared.extensions - -import android.util.Base64 -import com.forcetower.core.utils.WordUtils - -fun String.makeSemester(): String { - return if (this.length > 4) { - if (this[4] == '.') this - else this.substring(0, 4) + "." + this.substring(4) - } else { - this - } -} - -fun String?.toTitleCase(): String? = WordUtils.toTitleCase(this) - -fun String.toBase64(): String { - return Base64.encodeToString(this.toByteArray(), Base64.DEFAULT) -} - -fun String?.toBooleanOrNull(): Boolean? { - if (this == null) return null - return try { - java.lang.Boolean.parseBoolean(this) - } catch (t: Throwable) { - null - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.shared.extensions + +import android.util.Base64 +import com.forcetower.core.utils.WordUtils + +fun String.makeSemester(): String { + return if (this.length > 4) { + if (this[4] == '.') { + this + } else { + this.substring(0, 4) + "." + this.substring(4) + } + } else { + this + } +} + +fun String?.toTitleCase(): String? = WordUtils.toTitleCase(this) + +fun String.toBase64(): String { + return Base64.encodeToString(this.toByteArray(), Base64.DEFAULT) +} + +fun String?.toBooleanOrNull(): Boolean? { + if (this == null) return null + return try { + java.lang.Boolean.parseBoolean(this) + } catch (t: Throwable) { + null + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/siecomp/schedule/ScheduleItemHeaderDecoration.kt b/app/src/main/java/com/forcetower/uefs/feature/siecomp/schedule/ScheduleItemHeaderDecoration.kt index 7aa00c283..fba9ed4ab 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/siecomp/schedule/ScheduleItemHeaderDecoration.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/siecomp/schedule/ScheduleItemHeaderDecoration.kt @@ -1,183 +1,184 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.siecomp.schedule - -import android.content.Context -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.Typeface -import android.text.Layout -import android.text.SpannableStringBuilder -import android.text.StaticLayout -import android.text.TextPaint -import android.text.style.AbsoluteSizeSpan -import android.text.style.StyleSpan -import androidx.core.content.res.ResourcesCompat -import androidx.core.content.res.getColorOrThrow -import androidx.core.content.res.getDimensionOrThrow -import androidx.core.content.res.getDimensionPixelSizeOrThrow -import androidx.core.content.res.getResourceIdOrThrow -import androidx.core.graphics.withTranslation -import androidx.core.text.inSpans -import androidx.core.view.get -import androidx.core.view.isEmpty -import androidx.recyclerview.widget.RecyclerView -import com.forcetower.uefs.R -import com.forcetower.uefs.core.storage.eventdatabase.accessors.SessionWithData -import com.forcetower.uefs.core.util.siecomp.TimeUtils -import com.google.android.gms.common.util.PlatformVersion -import timber.log.Timber -import java.time.ZoneId -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter -import java.util.Locale - -class ScheduleItemHeaderDecoration( - context: Context, - sessions: List, - zoneId: ZoneId -) : RecyclerView.ItemDecoration() { - private val paint: TextPaint - private val width: Int - private val paddingTop: Int - private val hourMinTextSize: Int - private val meridiemTextSize: Int - private val hourFormatter = DateTimeFormatter.ofPattern("H").withLocale(Locale.getDefault()) - private val hourMinFormatter = DateTimeFormatter.ofPattern("H:m").withLocale(Locale.getDefault()) - private val meridiemFormatter = DateTimeFormatter.ofPattern("a").withLocale(Locale.getDefault()) - private val hoursString = context.getString(R.string.label_hours) - - init { - val attrs = context.obtainStyledAttributes( - R.style.Widget_Schedule_TimeHeaders, - R.styleable.TimeHeader - ) - - paint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply { - color = attrs.getColorOrThrow(R.styleable.TimeHeader_android_textColor) - textSize = attrs.getDimensionOrThrow(R.styleable.TimeHeader_hourTextSize) - try { - typeface = ResourcesCompat.getFont( - context, - attrs.getResourceIdOrThrow(R.styleable.TimeHeader_android_fontFamily) - ) - } catch (_: Exception) { - // ignore - } - } - - width = attrs.getDimensionPixelSizeOrThrow(R.styleable.TimeHeader_android_width) - paddingTop = attrs.getDimensionPixelSizeOrThrow(R.styleable.TimeHeader_android_paddingTop) - hourMinTextSize = attrs.getDimensionPixelSizeOrThrow(R.styleable.TimeHeader_hourMinTextSize) - meridiemTextSize = attrs.getDimensionPixelSizeOrThrow(R.styleable.TimeHeader_meridiemTextSize) - attrs.recycle() - } - - private val timeSlots: Map = - indexSessionHeaders(sessions, zoneId).map { - it.first to createHeader(it.second) - }.toMap() - - override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { - if (timeSlots.isEmpty() || parent.isEmpty()) return - - var earliestFoundHeaderPos = -1 - var prevHeaderTop = Int.MAX_VALUE - - for (i in parent.childCount - 1 downTo 0) { - val view = parent.getChildAt(i) - if (view == null) { - Timber.w( - """View is null. Index: $i, childCount: ${parent.childCount}, - |RecyclerView.State: $state""".trimMargin() - ) - continue - } - val viewTop = view.top + view.translationY.toInt() - if (view.bottom > 0 && viewTop < parent.height) { - val position = parent.getChildAdapterPosition(view) - timeSlots[position]?.let { layout -> - paint.alpha = (view.alpha * 255).toInt() - val top = (viewTop + paddingTop) - .coerceAtLeast(paddingTop) - .coerceAtMost(prevHeaderTop - layout.height) - c.withTranslation(y = top.toFloat()) { - layout.draw(c) - } - earliestFoundHeaderPos = position - prevHeaderTop = viewTop - } - } - } - - if (earliestFoundHeaderPos < 0) { - earliestFoundHeaderPos = parent.getChildAdapterPosition(parent[0]) + 1 - } - - for (headerPos in timeSlots.keys.reversed()) { - if (headerPos < earliestFoundHeaderPos) { - timeSlots[headerPos]?.let { - val top = (prevHeaderTop - it.height).coerceAtMost(paddingTop) - c.withTranslation(y = top.toFloat()) { - it.draw(c) - } - } - break - } - } - } - - private fun createHeader(startTime: ZonedDateTime): StaticLayout { - val text = if (startTime.minute == 0) { - SpannableStringBuilder(hourFormatter.format(startTime)) - } else { - SpannableStringBuilder().apply { - inSpans(AbsoluteSizeSpan(hourMinTextSize)) { - append(hourMinFormatter.format(startTime)) - } - } - }.apply { - append(System.lineSeparator()) - inSpans(AbsoluteSizeSpan(meridiemTextSize), StyleSpan(Typeface.BOLD)) { - append(hoursString) - } - } - return if (PlatformVersion.isAtLeastM()) { - StaticLayout.Builder.obtain(text, 0, text.length, paint, width) - .setText(text) - .setAlignment(Layout.Alignment.ALIGN_CENTER) - .setLineSpacing(0f, 1f) - .setIncludePad(false) - .build() - } else { - @Suppress("DEPRECATION") - StaticLayout(text, paint, width, Layout.Alignment.ALIGN_CENTER, 1f, 0f, false) - } - } -} - -fun indexSessionHeaders(sessions: List, zoneId: ZoneId): List> { - return sessions - .mapIndexed { index, session -> - index to TimeUtils.zonedTime(session.session.startTime, zoneId) - } - .distinctBy { it.second.hour to it.second.minute } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.siecomp.schedule + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Typeface +import android.text.Layout +import android.text.SpannableStringBuilder +import android.text.StaticLayout +import android.text.TextPaint +import android.text.style.AbsoluteSizeSpan +import android.text.style.StyleSpan +import androidx.core.content.res.ResourcesCompat +import androidx.core.content.res.getColorOrThrow +import androidx.core.content.res.getDimensionOrThrow +import androidx.core.content.res.getDimensionPixelSizeOrThrow +import androidx.core.content.res.getResourceIdOrThrow +import androidx.core.graphics.withTranslation +import androidx.core.text.inSpans +import androidx.core.view.get +import androidx.core.view.isEmpty +import androidx.recyclerview.widget.RecyclerView +import com.forcetower.uefs.R +import com.forcetower.uefs.core.storage.eventdatabase.accessors.SessionWithData +import com.forcetower.uefs.core.util.siecomp.TimeUtils +import com.google.android.gms.common.util.PlatformVersion +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.util.Locale +import timber.log.Timber + +class ScheduleItemHeaderDecoration( + context: Context, + sessions: List, + zoneId: ZoneId +) : RecyclerView.ItemDecoration() { + private val paint: TextPaint + private val width: Int + private val paddingTop: Int + private val hourMinTextSize: Int + private val meridiemTextSize: Int + private val hourFormatter = DateTimeFormatter.ofPattern("H").withLocale(Locale.getDefault()) + private val hourMinFormatter = DateTimeFormatter.ofPattern("H:m").withLocale(Locale.getDefault()) + private val meridiemFormatter = DateTimeFormatter.ofPattern("a").withLocale(Locale.getDefault()) + private val hoursString = context.getString(R.string.label_hours) + + init { + val attrs = context.obtainStyledAttributes( + R.style.Widget_Schedule_TimeHeaders, + R.styleable.TimeHeader + ) + + paint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply { + color = attrs.getColorOrThrow(R.styleable.TimeHeader_android_textColor) + textSize = attrs.getDimensionOrThrow(R.styleable.TimeHeader_hourTextSize) + try { + typeface = ResourcesCompat.getFont( + context, + attrs.getResourceIdOrThrow(R.styleable.TimeHeader_android_fontFamily) + ) + } catch (_: Exception) { + // ignore + } + } + + width = attrs.getDimensionPixelSizeOrThrow(R.styleable.TimeHeader_android_width) + paddingTop = attrs.getDimensionPixelSizeOrThrow(R.styleable.TimeHeader_android_paddingTop) + hourMinTextSize = attrs.getDimensionPixelSizeOrThrow(R.styleable.TimeHeader_hourMinTextSize) + meridiemTextSize = attrs.getDimensionPixelSizeOrThrow(R.styleable.TimeHeader_meridiemTextSize) + attrs.recycle() + } + + private val timeSlots: Map = + indexSessionHeaders(sessions, zoneId).map { + it.first to createHeader(it.second) + }.toMap() + + override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + if (timeSlots.isEmpty() || parent.isEmpty()) return + + var earliestFoundHeaderPos = -1 + var prevHeaderTop = Int.MAX_VALUE + + for (i in parent.childCount - 1 downTo 0) { + val view = parent.getChildAt(i) + if (view == null) { + Timber.w( + """View is null. Index: $i, childCount: ${parent.childCount}, + |RecyclerView.State: $state + """.trimMargin() + ) + continue + } + val viewTop = view.top + view.translationY.toInt() + if (view.bottom > 0 && viewTop < parent.height) { + val position = parent.getChildAdapterPosition(view) + timeSlots[position]?.let { layout -> + paint.alpha = (view.alpha * 255).toInt() + val top = (viewTop + paddingTop) + .coerceAtLeast(paddingTop) + .coerceAtMost(prevHeaderTop - layout.height) + c.withTranslation(y = top.toFloat()) { + layout.draw(c) + } + earliestFoundHeaderPos = position + prevHeaderTop = viewTop + } + } + } + + if (earliestFoundHeaderPos < 0) { + earliestFoundHeaderPos = parent.getChildAdapterPosition(parent[0]) + 1 + } + + for (headerPos in timeSlots.keys.reversed()) { + if (headerPos < earliestFoundHeaderPos) { + timeSlots[headerPos]?.let { + val top = (prevHeaderTop - it.height).coerceAtMost(paddingTop) + c.withTranslation(y = top.toFloat()) { + it.draw(c) + } + } + break + } + } + } + + private fun createHeader(startTime: ZonedDateTime): StaticLayout { + val text = if (startTime.minute == 0) { + SpannableStringBuilder(hourFormatter.format(startTime)) + } else { + SpannableStringBuilder().apply { + inSpans(AbsoluteSizeSpan(hourMinTextSize)) { + append(hourMinFormatter.format(startTime)) + } + } + }.apply { + append(System.lineSeparator()) + inSpans(AbsoluteSizeSpan(meridiemTextSize), StyleSpan(Typeface.BOLD)) { + append(hoursString) + } + } + return if (PlatformVersion.isAtLeastM()) { + StaticLayout.Builder.obtain(text, 0, text.length, paint, width) + .setText(text) + .setAlignment(Layout.Alignment.ALIGN_CENTER) + .setLineSpacing(0f, 1f) + .setIncludePad(false) + .build() + } else { + @Suppress("DEPRECATION") + StaticLayout(text, paint, width, Layout.Alignment.ALIGN_CENTER, 1f, 0f, false) + } + } +} + +fun indexSessionHeaders(sessions: List, zoneId: ZoneId): List> { + return sessions + .mapIndexed { index, session -> + index to TimeUtils.zonedTime(session.session.startTime, zoneId) + } + .distinctBy { it.second.hour to it.second.minute } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/siecomp/session/SIECOMPSessionViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/siecomp/session/SIECOMPSessionViewModel.kt index b17691a5c..70ac5a6bc 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/siecomp/session/SIECOMPSessionViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/siecomp/session/SIECOMPSessionViewModel.kt @@ -1,112 +1,112 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.siecomp.session - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.map -import com.forcetower.core.lifecycle.Event -import com.forcetower.uefs.core.model.siecomp.Session -import com.forcetower.uefs.core.model.siecomp.Speaker -import com.forcetower.uefs.core.model.siecomp.Tag -import com.forcetower.uefs.core.storage.repository.SIECOMPRepository -import com.forcetower.uefs.feature.shared.SetIntervalLiveData -import com.forcetower.uefs.feature.shared.extensions.setValueIfNew -import com.forcetower.uefs.feature.siecomp.common.SpeakerActions -import dagger.hilt.android.lifecycle.HiltViewModel -import timber.log.Timber -import java.time.Duration -import java.time.Instant -import java.time.ZoneId -import javax.inject.Inject - -private const val TEN_SECONDS = 10_000L - -@HiltViewModel -class SIECOMPSessionViewModel @Inject constructor( - private val repository: SIECOMPRepository -) : ViewModel(), SpeakerActions { - private val sessionId = MutableLiveData() - - private val _session = MediatorLiveData() - val session: LiveData - get() = _session - - private val _tags = MutableLiveData>() - val tags: LiveData> - get() = _tags - - private val _speakers = MutableLiveData>() - val speakers: LiveData> - get() = _speakers - - private val _navigateToSpeakerAction = MutableLiveData>() - val navigateToSpeakerAction: LiveData> - get() = _navigateToSpeakerAction - - val hasPhoto: LiveData - val timeUntilStart: LiveData - - val timeZoneId: ZoneId = ZoneId.systemDefault() - - init { - _session.addSource(sessionId) { - Timber.d("Session set to ID: $it") - refreshSession(it) - } - - hasPhoto = session.map { - !it?.photoUrl.isNullOrBlank() && it?.photoUrl != "null" - } - - timeUntilStart = SetIntervalLiveData.DefaultIntervalMapper.mapAtInterval(session, TEN_SECONDS) { session -> - session?.startTime?.let { startTime -> - val duration = Duration.between(Instant.now(), startTime) - when (duration.toMinutes()) { - in 1..30 -> duration - else -> null - } - } - } - } - - private fun refreshSession(value: Long?) { - if (value != null) { - // TODO Should attempt to fetch from network - // [In this case it will work since all data is already on database] - _session.addSource(repository.getSessionDetails(value)) { - _session.value = it.session - _tags.value = it.tags() - _speakers.value = it.speakers() - } - } - } - - fun setSessionId(id: Long?) { - sessionId.setValueIfNew(id) - } - - override fun onSpeakerClicked(id: Long) { - _navigateToSpeakerAction.value = Event(id) - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.siecomp.session + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.map +import com.forcetower.core.lifecycle.Event +import com.forcetower.uefs.core.model.siecomp.Session +import com.forcetower.uefs.core.model.siecomp.Speaker +import com.forcetower.uefs.core.model.siecomp.Tag +import com.forcetower.uefs.core.storage.repository.SIECOMPRepository +import com.forcetower.uefs.feature.shared.SetIntervalLiveData +import com.forcetower.uefs.feature.shared.extensions.setValueIfNew +import com.forcetower.uefs.feature.siecomp.common.SpeakerActions +import dagger.hilt.android.lifecycle.HiltViewModel +import java.time.Duration +import java.time.Instant +import java.time.ZoneId +import javax.inject.Inject +import timber.log.Timber + +private const val TEN_SECONDS = 10_000L + +@HiltViewModel +class SIECOMPSessionViewModel @Inject constructor( + private val repository: SIECOMPRepository +) : ViewModel(), SpeakerActions { + private val sessionId = MutableLiveData() + + private val _session = MediatorLiveData() + val session: LiveData + get() = _session + + private val _tags = MutableLiveData>() + val tags: LiveData> + get() = _tags + + private val _speakers = MutableLiveData>() + val speakers: LiveData> + get() = _speakers + + private val _navigateToSpeakerAction = MutableLiveData>() + val navigateToSpeakerAction: LiveData> + get() = _navigateToSpeakerAction + + val hasPhoto: LiveData + val timeUntilStart: LiveData + + val timeZoneId: ZoneId = ZoneId.systemDefault() + + init { + _session.addSource(sessionId) { + Timber.d("Session set to ID: $it") + refreshSession(it) + } + + hasPhoto = session.map { + !it?.photoUrl.isNullOrBlank() && it?.photoUrl != "null" + } + + timeUntilStart = SetIntervalLiveData.DefaultIntervalMapper.mapAtInterval(session, TEN_SECONDS) { session -> + session?.startTime?.let { startTime -> + val duration = Duration.between(Instant.now(), startTime) + when (duration.toMinutes()) { + in 1..30 -> duration + else -> null + } + } + } + } + + private fun refreshSession(value: Long?) { + if (value != null) { + // TODO Should attempt to fetch from network + // [In this case it will work since all data is already on database] + _session.addSource(repository.getSessionDetails(value)) { + _session.value = it.session + _tags.value = it.tags() + _speakers.value = it.speakers() + } + } + } + + fun setSessionId(id: Long?) { + sessionId.setValueIfNew(id) + } + + override fun onSpeakerClicked(id: Long) { + _navigateToSpeakerAction.value = Event(id) + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/siecomp/speaker/SIECOMPSpeakerViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/siecomp/speaker/SIECOMPSpeakerViewModel.kt index 20a7eb67b..0321c06dc 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/siecomp/speaker/SIECOMPSpeakerViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/siecomp/speaker/SIECOMPSpeakerViewModel.kt @@ -1,77 +1,77 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.siecomp.speaker - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.map -import com.forcetower.uefs.core.model.siecomp.Speaker -import com.forcetower.uefs.core.storage.repository.SIECOMPRepository -import com.forcetower.uefs.feature.shared.extensions.setValueIfNew -import dagger.hilt.android.lifecycle.HiltViewModel -import timber.log.Timber -import javax.inject.Inject - -@HiltViewModel -class SIECOMPSpeakerViewModel @Inject constructor( - private val repository: SIECOMPRepository -) : ViewModel() { - var uriString: String? = null - private val speakerId = MutableLiveData() - val access = repository.getAccess() - - private val _speaker = MediatorLiveData() - val speaker: LiveData - get() = _speaker - - val hasProfileImage: LiveData = _speaker.map { - !it?.image.isNullOrBlank() && it?.image != "null" - } - - init { - _speaker.addSource(speakerId) { - Timber.d("Speaked Id set to $it") - refreshSpeaker(it) - } - } - - private fun refreshSpeaker(id: Long?) { - if (id != null) { - val source = repository.getSpeaker(id) - _speaker.addSource(source) { value -> - _speaker.value = value - Timber.d("Speaker $value") - } - } else { - _speaker.value = null - } - } - - fun setSpeakerId(id: Long?) { - speakerId.setValueIfNew(id) - } - - fun sendSpeaker(speaker: Speaker, create: Boolean) { - repository.sendSpeaker(speaker, create) - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.siecomp.speaker + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.map +import com.forcetower.uefs.core.model.siecomp.Speaker +import com.forcetower.uefs.core.storage.repository.SIECOMPRepository +import com.forcetower.uefs.feature.shared.extensions.setValueIfNew +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import timber.log.Timber + +@HiltViewModel +class SIECOMPSpeakerViewModel @Inject constructor( + private val repository: SIECOMPRepository +) : ViewModel() { + var uriString: String? = null + private val speakerId = MutableLiveData() + val access = repository.getAccess() + + private val _speaker = MediatorLiveData() + val speaker: LiveData + get() = _speaker + + val hasProfileImage: LiveData = _speaker.map { + !it?.image.isNullOrBlank() && it?.image != "null" + } + + init { + _speaker.addSource(speakerId) { + Timber.d("Speaked Id set to $it") + refreshSpeaker(it) + } + } + + private fun refreshSpeaker(id: Long?) { + if (id != null) { + val source = repository.getSpeaker(id) + _speaker.addSource(source) { value -> + _speaker.value = value + Timber.d("Speaker $value") + } + } else { + _speaker.value = null + } + } + + fun setSpeakerId(id: Long?) { + speakerId.setValueIfNew(id) + } + + fun sendSpeaker(speaker: Speaker, create: Boolean) { + repository.sendSpeaker(speaker, create) + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/siecomp/speaker/SpeakerFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/siecomp/speaker/SpeakerFragment.kt index 6d50b46e4..f5c24212f 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/siecomp/speaker/SpeakerFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/siecomp/speaker/SpeakerFragment.kt @@ -1,124 +1,128 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.siecomp.speaker - -import android.graphics.drawable.Drawable -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.View.GONE -import android.view.View.VISIBLE -import android.view.ViewGroup -import androidx.core.app.NavUtils -import androidx.core.os.bundleOf -import androidx.core.view.doOnLayout -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Observer -import com.forcetower.core.adapters.ImageLoadListener -import com.forcetower.uefs.R -import com.forcetower.uefs.databinding.FragmentEventSpeakerBinding -import com.forcetower.uefs.feature.shared.UFragment -import com.forcetower.uefs.feature.shared.extensions.inTransaction -import com.forcetower.uefs.feature.shared.extensions.postponeEnterTransition -import com.forcetower.uefs.feature.siecomp.editor.CreateSpeakerFragment -import com.forcetower.uefs.feature.siecomp.session.PushUpScrollListener -import com.forcetower.uefs.feature.siecomp.speaker.EventSpeakerActivity.Companion.SPEAKER_ID -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class SpeakerFragment : UFragment() { - private val speakerViewModel: SIECOMPSpeakerViewModel by activityViewModels() - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - speakerViewModel.setSpeakerId(requireNotNull(arguments).getLong(SPEAKER_ID)) - activity?.postponeEnterTransition(500L) - - val binding = FragmentEventSpeakerBinding.inflate(inflater, container, false).apply { - lifecycleOwner = this@SpeakerFragment - } - - speakerViewModel.hasProfileImage.observe( - viewLifecycleOwner, - Observer { - if (!it) { - activity?.startPostponedEnterTransition() - } - } - ) - - val headLoadListener = object : ImageLoadListener { - override fun onImageLoaded(drawable: Drawable) { activity?.startPostponedEnterTransition() } - override fun onImageLoadFailed() { activity?.startPostponedEnterTransition() } - } - - val speakerAdapter = SpeakerAdapter(this, speakerViewModel, headLoadListener) - binding.speakerDetailRecyclerView.run { - adapter = speakerAdapter - itemAnimator?.run { - addDuration = 120L - moveDuration = 120L - changeDuration = 120L - removeDuration = 100L - } - doOnLayout { - addOnScrollListener( - PushUpScrollListener(binding.up, it, R.id.speaker_name, R.id.speaker_grid_image) - ) - } - } - binding.up.setOnClickListener { - NavUtils.navigateUpFromSameTask(requireActivity()) - } - - speakerViewModel.access.observe( - viewLifecycleOwner, - Observer { - binding.editFloat.visibility = if (it != null) { - VISIBLE - } else { - GONE - } - } - ) - - binding.editFloat.setOnClickListener { - parentFragmentManager.inTransaction { - replace( - R.id.speaker_container, - CreateSpeakerFragment().apply { - arguments = this@SpeakerFragment.arguments - } - ) - addToBackStack(null) - } - } - - return binding.root - } - - companion object { - fun newInstance(speakerId: Long): SpeakerFragment { - return SpeakerFragment().apply { - arguments = bundleOf(SPEAKER_ID to speakerId) - } - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.siecomp.speaker + +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import androidx.core.app.NavUtils +import androidx.core.os.bundleOf +import androidx.core.view.doOnLayout +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import com.forcetower.core.adapters.ImageLoadListener +import com.forcetower.uefs.R +import com.forcetower.uefs.databinding.FragmentEventSpeakerBinding +import com.forcetower.uefs.feature.shared.UFragment +import com.forcetower.uefs.feature.shared.extensions.inTransaction +import com.forcetower.uefs.feature.shared.extensions.postponeEnterTransition +import com.forcetower.uefs.feature.siecomp.editor.CreateSpeakerFragment +import com.forcetower.uefs.feature.siecomp.session.PushUpScrollListener +import com.forcetower.uefs.feature.siecomp.speaker.EventSpeakerActivity.Companion.SPEAKER_ID +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class SpeakerFragment : UFragment() { + private val speakerViewModel: SIECOMPSpeakerViewModel by activityViewModels() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + speakerViewModel.setSpeakerId(requireNotNull(arguments).getLong(SPEAKER_ID)) + activity?.postponeEnterTransition(500L) + + val binding = FragmentEventSpeakerBinding.inflate(inflater, container, false).apply { + lifecycleOwner = this@SpeakerFragment + } + + speakerViewModel.hasProfileImage.observe( + viewLifecycleOwner, + Observer { + if (!it) { + activity?.startPostponedEnterTransition() + } + } + ) + + val headLoadListener = object : ImageLoadListener { + override fun onImageLoaded(drawable: Drawable) { + activity?.startPostponedEnterTransition() + } + override fun onImageLoadFailed() { + activity?.startPostponedEnterTransition() + } + } + + val speakerAdapter = SpeakerAdapter(this, speakerViewModel, headLoadListener) + binding.speakerDetailRecyclerView.run { + adapter = speakerAdapter + itemAnimator?.run { + addDuration = 120L + moveDuration = 120L + changeDuration = 120L + removeDuration = 100L + } + doOnLayout { + addOnScrollListener( + PushUpScrollListener(binding.up, it, R.id.speaker_name, R.id.speaker_grid_image) + ) + } + } + binding.up.setOnClickListener { + NavUtils.navigateUpFromSameTask(requireActivity()) + } + + speakerViewModel.access.observe( + viewLifecycleOwner, + Observer { + binding.editFloat.visibility = if (it != null) { + VISIBLE + } else { + GONE + } + } + ) + + binding.editFloat.setOnClickListener { + parentFragmentManager.inTransaction { + replace( + R.id.speaker_container, + CreateSpeakerFragment().apply { + arguments = this@SpeakerFragment.arguments + } + ) + addToBackStack(null) + } + } + + return binding.root + } + + companion object { + fun newInstance(speakerId: Long): SpeakerFragment { + return SpeakerFragment().apply { + arguments = bundleOf(SPEAKER_ID to speakerId) + } + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/syncregistry/SyncRegistryBindingAdapters.kt b/app/src/main/java/com/forcetower/uefs/feature/syncregistry/SyncRegistryBindingAdapters.kt index 9e8f18ed7..b39f4fc5e 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/syncregistry/SyncRegistryBindingAdapters.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/syncregistry/SyncRegistryBindingAdapters.kt @@ -1,59 +1,62 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.feature.syncregistry - -import android.widget.TextView -import androidx.databinding.BindingAdapter -import com.forcetower.uefs.R -import com.forcetower.uefs.core.model.unes.NetworkType -import com.forcetower.uefs.feature.shared.extensions.formatFullDate - -@BindingAdapter("syncTime") -fun bindTime(tv: TextView, value: Long?) { - if (value == null) tv.text = "..." - else { - val date = value.formatFullDate() - tv.text = date - } -} - -@BindingAdapter(value = ["network", "networkType"], requireAll = true) -fun bindNetwork(tv: TextView, network: String, networkType: Int) { - val drawable = if (networkType == NetworkType.WIFI.ordinal) - R.drawable.ic_network_wifi_black_24dp - else - R.drawable.ic_network_cell_black_24dp - - tv.setCompoundDrawablesWithIntrinsicBounds(drawable, 0, 0, 0) - tv.text = network -} - -@BindingAdapter(value = ["syncComplete", "syncStatus"]) -fun bindStatus(tv: TextView, syncComplete: Boolean, syncStatus: Boolean) { - val message = if (!syncComplete) - tv.context.getString(R.string.sync_incomplete) - else if (syncStatus) - tv.context.getString(R.string.sync_completed) - else - tv.context.getString(R.string.sync_failed) - - tv.text = message -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.feature.syncregistry + +import android.widget.TextView +import androidx.databinding.BindingAdapter +import com.forcetower.uefs.R +import com.forcetower.uefs.core.model.unes.NetworkType +import com.forcetower.uefs.feature.shared.extensions.formatFullDate + +@BindingAdapter("syncTime") +fun bindTime(tv: TextView, value: Long?) { + if (value == null) { + tv.text = "..." + } else { + val date = value.formatFullDate() + tv.text = date + } +} + +@BindingAdapter(value = ["network", "networkType"], requireAll = true) +fun bindNetwork(tv: TextView, network: String, networkType: Int) { + val drawable = if (networkType == NetworkType.WIFI.ordinal) { + R.drawable.ic_network_wifi_black_24dp + } else { + R.drawable.ic_network_cell_black_24dp + } + + tv.setCompoundDrawablesWithIntrinsicBounds(drawable, 0, 0, 0) + tv.text = network +} + +@BindingAdapter(value = ["syncComplete", "syncStatus"]) +fun bindStatus(tv: TextView, syncComplete: Boolean, syncStatus: Boolean) { + val message = if (!syncComplete) { + tv.context.getString(R.string.sync_incomplete) + } else if (syncStatus) { + tv.context.getString(R.string.sync_completed) + } else { + tv.context.getString(R.string.sync_failed) + } + + tv.text = message +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/themeswitcher/ThemePreferencesManager.kt b/app/src/main/java/com/forcetower/uefs/feature/themeswitcher/ThemePreferencesManager.kt index 2b5d09f10..90e433a22 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/themeswitcher/ThemePreferencesManager.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/themeswitcher/ThemePreferencesManager.kt @@ -99,8 +99,9 @@ class ThemePreferencesManager(private val context: Context) { intArrayOf(R.id.theme_feature_background_color, background) ) for (i in themesMap.indices) { - if (themesMap[i][1] != 0) + if (themesMap[i][1] != 0) { ThemeOverlayUtils.setThemeOverlay(themesMap[i][0], themesMap[i][1]) + } } } @@ -111,8 +112,9 @@ class ThemePreferencesManager(private val context: Context) { intArrayOf(R.id.theme_feature_background_color, background) ) for (i in themesMap.indices) { - if (themesMap[i][1] != 0) + if (themesMap[i][1] != 0) { ThemeOverlayUtils.setThemeOverlay(themesMap[i][0], themesMap[i][1]) + } } activity.recreate() diff --git a/app/src/main/java/com/forcetower/uefs/feature/themeswitcher/ThemeSwitcherFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/themeswitcher/ThemeSwitcherFragment.kt index 641c55582..b769e8227 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/themeswitcher/ThemeSwitcherFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/themeswitcher/ThemeSwitcherFragment.kt @@ -44,8 +44,8 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber import javax.inject.Inject +import timber.log.Timber @AndroidEntryPoint class ThemeSwitcherFragment : BottomSheetDialogFragment() { @@ -141,8 +141,9 @@ class ThemeSwitcherFragment : BottomSheetDialogFragment() { val themeValues = resources.obtainTypedArray(overlays) val contentDescriptionArray = resources.obtainTypedArray(contentDescriptions) - if (themeValues.length() != contentDescriptionArray.length()) + if (themeValues.length() != contentDescriptionArray.length()) { throw IllegalStateException("Values and contents must be same length") + } for (i in 0 until themeValues.length()) { @StyleRes val valueThemeOverlay = themeValues.getResourceId(i, 0) diff --git a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/confirm/ConfirmEmailAccountFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/confirm/ConfirmEmailAccountFragment.kt index 66fd48b5a..44726e36b 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/confirm/ConfirmEmailAccountFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/confirm/ConfirmEmailAccountFragment.kt @@ -81,4 +81,4 @@ class ConfirmEmailAccountFragment : UFragment() { private fun onTooManyTries() { showSnack(getString(R.string.service_account_email_confirm_too_many_tries), Snackbar.LENGTH_LONG) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/confirm/ConfirmEmailAccountViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/confirm/ConfirmEmailAccountViewModel.kt index 7571afbb1..3e5311a2a 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/confirm/ConfirmEmailAccountViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/confirm/ConfirmEmailAccountViewModel.kt @@ -7,8 +7,8 @@ import com.forcetower.uefs.domain.usecase.auth.LinkEmailUseCase import com.forcetower.uefs.feature.unesaccount.confirm.vm.ConfirmEmailAccountEvent import com.forcetower.uefs.feature.unesaccount.confirm.vm.ConfirmEmailAccountState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.launch @HiltViewModel class ConfirmEmailAccountViewModel @Inject constructor( @@ -34,4 +34,4 @@ class ConfirmEmailAccountViewModel @Inject constructor( setState { it.copy(loading = false) } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/confirm/vm/ConfirmEmailAccountEvent.kt b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/confirm/vm/ConfirmEmailAccountEvent.kt index 7bd6f6c59..f6af086f9 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/confirm/vm/ConfirmEmailAccountEvent.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/confirm/vm/ConfirmEmailAccountEvent.kt @@ -6,4 +6,4 @@ sealed interface ConfirmEmailAccountEvent { data object EmailTaken : ConfirmEmailAccountEvent data object ConnectionFailed : ConfirmEmailAccountEvent data object TooManyTries : ConfirmEmailAccountEvent -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/email/LinkEmailAccountFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/email/LinkEmailAccountFragment.kt index 12b0fc36d..67e91e734 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/email/LinkEmailAccountFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/email/LinkEmailAccountFragment.kt @@ -66,4 +66,4 @@ class LinkEmailAccountFragment : UFragment() { private fun onSendError() { showSnack(getString(R.string.service_account_email_send_failed)) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/email/LinkEmailAccountViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/email/LinkEmailAccountViewModel.kt index a6ade26bb..a6314c45a 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/email/LinkEmailAccountViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/email/LinkEmailAccountViewModel.kt @@ -8,8 +8,8 @@ import com.forcetower.uefs.domain.usecase.auth.LinkEmailUseCase import com.forcetower.uefs.feature.unesaccount.email.vm.LinkEmailAccountEvent import com.forcetower.uefs.feature.unesaccount.email.vm.LinkEmailAccountState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.launch @HiltViewModel class LinkEmailAccountViewModel @Inject constructor( @@ -36,4 +36,4 @@ class LinkEmailAccountViewModel @Inject constructor( setState { it.copy(loading = false) } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/email/vm/LinkEmailAccountEvent.kt b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/email/vm/LinkEmailAccountEvent.kt index e8dae6004..af2946358 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/email/vm/LinkEmailAccountEvent.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/email/vm/LinkEmailAccountEvent.kt @@ -5,4 +5,4 @@ sealed interface LinkEmailAccountEvent { data class EmailSent(val token: String, val email: String) : LinkEmailAccountEvent data object InvalidInfo : LinkEmailAccountEvent data object SendError : LinkEmailAccountEvent -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/email/vm/LinkEmailAccountState.kt b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/email/vm/LinkEmailAccountState.kt index 10c2ca8b4..f112ed172 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/email/vm/LinkEmailAccountState.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/email/vm/LinkEmailAccountState.kt @@ -2,4 +2,4 @@ package com.forcetower.uefs.feature.unesaccount.email.vm data class LinkEmailAccountState( val loading: Boolean = false -) \ No newline at end of file +) diff --git a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/login/LoginAccountFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/login/LoginAccountFragment.kt index d05112d9f..a61c1fc79 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/login/LoginAccountFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/login/LoginAccountFragment.kt @@ -9,9 +9,7 @@ import androidx.core.view.isVisible import androidx.credentials.CredentialManager import androidx.credentials.GetCredentialRequest import androidx.credentials.GetCredentialResponse -import androidx.credentials.GetPasswordOption import androidx.credentials.GetPublicKeyCredentialOption -import androidx.credentials.PasswordCredential import androidx.credentials.PublicKeyCredential import androidx.credentials.exceptions.GetCredentialException import androidx.fragment.app.viewModels @@ -117,4 +115,4 @@ class LoginAccountFragment : UFragment() { private fun onLoginFailed() { showSnack(getString(R.string.service_account_login_anonymous_failed)) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/login/LoginAccountViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/login/LoginAccountViewModel.kt index cc23362ee..d899d9ccd 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/login/LoginAccountViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/login/LoginAccountViewModel.kt @@ -2,16 +2,15 @@ package com.forcetower.uefs.feature.unesaccount.login import androidx.lifecycle.viewModelScope import com.forcetower.core.lifecycle.viewmodel.BaseViewModel -import com.forcetower.uefs.domain.usecase.auth.EdgeAnonymousLoginUseCase -import dagger.hilt.android.lifecycle.HiltViewModel -import com.forcetower.core.lifecycle.viewmodel.EventViewModel import com.forcetower.uefs.domain.usecase.auth.CompleteAssertionUseCase +import com.forcetower.uefs.domain.usecase.auth.EdgeAnonymousLoginUseCase import com.forcetower.uefs.domain.usecase.auth.StartAssertionUseCase import com.forcetower.uefs.feature.unesaccount.login.vm.LoginAccountEvent import com.forcetower.uefs.feature.unesaccount.login.vm.LoginAccountState +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject @HiltViewModel class LoginAccountViewModel @Inject constructor( @@ -29,10 +28,11 @@ class LoginAccountViewModel @Inject constructor( Timber.e(it, "Failed to anonymously login") sendEvent { LoginAccountEvent.LoginFailed } }.onSuccess { user -> - if (user?.email != null) + if (user?.email != null) { sendEvent { LoginAccountEvent.SuccessHasEmail } - else + } else { sendEvent { LoginAccountEvent.SuccessLinkEmail } + } } setState { it.copy(loading = false) } } @@ -60,10 +60,11 @@ class LoginAccountViewModel @Inject constructor( Timber.e(it, "Failed to complete assertion") sendEvent { LoginAccountEvent.LoginFailed } }.onSuccess { user -> - if (user?.email != null) + if (user?.email != null) { sendEvent { LoginAccountEvent.SuccessHasEmail } - else + } else { sendEvent { LoginAccountEvent.SuccessLinkEmail } + } } setState { it.copy(loading = false) } } @@ -72,4 +73,4 @@ class LoginAccountViewModel @Inject constructor( fun completePasskeyLoading() { setState { it.copy(loading = false) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/login/vm/LoginAccountEvent.kt b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/login/vm/LoginAccountEvent.kt index 45ab8e860..585d6ed2a 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/login/vm/LoginAccountEvent.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/login/vm/LoginAccountEvent.kt @@ -5,4 +5,4 @@ sealed interface LoginAccountEvent { data object SuccessLinkEmail : LoginAccountEvent data object LoginFailed : LoginAccountEvent data class StartPasskeyAssertion(val flowId: String, val json: String) : LoginAccountEvent -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/login/vm/LoginAccountState.kt b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/login/vm/LoginAccountState.kt index bc14d5177..0330d32ee 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/login/vm/LoginAccountState.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/login/vm/LoginAccountState.kt @@ -2,4 +2,4 @@ package com.forcetower.uefs.feature.unesaccount.login.vm data class LoginAccountState( val loading: Boolean = false -) \ No newline at end of file +) diff --git a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/overview/AccountOverviewFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/overview/AccountOverviewFragment.kt index 25d9f1a03..3a5cf2f11 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/overview/AccountOverviewFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/overview/AccountOverviewFragment.kt @@ -126,14 +126,14 @@ class AccountOverviewFragment : UFragment() { val request = CreatePublicKeyCredentialRequest( requestJson = event.json, - preferImmediatelyAvailableCredentials = false, + preferImmediatelyAvailableCredentials = false ) lifecycleScope.launch { try { val result = manager.createCredential( context = requireActivity(), - request = request, + request = request ) handlePasskeyRegistrationResult(result, event) } catch (e: CreateCredentialCancellationException) { @@ -219,4 +219,4 @@ class AccountOverviewFragment : UFragment() { val imageUri = result.uriContent ?: return onImagePicked(imageUri) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/overview/AccountOverviewViewModel.kt b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/overview/AccountOverviewViewModel.kt index cd79ddf6f..e1ca5cd10 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/overview/AccountOverviewViewModel.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/overview/AccountOverviewViewModel.kt @@ -11,9 +11,9 @@ import com.forcetower.uefs.domain.usecase.profile.GetProfileUseCase import com.forcetower.uefs.feature.unesaccount.overview.vm.AccountOverviewEvent import com.forcetower.uefs.feature.unesaccount.overview.vm.AccountOverviewState import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject @HiltViewModel class AccountOverviewViewModel @Inject constructor( @@ -21,7 +21,8 @@ class AccountOverviewViewModel @Inject constructor( private val registerPasskeyUseCase: RegisterPasskeyUseCase, getProfile: GetProfileUseCase, private val changeProfilePictureUseCase: ChangeProfilePictureUseCase -) : BaseViewModel(AccountOverviewState() +) : BaseViewModel( + AccountOverviewState() ) { val user = getAccount().asLiveData() val profile = getProfile().asLiveData() @@ -82,4 +83,4 @@ class AccountOverviewViewModel @Inject constructor( setState { it.copy(uploadingPicture = false) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/overview/vm/AccountOverviewEvent.kt b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/overview/vm/AccountOverviewEvent.kt index deebdcf56..40153e6d3 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/overview/vm/AccountOverviewEvent.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/overview/vm/AccountOverviewEvent.kt @@ -4,5 +4,5 @@ sealed interface AccountOverviewEvent { data object PasskeyRegisterConnectionFailed : AccountOverviewEvent data object PasskeyRegisterCompleted : AccountOverviewEvent data object ImageUpdateFailed : AccountOverviewEvent - data class PasskeyRegister(val flowId: String, val json: String): AccountOverviewEvent -} \ No newline at end of file + data class PasskeyRegister(val flowId: String, val json: String) : AccountOverviewEvent +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/start/CreateAccountStartFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/start/CreateAccountStartFragment.kt index 784ab3022..18e99be12 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/start/CreateAccountStartFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/start/CreateAccountStartFragment.kt @@ -35,4 +35,4 @@ class CreateAccountStartFragment : UFragment() { findNavController().navigate(directions) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/why/CreateAccountReasonsFragment.kt b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/why/CreateAccountReasonsFragment.kt index 21aee046b..1402722ea 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/unesaccount/why/CreateAccountReasonsFragment.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/unesaccount/why/CreateAccountReasonsFragment.kt @@ -56,5 +56,4 @@ class CreateAccountReasonsFragment : UFragment() { findNavController().popBackStack() } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/forcetower/uefs/feature/web/CustomTabsHelper.kt b/app/src/main/java/com/forcetower/uefs/feature/web/CustomTabsHelper.kt index 028f8a2fc..e8d3a1380 100644 --- a/app/src/main/java/com/forcetower/uefs/feature/web/CustomTabsHelper.kt +++ b/app/src/main/java/com/forcetower/uefs/feature/web/CustomTabsHelper.kt @@ -26,8 +26,8 @@ import android.content.pm.PackageManager import android.net.Uri import android.text.TextUtils import androidx.browser.customtabs.CustomTabsService -import timber.log.Timber import java.util.ArrayList +import timber.log.Timber object CustomTabsHelper { private const val STABLE_PACKAGE = "com.android.chrome" diff --git a/app/src/main/java/com/forcetower/uefs/service/NotificationCreator.kt b/app/src/main/java/com/forcetower/uefs/service/NotificationCreator.kt index c2cdc354f..95a3f1b55 100644 --- a/app/src/main/java/com/forcetower/uefs/service/NotificationCreator.kt +++ b/app/src/main/java/com/forcetower/uefs/service/NotificationCreator.kt @@ -122,20 +122,23 @@ object NotificationCreator { return } - val channel = if (created) + val channel = if (created) { NotificationHelper.CHANNEL_ABSENCE_CREATE_ID - else + } else { NotificationHelper.CHANNEL_ABSENCE_REMOVE_ID + } - val message = if (created) + val message = if (created) { context.getString(R.string.notification_absence_posted, absence.description.toTitleCase()) - else + } else { context.getString(R.string.notification_absence_deleted, absence.description.toTitleCase()) + } - val titleRes = if (created) + val titleRes = if (created) { R.string.notification_absence_posted_title - else + } else { R.string.notification_absence_removed_title + } val builder = notificationBuilder(context, channel) .setContentTitle(context.getString(titleRes)) @@ -179,13 +182,16 @@ object NotificationCreator { 1 -> { val value = grade.grade.gradeDouble() Timber.d("Level 1 spoiler value: $value") - if (value == null) context.getString(R.string.notification_grade_posted_message_lv_0, grade.grade.name, discipline) - else when (value) { - in 0.0..6.9 -> context.getString(R.string.notification_grade_posted_message_lv_1_bad, grade.grade.name, discipline) - in 7.0..7.9 -> context.getString(R.string.notification_grade_posted_message_lv_1_pass, grade.grade.name, discipline) - in 8.0..9.9 -> context.getString(R.string.notification_grade_posted_message_lv_1_good, grade.grade.name, discipline) - 10.0 -> context.getString(R.string.notification_grade_posted_message_lv_1_perfect, grade.grade.name, discipline) - else -> context.getString(R.string.notification_grade_posted_message_lv_0, grade.grade.name, discipline) + if (value == null) { + context.getString(R.string.notification_grade_posted_message_lv_0, grade.grade.name, discipline) + } else { + when (value) { + in 0.0..6.9 -> context.getString(R.string.notification_grade_posted_message_lv_1_bad, grade.grade.name, discipline) + in 7.0..7.9 -> context.getString(R.string.notification_grade_posted_message_lv_1_pass, grade.grade.name, discipline) + in 8.0..9.9 -> context.getString(R.string.notification_grade_posted_message_lv_1_good, grade.grade.name, discipline) + 10.0 -> context.getString(R.string.notification_grade_posted_message_lv_1_perfect, grade.grade.name, discipline) + else -> context.getString(R.string.notification_grade_posted_message_lv_0, grade.grade.name, discipline) + } } } 2 -> context.getString(R.string.notification_grade_posted_message_lv_2, grade.grade.grade, discipline) diff --git a/app/src/main/java/com/forcetower/uefs/service/NotificationHelper.kt b/app/src/main/java/com/forcetower/uefs/service/NotificationHelper.kt index 23184b90f..f06cbe2cd 100644 --- a/app/src/main/java/com/forcetower/uefs/service/NotificationHelper.kt +++ b/app/src/main/java/com/forcetower/uefs/service/NotificationHelper.kt @@ -1,170 +1,171 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.service - -import android.annotation.TargetApi -import android.app.NotificationChannel -import android.app.NotificationChannelGroup -import android.app.NotificationManager -import android.content.Context -import android.content.ContextWrapper -import android.os.Build -import androidx.core.content.ContextCompat -import com.forcetower.uefs.R -import com.forcetower.uefs.core.util.VersionUtils - -class NotificationHelper(val context: Context) : ContextWrapper(context) { - - fun createChannels() { - if (!VersionUtils.isOreo()) return - - val cGrades = NotificationChannelGroup(CHANNEL_GROUP_GRADES_ID, getString(R.string.channel_group_grades)) - val cAbsences = NotificationChannelGroup(CHANNEL_GROUP_ABSENCE_ID, getString(R.string.channel_group_absences)) - val cMessages = NotificationChannelGroup(CHANNEL_GROUP_MESSAGES_ID, getString(R.string.channel_group_messages)) - val cGeneral = NotificationChannelGroup(CHANNEL_GROUP_GENERAL_ID, getString(R.string.channel_group_general)) - val cEvents = NotificationChannelGroup(CHANNEL_GROUP_EVENTS_ID, getString(R.string.channel_group_events)) - val cServices = NotificationChannelGroup(CHANNEL_GROUP_SERVICE_REQUEST_ID, getString(R.string.channel_group_service_request)) - val cDisciplines = NotificationChannelGroup(CHANNEL_GROUP_DISCIPLINE_ID, context.getString(R.string.channel_group_disciplines)) - val cSocial = NotificationChannelGroup(CHANNEL_GROUP_SOCIAL_ID, context.getString(R.string.channel_group_social_network)) - - val manager = getManager() - manager.createNotificationChannelGroups(listOf(cGrades, cMessages, cGeneral, cEvents, cServices, cDisciplines, cSocial, cAbsences)) - - val messages = createChannel(CHANNEL_MESSAGES_TEACHER_ID, getString(R.string.channel_messages_teachers), NotificationManager.IMPORTANCE_DEFAULT) - val uefsMsg = createChannel(CHANNEL_MESSAGES_UEFS_ID, getString(R.string.channel_messages_uefs), NotificationManager.IMPORTANCE_DEFAULT) - val posted = createChannel(CHANNEL_GRADES_POSTED_ID, getString(R.string.channel_grades_posted), NotificationManager.IMPORTANCE_DEFAULT) - val dateChanged = createChannel(CHANNEL_GRADES_DATE_CHANGED_ID, getString(R.string.channel_grades_date_changed), NotificationManager.IMPORTANCE_DEFAULT) - val valueChanged = createChannel(CHANNEL_GRADES_VALUE_CHANGED_ID, getString(R.string.channel_grades_value_changed), NotificationManager.IMPORTANCE_DEFAULT) - val created = createChannel(CHANNEL_GRADES_CREATED_ID, getString(R.string.channel_grades_created), NotificationManager.IMPORTANCE_DEFAULT) - val absenceCreate = createChannel(CHANNEL_ABSENCE_CREATE_ID, getString(R.string.channel_absence_created), NotificationManager.IMPORTANCE_DEFAULT) - val absenceRemove = createChannel(CHANNEL_ABSENCE_REMOVE_ID, getString(R.string.channel_absence_removed), NotificationManager.IMPORTANCE_DEFAULT) - val warnings = createChannel(CHANNEL_GENERAL_WARNINGS_ID, getString(R.string.warnings), NotificationManager.IMPORTANCE_DEFAULT) - val remote = createChannel(CHANNEL_GENERAL_REMOTE_ID, getString(R.string.remote), NotificationManager.IMPORTANCE_DEFAULT) - val eventGen = createChannel(CHANNEL_EVENTS_GENERAL_ID, getString(R.string.channel_events_general), NotificationManager.IMPORTANCE_DEFAULT) - val bigTray = createChannel(CHANNEL_GENERAL_BIGTRAY_ID, getString(R.string.channel_big_tray_quota), NotificationManager.IMPORTANCE_LOW) - val serviceFrg = createChannel(CHANNEL_GENERAL_SYNC_SERVICE_FOREGROUND, getString(R.string.channel_service_sync_foreground), NotificationManager.IMPORTANCE_LOW) - val commonLow = createChannel(CHANNEL_GENERAL_COMMON_LOW_ID, getString(R.string.channel_common_low), NotificationManager.IMPORTANCE_LOW) - val commonDef = createChannel(CHANNEL_GENERAL_COMMON_HIG_ID, getString(R.string.channel_common_hig), NotificationManager.IMPORTANCE_HIGH) - val svcCreate = createChannel(CHANNEL_SVC_REQ_CREATE_ID, getString(R.string.channel_svc_req_create), NotificationManager.IMPORTANCE_LOW) - val svcUpdate = createChannel(CHANNEL_SVC_REQ_UPDATE_ID, getString(R.string.channel_svc_req_update), NotificationManager.IMPORTANCE_DEFAULT) - val materialPost = createChannel(CHANNEL_DISCIPLINE_MATERIAL_POSTED, context.getString(R.string.channel_discipline_material_posted), NotificationManager.IMPORTANCE_DEFAULT) - val socialStatements = createChannel(CHANNEL_SOCIAL_STATEMENT_RECEIVED_ID, context.getString(R.string.channel_social_statement_received), NotificationManager.IMPORTANCE_DEFAULT) - - messages.group = CHANNEL_GROUP_MESSAGES_ID - uefsMsg.group = CHANNEL_GROUP_MESSAGES_ID - posted.group = CHANNEL_GROUP_GRADES_ID - dateChanged.group = CHANNEL_GROUP_GRADES_ID - valueChanged.group = CHANNEL_GROUP_GRADES_ID - created.group = CHANNEL_GROUP_GRADES_ID - warnings.group = CHANNEL_GROUP_GENERAL_ID - remote.group = CHANNEL_GROUP_GENERAL_ID - eventGen.group = CHANNEL_GROUP_EVENTS_ID - bigTray.group = CHANNEL_GROUP_GENERAL_ID - commonLow.group = CHANNEL_GROUP_GENERAL_ID - commonDef.group = CHANNEL_GROUP_GENERAL_ID - serviceFrg.group = CHANNEL_GROUP_GENERAL_ID - svcCreate.group = CHANNEL_GROUP_SERVICE_REQUEST_ID - svcUpdate.group = CHANNEL_GROUP_SERVICE_REQUEST_ID - materialPost.group = CHANNEL_GROUP_DISCIPLINE_ID - socialStatements.group = CHANNEL_GROUP_SOCIAL_ID - absenceCreate.group = CHANNEL_GROUP_ABSENCE_ID - absenceRemove.group = CHANNEL_GROUP_ABSENCE_ID - - manager.createNotificationChannel(messages) - manager.createNotificationChannel(uefsMsg) - manager.createNotificationChannel(posted) - manager.createNotificationChannel(dateChanged) - manager.createNotificationChannel(valueChanged) - manager.createNotificationChannel(created) - manager.createNotificationChannel(warnings) - manager.createNotificationChannel(remote) - manager.createNotificationChannel(eventGen) - manager.createNotificationChannel(bigTray) - manager.createNotificationChannel(serviceFrg) - manager.createNotificationChannel(commonLow) - manager.createNotificationChannel(commonDef) - manager.createNotificationChannel(svcCreate) - manager.createNotificationChannel(svcUpdate) - manager.createNotificationChannel(materialPost) - manager.createNotificationChannel(socialStatements) - manager.createNotificationChannel(absenceCreate) - manager.createNotificationChannel(absenceRemove) - - manager.deleteNotificationChannel(CHANNEL_MESSAGES_DCE_ID) - manager.deleteNotificationChannel(CHANNEL_MESSAGES_SAGRES_ID) - manager.deleteNotificationChannel(CHANNEL_GRADES_CHANGED_ID) - } - - @TargetApi(Build.VERSION_CODES.O) - private fun createChannel( - channelId: String, - name: CharSequence, - importance: Int, - vibration: LongArray = longArrayOf(150, 300, 150, 300), - showBadge: Boolean = true, - enableLights: Boolean = true - ): NotificationChannel { - val channel = NotificationChannel(channelId, name, importance) - channel.enableLights(enableLights) - channel.setShowBadge(showBadge) - channel.vibrationPattern = vibration - return channel - } - - private fun getManager() = ContextCompat.getSystemService(this, NotificationManager::class.java)!! - - companion object { - // Notification Groups - const val CHANNEL_GROUP_MESSAGES_ID = "com.forcetower.uefs.MESSAGES" - const val CHANNEL_GROUP_GRADES_ID = "com.forcetower.uefs.GRADES" - const val CHANNEL_GROUP_ABSENCE_ID = "com.forcetower.uefs.GRADES" - const val CHANNEL_GROUP_GENERAL_ID = "com.forcetower.uefs.GENERAL" - const val CHANNEL_GROUP_EVENTS_ID = "com.forcetower.uefs.EVENTS" - const val CHANNEL_GROUP_SERVICE_REQUEST_ID = "com.forcetower.uefs.SERVICE_REQUEST" - const val CHANNEL_GROUP_DISCIPLINE_ID = "com.forcetower.uefs.DISCIPLINE" - const val CHANNEL_GROUP_SOCIAL_ID = "com.forcetower.uefs.SOCIAL" - // Notification Channels - const val CHANNEL_MESSAGES_TEACHER_ID = "com.forcetower.uefs.MESSAGES.SAGRES.TEACHER.POST" - const val CHANNEL_MESSAGES_UEFS_ID = "com.forcetower.uefs.MESSAGES.SAGRES.UEFS.POST" - const val CHANNEL_GRADES_POSTED_ID = "com.forcetower.uefs.GRADES.POSTED" - const val CHANNEL_GRADES_CREATED_ID = "com.forcetower.uefs.GRADES.CREATE" - const val CHANNEL_GRADES_DATE_CHANGED_ID = "com.forcetower.uefs.GRADES.DATE_CHANGE" - const val CHANNEL_GRADES_VALUE_CHANGED_ID = "com.forcetower.uefs.GRADES.VALUE_CHANGED" - const val CHANNEL_ABSENCE_CREATE_ID = "com.forcetower.uefs.ABSENCE.CREATED" - const val CHANNEL_ABSENCE_REMOVE_ID = "com.forcetower.uefs.ABSENCE.REMOVED" - const val CHANNEL_GENERAL_WARNINGS_ID = "com.forcetower.uefs.GENERAL.WARNINGS" - const val CHANNEL_GENERAL_COMMON_LOW_ID = "com.forcetower.uefs.GENERAL.COMMON.LOW" - const val CHANNEL_GENERAL_COMMON_HIG_ID = "com.forcetower.uefs.GENERAL.COMMON.HIGH" - const val CHANNEL_GENERAL_REMOTE_ID = "com.forcetower.uefs.GENERAL.REMOTE" - const val CHANNEL_GENERAL_BIGTRAY_ID = "com.forcetower.uefs.GENERAL.BIGTRAY" - const val CHANNEL_GENERAL_SYNC_SERVICE_FOREGROUND = "com.forcetower.uefs.SYNC.FOREGROUND" - const val CHANNEL_EVENTS_GENERAL_ID = "com.forcetower.uefs.EVENTS.GENERAL" - const val CHANNEL_SVC_REQ_CREATE_ID = "com.forcetower.uefs.SERVICE_REQUEST.CREATE" - const val CHANNEL_SVC_REQ_UPDATE_ID = "com.forcetower.uefs.SERVICE_REQUEST.UPDATE" - const val CHANNEL_DISCIPLINE_MATERIAL_POSTED = "com.forcetower.uefs.DISCIPLINE.MATERIAL.POSTED" - const val CHANNEL_SOCIAL_STATEMENT_RECEIVED_ID = "com.forcetower.uefs.SOCIAL.STATEMENT.RECEIVED" - - // Deleted Channels - const val CHANNEL_MESSAGES_DCE_ID = "com.forcetower.uefs.MESSAGES.DCE" - const val CHANNEL_MESSAGES_SAGRES_ID = "com.forcetower.uefs.MESSAGES.SAGRES.POST" - const val CHANNEL_GRADES_CHANGED_ID = "com.forcetower.uefs.GRADES.CHANGE" - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.service + +import android.annotation.TargetApi +import android.app.NotificationChannel +import android.app.NotificationChannelGroup +import android.app.NotificationManager +import android.content.Context +import android.content.ContextWrapper +import android.os.Build +import androidx.core.content.ContextCompat +import com.forcetower.uefs.R +import com.forcetower.uefs.core.util.VersionUtils + +class NotificationHelper(val context: Context) : ContextWrapper(context) { + + fun createChannels() { + if (!VersionUtils.isOreo()) return + + val cGrades = NotificationChannelGroup(CHANNEL_GROUP_GRADES_ID, getString(R.string.channel_group_grades)) + val cAbsences = NotificationChannelGroup(CHANNEL_GROUP_ABSENCE_ID, getString(R.string.channel_group_absences)) + val cMessages = NotificationChannelGroup(CHANNEL_GROUP_MESSAGES_ID, getString(R.string.channel_group_messages)) + val cGeneral = NotificationChannelGroup(CHANNEL_GROUP_GENERAL_ID, getString(R.string.channel_group_general)) + val cEvents = NotificationChannelGroup(CHANNEL_GROUP_EVENTS_ID, getString(R.string.channel_group_events)) + val cServices = NotificationChannelGroup(CHANNEL_GROUP_SERVICE_REQUEST_ID, getString(R.string.channel_group_service_request)) + val cDisciplines = NotificationChannelGroup(CHANNEL_GROUP_DISCIPLINE_ID, context.getString(R.string.channel_group_disciplines)) + val cSocial = NotificationChannelGroup(CHANNEL_GROUP_SOCIAL_ID, context.getString(R.string.channel_group_social_network)) + + val manager = getManager() + manager.createNotificationChannelGroups(listOf(cGrades, cMessages, cGeneral, cEvents, cServices, cDisciplines, cSocial, cAbsences)) + + val messages = createChannel(CHANNEL_MESSAGES_TEACHER_ID, getString(R.string.channel_messages_teachers), NotificationManager.IMPORTANCE_DEFAULT) + val uefsMsg = createChannel(CHANNEL_MESSAGES_UEFS_ID, getString(R.string.channel_messages_uefs), NotificationManager.IMPORTANCE_DEFAULT) + val posted = createChannel(CHANNEL_GRADES_POSTED_ID, getString(R.string.channel_grades_posted), NotificationManager.IMPORTANCE_DEFAULT) + val dateChanged = createChannel(CHANNEL_GRADES_DATE_CHANGED_ID, getString(R.string.channel_grades_date_changed), NotificationManager.IMPORTANCE_DEFAULT) + val valueChanged = createChannel(CHANNEL_GRADES_VALUE_CHANGED_ID, getString(R.string.channel_grades_value_changed), NotificationManager.IMPORTANCE_DEFAULT) + val created = createChannel(CHANNEL_GRADES_CREATED_ID, getString(R.string.channel_grades_created), NotificationManager.IMPORTANCE_DEFAULT) + val absenceCreate = createChannel(CHANNEL_ABSENCE_CREATE_ID, getString(R.string.channel_absence_created), NotificationManager.IMPORTANCE_DEFAULT) + val absenceRemove = createChannel(CHANNEL_ABSENCE_REMOVE_ID, getString(R.string.channel_absence_removed), NotificationManager.IMPORTANCE_DEFAULT) + val warnings = createChannel(CHANNEL_GENERAL_WARNINGS_ID, getString(R.string.warnings), NotificationManager.IMPORTANCE_DEFAULT) + val remote = createChannel(CHANNEL_GENERAL_REMOTE_ID, getString(R.string.remote), NotificationManager.IMPORTANCE_DEFAULT) + val eventGen = createChannel(CHANNEL_EVENTS_GENERAL_ID, getString(R.string.channel_events_general), NotificationManager.IMPORTANCE_DEFAULT) + val bigTray = createChannel(CHANNEL_GENERAL_BIGTRAY_ID, getString(R.string.channel_big_tray_quota), NotificationManager.IMPORTANCE_LOW) + val serviceFrg = createChannel(CHANNEL_GENERAL_SYNC_SERVICE_FOREGROUND, getString(R.string.channel_service_sync_foreground), NotificationManager.IMPORTANCE_LOW) + val commonLow = createChannel(CHANNEL_GENERAL_COMMON_LOW_ID, getString(R.string.channel_common_low), NotificationManager.IMPORTANCE_LOW) + val commonDef = createChannel(CHANNEL_GENERAL_COMMON_HIG_ID, getString(R.string.channel_common_hig), NotificationManager.IMPORTANCE_HIGH) + val svcCreate = createChannel(CHANNEL_SVC_REQ_CREATE_ID, getString(R.string.channel_svc_req_create), NotificationManager.IMPORTANCE_LOW) + val svcUpdate = createChannel(CHANNEL_SVC_REQ_UPDATE_ID, getString(R.string.channel_svc_req_update), NotificationManager.IMPORTANCE_DEFAULT) + val materialPost = createChannel(CHANNEL_DISCIPLINE_MATERIAL_POSTED, context.getString(R.string.channel_discipline_material_posted), NotificationManager.IMPORTANCE_DEFAULT) + val socialStatements = createChannel(CHANNEL_SOCIAL_STATEMENT_RECEIVED_ID, context.getString(R.string.channel_social_statement_received), NotificationManager.IMPORTANCE_DEFAULT) + + messages.group = CHANNEL_GROUP_MESSAGES_ID + uefsMsg.group = CHANNEL_GROUP_MESSAGES_ID + posted.group = CHANNEL_GROUP_GRADES_ID + dateChanged.group = CHANNEL_GROUP_GRADES_ID + valueChanged.group = CHANNEL_GROUP_GRADES_ID + created.group = CHANNEL_GROUP_GRADES_ID + warnings.group = CHANNEL_GROUP_GENERAL_ID + remote.group = CHANNEL_GROUP_GENERAL_ID + eventGen.group = CHANNEL_GROUP_EVENTS_ID + bigTray.group = CHANNEL_GROUP_GENERAL_ID + commonLow.group = CHANNEL_GROUP_GENERAL_ID + commonDef.group = CHANNEL_GROUP_GENERAL_ID + serviceFrg.group = CHANNEL_GROUP_GENERAL_ID + svcCreate.group = CHANNEL_GROUP_SERVICE_REQUEST_ID + svcUpdate.group = CHANNEL_GROUP_SERVICE_REQUEST_ID + materialPost.group = CHANNEL_GROUP_DISCIPLINE_ID + socialStatements.group = CHANNEL_GROUP_SOCIAL_ID + absenceCreate.group = CHANNEL_GROUP_ABSENCE_ID + absenceRemove.group = CHANNEL_GROUP_ABSENCE_ID + + manager.createNotificationChannel(messages) + manager.createNotificationChannel(uefsMsg) + manager.createNotificationChannel(posted) + manager.createNotificationChannel(dateChanged) + manager.createNotificationChannel(valueChanged) + manager.createNotificationChannel(created) + manager.createNotificationChannel(warnings) + manager.createNotificationChannel(remote) + manager.createNotificationChannel(eventGen) + manager.createNotificationChannel(bigTray) + manager.createNotificationChannel(serviceFrg) + manager.createNotificationChannel(commonLow) + manager.createNotificationChannel(commonDef) + manager.createNotificationChannel(svcCreate) + manager.createNotificationChannel(svcUpdate) + manager.createNotificationChannel(materialPost) + manager.createNotificationChannel(socialStatements) + manager.createNotificationChannel(absenceCreate) + manager.createNotificationChannel(absenceRemove) + + manager.deleteNotificationChannel(CHANNEL_MESSAGES_DCE_ID) + manager.deleteNotificationChannel(CHANNEL_MESSAGES_SAGRES_ID) + manager.deleteNotificationChannel(CHANNEL_GRADES_CHANGED_ID) + } + + @TargetApi(Build.VERSION_CODES.O) + private fun createChannel( + channelId: String, + name: CharSequence, + importance: Int, + vibration: LongArray = longArrayOf(150, 300, 150, 300), + showBadge: Boolean = true, + enableLights: Boolean = true + ): NotificationChannel { + val channel = NotificationChannel(channelId, name, importance) + channel.enableLights(enableLights) + channel.setShowBadge(showBadge) + channel.vibrationPattern = vibration + return channel + } + + private fun getManager() = ContextCompat.getSystemService(this, NotificationManager::class.java)!! + + companion object { + // Notification Groups + const val CHANNEL_GROUP_MESSAGES_ID = "com.forcetower.uefs.MESSAGES" + const val CHANNEL_GROUP_GRADES_ID = "com.forcetower.uefs.GRADES" + const val CHANNEL_GROUP_ABSENCE_ID = "com.forcetower.uefs.GRADES" + const val CHANNEL_GROUP_GENERAL_ID = "com.forcetower.uefs.GENERAL" + const val CHANNEL_GROUP_EVENTS_ID = "com.forcetower.uefs.EVENTS" + const val CHANNEL_GROUP_SERVICE_REQUEST_ID = "com.forcetower.uefs.SERVICE_REQUEST" + const val CHANNEL_GROUP_DISCIPLINE_ID = "com.forcetower.uefs.DISCIPLINE" + const val CHANNEL_GROUP_SOCIAL_ID = "com.forcetower.uefs.SOCIAL" + + // Notification Channels + const val CHANNEL_MESSAGES_TEACHER_ID = "com.forcetower.uefs.MESSAGES.SAGRES.TEACHER.POST" + const val CHANNEL_MESSAGES_UEFS_ID = "com.forcetower.uefs.MESSAGES.SAGRES.UEFS.POST" + const val CHANNEL_GRADES_POSTED_ID = "com.forcetower.uefs.GRADES.POSTED" + const val CHANNEL_GRADES_CREATED_ID = "com.forcetower.uefs.GRADES.CREATE" + const val CHANNEL_GRADES_DATE_CHANGED_ID = "com.forcetower.uefs.GRADES.DATE_CHANGE" + const val CHANNEL_GRADES_VALUE_CHANGED_ID = "com.forcetower.uefs.GRADES.VALUE_CHANGED" + const val CHANNEL_ABSENCE_CREATE_ID = "com.forcetower.uefs.ABSENCE.CREATED" + const val CHANNEL_ABSENCE_REMOVE_ID = "com.forcetower.uefs.ABSENCE.REMOVED" + const val CHANNEL_GENERAL_WARNINGS_ID = "com.forcetower.uefs.GENERAL.WARNINGS" + const val CHANNEL_GENERAL_COMMON_LOW_ID = "com.forcetower.uefs.GENERAL.COMMON.LOW" + const val CHANNEL_GENERAL_COMMON_HIG_ID = "com.forcetower.uefs.GENERAL.COMMON.HIGH" + const val CHANNEL_GENERAL_REMOTE_ID = "com.forcetower.uefs.GENERAL.REMOTE" + const val CHANNEL_GENERAL_BIGTRAY_ID = "com.forcetower.uefs.GENERAL.BIGTRAY" + const val CHANNEL_GENERAL_SYNC_SERVICE_FOREGROUND = "com.forcetower.uefs.SYNC.FOREGROUND" + const val CHANNEL_EVENTS_GENERAL_ID = "com.forcetower.uefs.EVENTS.GENERAL" + const val CHANNEL_SVC_REQ_CREATE_ID = "com.forcetower.uefs.SERVICE_REQUEST.CREATE" + const val CHANNEL_SVC_REQ_UPDATE_ID = "com.forcetower.uefs.SERVICE_REQUEST.UPDATE" + const val CHANNEL_DISCIPLINE_MATERIAL_POSTED = "com.forcetower.uefs.DISCIPLINE.MATERIAL.POSTED" + const val CHANNEL_SOCIAL_STATEMENT_RECEIVED_ID = "com.forcetower.uefs.SOCIAL.STATEMENT.RECEIVED" + + // Deleted Channels + const val CHANNEL_MESSAGES_DCE_ID = "com.forcetower.uefs.MESSAGES.DCE" + const val CHANNEL_MESSAGES_SAGRES_ID = "com.forcetower.uefs.MESSAGES.SAGRES.POST" + const val CHANNEL_GRADES_CHANGED_ID = "com.forcetower.uefs.GRADES.CHANGE" + } +} diff --git a/app/src/main/java/com/forcetower/uefs/widget/BaselineGridTextView.kt b/app/src/main/java/com/forcetower/uefs/widget/BaselineGridTextView.kt index 30b76a19f..2d7b34cf7 100644 --- a/app/src/main/java/com/forcetower/uefs/widget/BaselineGridTextView.kt +++ b/app/src/main/java/com/forcetower/uefs/widget/BaselineGridTextView.kt @@ -146,10 +146,11 @@ class BaselineGridTextView @JvmOverloads constructor( private fun computeLineHeight() { val fm = paint.fontMetrics val fontHeight = abs(fm.ascent - fm.descent) + fm.leading - val desiredLineHeight = if (lineHeightHint > 0) + val desiredLineHeight = if (lineHeightHint > 0) { lineHeightHint - else + } else { lineHeightMultiplierHint * fontHeight + } val baselineAlignedLineHeight = (fourDp * ceil((desiredLineHeight / fourDp).toDouble()).toFloat() + 0.5f).toInt() setLineSpacing(baselineAlignedLineHeight - fontHeight, 1f) diff --git a/app/src/main/java/com/forcetower/uefs/widget/BottomSheetBehavior.kt b/app/src/main/java/com/forcetower/uefs/widget/BottomSheetBehavior.kt index f1ca1dee2..48a30de96 100644 --- a/app/src/main/java/com/forcetower/uefs/widget/BottomSheetBehavior.kt +++ b/app/src/main/java/com/forcetower/uefs/widget/BottomSheetBehavior.kt @@ -1,909 +1,923 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.widget - -import android.annotation.SuppressLint -import android.content.Context -import android.os.Bundle -import android.os.Parcel -import android.os.Parcelable -import android.util.AttributeSet -import android.view.MotionEvent -import android.view.VelocityTracker -import android.view.View -import android.view.ViewConfiguration -import android.view.ViewGroup -import androidx.annotation.IntDef -import androidx.annotation.VisibleForTesting -import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior -import androidx.core.view.ViewCompat -import androidx.customview.view.AbsSavedState -import androidx.customview.widget.ViewDragHelper -import com.forcetower.uefs.R -import com.forcetower.uefs.feature.shared.extensions.asBoolean -import com.forcetower.uefs.feature.shared.extensions.asInt -import java.lang.ref.WeakReference -import kotlin.math.abs -import kotlin.math.absoluteValue -import kotlin.math.max - -/** - * Copy of material lib's BottomSheetBehavior that includes some bug fixes. - */ -// TODO remove when a fixed version in material lib is released. -class BottomSheetBehavior : Behavior { - - companion object { - /** The bottom sheet is dragging. */ - const val STATE_DRAGGING = 1 - /** The bottom sheet is settling. */ - const val STATE_SETTLING = 2 - /** The bottom sheet is expanded. */ - const val STATE_EXPANDED = 3 - /** The bottom sheet is collapsed. */ - const val STATE_COLLAPSED = 4 - /** The bottom sheet is hidden. */ - const val STATE_HIDDEN = 5 - /** The bottom sheet is half-expanded (used when behavior_fitToContents is false). */ - const val STATE_HALF_EXPANDED = 6 - - /** - * Peek at the 16:9 ratio keyline of its parent. This can be used as a parameter for - * [setPeekHeight(Int)]. [getPeekHeight()] will return this when the value is set. - */ - const val PEEK_HEIGHT_AUTO = -1 - - private const val HIDE_THRESHOLD = 0.5f - private const val HIDE_FRICTION = 0.1f - - @IntDef( - value = [ - STATE_DRAGGING, - STATE_SETTLING, - STATE_EXPANDED, - STATE_COLLAPSED, - STATE_HIDDEN, - STATE_HALF_EXPANDED - ] - ) - @Retention(AnnotationRetention.SOURCE) - annotation class State - - /** Utility to get the [BottomSheetBehavior] from a [view]. */ - @JvmStatic - fun from(view: View): BottomSheetBehavior<*> { - val lp = view.layoutParams as? CoordinatorLayout.LayoutParams - ?: throw IllegalArgumentException("view is not a child of CoordinatorLayout") - return lp.behavior as? BottomSheetBehavior - ?: throw IllegalArgumentException("view not associated with this behavior") - } - } - - /** Callback for monitoring events about bottom sheets. */ - interface BottomSheetCallback { - /** - * Called when the bottom sheet changes its state. - * - * @param bottomSheet The bottom sheet view. - * @param newState The new state. This will be one of link [STATE_DRAGGING], - * [STATE_SETTLING], [STATE_EXPANDED], [STATE_COLLAPSED], [STATE_HIDDEN], or - * [STATE_HALF_EXPANDED]. - */ - fun onStateChanged(bottomSheet: View, newState: Int) {} - - /** - * Called when the bottom sheet is being dragged. - * - * @param bottomSheet The bottom sheet view. - * @param slideOffset The new offset of this bottom sheet within [-1,1] range. Offset - * increases as this bottom sheet is moving upward. From 0 to 1 the sheet is between - * collapsed and expanded states and from -1 to 0 it is between hidden and collapsed states. - */ - fun onSlide(bottomSheet: View, slideOffset: Float) {} - } - - /** The current state of the bottom sheet, backing property */ - private var _state = STATE_COLLAPSED - /** The current state of the bottom sheet */ - @State - var state - get() = _state - set(@State value) { - if (_state == value) { - return - } - if (viewRef == null) { - // Child is not laid out yet. Set our state and let onLayoutChild() handle it later. - if (value == STATE_COLLAPSED || - value == STATE_EXPANDED || - value == STATE_HALF_EXPANDED || - (isHideable && value == STATE_HIDDEN) - ) { - _state = value - } - return - } - - viewRef?.get()?.apply { - // Start the animation; wait until a pending layout if there is one. - if (parent != null && parent.isLayoutRequested && isAttachedToWindow) { - post { - startSettlingAnimation(this, value) - } - } else { - startSettlingAnimation(this, value) - } - } - } - - /** Whether to fit to contents. If false, the behavior will include [STATE_HALF_EXPANDED]. */ - private var isFitToContents = true - set(value) { - if (field != value) { - field = value - // If sheet is already laid out, recalculate the collapsed offset. - // Otherwise onLayoutChild will handle this later. - if (viewRef != null) { - collapsedOffset = calculateCollapsedOffset() - } - // Fix incorrect expanded settings. - setStateInternal( - if (field && state == STATE_HALF_EXPANDED) STATE_EXPANDED else state - ) - } - } - - /** Real peek height in pixels */ - private var _peekHeight = 0 - /** Peek height in pixels, or [PEEK_HEIGHT_AUTO] */ - private var peekHeight - get() = if (peekHeightAuto) PEEK_HEIGHT_AUTO else _peekHeight - set(value) { - var needLayout = false - if (value == PEEK_HEIGHT_AUTO) { - if (!peekHeightAuto) { - peekHeightAuto = true - needLayout = true - } - } else if (peekHeightAuto || _peekHeight != value) { - peekHeightAuto = false - _peekHeight = max(0, value) - collapsedOffset = parentHeight - value - needLayout = true - } - if (needLayout && (state == STATE_COLLAPSED || state == STATE_HIDDEN)) { - viewRef?.get()?.requestLayout() - } - } - - /** Whether the bottom sheet can be hidden. */ - var isHideable = false - set(value) { - if (field != value) { - field = value - if (!value && state == STATE_HIDDEN) { - // Fix invalid state by moving to collapsed - state = STATE_COLLAPSED - } - } - } - - /** Whether the bottom sheet can be dragged or not. */ - private var isDraggable = true - - /** Whether the bottom sheet should skip collapsed state after being expanded once. */ - private var skipCollapsed = false - - /** Whether animations should be disabled, to be used from UI tests. */ - @VisibleForTesting - var isAnimationDisabled = false - - /** Whether or not to use automatic peek height */ - private var peekHeightAuto = false - /** Minimum peek height allowed */ - private var peekHeightMin = 0 - /** The last peek height calculated in onLayoutChild */ - private var lastPeekHeight = 0 - - private var parentHeight = 0 - /** Bottom sheet's top offset in [STATE_EXPANDED] state. */ - private var fitToContentsOffset = 0 - /** Bottom sheet's top offset in [STATE_HALF_EXPANDED] state. */ - private var halfExpandedOffset = 0 - /** Bottom sheet's top offset in [STATE_COLLAPSED] state. */ - private var collapsedOffset = 0 - - /** Keeps reference to the bottom sheet outside of Behavior callbacks */ - private var viewRef: WeakReference? = null - /** Controls movement of the bottom sheet */ - private lateinit var dragHelper: ViewDragHelper - - // Touch event handling, etc - private var lastTouchX = 0 - private var lastTouchY = 0 - private var initialTouchY = 0 - private var activePointerId = MotionEvent.INVALID_POINTER_ID - private var acceptTouches = true - - private var minimumVelocity = 0 - private var maximumVelocity = 0 - private var velocityTracker: VelocityTracker? = null - - private var nestedScrolled = false - private var nestedScrollingChildRef: WeakReference? = null - - private val callbacks: MutableSet = mutableSetOf() - - constructor() : super() - - @SuppressLint("PrivateResource") - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { - // Re-use BottomSheetBehavior's attrs - val a = context.obtainStyledAttributes(attrs, com.google.android.material.R.styleable.BottomSheetBehavior_Layout) - val value = a.peekValue(com.google.android.material.R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight) - peekHeight = if (value != null && value.data == PEEK_HEIGHT_AUTO) { - value.data - } else { - a.getDimensionPixelSize( - com.google.android.material.R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight, - PEEK_HEIGHT_AUTO - ) - } - isHideable = a.getBoolean(com.google.android.material.R.styleable.BottomSheetBehavior_Layout_behavior_hideable, false) - isFitToContents = - a.getBoolean(com.google.android.material.R.styleable.BottomSheetBehavior_Layout_behavior_fitToContents, true) - skipCollapsed = - a.getBoolean(com.google.android.material.R.styleable.BottomSheetBehavior_Layout_behavior_skipCollapsed, false) - a.recycle() - val configuration = ViewConfiguration.get(context) - minimumVelocity = configuration.scaledMinimumFlingVelocity - maximumVelocity = configuration.scaledMaximumFlingVelocity - } - - override fun onSaveInstanceState(parent: CoordinatorLayout, child: V): Parcelable { - return SavedState( - super.onSaveInstanceState(parent, child) ?: Bundle.EMPTY, - state, - peekHeight, - isFitToContents, - isHideable, - skipCollapsed, - isDraggable - ) - } - - override fun onRestoreInstanceState(parent: CoordinatorLayout, child: V, state: Parcelable) { - val ss = state as SavedState - super.onRestoreInstanceState(parent, child, ss.superState ?: Bundle.EMPTY) - - isDraggable = ss.isDraggable - peekHeight = ss.peekHeight - isFitToContents = ss.isFitToContents - isHideable = ss.isHideable - skipCollapsed = ss.skipCollapsed - - // Set state last. Intermediate states are restored as collapsed state. - _state = if (ss.state == STATE_DRAGGING || ss.state == STATE_SETTLING) { - STATE_COLLAPSED - } else { - ss.state - } - } - - fun addBottomSheetCallback(callback: BottomSheetCallback) { - callbacks.add(callback) - } - - fun removeBottomSheetCallback(callback: BottomSheetCallback) { - callbacks.remove(callback) - } - - private fun setStateInternal(@State state: Int) { - if (_state != state) { - _state = state - viewRef?.get()?.let { view -> - callbacks.forEach { callback -> - callback.onStateChanged(view, state) - } - } - } - } - - // -- Layout - - override fun onLayoutChild( - parent: CoordinatorLayout, - child: V, - layoutDirection: Int - ): Boolean { - if (parent.fitsSystemWindows && !child.fitsSystemWindows) { - child.fitsSystemWindows = true - } - val savedTop = child.top - // First let the parent lay it out - parent.onLayoutChild(child, layoutDirection) - parentHeight = parent.height - - // Calculate peek and offsets - if (peekHeightAuto) { - if (peekHeightMin == 0) { - // init peekHeightMin - @SuppressLint("PrivateResource") - peekHeightMin = parent.resources.getDimensionPixelSize( - com.google.android.material.R.dimen.design_bottom_sheet_peek_height_min - ) - } - lastPeekHeight = max(peekHeightMin, parentHeight - parent.width * 9 / 16) - } else { - lastPeekHeight = _peekHeight - } - fitToContentsOffset = max(0, parentHeight - child.height) - halfExpandedOffset = parentHeight / 2 - collapsedOffset = calculateCollapsedOffset() - - // Offset the bottom sheet - when (state) { - STATE_EXPANDED -> ViewCompat.offsetTopAndBottom(child, getExpandedOffset()) - STATE_HALF_EXPANDED -> ViewCompat.offsetTopAndBottom(child, halfExpandedOffset) - STATE_HIDDEN -> ViewCompat.offsetTopAndBottom(child, parentHeight) - STATE_COLLAPSED -> ViewCompat.offsetTopAndBottom(child, collapsedOffset) - STATE_DRAGGING, STATE_SETTLING -> ViewCompat.offsetTopAndBottom( - child, - savedTop - child.top - ) - } - - // Init these for later - viewRef = WeakReference(child) - if (!::dragHelper.isInitialized) { - dragHelper = ViewDragHelper.create(parent, dragCallback) - } - return true - } - - private fun calculateCollapsedOffset(): Int { - return if (isFitToContents) { - max(parentHeight - lastPeekHeight, fitToContentsOffset) - } else { - parentHeight - lastPeekHeight - } - } - - private fun getExpandedOffset() = if (isFitToContents) fitToContentsOffset else 0 - - // -- Touch events and scrolling - - override fun onInterceptTouchEvent( - parent: CoordinatorLayout, - child: V, - event: MotionEvent - ): Boolean { - if (!isDraggable || !child.isShown) { - acceptTouches = false - return false - } - - val action = event.actionMasked - lastTouchX = event.x.toInt() - lastTouchY = event.y.toInt() - - // Record velocity - if (action == MotionEvent.ACTION_DOWN) { - resetVelocityTracker() - } - if (velocityTracker == null) { - velocityTracker = VelocityTracker.obtain() - } - velocityTracker?.addMovement(event) - - when (action) { - MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { - activePointerId = MotionEvent.INVALID_POINTER_ID - if (!acceptTouches) { - acceptTouches = true - return false - } - } - - MotionEvent.ACTION_DOWN -> { - activePointerId = event.getPointerId(event.actionIndex) - initialTouchY = event.y.toInt() - - clearNestedScroll() - - if (!parent.isPointInChildBounds(child, lastTouchX, initialTouchY)) { - // Not touching the sheet - acceptTouches = false - } - } - } - - return acceptTouches && - // CoordinatorLayout can call us before the view is laid out. >_< - ::dragHelper.isInitialized && - dragHelper.shouldInterceptTouchEvent(event) - } - - override fun onTouchEvent( - parent: CoordinatorLayout, - child: V, - event: MotionEvent - ): Boolean { - if (!isDraggable || !child.isShown) { - return false - } - - val action = event.actionMasked - if (action == MotionEvent.ACTION_DOWN && state == STATE_DRAGGING) { - return true - } - - lastTouchX = event.x.toInt() - lastTouchY = event.y.toInt() - - // Record velocity - if (action == MotionEvent.ACTION_DOWN) { - resetVelocityTracker() - } - if (velocityTracker == null) { - velocityTracker = VelocityTracker.obtain() - } - velocityTracker?.addMovement(event) - - // CoordinatorLayout can call us before the view is laid out. >_< - if (::dragHelper.isInitialized) { - dragHelper.processTouchEvent(event) - } - - if (acceptTouches && - action == MotionEvent.ACTION_MOVE && - exceedsTouchSlop(initialTouchY, lastTouchY) - ) { - // Manually capture the sheet since nothing beneath us is scrolling. - dragHelper.captureChildView(child, event.getPointerId(event.actionIndex)) - } - - return acceptTouches - } - - private fun resetVelocityTracker() { - activePointerId = MotionEvent.INVALID_POINTER_ID - velocityTracker?.recycle() - velocityTracker = null - } - - private fun exceedsTouchSlop(p1: Int, p2: Int) = abs(p1 - p2) >= dragHelper.touchSlop - - // Nested scrolling - - override fun onStartNestedScroll( - coordinatorLayout: CoordinatorLayout, - child: V, - directTargetChild: View, - target: View, - axes: Int, - type: Int - ): Boolean { - nestedScrolled = false - if (isDraggable && - viewRef?.get() == directTargetChild && - (axes and ViewCompat.SCROLL_AXIS_VERTICAL) != 0 - ) { - // Scrolling view is a descendent of the sheet and scrolling vertically. - // Let's follow along! - nestedScrollingChildRef = WeakReference(target) - return true - } - return false - } - - override fun onNestedPreScroll( - coordinatorLayout: CoordinatorLayout, - child: V, - target: View, - dx: Int, - dy: Int, - consumed: IntArray, - type: Int - ) { - if (type == ViewCompat.TYPE_NON_TOUCH) { - return // Ignore fling here - } - if (target != nestedScrollingChildRef?.get()) { - return - } - - val currentTop = child.top - val newTop = currentTop - dy - if (dy > 0) { // Upward - if (newTop < getExpandedOffset()) { - consumed[1] = currentTop - getExpandedOffset() - ViewCompat.offsetTopAndBottom(child, -consumed[1]) - setStateInternal(STATE_EXPANDED) - } else { - consumed[1] = dy - ViewCompat.offsetTopAndBottom(child, -dy) - setStateInternal(STATE_DRAGGING) - } - } else if (dy < 0) { // Downward - if (!target.canScrollVertically(-1)) { - if (newTop <= collapsedOffset || isHideable) { - consumed[1] = dy - ViewCompat.offsetTopAndBottom(child, -dy) - setStateInternal(STATE_DRAGGING) - } else { - consumed[1] = currentTop - collapsedOffset - ViewCompat.offsetTopAndBottom(child, -consumed[1]) - setStateInternal(STATE_COLLAPSED) - } - } - } - dispatchOnSlide(child.top) - nestedScrolled = true - } - - override fun onStopNestedScroll( - coordinatorLayout: CoordinatorLayout, - child: V, - target: View, - type: Int - ) { - if (child.top == getExpandedOffset()) { - setStateInternal(STATE_EXPANDED) - return - } - if (target != nestedScrollingChildRef?.get() || !nestedScrolled) { - return - } - - settleBottomSheet(child, getYVelocity(), true) - clearNestedScroll() - } - - override fun onNestedPreFling( - coordinatorLayout: CoordinatorLayout, - child: V, - target: View, - velocityX: Float, - velocityY: Float - ): Boolean { - return isDraggable && - target == nestedScrollingChildRef?.get() && - ( - state != STATE_EXPANDED || super.onNestedPreFling( - coordinatorLayout, - child, - target, - velocityX, - velocityY - ) - ) - } - - private fun clearNestedScroll() { - nestedScrolled = false - nestedScrollingChildRef = null - } - - // Settling - - private fun getYVelocity(): Float { - return velocityTracker?.run { - computeCurrentVelocity(1000, maximumVelocity.toFloat()) - getYVelocity(activePointerId) - } ?: 0f - } - - private fun settleBottomSheet(sheet: View, yVelocity: Float, isNestedScroll: Boolean) { - val top: Int - @State val targetState: Int - - val flinging = yVelocity.absoluteValue > minimumVelocity - if (flinging && yVelocity < 0) { // Moving up - if (isFitToContents) { - top = fitToContentsOffset - targetState = STATE_EXPANDED - } else { - if (sheet.top > halfExpandedOffset) { - top = halfExpandedOffset - targetState = STATE_HALF_EXPANDED - } else { - top = 0 - targetState = STATE_EXPANDED - } - } - } else if (isHideable && shouldHide(sheet, yVelocity)) { - top = parentHeight - targetState = STATE_HIDDEN - } else if (flinging && yVelocity > 0) { // Moving down - top = collapsedOffset - targetState = STATE_COLLAPSED - } else { - val currentTop = sheet.top - if (isFitToContents) { - if (abs(currentTop - fitToContentsOffset) - < abs(currentTop - collapsedOffset) - ) { - top = fitToContentsOffset - targetState = STATE_EXPANDED - } else { - top = collapsedOffset - targetState = STATE_COLLAPSED - } - } else { - if (currentTop < halfExpandedOffset) { - if (currentTop < abs(currentTop - collapsedOffset)) { - top = 0 - targetState = STATE_EXPANDED - } else { - top = halfExpandedOffset - targetState = STATE_HALF_EXPANDED - } - } else { - if (abs(currentTop - halfExpandedOffset) - < abs(currentTop - collapsedOffset) - ) { - top = halfExpandedOffset - targetState = STATE_HALF_EXPANDED - } else { - top = collapsedOffset - targetState = STATE_COLLAPSED - } - } - } - } - - val startedSettling = if (isNestedScroll) { - dragHelper.smoothSlideViewTo(sheet, sheet.left, top) - } else { - dragHelper.settleCapturedViewAt(sheet.left, top) - } - - if (startedSettling) { - setStateInternal(STATE_SETTLING) - ViewCompat.postOnAnimation(sheet, SettleRunnable(sheet, targetState)) - } else { - setStateInternal(targetState) - } - } - - private fun shouldHide(child: View, yVelocity: Float): Boolean { - if (skipCollapsed) { - return true - } - if (child.top < collapsedOffset) { - return false // it should not hide, but collapse. - } - val newTop = child.top + yVelocity * HIDE_FRICTION - return abs(newTop - collapsedOffset) / _peekHeight.toFloat() > HIDE_THRESHOLD - } - - private fun startSettlingAnimation(child: View, state: Int) { - var top: Int - var finalState = state - - when { - state == STATE_COLLAPSED -> top = collapsedOffset - state == STATE_EXPANDED -> top = getExpandedOffset() - state == STATE_HALF_EXPANDED -> { - top = halfExpandedOffset - // Skip to expanded state if we would scroll past the height of the contents. - if (isFitToContents && top <= fitToContentsOffset) { - finalState = STATE_EXPANDED - top = fitToContentsOffset - } - } - state == STATE_HIDDEN && isHideable -> top = parentHeight - else -> throw IllegalArgumentException("Invalid state: $state") - } - - if (isAnimationDisabled) { - // Prevent animations - ViewCompat.offsetTopAndBottom(child, top - child.top) - } - - if (dragHelper.smoothSlideViewTo(child, child.left, top)) { - setStateInternal(STATE_SETTLING) - ViewCompat.postOnAnimation(child, SettleRunnable(child, finalState)) - } else { - setStateInternal(finalState) - } - } - - private fun dispatchOnSlide(top: Int) { - viewRef?.get()?.let { sheet -> - val denom = if (top > collapsedOffset) { - parentHeight - collapsedOffset - } else { - collapsedOffset - getExpandedOffset() - } - callbacks.forEach { callback -> - callback.onSlide(sheet, (collapsedOffset - top).toFloat() / denom) - } - } - } - - private inner class SettleRunnable( - private val view: View, - @State private val state: Int - ) : Runnable { - override fun run() { - if (dragHelper.continueSettling(true)) { - view.postOnAnimation(this) - } else { - setStateInternal(state) - } - } - } - - private val dragCallback: ViewDragHelper.Callback = object : ViewDragHelper.Callback() { - - override fun tryCaptureView(child: View, pointerId: Int): Boolean { - when { - // Sanity check - state == STATE_DRAGGING -> return false - // recapture a settling sheet - dragHelper.viewDragState == ViewDragHelper.STATE_SETTLING -> return true - // let nested scroll handle this - nestedScrollingChildRef?.get() != null -> return false - } - - val dy = lastTouchY - initialTouchY - if (dy == 0) { - // ViewDragHelper tries to capture in onTouch for the ACTION_DOWN event, but there's - // really no way to check for a scrolling child without a direction, so wait. - return false - } - - if (state == STATE_COLLAPSED) { - if (isHideable) { - // Any drag should capture in order to expand or hide the sheet - return true - } - if (dy < 0) { - // Expand on upward movement, even if there's scrolling content underneath - return true - } - } - - // Check for scrolling content underneath the touch point that can scroll in the - // appropriate direction. - val scrollingChild = findScrollingChildUnder(child, lastTouchX, lastTouchY, -dy) - return scrollingChild == null - } - - private fun findScrollingChildUnder(view: View, x: Int, y: Int, direction: Int): View? { - if (view.visibility == View.VISIBLE && dragHelper.isViewUnder(view, x, y)) { - if (view.canScrollVertically(direction)) { - return view - } - if (view is ViewGroup) { - // TODO this doesn't account for elevation or child drawing order. - for (i in (view.childCount - 1) downTo 0) { - val child = view.getChildAt(i) - val found = - findScrollingChildUnder(child, x - child.left, y - child.top, direction) - if (found != null) { - return found - } - } - } - } - return null - } - - override fun getViewVerticalDragRange(child: View): Int { - return if (isHideable) parentHeight else collapsedOffset - } - - override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int { - val maxOffset = if (isHideable) parentHeight else collapsedOffset - return top.coerceIn(getExpandedOffset(), maxOffset) - } - - override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int) = child.left - - override fun onViewDragStateChanged(state: Int) { - if (state == ViewDragHelper.STATE_DRAGGING) { - setStateInternal(STATE_DRAGGING) - } - } - - override fun onViewPositionChanged(child: View, left: Int, top: Int, dx: Int, dy: Int) { - dispatchOnSlide(top) - } - - override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) { - settleBottomSheet(releasedChild, yvel, false) - } - } - - /** SavedState implementation */ - internal class SavedState : AbsSavedState { - - @State - internal val state: Int - internal val peekHeight: Int - internal val isFitToContents: Boolean - internal val isHideable: Boolean - internal val skipCollapsed: Boolean - internal val isDraggable: Boolean - - constructor(source: Parcel) : this(source, null) - - constructor(source: Parcel, loader: ClassLoader?) : super(source, loader) { - state = source.readInt() - peekHeight = source.readInt() - isFitToContents = source.readInt().asBoolean() - isHideable = source.readInt().asBoolean() - skipCollapsed = source.readInt().asBoolean() - isDraggable = source.readInt().asBoolean() - } - - constructor( - superState: Parcelable, - @State state: Int, - peekHeight: Int, - isFitToContents: Boolean, - isHideable: Boolean, - skipCollapsed: Boolean, - isDraggable: Boolean - ) : super(superState) { - this.state = state - this.peekHeight = peekHeight - this.isFitToContents = isFitToContents - this.isHideable = isHideable - this.skipCollapsed = skipCollapsed - this.isDraggable = isDraggable - } - - override fun writeToParcel(dest: Parcel, flags: Int) { - super.writeToParcel(dest, flags) - dest.apply { - writeInt(state) - writeInt(peekHeight) - writeInt(isFitToContents.asInt()) - writeInt(isHideable.asInt()) - writeInt(skipCollapsed.asInt()) - writeInt(isDraggable.asInt()) - } - } - - companion object { - @JvmField - val CREATOR: Parcelable.Creator = - object : Parcelable.ClassLoaderCreator { - override fun createFromParcel(source: Parcel): SavedState { - return SavedState(source, null) - } - - override fun createFromParcel(source: Parcel, loader: ClassLoader): SavedState { - return SavedState(source, loader) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.widget + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.VelocityTracker +import android.view.View +import android.view.ViewConfiguration +import android.view.ViewGroup +import androidx.annotation.IntDef +import androidx.annotation.VisibleForTesting +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior +import androidx.core.view.ViewCompat +import androidx.customview.view.AbsSavedState +import androidx.customview.widget.ViewDragHelper +import com.forcetower.uefs.R +import com.forcetower.uefs.feature.shared.extensions.asBoolean +import com.forcetower.uefs.feature.shared.extensions.asInt +import java.lang.ref.WeakReference +import kotlin.math.abs +import kotlin.math.absoluteValue +import kotlin.math.max + +/** + * Copy of material lib's BottomSheetBehavior that includes some bug fixes. + */ +// TODO remove when a fixed version in material lib is released. +class BottomSheetBehavior : Behavior { + + companion object { + /** The bottom sheet is dragging. */ + const val STATE_DRAGGING = 1 + + /** The bottom sheet is settling. */ + const val STATE_SETTLING = 2 + + /** The bottom sheet is expanded. */ + const val STATE_EXPANDED = 3 + + /** The bottom sheet is collapsed. */ + const val STATE_COLLAPSED = 4 + + /** The bottom sheet is hidden. */ + const val STATE_HIDDEN = 5 + + /** The bottom sheet is half-expanded (used when behavior_fitToContents is false). */ + const val STATE_HALF_EXPANDED = 6 + + /** + * Peek at the 16:9 ratio keyline of its parent. This can be used as a parameter for + * [setPeekHeight(Int)]. [getPeekHeight()] will return this when the value is set. + */ + const val PEEK_HEIGHT_AUTO = -1 + + private const val HIDE_THRESHOLD = 0.5f + private const val HIDE_FRICTION = 0.1f + + @IntDef( + value = [ + STATE_DRAGGING, + STATE_SETTLING, + STATE_EXPANDED, + STATE_COLLAPSED, + STATE_HIDDEN, + STATE_HALF_EXPANDED + ] + ) + @Retention(AnnotationRetention.SOURCE) + annotation class State + + /** Utility to get the [BottomSheetBehavior] from a [view]. */ + @JvmStatic + fun from(view: View): BottomSheetBehavior<*> { + val lp = view.layoutParams as? CoordinatorLayout.LayoutParams + ?: throw IllegalArgumentException("view is not a child of CoordinatorLayout") + return lp.behavior as? BottomSheetBehavior + ?: throw IllegalArgumentException("view not associated with this behavior") + } + } + + /** Callback for monitoring events about bottom sheets. */ + interface BottomSheetCallback { + /** + * Called when the bottom sheet changes its state. + * + * @param bottomSheet The bottom sheet view. + * @param newState The new state. This will be one of link [STATE_DRAGGING], + * [STATE_SETTLING], [STATE_EXPANDED], [STATE_COLLAPSED], [STATE_HIDDEN], or + * [STATE_HALF_EXPANDED]. + */ + fun onStateChanged(bottomSheet: View, newState: Int) {} + + /** + * Called when the bottom sheet is being dragged. + * + * @param bottomSheet The bottom sheet view. + * @param slideOffset The new offset of this bottom sheet within [-1,1] range. Offset + * increases as this bottom sheet is moving upward. From 0 to 1 the sheet is between + * collapsed and expanded states and from -1 to 0 it is between hidden and collapsed states. + */ + fun onSlide(bottomSheet: View, slideOffset: Float) {} + } + + /** The current state of the bottom sheet, backing property */ + private var _state = STATE_COLLAPSED + + /** The current state of the bottom sheet */ + @State + var state + get() = _state + set(@State value) { + if (_state == value) { + return + } + if (viewRef == null) { + // Child is not laid out yet. Set our state and let onLayoutChild() handle it later. + if (value == STATE_COLLAPSED || + value == STATE_EXPANDED || + value == STATE_HALF_EXPANDED || + (isHideable && value == STATE_HIDDEN) + ) { + _state = value + } + return + } + + viewRef?.get()?.apply { + // Start the animation; wait until a pending layout if there is one. + if (parent != null && parent.isLayoutRequested && isAttachedToWindow) { + post { + startSettlingAnimation(this, value) + } + } else { + startSettlingAnimation(this, value) + } + } + } + + /** Whether to fit to contents. If false, the behavior will include [STATE_HALF_EXPANDED]. */ + private var isFitToContents = true + set(value) { + if (field != value) { + field = value + // If sheet is already laid out, recalculate the collapsed offset. + // Otherwise onLayoutChild will handle this later. + if (viewRef != null) { + collapsedOffset = calculateCollapsedOffset() + } + // Fix incorrect expanded settings. + setStateInternal( + if (field && state == STATE_HALF_EXPANDED) STATE_EXPANDED else state + ) + } + } + + /** Real peek height in pixels */ + private var _peekHeight = 0 + + /** Peek height in pixels, or [PEEK_HEIGHT_AUTO] */ + private var peekHeight + get() = if (peekHeightAuto) PEEK_HEIGHT_AUTO else _peekHeight + set(value) { + var needLayout = false + if (value == PEEK_HEIGHT_AUTO) { + if (!peekHeightAuto) { + peekHeightAuto = true + needLayout = true + } + } else if (peekHeightAuto || _peekHeight != value) { + peekHeightAuto = false + _peekHeight = max(0, value) + collapsedOffset = parentHeight - value + needLayout = true + } + if (needLayout && (state == STATE_COLLAPSED || state == STATE_HIDDEN)) { + viewRef?.get()?.requestLayout() + } + } + + /** Whether the bottom sheet can be hidden. */ + var isHideable = false + set(value) { + if (field != value) { + field = value + if (!value && state == STATE_HIDDEN) { + // Fix invalid state by moving to collapsed + state = STATE_COLLAPSED + } + } + } + + /** Whether the bottom sheet can be dragged or not. */ + private var isDraggable = true + + /** Whether the bottom sheet should skip collapsed state after being expanded once. */ + private var skipCollapsed = false + + /** Whether animations should be disabled, to be used from UI tests. */ + @VisibleForTesting + var isAnimationDisabled = false + + /** Whether or not to use automatic peek height */ + private var peekHeightAuto = false + + /** Minimum peek height allowed */ + private var peekHeightMin = 0 + + /** The last peek height calculated in onLayoutChild */ + private var lastPeekHeight = 0 + + private var parentHeight = 0 + + /** Bottom sheet's top offset in [STATE_EXPANDED] state. */ + private var fitToContentsOffset = 0 + + /** Bottom sheet's top offset in [STATE_HALF_EXPANDED] state. */ + private var halfExpandedOffset = 0 + + /** Bottom sheet's top offset in [STATE_COLLAPSED] state. */ + private var collapsedOffset = 0 + + /** Keeps reference to the bottom sheet outside of Behavior callbacks */ + private var viewRef: WeakReference? = null + + /** Controls movement of the bottom sheet */ + private lateinit var dragHelper: ViewDragHelper + + // Touch event handling, etc + private var lastTouchX = 0 + private var lastTouchY = 0 + private var initialTouchY = 0 + private var activePointerId = MotionEvent.INVALID_POINTER_ID + private var acceptTouches = true + + private var minimumVelocity = 0 + private var maximumVelocity = 0 + private var velocityTracker: VelocityTracker? = null + + private var nestedScrolled = false + private var nestedScrollingChildRef: WeakReference? = null + + private val callbacks: MutableSet = mutableSetOf() + + constructor() : super() + + @SuppressLint("PrivateResource") + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + // Re-use BottomSheetBehavior's attrs + val a = context.obtainStyledAttributes(attrs, com.google.android.material.R.styleable.BottomSheetBehavior_Layout) + val value = a.peekValue(com.google.android.material.R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight) + peekHeight = if (value != null && value.data == PEEK_HEIGHT_AUTO) { + value.data + } else { + a.getDimensionPixelSize( + com.google.android.material.R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight, + PEEK_HEIGHT_AUTO + ) + } + isHideable = a.getBoolean(com.google.android.material.R.styleable.BottomSheetBehavior_Layout_behavior_hideable, false) + isFitToContents = + a.getBoolean(com.google.android.material.R.styleable.BottomSheetBehavior_Layout_behavior_fitToContents, true) + skipCollapsed = + a.getBoolean(com.google.android.material.R.styleable.BottomSheetBehavior_Layout_behavior_skipCollapsed, false) + a.recycle() + val configuration = ViewConfiguration.get(context) + minimumVelocity = configuration.scaledMinimumFlingVelocity + maximumVelocity = configuration.scaledMaximumFlingVelocity + } + + override fun onSaveInstanceState(parent: CoordinatorLayout, child: V): Parcelable { + return SavedState( + super.onSaveInstanceState(parent, child) ?: Bundle.EMPTY, + state, + peekHeight, + isFitToContents, + isHideable, + skipCollapsed, + isDraggable + ) + } + + override fun onRestoreInstanceState(parent: CoordinatorLayout, child: V, state: Parcelable) { + val ss = state as SavedState + super.onRestoreInstanceState(parent, child, ss.superState ?: Bundle.EMPTY) + + isDraggable = ss.isDraggable + peekHeight = ss.peekHeight + isFitToContents = ss.isFitToContents + isHideable = ss.isHideable + skipCollapsed = ss.skipCollapsed + + // Set state last. Intermediate states are restored as collapsed state. + _state = if (ss.state == STATE_DRAGGING || ss.state == STATE_SETTLING) { + STATE_COLLAPSED + } else { + ss.state + } + } + + fun addBottomSheetCallback(callback: BottomSheetCallback) { + callbacks.add(callback) + } + + fun removeBottomSheetCallback(callback: BottomSheetCallback) { + callbacks.remove(callback) + } + + private fun setStateInternal(@State state: Int) { + if (_state != state) { + _state = state + viewRef?.get()?.let { view -> + callbacks.forEach { callback -> + callback.onStateChanged(view, state) + } + } + } + } + + // -- Layout + + override fun onLayoutChild( + parent: CoordinatorLayout, + child: V, + layoutDirection: Int + ): Boolean { + if (parent.fitsSystemWindows && !child.fitsSystemWindows) { + child.fitsSystemWindows = true + } + val savedTop = child.top + // First let the parent lay it out + parent.onLayoutChild(child, layoutDirection) + parentHeight = parent.height + + // Calculate peek and offsets + if (peekHeightAuto) { + if (peekHeightMin == 0) { + // init peekHeightMin + @SuppressLint("PrivateResource") + peekHeightMin = parent.resources.getDimensionPixelSize( + com.google.android.material.R.dimen.design_bottom_sheet_peek_height_min + ) + } + lastPeekHeight = max(peekHeightMin, parentHeight - parent.width * 9 / 16) + } else { + lastPeekHeight = _peekHeight + } + fitToContentsOffset = max(0, parentHeight - child.height) + halfExpandedOffset = parentHeight / 2 + collapsedOffset = calculateCollapsedOffset() + + // Offset the bottom sheet + when (state) { + STATE_EXPANDED -> ViewCompat.offsetTopAndBottom(child, getExpandedOffset()) + STATE_HALF_EXPANDED -> ViewCompat.offsetTopAndBottom(child, halfExpandedOffset) + STATE_HIDDEN -> ViewCompat.offsetTopAndBottom(child, parentHeight) + STATE_COLLAPSED -> ViewCompat.offsetTopAndBottom(child, collapsedOffset) + STATE_DRAGGING, STATE_SETTLING -> ViewCompat.offsetTopAndBottom( + child, + savedTop - child.top + ) + } + + // Init these for later + viewRef = WeakReference(child) + if (!::dragHelper.isInitialized) { + dragHelper = ViewDragHelper.create(parent, dragCallback) + } + return true + } + + private fun calculateCollapsedOffset(): Int { + return if (isFitToContents) { + max(parentHeight - lastPeekHeight, fitToContentsOffset) + } else { + parentHeight - lastPeekHeight + } + } + + private fun getExpandedOffset() = if (isFitToContents) fitToContentsOffset else 0 + + // -- Touch events and scrolling + + override fun onInterceptTouchEvent( + parent: CoordinatorLayout, + child: V, + event: MotionEvent + ): Boolean { + if (!isDraggable || !child.isShown) { + acceptTouches = false + return false + } + + val action = event.actionMasked + lastTouchX = event.x.toInt() + lastTouchY = event.y.toInt() + + // Record velocity + if (action == MotionEvent.ACTION_DOWN) { + resetVelocityTracker() + } + if (velocityTracker == null) { + velocityTracker = VelocityTracker.obtain() + } + velocityTracker?.addMovement(event) + + when (action) { + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + activePointerId = MotionEvent.INVALID_POINTER_ID + if (!acceptTouches) { + acceptTouches = true + return false + } + } + + MotionEvent.ACTION_DOWN -> { + activePointerId = event.getPointerId(event.actionIndex) + initialTouchY = event.y.toInt() + + clearNestedScroll() + + if (!parent.isPointInChildBounds(child, lastTouchX, initialTouchY)) { + // Not touching the sheet + acceptTouches = false + } + } + } + + return acceptTouches && + // CoordinatorLayout can call us before the view is laid out. >_< + ::dragHelper.isInitialized && + dragHelper.shouldInterceptTouchEvent(event) + } + + override fun onTouchEvent( + parent: CoordinatorLayout, + child: V, + event: MotionEvent + ): Boolean { + if (!isDraggable || !child.isShown) { + return false + } + + val action = event.actionMasked + if (action == MotionEvent.ACTION_DOWN && state == STATE_DRAGGING) { + return true + } + + lastTouchX = event.x.toInt() + lastTouchY = event.y.toInt() + + // Record velocity + if (action == MotionEvent.ACTION_DOWN) { + resetVelocityTracker() + } + if (velocityTracker == null) { + velocityTracker = VelocityTracker.obtain() + } + velocityTracker?.addMovement(event) + + // CoordinatorLayout can call us before the view is laid out. >_< + if (::dragHelper.isInitialized) { + dragHelper.processTouchEvent(event) + } + + if (acceptTouches && + action == MotionEvent.ACTION_MOVE && + exceedsTouchSlop(initialTouchY, lastTouchY) + ) { + // Manually capture the sheet since nothing beneath us is scrolling. + dragHelper.captureChildView(child, event.getPointerId(event.actionIndex)) + } + + return acceptTouches + } + + private fun resetVelocityTracker() { + activePointerId = MotionEvent.INVALID_POINTER_ID + velocityTracker?.recycle() + velocityTracker = null + } + + private fun exceedsTouchSlop(p1: Int, p2: Int) = abs(p1 - p2) >= dragHelper.touchSlop + + // Nested scrolling + + override fun onStartNestedScroll( + coordinatorLayout: CoordinatorLayout, + child: V, + directTargetChild: View, + target: View, + axes: Int, + type: Int + ): Boolean { + nestedScrolled = false + if (isDraggable && + viewRef?.get() == directTargetChild && + (axes and ViewCompat.SCROLL_AXIS_VERTICAL) != 0 + ) { + // Scrolling view is a descendent of the sheet and scrolling vertically. + // Let's follow along! + nestedScrollingChildRef = WeakReference(target) + return true + } + return false + } + + override fun onNestedPreScroll( + coordinatorLayout: CoordinatorLayout, + child: V, + target: View, + dx: Int, + dy: Int, + consumed: IntArray, + type: Int + ) { + if (type == ViewCompat.TYPE_NON_TOUCH) { + return // Ignore fling here + } + if (target != nestedScrollingChildRef?.get()) { + return + } + + val currentTop = child.top + val newTop = currentTop - dy + if (dy > 0) { // Upward + if (newTop < getExpandedOffset()) { + consumed[1] = currentTop - getExpandedOffset() + ViewCompat.offsetTopAndBottom(child, -consumed[1]) + setStateInternal(STATE_EXPANDED) + } else { + consumed[1] = dy + ViewCompat.offsetTopAndBottom(child, -dy) + setStateInternal(STATE_DRAGGING) + } + } else if (dy < 0) { // Downward + if (!target.canScrollVertically(-1)) { + if (newTop <= collapsedOffset || isHideable) { + consumed[1] = dy + ViewCompat.offsetTopAndBottom(child, -dy) + setStateInternal(STATE_DRAGGING) + } else { + consumed[1] = currentTop - collapsedOffset + ViewCompat.offsetTopAndBottom(child, -consumed[1]) + setStateInternal(STATE_COLLAPSED) + } + } + } + dispatchOnSlide(child.top) + nestedScrolled = true + } + + override fun onStopNestedScroll( + coordinatorLayout: CoordinatorLayout, + child: V, + target: View, + type: Int + ) { + if (child.top == getExpandedOffset()) { + setStateInternal(STATE_EXPANDED) + return + } + if (target != nestedScrollingChildRef?.get() || !nestedScrolled) { + return + } + + settleBottomSheet(child, getYVelocity(), true) + clearNestedScroll() + } + + override fun onNestedPreFling( + coordinatorLayout: CoordinatorLayout, + child: V, + target: View, + velocityX: Float, + velocityY: Float + ): Boolean { + return isDraggable && + target == nestedScrollingChildRef?.get() && + ( + state != STATE_EXPANDED || super.onNestedPreFling( + coordinatorLayout, + child, + target, + velocityX, + velocityY + ) + ) + } + + private fun clearNestedScroll() { + nestedScrolled = false + nestedScrollingChildRef = null + } + + // Settling + + private fun getYVelocity(): Float { + return velocityTracker?.run { + computeCurrentVelocity(1000, maximumVelocity.toFloat()) + getYVelocity(activePointerId) + } ?: 0f + } + + private fun settleBottomSheet(sheet: View, yVelocity: Float, isNestedScroll: Boolean) { + val top: Int + + @State val targetState: Int + + val flinging = yVelocity.absoluteValue > minimumVelocity + if (flinging && yVelocity < 0) { // Moving up + if (isFitToContents) { + top = fitToContentsOffset + targetState = STATE_EXPANDED + } else { + if (sheet.top > halfExpandedOffset) { + top = halfExpandedOffset + targetState = STATE_HALF_EXPANDED + } else { + top = 0 + targetState = STATE_EXPANDED + } + } + } else if (isHideable && shouldHide(sheet, yVelocity)) { + top = parentHeight + targetState = STATE_HIDDEN + } else if (flinging && yVelocity > 0) { // Moving down + top = collapsedOffset + targetState = STATE_COLLAPSED + } else { + val currentTop = sheet.top + if (isFitToContents) { + if (abs(currentTop - fitToContentsOffset) + < abs(currentTop - collapsedOffset) + ) { + top = fitToContentsOffset + targetState = STATE_EXPANDED + } else { + top = collapsedOffset + targetState = STATE_COLLAPSED + } + } else { + if (currentTop < halfExpandedOffset) { + if (currentTop < abs(currentTop - collapsedOffset)) { + top = 0 + targetState = STATE_EXPANDED + } else { + top = halfExpandedOffset + targetState = STATE_HALF_EXPANDED + } + } else { + if (abs(currentTop - halfExpandedOffset) + < abs(currentTop - collapsedOffset) + ) { + top = halfExpandedOffset + targetState = STATE_HALF_EXPANDED + } else { + top = collapsedOffset + targetState = STATE_COLLAPSED + } + } + } + } + + val startedSettling = if (isNestedScroll) { + dragHelper.smoothSlideViewTo(sheet, sheet.left, top) + } else { + dragHelper.settleCapturedViewAt(sheet.left, top) + } + + if (startedSettling) { + setStateInternal(STATE_SETTLING) + ViewCompat.postOnAnimation(sheet, SettleRunnable(sheet, targetState)) + } else { + setStateInternal(targetState) + } + } + + private fun shouldHide(child: View, yVelocity: Float): Boolean { + if (skipCollapsed) { + return true + } + if (child.top < collapsedOffset) { + return false // it should not hide, but collapse. + } + val newTop = child.top + yVelocity * HIDE_FRICTION + return abs(newTop - collapsedOffset) / _peekHeight.toFloat() > HIDE_THRESHOLD + } + + private fun startSettlingAnimation(child: View, state: Int) { + var top: Int + var finalState = state + + when { + state == STATE_COLLAPSED -> top = collapsedOffset + state == STATE_EXPANDED -> top = getExpandedOffset() + state == STATE_HALF_EXPANDED -> { + top = halfExpandedOffset + // Skip to expanded state if we would scroll past the height of the contents. + if (isFitToContents && top <= fitToContentsOffset) { + finalState = STATE_EXPANDED + top = fitToContentsOffset + } + } + state == STATE_HIDDEN && isHideable -> top = parentHeight + else -> throw IllegalArgumentException("Invalid state: $state") + } + + if (isAnimationDisabled) { + // Prevent animations + ViewCompat.offsetTopAndBottom(child, top - child.top) + } + + if (dragHelper.smoothSlideViewTo(child, child.left, top)) { + setStateInternal(STATE_SETTLING) + ViewCompat.postOnAnimation(child, SettleRunnable(child, finalState)) + } else { + setStateInternal(finalState) + } + } + + private fun dispatchOnSlide(top: Int) { + viewRef?.get()?.let { sheet -> + val denom = if (top > collapsedOffset) { + parentHeight - collapsedOffset + } else { + collapsedOffset - getExpandedOffset() + } + callbacks.forEach { callback -> + callback.onSlide(sheet, (collapsedOffset - top).toFloat() / denom) + } + } + } + + private inner class SettleRunnable( + private val view: View, + @State private val state: Int + ) : Runnable { + override fun run() { + if (dragHelper.continueSettling(true)) { + view.postOnAnimation(this) + } else { + setStateInternal(state) + } + } + } + + private val dragCallback: ViewDragHelper.Callback = object : ViewDragHelper.Callback() { + + override fun tryCaptureView(child: View, pointerId: Int): Boolean { + when { + // Sanity check + state == STATE_DRAGGING -> return false + // recapture a settling sheet + dragHelper.viewDragState == ViewDragHelper.STATE_SETTLING -> return true + // let nested scroll handle this + nestedScrollingChildRef?.get() != null -> return false + } + + val dy = lastTouchY - initialTouchY + if (dy == 0) { + // ViewDragHelper tries to capture in onTouch for the ACTION_DOWN event, but there's + // really no way to check for a scrolling child without a direction, so wait. + return false + } + + if (state == STATE_COLLAPSED) { + if (isHideable) { + // Any drag should capture in order to expand or hide the sheet + return true + } + if (dy < 0) { + // Expand on upward movement, even if there's scrolling content underneath + return true + } + } + + // Check for scrolling content underneath the touch point that can scroll in the + // appropriate direction. + val scrollingChild = findScrollingChildUnder(child, lastTouchX, lastTouchY, -dy) + return scrollingChild == null + } + + private fun findScrollingChildUnder(view: View, x: Int, y: Int, direction: Int): View? { + if (view.visibility == View.VISIBLE && dragHelper.isViewUnder(view, x, y)) { + if (view.canScrollVertically(direction)) { + return view + } + if (view is ViewGroup) { + // TODO this doesn't account for elevation or child drawing order. + for (i in (view.childCount - 1) downTo 0) { + val child = view.getChildAt(i) + val found = + findScrollingChildUnder(child, x - child.left, y - child.top, direction) + if (found != null) { + return found + } + } + } + } + return null + } + + override fun getViewVerticalDragRange(child: View): Int { + return if (isHideable) parentHeight else collapsedOffset + } + + override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int { + val maxOffset = if (isHideable) parentHeight else collapsedOffset + return top.coerceIn(getExpandedOffset(), maxOffset) + } + + override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int) = child.left + + override fun onViewDragStateChanged(state: Int) { + if (state == ViewDragHelper.STATE_DRAGGING) { + setStateInternal(STATE_DRAGGING) + } + } + + override fun onViewPositionChanged(child: View, left: Int, top: Int, dx: Int, dy: Int) { + dispatchOnSlide(top) + } + + override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) { + settleBottomSheet(releasedChild, yvel, false) + } + } + + /** SavedState implementation */ + internal class SavedState : AbsSavedState { + + @State + internal val state: Int + internal val peekHeight: Int + internal val isFitToContents: Boolean + internal val isHideable: Boolean + internal val skipCollapsed: Boolean + internal val isDraggable: Boolean + + constructor(source: Parcel) : this(source, null) + + constructor(source: Parcel, loader: ClassLoader?) : super(source, loader) { + state = source.readInt() + peekHeight = source.readInt() + isFitToContents = source.readInt().asBoolean() + isHideable = source.readInt().asBoolean() + skipCollapsed = source.readInt().asBoolean() + isDraggable = source.readInt().asBoolean() + } + + constructor( + superState: Parcelable, + @State state: Int, + peekHeight: Int, + isFitToContents: Boolean, + isHideable: Boolean, + skipCollapsed: Boolean, + isDraggable: Boolean + ) : super(superState) { + this.state = state + this.peekHeight = peekHeight + this.isFitToContents = isFitToContents + this.isHideable = isHideable + this.skipCollapsed = skipCollapsed + this.isDraggable = isDraggable + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + super.writeToParcel(dest, flags) + dest.apply { + writeInt(state) + writeInt(peekHeight) + writeInt(isFitToContents.asInt()) + writeInt(isHideable.asInt()) + writeInt(skipCollapsed.asInt()) + writeInt(isDraggable.asInt()) + } + } + + companion object { + @JvmField + val CREATOR: Parcelable.Creator = + object : Parcelable.ClassLoaderCreator { + override fun createFromParcel(source: Parcel): SavedState { + return SavedState(source, null) + } + + override fun createFromParcel(source: Parcel, loader: ClassLoader): SavedState { + return SavedState(source, loader) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/widget/CountdownView.kt b/app/src/main/java/com/forcetower/uefs/widget/CountdownView.kt index 66dc4102e..c81de64e6 100644 --- a/app/src/main/java/com/forcetower/uefs/widget/CountdownView.kt +++ b/app/src/main/java/com/forcetower/uefs/widget/CountdownView.kt @@ -1,138 +1,138 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.widget - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.view.postDelayed -import com.airbnb.lottie.LottieAnimationView -import com.forcetower.uefs.R -import com.forcetower.uefs.core.util.siecomp.TimeUtils -import timber.log.Timber -import java.time.Duration -import java.time.ZonedDateTime -import kotlin.properties.ObservableProperty -import kotlin.reflect.KProperty - -class CountdownView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : ConstraintLayout(context, attrs, defStyleAttr) { - private val root: View = LayoutInflater.from(context).inflate(R.layout.countdown, this, true) - private var days1 by AnimateDigitDelegate { root.findViewById(R.id.countdown_days_1) } - private var days2 by AnimateDigitDelegate { root.findViewById(R.id.countdown_days_2) } - private var hours1 by AnimateDigitDelegate { root.findViewById(R.id.countdown_hours_1) } - private var hours2 by AnimateDigitDelegate { root.findViewById(R.id.countdown_hours_2) } - private var mins1 by AnimateDigitDelegate { root.findViewById(R.id.countdown_mins_1) } - private var mins2 by AnimateDigitDelegate { root.findViewById(R.id.countdown_mins_2) } - private var secs1 by AnimateDigitDelegate { root.findViewById(R.id.countdown_secs_1) } - private var secs2 by AnimateDigitDelegate { root.findViewById(R.id.countdown_secs_2) } - - private val updateTime: Runnable = object : Runnable { - private val conferenceStart = TimeUtils.EventDays.first().start - - override fun run() { - var timeUntilConf = Duration.between(ZonedDateTime.now(), conferenceStart) - - if (timeUntilConf.isNegative) { - return - } - - val days = timeUntilConf.toDays() - days1 = (days / 10).toInt() - days2 = (days % 10).toInt() - timeUntilConf = timeUntilConf.minusDays(days) - - val hours = timeUntilConf.toHours() - hours1 = (hours / 10).toInt() - hours2 = (hours % 10).toInt() - timeUntilConf = timeUntilConf.minusHours(hours) - - val mins = timeUntilConf.toMinutes() - mins1 = (mins / 10).toInt() - mins2 = (mins % 10).toInt() - timeUntilConf = timeUntilConf.minusMinutes(mins) - - val secs = timeUntilConf.seconds - secs1 = (secs / 10).toInt() - secs2 = (secs % 10).toInt() - - handler?.postDelayed(this, 1_000L) // Run self every second - } - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - Timber.d("Starting countdown") - handler?.post(updateTime) - } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - Timber.d("Stopping countdown") - handler?.removeCallbacks(updateTime) - } - - /** - * A delegate who upon receiving a new value, runs animations on a view obtained from - * [viewProvider] - */ - private class AnimateDigitDelegate( - private val viewProvider: () -> LottieAnimationView - ) : ObservableProperty(-1) { - override fun afterChange(property: KProperty<*>, oldValue: Int, newValue: Int) { - // Sanity check, `newValue` should always be in range [0–9] - if (newValue < 0 || newValue > 9) { - Timber.e("Trying to animate to digit: $newValue") - return - } - - if (oldValue != newValue) { - val view = viewProvider() - if (oldValue != -1) { - // Animate out the prev digit i.e play the second half of it's comp - view.setAnimation("anim/$oldValue.json") - view.setMinAndMaxProgress(0.5f, 1f) - // Some issues scheduling & playing 2 * 500ms comps every 1s. Speed up the - // outward anim slightly to give us some headroom ¯\_(ツ)_/¯ - view.speed = 1.1f - view.playAnimation() - - view.postDelayed(500L) { - view.setAnimation("anim/$newValue.json") - view.setMinAndMaxProgress(0f, 0.5f) - view.speed = 1f - view.playAnimation() - } - } else { - // Initial show, just animate in the desired digit - view.setAnimation("anim/$newValue.json") - view.setMinAndMaxProgress(0f, 0.5f) - view.playAnimation() - } - } - } - } -} +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.postDelayed +import com.airbnb.lottie.LottieAnimationView +import com.forcetower.uefs.R +import com.forcetower.uefs.core.util.siecomp.TimeUtils +import java.time.Duration +import java.time.ZonedDateTime +import kotlin.properties.ObservableProperty +import kotlin.reflect.KProperty +import timber.log.Timber + +class CountdownView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + private val root: View = LayoutInflater.from(context).inflate(R.layout.countdown, this, true) + private var days1 by AnimateDigitDelegate { root.findViewById(R.id.countdown_days_1) } + private var days2 by AnimateDigitDelegate { root.findViewById(R.id.countdown_days_2) } + private var hours1 by AnimateDigitDelegate { root.findViewById(R.id.countdown_hours_1) } + private var hours2 by AnimateDigitDelegate { root.findViewById(R.id.countdown_hours_2) } + private var mins1 by AnimateDigitDelegate { root.findViewById(R.id.countdown_mins_1) } + private var mins2 by AnimateDigitDelegate { root.findViewById(R.id.countdown_mins_2) } + private var secs1 by AnimateDigitDelegate { root.findViewById(R.id.countdown_secs_1) } + private var secs2 by AnimateDigitDelegate { root.findViewById(R.id.countdown_secs_2) } + + private val updateTime: Runnable = object : Runnable { + private val conferenceStart = TimeUtils.EventDays.first().start + + override fun run() { + var timeUntilConf = Duration.between(ZonedDateTime.now(), conferenceStart) + + if (timeUntilConf.isNegative) { + return + } + + val days = timeUntilConf.toDays() + days1 = (days / 10).toInt() + days2 = (days % 10).toInt() + timeUntilConf = timeUntilConf.minusDays(days) + + val hours = timeUntilConf.toHours() + hours1 = (hours / 10).toInt() + hours2 = (hours % 10).toInt() + timeUntilConf = timeUntilConf.minusHours(hours) + + val mins = timeUntilConf.toMinutes() + mins1 = (mins / 10).toInt() + mins2 = (mins % 10).toInt() + timeUntilConf = timeUntilConf.minusMinutes(mins) + + val secs = timeUntilConf.seconds + secs1 = (secs / 10).toInt() + secs2 = (secs % 10).toInt() + + handler?.postDelayed(this, 1_000L) // Run self every second + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + Timber.d("Starting countdown") + handler?.post(updateTime) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + Timber.d("Stopping countdown") + handler?.removeCallbacks(updateTime) + } + + /** + * A delegate who upon receiving a new value, runs animations on a view obtained from + * [viewProvider] + */ + private class AnimateDigitDelegate( + private val viewProvider: () -> LottieAnimationView + ) : ObservableProperty(-1) { + override fun afterChange(property: KProperty<*>, oldValue: Int, newValue: Int) { + // Sanity check, `newValue` should always be in range [0–9] + if (newValue < 0 || newValue > 9) { + Timber.e("Trying to animate to digit: $newValue") + return + } + + if (oldValue != newValue) { + val view = viewProvider() + if (oldValue != -1) { + // Animate out the prev digit i.e play the second half of it's comp + view.setAnimation("anim/$oldValue.json") + view.setMinAndMaxProgress(0.5f, 1f) + // Some issues scheduling & playing 2 * 500ms comps every 1s. Speed up the + // outward anim slightly to give us some headroom ¯\_(ツ)_/¯ + view.speed = 1.1f + view.playAnimation() + + view.postDelayed(500L) { + view.setAnimation("anim/$newValue.json") + view.setMinAndMaxProgress(0f, 0.5f) + view.speed = 1f + view.playAnimation() + } + } else { + // Initial show, just animate in the desired digit + view.setAnimation("anim/$newValue.json") + view.setMinAndMaxProgress(0f, 0.5f) + view.playAnimation() + } + } + } + } +} diff --git a/app/src/main/java/com/forcetower/uefs/widget/InkPageIndicator.kt b/app/src/main/java/com/forcetower/uefs/widget/InkPageIndicator.kt index 8497f996c..cc4124a3d 100644 --- a/app/src/main/java/com/forcetower/uefs/widget/InkPageIndicator.kt +++ b/app/src/main/java/com/forcetower/uefs/widget/InkPageIndicator.kt @@ -268,8 +268,9 @@ class InkPageIndicator @JvmOverloads constructor( 0 } if (dotCenterX != null) { - if (currentPage < dotCenterX!!.size) + if (currentPage < dotCenterX!!.size) { selectedDotX = dotCenterX!![currentPage] + } } } @@ -284,7 +285,6 @@ class InkPageIndicator @JvmOverloads constructor( } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - val desiredHeight = desiredHeight val height: Int height = when (MeasureSpec.getMode(heightMeasureSpec)) { @@ -321,7 +321,6 @@ class InkPageIndicator @JvmOverloads constructor( } private fun drawUnselected(canvas: Canvas) { - combinedUnselectedPath.rewind() // draw any settled, revealing or joining dots @@ -373,14 +372,12 @@ class InkPageIndicator @JvmOverloads constructor( joiningFraction: Float, dotRevealFraction: Float ): Path { - unselectedDotPath.rewind() if ((joiningFraction == 0f || joiningFraction == INVALID_FRACTION) && dotRevealFraction == 0f && !(page == currentPage && selectedDotInPosition) ) { - // case #1 – At rest unselectedDotPath.addCircle(dotCenterX!![page], dotCenterY, dotRadius, Path.Direction.CW) } @@ -388,7 +385,6 @@ class InkPageIndicator @JvmOverloads constructor( if (joiningFraction > 0f && joiningFraction <= 0.5f && retreatingJoinX1 == INVALID_FRACTION ) { - // case #2 – Joining neighbour, still separate // start with the left dot @@ -482,7 +478,6 @@ class InkPageIndicator @JvmOverloads constructor( if (joiningFraction > 0.5f && joiningFraction < 1f && retreatingJoinX1 == INVALID_FRACTION ) { - // case #3 – Joining neighbour, combined curved // adjust the fraction so that it goes from 0.3 -> 1 to produce a more realistic 'join' @@ -564,7 +559,6 @@ class InkPageIndicator @JvmOverloads constructor( ) } if (joiningFraction == 1f && retreatingJoinX1 == INVALID_FRACTION) { - // case #4 Joining neighbour, combined straight technically we could use case 3 for this // situation as well but assume that this is an optimization rather than faffing around // with beziers just to draw a rounded rect @@ -577,7 +571,6 @@ class InkPageIndicator @JvmOverloads constructor( // multiple dots and therefore animate it's movement smoothly if (dotRevealFraction > MINIMAL_REVEAL) { - // case #6 – previously hidden dot revealing unselectedDotPath.addCircle( centerX, @@ -628,7 +621,6 @@ class InkPageIndicator @JvmOverloads constructor( now: Int, steps: Int ): ValueAnimator { - // create the actual move animator val moveSelected = ValueAnimator.ofFloat(selectedDotX, moveTo) @@ -637,10 +629,11 @@ class InkPageIndicator @JvmOverloads constructor( was, now, steps, - if (now > was) + if (now > was) { RightwardStartPredicate(moveTo - (moveTo - selectedDotX) * 0.25f) - else + } else { LeftwardStartPredicate(moveTo + (selectedDotX - moveTo) * 0.25f) + } ) retreatAnimation!!.addListener( object : AnimatorListenerAdapter() { @@ -734,17 +727,19 @@ class InkPageIndicator @JvmOverloads constructor( // work out the start/end values of the retreating join from the direction we're // travelling in. Also look at the current selected dot position, i.e. we're moving on // before a prior anim has finished. - val initialX1 = if (now > was) + val initialX1 = if (now > was) { min(dotCenterX!![was], selectedDotX) - dotRadius - else + } else { dotCenterX!![now] - dotRadius + } val finalX1 = dotCenterX!![now] - dotRadius - val initialX2 = if (now > was) + val initialX2 = if (now > was) { dotCenterX!![now] + dotRadius - else + } else { max(dotCenterX!![was], selectedDotX) + dotRadius + } val finalX2 = dotCenterX!![now] + dotRadius diff --git a/app/src/main/java/com/forcetower/uefs/widget/RatingBarVectorFix.kt b/app/src/main/java/com/forcetower/uefs/widget/RatingBarVectorFix.kt index 5630cdee8..0125e44d6 100644 --- a/app/src/main/java/com/forcetower/uefs/widget/RatingBarVectorFix.kt +++ b/app/src/main/java/com/forcetower/uefs/widget/RatingBarVectorFix.kt @@ -36,8 +36,8 @@ import android.graphics.drawable.shapes.Shape import android.util.AttributeSet import android.view.Gravity import android.view.View -import androidx.appcompat.widget.AppCompatRatingBar import androidx.appcompat.graphics.drawable.DrawableWrapperCompat as DrawableWrapper +import androidx.appcompat.widget.AppCompatRatingBar class RatingBarVectorFix @JvmOverloads constructor( context: Context, @@ -106,14 +106,15 @@ class RatingBarVectorFix @JvmOverloads constructor( ) shapeDrawable.paint.shader = bitmapShader shapeDrawable.paint.colorFilter = drawable.paint.colorFilter - return if (clip) + return if (clip) { ClipDrawable( shapeDrawable, Gravity.START, ClipDrawable.HORIZONTAL ) - else + } else { shapeDrawable + } } else { return tileify(getBitmapDrawableFromVectorDrawable(drawable), clip) } diff --git a/app/src/main/java/com/forcetower/uefs/widget/Scroller.kt b/app/src/main/java/com/forcetower/uefs/widget/Scroller.kt index 6596b8baa..5be8cf6d9 100644 --- a/app/src/main/java/com/forcetower/uefs/widget/Scroller.kt +++ b/app/src/main/java/com/forcetower/uefs/widget/Scroller.kt @@ -1,492 +1,498 @@ -/* - * This file is part of the UNES Open Source Project. - * UNES is licensed under the GNU GPLv3. - * - * Copyright (c) 2020. João Paulo Sena - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.forcetower.uefs.widget - -import android.content.Context -import android.hardware.SensorManager -import android.view.ViewConfiguration -import android.view.animation.AnimationUtils -import android.view.animation.Interpolator -import kotlin.math.abs -import kotlin.math.exp -import kotlin.math.ln -import kotlin.math.max -import kotlin.math.min -import kotlin.math.roundToInt -import kotlin.math.sign -import kotlin.math.sqrt - -/** - * This class encapsulates scrolling. The duration of the scroll - * can be passed in the constructor and specifies the maximum time that - * the scrolling animation should take. Past this time, the scrolling is - * automatically moved to its final stage and computeScrollOffset() - * will always return false to indicate that scrolling is over. - */ -class Scroller -/** - * Create a Scroller with the specified interpolator. If the interpolator is - * null, the default (viscous) interpolator will be used. Specify whether or - * not to support progressive "flywheel" behavior in flinging. - */ -@JvmOverloads constructor(context: Context, private val mInterpolator: Interpolator? = null, private val mFlywheel: Boolean = true) { - private var mMode: Int = 0 - - /** - * Returns the start X offset in the scroll. - * - * @return The start X offset as an absolute distance from the origin. - */ - private var startX: Int = 0 - /** - * Returns the start Y offset in the scroll. - * - * @return The start Y offset as an absolute distance from the origin. - */ - var startY: Int = 0 - private set - private var mFinalX: Int = 0 - private var mFinalY: Int = 0 - - private var mMinX: Int = 0 - private var mMaxX: Int = 0 - private var mMinY: Int = 0 - private var mMaxY: Int = 0 - - /** - * Returns the current X offset in the scroll. - * - * @return The new X offset as an absolute distance from the origin. - */ - private var currX: Int = 0 - /** - * Returns the current Y offset in the scroll. - * - * @return The new Y offset as an absolute distance from the origin. - */ - var currY: Int = 0 - private set - private var mStartTime: Long = 0 - /** - * Returns how long the scroll event will take, in milliseconds. - * - * @return The duration of the scroll in milliseconds. - */ - var duration: Int = 0 - private set - private var mDurationReciprocal: Float = 0.toFloat() - private var mDeltaX: Float = 0.toFloat() - private var mDeltaY: Float = 0.toFloat() - /** - * - * Returns whether the scroller has finished scrolling. - * - * @return True if the scroller has finished scrolling, false otherwise. - */ - var isFinished: Boolean = false - private set - - private var mVelocity: Float = 0.toFloat() - - private var mDeceleration: Float = 0.toFloat() - private val mPpi: Float - - /** - * Returns the current velocity. - * - * @return The original velocity less the deceleration. Result may be - * negative. - */ - private val currVelocity: Float - get() = mVelocity - mDeceleration * timePassed() / 2000.0f - - /** - * Returns where the scroll will end. Valid only for "fling" scrolls. - * - * @return The final X offset as an absolute distance from the origin. - */ - /** - * Sets the final position (X) for this scroller. - * - * @param newX The new X offset as an absolute distance from the origin. - * @see .extendDuration - * @see .setFinalY - */ - var finalX: Int - get() = mFinalX - set(newX) { - mFinalX = newX - mDeltaX = (mFinalX - startX).toFloat() - isFinished = false - } - - /** - * Returns where the scroll will end. Valid only for "fling" scrolls. - * - * @return The final Y offset as an absolute distance from the origin. - */ - /** - * Sets the final position (Y) for this scroller. - * - * @param newY The new Y offset as an absolute distance from the origin. - * @see .extendDuration - * @see .setFinalX - */ - var finalY: Int - get() = mFinalY - set(newY) { - mFinalY = newY - mDeltaY = (mFinalY - startY).toFloat() - isFinished = false - } - - init { - isFinished = true - mPpi = context.resources.displayMetrics.density * 160.0f - mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction()) - } - - /** - * The amount of friction applied to flings. The default value - * is [android.view.ViewConfiguration.getScrollFriction]. - * - * @param friction A scalar dimension-less value representing the coefficient of - * friction. - */ - fun setFriction(friction: Float) { - mDeceleration = computeDeceleration(friction) - } - - private fun computeDeceleration(friction: Float): Float { - return ( - SensorManager.GRAVITY_EARTH // g (m/s^2) - - * 39.37f * // inch/meter - - mPpi // pixels per inch - - * friction - ) - } - - /** - * Force the finished field to a particular value. - * - * @param finished The new finished value. - */ - fun forceFinished(finished: Boolean) { - isFinished = finished - } - - /** - * Call this when you want to know the new location. If it returns true, - * the animation is not yet finished. loc will be altered to provide the - * new location. - */ - fun computeScrollOffset(): Boolean { - if (isFinished) { - return false - } - - val timePassed = (AnimationUtils.currentAnimationTimeMillis() - mStartTime).toInt() - - if (timePassed < duration) { - when (mMode) { - SCROLL_MODE -> { - var x = timePassed * mDurationReciprocal - - x = if (mInterpolator == null) - viscousFluid(x) - else - mInterpolator.getInterpolation(x) - - currX = startX + (x * mDeltaX).roundToInt() - currY = startY + (x * mDeltaY).roundToInt() - } - FLING_MODE -> { - val t = timePassed.toFloat() / duration - val index = (NB_SAMPLES * t).toInt() - val t_inf = index.toFloat() / NB_SAMPLES - val t_sup = (index + 1).toFloat() / NB_SAMPLES - val d_inf = SPLINE[index] - val d_sup = SPLINE[index + 1] - val distanceCoef = d_inf + (t - t_inf) / (t_sup - t_inf) * (d_sup - d_inf) - - currX = startX + (distanceCoef * (mFinalX - startX)).roundToInt() - // Pin to mMinX <= mCurrX <= mMaxX - currX = min(currX, mMaxX) - currX = max(currX, mMinX) - - currY = startY + (distanceCoef * (mFinalY - startY)).roundToInt() - // Pin to mMinY <= mCurrY <= mMaxY - currY = min(currY, mMaxY) - currY = max(currY, mMinY) - - if (currX == mFinalX && currY == mFinalY) { - isFinished = true - } - } - } - } else { - currX = mFinalX - currY = mFinalY - isFinished = true - } - return true - } - - /** - * Start scrolling by providing a starting point and the distance to travel. - * - * @param startX Starting horizontal scroll offset in pixels. Positive - * numbers will scroll the content to the left. - * @param startY Starting vertical scroll offset in pixels. Positive numbers - * will scroll the content up. - * @param dx Horizontal distance to travel. Positive numbers will scroll the - * content to the left. - * @param dy Vertical distance to travel. Positive numbers will scroll the - * content up. - * @param duration Duration of the scroll in milliseconds. - */ - @JvmOverloads - fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int, duration: Int = DEFAULT_DURATION) { - mMode = SCROLL_MODE - isFinished = false - this.duration = duration - mStartTime = AnimationUtils.currentAnimationTimeMillis() - this.startX = startX - this.startY = startY - mFinalX = startX + dx - mFinalY = startY + dy - mDeltaX = dx.toFloat() - mDeltaY = dy.toFloat() - mDurationReciprocal = 1.0f / this.duration.toFloat() - } - - /** - * Start scrolling based on a fling gesture. The distance travelled will - * depend on the initial velocity of the fling. - * - * @param startX Starting point of the scroll (X) - * @param startY Starting point of the scroll (Y) - * @param velocityX Initial velocity of the fling (X) measured in pixels per - * second. - * @param velocityY Initial velocity of the fling (Y) measured in pixels per - * second - * @param minX Minimum X value. The scroller will not scroll past this - * point. - * @param maxX Maximum X value. The scroller will not scroll past this - * point. - * @param minY Minimum Y value. The scroller will not scroll past this - * point. - * @param maxY Maximum Y value. The scroller will not scroll past this - * point. - */ - fun fling( - startX: Int, - startY: Int, - tVelocityX: Int, - tVelocityY: Int, - minX: Int, - maxX: Int, - minY: Int, - maxY: Int - ) { - var velocityX = tVelocityX - var velocityY = tVelocityY - // Continue a scroll or fling in progress - if (mFlywheel && !isFinished) { - val oldVel = currVelocity - - val dx = (mFinalX - this.startX).toFloat() - val dy = (mFinalY - this.startY).toFloat() - val hyp = sqrt(dx * dx + dy * dy) - - val ndx = dx / hyp - val ndy = dy / hyp - - val oldVelocityX = ndx * oldVel - val oldVelocityY = ndy * oldVel - if (sign(velocityX.toFloat()) == sign(oldVelocityX) && sign(velocityY.toFloat()) == sign(oldVelocityY)) { - velocityX += oldVelocityX.toInt() - velocityY += oldVelocityY.toInt() - } - } - - mMode = FLING_MODE - isFinished = false - - val velocity = sqrt(velocityX * velocityX + velocityY * velocityY.toDouble()).toFloat() - - mVelocity = velocity - val l = ln((START_TENSION * velocity / ALPHA).toDouble()) - duration = (1000.0 * exp(l / (DECELERATION_RATE - 1.0))).toInt() - mStartTime = AnimationUtils.currentAnimationTimeMillis() - this.startX = startX - this.startY = startY - - val coeffX = if (velocity == 0f) 1.0f else velocityX / velocity - val coeffY = if (velocity == 0f) 1.0f else velocityY / velocity - - val totalDistance = (ALPHA * exp(DECELERATION_RATE / (DECELERATION_RATE - 1.0) * l)).toInt() - - mMinX = minX - mMaxX = maxX - mMinY = minY - mMaxY = maxY - - mFinalX = startX + (totalDistance * coeffX).roundToInt() - // Pin to mMinX <= mFinalX <= mMaxX - mFinalX = min(mFinalX, mMaxX) - mFinalX = max(mFinalX, mMinX) - - mFinalY = startY + (totalDistance * coeffY).roundToInt() - // Pin to mMinY <= mFinalY <= mMaxY - mFinalY = min(mFinalY, mMaxY) - mFinalY = max(mFinalY, mMinY) - } - - /** - * Stops the animation. Contrary to [.forceFinished], - * aborting the animating cause the scroller to move to the final x and y - * position - * - * @see .forceFinished - */ - fun abortAnimation() { - currX = mFinalX - currY = mFinalY - isFinished = true - } - - /** - * Extend the scroll animation. This allows a running animation to scroll - * further and longer, when used with [.setFinalX] or [.setFinalY]. - * - * @param extend Additional time to scroll in milliseconds. - * @see .setFinalX - * @see .setFinalY - */ - fun extendDuration(extend: Int) { - val passed = timePassed() - duration = passed + extend - mDurationReciprocal = 1.0f / duration - isFinished = false - } - - /** - * Returns the time elapsed since the beginning of the scrolling. - * - * @return The elapsed time in milliseconds. - */ - private fun timePassed(): Int { - return (AnimationUtils.currentAnimationTimeMillis() - mStartTime).toInt() - } - - /** - * @hide - */ - fun isScrollingInDirection(xvel: Float, yvel: Float): Boolean { - return !isFinished && sign(xvel) == sign((mFinalX - startX).toFloat()) && - sign(yvel) == sign((mFinalY - startY).toFloat()) - } - - companion object { - private var sViscousFluidScale: Float = 0.toFloat() - private var sViscousFluidNormalize: Float = 0.toFloat() - private const val DEFAULT_DURATION = 250 - private const val SCROLL_MODE = 0 - private const val FLING_MODE = 1 - - private val DECELERATION_RATE = (ln(0.75) / ln(0.9)).toFloat() - private const val ALPHA = 800f // pixels / seconds - private const val START_TENSION = 0.4f // Tension at start: (0.4 * total T, 1.0 * Distance) - private const val END_TENSION = 1.0f - START_TENSION - private const val NB_SAMPLES = 100 - private val SPLINE = FloatArray(NB_SAMPLES + 1) - - init { - var x_min = 0.0f - for (i in 0..NB_SAMPLES) { - val t = i.toFloat() / NB_SAMPLES - var x_max = 1.0f - var x: Float - var tx: Float - var coef: Float - while (true) { - x = x_min + (x_max - x_min) / 2.0f - coef = 3.0f * x * (1.0f - x) - tx = coef * ((1.0f - x) * START_TENSION + x * END_TENSION) + x * x * x - if (abs(tx - t) < 1E-5) break - if (tx > t) - x_max = x - else - x_min = x - } - val d = coef + x * x * x - SPLINE[i] = d - } - SPLINE[NB_SAMPLES] = 1.0f - - // This controls the viscous fluid effect (how much of it) - sViscousFluidScale = 8.0f - // must be set to 1.0 (used in viscousFluid()) - sViscousFluidNormalize = 1.0f - sViscousFluidNormalize = 1.0f / viscousFluid(1.0f) - } - - internal fun viscousFluid(tX: Float): Float { - var x = tX - x *= sViscousFluidScale - if (x < 1.0f) { - x -= 1.0f - exp((-x).toDouble()).toFloat() - } else { - val start = 0.36787944117f // 1/e == exp(-1) - x = 1.0f - exp((1.0f - x).toDouble()).toFloat() - x = start + x * (1.0f - start) - } - x *= sViscousFluidNormalize - return x - } - } -} -/** - * Create a Scroller with the default duration and interpolator. - */ -/** - * Create a Scroller with the specified interpolator. If the interpolator is - * null, the default (viscous) interpolator will be used. "Flywheel" behavior will - * be in effect for apps targeting Honeycomb or newer. - */ -/** - * Start scrolling by providing a starting point and the distance to travel. - * The scroll will use the default value of 250 milliseconds for the - * duration. - * - * @param startX Starting horizontal scroll offset in pixels. Positive - * numbers will scroll the content to the left. - * @param startY Starting vertical scroll offset in pixels. Positive numbers - * will scroll the content up. - * @param dx Horizontal distance to travel. Positive numbers will scroll the - * content to the left. - * @param dy Vertical distance to travel. Positive numbers will scroll the - * content up. - */ +/* + * This file is part of the UNES Open Source Project. + * UNES is licensed under the GNU GPLv3. + * + * Copyright (c) 2020. João Paulo Sena + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.forcetower.uefs.widget + +import android.content.Context +import android.hardware.SensorManager +import android.view.ViewConfiguration +import android.view.animation.AnimationUtils +import android.view.animation.Interpolator +import kotlin.math.abs +import kotlin.math.exp +import kotlin.math.ln +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt +import kotlin.math.sign +import kotlin.math.sqrt + +/** + * This class encapsulates scrolling. The duration of the scroll + * can be passed in the constructor and specifies the maximum time that + * the scrolling animation should take. Past this time, the scrolling is + * automatically moved to its final stage and computeScrollOffset() + * will always return false to indicate that scrolling is over. + */ +class Scroller +/** + * Create a Scroller with the specified interpolator. If the interpolator is + * null, the default (viscous) interpolator will be used. Specify whether or + * not to support progressive "flywheel" behavior in flinging. + */ +@JvmOverloads constructor(context: Context, private val mInterpolator: Interpolator? = null, private val mFlywheel: Boolean = true) { + private var mMode: Int = 0 + + /** + * Returns the start X offset in the scroll. + * + * @return The start X offset as an absolute distance from the origin. + */ + private var startX: Int = 0 + + /** + * Returns the start Y offset in the scroll. + * + * @return The start Y offset as an absolute distance from the origin. + */ + var startY: Int = 0 + private set + private var mFinalX: Int = 0 + private var mFinalY: Int = 0 + + private var mMinX: Int = 0 + private var mMaxX: Int = 0 + private var mMinY: Int = 0 + private var mMaxY: Int = 0 + + /** + * Returns the current X offset in the scroll. + * + * @return The new X offset as an absolute distance from the origin. + */ + private var currX: Int = 0 + + /** + * Returns the current Y offset in the scroll. + * + * @return The new Y offset as an absolute distance from the origin. + */ + var currY: Int = 0 + private set + private var mStartTime: Long = 0 + + /** + * Returns how long the scroll event will take, in milliseconds. + * + * @return The duration of the scroll in milliseconds. + */ + var duration: Int = 0 + private set + private var mDurationReciprocal: Float = 0.toFloat() + private var mDeltaX: Float = 0.toFloat() + private var mDeltaY: Float = 0.toFloat() + + /** + * + * Returns whether the scroller has finished scrolling. + * + * @return True if the scroller has finished scrolling, false otherwise. + */ + var isFinished: Boolean = false + private set + + private var mVelocity: Float = 0.toFloat() + + private var mDeceleration: Float = 0.toFloat() + private val mPpi: Float + + /** + * Returns the current velocity. + * + * @return The original velocity less the deceleration. Result may be + * negative. + */ + private val currVelocity: Float + get() = mVelocity - mDeceleration * timePassed() / 2000.0f + + /** + * Returns where the scroll will end. Valid only for "fling" scrolls. + * + * @return The final X offset as an absolute distance from the origin. + */ + /** + * Sets the final position (X) for this scroller. + * + * @param newX The new X offset as an absolute distance from the origin. + * @see .extendDuration + * @see .setFinalY + */ + var finalX: Int + get() = mFinalX + set(newX) { + mFinalX = newX + mDeltaX = (mFinalX - startX).toFloat() + isFinished = false + } + + /** + * Returns where the scroll will end. Valid only for "fling" scrolls. + * + * @return The final Y offset as an absolute distance from the origin. + */ + /** + * Sets the final position (Y) for this scroller. + * + * @param newY The new Y offset as an absolute distance from the origin. + * @see .extendDuration + * @see .setFinalX + */ + var finalY: Int + get() = mFinalY + set(newY) { + mFinalY = newY + mDeltaY = (mFinalY - startY).toFloat() + isFinished = false + } + + init { + isFinished = true + mPpi = context.resources.displayMetrics.density * 160.0f + mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction()) + } + + /** + * The amount of friction applied to flings. The default value + * is [android.view.ViewConfiguration.getScrollFriction]. + * + * @param friction A scalar dimension-less value representing the coefficient of + * friction. + */ + fun setFriction(friction: Float) { + mDeceleration = computeDeceleration(friction) + } + + private fun computeDeceleration(friction: Float): Float { + return ( + SensorManager.GRAVITY_EARTH // g (m/s^2) + + * 39.37f * // inch/meter + + mPpi // pixels per inch + + * friction + ) + } + + /** + * Force the finished field to a particular value. + * + * @param finished The new finished value. + */ + fun forceFinished(finished: Boolean) { + isFinished = finished + } + + /** + * Call this when you want to know the new location. If it returns true, + * the animation is not yet finished. loc will be altered to provide the + * new location. + */ + fun computeScrollOffset(): Boolean { + if (isFinished) { + return false + } + + val timePassed = (AnimationUtils.currentAnimationTimeMillis() - mStartTime).toInt() + + if (timePassed < duration) { + when (mMode) { + SCROLL_MODE -> { + var x = timePassed * mDurationReciprocal + + x = if (mInterpolator == null) { + viscousFluid(x) + } else { + mInterpolator.getInterpolation(x) + } + + currX = startX + (x * mDeltaX).roundToInt() + currY = startY + (x * mDeltaY).roundToInt() + } + FLING_MODE -> { + val t = timePassed.toFloat() / duration + val index = (NB_SAMPLES * t).toInt() + val t_inf = index.toFloat() / NB_SAMPLES + val t_sup = (index + 1).toFloat() / NB_SAMPLES + val d_inf = SPLINE[index] + val d_sup = SPLINE[index + 1] + val distanceCoef = d_inf + (t - t_inf) / (t_sup - t_inf) * (d_sup - d_inf) + + currX = startX + (distanceCoef * (mFinalX - startX)).roundToInt() + // Pin to mMinX <= mCurrX <= mMaxX + currX = min(currX, mMaxX) + currX = max(currX, mMinX) + + currY = startY + (distanceCoef * (mFinalY - startY)).roundToInt() + // Pin to mMinY <= mCurrY <= mMaxY + currY = min(currY, mMaxY) + currY = max(currY, mMinY) + + if (currX == mFinalX && currY == mFinalY) { + isFinished = true + } + } + } + } else { + currX = mFinalX + currY = mFinalY + isFinished = true + } + return true + } + + /** + * Start scrolling by providing a starting point and the distance to travel. + * + * @param startX Starting horizontal scroll offset in pixels. Positive + * numbers will scroll the content to the left. + * @param startY Starting vertical scroll offset in pixels. Positive numbers + * will scroll the content up. + * @param dx Horizontal distance to travel. Positive numbers will scroll the + * content to the left. + * @param dy Vertical distance to travel. Positive numbers will scroll the + * content up. + * @param duration Duration of the scroll in milliseconds. + */ + @JvmOverloads + fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int, duration: Int = DEFAULT_DURATION) { + mMode = SCROLL_MODE + isFinished = false + this.duration = duration + mStartTime = AnimationUtils.currentAnimationTimeMillis() + this.startX = startX + this.startY = startY + mFinalX = startX + dx + mFinalY = startY + dy + mDeltaX = dx.toFloat() + mDeltaY = dy.toFloat() + mDurationReciprocal = 1.0f / this.duration.toFloat() + } + + /** + * Start scrolling based on a fling gesture. The distance travelled will + * depend on the initial velocity of the fling. + * + * @param startX Starting point of the scroll (X) + * @param startY Starting point of the scroll (Y) + * @param velocityX Initial velocity of the fling (X) measured in pixels per + * second. + * @param velocityY Initial velocity of the fling (Y) measured in pixels per + * second + * @param minX Minimum X value. The scroller will not scroll past this + * point. + * @param maxX Maximum X value. The scroller will not scroll past this + * point. + * @param minY Minimum Y value. The scroller will not scroll past this + * point. + * @param maxY Maximum Y value. The scroller will not scroll past this + * point. + */ + fun fling( + startX: Int, + startY: Int, + tVelocityX: Int, + tVelocityY: Int, + minX: Int, + maxX: Int, + minY: Int, + maxY: Int + ) { + var velocityX = tVelocityX + var velocityY = tVelocityY + // Continue a scroll or fling in progress + if (mFlywheel && !isFinished) { + val oldVel = currVelocity + + val dx = (mFinalX - this.startX).toFloat() + val dy = (mFinalY - this.startY).toFloat() + val hyp = sqrt(dx * dx + dy * dy) + + val ndx = dx / hyp + val ndy = dy / hyp + + val oldVelocityX = ndx * oldVel + val oldVelocityY = ndy * oldVel + if (sign(velocityX.toFloat()) == sign(oldVelocityX) && sign(velocityY.toFloat()) == sign(oldVelocityY)) { + velocityX += oldVelocityX.toInt() + velocityY += oldVelocityY.toInt() + } + } + + mMode = FLING_MODE + isFinished = false + + val velocity = sqrt(velocityX * velocityX + velocityY * velocityY.toDouble()).toFloat() + + mVelocity = velocity + val l = ln((START_TENSION * velocity / ALPHA).toDouble()) + duration = (1000.0 * exp(l / (DECELERATION_RATE - 1.0))).toInt() + mStartTime = AnimationUtils.currentAnimationTimeMillis() + this.startX = startX + this.startY = startY + + val coeffX = if (velocity == 0f) 1.0f else velocityX / velocity + val coeffY = if (velocity == 0f) 1.0f else velocityY / velocity + + val totalDistance = (ALPHA * exp(DECELERATION_RATE / (DECELERATION_RATE - 1.0) * l)).toInt() + + mMinX = minX + mMaxX = maxX + mMinY = minY + mMaxY = maxY + + mFinalX = startX + (totalDistance * coeffX).roundToInt() + // Pin to mMinX <= mFinalX <= mMaxX + mFinalX = min(mFinalX, mMaxX) + mFinalX = max(mFinalX, mMinX) + + mFinalY = startY + (totalDistance * coeffY).roundToInt() + // Pin to mMinY <= mFinalY <= mMaxY + mFinalY = min(mFinalY, mMaxY) + mFinalY = max(mFinalY, mMinY) + } + + /** + * Stops the animation. Contrary to [.forceFinished], + * aborting the animating cause the scroller to move to the final x and y + * position + * + * @see .forceFinished + */ + fun abortAnimation() { + currX = mFinalX + currY = mFinalY + isFinished = true + } + + /** + * Extend the scroll animation. This allows a running animation to scroll + * further and longer, when used with [.setFinalX] or [.setFinalY]. + * + * @param extend Additional time to scroll in milliseconds. + * @see .setFinalX + * @see .setFinalY + */ + fun extendDuration(extend: Int) { + val passed = timePassed() + duration = passed + extend + mDurationReciprocal = 1.0f / duration + isFinished = false + } + + /** + * Returns the time elapsed since the beginning of the scrolling. + * + * @return The elapsed time in milliseconds. + */ + private fun timePassed(): Int { + return (AnimationUtils.currentAnimationTimeMillis() - mStartTime).toInt() + } + + /** + * @hide + */ + fun isScrollingInDirection(xvel: Float, yvel: Float): Boolean { + return !isFinished && sign(xvel) == sign((mFinalX - startX).toFloat()) && + sign(yvel) == sign((mFinalY - startY).toFloat()) + } + + companion object { + private var sViscousFluidScale: Float = 0.toFloat() + private var sViscousFluidNormalize: Float = 0.toFloat() + private const val DEFAULT_DURATION = 250 + private const val SCROLL_MODE = 0 + private const val FLING_MODE = 1 + + private val DECELERATION_RATE = (ln(0.75) / ln(0.9)).toFloat() + private const val ALPHA = 800f // pixels / seconds + private const val START_TENSION = 0.4f // Tension at start: (0.4 * total T, 1.0 * Distance) + private const val END_TENSION = 1.0f - START_TENSION + private const val NB_SAMPLES = 100 + private val SPLINE = FloatArray(NB_SAMPLES + 1) + + init { + var x_min = 0.0f + for (i in 0..NB_SAMPLES) { + val t = i.toFloat() / NB_SAMPLES + var x_max = 1.0f + var x: Float + var tx: Float + var coef: Float + while (true) { + x = x_min + (x_max - x_min) / 2.0f + coef = 3.0f * x * (1.0f - x) + tx = coef * ((1.0f - x) * START_TENSION + x * END_TENSION) + x * x * x + if (abs(tx - t) < 1E-5) break + if (tx > t) { + x_max = x + } else { + x_min = x + } + } + val d = coef + x * x * x + SPLINE[i] = d + } + SPLINE[NB_SAMPLES] = 1.0f + + // This controls the viscous fluid effect (how much of it) + sViscousFluidScale = 8.0f + // must be set to 1.0 (used in viscousFluid()) + sViscousFluidNormalize = 1.0f + sViscousFluidNormalize = 1.0f / viscousFluid(1.0f) + } + + internal fun viscousFluid(tX: Float): Float { + var x = tX + x *= sViscousFluidScale + if (x < 1.0f) { + x -= 1.0f - exp((-x).toDouble()).toFloat() + } else { + val start = 0.36787944117f // 1/e == exp(-1) + x = 1.0f - exp((1.0f - x).toDouble()).toFloat() + x = start + x * (1.0f - start) + } + x *= sViscousFluidNormalize + return x + } + } +} +/** + * Create a Scroller with the default duration and interpolator. + */ +/** + * Create a Scroller with the specified interpolator. If the interpolator is + * null, the default (viscous) interpolator will be used. "Flywheel" behavior will + * be in effect for apps targeting Honeycomb or newer. + */ +/** + * Start scrolling by providing a starting point and the distance to travel. + * The scroll will use the default value of 250 milliseconds for the + * duration. + * + * @param startX Starting horizontal scroll offset in pixels. Positive + * numbers will scroll the content to the left. + * @param startY Starting vertical scroll offset in pixels. Positive numbers + * will scroll the content up. + * @param dx Horizontal distance to travel. Positive numbers will scroll the + * content to the left. + * @param dy Vertical distance to travel. Positive numbers will scroll the + * content up. + */ diff --git a/app/src/main/java/dev/forcetower/unes/usecases/courses/LoadCoursesUseCase.kt b/app/src/main/java/dev/forcetower/unes/usecases/courses/LoadCoursesUseCase.kt index 8cca19ff7..df4bd15c6 100644 --- a/app/src/main/java/dev/forcetower/unes/usecases/courses/LoadCoursesUseCase.kt +++ b/app/src/main/java/dev/forcetower/unes/usecases/courses/LoadCoursesUseCase.kt @@ -25,8 +25,8 @@ import com.forcetower.uefs.core.storage.repository.CourseRepository import com.forcetower.uefs.core.task.FlowUseCase import com.forcetower.uefs.core.task.UCaseResult import dagger.Reusable -import kotlinx.coroutines.flow.Flow import javax.inject.Inject +import kotlinx.coroutines.flow.Flow @Reusable class LoadCoursesUseCase @Inject constructor( diff --git a/core/src/main/java/com/forcetower/core/General.kt b/core/src/main/java/com/forcetower/core/General.kt index 5068ceee4..b7b3f4ea0 100644 --- a/core/src/main/java/com/forcetower/core/General.kt +++ b/core/src/main/java/com/forcetower/core/General.kt @@ -23,8 +23,8 @@ package com.forcetower.core import android.content.Context import com.forcetower.core.interfaces.DynamicDataSourceFactory import com.forcetower.core.interfaces.DynamicDataSourceFactoryProvider -import timber.log.Timber import kotlin.reflect.full.createInstance +import timber.log.Timber fun getDynamicDataSourceFactory(context: Context, className: String): DynamicDataSourceFactory? { return try { diff --git a/core/src/main/java/com/forcetower/core/base/BaseViewModelFactory.kt b/core/src/main/java/com/forcetower/core/base/BaseViewModelFactory.kt index 1d5582810..eab0d931d 100644 --- a/core/src/main/java/com/forcetower/core/base/BaseViewModelFactory.kt +++ b/core/src/main/java/com/forcetower/core/base/BaseViewModelFactory.kt @@ -7,8 +7,10 @@ import javax.inject.Provider @Suppress("UNCHECKED_CAST") class BaseViewModelFactory @Inject constructor( - private val creators: Map, - @JvmSuppressWildcards Provider> + private val creators: Map< + Class, + @JvmSuppressWildcards Provider + > ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { diff --git a/core/src/main/java/com/forcetower/core/database/BaseDao.kt b/core/src/main/java/com/forcetower/core/database/BaseDao.kt index eb498ab2f..b2f5bc9d0 100644 --- a/core/src/main/java/com/forcetower/core/database/BaseDao.kt +++ b/core/src/main/java/com/forcetower/core/database/BaseDao.kt @@ -48,4 +48,4 @@ abstract class BaseDao { val insertResult = insertIgnore(value) if (insertResult == -1L) update(value) } -} \ No newline at end of file +} diff --git a/core/src/main/java/com/forcetower/core/extensions/GeneralExts.kt b/core/src/main/java/com/forcetower/core/extensions/GeneralExts.kt index 53f18b09c..53805bd0e 100644 --- a/core/src/main/java/com/forcetower/core/extensions/GeneralExts.kt +++ b/core/src/main/java/com/forcetower/core/extensions/GeneralExts.kt @@ -24,6 +24,7 @@ import android.content.res.Resources import androidx.annotation.DimenRes import androidx.core.content.res.ResourcesCompat import androidx.lifecycle.MutableLiveData +import kotlin.math.abs fun Resources.getFloatUsingCompat(@DimenRes resId: Int): Float { return ResourcesCompat.getFloat(this, resId) @@ -35,3 +36,7 @@ val Boolean?.orFalse internal fun MutableLiveData.setValueIfNew(newValue: T) { if (this.value != newValue) value = newValue } + +fun Double.nearlyEquals(other: Double, difference: Double = 0.001): Boolean { + return abs(this - other) <= difference +} diff --git a/core/src/main/java/com/forcetower/core/transition/BackgroundFade.kt b/core/src/main/java/com/forcetower/core/transition/BackgroundFade.kt index 25348db7f..ce0824cdd 100644 --- a/core/src/main/java/com/forcetower/core/transition/BackgroundFade.kt +++ b/core/src/main/java/com/forcetower/core/transition/BackgroundFade.kt @@ -37,6 +37,7 @@ import com.forcetower.core.utils.ViewUtils */ class BackgroundFade : Visibility { constructor() : super() + @Keep constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) @@ -58,10 +59,14 @@ class BackgroundFade : Visibility { startValues: TransitionValues?, endValues: TransitionValues? ): Animator? { - return if (view == null || view.background == null) null else ObjectAnimator.ofInt( - view.background, - ViewUtils.DRAWABLE_ALPHA, - 0 - ) + return if (view == null || view.background == null) { + null + } else { + ObjectAnimator.ofInt( + view.background, + ViewUtils.DRAWABLE_ALPHA, + 0 + ) + } } } diff --git a/dynamic-features/aeri/src/main/java/com/forcetower/uefs/aeri/core/injection/module/FeatureViewModels.kt b/dynamic-features/aeri/src/main/java/com/forcetower/uefs/aeri/core/injection/module/FeatureViewModels.kt index 9f1e10203..1e509c0bc 100644 --- a/dynamic-features/aeri/src/main/java/com/forcetower/uefs/aeri/core/injection/module/FeatureViewModels.kt +++ b/dynamic-features/aeri/src/main/java/com/forcetower/uefs/aeri/core/injection/module/FeatureViewModels.kt @@ -37,6 +37,7 @@ abstract class FeatureViewModels { @IntoMap @ViewModelKey(AERIViewModel::class) abstract fun aeri(viewModel: AERIViewModel): ViewModel + @Binds abstract fun bindViewModelFactory(factory: BaseViewModelFactory): ViewModelProvider.Factory } diff --git a/dynamic-features/aeri/src/main/java/com/forcetower/uefs/aeri/core/storage/repository/AERIRepository.kt b/dynamic-features/aeri/src/main/java/com/forcetower/uefs/aeri/core/storage/repository/AERIRepository.kt index cafa91fc3..cecf8ac63 100644 --- a/dynamic-features/aeri/src/main/java/com/forcetower/uefs/aeri/core/storage/repository/AERIRepository.kt +++ b/dynamic-features/aeri/src/main/java/com/forcetower/uefs/aeri/core/storage/repository/AERIRepository.kt @@ -33,11 +33,11 @@ import com.forcetower.uefs.aeri.core.storage.database.AERIDatabase import com.google.android.play.core.splitcompat.SplitCompat import dagger.Reusable import dev.forcetower.oversee.Oversee +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext import timber.log.Timber -import javax.inject.Inject @Reusable class AERIRepository @Inject constructor( diff --git a/dynamic-features/aeri/src/main/java/com/forcetower/uefs/aeri/feature/AERIMessagesAdapter.kt b/dynamic-features/aeri/src/main/java/com/forcetower/uefs/aeri/feature/AERIMessagesAdapter.kt index d25f58498..6f8d30c36 100644 --- a/dynamic-features/aeri/src/main/java/com/forcetower/uefs/aeri/feature/AERIMessagesAdapter.kt +++ b/dynamic-features/aeri/src/main/java/com/forcetower/uefs/aeri/feature/AERIMessagesAdapter.kt @@ -44,7 +44,9 @@ class AERIMessagesAdapter( } inner class AnnouncementHolder(val binding: ItemAnnouncementBinding, interactor: AnnouncementInteractor) : RecyclerView.ViewHolder(binding.root) { - init { binding.interactor = interactor } + init { + binding.interactor = interactor + } } private object DiffCallback : DiffUtil.ItemCallback() { diff --git a/dynamic-features/aeri/src/main/java/com/forcetower/uefs/aeri/feature/AERINewsFragment.kt b/dynamic-features/aeri/src/main/java/com/forcetower/uefs/aeri/feature/AERINewsFragment.kt index 4fb3da946..c29f7f5c1 100644 --- a/dynamic-features/aeri/src/main/java/com/forcetower/uefs/aeri/feature/AERINewsFragment.kt +++ b/dynamic-features/aeri/src/main/java/com/forcetower/uefs/aeri/feature/AERINewsFragment.kt @@ -42,8 +42,8 @@ import com.forcetower.uefs.feature.shared.UFragment import com.forcetower.uefs.feature.web.CustomTabActivityHelper import com.google.android.play.core.splitcompat.SplitCompat import dagger.hilt.android.EntryPointAccessors -import timber.log.Timber import javax.inject.Inject +import timber.log.Timber @Keep class AERINewsFragment : UFragment() { diff --git a/dynamic-features/conference/src/main/java/dev/forcetower/conference/core/model/persistence/Session.kt b/dynamic-features/conference/src/main/java/dev/forcetower/conference/core/model/persistence/Session.kt index 09ddc3549..9694bd601 100644 --- a/dynamic-features/conference/src/main/java/dev/forcetower/conference/core/model/persistence/Session.kt +++ b/dynamic-features/conference/src/main/java/dev/forcetower/conference/core/model/persistence/Session.kt @@ -48,6 +48,7 @@ data class Session( val dayId: String ) { val hasPhoto inline get() = !photoUrl.isNullOrBlank() + @Ignore val duration = endTime.toInstant().toEpochMilli() - startTime.toInstant().toEpochMilli() diff --git a/dynamic-features/dashboard/src/main/java/com/forcetower/uefs/dashboard/core/storage/repository/DashboardRepository.kt b/dynamic-features/dashboard/src/main/java/com/forcetower/uefs/dashboard/core/storage/repository/DashboardRepository.kt index b5ce59a66..a582ea56f 100644 --- a/dynamic-features/dashboard/src/main/java/com/forcetower/uefs/dashboard/core/storage/repository/DashboardRepository.kt +++ b/dynamic-features/dashboard/src/main/java/com/forcetower/uefs/dashboard/core/storage/repository/DashboardRepository.kt @@ -31,9 +31,9 @@ import com.forcetower.uefs.core.storage.database.UDatabase import com.forcetower.uefs.core.storage.database.aggregation.ClassLocationWithData import com.forcetower.uefs.core.work.affinity.AnswerAffinityWorker import dagger.Reusable -import kotlinx.coroutines.flow.Flow import java.util.Calendar import javax.inject.Inject +import kotlinx.coroutines.flow.Flow @Reusable class DashboardRepository @Inject constructor( diff --git a/dynamic-features/dashboard/src/main/java/com/forcetower/uefs/dashboard/feature/DashboardAdapter.kt b/dynamic-features/dashboard/src/main/java/com/forcetower/uefs/dashboard/feature/DashboardAdapter.kt index 10118132c..313c6f35b 100644 --- a/dynamic-features/dashboard/src/main/java/com/forcetower/uefs/dashboard/feature/DashboardAdapter.kt +++ b/dynamic-features/dashboard/src/main/java/com/forcetower/uefs/dashboard/feature/DashboardAdapter.kt @@ -26,7 +26,6 @@ import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import com.forcetower.uefs.core.model.unes.Account import com.forcetower.uefs.core.model.unes.EdgeServiceAccount import com.forcetower.uefs.core.model.unes.Message import com.forcetower.uefs.core.model.unes.SStudent diff --git a/dynamic-features/dashboard/src/main/java/com/forcetower/uefs/dashboard/feature/DashboardFragment.kt b/dynamic-features/dashboard/src/main/java/com/forcetower/uefs/dashboard/feature/DashboardFragment.kt index 83581c24e..e37e45ef6 100644 --- a/dynamic-features/dashboard/src/main/java/com/forcetower/uefs/dashboard/feature/DashboardFragment.kt +++ b/dynamic-features/dashboard/src/main/java/com/forcetower/uefs/dashboard/feature/DashboardFragment.kt @@ -42,8 +42,8 @@ import com.forcetower.uefs.feature.shared.UFragment import com.google.android.play.core.install.model.InstallStatus import com.google.android.play.core.splitcompat.SplitCompat import dagger.hilt.android.EntryPointAccessors -import timber.log.Timber import javax.inject.Inject +import timber.log.Timber @Keep class DashboardFragment : UFragment() { diff --git a/dynamic-features/dashboard/src/main/java/com/forcetower/uefs/dashboard/feature/DashboardViewModel.kt b/dynamic-features/dashboard/src/main/java/com/forcetower/uefs/dashboard/feature/DashboardViewModel.kt index 78a653aab..a66412514 100644 --- a/dynamic-features/dashboard/src/main/java/com/forcetower/uefs/dashboard/feature/DashboardViewModel.kt +++ b/dynamic-features/dashboard/src/main/java/com/forcetower/uefs/dashboard/feature/DashboardViewModel.kt @@ -27,16 +27,15 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import androidx.lifecycle.map import com.forcetower.core.lifecycle.Event -import com.forcetower.uefs.core.model.unes.Account import com.forcetower.uefs.core.model.unes.EdgeServiceAccount import com.forcetower.uefs.core.model.unes.SStudent import com.forcetower.uefs.core.storage.database.aggregation.ClassLocationWithData import com.forcetower.uefs.core.storage.repository.SagresDataRepository import com.forcetower.uefs.dashboard.core.storage.repository.DashboardRepository import com.forcetower.uefs.feature.shared.TimeLiveData -import timber.log.Timber import java.util.Calendar import javax.inject.Inject +import timber.log.Timber class DashboardViewModel @Inject constructor( private val repository: DashboardRepository, @@ -60,8 +59,9 @@ class DashboardViewModel @Inject constructor( val calendar = Calendar.getInstance() val currentTimeInt = calendar.get(Calendar.HOUR_OF_DAY) * 60 + calendar.get(Calendar.MINUTE) val startInt = it?.location?.startsAtInt - if (startInt == null) false - else { + if (startInt == null) { + false + } else { startInt <= currentTimeInt } } diff --git a/dynamic-features/disciplines/src/main/java/dev/forcetower/disciplines/feature/DisciplineFragment.kt b/dynamic-features/disciplines/src/main/java/dev/forcetower/disciplines/feature/DisciplineFragment.kt index efd395cfe..9185c39d0 100644 --- a/dynamic-features/disciplines/src/main/java/dev/forcetower/disciplines/feature/DisciplineFragment.kt +++ b/dynamic-features/disciplines/src/main/java/dev/forcetower/disciplines/feature/DisciplineFragment.kt @@ -54,8 +54,8 @@ import dagger.hilt.android.EntryPointAccessors import dev.forcetower.disciplines.core.injection.DaggerDisciplineComponent import dev.forcetower.disciplines.databinding.FragmentDisciplineBinding import dev.forcetower.disciplines.feature.dialog.SelectGroupDialog -import timber.log.Timber import javax.inject.Inject +import timber.log.Timber @Keep class DisciplineFragment : UFragment() { diff --git a/dynamic-features/disciplines/src/main/java/dev/forcetower/disciplines/feature/DisciplinePerformanceAdapter.kt b/dynamic-features/disciplines/src/main/java/dev/forcetower/disciplines/feature/DisciplinePerformanceAdapter.kt index 87813dd78..b8df95fda 100644 --- a/dynamic-features/disciplines/src/main/java/dev/forcetower/disciplines/feature/DisciplinePerformanceAdapter.kt +++ b/dynamic-features/disciplines/src/main/java/dev/forcetower/disciplines/feature/DisciplinePerformanceAdapter.kt @@ -119,35 +119,45 @@ class DisciplinePerformanceAdapter( val binding: ItemDisciplineStatusGroupingNameBinding, listener: DisciplinesSemestersActions ) : DisciplineHolder(binding.root) { - init { binding.actions = listener } + init { + binding.actions = listener + } } class MeanHolder( val binding: ItemDisciplineStatusMeanBinding, listener: DisciplinesSemestersActions ) : DisciplineHolder(binding.root) { - init { binding.actions = listener } + init { + binding.actions = listener + } } class FinalsHolder( val binding: ItemDisciplineStatusFinalsBinding, listener: DisciplinesSemestersActions ) : DisciplineHolder(binding.root) { - init { binding.actions = listener } + init { + binding.actions = listener + } } class GradeHolder( val binding: ItemGradeBinding, listener: DisciplinesSemestersActions ) : DisciplineHolder(binding.root) { - init { binding.actions = listener } + init { + binding.actions = listener + } } class HeaderHolder( val binding: ItemDisciplineStatusNameResumedBinding, listener: DisciplinesSemestersActions ) : DisciplineHolder(binding.root) { - init { binding.actions = listener } + init { + binding.actions = listener + } } class DividerHolder( @@ -158,7 +168,9 @@ class DisciplinePerformanceAdapter( val binding: ItemDisciplineEmptyDataBinding, actions: DisciplinesSemestersActions ) : DisciplineHolder(binding.root) { - init { binding.actions = actions } + init { + binding.actions = actions + } } } diff --git a/dynamic-features/disciplines/src/main/java/dev/forcetower/disciplines/feature/DisciplineSemesterAdapter.kt b/dynamic-features/disciplines/src/main/java/dev/forcetower/disciplines/feature/DisciplineSemesterAdapter.kt index ddc9537f9..c1234891c 100644 --- a/dynamic-features/disciplines/src/main/java/dev/forcetower/disciplines/feature/DisciplineSemesterAdapter.kt +++ b/dynamic-features/disciplines/src/main/java/dev/forcetower/disciplines/feature/DisciplineSemesterAdapter.kt @@ -44,7 +44,9 @@ class DisciplineSemesterAdapter( } inner class SemesterHolder(val binding: ItemDisciplinesSemesterIndicatorBinding) : RecyclerView.ViewHolder(binding.root) { - init { binding.actions = actions } + init { + binding.actions = actions + } } private object SemesterDiff : DiffUtil.ItemCallback() { diff --git a/dynamic-features/disciplines/src/main/java/dev/forcetower/disciplines/feature/DisciplinesViewModel.kt b/dynamic-features/disciplines/src/main/java/dev/forcetower/disciplines/feature/DisciplinesViewModel.kt index f4d4d7dbf..7ce6b2fab 100644 --- a/dynamic-features/disciplines/src/main/java/dev/forcetower/disciplines/feature/DisciplinesViewModel.kt +++ b/dynamic-features/disciplines/src/main/java/dev/forcetower/disciplines/feature/DisciplinesViewModel.kt @@ -34,12 +34,12 @@ import com.forcetower.uefs.core.model.unes.Semester import com.forcetower.uefs.core.storage.database.aggregation.ClassFullWithGroup import com.forcetower.uefs.core.storage.repository.DisciplinesRepository import com.forcetower.uefs.core.storage.repository.SagresGradesRepository +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject class DisciplinesViewModel @Inject constructor( private val repository: DisciplinesRepository, diff --git a/dynamic-features/event/src/main/java/dev/forcetower/event/core/repository/EventRepository.kt b/dynamic-features/event/src/main/java/dev/forcetower/event/core/repository/EventRepository.kt index 1ab66bec2..b40430a32 100644 --- a/dynamic-features/event/src/main/java/dev/forcetower/event/core/repository/EventRepository.kt +++ b/dynamic-features/event/src/main/java/dev/forcetower/event/core/repository/EventRepository.kt @@ -34,15 +34,15 @@ import com.forcetower.uefs.core.storage.network.UService import com.forcetower.uefs.core.util.ImgurUploader import com.google.android.play.core.splitcompat.SplitCompat import dev.forcetower.event.core.work.CreateEventWorker -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient -import timber.log.Timber import java.io.ByteArrayOutputStream import java.io.InputStream import java.time.ZonedDateTime import java.util.UUID import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import timber.log.Timber class EventRepository @Inject constructor( private val database: UDatabase, diff --git a/dynamic-features/event/src/main/java/dev/forcetower/event/core/work/CreateEventWorker.kt b/dynamic-features/event/src/main/java/dev/forcetower/event/core/work/CreateEventWorker.kt index 6426b8f52..9854a5292 100644 --- a/dynamic-features/event/src/main/java/dev/forcetower/event/core/work/CreateEventWorker.kt +++ b/dynamic-features/event/src/main/java/dev/forcetower/event/core/work/CreateEventWorker.kt @@ -32,8 +32,8 @@ import com.forcetower.uefs.core.work.enqueueUnique import dagger.hilt.android.EntryPointAccessors import dev.forcetower.event.core.injection.DaggerEventComponent import dev.forcetower.event.core.repository.EventRepository -import timber.log.Timber import javax.inject.Inject +import timber.log.Timber class CreateEventWorker( private val context: Context, diff --git a/dynamic-features/event/src/main/java/dev/forcetower/event/feature/create/CreateEventFragment.kt b/dynamic-features/event/src/main/java/dev/forcetower/event/feature/create/CreateEventFragment.kt index d2d4ae603..f6b9f5be6 100644 --- a/dynamic-features/event/src/main/java/dev/forcetower/event/feature/create/CreateEventFragment.kt +++ b/dynamic-features/event/src/main/java/dev/forcetower/event/feature/create/CreateEventFragment.kt @@ -61,12 +61,12 @@ import dev.forcetower.event.core.binding.formattedDate import dev.forcetower.event.core.injection.DaggerEventComponent import dev.forcetower.event.databinding.FragmentCreateEventBinding import dev.forcetower.event.feature.details.EventDetailsActivity -import timber.log.Timber import java.time.Instant import java.time.ZoneId import java.time.ZonedDateTime import java.util.Calendar import javax.inject.Inject +import timber.log.Timber @Keep class CreateEventFragment : UFragment() { @@ -292,7 +292,9 @@ class CreateEventFragment : UFragment() { } val free = binding.checkFree.isChecked - val price = if (free) null else { + val price = if (free) { + null + } else { try { binding.inputPrice.text.toString().toDouble() } catch (error: Throwable) { diff --git a/dynamic-features/event/src/main/java/dev/forcetower/event/feature/create/CreationViewModel.kt b/dynamic-features/event/src/main/java/dev/forcetower/event/feature/create/CreationViewModel.kt index 1fc428f4c..3bb92d464 100644 --- a/dynamic-features/event/src/main/java/dev/forcetower/event/feature/create/CreationViewModel.kt +++ b/dynamic-features/event/src/main/java/dev/forcetower/event/feature/create/CreationViewModel.kt @@ -27,10 +27,10 @@ import androidx.lifecycle.viewModelScope import com.forcetower.uefs.core.model.unes.Course import com.forcetower.uefs.core.model.unes.Event import dev.forcetower.event.core.repository.EventRepository -import kotlinx.coroutines.launch import java.time.ZonedDateTime import java.util.Calendar import javax.inject.Inject +import kotlinx.coroutines.launch class CreationViewModel @Inject constructor( private val repository: EventRepository diff --git a/dynamic-features/event/src/main/java/dev/forcetower/event/feature/listing/EventViewModel.kt b/dynamic-features/event/src/main/java/dev/forcetower/event/feature/listing/EventViewModel.kt index b69ef623c..897c2ecca 100644 --- a/dynamic-features/event/src/main/java/dev/forcetower/event/feature/listing/EventViewModel.kt +++ b/dynamic-features/event/src/main/java/dev/forcetower/event/feature/listing/EventViewModel.kt @@ -26,8 +26,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.forcetower.uefs.core.model.unes.Event import dev.forcetower.event.core.repository.EventRepository -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.launch typealias SingleEventAction = com.forcetower.core.lifecycle.Event