diff --git a/app/build.gradle b/app/build.gradle index b7f5face2..59f90fdbd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -113,6 +113,7 @@ dependencies { implementation "com.jakewharton.timber:timber:4.7.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") { transitive = true } @@ -125,7 +126,7 @@ dependencies { testImplementation "junit:junit:4.12" testImplementation "io.mockk:mockk:1.8.9" 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:runner:1.1.0' diff --git a/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt b/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt index 5aadf5f28..b7d7c4897 100644 --- a/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt +++ b/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt @@ -2,6 +2,7 @@ package io.github.wulkanowy import android.content.Context import androidx.multidex.MultiDex +import com.akaita.java.rxjava2debug.RxJava2Debug import com.crashlytics.android.Crashlytics import com.crashlytics.android.answers.Answers import com.crashlytics.android.core.CrashlyticsCore @@ -29,22 +30,20 @@ class WulkanowyApp : DaggerApplication() { AndroidThreeTen.init(this) initializeFabric() if (DEBUG) enableDebugLog() + RxJava2Debug.enableRxJava2AssemblyTracking(arrayOf(BuildConfig.APPLICATION_ID)) } private fun enableDebugLog() { - Timber.plant(DebugLogTree) + Timber.plant(DebugLogTree()) FlexibleAdapter.enableLogs(Log.Level.DEBUG) } private fun initializeFabric() { - Fabric.with(Fabric.Builder(this) - .kits(Crashlytics.Builder() - .core(CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG || !BuildConfig.FABRIC_ENABLED).build()) - .build(), - Answers()) - .debuggable(BuildConfig.DEBUG) - .build()) - Timber.plant(CrashlyticsTree) + Fabric.with(Fabric.Builder(this).kits( + Crashlytics.Builder().core(CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG || !BuildConfig.FABRIC_ENABLED).build()).build(), + Answers() + ).debuggable(BuildConfig.DEBUG).build()) + Timber.plant(CrashlyticsTree()) } override fun applicationInjector(): AndroidInjector { diff --git a/app/src/main/java/io/github/wulkanowy/data/ErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/data/ErrorHandler.kt index 3ba1cd351..481690a75 100644 --- a/app/src/main/java/io/github/wulkanowy/data/ErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/data/ErrorHandler.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.data import android.content.res.Resources +import com.akaita.java.rxjava2debug.RxJava2Debug import io.github.wulkanowy.R import io.github.wulkanowy.api.login.NotLoggedInException import timber.log.Timber @@ -14,7 +15,7 @@ open class ErrorHandler @Inject constructor(private val resources: Resources) { var showErrorMessage: (String) -> Unit = {} 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) { is UnknownHostException -> resources.getString(R.string.all_no_internet) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/remote/SessionRemote.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/remote/SessionRemote.kt index f01c677ba..c6757ab6a 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/remote/SessionRemote.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/remote/SessionRemote.kt @@ -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.Student import io.reactivex.Single +import okhttp3.logging.HttpLoggingInterceptor +import timber.log.Timber import java.net.URL import javax.inject.Inject import javax.inject.Singleton @@ -75,6 +77,7 @@ class SessionRemote @Inject constructor(private val api: Api) { fun initApi(student: Student, reInitialize: Boolean = false) { if (if (reInitialize) true else 0 == api.studentId) { api.run { + logLevel = HttpLoggingInterceptor.Level.NONE email = student.email password = student.password symbol = student.symbol @@ -84,6 +87,9 @@ class SessionRemote @Inject constructor(private val api: Api) { studentId = student.studentId loginType = Api.LoginType.valueOf(student.loginType) notifyDataChanged() + setInterceptor(HttpLoggingInterceptor(HttpLoggingInterceptor.Logger { + Timber.d(it) + }).setLevel(HttpLoggingInterceptor.Level.BASIC)) } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutPresenter.kt index 422de450e..d0ca64f9f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutPresenter.kt @@ -3,8 +3,10 @@ package io.github.wulkanowy.ui.modules.about import com.mikepenz.aboutlibraries.Libs import com.mikepenz.aboutlibraries.Libs.SpecialButton.SPECIAL1 import com.mikepenz.aboutlibraries.Libs.SpecialButton.SPECIAL2 +import com.mikepenz.aboutlibraries.Libs.SpecialButton.SPECIAL3 import io.github.wulkanowy.data.ErrorHandler import io.github.wulkanowy.ui.base.BasePresenter +import timber.log.Timber import javax.inject.Inject class AboutPresenter @Inject constructor(errorHandler: ErrorHandler) : BasePresenter(errorHandler) { @@ -12,9 +14,15 @@ class AboutPresenter @Inject constructor(errorHandler: ErrorHandler) : BasePrese fun onExtraSelect(type: Libs.SpecialButton?) { view?.run { when (type) { - SPECIAL1 -> openSourceWebView() - SPECIAL2 -> openIssuesWebView() - else -> TODO() + SPECIAL1 -> { + Timber.i("Opening github page") + openSourceWebView() + } + SPECIAL2 -> { + Timber.i("Opening issues page") + openIssuesWebView() + } + SPECIAL3 -> { } } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt index c85fa0f0c..7df50d9e9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt @@ -6,7 +6,13 @@ import io.github.wulkanowy.data.repositories.AttendanceRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SessionRepository 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.now import org.threeten.bp.LocalDate.ofEpochDay @@ -14,11 +20,11 @@ import java.util.concurrent.TimeUnit.MILLISECONDS import javax.inject.Inject class AttendancePresenter @Inject constructor( - private val errorHandler: ErrorHandler, - private val schedulers: SchedulersProvider, - private val attendanceRepository: AttendanceRepository, - private val sessionRepository: SessionRepository, - private val prefRepository: PreferencesRepository + private val errorHandler: ErrorHandler, + private val schedulers: SchedulersProvider, + private val attendanceRepository: AttendanceRepository, + private val sessionRepository: SessionRepository, + private val prefRepository: PreferencesRepository ) : BasePresenter(errorHandler) { lateinit var currentDate: LocalDate @@ -34,11 +40,13 @@ class AttendancePresenter @Inject constructor( fun onPreviousDay() { loadData(currentDate.previousSchoolDay) reloadView() + logEvent("Attendance day changed", mapOf("button" to "prev", "date" to currentDate.toFormattedString())) } fun onNextDay() { loadData(currentDate.nextSchoolDay) reloadView() + logEvent("Attendance day changed", mapOf("button" to "next", "date" to currentDate.toFormattedString())) } fun onSwipeRefresh() { @@ -59,33 +67,34 @@ class AttendancePresenter @Inject constructor( disposable.apply { clear() add(sessionRepository.getSemesters() - .delay(200, MILLISECONDS) - .map { it.single { semester -> semester.current } } - .flatMap { attendanceRepository.getAttendance(it, date, date, forceRefresh) } - .map { list -> - if (prefRepository.showPresent) list - else list.filter { !it.presence } + .delay(200, MILLISECONDS) + .map { it.single { semester -> semester.current } } + .flatMap { attendanceRepository.getAttendance(it, date, date, forceRefresh) } + .map { list -> + if (prefRepository.showPresent) list + 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 } } - .subscribeOn(schedulers.backgroundThread) - .observeOn(schedulers.mainThread) - .doFinally { - view?.run { - hideRefresh() - showProgress(false) - } - } - .subscribe({ - view?.apply { - updateData(it) - showEmpty(it.isEmpty()) - showContent(it.isNotEmpty()) - } - }) { - view?.run { showEmpty(isViewEmpty) } - errorHandler.proceed(it) + } + .subscribe({ + view?.apply { + updateData(it) + showEmpty(it.isEmpty()) + showContent(it.isNotEmpty()) } + logEvent("Attendance load", mapOf("items" to it.size, "forceRefresh" to forceRefresh, "date" to currentDate.toFormattedString())) + }) { + view?.run { showEmpty(isViewEmpty) } + errorHandler.proceed(it) + } ) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt index 6f9cb8b25..f2bf7b077 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt @@ -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.SessionRepository 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.now import org.threeten.bp.LocalDate.ofEpochDay @@ -14,10 +20,10 @@ import java.util.concurrent.TimeUnit.MILLISECONDS import javax.inject.Inject class ExamPresenter @Inject constructor( - private val errorHandler: ErrorHandler, - private val schedulers: SchedulersProvider, - private val examRepository: ExamRepository, - private val sessionRepository: SessionRepository + private val errorHandler: ErrorHandler, + private val schedulers: SchedulersProvider, + private val examRepository: ExamRepository, + private val sessionRepository: SessionRepository ) : BasePresenter(errorHandler) { lateinit var currentDate: LocalDate @@ -33,11 +39,13 @@ class ExamPresenter @Inject constructor( fun onPreviousWeek() { loadData(currentDate.minusDays(7)) reloadView() + logEvent("Exam week changed", mapOf("button" to "prev", "date" to currentDate.toFormattedString())) } fun onNextWeek() { loadData(currentDate.plusDays(7)) reloadView() + logEvent("Exam week changed", mapOf("button" to "next", "date" to currentDate.toFormattedString())) } fun onSwipeRefresh() { @@ -58,30 +66,31 @@ class ExamPresenter @Inject constructor( disposable.apply { clear() add(sessionRepository.getSemesters() - .delay(200, MILLISECONDS) - .map { it.single { semester -> semester.current } } - .flatMap { - examRepository.getExams(it, currentDate.monday, currentDate.friday, forceRefresh) - }.map { it.groupBy { exam -> exam.date }.toSortedMap() } - .map { createExamItems(it) } - .subscribeOn(schedulers.backgroundThread) - .observeOn(schedulers.mainThread) - .doFinally { - view?.run { - hideRefresh() - showProgress(false) - } + .delay(200, MILLISECONDS) + .map { it.single { semester -> semester.current } } + .flatMap { + examRepository.getExams(it, currentDate.monday, currentDate.friday, forceRefresh) + }.map { it.groupBy { exam -> exam.date }.toSortedMap() } + .map { createExamItems(it) } + .subscribeOn(schedulers.backgroundThread) + .observeOn(schedulers.mainThread) + .doFinally { + view?.run { + hideRefresh() + showProgress(false) } - .subscribe({ - view?.apply { - updateData(it) - showEmpty(it.isEmpty()) - showContent(it.isNotEmpty()) - } - }) { - view?.run { showEmpty(isViewEmpty) } - errorHandler.proceed(it) - }) + } + .subscribe({ + view?.apply { + updateData(it) + showEmpty(it.isEmpty()) + showContent(it.isNotEmpty()) + } + logEvent("Exam load", mapOf("items" to it.size, "forceRefresh" to forceRefresh, "date" to currentDate.toFormattedString())) + }) { + view?.run { showEmpty(isViewEmpty) } + errorHandler.proceed(it) + }) } } @@ -102,7 +111,7 @@ class ExamPresenter @Inject constructor( showPreButton(!currentDate.minusDays(7).isHolidays) showNextButton(!currentDate.plusDays(7).isHolidays) updateNavigationWeek("${currentDate.monday.toFormattedString("dd.MM")} - " + - currentDate.friday.toFormattedString("dd.MM")) + currentDate.friday.toFormattedString("dd.MM")) } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt index 1dfced711..5d678915f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt @@ -5,7 +5,9 @@ import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.repositories.SessionRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.utils.SchedulersProvider +import io.github.wulkanowy.utils.logEvent import io.reactivex.Completable +import timber.log.Timber import java.util.concurrent.TimeUnit.MILLISECONDS import javax.inject.Inject @@ -48,6 +50,7 @@ class GradePresenter @Inject constructor( notifyChildrenSemesterChange() loadChild(it.currentPageIndex) } + logEvent("Semester changed", mapOf("number" to index + 1)) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt index 5e58b2f4f..46539aecf 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt @@ -8,15 +8,17 @@ import io.github.wulkanowy.data.repositories.SessionRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.utils.SchedulersProvider import io.github.wulkanowy.utils.calcAverage +import io.github.wulkanowy.utils.logEvent import io.github.wulkanowy.utils.valueColor import timber.log.Timber import javax.inject.Inject class GradeDetailsPresenter @Inject constructor( - private val errorHandler: ErrorHandler, - private val schedulers: SchedulersProvider, - private val gradeRepository: GradeRepository, - private val sessionRepository: SessionRepository) : BasePresenter(errorHandler) { + private val errorHandler: ErrorHandler, + private val schedulers: SchedulersProvider, + private val gradeRepository: GradeRepository, + private val sessionRepository: SessionRepository +) : BasePresenter(errorHandler) { override fun onAttachView(view: GradeDetailsView) { super.onAttachView(view) @@ -25,27 +27,28 @@ class GradeDetailsPresenter @Inject constructor( fun onParentViewLoadData(semesterId: Int, forceRefresh: Boolean) { disposable.add(sessionRepository.getSemesters() - .flatMap { gradeRepository.getGrades(it.first { item -> item.semesterId == semesterId }, forceRefresh) } - .map { createGradeItems(it.groupBy { grade -> grade.subject }.toSortedMap()) } - .subscribeOn(schedulers.backgroundThread) - .observeOn(schedulers.mainThread) - .doFinally { - view?.run { - showRefresh(false) - showProgress(false) - notifyParentDataLoaded(semesterId) - } + .flatMap { gradeRepository.getGrades(it.first { item -> item.semesterId == semesterId }, forceRefresh) } + .map { createGradeItems(it.groupBy { grade -> grade.subject }.toSortedMap()) } + .subscribeOn(schedulers.backgroundThread) + .observeOn(schedulers.mainThread) + .doFinally { + view?.run { + showRefresh(false) + showProgress(false) + notifyParentDataLoaded(semesterId) } - .subscribe({ - view?.run { - showEmpty(it.isEmpty()) - showContent(it.isNotEmpty()) - updateData(it) - } - }) { - view?.run { showEmpty(isViewEmpty) } - errorHandler.proceed(it) - }) + } + .subscribe({ + view?.run { + showEmpty(it.isEmpty()) + showContent(it.isNotEmpty()) + updateData(it) + } + logEvent("Grade details load", mapOf("items" to it.size, "forceRefresh" to forceRefresh)) + }) { + view?.run { showEmpty(isViewEmpty) } + errorHandler.proceed(it) + }) } fun onGradeItemSelected(item: AbstractFlexibleItem<*>?) { @@ -92,16 +95,16 @@ class GradeDetailsPresenter @Inject constructor( return items.map { it.value.calcAverage().let { average -> GradeDetailsHeader( - subject = it.key, - average = formatAverage(average), - number = view?.getGradeNumberString(it.value.size).orEmpty(), - newGrades = it.value.filter { grade -> !grade.isRead }.size + subject = it.key, + average = formatAverage(average), + number = view?.getGradeNumberString(it.value.size).orEmpty(), + newGrades = it.value.filter { grade -> !grade.isRead }.size ).apply { subItems = it.value.map { item -> GradeDetailsItem( - grade = item, - weightString = view?.weightString.orEmpty(), - valueColor = item.valueColor + grade = item, + weightString = view?.weightString.orEmpty(), + valueColor = item.valueColor ) } } @@ -118,9 +121,9 @@ class GradeDetailsPresenter @Inject constructor( private fun updateGrade(grade: Grade) { disposable.add(gradeRepository.updateGrade(grade) - .subscribeOn(schedulers.backgroundThread) - .observeOn(schedulers.mainThread) - .subscribe({}) { error -> errorHandler.proceed(error) }) + .subscribeOn(schedulers.backgroundThread) + .observeOn(schedulers.mainThread) + .subscribe({}) { error -> errorHandler.proceed(error) }) Timber.d("Grade ${grade.id} updated") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt index 1a2296f86..a6479e4de 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt @@ -8,17 +8,18 @@ import io.github.wulkanowy.data.repositories.SessionRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.utils.SchedulersProvider import io.github.wulkanowy.utils.calcAverage +import io.github.wulkanowy.utils.logEvent import java.lang.String.format import java.util.Locale.FRANCE import javax.inject.Inject class GradeSummaryPresenter @Inject constructor( - private val errorHandler: ErrorHandler, - private val gradeSummaryRepository: GradeSummaryRepository, - private val gradeRepository: GradeRepository, - private val sessionRepository: SessionRepository, - private val schedulers: SchedulersProvider) - : BasePresenter(errorHandler) { + private val errorHandler: ErrorHandler, + private val gradeSummaryRepository: GradeSummaryRepository, + private val gradeRepository: GradeRepository, + private val sessionRepository: SessionRepository, + private val schedulers: SchedulersProvider +) : BasePresenter(errorHandler) { override fun onAttachView(view: GradeSummaryView) { super.onAttachView(view) @@ -27,43 +28,44 @@ class GradeSummaryPresenter @Inject constructor( fun onParentViewLoadData(semesterId: Int, forceRefresh: Boolean) { disposable.add(sessionRepository.getSemesters() - .map { semester -> semester.first { it.semesterId == semesterId } } - .flatMap { - gradeSummaryRepository.getGradesSummary(it, forceRefresh) - .flatMap { gradesSummary -> - gradeRepository.getGrades(it, forceRefresh) - .map { grades -> - grades.groupBy { grade -> grade.subject } - .mapValues { entry -> entry.value.calcAverage() } - .filterValues { value -> value != 0.0 } - .let { averages -> - createGradeSummaryItems(gradesSummary, averages) to - GradeSummaryScrollableHeader( - formatAverage(gradesSummary.calcAverage()), - formatAverage(averages.values.average()) - ) - } - } + .map { semester -> semester.first { it.semesterId == semesterId } } + .flatMap { + gradeSummaryRepository.getGradesSummary(it, forceRefresh) + .flatMap { gradesSummary -> + gradeRepository.getGrades(it, forceRefresh) + .map { grades -> + grades.groupBy { grade -> grade.subject } + .mapValues { entry -> entry.value.calcAverage() } + .filterValues { value -> value != 0.0 } + .let { averages -> + createGradeSummaryItems(gradesSummary, averages) to + GradeSummaryScrollableHeader( + formatAverage(gradesSummary.calcAverage()), + formatAverage(averages.values.average()) + ) + } } + } + } + .subscribeOn(schedulers.backgroundThread) + .observeOn(schedulers.mainThread) + .doFinally { + view?.run { + showRefresh(false) + showProgress(false) + notifyParentDataLoaded(semesterId) } - .subscribeOn(schedulers.backgroundThread) - .observeOn(schedulers.mainThread) - .doFinally { - view?.run { - showRefresh(false) - showProgress(false) - notifyParentDataLoaded(semesterId) - } - }.subscribe({ - view?.run { - showEmpty(it.first.isEmpty()) - showContent(it.first.isNotEmpty()) - updateDataSet(it.first, it.second) - } - }) { - view?.run { showEmpty(isViewEmpty) } - errorHandler.proceed(it) - }) + }.subscribe({ + view?.run { + showEmpty(it.first.isEmpty()) + showContent(it.first.isNotEmpty()) + updateDataSet(it.first, it.second) + } + logEvent("Grade summary load", mapOf("items" to it.first.size, "forceRefresh" to forceRefresh)) + }) { + view?.run { showEmpty(isViewEmpty) } + errorHandler.proceed(it) + }) } fun onSwipeRefresh() { @@ -88,24 +90,24 @@ class GradeSummaryPresenter @Inject constructor( } private fun createGradeSummaryItems(gradesSummary: List, averages: Map) - : List { + : List { return gradesSummary.filter { !checkEmpty(it, averages) } - .flatMap { gradeSummary -> - GradeSummaryHeader( - name = gradeSummary.subject, - average = formatAverage(averages.getOrElse(gradeSummary.subject) { 0.0 }, "") - ).let { - listOf(GradeSummaryItem( - header = it, - title = view?.predictedString.orEmpty(), - grade = gradeSummary.predictedGrade - ), GradeSummaryItem( - header = it, - title = view?.finalString.orEmpty(), - grade = gradeSummary.finalGrade - )) - } + .flatMap { gradeSummary -> + GradeSummaryHeader( + name = gradeSummary.subject, + average = formatAverage(averages.getOrElse(gradeSummary.subject) { 0.0 }, "") + ).let { + listOf(GradeSummaryItem( + header = it, + title = view?.predictedString.orEmpty(), + grade = gradeSummary.predictedGrade + ), GradeSummaryItem( + header = it, + title = view?.finalString.orEmpty(), + grade = gradeSummary.finalGrade + )) } + } } private fun checkEmpty(gradeSummary: GradeSummary, averages: Map): Boolean { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt index c1420a672..36912b2b8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt @@ -4,13 +4,16 @@ import io.github.wulkanowy.data.repositories.SessionRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.modules.login.LoginErrorHandler 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 class LoginFormPresenter @Inject constructor( - private val schedulers: SchedulersProvider, - private val errorHandler: LoginErrorHandler, - private val sessionRepository: SessionRepository) - : BasePresenter(errorHandler) { + private val schedulers: SchedulersProvider, + private val errorHandler: LoginErrorHandler, + private val sessionRepository: SessionRepository +) : BasePresenter(errorHandler) { private var wasEmpty = false @@ -22,33 +25,39 @@ class LoginFormPresenter @Inject constructor( fun attemptLogin(email: String, password: String, symbol: String, endpoint: String) { if (!validateCredentials(email, password, symbol)) return disposable.add(sessionRepository.getConnectedStudents(email, password, symbol, endpoint) - .observeOn(schedulers.mainThread) - .subscribeOn(schedulers.backgroundThread) - .doOnSubscribe { - view?.run { - hideSoftKeyboard() - showLoginProgress(true) - errorHandler.doOnBadCredentials = { - setErrorPassIncorrect() - showSoftKeyboard() - } + .observeOn(schedulers.mainThread) + .subscribeOn(schedulers.backgroundThread) + .doOnSubscribe { + view?.run { + hideSoftKeyboard() + showLoginProgress(true) + errorHandler.doOnBadCredentials = { + setErrorPassIncorrect() + showSoftKeyboard() + Timber.i("Entered wrong username or password") } - sessionRepository.clearCache() } - .doFinally { view?.showLoginProgress(false) } - .subscribe({ - view?.run { - if (it.isEmpty() && !wasEmpty) { - showSymbolInput() - wasEmpty = true - } else if (it.isEmpty() && wasEmpty) { - showSymbolInput() - setErrorSymbolIncorrect() - } else { - switchNextView() - } + sessionRepository.clearCache() + } + .doFinally { view?.showLoginProgress(false) } + .subscribe({ + view?.run { + if (it.isEmpty() && !wasEmpty) { + showSymbolInput() + wasEmpty = true + } else if (it.isEmpty() && wasEmpty) { + showSymbolInput() + 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 { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/options/LoginOptionsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/options/LoginOptionsPresenter.kt index 357f8a051..4a7bb38c0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/options/LoginOptionsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/options/LoginOptionsPresenter.kt @@ -5,13 +5,14 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.SessionRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.utils.SchedulersProvider +import io.github.wulkanowy.utils.logRegister import javax.inject.Inject class LoginOptionsPresenter @Inject constructor( - private val errorHandler: ErrorHandler, - private val repository: SessionRepository, - private val schedulers: SchedulersProvider) - : BasePresenter(errorHandler) { + private val errorHandler: ErrorHandler, + private val repository: SessionRepository, + private val schedulers: SchedulersProvider +) : BasePresenter(errorHandler) { override fun onAttachView(view: LoginOptionsView) { super.onAttachView(view) @@ -20,25 +21,30 @@ class LoginOptionsPresenter @Inject constructor( fun refreshData() { disposable.add(repository.cachedStudents - .observeOn(schedulers.mainThread) - .subscribeOn(schedulers.backgroundThread) - .doOnSubscribe { view?.showActionBar(true) } - .doFinally { repository.clearCache() } - .subscribe({ - view?.updateData(it.map { student -> - LoginOptionsItem(student) - }) - }, { errorHandler.proceed(it) })) + .observeOn(schedulers.mainThread) + .subscribeOn(schedulers.backgroundThread) + .doOnSubscribe { view?.showActionBar(true) } + .doFinally { repository.clearCache() } + .subscribe({ + view?.updateData(it.map { student -> + LoginOptionsItem(student) + }) + }, { errorHandler.proceed(it) })) } fun onSelectStudent(student: Student) { disposable.add(repository.saveStudent(student) - .subscribeOn(schedulers.backgroundThread) - .observeOn(schedulers.mainThread) - .doOnSubscribe { _ -> - view?.showLoginProgress(true) - view?.showActionBar(false) + .subscribeOn(schedulers.backgroundThread) + .observeOn(schedulers.mainThread) + .doOnSubscribe { + view?.run { + showLoginProgress(true) + showActionBar(false) } - .subscribe({ view?.openMainView() }, { errorHandler.proceed(it) })) + } + .subscribe({ + logRegister("Success", true, student.symbol, student.endpoint) + view?.openMainView() + }, { errorHandler.proceed(it) })) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt index 32b7bf303..704b9e7ba 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt @@ -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.timetable.TimetableFragment import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.logLogin import io.github.wulkanowy.utils.safelyPopFragment import io.github.wulkanowy.utils.setOnViewChangeListener import kotlinx.android.synthetic.main.activity_main.* diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt index 1defc4691..03dce3f25 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt @@ -4,6 +4,7 @@ import io.github.wulkanowy.data.ErrorHandler import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.services.job.ServiceHelper import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.utils.logLogin import javax.inject.Inject class MainPresenter @Inject constructor( @@ -21,6 +22,11 @@ class MainPresenter @Inject constructor( initView() } + when (initMenuIndex) { + 1 -> logLogin("Grades") + 3 -> logLogin("Timetable") + } + serviceHelper.startFullSyncService() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsPresenter.kt index d3aa027c9..25e48f87c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsPresenter.kt @@ -5,6 +5,7 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.services.job.ServiceHelper import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.utils.isHolidays +import io.github.wulkanowy.utils.logEvent import org.threeten.bp.LocalDate.now import javax.inject.Inject @@ -36,5 +37,7 @@ class SettingsPresenter @Inject constructor( view?.setTheme(preferencesRepository.currentTheme) } } + + logEvent("Setting changed", mapOf("name" to key)) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashPresenter.kt index db67f457a..d9dd9b9e7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashPresenter.kt @@ -4,6 +4,7 @@ import io.github.wulkanowy.data.ErrorHandler import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SessionRepository import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.utils.logLogin import javax.inject.Inject class SplashPresenter @Inject constructor( @@ -16,7 +17,10 @@ class SplashPresenter @Inject constructor( super.onAttachView(view) view.run { setCurrentThemeMode(preferencesRepository.currentTheme) - if (sessionRepository.isSessionSaved) openMainView() else openLoginView() + if (sessionRepository.isSessionSaved) { + logLogin("Open app") + openMainView() + } else openLoginView() } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt index 40c4aa5db..1a0eae836 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt @@ -5,7 +5,13 @@ import io.github.wulkanowy.data.ErrorHandler import io.github.wulkanowy.data.repositories.SessionRepository import io.github.wulkanowy.data.repositories.TimetableRepository 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.now import org.threeten.bp.LocalDate.ofEpochDay @@ -13,10 +19,10 @@ import java.util.concurrent.TimeUnit.MILLISECONDS import javax.inject.Inject class TimetablePresenter @Inject constructor( - private val errorHandler: ErrorHandler, - private val schedulers: SchedulersProvider, - private val timetableRepository: TimetableRepository, - private val sessionRepository: SessionRepository + private val errorHandler: ErrorHandler, + private val schedulers: SchedulersProvider, + private val timetableRepository: TimetableRepository, + private val sessionRepository: SessionRepository ) : BasePresenter(errorHandler) { lateinit var currentDate: LocalDate @@ -32,11 +38,13 @@ class TimetablePresenter @Inject constructor( fun onPreviousDay() { loadData(currentDate.previousSchoolDay) reloadView() + logEvent("Timetable day changed", mapOf("button" to "prev", "date" to currentDate.toFormattedString())) } fun onNextDay() { loadData(currentDate.nextSchoolDay) reloadView() + logEvent("Timetable day changed", mapOf("button" to "next", "date" to currentDate.toFormattedString())) } fun onSwipeRefresh() { @@ -57,29 +65,30 @@ class TimetablePresenter @Inject constructor( disposable.apply { clear() add(sessionRepository.getSemesters() - .delay(200, MILLISECONDS) - .map { it.single { semester -> semester.current } } - .flatMap { timetableRepository.getTimetable(it, currentDate, currentDate, forceRefresh) } - .map { items -> items.map { TimetableItem(it, view?.roomString.orEmpty()) } } - .map { items -> items.sortedBy { it.lesson.number } } - .subscribeOn(schedulers.backgroundThread) - .observeOn(schedulers.mainThread) - .doFinally { - view?.run { - hideRefresh() - showProgress(false) - } + .delay(200, MILLISECONDS) + .map { it.single { semester -> semester.current } } + .flatMap { timetableRepository.getTimetable(it, currentDate, currentDate, forceRefresh) } + .map { items -> items.map { TimetableItem(it, view?.roomString.orEmpty()) } } + .map { items -> items.sortedBy { it.lesson.number } } + .subscribeOn(schedulers.backgroundThread) + .observeOn(schedulers.mainThread) + .doFinally { + view?.run { + hideRefresh() + showProgress(false) } - .subscribe({ - view?.apply { - updateData(it) - showEmpty(it.isEmpty()) - showContent(it.isNotEmpty()) - } - }) { - view?.run { showEmpty(isViewEmpty()) } - errorHandler.proceed(it) - }) + } + .subscribe({ + view?.apply { + updateData(it) + showEmpty(it.isEmpty()) + showContent(it.isNotEmpty()) + } + logEvent("Timetable load", mapOf("items" to it.size, "forceRefresh" to forceRefresh, "date" to currentDate.toFormattedString())) + }) { + view?.run { showEmpty(isViewEmpty()) } + errorHandler.proceed(it) + }) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/widgets/timetable/TimetableWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/widgets/timetable/TimetableWidgetProvider.kt index c2175480f..8439aed4f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/widgets/timetable/TimetableWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/widgets/timetable/TimetableWidgetProvider.kt @@ -15,6 +15,7 @@ import io.github.wulkanowy.data.db.SharedPrefHelper import io.github.wulkanowy.services.widgets.TimetableWidgetService 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.utils.logEvent import io.github.wulkanowy.utils.nextOrSameSchoolDay import io.github.wulkanowy.utils.nextSchoolDay import io.github.wulkanowy.utils.previousSchoolDay @@ -78,16 +79,19 @@ class TimetableWidgetProvider : AppWidgetProvider() { AndroidInjection.inject(this, context) intent?.let { val widgetKey = "timetable_widget_${it.getIntExtra(EXTRA_TOGGLED_WIDGET_ID, 0)}" - when (it.getStringExtra(EXTRA_BUTTON_TYPE)) { - BUTTON_NEXT -> { - LocalDate.ofEpochDay(sharedPref.getLong(widgetKey, 0)).nextSchoolDay - .let { date -> sharedPref.putLong(widgetKey, date.toEpochDay(), true) } + it.getStringExtra(EXTRA_BUTTON_TYPE).let { button -> + when (button) { + BUTTON_NEXT -> { + 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 -> { - 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?.also { btn -> if (btn.isNotBlank()) logEvent("Widget day changed", mapOf("button" to button)) } } } super.onReceive(context, intent) diff --git a/app/src/main/java/io/github/wulkanowy/utils/FabricUtils.kt b/app/src/main/java/io/github/wulkanowy/utils/FabricUtils.kt new file mode 100644 index 000000000..a82c4f817 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/FabricUtils.kt @@ -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 logEvent(name: String, params: Map) { + 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) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/utils/LoggerUtils.kt b/app/src/main/java/io/github/wulkanowy/utils/LoggerUtils.kt index 532209ea3..565374130 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/LoggerUtils.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/LoggerUtils.kt @@ -3,25 +3,20 @@ package io.github.wulkanowy.utils import com.crashlytics.android.Crashlytics 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?) { Crashlytics.setInt("priority", priority) Crashlytics.setString("tag", tag) - if (t == null) { - Crashlytics.log(message) - } else { - Crashlytics.setString("message", message) - Crashlytics.logException(t) - } + if (t == null) Crashlytics.log(message) + else Crashlytics.logException(t) } } - -object DebugLogTree : Timber.DebugTree() { - - override fun createStackElementTag(element: StackTraceElement): String? { - return super.createStackElementTag(element) + " - ${element.lineNumber}" - } -} - diff --git a/app/src/main/java/io/github/wulkanowy/utils/SchedulersProvider.kt b/app/src/main/java/io/github/wulkanowy/utils/SchedulersProvider.kt index 6f67e88fb..e426d4d5d 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/SchedulersProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/SchedulersProvider.kt @@ -11,4 +11,4 @@ open class SchedulersProvider { open val backgroundThread: Scheduler get() = Schedulers.io() -} \ No newline at end of file +} diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenterTest.kt index d72d94dfc..6bae21d21 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenterTest.kt @@ -123,7 +123,7 @@ class LoginFormPresenterTest { @Test fun loginErrorTest() { - val testException = RuntimeException() + val testException = RuntimeException("test") doReturn(Single.error>(testException)) .`when`(repository).getConnectedStudents(anyString(), anyString(), anyString(), anyString()) presenter.attemptLogin("@", "123456", "test", "https://fakelog.cf")