diff --git a/.travis.yml b/.travis.yml index e1d86ee3..8920018e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,9 +11,9 @@ cache: - $HOME/.gradle/caches/ - $HOME/.gradle/wrapper/ -#branches: -# only: -# - master +branches: + only: + - master android: licenses: diff --git a/app/build.gradle b/app/build.gradle index 9b3cb929..6e3911fe 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -73,7 +73,7 @@ play { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - implementation('com.github.wulkanowy:api:f941c4b1c7') { exclude module: "threetenbp" } + implementation('com.github.wulkanowy:api:0bbd246778') { exclude module: "threetenbp" } implementation "androidx.legacy:legacy-support-v4:1.0.0" implementation "androidx.appcompat:appcompat:1.0.2" diff --git a/app/src/androidTest/java/io/github/wulkanowy/data/repositories/local/CompletedLessonsLocalTest.kt b/app/src/androidTest/java/io/github/wulkanowy/data/repositories/local/CompletedLessonsLocalTest.kt new file mode 100644 index 00000000..9da6fdf6 --- /dev/null +++ b/app/src/androidTest/java/io/github/wulkanowy/data/repositories/local/CompletedLessonsLocalTest.kt @@ -0,0 +1,57 @@ +package io.github.wulkanowy.data.repositories.local + +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.github.wulkanowy.data.db.AppDatabase +import io.github.wulkanowy.data.db.entities.CompletedLesson +import io.github.wulkanowy.data.db.entities.Semester +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 CompletedLessonsLocalTest { + + private lateinit var completedLessonsLocal: CompletedLessonsLocal + + private lateinit var testDb: AppDatabase + + @Before + fun createDb() { + testDb = Room.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), AppDatabase::class.java) + .build() + completedLessonsLocal = CompletedLessonsLocal(testDb.completedLessonsDao) + } + + @After + fun closeDb() { + testDb.close() + } + + @Test + fun saveAndReadTest() { + completedLessonsLocal.saveCompletedLessons(listOf( + getCompletedLesson(LocalDate.of(2018, 9, 10), 1), + getCompletedLesson(LocalDate.of(2018, 9, 14), 2), + getCompletedLesson(LocalDate.of(2018, 9, 17), 3) + )) + + val completed = completedLessonsLocal + .getCompletedLessons(Semester(1, 1, 2, "", 3, 1), + LocalDate.of(2018, 9, 10), + LocalDate.of(2018, 9, 14) + ) + .blockingGet() + assertEquals(2, completed.size) + assertEquals(completed[0].date, LocalDate.of(2018, 9, 10)) + assertEquals(completed[1].date, LocalDate.of(2018, 9, 14)) + } + + private fun getCompletedLesson(date: LocalDate, number: Int): CompletedLesson { + return CompletedLesson(1, 2, date, number, "", "", "", "", "", "", "") + } +} 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 e72175bb..fc458c69 100644 --- a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt @@ -117,4 +117,8 @@ internal class RepositoryModule { @Singleton @Provides fun provideLuckyNumberDao(database: AppDatabase) = database.luckyNumberDao + + @Singleton + @Provides + fun provideCompletedLessonsDao(database: AppDatabase) = database.completedLessonsDao } 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 7dfd45fc..f6fa9702 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 io.github.wulkanowy.data.db.dao.HomeworkDao import io.github.wulkanowy.data.db.dao.LuckyNumberDao import io.github.wulkanowy.data.db.dao.MessagesDao import io.github.wulkanowy.data.db.dao.NoteDao +import io.github.wulkanowy.data.db.dao.CompletedLessonsDao import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.StudentDao import io.github.wulkanowy.data.db.dao.SubjectDao @@ -28,11 +29,13 @@ import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Note +import io.github.wulkanowy.data.db.entities.CompletedLesson import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Subject import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.migrations.Migration2 +import io.github.wulkanowy.data.db.migrations.Migration3 import javax.inject.Singleton @Singleton @@ -50,9 +53,10 @@ import javax.inject.Singleton Note::class, Homework::class, Subject::class, - LuckyNumber::class + LuckyNumber::class, + CompletedLesson::class ], - version = 2, + version = 3, exportSchema = false ) @TypeConverters(Converters::class) @@ -63,7 +67,8 @@ abstract class AppDatabase : RoomDatabase() { return Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database") .setJournalMode(TRUNCATE) .addMigrations( - Migration2() + Migration2(), + Migration3() ) .build() } @@ -94,4 +99,6 @@ abstract class AppDatabase : RoomDatabase() { abstract val subjectDao: SubjectDao abstract val luckyNumberDao: LuckyNumberDao + + abstract val completedLessonsDao: CompletedLessonsDao } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/CompletedLessonsDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/CompletedLessonsDao.kt new file mode 100644 index 00000000..3f2e29b8 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/CompletedLessonsDao.kt @@ -0,0 +1,24 @@ +package io.github.wulkanowy.data.db.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import io.github.wulkanowy.data.db.entities.CompletedLesson +import io.reactivex.Maybe +import org.threeten.bp.LocalDate +import javax.inject.Singleton + +@Singleton +@Dao +interface CompletedLessonsDao { + + @Insert + fun insertAll(exams: List): List + + @Delete + fun deleteAll(exams: List) + + @Query("SELECT * FROM CompletedLesson WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end") + fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Maybe> +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/CompletedLesson.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/CompletedLesson.kt new file mode 100644 index 00000000..fbb5b3b6 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/CompletedLesson.kt @@ -0,0 +1,40 @@ +package io.github.wulkanowy.data.db.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import org.threeten.bp.LocalDate +import java.io.Serializable + +@Entity(tableName = "CompletedLesson") +data class CompletedLesson( + + @ColumnInfo(name = "student_id") + var studentId: Int, + + @ColumnInfo(name = "diary_id") + var diaryId: Int, + + var date: LocalDate, + + var number: Int, + + var subject: String, + + var topic: String, + + var teacher: String, + + @ColumnInfo(name = "teacher_symbol") + var teacherSymbol: String, + + var substitution: String, + + var absence: String, + + var resources: String +) : Serializable { + + @PrimaryKey(autoGenerate = true) + var id: Long = 0 +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration3.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration3.kt new file mode 100644 index 00000000..974c33e9 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration3.kt @@ -0,0 +1,23 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration3 : Migration(2, 3) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE CompletedLesson (" + + "id INTEGER NOT NULL PRIMARY KEY, " + + "student_id INTEGER NOT NULL, " + + "diary_id INTEGER NOT NULL, " + + "date INTEGER NOT NULL, " + + "number INTEGER NOT NULL, " + + "subject TEXT NOT NULL, " + + "topic TEXT NOT NULL, " + + "teacher TEXT NOT NULL, " + + "teacher_symbol TEXT NOT NULL, " + + "substitution TEXT NOT NULL, " + + "absence TEXT NOT NULL, " + + "resources TEXT NOT NULL)") + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt new file mode 100644 index 00000000..b8253d8d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.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.CompletedLesson +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.repositories.local.CompletedLessonsLocal +import io.github.wulkanowy.data.repositories.remote.CompletedLessonsRemote +import io.github.wulkanowy.utils.friday +import io.github.wulkanowy.utils.monday +import io.reactivex.Single +import org.threeten.bp.LocalDate +import java.net.UnknownHostException +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class CompletedLessonsRepository @Inject constructor( + private val settings: InternetObservingSettings, + private val local: CompletedLessonsLocal, + private val remote: CompletedLessonsRemote +) { + + fun getCompletedLessons(semester: Semester, startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean = false): Single> { + return Single.fromCallable { startDate.monday to endDate.friday } + .flatMap { dates -> + local.getCompletedLessons(semester, dates.first, dates.second).filter { !forceRefresh } + .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings) + .flatMap { + if (it) remote.getCompletedLessons(semester, dates.first, dates.second) + else Single.error(UnknownHostException()) + }.flatMap { new -> + local.getCompletedLessons(semester, dates.first, dates.second) + .toSingle(emptyList()) + .doOnSuccess { old -> + local.deleteCompleteLessons(old - new) + local.saveCompletedLessons(new - old) + } + }.flatMap { + local.getCompletedLessons(semester, dates.first, dates.second) + .toSingle(emptyList()) + }).map { list -> list.filter { it.date in startDate..endDate } } + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/local/CompletedLessonsLocal.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/local/CompletedLessonsLocal.kt new file mode 100644 index 00000000..f3ff42ff --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/local/CompletedLessonsLocal.kt @@ -0,0 +1,25 @@ +package io.github.wulkanowy.data.repositories.local + +import io.github.wulkanowy.data.db.dao.CompletedLessonsDao +import io.github.wulkanowy.data.db.entities.CompletedLesson +import io.github.wulkanowy.data.db.entities.Semester +import io.reactivex.Maybe +import org.threeten.bp.LocalDate +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class CompletedLessonsLocal @Inject constructor(private val completedLessonsDb: CompletedLessonsDao) { + + fun getCompletedLessons(semester: Semester, start: LocalDate, end: LocalDate): Maybe> { + return completedLessonsDb.loadAll(semester.diaryId, semester.studentId, start, end).filter { !it.isEmpty() } + } + + fun saveCompletedLessons(completedLessons: List) { + completedLessonsDb.insertAll(completedLessons) + } + + fun deleteCompleteLessons(completedLessons: List) { + completedLessonsDb.deleteAll(completedLessons) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/remote/CompletedLessonsRemote.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/remote/CompletedLessonsRemote.kt new file mode 100644 index 00000000..0d9b28ac --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/remote/CompletedLessonsRemote.kt @@ -0,0 +1,37 @@ +package io.github.wulkanowy.data.repositories.remote + +import io.github.wulkanowy.api.Api +import io.github.wulkanowy.api.toLocalDate +import io.github.wulkanowy.data.db.entities.CompletedLesson +import io.github.wulkanowy.data.db.entities.Semester +import io.reactivex.Single +import org.threeten.bp.LocalDate +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class CompletedLessonsRemote @Inject constructor(private val api: Api) { + + fun getCompletedLessons(semester: Semester, startDate: LocalDate, endDate: LocalDate): Single> { + return Single.just(api.apply { diaryId = semester.diaryId }) + .flatMap { it.getCompletedLessons(startDate, endDate) } + .map { lessons -> + lessons.map { + it.absence + CompletedLesson( + studentId = semester.studentId, + diaryId = semester.diaryId, + date = it.date.toLocalDate(), + number = it.number, + subject = it.subject, + topic = it.topic, + teacher = it.teacher, + teacherSymbol = it.teacherSymbol, + substitution = it.substitution, + absence = it.absence, + resources = it.resources + ) + } + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/job/SyncWorker.kt b/app/src/main/java/io/github/wulkanowy/services/job/SyncWorker.kt index 5ad8ce0f..a5fd754e 100644 --- a/app/src/main/java/io/github/wulkanowy/services/job/SyncWorker.kt +++ b/app/src/main/java/io/github/wulkanowy/services/job/SyncWorker.kt @@ -4,6 +4,7 @@ import com.firebase.jobdispatcher.JobParameters import com.firebase.jobdispatcher.SimpleJobService import dagger.android.AndroidInjection import io.github.wulkanowy.data.repositories.AttendanceRepository +import io.github.wulkanowy.data.repositories.CompletedLessonsRepository import io.github.wulkanowy.data.repositories.ExamRepository import io.github.wulkanowy.data.repositories.GradeRepository import io.github.wulkanowy.data.repositories.GradeSummaryRepository @@ -64,6 +65,9 @@ class SyncWorker : SimpleJobService() { @Inject lateinit var luckyNumber: LuckyNumberRepository + @Inject + lateinit var completedLessons: CompletedLessonsRepository + @Inject lateinit var prefRepository: PreferencesRepository @@ -105,7 +109,8 @@ class SyncWorker : SimpleJobService() { note.getNotes(it.first, true, notify).ignoreElement(), homework.getHomework(it.first, LocalDate.now(), true).ignoreElement(), homework.getHomework(it.first, LocalDate.now().plusDays(1), true).ignoreElement(), - luckyNumber.getLuckyNumber(it.first, true, notify).ignoreElement() + luckyNumber.getLuckyNumber(it.first, true, notify).ignoreElement(), + completedLessons.getCompletedLessons(it.first, start, end, true).ignoreElement() ) ) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt index a8f210e6..83cbf0ef 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt @@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.base import android.content.res.Resources import com.readystatesoftware.chuck.api.ChuckCollector import io.github.wulkanowy.R +import io.github.wulkanowy.api.interceptor.FeatureDisabledException import io.github.wulkanowy.api.interceptor.ServiceUnavailableException import io.github.wulkanowy.api.login.NotLoggedInException import timber.log.Timber @@ -26,6 +27,7 @@ open class ErrorHandler @Inject constructor(protected val resources: Resources, is SocketTimeoutException -> resources.getString(R.string.error_timeout) is NotLoggedInException -> resources.getString(R.string.error_login_failed) is ServiceUnavailableException -> resources.getString(R.string.error_service_unavailable) + is FeatureDisabledException -> resources.getString(R.string.error_feature_disabled) else -> resources.getString(R.string.error_unknown) }), error) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/session/SessionErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/base/session/SessionErrorHandler.kt index f8ea6e3d..1d81e932 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/session/SessionErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/session/SessionErrorHandler.kt @@ -6,7 +6,10 @@ import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.security.ScramblerException import javax.inject.Inject -class SessionErrorHandler @Inject constructor(resources: Resources, chuckCollector: ChuckCollector) : ErrorHandler(resources, chuckCollector) { +open class SessionErrorHandler @Inject constructor( + resources: Resources, + chuckCollector: ChuckCollector +) : ErrorHandler(resources, chuckCollector) { var onDecryptionFail: () -> Unit = {} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt index 18c3a422..a70ff2d6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt @@ -8,7 +8,10 @@ import io.github.wulkanowy.api.login.BadCredentialsException import io.github.wulkanowy.ui.base.ErrorHandler import javax.inject.Inject -class LoginErrorHandler @Inject constructor(resources: Resources, chuckCollector: ChuckCollector) : ErrorHandler(resources, chuckCollector) { +class LoginErrorHandler @Inject constructor( + resources: Resources, + chuckCollector: ChuckCollector +) : ErrorHandler(resources, chuckCollector) { var onBadCredentials: () -> Unit = {} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainModule.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainModule.kt index 1d98ce63..766c6129 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainModule.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainModule.kt @@ -24,6 +24,7 @@ import io.github.wulkanowy.ui.modules.more.MoreFragment import io.github.wulkanowy.ui.modules.note.NoteFragment import io.github.wulkanowy.ui.modules.settings.SettingsFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment +import io.github.wulkanowy.ui.modules.timetable.completed.CompletedLessonsFragment @Module abstract class MainModule { @@ -93,5 +94,9 @@ abstract class MainModule { @PerFragment @ContributesAndroidInjector - abstract fun bindsAccountDialog(): AccountDialog + abstract fun bindAccountDialog(): AccountDialog + + @PerFragment + @ContributesAndroidInjector + abstract fun bindCompletedLessonsFragment(): CompletedLessonsFragment } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt index 71c2f70b..7019415e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt @@ -2,6 +2,9 @@ package io.github.wulkanowy.ui.modules.timetable import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup import eu.davidea.flexibleadapter.FlexibleAdapter @@ -12,6 +15,7 @@ import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.ui.base.session.BaseSessionFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.modules.timetable.completed.CompletedLessonsFragment import io.github.wulkanowy.utils.setOnItemClickListener import kotlinx.android.synthetic.main.fragment_timetable.* import javax.inject.Inject @@ -39,6 +43,14 @@ class TimetableFragment : BaseSessionFragment(), TimetableView, MainView.MainChi override val isViewEmpty: Boolean get() = timetableAdapter.isEmpty + override val currentStackSize: Int? + get() = (activity as? MainActivity)?.currentStackSize + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_timetable, container, false) } @@ -63,6 +75,15 @@ class TimetableFragment : BaseSessionFragment(), TimetableView, MainView.MainChi timetableNextButton.setOnClickListener { presenter.onNextDay() } } + override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { + inflater?.inflate(R.menu.action_menu_timetable, menu) + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + return if (item?.itemId == R.id.timetableMenuCompletedLessons) presenter.onCompletedLessonsSwitchSelected() + else false + } + override fun updateData(data: List) { timetableAdapter.updateDataSet(data, true) } @@ -87,6 +108,10 @@ class TimetableFragment : BaseSessionFragment(), TimetableView, MainView.MainChi presenter.onViewReselected() } + override fun popView() { + (activity as? MainActivity)?.popView() + } + override fun showEmpty(show: Boolean) { timetableEmpty.visibility = if (show) View.VISIBLE else View.GONE } @@ -111,6 +136,10 @@ class TimetableFragment : BaseSessionFragment(), TimetableView, MainView.MainChi (activity as? MainActivity)?.showDialogFragment(TimetableDialog.newInstance(lesson)) } + override fun openCompletedLessonsView() { + (activity as? MainActivity)?.pushView(CompletedLessonsFragment.newInstance()) + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) 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 58c9d030..776dd890 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 @@ -57,12 +57,16 @@ class TimetablePresenter @Inject constructor( } fun onViewReselected() { - Timber.i("Exam view is reselected") - now().nextOrSameSchoolDay.also { - if (currentDate != it) { - loadData(it) - reloadView() - } else if (view?.isViewEmpty == false) view?.resetView() + Timber.i("Timetable view is reselected") + view?.also { view -> + if (view.currentStackSize == 1) { + now().nextOrSameSchoolDay.also { + if (currentDate != it) { + loadData(it) + reloadView() + } else if (!view.isViewEmpty) view.resetView() + } + } else view.popView() } } @@ -73,6 +77,11 @@ class TimetablePresenter @Inject constructor( } } + fun onCompletedLessonsSwitchSelected(): Boolean { + view?.openCompletedLessonsView() + return true + } + private fun loadData(date: LocalDate, forceRefresh: Boolean = false) { Timber.i("Loading timetable data started") currentDate = date diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt index 0ad0eaeb..ac01d221 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt @@ -9,6 +9,8 @@ interface TimetableView : BaseSessionView { val isViewEmpty: Boolean + val currentStackSize: Int? + fun initView() fun updateData(data: List) @@ -32,4 +34,8 @@ interface TimetableView : BaseSessionView { fun showNextButton(show: Boolean) fun showTimetableDialog(lesson: Timetable) + + fun popView() + + fun openCompletedLessonsView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonDialog.kt new file mode 100644 index 00000000..8f7b1ec5 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonDialog.kt @@ -0,0 +1,73 @@ +package io.github.wulkanowy.ui.modules.timetable.completed + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.CompletedLesson +import kotlinx.android.synthetic.main.dialog_lesson_completed.* + +class CompletedLessonDialog : DialogFragment() { + + private lateinit var completedLesson: CompletedLesson + + companion object { + private const val ARGUMENT_KEY = "Item" + + fun newInstance(exam: CompletedLesson): CompletedLessonDialog { + return CompletedLessonDialog().apply { + arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_TITLE, 0) + arguments?.run { + completedLesson = getSerializable(CompletedLessonDialog.ARGUMENT_KEY) as CompletedLesson + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.dialog_lesson_completed, container, false) + } + + @SuppressLint("SetTextI18n") + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + completedLessonDialogSubject.text = completedLesson.subject + completedLessonDialogTopic.text = completedLesson.topic + completedLessonDialogTeacher.text = completedLesson.teacher + completedLessonDialogAbsence.text = completedLesson.absence + completedLessonDialogChanges.text = completedLesson.substitution + completedLessonDialogResources.text = completedLesson.resources + + completedLesson.substitution.let { + if (it.isBlank()) { + completedLessonDialogChangesTitle.visibility = View.GONE + completedLessonDialogChanges.visibility = View.GONE + } else completedLessonDialogChanges.text = it + } + + completedLesson.absence.let { + if (it.isBlank()) { + completedLessonDialogAbsenceTitle.visibility = View.GONE + completedLessonDialogAbsence.visibility = View.GONE + } else completedLessonDialogAbsence.text = it + } + + completedLesson.resources.let { + if (it.isBlank()) { + completedLessonDialogResourcesTitle.visibility = View.GONE + completedLessonDialogResources.visibility = View.GONE + } else completedLessonDialogResources.text = it + } + + completedLessonDialogClose.setOnClickListener { dismiss() } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonItem.kt new file mode 100644 index 00000000..716903f5 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonItem.kt @@ -0,0 +1,53 @@ +package io.github.wulkanowy.ui.modules.timetable.completed + +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.CompletedLesson +import kotlinx.android.extensions.LayoutContainer +import kotlinx.android.synthetic.main.item_completed_lesson.* + +class CompletedLessonItem(val completedLesson: CompletedLesson) : AbstractFlexibleItem() { + + override fun getLayoutRes() = R.layout.item_completed_lesson + + override fun createViewHolder(view: View?, adapter: FlexibleAdapter>?): CompletedLessonItem.ViewHolder { + return CompletedLessonItem.ViewHolder(view, adapter) + } + + override fun bindViewHolder(adapter: FlexibleAdapter>?, holder: CompletedLessonItem.ViewHolder?, position: Int, payloads: MutableList?) { + holder?.apply { + completedLessonItemNumber.text = completedLesson.number.toString() + completedLessonItemSubject.text = completedLesson.subject + completedLessonItemTopic.text = completedLesson.topic + completedLessonItemAlert.visibility = if (completedLesson.substitution.isNotEmpty()) VISIBLE else GONE + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CompletedLessonItem + + if (completedLesson != other.completedLesson) return false + + return true + } + + override fun hashCode(): Int { + return completedLesson.hashCode() + } + + class ViewHolder(view: View?, adapter: FlexibleAdapter>?) : FlexibleViewHolder(view, adapter), + LayoutContainer { + + override val containerView: View? + get() = contentView + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsErrorHandler.kt new file mode 100644 index 00000000..50d55685 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsErrorHandler.kt @@ -0,0 +1,27 @@ +package io.github.wulkanowy.ui.modules.timetable.completed + +import android.content.res.Resources +import com.readystatesoftware.chuck.api.ChuckCollector +import io.github.wulkanowy.api.interceptor.FeatureDisabledException +import io.github.wulkanowy.ui.base.session.SessionErrorHandler +import javax.inject.Inject + +class CompletedLessonsErrorHandler @Inject constructor( + resources: Resources, + chuckCollector: ChuckCollector +) : SessionErrorHandler(resources, chuckCollector) { + + var onFeatureDisabled: () -> Unit = {} + + override fun proceed(error: Throwable) { + when (error) { + is FeatureDisabledException -> onFeatureDisabled() + else -> super.proceed(error) + } + } + + override fun clear() { + super.clear() + onFeatureDisabled = {} + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt new file mode 100644 index 00000000..2ccd15ea --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt @@ -0,0 +1,120 @@ +package io.github.wulkanowy.ui.modules.timetable.completed + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +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.CompletedLesson +import io.github.wulkanowy.ui.base.session.BaseSessionFragment +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.utils.setOnItemClickListener +import kotlinx.android.synthetic.main.fragment_timetable_completed.* +import javax.inject.Inject + +class CompletedLessonsFragment : BaseSessionFragment(), CompletedLessonsView, MainView.TitledView { + + @Inject + lateinit var presenter: CompletedLessonsPresenter + + @Inject + lateinit var completedLessonsAdapter: FlexibleAdapter> + + companion object { + private const val SAVED_DATE_KEY = "CURRENT_DATE" + + fun newInstance() = CompletedLessonsFragment() + } + + override val titleStringId: Int + get() = R.string.completed_lessons_title + + override val isViewEmpty + get() = completedLessonsAdapter.isEmpty + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_timetable_completed, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + messageContainer = completedLessonsRecycler + presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY)) + } + + override fun initView() { + completedLessonsAdapter.run { + setOnItemClickListener { presenter.onCompletedLessonsItemSelected(it) } + } + + completedLessonsRecycler.run { + layoutManager = SmoothScrollLinearLayoutManager(context) + adapter = completedLessonsAdapter + } + completedLessonsSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } + completedLessonsPreviousButton.setOnClickListener { presenter.onPreviousDay() } + completedLessonsNextButton.setOnClickListener { presenter.onNextDay() } + } + + override fun updateData(data: List) { + completedLessonsAdapter.updateDataSet(data, true) + } + + override fun clearData() { + completedLessonsAdapter.clear() + } + + override fun updateNavigationDay(date: String) { + completedLessonsNavDate.text = date + } + + override fun hideRefresh() { + completedLessonsSwipe.isRefreshing = false + } + + override fun showEmpty(show: Boolean) { + completedLessonsEmpty.visibility = if (show) View.VISIBLE else View.GONE + } + + override fun showFeatureDisabled() { + context?.let { + completedLessonsInfo.text = getString(R.string.error_feature_disabled) + completedLessonsInfoImage.setImageDrawable(ContextCompat.getDrawable(it, R.drawable.ic_all_close_circle_24dp)) + } + } + + override fun showProgress(show: Boolean) { + completedLessonsProgress.visibility = if (show) View.VISIBLE else View.GONE + } + + override fun showContent(show: Boolean) { + completedLessonsRecycler.visibility = if (show) View.VISIBLE else View.GONE + } + + override fun showPreButton(show: Boolean) { + completedLessonsPreviousButton.visibility = if (show) View.VISIBLE else View.INVISIBLE + } + + override fun showNextButton(show: Boolean) { + completedLessonsNextButton.visibility = if (show) View.VISIBLE else View.INVISIBLE + } + + override fun showCompletedLessonDialog(completedLesson: CompletedLesson) { + (activity as? MainActivity)?.showDialogFragment(CompletedLessonDialog.newInstance(completedLesson)) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putLong(CompletedLessonsFragment.SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) + } + + override fun onDestroyView() { + presenter.onDetachView() + super.onDestroyView() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsPresenter.kt new file mode 100644 index 00000000..2da0b74f --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsPresenter.kt @@ -0,0 +1,116 @@ +package io.github.wulkanowy.ui.modules.timetable.completed + +import com.google.firebase.analytics.FirebaseAnalytics.Param.START_DATE +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import io.github.wulkanowy.data.repositories.CompletedLessonsRepository +import io.github.wulkanowy.data.repositories.SemesterRepository +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.ui.base.session.BaseSessionPresenter +import io.github.wulkanowy.utils.FirebaseAnalyticsHelper +import io.github.wulkanowy.utils.SchedulersProvider +import io.github.wulkanowy.utils.isHolidays +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 +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class CompletedLessonsPresenter @Inject constructor( + private val schedulers: SchedulersProvider, + private val errorHandler: CompletedLessonsErrorHandler, + private val studentRepository: StudentRepository, + private val semesterRepository: SemesterRepository, + private val completedLessonsRepository: CompletedLessonsRepository, + private val analytics: FirebaseAnalyticsHelper +) : BaseSessionPresenter(errorHandler) { + + lateinit var currentDate: LocalDate + private set + + fun onAttachView(view: CompletedLessonsView, date: Long?) { + super.onAttachView(view) + Timber.i("Completed lessons is attached") + view.initView() + loadData(ofEpochDay(date ?: now().nextOrSameSchoolDay.toEpochDay())) + reloadView() + errorHandler.onFeatureDisabled = { + this.view?.showFeatureDisabled() + Timber.i("Completed lessons feature disabled by school") + } + } + + fun onPreviousDay() { + loadData(currentDate.previousSchoolDay) + reloadView() + } + + fun onNextDay() { + loadData(currentDate.nextSchoolDay) + reloadView() + } + + fun onSwipeRefresh() { + Timber.i("Force refreshing the completed lessons") + loadData(currentDate, true) + } + + fun onCompletedLessonsItemSelected(item: AbstractFlexibleItem<*>?) { + if (item is CompletedLessonItem) { + Timber.i("Select completed lessons item ${item.completedLesson.id}") + view?.showCompletedLessonDialog(item.completedLesson) + } + } + + private fun loadData(date: LocalDate, forceRefresh: Boolean = false) { + Timber.i("Loading completed lessons data started") + currentDate = date + disposable.apply { + clear() + add(studentRepository.getCurrentStudent() + .flatMap { semesterRepository.getCurrentSemester(it) } + .delay(200, TimeUnit.MILLISECONDS) + .flatMap { completedLessonsRepository.getCompletedLessons(it, currentDate, currentDate, forceRefresh) } + .map { items -> items.map { CompletedLessonItem(it) } } + .map { items -> items.sortedBy { it.completedLesson.number } } + .subscribeOn(schedulers.backgroundThread) + .observeOn(schedulers.mainThread) + .doFinally { + view?.run { + hideRefresh() + showProgress(false) + } + } + .subscribe({ + Timber.i("Loading completed lessons lessons result: Success") + view?.apply { + updateData(it) + showEmpty(it.isEmpty()) + showContent(it.isNotEmpty()) + } + analytics.logEvent("load_completed_lessons", "items" to it.size, "force_refresh" to forceRefresh, START_DATE to currentDate.toFormattedString("yyyy-MM-dd")) + }) { + Timber.i("Loading completed lessons result: An exception occurred") + view?.run { showEmpty(isViewEmpty) } + errorHandler.dispatch(it) + }) + } + } + + private fun reloadView() { + Timber.i("Reload completed lessons view with the date ${currentDate.toFormattedString()}") + view?.apply { + showProgress(true) + showContent(false) + showEmpty(false) + clearData() + showNextButton(!currentDate.plusDays(1).isHolidays) + showPreButton(!currentDate.minusDays(1).isHolidays) + updateNavigationDay(currentDate.toFormattedString("EEEE \n dd.MM.YYYY").capitalize()) + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsView.kt new file mode 100644 index 00000000..0dc52362 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsView.kt @@ -0,0 +1,33 @@ +package io.github.wulkanowy.ui.modules.timetable.completed + +import io.github.wulkanowy.data.db.entities.CompletedLesson +import io.github.wulkanowy.ui.base.session.BaseSessionView + +interface CompletedLessonsView : BaseSessionView { + + val isViewEmpty: Boolean + + fun initView() + + fun updateData(data: List) + + fun clearData() + + fun updateNavigationDay(date: String) + + fun hideRefresh() + + fun showEmpty(show: Boolean) + + fun showFeatureDisabled() + + fun showProgress(show: Boolean) + + fun showContent(show: Boolean) + + fun showPreButton(show: Boolean) + + fun showNextButton(show: Boolean) + + fun showCompletedLessonDialog(completedLesson: CompletedLesson) +} diff --git a/app/src/main/res/drawable/ic_all_close_circle_24dp.xml b/app/src/main/res/drawable/ic_all_close_circle_24dp.xml new file mode 100644 index 00000000..3809c797 --- /dev/null +++ b/app/src/main/res/drawable/ic_all_close_circle_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_main_lessons_completed_24dp.xml b/app/src/main/res/drawable/ic_menu_main_lessons_completed_24dp.xml new file mode 100644 index 00000000..c0664c37 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_main_lessons_completed_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/dialog_lesson_completed.xml b/app/src/main/res/layout/dialog_lesson_completed.xml new file mode 100644 index 00000000..161eb4c5 --- /dev/null +++ b/app/src/main/res/layout/dialog_lesson_completed.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +