Commit c3d457c3 authored by Filip Wiesner's avatar Filip Wiesner

Migrated auth logic, auth logo change

- Migrated authentication logic too Corutine version
- auth overhaul: user loading progressbar, welcome logo changed to 'Raven'
- few LiveData and Job helpers (fetchUsing, indicateProgressBy)
parent 03596a8d
......@@ -13,20 +13,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="426" />
<option name="y" value="-47" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="authFragmentEx">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="419" />
<option name="y" value="331" />
<option name="x" value="391" />
<option name="y" value="36" />
</Point>
</option>
</LayoutPositions>
......
......@@ -3,7 +3,7 @@ package com.cvut.blackbird.dinjection.components
import com.cvut.blackbird.dinjection.modules.ContextModule
import com.cvut.blackbird.dinjection.modules.ServicesModule
import com.cvut.blackbird.dinjection.modules.RoomModule
import com.cvut.blackbird.experimental.auth.AuthModelEx
import com.cvut.blackbird.model.flows.AuthModel
import com.cvut.blackbird.model.flows.*
import dagger.Component
import javax.inject.Singleton
......@@ -16,5 +16,4 @@ interface ApplicationComponent {
fun inject(model: TasksModel)
fun inject(model: TimetableModel)
fun inject(model: SearchModel)
fun inject(model: AuthModelEx)
}
\ No newline at end of file
package com.cvut.blackbird.experimental.auth
import android.content.Intent
import android.net.Uri
import androidx.lifecycle.ViewModelProviders
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import androidx.lifecycle.Observer
import androidx.navigation.findNavController
import com.cvut.blackbird.R
import com.cvut.blackbird.extensions.*
import com.cvut.blackbird.model.Success
import com.cvut.blackbird.model.entities.Student
import com.cvut.blackbird.model.services.AuthService
import com.cvut.blackbird.model.services.UserInfo
import com.cvut.blackbird.model.support.AuthResult
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.auth_fragment.*
class AuthFragmentEx : Fragment() {
var lastValidUser = Student()
companion object {
fun newInstance() = AuthFragmentEx()
}
private lateinit var viewModel: AuthViewModelEx
private var snack: Snackbar? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.auth_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(AuthViewModelEx::class.java)
setupUi()
setupBinding()
}
private fun setupUi() {
snack = Snackbar
.make(view!!, "", Snackbar.LENGTH_INDEFINITE)
.setAction("Try Again") { viewModel.refreshToken() }
acceptBtn.setOnClickListener {
onLogged()
}
}
private fun setupBinding() {
usernameInput.onChange {if(it.isNotBlank()) viewModel.fetchStudent(usernameInput.text.toString())}
viewModel.studentResult.observe(this, Observer {
if(it is Success) {
infoMessage.text = "Found user: ${it.value.name} ${it.value.surname}\nStudying: ${it.value.programme}"
lastValidUser = it.value
usernameInputLayout.isErrorEnabled = false
closeKeyboard()
} else if (usernameInput.text!!.isNotBlank()) {
usernameInputLayout.isErrorEnabled = true
usernameInputLayout.error = "Username not found!"
}
})
viewModel.authStatus.observe(this, Observer { result ->
if (result != null) {
if (result == AuthResult.SUCCESS) {
if(UserInfo.username.isNotBlank()) onLogged()
else secondPhase()
} else if (result != AuthResult.NOT_YET) {
snack?.setText("Error: ${result.name}")
infoMessage.text = result.errorDesc
if (!result.critical) authorize()
else snack!!.show()
}
} else {
snack?.setText("There was some unexpected error, sorry about that :(")
snack?.show()
}
})
authProgress.bindVisibilityTo(this, viewModel.loadingStatus)
acceptBtn.bindEnabledToSuccess(this, viewModel.studentResult)
viewModel.loadingStatus.observe(this, Observer {
if (it == true) snack?.dismiss()
})
}
private fun secondPhase() {
val animLength: Long = 1000
val anim = AlphaAnimation(0.0f, 1.0f)
anim.duration = animLength
acceptBtn.startAnimation(anim)
usernameInputLayout.startAnimation(anim)
acceptBtn.visibility = View.VISIBLE
usernameInputLayout.visibility = View.VISIBLE
}
/**
* Auth processes handling
*/
private fun onLogged() {
UserInfo.setStudent(lastValidUser)
view!!.findNavController().navigateUp()
}
override fun onResume() {
super.onResume()
if (activity?.intent?.data != null) {
getAccessToken(activity!!.intent!!.data!!)
} else {
authorize()
}
}
private fun getAccessToken(data: Uri) {
val response = Uri.parse(data.toString())
if (response.queryParameterNames.contains("code"))
viewModel.initToken(response.getQueryParameter("code") ?: "")
else throw Throwable("Code was not returned when trying to authenticate")
}
// Authorization
private fun authorize() {
val uri = buildUri("https://auth.fit.cvut.cz/oauth/authorize") {
appendParameters(hashMapOf(
"response_type" to "code",
"client_id" to AuthService.clientId,
"redirect_uri" to "kosapp://callback"
))
}
val intent = Intent(Intent.ACTION_VIEW, uri)
if (activity != null && intent.resolveActivity(activity!!.packageManager) != null) {
startActivity(intent)
}
}
}
package com.cvut.blackbird.experimental.auth
import android.util.Log
import com.cvut.blackbird.BlackBirdAC
import com.cvut.blackbird.model.*
import com.cvut.blackbird.model.entities.Student
import com.cvut.blackbird.model.services.AuthInfo
import com.cvut.blackbird.model.services.AuthServiceEx
import com.cvut.blackbird.model.services.KosServiceEx
import com.cvut.blackbird.model.support.AuthResult
import kotlinx.coroutines.experimental.delay
import javax.inject.Inject
class AuthModelEx: BlackBirdModel() {
@Inject
lateinit var authService: AuthServiceEx
@Inject
lateinit var kosService: KosServiceEx
init {
BlackBirdAC.graph.inject(this)
}
suspend fun initToken(code: String): AuthResult {
val result: AuthResult
try {
val response = authService.getTokenEx(code).execute()
if (response.body()?.token != null && response.body()?.refreshToken != null) {
AuthInfo.accessToken = response.body()!!.token
AuthInfo.refreshToken = response.body()!!.refreshToken!!
result = AuthResult.SUCCESS
} else
result = AuthResult.resolveReturnJson(response.errorBody()?.string())
} catch (e: Throwable) {
return AuthResult.UNEXPECTED_ERROR.apply {
errorDesc = "${e.message}\n${e.localizedMessage}"
}
}
return result
}
suspend fun refreshToken() : AuthResult {
val result: AuthResult
try {
val response = authService.refreshTokenEx().execute()
result = if (response.body()?.refreshToken != null && response.body()?.token != null) {
AuthInfo.accessToken = response.body()!!.token
AuthResult.SUCCESS.setDesc("Succesfully refreshed access token")
}
else {
val errorBody = response.errorBody()?.string()
AuthResult.resolveReturnJson(
errorBody,
"Failed in refreshing access token\n" +
"Message: ${response.message()}\n" +
"Error body: $errorBody"
)
}
} catch (e: Throwable) {
return AuthResult.UNEXPECTED_ERROR.apply {
errorDesc = "${e.message}\n${e.localizedMessage}"
}
}
return result
}
suspend fun requestStudent(username: String) : Result<Student> {
val result: Result<Student>
try {
val response = kosService.getStudentEx(username).execute()
result = if (response.body() != null)
Success(response.body()!!)
else {
val errorBody = response.errorBody()?.string()
Failure("Failed while fetching student\n" +
"Message: ${response.message()}\n" +
"Error body: $errorBody")
}
} catch (e: Throwable) {
return Failure("Failed while fetching student\n" +
"Error message: ${e.localizedMessage}")
}
return result
}
}
\ No newline at end of file
package com.cvut.blackbird.experimental.auth
import android.util.Log
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.withDefault
import com.cvut.blackbird.model.NotYet
import com.cvut.blackbird.model.Result
import com.cvut.blackbird.model.entities.Student
import com.cvut.blackbird.model.support.AuthResult
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.launch
class AuthViewModelEx : ViewModel() {
private val model: AuthModelEx = AuthModelEx()
//Output
private val _authStatus = MutableLiveData<AuthResult>() withDefault AuthResult.NOT_YET
private val _loadingStatus = MutableLiveData<Boolean>() withDefault false
private val _studentResult = MutableLiveData<Result<Student>>() withDefault NotYet()
val authStatus: LiveData<AuthResult> get() = _authStatus
val loadingStatus: LiveData<Boolean> get() = _loadingStatus
val studentResult: LiveData<Result<Student>> get() = _studentResult
fun initToken(code: String) {
launch { _loadingStatus.asBoolProgressStatus {
val token = model.initToken(code)
_authStatus.postValue(token)
} }
}
var refreshJob: Job? = null
fun refreshToken() {
refreshJob?.cancel()
refreshJob = launch {
_loadingStatus.asBoolProgressStatus {
val token = model.refreshToken()
_authStatus.postValue(token)
}
}
}
var studentJob: Job? = null
fun fetchStudent(username: String) {
studentJob?.cancel()
studentJob = _studentResult fetchUsing { model.requestStudent(username) }
}
}
\ No newline at end of file
......@@ -9,6 +9,7 @@ import com.cvut.blackbird.model.BlackBirdModel
import com.cvut.blackbird.model.Loading
import com.cvut.blackbird.model.Result
import com.cvut.blackbird.model.Success
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.launch
fun View.bindVisibilityTo(owner: LifecycleOwner, liveData: LiveData<Boolean>, invert: Boolean = false) {
......@@ -52,3 +53,9 @@ infix fun<T> MutableLiveData<Result<T>>.fetchUsing(job: suspend () -> Result<T>)
postValue(Loading())
postValue(job.invoke())
}
infix fun Job.indicateProgressBy(status: MutableLiveData<Boolean>): Job {
status.postValue(true)
invokeOnCompletion { status.postValue(false) }
return this
}
......@@ -10,7 +10,7 @@ import com.cvut.blackbird.model.services.UserInfo
import kotlinx.android.synthetic.main.black_bird_ac_activity.*
class BlackBirdMain : AppCompatActivity() {
lateinit var navController: NavController
private lateinit var navController: NavController
// private val disposeBag = ArrayList<Disposable>()
override fun onCreate(savedInstanceState: Bundle?) {
......@@ -21,7 +21,7 @@ class BlackBirdMain : AppCompatActivity() {
disableAuthPop()
if (UserInfo.username.isBlank())
navController.navigate(R.id.action_navigationFragment_to_authFragmentEx)
navController.navigate(R.id.action_navigationFragment_to_authFragment)
}
override fun onNavigateUp(): Boolean = findNavController(navHost.id).navigateUp()
......
......@@ -11,8 +11,8 @@ import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import androidx.lifecycle.Observer
import androidx.navigation.findNavController
import com.cvut.blackbird.R
import com.cvut.blackbird.experimental.auth.AuthViewModelEx
import com.cvut.blackbird.extensions.*
import com.cvut.blackbird.model.Success
import com.cvut.blackbird.model.entities.Student
......@@ -29,22 +29,22 @@ class AuthFragment : Fragment() {
fun newInstance() = AuthFragment()
}
private lateinit var viewModel: AuthViewModelEx
private lateinit var viewModel: AuthViewModel
private var snack: Snackbar? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.auth_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(AuthViewModelEx::class.java)
viewModel = ViewModelProviders.of(this).get(AuthViewModel::class.java)
setupUi()
setupBinding()
viewModel.refreshToken()
}
private fun setupUi() {
snack = Snackbar
.make(view!!, "", Snackbar.LENGTH_INDEFINITE)
......@@ -67,18 +67,19 @@ class AuthFragment : Fragment() {
} else if (usernameInput.text!!.isNotBlank()) {
usernameInputLayout.isErrorEnabled = true
usernameInputLayout.error = "Username not found!"
}
})
viewModel.authStatus.observe(this, Observer {
if (it != null) {
if (it == AuthResult.SUCCESS) {
viewModel.authStatus.observe(this, Observer { result ->
if (result != null) {
if (result == AuthResult.SUCCESS) {
if(UserInfo.username.isNotBlank()) onLogged()
else secondPhase()
} else {
snack?.setText("Error: ${it.name}")
infoMessage.text = it.errorDesc
if (!it.critical) authorize()
} else if (result != AuthResult.NOT_YET) {
snack?.setText("Error: ${result.name}")
infoMessage.text = result.errorDesc
if (!result.critical) authorize()
else snack!!.show()
}
} else {
......@@ -87,9 +88,10 @@ class AuthFragment : Fragment() {
}
})
authProgress.bindVisibilityTo(this, viewModel.loadingStatus)
authProgress.bindVisibilityTo(this, viewModel.authLoadingStatus)
usernameStatus.bindVisibilityTo(this, viewModel.userLoadingStatus)
acceptBtn.bindEnabledToSuccess(this, viewModel.studentResult)
viewModel.loadingStatus.observe(this, Observer {
viewModel.authLoadingStatus.observe(this, Observer {
if (it == true) snack?.dismiss()
})
}
......@@ -105,13 +107,31 @@ class AuthFragment : Fragment() {
usernameInputLayout.visibility = View.VISIBLE
}
/**
* Auth processes handling
*/
private fun onLogged() {
UserInfo.setStudent(lastValidUser)
view!!.findNavController().navigateUp()
}
override fun onResume() {
super.onResume()
if (activity?.intent?.data != null) {
getAccessToken(activity!!.intent!!.data!!)
} else {
authorize()
}
}
private fun getAccessToken(data: Uri) {
val response = Uri.parse(data.toString())
if (response.queryParameterNames.contains("code"))
viewModel.initToken(response.getQueryParameter("code") ?: "")
else throw Throwable("Code was not returned when trying to authenticate")
}
// Authorization
private fun authorize() {
......@@ -128,20 +148,4 @@ class AuthFragment : Fragment() {
startActivity(intent)
}
}
private fun getAccessToken(data: Uri?) {
if (data != null) {
val response = Uri.parse(data.toString())
if (response.queryParameterNames.contains("code"))
viewModel.initToken(response.getQueryParameter("code") ?: "")
else throw Throwable("Code was not returned when trying to authenticate")
}
}
override fun onResume() {
super.onResume()
if (activity != null && activity!!.intent != null) {
getAccessToken(activity?.intent?.data)
}
}
}
package com.cvut.blackbird.flows.authentication
import androidx.lifecycle.LiveData
import androidx.lifecycle.LiveDataReactiveStreams
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.cvut.blackbird.model.Loading
import com.cvut.blackbird.model.flows.AuthModel
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.model.NotYet
import com.cvut.blackbird.model.Result
import com.cvut.blackbird.model.entities.Student
import com.cvut.blackbird.model.flows.AuthModel
import com.cvut.blackbird.model.support.AuthResult
import com.github.pwittchen.reactivenetwork.library.rx2.Connectivity
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import io.reactivex.BackpressureStrategy
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.launch
class AuthViewModel : ViewModel() {
private val model: AuthModel = AuthModel()
//Private
//Input
private val requestSubject = PublishSubject.create<String>()
private val refreshSubject = PublishSubject.create<Unit>()
private val fetchStudent = PublishSubject.create<String>()
fun requestToken(code: String) { requestSubject.onNext(code) }
fun requestTokenRefresh() { refreshSubject.onNext(Unit) }
fun fetchStudent(username: String) {fetchStudent.onNext(username)}
//Output
private val _authStatus: LiveData<AuthResult>
private val _loadingStatus: LiveData<Boolean>
private val _studentResult: LiveData<Result<Student>>
val authStatus get() = _authStatus
val loadingStatus get() = _loadingStatus
val studentResult get() = _studentResult
init {
_authStatus = configAuthStatus(model.resultTokenRequest, model.resultTokenRefresh)
_loadingStatus = configLoadingStatus(model.resultTokenRequest, model.resultTokenRefresh)
_studentResult = LiveDataReactiveStreams.fromPublisher(model.resultStudentRequest.toFlowable(BackpressureStrategy.LATEST))
model.requestTokenRefresh = refreshSubject
model.requestNewToken = requestSubject
model.requestStudent = fetchStudent.throttleWithTimeout(500,TimeUnit.MILLISECONDS)
model.prepareInput()
}
private fun configAuthStatus(
request: Observable<AuthResult>,
refresh: Observable<AuthResult>
): LiveData<AuthResult> {
return LiveDataReactiveStreams.fromPublisher(
Observable
.merge(request, refresh)
.filter{ it != AuthResult.NOT_YET}
.toFlowable(BackpressureStrategy.LATEST)
)
private val _authStatus = MutableLiveData<AuthResult>() withDefault AuthResult.NOT_YET
private val _studentResult = MutableLiveData<Result<Student>>() withDefault NotYet()
private val _authLoadingStatus = MutableLiveData<Boolean>() withDefault false
private val _userLoadingStatus = MutableLiveData<Boolean>() withDefault false