Logger refactor (#175)

This commit is contained in:
Mikołaj Pich 2018-11-03 14:49:20 +01:00 committed by Rafał Borcz
parent 7f6f632b73
commit 5e30c8e949
22 changed files with 392 additions and 266 deletions

View File

@ -113,6 +113,7 @@ dependencies {
implementation "com.jakewharton.timber:timber:4.7.1" implementation "com.jakewharton.timber:timber:4.7.1"
implementation "at.favre.lib:slf4j-timber:1.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1"
implementation 'com.akaita.java:rxjava2-debug:1.3.0'
implementation("com.crashlytics.sdk.android:crashlytics:2.9.5@aar") { implementation("com.crashlytics.sdk.android:crashlytics:2.9.5@aar") {
transitive = true transitive = true
} }
@ -125,7 +126,7 @@ dependencies {
testImplementation "junit:junit:4.12" testImplementation "junit:junit:4.12"
testImplementation "io.mockk:mockk:1.8.9" testImplementation "io.mockk:mockk:1.8.9"
testImplementation "org.mockito:mockito-inline:2.23.0" testImplementation "org.mockito:mockito-inline:2.23.0"
testImplementation 'org.threeten:threetenbp:1.3.7' testImplementation 'org.threeten:threetenbp:1.3.8'
androidTestImplementation 'androidx.test:core:1.0.0' androidTestImplementation 'androidx.test:core:1.0.0'
androidTestImplementation 'androidx.test:runner:1.1.0' androidTestImplementation 'androidx.test:runner:1.1.0'

View File

@ -2,6 +2,7 @@ package io.github.wulkanowy
import android.content.Context import android.content.Context
import androidx.multidex.MultiDex import androidx.multidex.MultiDex
import com.akaita.java.rxjava2debug.RxJava2Debug
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.crashlytics.android.answers.Answers import com.crashlytics.android.answers.Answers
import com.crashlytics.android.core.CrashlyticsCore import com.crashlytics.android.core.CrashlyticsCore
@ -29,22 +30,20 @@ class WulkanowyApp : DaggerApplication() {
AndroidThreeTen.init(this) AndroidThreeTen.init(this)
initializeFabric() initializeFabric()
if (DEBUG) enableDebugLog() if (DEBUG) enableDebugLog()
RxJava2Debug.enableRxJava2AssemblyTracking(arrayOf(BuildConfig.APPLICATION_ID))
} }
private fun enableDebugLog() { private fun enableDebugLog() {
Timber.plant(DebugLogTree) Timber.plant(DebugLogTree())
FlexibleAdapter.enableLogs(Log.Level.DEBUG) FlexibleAdapter.enableLogs(Log.Level.DEBUG)
} }
private fun initializeFabric() { private fun initializeFabric() {
Fabric.with(Fabric.Builder(this) Fabric.with(Fabric.Builder(this).kits(
.kits(Crashlytics.Builder() Crashlytics.Builder().core(CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG || !BuildConfig.FABRIC_ENABLED).build()).build(),
.core(CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG || !BuildConfig.FABRIC_ENABLED).build()) Answers()
.build(), ).debuggable(BuildConfig.DEBUG).build())
Answers()) Timber.plant(CrashlyticsTree())
.debuggable(BuildConfig.DEBUG)
.build())
Timber.plant(CrashlyticsTree)
} }
override fun applicationInjector(): AndroidInjector<out DaggerApplication> { override fun applicationInjector(): AndroidInjector<out DaggerApplication> {

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.data package io.github.wulkanowy.data
import android.content.res.Resources import android.content.res.Resources
import com.akaita.java.rxjava2debug.RxJava2Debug
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.api.login.NotLoggedInException import io.github.wulkanowy.api.login.NotLoggedInException
import timber.log.Timber import timber.log.Timber
@ -14,7 +15,7 @@ open class ErrorHandler @Inject constructor(private val resources: Resources) {
var showErrorMessage: (String) -> Unit = {} var showErrorMessage: (String) -> Unit = {}
open fun proceed(error: Throwable) { open fun proceed(error: Throwable) {
Timber.e(error, "An exception occurred while the Wulkanowy was running") Timber.e(RxJava2Debug.getEnhancedStackTrace(error), "An exception occurred while the Wulkanowy was running")
showErrorMessage((when (error) { showErrorMessage((when (error) {
is UnknownHostException -> resources.getString(R.string.all_no_internet) is UnknownHostException -> resources.getString(R.string.all_no_internet)

View File

@ -4,6 +4,8 @@ import io.github.wulkanowy.api.Api
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.reactivex.Single import io.reactivex.Single
import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber
import java.net.URL import java.net.URL
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -75,6 +77,7 @@ class SessionRemote @Inject constructor(private val api: Api) {
fun initApi(student: Student, reInitialize: Boolean = false) { fun initApi(student: Student, reInitialize: Boolean = false) {
if (if (reInitialize) true else 0 == api.studentId) { if (if (reInitialize) true else 0 == api.studentId) {
api.run { api.run {
logLevel = HttpLoggingInterceptor.Level.NONE
email = student.email email = student.email
password = student.password password = student.password
symbol = student.symbol symbol = student.symbol
@ -84,6 +87,9 @@ class SessionRemote @Inject constructor(private val api: Api) {
studentId = student.studentId studentId = student.studentId
loginType = Api.LoginType.valueOf(student.loginType) loginType = Api.LoginType.valueOf(student.loginType)
notifyDataChanged() notifyDataChanged()
setInterceptor(HttpLoggingInterceptor(HttpLoggingInterceptor.Logger {
Timber.d(it)
}).setLevel(HttpLoggingInterceptor.Level.BASIC))
} }
} }
} }

View File

@ -3,8 +3,10 @@ package io.github.wulkanowy.ui.modules.about
import com.mikepenz.aboutlibraries.Libs import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.Libs.SpecialButton.SPECIAL1 import com.mikepenz.aboutlibraries.Libs.SpecialButton.SPECIAL1
import com.mikepenz.aboutlibraries.Libs.SpecialButton.SPECIAL2 import com.mikepenz.aboutlibraries.Libs.SpecialButton.SPECIAL2
import com.mikepenz.aboutlibraries.Libs.SpecialButton.SPECIAL3
import io.github.wulkanowy.data.ErrorHandler import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class AboutPresenter @Inject constructor(errorHandler: ErrorHandler) : BasePresenter<AboutView>(errorHandler) { class AboutPresenter @Inject constructor(errorHandler: ErrorHandler) : BasePresenter<AboutView>(errorHandler) {
@ -12,9 +14,15 @@ class AboutPresenter @Inject constructor(errorHandler: ErrorHandler) : BasePrese
fun onExtraSelect(type: Libs.SpecialButton?) { fun onExtraSelect(type: Libs.SpecialButton?) {
view?.run { view?.run {
when (type) { when (type) {
SPECIAL1 -> openSourceWebView() SPECIAL1 -> {
SPECIAL2 -> openIssuesWebView() Timber.i("Opening github page")
else -> TODO() openSourceWebView()
}
SPECIAL2 -> {
Timber.i("Opening issues page")
openIssuesWebView()
}
SPECIAL3 -> { }
} }
} }
} }

View File

@ -6,7 +6,13 @@ import io.github.wulkanowy.data.repositories.AttendanceRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SessionRepository import io.github.wulkanowy.data.repositories.SessionRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.logEvent
import io.github.wulkanowy.utils.nextSchoolDay
import io.github.wulkanowy.utils.previousOrSameSchoolDay
import io.github.wulkanowy.utils.previousSchoolDay
import io.github.wulkanowy.utils.toFormattedString
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDate.now import org.threeten.bp.LocalDate.now
import org.threeten.bp.LocalDate.ofEpochDay import org.threeten.bp.LocalDate.ofEpochDay
@ -14,11 +20,11 @@ import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject import javax.inject.Inject
class AttendancePresenter @Inject constructor( class AttendancePresenter @Inject constructor(
private val errorHandler: ErrorHandler, private val errorHandler: ErrorHandler,
private val schedulers: SchedulersProvider, private val schedulers: SchedulersProvider,
private val attendanceRepository: AttendanceRepository, private val attendanceRepository: AttendanceRepository,
private val sessionRepository: SessionRepository, private val sessionRepository: SessionRepository,
private val prefRepository: PreferencesRepository private val prefRepository: PreferencesRepository
) : BasePresenter<AttendanceView>(errorHandler) { ) : BasePresenter<AttendanceView>(errorHandler) {
lateinit var currentDate: LocalDate lateinit var currentDate: LocalDate
@ -34,11 +40,13 @@ class AttendancePresenter @Inject constructor(
fun onPreviousDay() { fun onPreviousDay() {
loadData(currentDate.previousSchoolDay) loadData(currentDate.previousSchoolDay)
reloadView() reloadView()
logEvent("Attendance day changed", mapOf("button" to "prev", "date" to currentDate.toFormattedString()))
} }
fun onNextDay() { fun onNextDay() {
loadData(currentDate.nextSchoolDay) loadData(currentDate.nextSchoolDay)
reloadView() reloadView()
logEvent("Attendance day changed", mapOf("button" to "next", "date" to currentDate.toFormattedString()))
} }
fun onSwipeRefresh() { fun onSwipeRefresh() {
@ -59,33 +67,34 @@ class AttendancePresenter @Inject constructor(
disposable.apply { disposable.apply {
clear() clear()
add(sessionRepository.getSemesters() add(sessionRepository.getSemesters()
.delay(200, MILLISECONDS) .delay(200, MILLISECONDS)
.map { it.single { semester -> semester.current } } .map { it.single { semester -> semester.current } }
.flatMap { attendanceRepository.getAttendance(it, date, date, forceRefresh) } .flatMap { attendanceRepository.getAttendance(it, date, date, forceRefresh) }
.map { list -> .map { list ->
if (prefRepository.showPresent) list if (prefRepository.showPresent) list
else list.filter { !it.presence } else list.filter { !it.presence }
}
.map { items -> items.map { AttendanceItem(it) } }
.map { items -> items.sortedBy { it.attendance.number } }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally {
view?.run {
hideRefresh()
showProgress(false)
} }
.map { items -> items.map { AttendanceItem(it) } } }
.map { items -> items.sortedBy { it.attendance.number } } .subscribe({
.subscribeOn(schedulers.backgroundThread) view?.apply {
.observeOn(schedulers.mainThread) updateData(it)
.doFinally { showEmpty(it.isEmpty())
view?.run { showContent(it.isNotEmpty())
hideRefresh()
showProgress(false)
}
}
.subscribe({
view?.apply {
updateData(it)
showEmpty(it.isEmpty())
showContent(it.isNotEmpty())
}
}) {
view?.run { showEmpty(isViewEmpty) }
errorHandler.proceed(it)
} }
logEvent("Attendance load", mapOf("items" to it.size, "forceRefresh" to forceRefresh, "date" to currentDate.toFormattedString()))
}) {
view?.run { showEmpty(isViewEmpty) }
errorHandler.proceed(it)
}
) )
} }
} }

View File

@ -6,7 +6,13 @@ import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.repositories.ExamRepository import io.github.wulkanowy.data.repositories.ExamRepository
import io.github.wulkanowy.data.repositories.SessionRepository import io.github.wulkanowy.data.repositories.SessionRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.logEvent
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.nextOrSameSchoolDay
import io.github.wulkanowy.utils.toFormattedString
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDate.now import org.threeten.bp.LocalDate.now
import org.threeten.bp.LocalDate.ofEpochDay import org.threeten.bp.LocalDate.ofEpochDay
@ -14,10 +20,10 @@ import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject import javax.inject.Inject
class ExamPresenter @Inject constructor( class ExamPresenter @Inject constructor(
private val errorHandler: ErrorHandler, private val errorHandler: ErrorHandler,
private val schedulers: SchedulersProvider, private val schedulers: SchedulersProvider,
private val examRepository: ExamRepository, private val examRepository: ExamRepository,
private val sessionRepository: SessionRepository private val sessionRepository: SessionRepository
) : BasePresenter<ExamView>(errorHandler) { ) : BasePresenter<ExamView>(errorHandler) {
lateinit var currentDate: LocalDate lateinit var currentDate: LocalDate
@ -33,11 +39,13 @@ class ExamPresenter @Inject constructor(
fun onPreviousWeek() { fun onPreviousWeek() {
loadData(currentDate.minusDays(7)) loadData(currentDate.minusDays(7))
reloadView() reloadView()
logEvent("Exam week changed", mapOf("button" to "prev", "date" to currentDate.toFormattedString()))
} }
fun onNextWeek() { fun onNextWeek() {
loadData(currentDate.plusDays(7)) loadData(currentDate.plusDays(7))
reloadView() reloadView()
logEvent("Exam week changed", mapOf("button" to "next", "date" to currentDate.toFormattedString()))
} }
fun onSwipeRefresh() { fun onSwipeRefresh() {
@ -58,30 +66,31 @@ class ExamPresenter @Inject constructor(
disposable.apply { disposable.apply {
clear() clear()
add(sessionRepository.getSemesters() add(sessionRepository.getSemesters()
.delay(200, MILLISECONDS) .delay(200, MILLISECONDS)
.map { it.single { semester -> semester.current } } .map { it.single { semester -> semester.current } }
.flatMap { .flatMap {
examRepository.getExams(it, currentDate.monday, currentDate.friday, forceRefresh) examRepository.getExams(it, currentDate.monday, currentDate.friday, forceRefresh)
}.map { it.groupBy { exam -> exam.date }.toSortedMap() } }.map { it.groupBy { exam -> exam.date }.toSortedMap() }
.map { createExamItems(it) } .map { createExamItems(it) }
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.doFinally { .doFinally {
view?.run { view?.run {
hideRefresh() hideRefresh()
showProgress(false) showProgress(false)
}
} }
.subscribe({ }
view?.apply { .subscribe({
updateData(it) view?.apply {
showEmpty(it.isEmpty()) updateData(it)
showContent(it.isNotEmpty()) showEmpty(it.isEmpty())
} showContent(it.isNotEmpty())
}) { }
view?.run { showEmpty(isViewEmpty) } logEvent("Exam load", mapOf("items" to it.size, "forceRefresh" to forceRefresh, "date" to currentDate.toFormattedString()))
errorHandler.proceed(it) }) {
}) view?.run { showEmpty(isViewEmpty) }
errorHandler.proceed(it)
})
} }
} }
@ -102,7 +111,7 @@ class ExamPresenter @Inject constructor(
showPreButton(!currentDate.minusDays(7).isHolidays) showPreButton(!currentDate.minusDays(7).isHolidays)
showNextButton(!currentDate.plusDays(7).isHolidays) showNextButton(!currentDate.plusDays(7).isHolidays)
updateNavigationWeek("${currentDate.monday.toFormattedString("dd.MM")} - " + updateNavigationWeek("${currentDate.monday.toFormattedString("dd.MM")} - " +
currentDate.friday.toFormattedString("dd.MM")) currentDate.friday.toFormattedString("dd.MM"))
} }
} }
} }

View File

@ -5,7 +5,9 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.repositories.SessionRepository import io.github.wulkanowy.data.repositories.SessionRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.SchedulersProvider import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.logEvent
import io.reactivex.Completable import io.reactivex.Completable
import timber.log.Timber
import java.util.concurrent.TimeUnit.MILLISECONDS import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject import javax.inject.Inject
@ -48,6 +50,7 @@ class GradePresenter @Inject constructor(
notifyChildrenSemesterChange() notifyChildrenSemesterChange()
loadChild(it.currentPageIndex) loadChild(it.currentPageIndex)
} }
logEvent("Semester changed", mapOf("number" to index + 1))
} }
} }

View File

@ -8,15 +8,17 @@ import io.github.wulkanowy.data.repositories.SessionRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.SchedulersProvider import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.calcAverage import io.github.wulkanowy.utils.calcAverage
import io.github.wulkanowy.utils.logEvent
import io.github.wulkanowy.utils.valueColor import io.github.wulkanowy.utils.valueColor
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class GradeDetailsPresenter @Inject constructor( class GradeDetailsPresenter @Inject constructor(
private val errorHandler: ErrorHandler, private val errorHandler: ErrorHandler,
private val schedulers: SchedulersProvider, private val schedulers: SchedulersProvider,
private val gradeRepository: GradeRepository, private val gradeRepository: GradeRepository,
private val sessionRepository: SessionRepository) : BasePresenter<GradeDetailsView>(errorHandler) { private val sessionRepository: SessionRepository
) : BasePresenter<GradeDetailsView>(errorHandler) {
override fun onAttachView(view: GradeDetailsView) { override fun onAttachView(view: GradeDetailsView) {
super.onAttachView(view) super.onAttachView(view)
@ -25,27 +27,28 @@ class GradeDetailsPresenter @Inject constructor(
fun onParentViewLoadData(semesterId: Int, forceRefresh: Boolean) { fun onParentViewLoadData(semesterId: Int, forceRefresh: Boolean) {
disposable.add(sessionRepository.getSemesters() disposable.add(sessionRepository.getSemesters()
.flatMap { gradeRepository.getGrades(it.first { item -> item.semesterId == semesterId }, forceRefresh) } .flatMap { gradeRepository.getGrades(it.first { item -> item.semesterId == semesterId }, forceRefresh) }
.map { createGradeItems(it.groupBy { grade -> grade.subject }.toSortedMap()) } .map { createGradeItems(it.groupBy { grade -> grade.subject }.toSortedMap()) }
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.doFinally { .doFinally {
view?.run { view?.run {
showRefresh(false) showRefresh(false)
showProgress(false) showProgress(false)
notifyParentDataLoaded(semesterId) notifyParentDataLoaded(semesterId)
}
} }
.subscribe({ }
view?.run { .subscribe({
showEmpty(it.isEmpty()) view?.run {
showContent(it.isNotEmpty()) showEmpty(it.isEmpty())
updateData(it) showContent(it.isNotEmpty())
} updateData(it)
}) { }
view?.run { showEmpty(isViewEmpty) } logEvent("Grade details load", mapOf("items" to it.size, "forceRefresh" to forceRefresh))
errorHandler.proceed(it) }) {
}) view?.run { showEmpty(isViewEmpty) }
errorHandler.proceed(it)
})
} }
fun onGradeItemSelected(item: AbstractFlexibleItem<*>?) { fun onGradeItemSelected(item: AbstractFlexibleItem<*>?) {
@ -92,16 +95,16 @@ class GradeDetailsPresenter @Inject constructor(
return items.map { return items.map {
it.value.calcAverage().let { average -> it.value.calcAverage().let { average ->
GradeDetailsHeader( GradeDetailsHeader(
subject = it.key, subject = it.key,
average = formatAverage(average), average = formatAverage(average),
number = view?.getGradeNumberString(it.value.size).orEmpty(), number = view?.getGradeNumberString(it.value.size).orEmpty(),
newGrades = it.value.filter { grade -> !grade.isRead }.size newGrades = it.value.filter { grade -> !grade.isRead }.size
).apply { ).apply {
subItems = it.value.map { item -> subItems = it.value.map { item ->
GradeDetailsItem( GradeDetailsItem(
grade = item, grade = item,
weightString = view?.weightString.orEmpty(), weightString = view?.weightString.orEmpty(),
valueColor = item.valueColor valueColor = item.valueColor
) )
} }
} }
@ -118,9 +121,9 @@ class GradeDetailsPresenter @Inject constructor(
private fun updateGrade(grade: Grade) { private fun updateGrade(grade: Grade) {
disposable.add(gradeRepository.updateGrade(grade) disposable.add(gradeRepository.updateGrade(grade)
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.subscribe({}) { error -> errorHandler.proceed(error) }) .subscribe({}) { error -> errorHandler.proceed(error) })
Timber.d("Grade ${grade.id} updated") Timber.d("Grade ${grade.id} updated")
} }
} }

View File

@ -8,17 +8,18 @@ import io.github.wulkanowy.data.repositories.SessionRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.SchedulersProvider import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.calcAverage import io.github.wulkanowy.utils.calcAverage
import io.github.wulkanowy.utils.logEvent
import java.lang.String.format import java.lang.String.format
import java.util.Locale.FRANCE import java.util.Locale.FRANCE
import javax.inject.Inject import javax.inject.Inject
class GradeSummaryPresenter @Inject constructor( class GradeSummaryPresenter @Inject constructor(
private val errorHandler: ErrorHandler, private val errorHandler: ErrorHandler,
private val gradeSummaryRepository: GradeSummaryRepository, private val gradeSummaryRepository: GradeSummaryRepository,
private val gradeRepository: GradeRepository, private val gradeRepository: GradeRepository,
private val sessionRepository: SessionRepository, private val sessionRepository: SessionRepository,
private val schedulers: SchedulersProvider) private val schedulers: SchedulersProvider
: BasePresenter<GradeSummaryView>(errorHandler) { ) : BasePresenter<GradeSummaryView>(errorHandler) {
override fun onAttachView(view: GradeSummaryView) { override fun onAttachView(view: GradeSummaryView) {
super.onAttachView(view) super.onAttachView(view)
@ -27,43 +28,44 @@ class GradeSummaryPresenter @Inject constructor(
fun onParentViewLoadData(semesterId: Int, forceRefresh: Boolean) { fun onParentViewLoadData(semesterId: Int, forceRefresh: Boolean) {
disposable.add(sessionRepository.getSemesters() disposable.add(sessionRepository.getSemesters()
.map { semester -> semester.first { it.semesterId == semesterId } } .map { semester -> semester.first { it.semesterId == semesterId } }
.flatMap { .flatMap {
gradeSummaryRepository.getGradesSummary(it, forceRefresh) gradeSummaryRepository.getGradesSummary(it, forceRefresh)
.flatMap { gradesSummary -> .flatMap { gradesSummary ->
gradeRepository.getGrades(it, forceRefresh) gradeRepository.getGrades(it, forceRefresh)
.map { grades -> .map { grades ->
grades.groupBy { grade -> grade.subject } grades.groupBy { grade -> grade.subject }
.mapValues { entry -> entry.value.calcAverage() } .mapValues { entry -> entry.value.calcAverage() }
.filterValues { value -> value != 0.0 } .filterValues { value -> value != 0.0 }
.let { averages -> .let { averages ->
createGradeSummaryItems(gradesSummary, averages) to createGradeSummaryItems(gradesSummary, averages) to
GradeSummaryScrollableHeader( GradeSummaryScrollableHeader(
formatAverage(gradesSummary.calcAverage()), formatAverage(gradesSummary.calcAverage()),
formatAverage(averages.values.average()) formatAverage(averages.values.average())
) )
} }
}
} }
}
}
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally {
view?.run {
showRefresh(false)
showProgress(false)
notifyParentDataLoaded(semesterId)
} }
.subscribeOn(schedulers.backgroundThread) }.subscribe({
.observeOn(schedulers.mainThread) view?.run {
.doFinally { showEmpty(it.first.isEmpty())
view?.run { showContent(it.first.isNotEmpty())
showRefresh(false) updateDataSet(it.first, it.second)
showProgress(false) }
notifyParentDataLoaded(semesterId) logEvent("Grade summary load", mapOf("items" to it.first.size, "forceRefresh" to forceRefresh))
} }) {
}.subscribe({ view?.run { showEmpty(isViewEmpty) }
view?.run { errorHandler.proceed(it)
showEmpty(it.first.isEmpty()) })
showContent(it.first.isNotEmpty())
updateDataSet(it.first, it.second)
}
}) {
view?.run { showEmpty(isViewEmpty) }
errorHandler.proceed(it)
})
} }
fun onSwipeRefresh() { fun onSwipeRefresh() {
@ -88,24 +90,24 @@ class GradeSummaryPresenter @Inject constructor(
} }
private fun createGradeSummaryItems(gradesSummary: List<GradeSummary>, averages: Map<String, Double>) private fun createGradeSummaryItems(gradesSummary: List<GradeSummary>, averages: Map<String, Double>)
: List<GradeSummaryItem> { : List<GradeSummaryItem> {
return gradesSummary.filter { !checkEmpty(it, averages) } return gradesSummary.filter { !checkEmpty(it, averages) }
.flatMap { gradeSummary -> .flatMap { gradeSummary ->
GradeSummaryHeader( GradeSummaryHeader(
name = gradeSummary.subject, name = gradeSummary.subject,
average = formatAverage(averages.getOrElse(gradeSummary.subject) { 0.0 }, "") average = formatAverage(averages.getOrElse(gradeSummary.subject) { 0.0 }, "")
).let { ).let {
listOf(GradeSummaryItem( listOf(GradeSummaryItem(
header = it, header = it,
title = view?.predictedString.orEmpty(), title = view?.predictedString.orEmpty(),
grade = gradeSummary.predictedGrade grade = gradeSummary.predictedGrade
), GradeSummaryItem( ), GradeSummaryItem(
header = it, header = it,
title = view?.finalString.orEmpty(), title = view?.finalString.orEmpty(),
grade = gradeSummary.finalGrade grade = gradeSummary.finalGrade
)) ))
}
} }
}
} }
private fun checkEmpty(gradeSummary: GradeSummary, averages: Map<String, Double>): Boolean { private fun checkEmpty(gradeSummary: GradeSummary, averages: Map<String, Double>): Boolean {

View File

@ -4,13 +4,16 @@ import io.github.wulkanowy.data.repositories.SessionRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
import io.github.wulkanowy.utils.SchedulersProvider import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.logEvent
import io.github.wulkanowy.utils.logRegister
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class LoginFormPresenter @Inject constructor( class LoginFormPresenter @Inject constructor(
private val schedulers: SchedulersProvider, private val schedulers: SchedulersProvider,
private val errorHandler: LoginErrorHandler, private val errorHandler: LoginErrorHandler,
private val sessionRepository: SessionRepository) private val sessionRepository: SessionRepository
: BasePresenter<LoginFormView>(errorHandler) { ) : BasePresenter<LoginFormView>(errorHandler) {
private var wasEmpty = false private var wasEmpty = false
@ -22,33 +25,39 @@ class LoginFormPresenter @Inject constructor(
fun attemptLogin(email: String, password: String, symbol: String, endpoint: String) { fun attemptLogin(email: String, password: String, symbol: String, endpoint: String) {
if (!validateCredentials(email, password, symbol)) return if (!validateCredentials(email, password, symbol)) return
disposable.add(sessionRepository.getConnectedStudents(email, password, symbol, endpoint) disposable.add(sessionRepository.getConnectedStudents(email, password, symbol, endpoint)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.doOnSubscribe { .doOnSubscribe {
view?.run { view?.run {
hideSoftKeyboard() hideSoftKeyboard()
showLoginProgress(true) showLoginProgress(true)
errorHandler.doOnBadCredentials = { errorHandler.doOnBadCredentials = {
setErrorPassIncorrect() setErrorPassIncorrect()
showSoftKeyboard() showSoftKeyboard()
} Timber.i("Entered wrong username or password")
} }
sessionRepository.clearCache()
} }
.doFinally { view?.showLoginProgress(false) } sessionRepository.clearCache()
.subscribe({ }
view?.run { .doFinally { view?.showLoginProgress(false) }
if (it.isEmpty() && !wasEmpty) { .subscribe({
showSymbolInput() view?.run {
wasEmpty = true if (it.isEmpty() && !wasEmpty) {
} else if (it.isEmpty() && wasEmpty) { showSymbolInput()
showSymbolInput() wasEmpty = true
setErrorSymbolIncorrect() } else if (it.isEmpty() && wasEmpty) {
} else { showSymbolInput()
switchNextView() setErrorSymbolIncorrect()
} logRegister("No student found", false, if (symbol.isEmpty()) "nil" else symbol, endpoint)
} else {
switchNextView()
logEvent("Found students", mapOf("students" to it.size, "symbol" to it.joinToString { student -> student.symbol }, "endpoint" to endpoint))
} }
}, { errorHandler.proceed(it) })) }
}, {
errorHandler.proceed(it)
logRegister(it.localizedMessage, false, if (symbol.isEmpty()) "nil" else symbol, endpoint)
}))
} }
private fun validateCredentials(login: String, password: String, symbol: String): Boolean { private fun validateCredentials(login: String, password: String, symbol: String): Boolean {

View File

@ -5,13 +5,14 @@ import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.SessionRepository import io.github.wulkanowy.data.repositories.SessionRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.SchedulersProvider import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.logRegister
import javax.inject.Inject import javax.inject.Inject
class LoginOptionsPresenter @Inject constructor( class LoginOptionsPresenter @Inject constructor(
private val errorHandler: ErrorHandler, private val errorHandler: ErrorHandler,
private val repository: SessionRepository, private val repository: SessionRepository,
private val schedulers: SchedulersProvider) private val schedulers: SchedulersProvider
: BasePresenter<LoginOptionsView>(errorHandler) { ) : BasePresenter<LoginOptionsView>(errorHandler) {
override fun onAttachView(view: LoginOptionsView) { override fun onAttachView(view: LoginOptionsView) {
super.onAttachView(view) super.onAttachView(view)
@ -20,25 +21,30 @@ class LoginOptionsPresenter @Inject constructor(
fun refreshData() { fun refreshData() {
disposable.add(repository.cachedStudents disposable.add(repository.cachedStudents
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.doOnSubscribe { view?.showActionBar(true) } .doOnSubscribe { view?.showActionBar(true) }
.doFinally { repository.clearCache() } .doFinally { repository.clearCache() }
.subscribe({ .subscribe({
view?.updateData(it.map { student -> view?.updateData(it.map { student ->
LoginOptionsItem(student) LoginOptionsItem(student)
}) })
}, { errorHandler.proceed(it) })) }, { errorHandler.proceed(it) }))
} }
fun onSelectStudent(student: Student) { fun onSelectStudent(student: Student) {
disposable.add(repository.saveStudent(student) disposable.add(repository.saveStudent(student)
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.doOnSubscribe { _ -> .doOnSubscribe {
view?.showLoginProgress(true) view?.run {
view?.showActionBar(false) showLoginProgress(true)
showActionBar(false)
} }
.subscribe({ view?.openMainView() }, { errorHandler.proceed(it) })) }
.subscribe({
logRegister("Success", true, student.symbol, student.endpoint)
view?.openMainView()
}, { errorHandler.proceed(it) }))
} }
} }

View File

@ -18,6 +18,7 @@ import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.more.MoreFragment import io.github.wulkanowy.ui.modules.more.MoreFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.logLogin
import io.github.wulkanowy.utils.safelyPopFragment import io.github.wulkanowy.utils.safelyPopFragment
import io.github.wulkanowy.utils.setOnViewChangeListener import io.github.wulkanowy.utils.setOnViewChangeListener
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*

View File

@ -4,6 +4,7 @@ import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.services.job.ServiceHelper import io.github.wulkanowy.services.job.ServiceHelper
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.logLogin
import javax.inject.Inject import javax.inject.Inject
class MainPresenter @Inject constructor( class MainPresenter @Inject constructor(
@ -21,6 +22,11 @@ class MainPresenter @Inject constructor(
initView() initView()
} }
when (initMenuIndex) {
1 -> logLogin("Grades")
3 -> logLogin("Timetable")
}
serviceHelper.startFullSyncService() serviceHelper.startFullSyncService()
} }

View File

@ -5,6 +5,7 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.services.job.ServiceHelper import io.github.wulkanowy.services.job.ServiceHelper
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.isHolidays import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.logEvent
import org.threeten.bp.LocalDate.now import org.threeten.bp.LocalDate.now
import javax.inject.Inject import javax.inject.Inject
@ -36,5 +37,7 @@ class SettingsPresenter @Inject constructor(
view?.setTheme(preferencesRepository.currentTheme) view?.setTheme(preferencesRepository.currentTheme)
} }
} }
logEvent("Setting changed", mapOf("name" to key))
} }
} }

View File

@ -4,6 +4,7 @@ import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SessionRepository import io.github.wulkanowy.data.repositories.SessionRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.logLogin
import javax.inject.Inject import javax.inject.Inject
class SplashPresenter @Inject constructor( class SplashPresenter @Inject constructor(
@ -16,7 +17,10 @@ class SplashPresenter @Inject constructor(
super.onAttachView(view) super.onAttachView(view)
view.run { view.run {
setCurrentThemeMode(preferencesRepository.currentTheme) setCurrentThemeMode(preferencesRepository.currentTheme)
if (sessionRepository.isSessionSaved) openMainView() else openLoginView() if (sessionRepository.isSessionSaved) {
logLogin("Open app")
openMainView()
} else openLoginView()
} }
} }
} }

View File

@ -5,7 +5,13 @@ import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.repositories.SessionRepository import io.github.wulkanowy.data.repositories.SessionRepository
import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.logEvent
import io.github.wulkanowy.utils.nextOrSameSchoolDay
import io.github.wulkanowy.utils.nextSchoolDay
import io.github.wulkanowy.utils.previousSchoolDay
import io.github.wulkanowy.utils.toFormattedString
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDate.now import org.threeten.bp.LocalDate.now
import org.threeten.bp.LocalDate.ofEpochDay import org.threeten.bp.LocalDate.ofEpochDay
@ -13,10 +19,10 @@ import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject import javax.inject.Inject
class TimetablePresenter @Inject constructor( class TimetablePresenter @Inject constructor(
private val errorHandler: ErrorHandler, private val errorHandler: ErrorHandler,
private val schedulers: SchedulersProvider, private val schedulers: SchedulersProvider,
private val timetableRepository: TimetableRepository, private val timetableRepository: TimetableRepository,
private val sessionRepository: SessionRepository private val sessionRepository: SessionRepository
) : BasePresenter<TimetableView>(errorHandler) { ) : BasePresenter<TimetableView>(errorHandler) {
lateinit var currentDate: LocalDate lateinit var currentDate: LocalDate
@ -32,11 +38,13 @@ class TimetablePresenter @Inject constructor(
fun onPreviousDay() { fun onPreviousDay() {
loadData(currentDate.previousSchoolDay) loadData(currentDate.previousSchoolDay)
reloadView() reloadView()
logEvent("Timetable day changed", mapOf("button" to "prev", "date" to currentDate.toFormattedString()))
} }
fun onNextDay() { fun onNextDay() {
loadData(currentDate.nextSchoolDay) loadData(currentDate.nextSchoolDay)
reloadView() reloadView()
logEvent("Timetable day changed", mapOf("button" to "next", "date" to currentDate.toFormattedString()))
} }
fun onSwipeRefresh() { fun onSwipeRefresh() {
@ -57,29 +65,30 @@ class TimetablePresenter @Inject constructor(
disposable.apply { disposable.apply {
clear() clear()
add(sessionRepository.getSemesters() add(sessionRepository.getSemesters()
.delay(200, MILLISECONDS) .delay(200, MILLISECONDS)
.map { it.single { semester -> semester.current } } .map { it.single { semester -> semester.current } }
.flatMap { timetableRepository.getTimetable(it, currentDate, currentDate, forceRefresh) } .flatMap { timetableRepository.getTimetable(it, currentDate, currentDate, forceRefresh) }
.map { items -> items.map { TimetableItem(it, view?.roomString.orEmpty()) } } .map { items -> items.map { TimetableItem(it, view?.roomString.orEmpty()) } }
.map { items -> items.sortedBy { it.lesson.number } } .map { items -> items.sortedBy { it.lesson.number } }
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.doFinally { .doFinally {
view?.run { view?.run {
hideRefresh() hideRefresh()
showProgress(false) showProgress(false)
}
} }
.subscribe({ }
view?.apply { .subscribe({
updateData(it) view?.apply {
showEmpty(it.isEmpty()) updateData(it)
showContent(it.isNotEmpty()) showEmpty(it.isEmpty())
} showContent(it.isNotEmpty())
}) { }
view?.run { showEmpty(isViewEmpty()) } logEvent("Timetable load", mapOf("items" to it.size, "forceRefresh" to forceRefresh, "date" to currentDate.toFormattedString()))
errorHandler.proceed(it) }) {
}) view?.run { showEmpty(isViewEmpty()) }
errorHandler.proceed(it)
})
} }
} }

View File

@ -15,6 +15,7 @@ import io.github.wulkanowy.data.db.SharedPrefHelper
import io.github.wulkanowy.services.widgets.TimetableWidgetService import io.github.wulkanowy.services.widgets.TimetableWidgetService
import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainActivity.Companion.EXTRA_START_MENU_INDEX import io.github.wulkanowy.ui.modules.main.MainActivity.Companion.EXTRA_START_MENU_INDEX
import io.github.wulkanowy.utils.logEvent
import io.github.wulkanowy.utils.nextOrSameSchoolDay import io.github.wulkanowy.utils.nextOrSameSchoolDay
import io.github.wulkanowy.utils.nextSchoolDay import io.github.wulkanowy.utils.nextSchoolDay
import io.github.wulkanowy.utils.previousSchoolDay import io.github.wulkanowy.utils.previousSchoolDay
@ -78,16 +79,19 @@ class TimetableWidgetProvider : AppWidgetProvider() {
AndroidInjection.inject(this, context) AndroidInjection.inject(this, context)
intent?.let { intent?.let {
val widgetKey = "timetable_widget_${it.getIntExtra(EXTRA_TOGGLED_WIDGET_ID, 0)}" val widgetKey = "timetable_widget_${it.getIntExtra(EXTRA_TOGGLED_WIDGET_ID, 0)}"
when (it.getStringExtra(EXTRA_BUTTON_TYPE)) { it.getStringExtra(EXTRA_BUTTON_TYPE).let { button ->
BUTTON_NEXT -> { when (button) {
LocalDate.ofEpochDay(sharedPref.getLong(widgetKey, 0)).nextSchoolDay BUTTON_NEXT -> {
.let { date -> sharedPref.putLong(widgetKey, date.toEpochDay(), true) } LocalDate.ofEpochDay(sharedPref.getLong(widgetKey, 0)).nextSchoolDay
.let { date -> sharedPref.putLong(widgetKey, date.toEpochDay(), true) }
}
BUTTON_PREV -> {
LocalDate.ofEpochDay(sharedPref.getLong(widgetKey, 0)).previousSchoolDay
.let { date -> sharedPref.putLong(widgetKey, date.toEpochDay(), true) }
}
BUTTON_RESET -> sharedPref.putLong(widgetKey, LocalDate.now().nextOrSameSchoolDay.toEpochDay(), true)
} }
BUTTON_PREV -> { button?.also { btn -> if (btn.isNotBlank()) logEvent("Widget day changed", mapOf("button" to button)) }
LocalDate.ofEpochDay(sharedPref.getLong(widgetKey, 0)).previousSchoolDay
.let { date -> sharedPref.putLong(widgetKey, date.toEpochDay(), true) }
}
BUTTON_RESET -> sharedPref.putLong(widgetKey, LocalDate.now().nextOrSameSchoolDay.toEpochDay(), true)
} }
} }
super.onReceive(context, intent) super.onReceive(context, intent)

View File

@ -0,0 +1,48 @@
package io.github.wulkanowy.utils
import com.crashlytics.android.answers.Answers
import com.crashlytics.android.answers.CustomEvent
import com.crashlytics.android.answers.LoginEvent
import com.crashlytics.android.answers.SignUpEvent
import timber.log.Timber
fun logLogin(method: String) {
try {
Answers.getInstance().logLogin(LoginEvent().putMethod(method))
} catch (e: Throwable) {
Timber.d(e)
}
}
fun logRegister(message: String, result: Boolean, symbol: String, endpoint: String) {
try {
Answers.getInstance().logSignUp(SignUpEvent()
.putMethod("Login activity")
.putSuccess(result)
.putCustomAttribute("symbol", symbol)
.putCustomAttribute("message", message)
.putCustomAttribute("endpoint", endpoint)
)
} catch (e: Throwable) {
Timber.d(e)
}
}
fun <T> logEvent(name: String, params: Map<String, T>) {
try {
Answers.getInstance().logCustom(CustomEvent(name)
.apply {
params.forEach {
when {
it.value is String -> putCustomAttribute(it.key, it.value as String)
it.value is Number -> putCustomAttribute(it.key, it.value as Number)
it.value is Boolean -> putCustomAttribute(it.key, if ((it.value as Boolean)) "true" else "false")
else -> Timber.w("logEvent() unknown value type: ${it.value}")
}
}
}
)
} catch (e: Throwable) {
Timber.d(e)
}
}

View File

@ -3,25 +3,20 @@ package io.github.wulkanowy.utils
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import timber.log.Timber import timber.log.Timber
object CrashlyticsTree : Timber.Tree() { class DebugLogTree : Timber.DebugTree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
super.log(priority, "Wulkanowy", message, t)
}
}
class CrashlyticsTree : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
Crashlytics.setInt("priority", priority) Crashlytics.setInt("priority", priority)
Crashlytics.setString("tag", tag) Crashlytics.setString("tag", tag)
if (t == null) { if (t == null) Crashlytics.log(message)
Crashlytics.log(message) else Crashlytics.logException(t)
} else {
Crashlytics.setString("message", message)
Crashlytics.logException(t)
}
} }
} }
object DebugLogTree : Timber.DebugTree() {
override fun createStackElementTag(element: StackTraceElement): String? {
return super.createStackElementTag(element) + " - ${element.lineNumber}"
}
}

View File

@ -11,4 +11,4 @@ open class SchedulersProvider {
open val backgroundThread: Scheduler open val backgroundThread: Scheduler
get() = Schedulers.io() get() = Schedulers.io()
} }

View File

@ -123,7 +123,7 @@ class LoginFormPresenterTest {
@Test @Test
fun loginErrorTest() { fun loginErrorTest() {
val testException = RuntimeException() val testException = RuntimeException("test")
doReturn(Single.error<List<Student>>(testException)) doReturn(Single.error<List<Student>>(testException))
.`when`(repository).getConnectedStudents(anyString(), anyString(), anyString(), anyString()) .`when`(repository).getConnectedStudents(anyString(), anyString(), anyString(), anyString())
presenter.attemptLogin("@", "123456", "test", "https://fakelog.cf") presenter.attemptLogin("@", "123456", "test", "https://fakelog.cf")