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

Logo, new nav, Corutine auth

parent d30dc82a
......@@ -27,7 +27,7 @@
</value>
</option>
</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" />
</component>
<component name="ProjectType">
......
......@@ -13,25 +13,46 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="114" />
<option name="y" value="-135" />
<option name="x" value="426" />
<option name="y" value="-47" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="blackBirdMain">
<entry key="authFragmentEx">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-217" />
<option name="y" value="-37" />
<option name="x" value="419" />
<option name="y" value="331" />
</Point>
</option>
</LayoutPositions>
</value>
</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>
</option>
</LayoutPositions>
......
......@@ -30,6 +30,7 @@ dependencies {
def anko_version = "0.10.5"
def room_version = "2.0.0-rc01"
def dagger_version = "2.16"
def nav_version = "1.0.0-alpha05"
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
......@@ -47,6 +48,11 @@ dependencies {
implementation 'androidx.core:core-ktx:1.0.0-rc02'
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
implementation "com.google.dagger:dagger:$dagger_version"
implementation "com.google.dagger:dagger-android:$dagger_version"
......
......@@ -7,9 +7,8 @@
<application
android:name=".BlackBirdAC"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:icon="@drawable/ic_raven"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".flows.BlackBirdMain">
......
......@@ -3,10 +3,9 @@ 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.*
import com.github.pwittchen.reactivenetwork.library.rx2.Connectivity
import dagger.Component
import io.reactivex.Observable
import javax.inject.Singleton
@Singleton
......@@ -14,8 +13,8 @@ import javax.inject.Singleton
interface ApplicationComponent {
fun inject(model: AuthModel)
fun inject(model: ExamsModel)
fun inject(model: TasksModel)
fun inject(model: TimetableModel)
fun inject(model: SearchModel)
fun inject(model: AuthModelCR)
fun inject(model: AuthModelEx)
}
\ No newline at end of file
package com.cvut.blackbird.dinjection.modules
import android.content.Context
import com.cvut.blackbird.model.services.AuthService
import com.cvut.blackbird.model.services.KosService
import com.cvut.blackbird.model.services.SiriusService
import com.cvut.blackbird.model.services.*
import com.github.pwittchen.reactivenetwork.library.rx2.Connectivity
import com.google.gson.GsonBuilder
import com.google.gson.JsonDeserializer
......@@ -46,6 +44,15 @@ class ServicesModule(val context: Context) {
}.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
@Singleton
fun providesKosService(): KosService {
......@@ -58,6 +65,17 @@ class ServicesModule(val context: Context) {
}.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
@Singleton
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
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
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import android.view.View
import androidx.lifecycle.MutableLiveData
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.launch
fun View.bindVisibilityTo(owner: LifecycleOwner, liveData: LiveData<Boolean>, invert: Boolean = false) {
liveData.observe(owner , Observer{
......@@ -32,3 +34,21 @@ fun <T> View.bindEnabledToSuccess(owner: LifecycleOwner, liveData: LiveData<Resu
isClickable = it is Success
})
}
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)
}
suspend fun<T> MutableLiveData<Result<T>>.asProgressStatus(job: suspend () -> Unit) {
postValue(Loading())
job.invoke()
}
infix fun<T> MutableLiveData<Result<T>>.fetchUsing(job: suspend () -> Result<T>) =
launch {
postValue(Loading())
postValue(job.invoke())
}
......@@ -2,81 +2,60 @@ package com.cvut.blackbird.flows
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.text.Html
import android.view.animation.AnimationUtils
import android.widget.TextView
import androidx.fragment.app.Fragment
import com.chibatching.kotpref.Kotpref
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import com.cvut.blackbird.R
import com.cvut.blackbird.extensions.disposeTo
import com.cvut.blackbird.flows.authentication.AuthFragment
import com.cvut.blackbird.model.services.AppState
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import org.jetbrains.anko.contentView
import com.cvut.blackbird.model.services.UserInfo
import kotlinx.android.synthetic.main.black_bird_ac_activity.*
class BlackBirdMain : AppCompatActivity() {
private val disposeBag = ArrayList<Disposable>()
lateinit var navController: NavController
// private val disposeBag = ArrayList<Disposable>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.black_bird_ac_activity)
setupConnectivityObserver()
if (savedInstanceState == null) {
goToAuth()
}
}
private fun setupConnectivityObserver() {
val networkSnack = Snackbar.make(contentView!!,"", Snackbar.LENGTH_SHORT)
var first = true
ReactiveNetwork.observeInternetConnectivity()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.filter {
if (first && it) {
first = false
false
} else true}
.subscribe {
networkSnack.dismiss()
if (it) networkSnack.setText(R.string.network_connected)
else networkSnack.setText(R.string.network_disconnected)
networkSnack.show()