Commit 03596a8d authored by Filip Wiesner's avatar Filip Wiesner

Logo, new nav, Corutine auth

parent d30dc82a
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
</value> </value>
</option> </option>
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">
......
...@@ -13,25 +13,46 @@ ...@@ -13,25 +13,46 @@
<LayoutPositions> <LayoutPositions>
<option name="myPosition"> <option name="myPosition">
<Point> <Point>
<option name="x" value="114" /> <option name="x" value="426" />
<option name="y" value="-135" /> <option name="y" value="-47" />
</Point> </Point>
</option> </option>
</LayoutPositions> </LayoutPositions>
</value> </value>
</entry> </entry>
<entry key="blackBirdMain"> <entry key="authFragmentEx">
<value> <value>
<LayoutPositions> <LayoutPositions>
<option name="myPosition"> <option name="myPosition">
<Point> <Point>
<option name="x" value="-217" /> <option name="x" value="419" />
<option name="y" value="-37" /> <option name="y" value="331" />
</Point> </Point>
</option> </option>
</LayoutPositions> </LayoutPositions>
</value> </value>
</entry> </entry>
<entry key="navigationFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="138" />
<option name="y" value="239" />
</Point>
</option>
<option name="myPositions">
<map>
<entry key="action_navigationFragment_to_authFragment">
<value>
<LayoutPositions />
</value>
</entry>
</map>
</option>
</LayoutPositions>
</value>
</entry>
</map> </map>
</option> </option>
</LayoutPositions> </LayoutPositions>
......
...@@ -30,6 +30,7 @@ dependencies { ...@@ -30,6 +30,7 @@ dependencies {
def anko_version = "0.10.5" def anko_version = "0.10.5"
def room_version = "2.0.0-rc01" def room_version = "2.0.0-rc01"
def dagger_version = "2.16" def dagger_version = "2.16"
def nav_version = "1.0.0-alpha05"
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
...@@ -47,6 +48,11 @@ dependencies { ...@@ -47,6 +48,11 @@ dependencies {
implementation 'androidx.core:core-ktx:1.0.0-rc02' implementation 'androidx.core:core-ktx:1.0.0-rc02'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.25.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.25.3'
//Navigation
implementation "android.arch.navigation:navigation-fragment-ktx:$nav_version"
// use -ktx for Kotlin
implementation "android.arch.navigation:navigation-ui-ktx:$nav_version" // use -ktx for Kotlin
//Dependency injection //Dependency injection
implementation "com.google.dagger:dagger:$dagger_version" implementation "com.google.dagger:dagger:$dagger_version"
implementation "com.google.dagger:dagger-android:$dagger_version" implementation "com.google.dagger:dagger-android:$dagger_version"
......
...@@ -7,9 +7,8 @@ ...@@ -7,9 +7,8 @@
<application <application
android:name=".BlackBirdAC" android:name=".BlackBirdAC"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@drawable/ic_raven"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<activity android:name=".flows.BlackBirdMain"> <activity android:name=".flows.BlackBirdMain">
......
...@@ -3,10 +3,9 @@ package com.cvut.blackbird.dinjection.components ...@@ -3,10 +3,9 @@ package com.cvut.blackbird.dinjection.components
import com.cvut.blackbird.dinjection.modules.ContextModule import com.cvut.blackbird.dinjection.modules.ContextModule
import com.cvut.blackbird.dinjection.modules.ServicesModule import com.cvut.blackbird.dinjection.modules.ServicesModule
import com.cvut.blackbird.dinjection.modules.RoomModule import com.cvut.blackbird.dinjection.modules.RoomModule
import com.cvut.blackbird.experimental.auth.AuthModelEx
import com.cvut.blackbird.model.flows.* import com.cvut.blackbird.model.flows.*
import com.github.pwittchen.reactivenetwork.library.rx2.Connectivity
import dagger.Component import dagger.Component
import io.reactivex.Observable
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
...@@ -14,8 +13,8 @@ import javax.inject.Singleton ...@@ -14,8 +13,8 @@ import javax.inject.Singleton
interface ApplicationComponent { interface ApplicationComponent {
fun inject(model: AuthModel) fun inject(model: AuthModel)
fun inject(model: ExamsModel) fun inject(model: TasksModel)
fun inject(model: TimetableModel) fun inject(model: TimetableModel)
fun inject(model: SearchModel) fun inject(model: SearchModel)
fun inject(model: AuthModelCR) fun inject(model: AuthModelEx)
} }
\ No newline at end of file
package com.cvut.blackbird.dinjection.modules package com.cvut.blackbird.dinjection.modules
import android.content.Context import android.content.Context
import com.cvut.blackbird.model.services.AuthService import com.cvut.blackbird.model.services.*
import com.cvut.blackbird.model.services.KosService
import com.cvut.blackbird.model.services.SiriusService
import com.github.pwittchen.reactivenetwork.library.rx2.Connectivity import com.github.pwittchen.reactivenetwork.library.rx2.Connectivity
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.JsonDeserializer import com.google.gson.JsonDeserializer
...@@ -46,6 +44,15 @@ class ServicesModule(val context: Context) { ...@@ -46,6 +44,15 @@ class ServicesModule(val context: Context) {
}.build().create(AuthService::class.java) }.build().create(AuthService::class.java)
} }
@Provides
@Singleton
fun providesAuthServiceEx(): AuthServiceEx {
return Retrofit.Builder().apply {
baseUrl(AuthService.url)
addConverterFactory(GsonConverterFactory.create())
}.build().create(AuthServiceEx::class.java)
}
@Provides @Provides
@Singleton @Singleton
fun providesKosService(): KosService { fun providesKosService(): KosService {
...@@ -58,6 +65,17 @@ class ServicesModule(val context: Context) { ...@@ -58,6 +65,17 @@ class ServicesModule(val context: Context) {
}.build().create(KosService::class.java) }.build().create(KosService::class.java)
} }
@Provides
@Singleton
fun providesKosServiceEx(): KosServiceEx {
return Retrofit.Builder().apply {
baseUrl(KosService.url)
addConverterFactory(TikXmlConverterFactory.create(TikXml.Builder()
.exceptionOnUnreadXml(false)
.build()))
}.build().create(KosServiceEx::class.java)
}
@Provides @Provides
@Singleton @Singleton
fun providesSiriusService(): SiriusService { fun providesSiriusService(): SiriusService {
......
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
package com.cvut.blackbird.extensions package com.cvut.blackbird.extensions
import androidx.fragment.app.Fragment
import android.app.Activity
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.core.content.ContextCompat.getSystemService
public fun Fragment.closeKeyboard() {
val imm = activity?.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
val view = activity?.currentFocus ?: View(activity)
imm.hideSoftInputFromWindow(view.windowToken, 0)
}
\ No newline at end of file
...@@ -4,10 +4,12 @@ import androidx.lifecycle.LifecycleOwner ...@@ -4,10 +4,12 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import android.view.View import android.view.View
import androidx.lifecycle.MutableLiveData
import com.cvut.blackbird.model.BlackBirdModel import com.cvut.blackbird.model.BlackBirdModel
import com.cvut.blackbird.model.Loading import com.cvut.blackbird.model.Loading
import com.cvut.blackbird.model.Result import com.cvut.blackbird.model.Result
import com.cvut.blackbird.model.Success import com.cvut.blackbird.model.Success
import kotlinx.coroutines.experimental.launch
fun View.bindVisibilityTo(owner: LifecycleOwner, liveData: LiveData<Boolean>, invert: Boolean = false) { fun View.bindVisibilityTo(owner: LifecycleOwner, liveData: LiveData<Boolean>, invert: Boolean = false) {
liveData.observe(owner , Observer{ liveData.observe(owner , Observer{
...@@ -31,4 +33,22 @@ fun <T> View.bindEnabledToSuccess(owner: LifecycleOwner, liveData: LiveData<Resu ...@@ -31,4 +33,22 @@ fun <T> View.bindEnabledToSuccess(owner: LifecycleOwner, liveData: LiveData<Resu
liveData.observe(owner , Observer{ liveData.observe(owner , Observer{
isClickable = it is Success isClickable = it is Success
}) })
} }
\ No newline at end of file
public infix fun<T> MutableLiveData<T>.withDefault(init: T) = apply { value = init }
suspend fun MutableLiveData<Boolean>.asBoolProgressStatus(job: suspend () -> Unit) {
postValue(true)
job.invoke()
postValue(false)