Commit be9aa09f authored by Filip Wiesner's avatar Filip Wiesner

Getting rod of Rxjava, breaks hours/minutes, kotlinify Student entity

- Completely removed RxJava from project. Using Corutines now
- Line Descriptors in timetable now show hours and minutes
- Student entity is in Kotlin now
- Automatically refresh token on 401 error
- Quality Of Life improvements to BBModel
parent cc9cca77
......@@ -40,6 +40,8 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0-rc01'
implementation 'androidx.legacy:legacy-support-v4:1.0.0-rc02'
testImplementation 'junit:junit:4.12'
// Testing
androidTestImplementation 'androidx.test:runner:1.1.0-alpha4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha4'
......@@ -50,8 +52,7 @@ dependencies {
//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
implementation "android.arch.navigation:navigation-ui-ktx:$nav_version"
//Dependency injection
implementation "com.google.dagger:dagger:$dagger_version"
......@@ -63,8 +64,8 @@ dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'com.tickaroo.tikxml:retrofit-converter:0.8.13'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
implementation 'com.github.pwittchen:reactivenetwork-rx2:1.0.0'
// implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
// implementation 'com.github.pwittchen:reactivenetwork-rx2:1.0.0'
// Shared Prefs
implementation 'com.chibatching.kotpref:kotpref:2.5.0'
......@@ -77,9 +78,9 @@ dependencies {
implementation 'net.danlew:android.joda:2.9.9.4'
// Reactive stuff
implementation 'io.reactivex.rxjava2:rxkotlin:2.2.0'
implementation 'androidx.lifecycle:lifecycle-reactivestreams:2.0.0-rc01'
implementation 'com.jakewharton.rxbinding2:rxbinding-kotlin:2.1.1'
// implementation 'io.reactivex.rxjava2:rxkotlin:2.2.0'
// implementation 'androidx.lifecycle:lifecycle-reactivestreams:2.0.0-rc01'
// implementation 'com.jakewharton.rxbinding2:rxbinding-kotlin:2.1.1'
// Animation/Trasnition
implementation 'bg.devlabs.transitioner:transitioner:1.3'
......@@ -88,7 +89,7 @@ dependencies {
// Database
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-rxjava2:$room_version"
// implementation "androidx.room:room-rxjava2:$room_version"
implementation "androidx.paging:paging-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
......
......@@ -11,8 +11,8 @@ import net.danlew.android.joda.JodaTimeAndroid
class BlackBirdAC: Application(){
companion object {
const val LOG_TAG = "BLACK_BIRD"
//platformStatic allow access it from java code
@JvmStatic lateinit var graph: ApplicationComponent
}
......
......@@ -3,6 +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.model.BlackBirdModel
import com.cvut.blackbird.model.flows.AuthModel
import com.cvut.blackbird.model.flows.*
import dagger.Component
......@@ -11,7 +12,7 @@ import javax.inject.Singleton
@Singleton
@Component(modules = [ContextModule::class, RoomModule::class, ServicesModule::class])
interface ApplicationComponent {
fun inject(model: BlackBirdModel)
fun inject(model: AuthModel)
fun inject(model: TasksModel)
fun inject(model: TimetableModel)
......
......@@ -2,7 +2,6 @@ package com.cvut.blackbird.dinjection.modules
import android.content.Context
import com.cvut.blackbird.model.services.*
import com.github.pwittchen.reactivenetwork.library.rx2.Connectivity
import com.google.gson.GsonBuilder
import com.google.gson.JsonDeserializer
import com.google.gson.JsonPrimitive
......@@ -11,14 +10,11 @@ import com.tickaroo.tikxml.TikXml
import com.tickaroo.tikxml.retrofit.TikXmlConverterFactory
import dagger.Module
import dagger.Provides
import io.reactivex.Observable
import org.joda.time.DateTime
import org.joda.time.format.ISODateTimeFormat
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
......@@ -39,49 +35,26 @@ class ServicesModule(val context: Context) {
fun providesAuthService(): AuthService {
return Retrofit.Builder().apply {
baseUrl(AuthService.url)
addCallAdapterFactory(RxJava2CallAdapterFactory.create())
addConverterFactory(GsonConverterFactory.create())
}.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 {
return Retrofit.Builder().apply {
baseUrl(KosService.url)
addCallAdapterFactory(RxJava2CallAdapterFactory.create())
addConverterFactory(TikXmlConverterFactory.create(TikXml.Builder()
.exceptionOnUnreadXml(false)
.build()))
}.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 {
return Retrofit.Builder().apply {
baseUrl(SiriusService.url)
addCallAdapterFactory(RxJava2CallAdapterFactory.create())
addConverterFactory(GsonConverterFactory.create(
GsonBuilder()
.registerTypeAdapter(DateTime::class.java, dateTimeSer)
......@@ -90,10 +63,4 @@ class ServicesModule(val context: Context) {
))
}.build().create(SiriusService::class.java)
}
@Provides
@Singleton
fun providesNetworkState(): Observable<Connectivity> {
return ReactiveNetwork.observeNetworkConnectivity(context)
}
}
\ No newline at end of file
package com.cvut.blackbird.extensions
import org.joda.time.DateTime
fun DateTime.atEndOfTheDay(): DateTime {
return this.withHourOfDay(23)
.withMinuteOfHour(59)
.withSecondOfMinute(59)
.withMillisOfSecond(99)
}
fun DateTime.atStartOfTheDay(): DateTime {
return this.withHourOfDay(0)
.withMinuteOfHour(0)
.withSecondOfMinute(0)
.withMillisOfSecond(0)
}
\ No newline at end of file
......@@ -25,13 +25,22 @@ infix fun<T> Glue.enabledToSuccessOf(source: LiveData<Result<T>>) {
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)
}
infix fun Job.indicateProgressBy(status: MutableLiveData<Boolean>): Job {
status.postValue(true)
invokeOnCompletion { status.postValue(false) }
return this
}
suspend fun<T> MutableLiveData<Result<T>>.asProgressStatus(job: suspend () -> Unit) {
suspend infix fun<T> MutableLiveData<Result<T>>.asProgressStatus(job: suspend () -> Unit) {
postValue(Loading())
job.invoke()
}
......@@ -40,9 +49,3 @@ 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
}
package com.cvut.blackbird.extensions
import io.reactivex.Observable
import io.reactivex.disposables.Disposable
import io.reactivex.functions.Consumer
import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.BehaviorSubject
class Variable<T>(default: T) {
val subject: BehaviorSubject<T> = BehaviorSubject.createDefault(default)
var value: T
get() = subject.value!!
set(value) = subject.onNext(value)
fun subscribe(onNext: Consumer<in T>): Disposable = subject.subscribe(onNext)
fun asObservable(): Observable<T> = subject.subscribeOn(Schedulers.newThread())
}
infix fun Disposable.disposeTo(bag: MutableList<Disposable>) {
bag.add(this)
}
\ No newline at end of file
//import io.reactivex.Observable
//import io.reactivex.disposables.Disposable
//import io.reactivex.functions.Consumer
//import io.reactivex.schedulers.Schedulers
//import io.reactivex.subjects.BehaviorSubject
//
//
//class Variable<T>(default: T) {
// val subject: BehaviorSubject<T> = BehaviorSubject.createDefault(default)
//
// var value: T
// get() = subject.value!!
// set(value) = subject.onNext(value)
//
// fun subscribe(onNext: Consumer<in T>): Disposable = subject.subscribe(onNext)
//
// fun asObservable(): Observable<T> = subject.subscribeOn(Schedulers.newThread())
//}
//
//infix fun Disposable.disposeTo(bag: MutableList<Disposable>) {
// bag.add(this)
//}
\ No newline at end of file
......@@ -52,13 +52,9 @@ class AuthFragment : Fragment() {
snack = Snackbar
.make(view!!, "", Snackbar.LENGTH_INDEFINITE)
.setAction("Try Again") { viewModel.refreshToken() }
this bind acceptBtn clickTo ::onLogged
}
private fun setupBinding() {
send(usernameInput).textChangeTo(viewModel::fetchStudent).ignoreBlank()
observe(viewModel.studentResult) { result ->
if(result is Success) {
......@@ -90,6 +86,8 @@ class AuthFragment : Fragment() {
this bind authProgress visibilityTo viewModel.authLoadingStatus
this bind usernameStatus visibilityTo viewModel.userLoadingStatus
this bind acceptBtn enabledToSuccessOf viewModel.studentResult
this bind acceptBtn clickTo ::onLogged
send(usernameInput) .textChangeTo (viewModel::fetchStudent) .ignoreBlank()
}
private fun secondPhase() {
......
......@@ -39,7 +39,7 @@ class AuthViewModel : ViewModel() {
var refreshJob: Job? = null
fun refreshToken() {
refreshJob?.cancel()
refreshJob = launch { _authStatus.postValue(model.refreshToken()) }
refreshJob = launch { _authStatus.postValue(model.refreshTokenGuarded()) }
.indicateProgressBy(_authLoadingStatus)
}
......
......@@ -2,16 +2,20 @@ package com.cvut.blackbird.flows.tasks
import androidx.lifecycle.ViewModelProviders
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.*
import com.cvut.blackbird.BlackBirdAC
import com.cvut.blackbird.R
import com.cvut.blackbird.flows.zupport.EventListAdapter
import com.cvut.blackbird.model.Success
import com.cvut.blackbird.model.Failure
import com.cvut.blackbird.support.glue.bind
import com.cvut.blackbird.support.glue.observe
import com.cvut.blackbird.support.glue.toPassValueTo
import kotlinx.android.synthetic.main.tasks_fragment.*
import org.jetbrains.anko.toast
......@@ -34,7 +38,7 @@ class TasksFragment : Fragment() {
viewModel = ViewModelProviders.of(this).get(TasksViewModel::class.java)
setupUi()
setupBinding()
viewModel.requestExams()
viewModel.refresh()
}
private fun setupUi() {
......@@ -46,9 +50,10 @@ class TasksFragment : Fragment() {
}
private fun setupBinding() {
viewModel.exams.observe(this, Observer {
if (it is Success)
listAdapter.submitList(it.value)
})
this bind viewModel.exams toPassValueTo listAdapter::submitList
observe(viewModel.refreshResult) {
Log.d(BlackBirdAC.LOG_TAG, (it as? Failure)?.message ?: it.toString())
}
}
}
package com.cvut.blackbird.flows.tasks
import androidx.lifecycle.LiveData
import androidx.lifecycle.LiveDataReactiveStreams
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.cvut.blackbird.extensions.fetchUsing
import com.cvut.blackbird.extensions.indicateProgressBy
import com.cvut.blackbird.extensions.withDefault
import com.cvut.blackbird.model.*
import com.cvut.blackbird.model.entities.Event
import com.cvut.blackbird.model.flows.TasksModel
import io.reactivex.BackpressureStrategy
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
import kotlin.concurrent.thread
class TasksViewModel : ViewModel() {
private val model: TasksModel = TasksModel()
//Input
private val examsSubject = PublishSubject.create<Unit>()
fun requestExams() { examsSubject.onNext(Unit) }
//Output
private val _loadingStatus: LiveData<Boolean>
private val _examsUpdateRequestResult: LiveData<Result<List<Event>>>
private val _exams: LiveData<Result<List<Event>>>
val loadingStatus get() = _loadingStatus
val examsUpdateResult get() = _examsUpdateRequestResult
val exams get() = _exams
init {
_exams = configExams(model.exams)
_examsUpdateRequestResult = configExams(model.resultExamsUpdateRequest)
_loadingStatus = configLoadingStatus(model.resultExamsUpdateRequest)
private val _loadingStatus = MutableLiveData<Boolean>() withDefault false
private val _refreshResult = MutableLiveData<Result<Unit>>() withDefault NotYet()
private val _exams = MutableLiveData<List<Event>>() withDefault listOf()
model.requestExams = examsSubject
val loadingStatus: LiveData<Boolean> get() = _loadingStatus
val refreshResult: LiveData<Result<Unit>> get() = _refreshResult
val exams: LiveData<List<Event>> get() = _exams
init { configExams() }
model.prepareInput()
private var refreshJob: Job? = null
fun refresh() {
refreshJob?.cancel()
refreshJob = _refreshResult fetchUsing {
model.refreshEvents()
} indicateProgressBy _loadingStatus
}
private fun configExams(
exams: Observable<Result<List<Event>>>
): LiveData<Result<List<Event>>> {
return LiveDataReactiveStreams.fromPublisher(
exams
.filter{ it is Success || it is Failure }
.toFlowable(BackpressureStrategy.LATEST)
)
private fun configExams() {
launch {
val exams = model.getExams()
withContext(UI) {
exams.observeForever { timetable -> _exams.apply {
if (value?.equals(timetable) == false) postValue(timetable)
} }
}
}
}
private fun configLoadingStatus(
exams: Observable<Result<List<Event>>>
): LiveData<Boolean> {
return LiveDataReactiveStreams.fromPublisher(
exams
.map {
it is Loading
}.toFlowable(BackpressureStrategy.LATEST)
)
}
}
package com.cvut.blackbird.flows.timetable
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.util.TypedValue
import android.view.*
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import androidx.fragment.app.Fragment
......@@ -18,10 +17,7 @@ import com.cvut.blackbird.extensions.*
import com.cvut.blackbird.model.entities.Event
import com.cvut.blackbird.support.glue.bind
import com.cvut.blackbird.support.glue.clickTo
import com.cvut.blackbird.support.glue.send
import com.cvut.blackbird.support.glue.toPassValueTo
import org.joda.time.DateTime
import org.joda.time.DateTimeConstants
import com.cvut.blackbird.support.wobbly.WobblyAdapter
import com.cvut.blackbird.support.wobbly.WobblyElement
import kotlinx.android.synthetic.main.timetable_dots_fragment.*
......@@ -30,7 +26,6 @@ import kotlinx.android.synthetic.main.timetable_dots_fragment.*
class TimetableFragment : Fragment() {
companion object {
const val DAY_BALLS_SPACING = 0
const val MIN_DIFF_TO_NOTIFY = 15
fun newInstance() = TimetableFragment()
}
......@@ -45,50 +40,50 @@ class TimetableFragment : Fragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(activity!!).get(TimetableViewModel::class.java)
viewModel.refreshTimetable()
updateDateText(viewModel.date)
viewModel.refresh()
setupUI()
// setupUI()
wobblyTimetable.adapter = getAdapter()
setupBinding()
}
private fun setupUI() {
dotsLeftArrowBtn.setOnClickListener { updateDateText(viewModel.date.minusWeeks(1)) }
dotsRightArrowBtn.setOnClickListener { updateDateText(viewModel.date.plusWeeks(1)) }
}
private fun updateDateText(date: DateTime) {
viewModel.requestTimetable(
date.withDayOfWeek(DateTimeConstants.MONDAY)
.withHourOfDay(0).millis,
date.withDayOfWeek(DateTimeConstants.SUNDAY)
.withHourOfDay(23)
.withMinuteOfHour(59)
.withSecondOfMinute(59)
.withMillisOfSecond(99).millis
)
viewModel.date = date
dotsWeekDateTxt.text = date.monthOfYear().asText + " ${date.withDayOfWeek(1).dayOfMonth} - ${date.withDayOfWeek(5).dayOfMonth}"
}
// private fun setupUI() {
//
// }
private fun setupBinding() {
wobblyTimetable.adapter = getAdapter()
this bind dotsLeftArrowBtn clickTo viewModel::previousWeek
this bind dotsRightArrowBtn clickTo viewModel::nextWeek
this bind viewModel.timetable toPassValueTo ::onWeekChange
this bind viewModel.displayedWeek toPassValueTo { date ->
dotsWeekDateTxt.text = date.monthOfYear().asText + " ${date.withDayOfWeek(1).dayOfMonth} - ${date.withDayOfWeek(5).dayOfMonth}"
}
}
private fun onWeekChange(events: List<Event>) {
private fun onWeekChange(events: Map<Int,List<Event>>) {
wobblyTimetable.fadeRefresh {
events.groupBy { it.startsAt.dayOfWeek }.entries
.forEach {(day, events) ->
wobblyTimetable.addLineDescription(day - 1, 0, events.first().startsAt.toString("HH:mm"))
events.forEachIndexed { index, event ->
if (index > 0) {
val difference = event.startsAt.minuteOfDay - events[index - 1].endsAt.minuteOfDay
if (difference > MIN_DIFF_TO_NOTIFY)
wobblyTimetable.addLineDescription(day - 1, index, "$difference min")
}
}
events.forEach {(day, events) ->
wobblyTimetable.addLineDescription(day - 1, 0, events.first().startsAt.toString("HH:mm"))
events.forEachIndexed { index, event ->
if (index > 0) {
val difference = event.startsAt.minuteOfDay - events[index - 1].endsAt.minuteOfDay
if (difference > MIN_DIFF_TO_NOTIFY)
wobblyTimetable.addLineDescription(day - 1, index, buildString {
if (difference < 60) append(difference.toString() + " minutes")
else {
if (difference / 60 > 0) append((difference / 60).toString() + "h ")
if (difference % 60 > 0) append((difference % 60).toString() + "min")
else {
delete(length - 2, length)
append(" hours")
}
}
})
}
}
}
}
}
......@@ -98,51 +93,57 @@ class TimetableFragment : Fragment() {
// detail.revealFrom { top(); right() }
}
fun getAdapter():WobblyAdapter<Event> {
val days = arrayOf("Mon", "Tue", "Wed", "Thu", "Fri")
return object : WobblyAdapter<Event> {
override fun getColumnCount() = 5
override fun getItem(position: Int, column: Int) = viewModel.timetable.value?.filter { it.startsAt.dayOfWeek == column + 1}?.get(position) ?: Event.empty
override fun getItemView(position: Int, column: Int): WobblyElement = TimetableBall(activity!!).apply { setOnClickListener { onLectureClick(it) } }
override fun getItemCount(column: Int): Int = viewModel.timetable
.value?.filter { it.startsAt.dayOfWeek == column + 1 }?.size ?: 0
override fun getHeader(column: Int) = TextView(activity).apply {
background = GradientDrawable().apply {
setColor(Color.DKGRAY)
shape = GradientDrawable.OVAL }
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { setMargins(10.dpToPx,0,10.dpToPx,0) }
text = days[column]