Commit 5e66cff6 authored by Filip Wiesner's avatar Filip Wiesner

VM global progress, AC log | FIX token refesh loop

parent 96101fa5
package com.cvut.blackbird
import android.app.Application
import android.util.Log
import androidx.fragment.app.FragmentManager
import com.chibatching.kotpref.Kotpref
import com.cvut.blackbird.dinjection.components.ApplicationComponent
......@@ -15,6 +16,8 @@ class BlackBirdAC: Application(){
const val LOG_TAG = "BLACK_BIRD"
//platformStatic allow access it from java code
@JvmStatic lateinit var graph: ApplicationComponent
fun log(message: String) = Log.i(LOG_TAG, message)
}
override fun onCreate() {
......
package com.cvut.blackbird.dinjection.modules
import android.content.Context
import com.cvut.blackbird.model.services.*
import com.cvut.blackbird.model.services.AuthService
import com.cvut.blackbird.model.services.KosService
import com.cvut.blackbird.model.services.SiriusService
import com.google.gson.GsonBuilder
import com.google.gson.JsonDeserializer
import com.google.gson.JsonPrimitive
......@@ -17,7 +19,6 @@ import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
@Module
class ServicesModule(val context: Context) {
private val fmt = ISODateTimeFormat.dateTime()
......
......@@ -43,7 +43,11 @@ inline infix fun<T> MutableLiveData<Result<T>>.asProgressStatus(job: () -> Resul
postValue(Loading())
postValue(job.invoke())
}
inline infix fun<T> MutableLiveData<Result<T>>.fetchUsing(crossinline job: suspend () -> Result<T>) =
inline infix fun<T> MutableLiveData<Result<T>>.fetchUsing(job: () -> Result<T>) {
postValue(Loading())
postValue(job.invoke())
}
inline infix fun<T> MutableLiveData<Result<T>>.fetchUsingJob(crossinline job: suspend () -> Result<T>) =
launch {
postValue(Loading())
postValue(job.invoke())
......
package com.cvut.blackbird.flows
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.cvut.blackbird.extensions.JobStrategy
import com.cvut.blackbird.extensions.doWith
import com.cvut.blackbird.extensions.withDefault
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.launch
......@@ -12,14 +15,38 @@ class Initialized<T>(val data: T): LateInit<T>()
open class BlackBirdVM: ViewModel() {
private val jobs = hashMapOf<String, Job>()
private val _globalProgress = MutableLiveData<Boolean>() withDefault false
val globalProgress: LiveData<Boolean> get() = _globalProgress
/**
* Starts (launches) 'work' block in background with its tag
* If another work with that tag already exists, the job will be handled according to strategy
* @param key ('undefined' by default)
* @param strategy ('cancel' strategy by default)
* @param work
*/
fun startJob(
key: String = "undefined",
strategy: JobStrategy = JobStrategy.CANCEL,
work: suspend ()->Unit
) {
if (jobs.containsKey(key))
jobs[key] = doWith(jobs[key], strategy) { launch { work() } }
else
jobs[key] = launch { work() }
) = (
if (jobs.containsKey(key)) // if job with the same tag already exists, behave according to strategy
doWith(jobs[key], strategy) { launch { work() } }
else // if there is no job with this tag, create it
launch { work() }
).apply {
indicateStart() // Indicate start of this job and plan end
invokeOnCompletion { indicateEnd() }
jobs[key] = this // assign created job (in if-else expr.) to given tag
}
private fun indicateStart() = _globalProgress.postValue(true)
private fun indicateEnd() {
if (jobs.all { it.value.isCompleted }) {
// indicate global end only if every job was completed
_globalProgress.postValue(false)
}
}
}
\ No newline at end of file
......@@ -2,11 +2,8 @@ package com.cvut.blackbird.flows.authentication
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.cvut.blackbird.extensions.asBoolProgressStatus
import com.cvut.blackbird.extensions.fetchUsing
import com.cvut.blackbird.extensions.indicateProgressBy
import com.cvut.blackbird.extensions.withDefault
import com.cvut.blackbird.extensions.*
import com.cvut.blackbird.flows.BlackBirdVM
import com.cvut.blackbird.model.Failure
import com.cvut.blackbird.model.NotYet
import com.cvut.blackbird.model.Result
......@@ -16,7 +13,7 @@ import com.cvut.blackbird.model.support.AuthResult
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.launch
class AuthViewModel : ViewModel() {
class AuthViewModel : BlackBirdVM() {
private val model: AuthModel = AuthModel()
//Output
......@@ -30,25 +27,25 @@ class AuthViewModel : ViewModel() {
val userInitResult: LiveData<Result<Unit>> get() = _userInitResult
val userInitState: LiveData<String> get() = _userInitState
init {
globalProgress passTo _authLoadingStatus
}
fun initToken(code: String) {
launch { _authLoadingStatus.asBoolProgressStatus {
startJob("token") {
val token = model.initToken(code)
_authStatus.postValue(token)
} }
}
}
private var refreshJob: Job? = null
fun refreshToken() {
refreshJob?.cancel()
refreshJob = launch { _authStatus.postValue(model.refreshTokenGuarded()) }
.indicateProgressBy(_authLoadingStatus)
startJob("refresh") {
_authStatus.postValue(model.refreshTokenGuarded())
}
}
private var initJob: Job? = null
fun initUser() {
initJob?.cancel()
initJob = _userInitResult.fetchUsing {
fun initUser() { startJob("init") {
_userInitResult fetchUsing {
_userInitState.postValue("Looking up your account")
val user = model.loadUser()
......@@ -68,6 +65,6 @@ class AuthViewModel : ViewModel() {
} else return@fetchUsing teachers as Failure
courses
} indicateProgressBy _authLoadingStatus
}
}
} }
}
\ No newline at end of file
......@@ -32,20 +32,24 @@ class EventDetailViewModel: BlackBirdVM(), EventDetailVM {
* INPUT
*/
override fun setEvent(event: Event) = startJob("set") {
_detailedEvent.postValue( model.getDetailedEvent(event.id) )
override fun setEvent(event: Event) {
startJob("set") {
_detailedEvent.postValue( model.getDetailedEvent(event.id) )
}
}
override fun changeEventNote(text: String) = startJob("note") {
if (detailedEvent.dontHaveValue()) return@startJob
override fun changeEventNote(text: String) {
startJob("note") {
if (detailedEvent.dontHaveValue()) return@startJob
val note = if (detailedEvent.value?.userNotes?.firstOrNull() != null)
detailedEvent.value!!.userNotes.first()
else
model.createNote(detailedEvent.value!!.event.id)
val note = if (detailedEvent.value?.userNotes?.firstOrNull() != null)
detailedEvent.value!!.userNotes.first()
else
model.createNote(detailedEvent.value!!.event.id)
// TODO: You will create note every time because the change in DB wont propagate here (event is not LD)
model.updateNote(note.apply { this.note = text })
// TODO: You will create note every time because the change in DB wont propagate here (event is not LD)
model.updateNote(note.apply { this.note = text })
}
}
}
\ No newline at end of file
......@@ -2,16 +2,16 @@ package com.cvut.blackbird.flows.tasks
import androidx.lifecycle.*
import com.cvut.blackbird.extensions.*
import com.cvut.blackbird.flows.BlackBirdVM
import com.cvut.blackbird.model.*
import com.cvut.blackbird.model.entities.Event
import com.cvut.blackbird.model.entities.News
import com.cvut.blackbird.model.flows.TasksModel
import com.github.magneticflux.livedata.zipTo
import kotlinx.coroutines.experimental.*
import kotlinx.coroutines.experimental.android.UI
class TasksViewModel : ViewModel() {
class TasksViewModel : BlackBirdVM() {
private val model: TasksModel = TasksModel()
private val _loadingStatus = MutableLiveData<Boolean>() withDefault false
......@@ -29,28 +29,25 @@ class TasksViewModel : ViewModel() {
}
private var refreshJob: Job? = null
fun refresh() {
refreshJob?.cancel()
refreshJob = _refreshResult
.fetchUsing {
val resultNews = model.getNews()
if (resultNews is Success) {
_news.postValue(resultNews.value)
model.refreshEvents()
} else Failure((resultNews as? Failure)
?.message ?: "Unexpected error")
}
.indicateProgressBy( _loadingStatus )
}
fun refresh() { startJob("refresh") {
_refreshResult fetchUsing {
val resultNews = model.getNews()
if (resultNews is Success) {
_news.postValue(resultNews.value)
model.refreshEvents()
} else Failure((resultNews as? Failure)
?.message ?: "Unexpected error")
}
} indicateProgressBy _loadingStatus }
private fun configTasks() { launch {
val combined = combineLiveData(model.getPinnedEvents(), model.getExams(), _news) {
pinned: List<Event>?, exams: List<Event>?, news: List<News>? ->
val resultTasks = ArrayList<Task>()
if (news != null && news.isNotEmpty()) resultTasks.add(NewsTask(news))
if (pinned != null) resultTasks.addAll(pinned.map { PinnedTask(it) })
if (exams != null) resultTasks.addAll(exams.map { ExamTask(it) })
if (news != null && news.isNotEmpty()) resultTasks.add(NewsTask(news))
resultTasks as List<Task>
}
......
package com.cvut.blackbird.model
import android.util.Log
import com.cvut.blackbird.BlackBirdAC
import com.cvut.blackbird.BlackBirdAC.Companion.log
import com.cvut.blackbird.model.database.EventDao
import com.cvut.blackbird.model.services.*
import com.cvut.blackbird.model.services.AuthInfo
import com.cvut.blackbird.model.services.AuthService
import com.cvut.blackbird.model.services.SiriusService
import com.cvut.blackbird.model.services.UserInfo
import retrofit2.Call
import java.net.HttpURLConnection
import javax.inject.Inject
sealed class Result<T>
......@@ -22,7 +26,7 @@ abstract class BlackBirdModel {
}
protected suspend fun refreshEvents(siriusService: SiriusService, eventDao: EventDao): Result<Unit> {
val result = fetch(siriusService.getEvents(UserInfo.username))
val result = fetch { siriusService.getEvents(UserInfo.username) }
if (result is Success) {
val events = result.value.events
if (events != null)
......@@ -33,13 +37,13 @@ abstract class BlackBirdModel {
return Failure("No error message")
}
protected suspend fun refreshToken(): Result<Unit> {
Log.d(BlackBirdAC.LOG_TAG, "Refreshing token")
suspend fun refreshToken(): Result<Unit> {
log("Refreshing token")
return try {
val response = authService.refreshToken().execute()
if (response.isSuccessful && response.body() != null) {
AuthInfo.accessToken = response.body()!!.token
Log.d(BlackBirdAC.LOG_TAG, "Token refreshed")
log("Token refreshed")
Success(Unit)
} else {
Failure(response.errorBody()?.string() ?: "No error message")
......@@ -49,23 +53,29 @@ abstract class BlackBirdModel {
}
}
suspend inline fun <T> fetch(call: () -> Call<T>): Result<T> {
var result: Result<T> = NotYet()
suspend fun <T> fetch(call: Call<T>): Result<T> {
val result: Result<T>
try {
val response = call.execute()
result = if (response.isSuccessful && response.body() != null) {
Success(response.body()!!)
} else {
// TODO Get rid of this mess. Call wont change its auth
if (response.code() == 401 && refreshToken() is Success)
fetch(call.clone()) //Clone to not get "Already executed" exception
else
Failure(response.errorBody()?.string() ?: "No error message")
while (result is NotYet) { // repeat until we have result
val response = call().execute()
result = if (response.isSuccessful && response.body() != null)
// if request was successful (code 200 - 299) and body is not empty
Success(response.body()!!)
else {
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED
&& refreshToken() is Success)
// if request was rejected because of bad AuthToken but token refresh was successful
// repeat request
NotYet()
else
Failure(response.errorBody()?.string() ?: "No error message")
}
}
} catch (e: Throwable) {
return Failure(e.localizedMessage)
}
return result
}
}
\ No newline at end of file
......@@ -71,11 +71,11 @@ class AuthModel: BlackBirdModel() {
}
suspend fun loadUser(): Result<Unit> {
val userInfo = fetch(authService.getUserInfo())
val userInfo = fetch { authService.getUserInfo() }
val user = if (userInfo is Success) {
UserInfo.facultyAbbr = userInfo.value.email
.split('.','@')[1]
fetch(kosService.getStudent(userInfo.value.username))
fetch { kosService.getStudent(userInfo.value.username) }
} else Failure((userInfo as? Failure)?.message ?: "Unexpected Error")
return if (user is Success) {
......@@ -92,7 +92,7 @@ class AuthModel: BlackBirdModel() {
val teachers = eventDao.getTeachers()
for (teacher in teachers)
deferredTeacherList.add(async { fetch(kosService.getTeacher(teacher)) })
deferredTeacherList.add(async { fetch { kosService.getTeacher(teacher) } })
deferredTeacherList.forEach { completedTeacherList.add(it.await()) }
val result = completedTeacherList.evaluateResult()
......@@ -104,10 +104,10 @@ class AuthModel: BlackBirdModel() {
val deferredCourseList = ArrayList<Deferred<Result<Course>>>()
val completedCourses = ArrayList<Result<Course>>()
val courses = fetch(kosService.getStudentsCourses())
val courses = fetch { kosService.getStudentsCourses() }
if (courses is Success) {
for (link in courses.value.courses)
deferredCourseList.add(async { fetch(kosService.getCourse(link.name)) })
deferredCourseList.add(async { fetch { kosService.getCourse(link.name) } })
deferredCourseList.forEach { completedCourses.add(it.await()) }
} else return Failure((courses as Failure).message)
......
......@@ -14,8 +14,6 @@ interface AuthService {
const val clientId = "7d7f8a83-b883-4029-b747-d3a99a9da235"
const val clientSecret = "nCx6ggFvQLpfe4zTX61LYrCJ0Eygfa49"
private val accessToken = "Bearer ${AuthInfo.accessToken}"
val auth:String get() = "Basic " + ("$clientId:$clientSecret").base64Encoded()
}
......@@ -43,6 +41,6 @@ interface AuthService {
@GET("userinfo")
fun getUserInfo(
@Header("Authorization") token: String = AuthService.accessToken
@Header("Authorization") token: String = AuthInfo.accessTokenHeader
): Call<User>
}
\ No newline at end of file
......@@ -10,7 +10,6 @@ import retrofit2.http.*
interface KosService {
companion object {
const val url = "https://kosapi.feld.cvut.cz/api/3/"
private val accessToken = "Bearer ${AuthInfo.accessToken}"
}
......@@ -22,14 +21,14 @@ interface KosService {
@GET("students/{user}")
fun getStudent(
@Path("user") user: String,
@Header("Authorization") token: String = accessToken
@Header("Authorization") token: String = AuthInfo.accessTokenHeader
): Call<Student>
@GET("students/{user}/enrolledCourses")
fun getStudentsCourses(
@Path("user") user: String = UserInfo.username,
@Query("sem") semester: String = "none",
@Header("Authorization") token: String = accessToken,
@Header("Authorization") token: String = AuthInfo.accessTokenHeader,
@Query("limit") resultsLimit: Int = 1000,
@Query("lang") lang: String = "en",
@Query("multilang") multilang: Boolean = false
......@@ -44,7 +43,7 @@ interface KosService {
@GET("courses/{course}")
fun getCourse(
@Path("course") course: String,
@Header("Authorization") token: String = accessToken,
@Header("Authorization") token: String = AuthInfo.accessTokenHeader,
@Query("lang") lang: String = "en",
@Query("multilang") multilang: Boolean = false
): Call<Course>
......@@ -58,7 +57,7 @@ interface KosService {
@GET("teachers/{teacher}")
fun getTeacher(
@Path("teacher") teacherUsername: String,
@Header("Authorization") token: String = accessToken,
@Header("Authorization") token: String = AuthInfo.accessTokenHeader,
@Query("lang") lang: String = "en",
@Query("multilang") multilang: Boolean = false
): Call<Teacher>
......
......@@ -8,6 +8,8 @@ import com.cvut.blackbird.model.entities.Student
object AuthInfo: KotprefModel() {
var accessToken by stringPref()
var refreshToken by stringPref()
val accessTokenHeader get() = "Bearer $accessToken"
}
object UserInfo: KotprefModel() {
......
......@@ -8,14 +8,13 @@ import retrofit2.http.*
interface SiriusService {
companion object {
const val url = "https://sirius.fit.cvut.cz/api/v1/"
private val accessToken = "Bearer ${AuthInfo.accessToken}"
}
@GET("people/{username}/events")
fun getEvents(
@Path("username") username: String = UserInfo.username,
@Query("limit") resultsLimit: Int = 1000,
@Header("Authorization") token: String = accessToken
@Header("Authorization") token: String = AuthInfo.accessTokenHeader
): Call<EventParent>
// @FormUrlEncoded
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment