From 5cd8ed88c03e9a4a578da211228b6540ee7aabc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sat, 6 Oct 2018 10:53:34 +0200 Subject: [PATCH] Refactor timetable module (#160) --- .idea/codeStyleSettings.xml | 226 -------------- app/build.gradle | 5 +- .../repositories/local/TimetableLocalTest.kt | 51 ++++ .../github/wulkanowy/data/RepositoryModule.kt | 4 + .../github/wulkanowy/data/db/AppDatabase.kt | 3 + .../io/github/wulkanowy/data/db/Converters.kt | 12 +- .../wulkanowy/data/db/dao/TimetableDao.kt | 22 ++ .../wulkanowy/data/db/entities/Timetable.kt | 43 +++ .../data/repositories/AttendanceRepository.kt | 2 + .../data/repositories/TimetableRepository.kt | 45 +++ .../data/repositories/local/TimetableLocal.kt | 24 ++ .../repositories/remote/TimetableRemote.kt | 40 +++ .../ui/main/attendance/AttendanceFragment.kt | 5 - .../ui/main/attendance/AttendancePresenter.kt | 4 +- .../wulkanowy/ui/main/exam/ExamFragment.kt | 5 - .../github/wulkanowy/ui/main/exam/ExamView.kt | 1 - .../wulkanowy/ui/main/more/MoreFragment.kt | 2 +- .../ui/main/timetable/TimetableDialog.kt | 79 +++++ .../ui/main/timetable/TimetableFragment.kt | 92 +++++- .../ui/main/timetable/TimetableItem.kt | 65 ++++ .../ui/main/timetable/TimetablePresenter.kt | 93 ++++++ .../ui/main/timetable/TimetableView.kt | 30 ++ .../github/wulkanowy/utils/TimeExtension.kt | 8 +- app/src/main/res/layout/dialog_timetable.xml | 277 +++++++----------- .../main/res/layout/fragment_timetable.xml | 103 ++++++- .../res/layout/fragment_timetable_tab.xml | 73 ----- app/src/main/res/layout/item_timetable.xml | 154 +++++----- app/src/main/res/values-pl/strings.xml | 4 +- app/src/main/res/values/strings.xml | 2 +- .../remote/TimetableRemoteTest.kt | 54 ++++ .../wulkanowy/utils/TimeExtensionTest.kt | 7 + build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 1 - 33 files changed, 945 insertions(+), 593 deletions(-) delete mode 100644 .idea/codeStyleSettings.xml create mode 100644 app/src/androidTest/java/io/github/wulkanowy/data/repositories/local/TimetableLocalTest.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableDao.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/entities/Timetable.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/repositories/local/TimetableLocal.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/repositories/remote/TimetableRemote.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/main/timetable/TimetableDialog.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/main/timetable/TimetableItem.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/main/timetable/TimetablePresenter.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/main/timetable/TimetableView.kt delete mode 100644 app/src/main/res/layout/fragment_timetable_tab.xml create mode 100644 app/src/test/java/io/github/wulkanowy/data/repositories/remote/TimetableRemoteTest.kt diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml deleted file mode 100644 index f5bd97e62..000000000 --- a/.idea/codeStyleSettings.xml +++ /dev/null @@ -1,226 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index d2089beb0..67af01b68 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -72,10 +72,7 @@ ext.supportVersion = "28.0.0" dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation('com.github.wulkanowy:api:07201a4') { - exclude module: "threetenbp" - } - + implementation('com.github.wulkanowy:api:f54b673') { exclude module: "threetenbp" } implementation "com.android.support:support-v4:$supportVersion" implementation "com.android.support:appcompat-v7:$supportVersion" implementation "com.android.support:design:$supportVersion" diff --git a/app/src/androidTest/java/io/github/wulkanowy/data/repositories/local/TimetableLocalTest.kt b/app/src/androidTest/java/io/github/wulkanowy/data/repositories/local/TimetableLocalTest.kt new file mode 100644 index 000000000..64531239b --- /dev/null +++ b/app/src/androidTest/java/io/github/wulkanowy/data/repositories/local/TimetableLocalTest.kt @@ -0,0 +1,51 @@ +package io.github.wulkanowy.data.repositories.local + +import android.arch.persistence.room.Room +import android.support.test.InstrumentationRegistry +import android.support.test.runner.AndroidJUnit4 +import io.github.wulkanowy.data.db.AppDatabase +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Timetable +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.threeten.bp.LocalDate +import kotlin.test.assertEquals + +@RunWith(AndroidJUnit4::class) +class TimetableLocalTest { + + private lateinit var timetableDb: TimetableLocal + + private lateinit var testDb: AppDatabase + + @Before + fun createDb() { + testDb = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(), AppDatabase::class.java).build() + timetableDb = TimetableLocal(testDb.timetableDao()) + } + + @After + fun closeDb() { + testDb.close() + } + + @Test + fun saveAndReadTest() { + timetableDb.saveLessons(listOf( + Timetable(studentId = "1", diaryId = "2", date = LocalDate.of(2018, 9, 10)), + Timetable(studentId = "1", diaryId = "2", date = LocalDate.of(2018, 9, 14)), + Timetable(studentId = "1", diaryId = "2", date = LocalDate.of(2018, 9, 17)) // in next week + )) + + val exams = timetableDb.getLessons( + Semester(studentId = "1", diaryId = "2", semesterId = "3", diaryName = "", semesterName = 1), + LocalDate.of(2018, 9, 10), + LocalDate.of(2018, 9, 14) + ).blockingGet() + assertEquals(2, exams.size) + assertEquals(exams[0].date, LocalDate.of(2018, 9, 10)) + assertEquals(exams[1].date, LocalDate.of(2018, 9, 14)) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt index cf1031e5d..05b703478 100644 --- a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt @@ -63,4 +63,8 @@ internal class RepositoryModule { @Singleton @Provides fun provideAttendanceDao(database: AppDatabase) = database.attendanceDao() + + @Singleton + @Provides + fun provideTimetableDao(database: AppDatabase) = database.timetableDao() } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt index cc39b58e1..060ee2203 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt @@ -15,6 +15,7 @@ import javax.inject.Singleton Student::class, Semester::class, Exam::class, + Timetable::class, Attendance::class, Grade::class, GradeSummary::class @@ -38,6 +39,8 @@ abstract class AppDatabase : RoomDatabase() { abstract fun examsDao(): ExamDao + abstract fun timetableDao(): TimetableDao + abstract fun attendanceDao(): AttendanceDao abstract fun gradeDao(): GradeDao diff --git a/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt b/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt index 8e92d6ebb..cde0348d8 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt @@ -7,7 +7,7 @@ import java.util.* class Converters { @TypeConverter - fun fromTimestamp(value: Long?): LocalDate? = value?.run { + fun timestampToDate(value: Long?): LocalDate? = value?.run { DateTimeUtils.toInstant(Date(value)).atZone(ZoneOffset.UTC).toLocalDate() } @@ -15,4 +15,14 @@ class Converters { fun dateToTimestamp(date: LocalDate?): Long? { return date?.atStartOfDay()?.toInstant(ZoneOffset.UTC)?.toEpochMilli() } + + @TypeConverter + fun timestampToTime(value: Long?): LocalDateTime? = value?.let { + LocalDateTime.ofInstant(Instant.ofEpochMilli(value), ZoneOffset.UTC) + } + + @TypeConverter + fun timeToTimestamp(date: LocalDateTime?): Long? { + return date?.atZone(ZoneOffset.UTC)?.toInstant()?.toEpochMilli() + } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableDao.kt new file mode 100644 index 000000000..58dba51a3 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableDao.kt @@ -0,0 +1,22 @@ +package io.github.wulkanowy.data.db.dao + +import android.arch.persistence.room.Dao +import android.arch.persistence.room.Delete +import android.arch.persistence.room.Insert +import android.arch.persistence.room.Query +import io.github.wulkanowy.data.db.entities.Timetable +import io.reactivex.Maybe +import org.threeten.bp.LocalDate + +@Dao +interface TimetableDao { + + @Insert + fun insertAll(exams: List): List + + @Delete + fun deleteAll(exams: List) + + @Query("SELECT * FROM Timetable WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end") + fun getTimetable(diaryId: String, studentId: String, from: LocalDate, end: LocalDate): Maybe> +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Timetable.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Timetable.kt new file mode 100644 index 000000000..b06f070ff --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Timetable.kt @@ -0,0 +1,43 @@ +package io.github.wulkanowy.data.db.entities + +import android.arch.persistence.room.ColumnInfo +import android.arch.persistence.room.Entity +import android.arch.persistence.room.PrimaryKey +import org.threeten.bp.LocalDate +import org.threeten.bp.LocalDateTime +import java.io.Serializable + +@Entity(tableName = "Timetable") +data class Timetable( + + @PrimaryKey(autoGenerate = true) + var id: Long = 0, + + @ColumnInfo(name = "student_id") + var studentId: String = "", + + @ColumnInfo(name = "diary_id") + var diaryId: String = "", + + val number: Int = 0, + + val start: LocalDateTime = LocalDateTime.now(), + + val end: LocalDateTime = LocalDateTime.now(), + + val date: LocalDate, + + val subject: String = "", + + val group: String = "", + + val room: String = "", + + val teacher: String = "", + + val info: String = "", + + val changes: Boolean = false, + + val canceled: Boolean = false +) : Serializable diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt index 91986826d..ddbc4d069 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt @@ -13,7 +13,9 @@ import org.threeten.bp.LocalDate import org.threeten.bp.temporal.TemporalAdjusters import java.net.UnknownHostException import javax.inject.Inject +import javax.inject.Singleton +@Singleton class AttendanceRepository @Inject constructor( private val settings: InternetObservingSettings, private val local: AttendanceLocal, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt new file mode 100644 index 000000000..728e67a3d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt @@ -0,0 +1,45 @@ +package io.github.wulkanowy.data.repositories + +import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork +import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.data.repositories.local.TimetableLocal +import io.github.wulkanowy.data.repositories.remote.TimetableRemote +import io.github.wulkanowy.utils.weekFirstDayAlwaysCurrent +import io.reactivex.Single +import org.threeten.bp.DayOfWeek +import org.threeten.bp.LocalDate +import org.threeten.bp.temporal.TemporalAdjusters +import java.net.UnknownHostException +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TimetableRepository @Inject constructor( + private val settings: InternetObservingSettings, + private val local: TimetableLocal, + private val remote: TimetableRemote +) { + + fun getTimetable(semester: Semester, startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean = false): Single> { + val start = startDate.weekFirstDayAlwaysCurrent + val end = endDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.FRIDAY)) + + return local.getLessons(semester, start, end).filter { !forceRefresh } + .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap { + if (it) remote.getLessons(semester, start, end) + else Single.error(UnknownHostException()) + }.flatMap { newLessons -> + local.getLessons(semester, start, end).toSingle(emptyList()).map { lessons -> + local.deleteLessons(lessons - newLessons) + local.saveLessons(newLessons - lessons) + newLessons + } + }).map { list -> + list.asSequence().filter { + it.date in startDate..endDate + }.toList() + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/local/TimetableLocal.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/local/TimetableLocal.kt new file mode 100644 index 000000000..792cee8e3 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/local/TimetableLocal.kt @@ -0,0 +1,24 @@ +package io.github.wulkanowy.data.repositories.local + +import io.github.wulkanowy.data.db.dao.TimetableDao +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Timetable +import io.reactivex.Maybe +import org.threeten.bp.LocalDate +import javax.inject.Inject + +class TimetableLocal @Inject constructor(private val timetableDb: TimetableDao) { + + fun getLessons(semester: Semester, startDate: LocalDate, endDate: LocalDate): Maybe> { + return timetableDb.getTimetable(semester.diaryId, semester.studentId, startDate, endDate) + .filter { !it.isEmpty() } + } + + fun saveLessons(lessons: List) { + timetableDb.insertAll(lessons) + } + + fun deleteLessons(exams: List) { + timetableDb.deleteAll(exams) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/remote/TimetableRemote.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/remote/TimetableRemote.kt new file mode 100644 index 000000000..16daf19d7 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/remote/TimetableRemote.kt @@ -0,0 +1,40 @@ +package io.github.wulkanowy.data.repositories.remote + +import io.github.wulkanowy.api.Api +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.utils.toLocalDate +import io.github.wulkanowy.utils.toLocalDateTime +import io.reactivex.Single +import org.threeten.bp.LocalDate +import javax.inject.Inject + +class TimetableRemote @Inject constructor(private val api: Api) { + + fun getLessons(semester: Semester, startDate: LocalDate, endDate: LocalDate): Single> { + return Single.just(api.run { + if (diaryId != semester.diaryId) { + diaryId = semester.diaryId + notifyDataChanged() + } + }).flatMap { api.getTimetable(startDate, endDate) }.map { lessons -> + lessons.map { + Timetable( + studentId = semester.studentId, + diaryId = semester.diaryId, + number = it.number, + start = it.start.toLocalDateTime(), + end = it.end.toLocalDateTime(), + date = it.date.toLocalDate(), + subject = it.subject, + group = it.group, + room = it.room, + teacher = it.teacher, + info = it.info, + changes = it.changes, + canceled = it.canceled + ) + } + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/main/attendance/AttendanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/main/attendance/AttendanceFragment.kt index 3efd61ba8..283ccda69 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/main/attendance/AttendanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/main/attendance/AttendanceFragment.kt @@ -101,11 +101,6 @@ class AttendanceFragment : BaseFragment(), AttendanceView { outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) } - override fun onViewStateRestored(savedInstanceState: Bundle?) { - super.onViewStateRestored(savedInstanceState) - presenter.loadData(date = savedInstanceState?.getLong(SAVED_DATE_KEY)) - } - override fun onDestroyView() { super.onDestroyView() presenter.detachView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/main/attendance/AttendancePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/main/attendance/AttendancePresenter.kt index 20da58c69..d852dcf13 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/main/attendance/AttendancePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/main/attendance/AttendancePresenter.kt @@ -40,7 +40,7 @@ class AttendancePresenter @Inject constructor( disposable.add(sessionRepository.getSemesters() .map { selectSemester(it, -1) } .flatMap { attendanceRepository.getAttendance(it, currentDate, currentDate, forceRefresh) } - .map { createTimetableItems(it) } + .map { createAttendanceItems(it) } .subscribeOn(schedulers.backgroundThread()) .observeOn(schedulers.mainThread()) .doOnSubscribe { @@ -74,7 +74,7 @@ class AttendancePresenter @Inject constructor( }) } - private fun createTimetableItems(items: List): List { + private fun createAttendanceItems(items: List): List { return items.map { AttendanceItem().apply { attendance = it } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/main/exam/ExamFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/main/exam/ExamFragment.kt index 5bd94d187..94c781337 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/main/exam/ExamFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/main/exam/ExamFragment.kt @@ -94,11 +94,6 @@ class ExamFragment : BaseFragment(), ExamView { outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) } - override fun onViewStateRestored(savedInstanceState: Bundle?) { - super.onViewStateRestored(savedInstanceState) - presenter.loadData(date = savedInstanceState?.getLong(SAVED_DATE_KEY)) - } - override fun onDestroyView() { super.onDestroyView() presenter.detachView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/main/exam/ExamView.kt b/app/src/main/java/io/github/wulkanowy/ui/main/exam/ExamView.kt index 64ee9517a..e97d40771 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/main/exam/ExamView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/main/exam/ExamView.kt @@ -2,7 +2,6 @@ package io.github.wulkanowy.ui.main.exam import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.ui.base.BaseView -import org.threeten.bp.LocalDate interface ExamView : BaseView { diff --git a/app/src/main/java/io/github/wulkanowy/ui/main/more/MoreFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/main/more/MoreFragment.kt index c1431a9b5..2bec077e0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/main/more/MoreFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/main/more/MoreFragment.kt @@ -14,7 +14,7 @@ class MoreFragment : BaseFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_timetable, container, false) + return inflater.inflate(R.layout.fragment_attendance, container, false) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/main/timetable/TimetableDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/main/timetable/TimetableDialog.kt new file mode 100644 index 000000000..1bd82f59e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/main/timetable/TimetableDialog.kt @@ -0,0 +1,79 @@ +package io.github.wulkanowy.ui.main.timetable + +import android.annotation.SuppressLint +import android.os.Bundle +import android.support.v4.app.DialogFragment +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.ViewGroup +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.utils.toFormattedString +import kotlinx.android.synthetic.main.dialog_timetable.* + +class TimetableDialog : DialogFragment() { + + private lateinit var lesson: Timetable + + companion object { + private const val ARGUMENT_KEY = "Item" + + fun newInstance(exam: Timetable): TimetableDialog { + return TimetableDialog().apply { + arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(DialogFragment.STYLE_NORMAL, R.style.DialogFragmentTheme) + arguments?.run { + lesson = getSerializable(ARGUMENT_KEY) as Timetable + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + dialog.setTitle(getString(R.string.all_details)) + return inflater.inflate(R.layout.dialog_timetable, container, false) + } + + @SuppressLint("SetTextI18n") + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + timetableDialogSubject.text = lesson.subject + timetableDialogTime.text = "${lesson.start.toFormattedString("HH:mm")} - ${lesson.end.toFormattedString("HH:mm")}" + + lesson.group.let { + if (it.isBlank()) { + timetableDialogGroupTitle.visibility = GONE + timetableDialogGroup.visibility = GONE + } else timetableDialogGroup.text = it + } + + lesson.room.let { + if (it.isBlank()) { + timetableDialogRoomTitle.visibility = GONE + timetableDialogRoom.visibility = GONE + } else timetableDialogRoom.text = it + } + + lesson.teacher.let { + if (it.isBlank()) { + timetableDialogTeacherTitle.visibility = GONE + timetableDialogTeacher.visibility = GONE + } else timetableDialogTeacher.text = it + } + + lesson.info.let { + if (it.isBlank()) { + timetableDialogChangesTitle.visibility = GONE + timetableDialogChanges.visibility = GONE + } else timetableDialogChanges.text = it + } + + timetableDialogClose.setOnClickListener { dismiss() } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/main/timetable/TimetableFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/main/timetable/TimetableFragment.kt index 7cbf438b2..157b7b487 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/main/timetable/TimetableFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/main/timetable/TimetableFragment.kt @@ -4,17 +4,105 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.utils.setOnItemClickListener +import kotlinx.android.synthetic.main.fragment_timetable.* +import javax.inject.Inject -class TimetableFragment : BaseFragment() { +class TimetableFragment : BaseFragment(), TimetableView { + + @Inject + lateinit var presenter: TimetablePresenter + + @Inject + lateinit var timetableAdapter: FlexibleAdapter> companion object { + private const val SAVED_DATE_KEY = "CURRENT_DATE" fun newInstance() = TimetableFragment() } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_timetable, container, false) } -} + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + presenter.run { + attachView(this@TimetableFragment) + loadData(date = savedInstanceState?.getLong(SAVED_DATE_KEY)) + } + } + + override fun initView() { + timetableAdapter.run { + isAutoCollapseOnExpand = true + isAutoScrollOnExpand = true + setOnItemClickListener { presenter.onTimetableItemSelected(getItem(it))} + } + timetableRecycler.run { + layoutManager = SmoothScrollLinearLayoutManager(context) + adapter = timetableAdapter + } + timetableSwipe.setOnRefreshListener { presenter.loadData(date = null, forceRefresh = true) } + timetablePreviousButton.setOnClickListener { presenter.loadTimetableForPreviousDay() } + timetableNextButton.setOnClickListener { presenter.loadTimetableForNextDay() } + } + + override fun updateData(data: List) { + timetableAdapter.updateDataSet(data, true) + } + + override fun clearData() { + timetableAdapter.clear() + } + + override fun updateNavigationDay(date: String) { + timetableNavDate.text = date + } + + override fun isViewEmpty() = timetableAdapter.isEmpty + + override fun showEmpty(show: Boolean) { + timetableEmpty.visibility = if (show) View.VISIBLE else View.GONE + } + + override fun showProgress(show: Boolean) { + timetableProgress.visibility = if (show) View.VISIBLE else View.GONE + } + + override fun showContent(show: Boolean) { + timetableRecycler.visibility = if (show) View.VISIBLE else View.GONE + } + + override fun showRefresh(show: Boolean) { + timetableSwipe.isRefreshing = show + } + + override fun showPreButton(show: Boolean) { + timetablePreviousButton.visibility = if (show) View.VISIBLE else View.INVISIBLE + } + + override fun showNextButton(show: Boolean) { + timetableNextButton.visibility = if (show) View.VISIBLE else View.INVISIBLE + } + + override fun showTimetableDialog(lesson: Timetable) { + TimetableDialog.newInstance(lesson).show(fragmentManager, lesson.toString()) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) + } + + override fun onDestroyView() { + super.onDestroyView() + presenter.detachView() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/main/timetable/TimetableItem.kt b/app/src/main/java/io/github/wulkanowy/ui/main/timetable/TimetableItem.kt new file mode 100644 index 000000000..cfd75cb88 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/main/timetable/TimetableItem.kt @@ -0,0 +1,65 @@ +package io.github.wulkanowy.ui.main.timetable + +import android.annotation.SuppressLint +import android.graphics.Paint +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.viewholders.FlexibleViewHolder +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.utils.toFormattedString +import kotlinx.android.extensions.LayoutContainer +import kotlinx.android.synthetic.main.item_timetable.* + +class TimetableItem : AbstractFlexibleItem() { + + lateinit var lesson: Timetable + + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder { + return ViewHolder(view, adapter) + } + + override fun getLayoutRes(): Int = R.layout.item_timetable + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as TimetableItem + + if (lesson != other.lesson) return false + return true + } + + override fun hashCode(): Int { + return lesson.hashCode() + } + + override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ViewHolder, + position: Int, payloads: MutableList?) { + holder.bind(lesson) + } + + class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter), + LayoutContainer { + + override val containerView: View + get() = contentView + + @SuppressLint("SetTextI18n") + fun bind(lesson: Timetable) { + timetableItemNumber.text = lesson.number.toString() + timetableItemSubject.text = lesson.subject + timetableItemRoom.text = if (lesson.room.isNotBlank()) "${view.context.getString(R.string.timetable_room)} ${lesson.room}" else "" + timetableItemTime.text = "${lesson.start.toFormattedString("HH:mm")} - ${lesson.end.toFormattedString("HH:mm")}" + timetableItemAlert.visibility = if (lesson.changes || lesson.canceled) VISIBLE else GONE + timetableItemSubject.paintFlags = + if (lesson.canceled) timetableItemSubject.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG + else timetableItemSubject.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/main/timetable/TimetablePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/main/timetable/TimetablePresenter.kt new file mode 100644 index 000000000..98aa0c71b --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/main/timetable/TimetablePresenter.kt @@ -0,0 +1,93 @@ +package io.github.wulkanowy.ui.main.timetable + +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import io.github.wulkanowy.data.ErrorHandler +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Timetable +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.schedulers.SchedulersManager +import org.threeten.bp.LocalDate +import javax.inject.Inject + +class TimetablePresenter @Inject constructor( + private val errorHandler: ErrorHandler, + private val schedulers: SchedulersManager, + private val timetableRepository: TimetableRepository, + private val sessionRepository: SessionRepository +) : BasePresenter(errorHandler) { + + var currentDate: LocalDate = LocalDate.now().nearSchoolDayNextOnWeekEnd + private set + + override fun attachView(view: TimetableView) { + super.attachView(view) + view.initView() + } + + fun loadTimetableForPreviousDay() = loadData(currentDate.previousWorkDay.toEpochDay()) + + fun loadTimetableForNextDay() = loadData(currentDate.nextWorkDay.toEpochDay()) + + fun loadData(date: Long?, forceRefresh: Boolean = false) { + this.currentDate = LocalDate.ofEpochDay(date ?: currentDate.nearSchoolDayNextOnWeekEnd.toEpochDay()) + if (currentDate.isHolidays) return + + disposable.clear() + disposable.add(sessionRepository.getSemesters() + .map { selectSemester(it, -1) } + .flatMap { timetableRepository.getTimetable(it, currentDate, currentDate, forceRefresh) } + .map { createTimetableItems(it) } + .subscribeOn(schedulers.backgroundThread()) + .observeOn(schedulers.mainThread()) + .doOnSubscribe { + view?.run { + showRefresh(forceRefresh) + showProgress(!forceRefresh) + if (!forceRefresh) clearData() + showPreButton(!currentDate.minusDays(1).isHolidays) + showNextButton(!currentDate.plusDays(1).isHolidays) + updateNavigationDay(currentDate.toFormattedString("EEEE \n dd.MM.YYYY").capitalize()) + } + } + .doFinally { + view?.run { + showRefresh(false) + showProgress(false) + } + } + .subscribe({ + view?.run { + showEmpty(it.isEmpty()) + showContent(it.isNotEmpty()) + updateData(it) + } + }) { + view?.run { showEmpty(isViewEmpty()) } + errorHandler.proceed(it) + }) + } + + private fun createTimetableItems(items: List): List { + return items.map { + TimetableItem().apply { lesson = it } + } + } + + fun onTimetableItemSelected(item: AbstractFlexibleItem<*>?) { + if (item is TimetableItem) view?.showTimetableDialog(item.lesson) + } + + private fun selectSemester(semesters: List, index: Int): Semester { + return semesters.single { it.current }.let { currentSemester -> + if (index == -1) currentSemester + else semesters.single { semester -> + semester.run { + semesterName - 1 == index && diaryId == currentSemester.diaryId + } + } + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/main/timetable/TimetableView.kt b/app/src/main/java/io/github/wulkanowy/ui/main/timetable/TimetableView.kt new file mode 100644 index 000000000..a4e1c3603 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/main/timetable/TimetableView.kt @@ -0,0 +1,30 @@ +package io.github.wulkanowy.ui.main.timetable + +import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.ui.base.BaseView + +interface TimetableView : BaseView { + + fun initView() + + fun updateData(data: List) + + fun updateNavigationDay(date: String) + + fun showEmpty(show: Boolean) + + fun showProgress(show: Boolean) + + fun showContent(show: Boolean) + + fun showRefresh(show: Boolean) + + fun showPreButton(show: Boolean) + + fun showNextButton(show: Boolean) + + fun showTimetableDialog(lesson: Timetable) + + fun isViewEmpty(): Boolean + fun clearData() +} diff --git a/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt index 04cf01127..2501e013f 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt @@ -2,6 +2,7 @@ package io.github.wulkanowy.utils import org.threeten.bp.DayOfWeek.* import org.threeten.bp.LocalDate +import org.threeten.bp.LocalDateTime import org.threeten.bp.format.DateTimeFormatter import org.threeten.bp.format.DateTimeFormatter.ofPattern import org.threeten.bp.temporal.TemporalAdjusters @@ -15,13 +16,16 @@ fun Date.toLocalDate(): LocalDate { return LocalDate.parse(SimpleDateFormat(DATE_PATTERN, Locale.getDefault()).format(this)) } +fun Date.toLocalDateTime(): LocalDateTime = LocalDateTime.parse(SimpleDateFormat("yyyy-MM-dd HH:mm:ss", + Locale.getDefault()).format(this), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + fun String.toLocalDate(format: String = DATE_PATTERN): LocalDate { return LocalDate.parse(this, DateTimeFormatter.ofPattern(format)) } -fun LocalDate.toFormattedString(format: String): String = this.format(ofPattern(format)) +fun LocalDate.toFormattedString(format: String = DATE_PATTERN): String = this.format(ofPattern(format)) -fun LocalDate.toFormattedString(): String = this.toFormattedString(DATE_PATTERN) +fun LocalDateTime.toFormattedString(format: String = DATE_PATTERN): String = this.format(DateTimeFormatter.ofPattern(format)) inline val LocalDate.nextWorkDay: LocalDate get() { diff --git a/app/src/main/res/layout/dialog_timetable.xml b/app/src/main/res/layout/dialog_timetable.xml index 365af4bfe..a1f2b62f5 100644 --- a/app/src/main/res/layout/dialog_timetable.xml +++ b/app/src/main/res/layout/dialog_timetable.xml @@ -1,6 +1,5 @@ @@ -8,194 +7,122 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:minWidth="300dp" - android:orientation="vertical"> + android:orientation="vertical" + android:padding="20dp"> - + android:text="@string/timetable_changes" + android:textColor="@color/colorPrimary" + android:textSize="17sp" /> - + - + - + - + - + - + - + - + - + - + - + - +