From 7ec7afed87333af6ee460e87a57bb3bf3cb3a8fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Sep 2021 08:22:06 +0000 Subject: [PATCH 001/357] Bump firebase-bom from 28.4.0 to 28.4.1 (#1520) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 74585998..8e8be8bb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -211,7 +211,7 @@ dependencies { implementation 'me.xdrop:fuzzywuzzy:1.3.1' implementation 'com.fredporciuncula:flow-preferences:1.5.0' - playImplementation platform('com.google.firebase:firebase-bom:28.4.0') + playImplementation platform('com.google.firebase:firebase-bom:28.4.1') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From 037dbd792f6d5b5381419ee6529cfc9156598e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 16 Sep 2021 10:51:38 +0200 Subject: [PATCH 002/357] Add conference dialog (#1519) --- .../modules/conference/ConferenceAdapter.kt | 7 +- .../ui/modules/conference/ConferenceDialog.kt | 60 +++++ .../modules/conference/ConferenceFragment.kt | 13 +- .../modules/conference/ConferencePresenter.kt | 5 + .../ui/modules/conference/ConferenceView.kt | 2 + .../SchoolAnnouncementDialog.kt | 2 +- app/src/main/res/layout/dialog_conference.xml | 211 ++++++++++++++++++ .../res/layout/dialog_school_announcement.xml | 1 + app/src/main/res/values/strings.xml | 6 +- 9 files changed, 303 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceDialog.kt create mode 100644 app/src/main/res/layout/dialog_conference.xml diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceAdapter.kt index c8728614..f63b293c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceAdapter.kt @@ -14,6 +14,8 @@ class ConferenceAdapter @Inject constructor() : var items = emptyList() + var onItemClickListener: (Conference) -> Unit = {} + override fun getItemCount() = items.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( @@ -28,7 +30,10 @@ class ConferenceAdapter @Inject constructor() : conferenceItemTitle.text = item.title conferenceItemSubject.text = item.subject conferenceItemContent.text = item.agenda - conferenceItemContent.visibility = if (item.agenda.isBlank()) View.GONE else View.VISIBLE + conferenceItemContent.visibility = + if (item.agenda.isBlank()) View.GONE else View.VISIBLE + + root.setOnClickListener { onItemClickListener(item) } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceDialog.kt new file mode 100644 index 00000000..477b762b --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceDialog.kt @@ -0,0 +1,60 @@ +package io.github.wulkanowy.ui.modules.conference + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.fragment.app.DialogFragment +import io.github.wulkanowy.data.db.entities.Conference +import io.github.wulkanowy.databinding.DialogConferenceBinding +import io.github.wulkanowy.utils.lifecycleAwareVariable +import io.github.wulkanowy.utils.toFormattedString + +class ConferenceDialog : DialogFragment() { + + private var binding: DialogConferenceBinding by lifecycleAwareVariable() + + private lateinit var conference: Conference + + companion object { + + private const val ARGUMENT_KEY = "item" + + fun newInstance(conference: Conference) = ConferenceDialog().apply { + arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, conference) } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_TITLE, 0) + arguments?.let { + conference = it.getSerializable(ARGUMENT_KEY) as Conference + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DialogConferenceBinding.inflate(inflater).also { binding = it }.root + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + with(binding) { + conferenceDialogClose.setOnClickListener { dismiss() } + + conferenceDialogSubjectValue.text = conference.subject + conferenceDialogDateValue.text = conference.date.toFormattedString("dd.MM.yyyy HH:mm") + conferenceDialogHeaderValue.text = conference.title + conferenceDialogAgendaValue.text = conference.agenda + conferenceDialogPresentValue.text = conference.presentOnConference + conferenceDialogPresentValue.isVisible = conference.presentOnConference.isNotBlank() + conferenceDialogPresentTitle.isVisible = conference.presentOnConference.isNotBlank() + conferenceDialogAgendaValue.isVisible = conference.agenda.isNotBlank() + conferenceDialogAgendaTitle.isVisible = conference.agenda.isNotBlank() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceFragment.kt index dd10a65e..b9642b1c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceFragment.kt @@ -8,6 +8,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.databinding.FragmentConferenceBinding import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.utils.getThemeAttrColor @@ -41,6 +42,8 @@ class ConferenceFragment : BaseFragment(R.layout.frag } override fun initView() { + conferencesAdapter.onItemClickListener = presenter::onItemSelected + with(binding.conferenceRecycler) { layoutManager = LinearLayoutManager(context) adapter = conferencesAdapter @@ -50,7 +53,11 @@ class ConferenceFragment : BaseFragment(R.layout.frag with(binding) { conferenceSwipe.setOnRefreshListener(presenter::onSwipeRefresh) conferenceSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) - conferenceSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh)) + conferenceSwipe.setProgressBackgroundColorSchemeColor( + requireContext().getThemeAttrColor( + R.attr.colorSwipeRefresh + ) + ) conferenceErrorRetry.setOnClickListener { presenter.onRetry() } conferenceErrorDetails.setOnClickListener { presenter.onDetailsClick() } } @@ -98,6 +105,10 @@ class ConferenceFragment : BaseFragment(R.layout.frag binding.conferenceRecycler.visibility = if (show) View.VISIBLE else View.GONE } + override fun openConferenceDialog(conference: Conference) { + (activity as? MainActivity)?.showDialogFragment(ConferenceDialog.newInstance(conference)) + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferencePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferencePresenter.kt index cc7e50db..dab170da 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferencePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferencePresenter.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.modules.conference import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.repositories.ConferenceRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository @@ -43,6 +44,10 @@ class ConferencePresenter @Inject constructor( loadData(true) } + fun onItemSelected(conference: Conference) { + view?.openConferenceDialog(conference) + } + fun onDetailsClick() { view?.showErrorDetailsDialog(lastError) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceView.kt index f3d1b3b3..4f73394d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceView.kt @@ -26,4 +26,6 @@ interface ConferenceView : BaseView { fun enableSwipe(enable: Boolean) fun showContent(show: Boolean) + + fun openConferenceDialog(conference: Conference) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt index ed4b0ac9..7dcd51ce 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt @@ -38,7 +38,7 @@ class SchoolAnnouncementDialog : DialogFragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ) = DialogSchoolAnnouncementBinding.inflate(inflater).apply { binding = this }.root + ) = DialogSchoolAnnouncementBinding.inflate(inflater).also { binding = it }.root override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/res/layout/dialog_conference.xml b/app/src/main/res/layout/dialog_conference.xml new file mode 100644 index 00000000..d08edf4f --- /dev/null +++ b/app/src/main/res/layout/dialog_conference.xml @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_school_announcement.xml b/app/src/main/res/layout/dialog_school_announcement.xml index cbce4e02..96c11d4a 100644 --- a/app/src/main/res/layout/dialog_school_announcement.xml +++ b/app/src/main/res/layout/dialog_school_announcement.xml @@ -118,6 +118,7 @@ android:layout_marginEnd="24dp" android:paddingStart="0dp" android:paddingEnd="16dp" + tools:maxLines="5" android:text="@string/all_no_data" android:textIsSelectable="true" android:textSize="16sp" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 295d8319..de85614b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -336,10 +336,12 @@ Today\'s lucky number is: %s Show history + Lucky number history No info about lucky numbers + Mobile devices No devices @@ -388,7 +390,8 @@ You have %1$d new conference You have %1$d new conferences - + Present at conference + Agenda School announcements @@ -585,6 +588,7 @@ Yes No Save + Title From da668f93cf309f4ae4c86f492b23849ca3c66dc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 16 Sep 2021 11:24:52 +0200 Subject: [PATCH 003/357] Fix bugs in dashboard (#1517) --- .../ui/modules/dashboard/DashboardAdapter.kt | 11 +++- .../ui/modules/dashboard/DashboardFragment.kt | 9 ++-- .../modules/dashboard/DashboardPresenter.kt | 50 +++++++++++-------- .../ui/modules/timetable/TimetableFragment.kt | 14 +++++- .../res/layout/subitem_dashboard_grades.xml | 4 +- 5 files changed, 56 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt index 0eda4932..11b575c1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt @@ -53,7 +53,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter Unit = {} - var onLessonsTileClickListener: () -> Unit = {} + var onLessonsTileClickListener: (LocalDate) -> Unit = {} var onHomeworkTileClickListener: () -> Unit = {} @@ -275,10 +275,12 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { + dateToNavigate = currentDate updateLessonView(item, currentTimetable, binding) binding.dashboardLessonsItemTitleTomorrow.isVisible = false } tomorrowTimetable.isNotEmpty() -> { + dateToNavigate = tomorrowDate updateLessonView(item, tomorrowTimetable, binding) binding.dashboardLessonsItemTitleTomorrow.isVisible = true } currentDayHeader != null && currentDayHeader.content.isNotBlank() -> { + dateToNavigate = currentDate updateLessonView(item, emptyList(), binding, currentDayHeader) binding.dashboardLessonsItemTitleTomorrow.isVisible = false } tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> { + dateToNavigate = tomorrowDate updateLessonView(item, emptyList(), binding, tomorrowDayHeader) binding.dashboardLessonsItemTitleTomorrow.isVisible = true } else -> { + dateToNavigate = tomorrowDate updateLessonView(item, emptyList(), binding) binding.dashboardLessonsItemTitleTomorrow.isVisible = !(item.isLoading && item.error == null) @@ -326,7 +333,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter(R.layout.fragme override fun initView() { val mainActivity = requireActivity() as MainActivity val itemTouchHelper = ItemTouchHelper( - DashboardItemMoveCallback( - dashboardAdapter, - presenter::onDragAndDropEnd - ) + DashboardItemMoveCallback(dashboardAdapter, presenter::onDragAndDropEnd) ) dashboardAdapter.apply { @@ -87,7 +84,9 @@ class DashboardFragment : BaseFragment(R.layout.fragme onAttendanceTileClickListener = { mainActivity.pushView(AttendanceSummaryFragment.newInstance()) } - onLessonsTileClickListener = { mainActivity.pushView(TimetableFragment.newInstance()) } + onLessonsTileClickListener = { + mainActivity.pushView(TimetableFragment.newInstance(it)) + } onGradeTileClickListener = { mainActivity.pushView(GradeFragment.newInstance()) } onHomeworkTileClickListener = { mainActivity.pushView(HomeworkFragment.newInstance()) } onAnnouncementsTileClickListener = { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index 027bcc05..108a086b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -492,31 +492,37 @@ class DashboardPresenter @Inject constructor( end = LocalDate.now().plusDays(7), forceRefresh = forceRefresh ) - }.onEach { - when (it.status) { - Status.LOADING -> { - Timber.i("Loading dashboard exams data started") - if (forceRefresh) return@onEach - updateData( - DashboardItem.Exams(it.data.orEmpty(), isLoading = true), - forceRefresh - ) + } + .map { examResource -> + val sortedExams = examResource.data?.sortedBy { it.date } - if (!it.data.isNullOrEmpty()) { - firstLoadedItemList += DashboardItem.Type.EXAMS + examResource.copy(data = sortedExams) + } + .onEach { + when (it.status) { + Status.LOADING -> { + Timber.i("Loading dashboard exams data started") + if (forceRefresh) return@onEach + updateData( + DashboardItem.Exams(it.data.orEmpty(), isLoading = true), + forceRefresh + ) + + if (!it.data.isNullOrEmpty()) { + firstLoadedItemList += DashboardItem.Type.EXAMS + } + } + Status.SUCCESS -> { + Timber.i("Loading dashboard exams result: Success") + updateData(DashboardItem.Exams(it.data ?: emptyList()), forceRefresh) + } + Status.ERROR -> { + Timber.i("Loading dashboard exams result: An exception occurred") + errorHandler.dispatch(it.error!!) + updateData(DashboardItem.Exams(error = it.error), forceRefresh) } } - Status.SUCCESS -> { - Timber.i("Loading dashboard exams result: Success") - updateData(DashboardItem.Exams(it.data ?: emptyList()), forceRefresh) - } - Status.ERROR -> { - Timber.i("Loading dashboard exams result: An exception occurred") - errorHandler.dispatch(it.error!!) - updateData(DashboardItem.Exams(error = it.error), forceRefresh) - } - } - }.launch("dashboard_exams") + }.launch("dashboard_exams") } private fun loadConferences(student: Student, forceRefresh: Boolean) { 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 83218a0d..4478a2a6 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 @@ -44,7 +44,13 @@ class TimetableFragment : BaseFragment(R.layout.fragme companion object { private const val SAVED_DATE_KEY = "CURRENT_DATE" - fun newInstance() = TimetableFragment() + private const val ARGUMENT_DATE_KEY = "ARGUMENT_DATE" + + fun newInstance(date: LocalDate? = null) = TimetableFragment().apply { + arguments = Bundle().apply { + date?.let { putLong(ARGUMENT_DATE_KEY, it.toEpochDay()) } + } + } } override val titleStringId get() = R.string.timetable_title @@ -62,7 +68,11 @@ class TimetableFragment : BaseFragment(R.layout.fragme super.onViewCreated(view, savedInstanceState) binding = FragmentTimetableBinding.bind(view) messageContainer = binding.timetableRecycler - presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY)) + + val initDate = savedInstanceState?.getLong(SAVED_DATE_KEY) + ?: arguments?.getLong(ARGUMENT_DATE_KEY)?.takeUnless { it == 0L } + + presenter.onAttachView(this, initDate) } override fun initView() { diff --git a/app/src/main/res/layout/subitem_dashboard_grades.xml b/app/src/main/res/layout/subitem_dashboard_grades.xml index 61faa6ac..9354be3d 100644 --- a/app/src/main/res/layout/subitem_dashboard_grades.xml +++ b/app/src/main/res/layout/subitem_dashboard_grades.xml @@ -11,12 +11,13 @@ android:layout_height="wrap_content" android:ellipsize="end" android:includeFontPadding="false" + android:maxEms="15" android:maxLines="1" android:textSize="13sp" app:layout_constraintBottom_toBottomOf="@id/dashboard_grades_subitem_grade_container" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/dashboard_grades_subitem_grade_container" - tools:text="Urządzenia techniki kompu..." /> + tools:text="Urządzenia techniki komputerowych" /> From c568bc1515c3937e9b5bc5b3da959a27bdc28757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 16 Sep 2021 11:29:11 +0200 Subject: [PATCH 004/357] Fix ghost account after logout not current student (#1518) --- .../accountdetails/AccountDetailsFragment.kt | 6 +++++- .../accountdetails/AccountDetailsPresenter.kt | 15 +++++++++++---- .../account/accountdetails/AccountDetailsView.kt | 4 +++- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt index a9890ad9..c3137ec5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt @@ -121,10 +121,14 @@ class AccountDetailsFragment : } } - override fun popView() { + override fun popViewToMain() { (requireActivity() as MainActivity).popView(2) } + override fun popViewToAccounts() { + (requireActivity() as MainActivity).popView(1) + } + override fun recreateMainView() { requireActivity().recreate() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt index d4cba580..1f44cbbc 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt @@ -119,7 +119,7 @@ class AccountDetailsPresenter @Inject constructor( } } }.afterLoading { - view?.popView() + view?.popViewToMain() }.launch("switch") } @@ -152,11 +152,14 @@ class AccountDetailsPresenter @Inject constructor( syncManager.stopSyncWorker() openClearLoginView() } - studentWithSemesters!!.student.isCurrent -> { + studentWithSemesters?.student?.isCurrent == true -> { Timber.i("Logout result: Logout student and switch to another") recreateMainView() } - else -> Timber.i("Logout result: Logout student") + else -> { + Timber.i("Logout result: Logout student") + recreateMainView() + } } } Status.ERROR -> { @@ -165,7 +168,11 @@ class AccountDetailsPresenter @Inject constructor( } } }.afterLoading { - view?.popView() + if (studentWithSemesters?.student?.isCurrent == true) { + view?.popViewToMain() + } else { + view?.popViewToAccounts() + } }.launch("logout") } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsView.kt index 652f0c1a..aeb743fa 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsView.kt @@ -15,7 +15,9 @@ interface AccountDetailsView : BaseView { fun showLogoutConfirmDialog() - fun popView() + fun popViewToMain() + + fun popViewToAccounts() fun recreateMainView() From 258782c6481e13935ca6724bbd17b820f5dead80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 16 Sep 2021 11:30:05 +0200 Subject: [PATCH 005/357] New Crowdin updates (#1482) --- app/src/main/res/values-cs/strings.xml | 43 ++++++++++++++------------ app/src/main/res/values-de/strings.xml | 3 ++ app/src/main/res/values-pl/strings.xml | 37 ++++++++++++---------- app/src/main/res/values-ru/strings.xml | 3 ++ app/src/main/res/values-sk/strings.xml | 43 ++++++++++++++------------ app/src/main/res/values-uk/strings.xml | 3 ++ 6 files changed, 75 insertions(+), 57 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index fedbc903..33ac3616 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -402,6 +402,8 @@ Máte %1$d nových setkání Máte %1$d nových setkání + Present at conference + Agenda Školní oznámení Žádná školní oznámení @@ -516,10 +518,10 @@ Další: Později: - ještě %1$d další lekce - ještě %1$d další lekce - ještě %1$d dalších lekcí - ještě %1$d dalších lekcí + ještě %1$d lekce + ještě %1$d lekce + ještě %1$d lekcí + ještě %1$d lekcí do %1$s Žádné nadcházející lekce @@ -528,10 +530,10 @@ Žádné domácí úkoly do vykonána Při načítání domácích úkolů došlo k chybě - Ještě %1$d další domácí úkol - Ještě %1$d další domácí úkoly - Ještě %1$d dalších domácích úkolů - Ještě %1$d dalších domácích úkolů + Ještě %1$d domácí úkol + Ještě %1$d domácí úkoly + Ještě %1$d domácích úkolů + Ještě %1$d domácích úkolů do %1$s Poslední známky @@ -541,28 +543,28 @@ Žádná aktuální oznámení Při načítání oznámení došlo k chybě - Ještě %1$d další oznámení - Ještě %1$d další oznámení - Ještě %1$d dalších oznámení - Ještě %1$d dalších oznámení + Ještě %1$d oznámení + Ještě %1$d oznámení + Ještě %1$d oznámení + Ještě %1$d oznámení Zkoušky Žádné nadcházející zkoušky Při načítání zkoušek došlo k chybě - Ještě %1$d další zkouška - Ještě %1$d další zkoušky - Ještě %1$d dalších zkoušek - Ještě %1$d dalších zkoušek + Ještě %1$d zkouška + Ještě %1$d zkoušky + Ještě %1$d zkoušek + Ještě %1$d zkoušek Setkání Žádná nadcházející setkání Při načítání setkání došlo k chybě - Ještě %1$d další setkání - Ještě %1$d další setkání - Ještě %1$d dalších setkání - Ještě %1$d dalších setkání + Ještě %1$d setkání + Ještě %1$d setkání + Ještě %1$d setkání + Ještě %1$d setkání Při načítání dat došlo k chybě Žádné @@ -590,6 +592,7 @@ Ano Ne Uložit + Title Žádné lekce Vybrat motiv diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 5f14a425..f86c3076 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -344,6 +344,8 @@ Sie haben %1$d neue konferenz Sie haben %1$d neue konferenzen + Present at conference + Agenda Schulankündigungen Keine schulankündigungen @@ -512,6 +514,7 @@ Ja Nein Speichern + Title Keine Lektionen Thema wählen diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 14f23cd2..cfc6810e 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -402,6 +402,8 @@ Masz %1$d nowych zebrań Masz %1$d nowych zebrań + Obecność na zebraniu + Agenda Ogłoszenia szkolne Brak ogłoszeń szkolnych @@ -516,10 +518,10 @@ Następnie: Później: - jeszcze %1$d dodatkowa lekcja - jeszcze %1$d dodatkowe lekcje - jeszcze %1$d dodatkowych lekcji - jeszcze %1$d dodatkowych lekcji + jeszcze %1$d lekcja + jeszcze %1$d lekcje + jeszcze %1$d lekcji + jeszcze %1$d lekcji do %1$s Brak nadchodzących lekcji @@ -528,10 +530,10 @@ Brak prac domowych do wykonania Wystąpił błąd podczas ładowania zadań domowych - Jeszcze %1$d dodatkowe zadanie domowe - Jeszcze %1$d dodatkowe zadania domowe - Jeszcze %1$d dodatkowych zadań domowych - Jeszcze %1$d dodatkowych zadań domowych + Jeszcze %1$d zadanie domowe + Jeszcze %1$d zadania domowe + Jeszcze %1$d zadań domowych + Jeszcze %1$d zadań domowych do %1$s Ostatnie oceny @@ -541,19 +543,19 @@ Brak aktualnych ogłoszeń Wystąpił błąd podczas ładowania ogłoszeń - Jeszcze %1$d dodatkowe ogłoszenie - Jeszcze %1$d dodatkowe ogłoszenia - Jeszcze %1$d dodatkowych ogłoszeń - Jeszcze %1$d dodatkowych ogłoszeń + Jeszcze %1$d ogłoszenie + Jeszcze %1$d ogłoszenia + Jeszcze %1$d ogłoszeń + Jeszcze %1$d ogłoszeń Sprawdziany Brak nadchodzących sprawdzianów Wystąpił błąd podczas ładowania sprawdzianów - Jeszcze %1$d dodatkowy sprawdzian - Jeszcze %1$d dodatkowe sprawdziany - Jeszcze %1$d dodatkowych sprawdzianów - Jeszcze %1$d dodatkowych sprawdzianów + Jeszcze %1$d sprawdzian + Jeszcze %1$d sprawdziany + Jeszcze %1$d sprawdzianów + Jeszcze %1$d sprawdzianów Zebrania Brak nadchodzących zebrań @@ -562,7 +564,7 @@ Jeszcze %1$d dodatkowe zebranie Jeszcze %1$d dodatkowe zebrania Jeszcze %1$d dodatkowych zebrań - Jeszcze %1$d dodatkowych zebrań + Jeszcze %1$d zebrań Wystąpił błąd podczas ładowania danych Brak @@ -590,6 +592,7 @@ Tak Nie Zapisz + Tytuł Brak lekcji Wybierz motyw diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 09b8a146..4e41088f 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -402,6 +402,8 @@ У вас %1$d новая конференция У вас %1$d новых конференций + Present at conference + Agenda Объявления школ Нет объявлений о школе @@ -590,6 +592,7 @@ Да Нет Сохранить + Title Нет уроков Выбрать тему diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index ad078e33..75a42467 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -402,6 +402,8 @@ Máte %1$d nových stretnutí Máte %1$d nových stretnutí + Present at conference + Agenda Školské oznámenia Žiadne školské oznámenia @@ -516,10 +518,10 @@ Ďalej: Neskôr: - ešte %1$d ďalší lekcia - ešte %1$d ďalšie lekcie - ešte %1$d ďalších lekcií - ešte %1$d ďalších lekcií + ešte %1$d lekcia + ešte %1$d lekcie + ešte %1$d lekcií + ešte %1$d lekcií do %1$s Žiadne nadchádzajúce lekcie @@ -528,10 +530,10 @@ Žiadne domáce úlohy do vykonaná Pri načítaní domácich úloh došlo k chybe - Ešte %1$d ďalšia domáca úloha - Ešte %1$d ďalšie domáce úlohy - Ešte %1$d ďalších domácich úloh - Ešte %1$d ďalších domácich úloh + Ešte %1$d domáca úloha + Ešte %1$d domáce úlohy + Ešte %1$d domácich úloh + Ešte %1$d domácich úloh do %1$s Posledné známky @@ -541,28 +543,28 @@ Žiadne aktuálne oznámenia Pri načítaní oznámení došlo k chybe - Ešte %1$d ďalšie oznámenie - Ešte %1$d ďalšie oznámenia - Ešte %1$d ďalších oznámení - Ešte %1$d ďalších oznámení + Ešte %1$d oznámenie + Ešte %1$d oznámenia + Ešte %1$d oznámení + Ešte %1$d oznámení Skúšky Žiadne nadchádzajúce skúšky Pri načítaní skúšok došlo k chybe - Ešte %1$d ďalšia skúška - Ešte %1$d ďalšie skúšky - Ešte %1$d ďalších skúšok - Ešte %1$d ďalších skúšok + Ešte %1$d skúška + Ešte %1$d skúšky + Ešte %1$d skúšok + Ešte %1$d skúšok Stretnutie Žiadna nadchádzajúce stretnutie Pri načítaní stretnutí došlo k chybe - Ešte %1$d ďalšie stretnutie - Ešte %1$d ďalšie stretnutia - Ešte %1$d ďalších stretnutí - Ešte %1$d ďalších stretnutí + Ešte %1$d stretnutie + Ešte %1$d stretnutia + Ešte %1$d stretnutí + Ešte %1$d stretnutí Pri načítaní dát došlo k chybe Žiadne @@ -590,6 +592,7 @@ Áno Nie Uložiť + Title Žiadne lekcie Vybrať motív diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 41f1c300..51f25881 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -402,6 +402,8 @@ У вас є %1$d нова конференція У вас є %1$d нових конференцій + Present at conference + Agenda Оголошення школи Жодних навчальних оголошень @@ -590,6 +592,7 @@ Так Ні Зберегти + Title Брак уроків Увібрати тему From 5ba8289c8707840ec45b1ba6d182d5052513c027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Thu, 16 Sep 2021 11:59:23 +0200 Subject: [PATCH 006/357] Display info in timetable as-is when lesson has change flag (#1521) --- app/build.gradle | 2 +- .../wulkanowy/ui/modules/timetable/TimetableDialog.kt | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 8e8be8bb..7436ffcd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -157,7 +157,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:1.2.2" + implementation "io.github.wulkanowy:sdk:d74acd3989" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt index 3f8622f8..bead8fb2 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt @@ -13,7 +13,6 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.databinding.DialogTimetableBinding import io.github.wulkanowy.utils.capitalise -import io.github.wulkanowy.utils.decapitalise import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.toFormattedString @@ -52,7 +51,7 @@ class TimetableDialog : DialogFragment() { super.onViewCreated(view, savedInstanceState) with(lesson) { - setInfo(info, teacher, canceled, changes) + setInfo(info, canceled, changes) setSubject(subject, subjectOld) setTeacher(teacher, teacherOld) setGroup(group) @@ -80,7 +79,7 @@ class TimetableDialog : DialogFragment() { } @SuppressLint("DefaultLocale") - private fun setInfo(info: String, teacher: String, canceled: Boolean, changes: Boolean) { + private fun setInfo(info: String, canceled: Boolean, changes: Boolean) { with(binding) { when { info.isNotBlank() -> { @@ -102,8 +101,6 @@ class TimetableDialog : DialogFragment() { timetableDialogChangesValue.text = when { canceled && !changes -> "Lekcja odwołana: $info" - changes && teacher.isNotBlank() -> "Zastępstwo: $teacher" - changes && teacher.isBlank() -> "Zastępstwo, ${info.decapitalise()}" else -> info.capitalise() } } From 9cb4754132e5b787b29d47eabe6c8c7990b3e8a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Thu, 16 Sep 2021 12:01:49 +0200 Subject: [PATCH 007/357] Version 1.2.3 --- app/build.gradle | 6 +++--- app/src/main/play/release-notes/pl-PL/default.txt | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7436ffcd..814fce3e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,8 +21,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 30 - versionCode 95 - versionName "1.2.2" + versionCode 96 + versionName "1.2.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -157,7 +157,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:d74acd3989" + implementation "io.github.wulkanowy:sdk:1.2.3" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 3456c3d0..7de10a26 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,8 +1,8 @@ -Wersja 1.2.2 +Wersja 1.2.3 -- naprawiliśmy problem z widocznością zadań w aplikacji gdy widoczne są one na stronie www dziennika (nadal pozostaje błąd z zadaniami widocznymi tylko w oficjalnej aplikacji - czekamy na poprawkę po stronie VULCANa) -- odblokowaliśmy niedzielę w wyborze daty w planie lekcji i innych zakładkach -- przywróciliśmy odnośnik do szczęśliwego numerka w menu Więcej -- naprawiliśmy drobne błędy ze stabilnością i wyglądem +- naprawiliśmy pomieszane imiona nauczycieli z salami w planie lekcji +- dodaliśmy brakujące okienka ze szczegółami na ekranie zebrań +- klikając w kafelek z lekcjami na jutro aplikacja teraz przekierowuje na ekran z planem na jutro +- naprawiliśmy błąd przy wylogowywaniu innego niż bieżący uczeń Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases From 6e5481f3459981d20ac989ca214cad6123528de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 20 Sep 2021 11:38:13 +0200 Subject: [PATCH 008/357] Upgrade Gradle Play Publisher to 3.6.0 (#1526) --- .github/workflows/deploy-store.yml | 10 ++++------ app/build.gradle | 7 +++++-- app/key.p12.gpg | Bin 5129 -> 0 bytes build.gradle | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) delete mode 100644 app/key.p12.gpg diff --git a/.github/workflows/deploy-store.yml b/.github/workflows/deploy-store.yml index 8015ef64..068b9c10 100644 --- a/.github/workflows/deploy-store.yml +++ b/.github/workflows/deploy-store.yml @@ -28,15 +28,14 @@ jobs: SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }} run: | gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/google-services.json.gpg - gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg - name: Upload apk to google play env: + PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }} PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }} PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }} - PLAY_SERVICE_ACCOUNT_EMAIL: ${{ secrets.PLAY_SERVICE_ACCOUNT_EMAIL }} - PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }} - run: ./gradlew publishPlayRelease -PenableFirebase --stacktrace; + ANDROID_PUBLISHER_CREDENTIALS: ${{ secrets.ANDROID_PUBLISHER_CREDENTIALS }} + run: ./gradlew publishPlayReleaseApps -PenableFirebase --stacktrace; deploy-app-gallery: name: Deploy to AppGallery @@ -60,7 +59,6 @@ jobs: SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }} run: | gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/agconnect-services.json.gpg - gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg - name: Prepare credentials env: @@ -68,7 +66,7 @@ jobs: run: echo $AGC_CREDENTIALS > ./app/src/release/agconnect-credentials.json - name: Build and publish HMS version env: + PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }} PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }} PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }} - PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }} run: ./gradlew bundleHmsRelease --stacktrace && ./gradlew publishHuaweiAppGalleryHmsRelease --stacktrace diff --git a/app/build.gradle b/app/build.gradle index 814fce3e..466924b0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -96,6 +96,10 @@ android { } } + playConfigs { + play { enabled.set(true) } + } + buildFeatures { viewBinding true } @@ -130,11 +134,10 @@ kapt { } play { - serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf" - serviceAccountCredentials = file('key.p12') defaultToAppBundles = false track = 'production' updatePriority = 3 + enabled.set(false) } huaweiPublish { diff --git a/app/key.p12.gpg b/app/key.p12.gpg deleted file mode 100644 index e9b6d06ebe2c5f9e8f062549a09b948be52be482..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5129 zcmeC-W#MFITEnt&@~l4PN&hdsVRSLL5xZsG&kIirLW3?%RSMDfJh=Vim4w%BsTwYe zRycjwxR7h3t5}cjqdyq(syKH@*w*0lLu>Yac8 zb$NfD5ZPK@ay~smie78 zAIj6Vnm_m?daE*0ivP2#%KqaflA>>`BX0{===_>!!QaKU^Y7!00t#Mhud`iPviZ33 z!B37_g_C^i_%^kcbqc@UxU=$l_nniG>u={Au3_p4j}P&^dstqGMJ3xTL*f0|P8*Xo zh3=jgJNcX1f_Wy+RCZ)dHe3H8`h7&Hit(YF0xKrkCSJ0$c&WWLu#@4puAxYtklpw7 zQ%)RV5MI6ZQ|G;HxATdj-4r|eokw||FE&pfx|Hl8OcUAj7>J(6QgwnpTLux=H< zmvidw?^B=UizOb_&6}Rp6?*pH_xWx=9X4*{6ngOCi(cq=w#D10dDJx4Pi4ti>#$v6 zHj^cf!qpD<;DbTC_Ldx#|K}j-5I7^m`|r{dMYT)Ak{bLc^G>XPeSAi`|A|}AZicLX zBh1;fP*$+|PJ6^0sV83V3VEN)hznUIRIKCUT(qcmQP8#0sS|iQuBfkT*e0D`^LNFT zvRll9#mcA+GcEzdmi;{Qlonhw_`2D-J?9+GVo)echRplp4wdC|} zEZS>iwNvugwvMz2xc~f9dD58~$>i6rIAljOFgRwC9Gb7nbgr zBmTX;`eNMD%%~UP=J);zR+zizes|E)=_>yI_Rbx-@9k=T-%S-eSi?8%>SVsU930J%nBFHYnO`t{y3HW;`OzXDOvS~hnp2o7uNpR`Juk!zSmE~3kh4ooA-xFo}b@8 zmzn=UK+2{q0o&iJeTjXOvxqj$5g`**QJI!}*S0 z(lwd%o^rh!w-a~Y^0M|@c}_hx-^=~uf^#|62mDvpw90d=vsfSEqq11@Z?(}9>6u42 zyt2wX+#?^N_$1=ZB9rxT-)c8`nZG#@{zPTQw@0>|rjZig^0z6p1^)ke@yk5_HEX8K zwA_98^POo-ftekV-wooW7BV(_t}=S5*c87lb?#HK)BZAdOV~Gc8Q-j5_1fpy)~iCX zSv)pLcS6OK!Y-PxxpqWaDvs+p(MTzx=7ZYkX(4XT5q7 zULi8+fo{%<!n$v1-kGUp# z9^Gvznf(6Bf8j$<6;$t39^Cq;cR}>$Qx`S7`xtq*turin-?Fp$3(NcuON3TOe{3lU zyYj34W7wH6kxK^J4L&FAV`Bbao43?0!PwREix``%@r~8Cb5FlosUG7|d#4=Hu#uM9ci%j}?Y#eX^)G@g z3zMSP&)?g8SM*H5(t_q`NB%ZR2)E6@VI0a(o$mbi%lvnTm+<+s_#4)J{5aFI`D@O% zHR}Y#4*ig_4H0`#ul%U3$ytJBh47(SXYFQG_j4WyEPhe;zOH-4)9I&pmkC7PHVF7J z&AV{=IriL>`Q{O_&%7TUYq8mAcv3t_&3V$9^>6gguGQwPIcR+_g=OT~0 zdDC}YN$!&0JcDDk{*thqWpkJ{4R+}4Vb~}6*1F%R=5&C!44cLJ;(f>G&Eu4~llN5V z@x50ih1aey7?y6T*Uj*L+r3HH`Qx0*t0$+e_x9ebDOHb{Tb)ND2T@LS+mAsPX3Rh?9`iYlqs+pXx zI9G4ym(+>DC-?upnYQhH{EC&kCZ9MPd*h&HS>`|X%d$)6r2PFg<%EQ@b71n4E0Qyk z4!5qJTqhJQUG}~_XWOl9`<6IqxIf#m$JzD8F6ZSh<_87+=)4w^mhfTsT;DIvEAH8E zF5PnEo>-@D`eBtd%PN-(?c1HV^w^b99WH&@3s3(mdI(FD9^h1#+O+@kvxWPD?=GqF zFX-64p*h=&Ti@WVI8XM}Kuz6mvC5)XrzCD=tnQb4&ssT!Yl^(Nt+v>?+w11|m1&zf zifgO$Jh$HvYTa`0uY(2j$H+b^D z&pp`snDvtKNA-(-bwUqZYqK9MS?;VI(_F=W_wdV!$I9LBT)omiBXq+p`^Zb7oV#A^ zsVZ^pw73xy&8;B1!G%{vV&2L!LFuk*3ybY%e~DiZUHgr<(J{ltNm1b2fpc59Y`(MJ zGno-{~wRDlfO?nyrJg6E`4p54eRD692YIGsjJ$d-}3yyjTiR(E7oP^A6eyH z$RH=8R{2*?`eHT9v#>+kwG~Cgl_>XS2_G6~6D> zEG^Crp?edQrKl1%KdyeC^zEvu_QvLaV@m<{Q5ckt6 zE;qMLVZxlHjN4hB9Ge`WpUwHY^#W5%%o5r86JJDBzp@tX%a6J(6O+vv665gPWp%QT zyvIy`d#me-?JQ6Kom-Zmc2+z-PGA*pY(#|evBx~}-FarM{BF(rw&eW&*7?Zt_Oy~p zvA9LwGg#T*%)R;2gFSa!$@WhQ#Y~Z~wHPd$Ms$3k&aQL(Q74 zspgOSlOIUwCv7oqU&CORxuuI|tJsfiW&*yIKf9KQoV31Ha#Lg55`|k_dDrjudWkVy zs9d4>j$2>(7YH zJb3OkvqL_A^@&^_ zw-vNxCpjgpNjtgI@=%iFj1LLsHM!=+7m^E)yZyXi^St-DrpXK4^xa3j zEZN0;*e37v*!%UIRaCfNKktV5Ut``eO#OTG*Yqe>VT15E-<26IpSc=dFT4=7eG2m- zyW4Z#{=FmYvusxLtJ7DwckGhr5I7}nbVXs+&UP7tQ;$4K(!1C!_oRF@&YUr2p5E=e zjf!IbojfJ-6Zu3I?<#lx>U_HMwU?9RM$7%u%!MgiyH>izZb-P>ed*$qPg{?y-(ElI zMBAa+4_udh)2rXO+WN}5jq_pPY%rP9VS z{vv)CMf&EsPu7*RS9&_{%KYSS>wQ$e>)g0m|JdB;js3^WvR&b)j9osjvs@Z+F5Gm* ztYE&ZlNZjLybZa~n|}V+wd&yQ(9Vmp@aW`|zuD(%#p5ug+XEW4RPtke-ZT zY=K4prbVB)1RuNp|9w#H@r2y7%rXInt1Sa=t}0&AxO;+NXWR32Cv1OrocMD8{mjw> zJ!`{P7k~H8>vdpXFx!pkw8p2A59hejB}>>g#WIx_vi7wUH~*lj%~R>%X_( zs=C9=t2B#*13Z@VEL1e+=TAPSbbIv|4ed__(pwj5{4y~Jage^UTx;PmXJO5^i}$aJ zvf|z&8hBUoX?mb?Q2&X2M)G_Ldjh__-QpX(c}b?stLLY_C$lxOPUD=sJU@T-rz`jG zNG5EHXk9qRMm3&)&W40vS6^xGvDsi4w&&!NFvkB?AB%Nb!}$&#_71xFqUH3i*(b7s zzV)QVoZc<6ZStAMQ_Q@)9Q$VWI6w9a6W_T0#75g{-k^*%6GiSM$$krZcUPsVJzqrX zpWn&jD`p9Yd}y4ZqqkzW`^2DW-8tLrQm$r}@F~l6OMSYe=$-aP?%NcLyd5`T zS$%Sq%&fE1T?}&`eR!UB**3_mB)V^5?UcD!Uo1}1n4S+?v%J7wlO7G_}4>^<8N4RMjSl--)ef5uIO3)PoL+ySWj&F zKBI7jl&YgLSL45rrkhx^GFIIBB4Kf+VMdxll|0AP%S!gkW-sk9u&-owUzd>J*;2Cg zM95a(^K&Xxo%0N4vgawRF7H0SHRJTRFE5$mwcS+xudXq2*U`C9W#8a3VNR{PLPU1G z+Ous^dw$s|{yge-dsCUo!;G0pRc|7luTMRHEoHx|ctVi%R_pqe`POXZnLDN4&6V7u z9=W41jVV;lW(wbyDaV~ozskzryxg(sRVC*O!@c&CD%*TG(&Hssg%(-<%y+P<*;^EP zp35X}9Use^dFnSf-R51(nDII$*0^R;p!-Ip-EQZ${nHVi*;hT)e^tVQZo>m}?Ejou z{jucQ(NqtOn926C6E@oXS=w%*eQnl00pUl!66#EEnjM*5<>@SQtT5a^<+A?2sa7mS zZizfg4s`}EpQ0bBcfaVsmjzqC&OOB5)TdG%ocn86fL!PKXk#87rw=;~w?-e3Pu@5G zn{ED^p!GRckCt(juMTXtVzSh0p1IB;;mIFP!Fvxv&fb-l>-ra$_VCWE*@^oc!?!z4 zexM`uBcmm29=qq3?G~3FKc2k2W4}y$@X|w34}EJ>5BD8r{h+hd()|vnqJbu5~ zU~9^B-2d5cqB~p7*BO<+#F-aA`?cL5Qd}p;d>h-0*1HetpJ~V3Og-cu zrm{VGz2+B_O@&WFl+OBREELMxaj&swz0%ThW^JJx_R|{kInO%ge{o}sN_^6Vzk%3$1XZq-|^| z&!0V6VKwoF`jq6w?y?VmUw7m#y485J@P~~`$v!{fPurthc-|k}ZenRPSI8k|Q{9ZJ zpRbp1IwyJ7<)g@t&5<<|@=H1Q{+?{*y>Gou@fz>``Ztx?B@dn*T`3N$OSr diff --git a/build.gradle b/build.gradle index 7fb02eb9..e425974d 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { classpath 'com.google.gms:google-services:4.3.10' classpath 'com.huawei.agconnect:agcp:1.6.0.300' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' - classpath "com.github.triplet.gradle:play-publisher:2.8.0" + classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.3.0" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" From 36daa7ccc1c25878fdcaf5f8475ecd6e6240745f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Wed, 22 Sep 2021 09:25:16 +0200 Subject: [PATCH 009/357] Always include all language resources in app bundle (#1527) --- app/build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index 466924b0..bcb84c19 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -104,6 +104,12 @@ android { viewBinding true } + bundle { + language { + enableSplit = false + } + } + testOptions.unitTests { includeAndroidResources = true } From 6615e684303bc0938ed8e706ccd400cdc6877855 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Sep 2021 09:25:54 +0200 Subject: [PATCH 010/357] Bump kotlin_version from 1.5.30 to 1.5.31 (#1528) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e425974d..15d3c792 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlin_version = '1.5.30' + kotlin_version = '1.5.31' about_libraries = '8.9.1' hilt_version = "2.38.1" } From a43ffcdef482a295623dee33fb67e1c7e177aea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Fri, 24 Sep 2021 21:02:51 +0200 Subject: [PATCH 011/357] Display bad credentials error in the message box above login form (#1525) --- app/build.gradle | 2 +- .../ui/modules/login/LoginActivity.kt | 5 +- .../ui/modules/login/LoginErrorHandler.kt | 4 +- .../login/advanced/LoginAdvancedFragment.kt | 67 +++++++++++++------ .../login/advanced/LoginAdvancedPresenter.kt | 4 +- .../login/advanced/LoginAdvancedView.kt | 2 +- .../modules/login/form/LoginFormFragment.kt | 48 ++++++++++--- .../modules/login/form/LoginFormPresenter.kt | 2 +- .../ui/modules/login/form/LoginFormView.kt | 2 +- .../main/res/layout/fragment_login_form.xml | 28 +++++--- app/src/main/res/values-cs/strings.xml | 3 +- app/src/main/res/values-de/strings.xml | 3 +- app/src/main/res/values-pl/strings.xml | 3 +- app/src/main/res/values-ru/strings.xml | 3 +- app/src/main/res/values-sk/strings.xml | 3 +- app/src/main/res/values-uk/strings.xml | 3 +- app/src/main/res/values/strings.xml | 3 +- 17 files changed, 126 insertions(+), 59 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bcb84c19..cea9ae81 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -166,7 +166,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:1.2.3" + implementation "io.github.wulkanowy:sdk:8f3721f1f9" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt index 8d96a498..10f6c073 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt @@ -103,9 +103,8 @@ class LoginActivity : BaseActivity(), Logi } override fun notifyInitSymbolFragment(loginData: Triple) { - (loginAdapter.getFragmentInstance(1) as? LoginSymbolFragment)?.onParentInitSymbolFragment( - loginData - ) + (loginAdapter.getFragmentInstance(1) as? LoginSymbolFragment) + ?.onParentInitSymbolFragment(loginData) } override fun notifyInitStudentSelectFragment(studentsWithSemesters: List) { 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 ed456324..2f76cd51 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 @@ -13,7 +13,7 @@ import javax.inject.Inject class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) { - var onBadCredentials: () -> Unit = {} + var onBadCredentials: (String?) -> Unit = {} var onInvalidToken: (String) -> Unit = {} @@ -25,7 +25,7 @@ class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler override fun proceed(error: Throwable) { when (error) { - is BadCredentialsException -> onBadCredentials() + is BadCredentialsException -> onBadCredentials(error.message) is SQLiteConstraintException -> onStudentDuplicate(resources.getString(R.string.login_duplicate_student)) is TokenDeadException -> onInvalidToken(resources.getString(R.string.login_expired_token)) is InvalidTokenException -> onInvalidToken(resources.getString(R.string.login_invalid_token)) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt index 9231914c..0672d75f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt @@ -51,10 +51,12 @@ class LoginAdvancedFragment : private lateinit var hostSymbols: Array override val formHostValue: String - get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty() + get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) + .orEmpty() override val formHostSymbol: String - get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty() + get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) + .orEmpty() override val formPinValue: String get() = binding.loginFormPin.text.toString().trim() @@ -92,39 +94,62 @@ class LoginAdvancedFragment : loginFormSignIn.setOnClickListener { presenter.onSignInClick() } loginTypeSwitch.setOnCheckedChangeListener { _, checkedId -> - presenter.onLoginModeSelected(when (checkedId) { - R.id.loginTypeApi -> Sdk.Mode.API - R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER - else -> Sdk.Mode.HYBRID - }) + presenter.onLoginModeSelected( + when (checkedId) { + R.id.loginTypeApi -> Sdk.Mode.API + R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER + else -> Sdk.Mode.HYBRID + } + ) } loginFormPin.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() } loginFormPass.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() } - loginFormSymbol.setAdapter(ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, resources.getStringArray(R.array.symbols_values))) + loginFormSymbol.setAdapter( + ArrayAdapter( + requireContext(), + android.R.layout.simple_list_item_1, + resources.getStringArray(R.array.symbols_values) + ) + ) } with(binding.loginFormHost) { setText(hostKeys.getOrNull(0).orEmpty()) - setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys)) + setAdapter( + LoginSymbolAdapter( + context, + R.layout.support_simple_spinner_dropdown_item, + hostKeys + ) + ) setOnClickListener { if (binding.loginFormContainer.visibility == GONE) dismissDropDown() } } } override fun showMobileApiWarningMessage() { - binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_mobile_api) + binding.loginFormAdvancedWarningInfo.text = + getString(R.string.login_advanced_warning_mobile_api) } override fun showScraperWarningMessage() { - binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_scraper) + binding.loginFormAdvancedWarningInfo.text = + getString(R.string.login_advanced_warning_scraper) } override fun showHybridWarningMessage() { - binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_hybrid) + binding.loginFormAdvancedWarningInfo.text = + getString(R.string.login_advanced_warning_hybrid) } - override fun setDefaultCredentials(username: String, pass: String, symbol: String, token: String, pin: String) { + override fun setDefaultCredentials( + username: String, + pass: String, + symbol: String, + token: String, + pin: String + ) { with(binding) { loginFormUsername.setText(username) loginFormPass.setText(pass) @@ -177,10 +202,10 @@ class LoginAdvancedFragment : } } - override fun setErrorPassIncorrect() { + override fun setErrorPassIncorrect(message: String?) { with(binding.loginFormPassLayout) { requestFocus() - error = getString(R.string.login_incorrect_password) + error = message ?: getString(R.string.login_incorrect_password) } } @@ -296,11 +321,13 @@ class LoginAdvancedFragment : } override fun notifyParentAccountLogged(studentsWithSemesters: List) { - (activity as? LoginActivity)?.onFormFragmentAccountLogged(studentsWithSemesters, Triple( - binding.loginFormUsername.text.toString(), - binding.loginFormPass.text.toString(), - resources.getStringArray(R.array.hosts_values)[1] - )) + (activity as? LoginActivity)?.onFormFragmentAccountLogged( + studentsWithSemesters, Triple( + binding.loginFormUsername.text.toString(), + binding.loginFormPass.text.toString(), + resources.getStringArray(R.array.hosts_values)[1] + ) + ) } override fun onResume() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt index 891a6b0b..17d8c5ec 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt @@ -34,9 +34,9 @@ class LoginAdvancedPresenter @Inject constructor( } } - private fun onBadCredentials() { + private fun onBadCredentials(message: String?) { view?.run { - setErrorPassIncorrect() + setErrorPassIncorrect(message) showSoftKeyboard() Timber.i("Entered wrong username or password") } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt index 029a6b4d..1d2b2856 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt @@ -49,7 +49,7 @@ interface LoginAdvancedView : BaseView { fun setErrorPassInvalid(focus: Boolean) - fun setErrorPassIncorrect() + fun setErrorPassIncorrect(message: String?) fun clearUsernameError() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt index e383072e..6e0294a4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt @@ -5,6 +5,7 @@ import android.os.Bundle import android.view.View import android.view.View.GONE import android.view.View.VISIBLE +import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -41,10 +42,12 @@ class LoginFormFragment : BaseFragment(R.layout.fragme get() = binding.loginFormPass.text.toString() override val formHostValue: String - get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty() + get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) + .orEmpty() override val formHostSymbol: String - get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty() + get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) + .orEmpty() override val nicknameLabel: String get() = getString(R.string.login_nickname_hint) @@ -88,7 +91,13 @@ class LoginFormFragment : BaseFragment(R.layout.fragme with(binding.loginFormHost) { setText(hostKeys.getOrNull(0).orEmpty()) - setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys)) + setAdapter( + LoginSymbolAdapter( + context, + R.layout.support_simple_spinner_dropdown_item, + hostKeys + ) + ) setOnClickListener { if (binding.loginFormContainer.visibility == GONE) dismissDropDown() } } } @@ -142,24 +151,31 @@ class LoginFormFragment : BaseFragment(R.layout.fragme } } - override fun setErrorPassIncorrect() { - with(binding.loginFormPassLayout) { - error = getString(R.string.login_incorrect_password) + override fun setErrorPassIncorrect(message: String?) { + val error = message ?: getString(R.string.login_incorrect_password_default) + + with(binding) { + loginFormUsernameLayout.error = " " + loginFormPassLayout.error = " " + loginFormErrorBox.text = getString(R.string.login_incorrect_password, error) + loginFormErrorBox.isVisible = true } } override fun setErrorEmailInvalid(domain: String) { with(binding.loginFormUsernameLayout) { - error = getString(R.string.login_invalid_custom_email,domain) + error = getString(R.string.login_invalid_custom_email, domain) } } override fun clearUsernameError() { binding.loginFormUsernameLayout.error = null + binding.loginFormErrorBox.isVisible = false } override fun clearPassError() { binding.loginFormPassLayout.error = null + binding.loginFormErrorBox.isVisible = false } override fun showSoftKeyboard() { @@ -183,12 +199,18 @@ class LoginFormFragment : BaseFragment(R.layout.fragme binding.loginFormVersion.text = "v${appInfo.versionName}" } - override fun notifyParentAccountLogged(studentsWithSemesters: List, loginData: Triple) { + override fun notifyParentAccountLogged( + studentsWithSemesters: List, + loginData: Triple + ) { (activity as? LoginActivity)?.onFormFragmentAccountLogged(studentsWithSemesters, loginData) } override fun openPrivacyPolicyPage() { - context?.openInternetBrowser("https://wulkanowy.github.io/polityka-prywatnosci.html", ::showMessage) + context?.openInternetBrowser( + "https://wulkanowy.github.io/polityka-prywatnosci.html", + ::showMessage + ) } override fun showContact(show: Boolean) { @@ -210,7 +232,10 @@ class LoginFormFragment : BaseFragment(R.layout.fragme } override fun openFaqPage() { - context?.openInternetBrowser("https://wulkanowy.github.io/czesto-zadawane-pytania/dlaczego-nie-moge-sie-zalogowac", ::showMessage) + context?.openInternetBrowser( + "https://wulkanowy.github.io/czesto-zadawane-pytania/dlaczego-nie-moge-sie-zalogowac", + ::showMessage + ) } override fun onResume() { @@ -223,7 +248,8 @@ class LoginFormFragment : BaseFragment(R.layout.fragme chooserTitle = requireContext().getString(R.string.login_email_intent_title), email = "wulkanowyinc@gmail.com", subject = requireContext().getString(R.string.login_email_subject), - body = requireContext().getString(R.string.login_email_text, + body = requireContext().getString( + R.string.login_email_text, "${appInfo.systemManufacturer} ${appInfo.systemModel}", appInfo.systemVersion.toString(), appInfo.versionName, 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 d79c422d..bd876b84 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 @@ -30,7 +30,7 @@ class LoginFormPresenter @Inject constructor( showVersion() loginErrorHandler.onBadCredentials = { - setErrorPassIncorrect() + setErrorPassIncorrect(it) showSoftKeyboard() Timber.i("Entered wrong username or password") } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt index 079629ef..efdaa082 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt @@ -37,7 +37,7 @@ interface LoginFormView : BaseView { fun setErrorPassInvalid(focus: Boolean) - fun setErrorPassIncorrect() + fun setErrorPassIncorrect(message: String?) fun setErrorEmailInvalid(domain: String) diff --git a/app/src/main/res/layout/fragment_login_form.xml b/app/src/main/res/layout/fragment_login_form.xml index 06d1fa5e..d1c997ff 100644 --- a/app/src/main/res/layout/fragment_login_form.xml +++ b/app/src/main/res/layout/fragment_login_form.xml @@ -110,10 +110,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="32dp" - android:layout_marginLeft="32dp" android:layout_marginTop="32dp" android:layout_marginEnd="32dp" - android:layout_marginRight="32dp" android:gravity="center_horizontal" android:text="@string/login_header_default" android:textSize="16sp" @@ -126,6 +124,20 @@ app:layout_constraintVertical_chainStyle="packed" app:layout_goneMarginTop="64dp" /> + + app:layout_constraintTop_toBottomOf="@+id/loginFormErrorBox" + app:layout_goneMarginTop="48dp"> @@ -217,7 +227,6 @@ android:layout_marginRight="24dp" android:hint="@string/login_host_hint" android:orientation="vertical" - app:layout_constraintBottom_toTopOf="@+id/loginFormAdvancedButton" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginFormRecoverLink"> @@ -262,14 +271,13 @@ android:id="@+id/loginFormPrivacyLink" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="24dp" android:gravity="start|center_vertical" android:text="@string/login_privacy_policy" android:textColor="?android:textColorSecondary" android:textSize="12sp" app:fontFamily="sans-serif-medium" app:layout_constraintStart_toStartOf="@id/loginFormAdvancedButton" - app:layout_constraintTop_toBottomOf="@+id/loginFormAdvancedButton" + app:layout_constraintTop_toTopOf="@+id/loginFormVersion" tools:visibility="visible" /> Symbol Přihlásit Toto heslo je příliš krátké - Přihlašovací údaje jsou nesprávné. Ujistěte se, že je v poli níže vybrána správná variace deníku UONET+ + Přihlašovací údaje jsou nesprávné + %1$s. Ujistěte se, že je v poli níže vybrána správná variace deníku UONET+ Neplatný PIN Neplatný token Token vypršel diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index f86c3076..8c30c7a9 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -42,7 +42,8 @@ Symbol Anmelden Passwort ist zu kurz - Anmeldedaten sind falsch. Stellen Sie sicher, dass die richtige UONET+ Registervariation im unteren Feld ausgewählt ist + Anmeldedaten sind falsch + %1$s. Stellen Sie sicher, dass die richtige UONET+ Registervariation im unteren Feld ausgewählt ist Ungültige PIN Ungültige token Token ist nicht mehr gültig diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index cfc6810e..6ac94a4e 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -42,7 +42,8 @@ Symbol Zaloguj To hasło jest za krótkie - Dane logowania są niepoprawne. Upewnij się, że została wybrana odpowiednia odmiana dziennika UONET+ w polu poniżej + Dane logowania są niepoprawne + %1$s. Upewnij się, że poniżej została wybrana odpowiednia odmiana dziennika UONET+ Nieprawidłowy PIN Nieprawidłowy token Token stracił ważność diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 4e41088f..54ae5e1b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -42,7 +42,8 @@ Symbol Войти Слишком короткий пароль - Данные для входа неверны. Убедитесь, что в поле ниже выбран правильный вариант регистра UONET+ + Данные для входа неверны + %1$s. Убедитесь, что в поле ниже выбран правильный вариант регистра UONET+ Неправильный PIN Неверный token Token просрочен diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 75a42467..b1fd78ae 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -42,7 +42,8 @@ Symbol Prihlásiť Toto heslo je príliš krátke - Prihlasovacie údaje sú nesprávne. Uistite sa, že je v poli nižšie vybraná správna variácie denníka UONET+ + Prihlasovacie údaje sú nesprávne + %1$s. Uistite sa, že je v poli nižšie vybraná správna variácie denníka UONET+ Neplatný PIN Neplatný token Platnosť tokenu vypršala diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 51f25881..70f40b66 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -42,7 +42,8 @@ Symbol Увійти Занадто короткий пароль - Дані для входу неправильні. Переконайтеся, що у полі нижче вказано правильний варіант реєстрації UONET+ + Дані для входу неправильні + %1$s. Переконайтеся, що у полі нижче вказано правильний варіант реєстрації UONET+ Неправильний PIN Неправильний token Минув термін дії токену diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index de85614b..57540110 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -46,7 +46,8 @@ Symbol Sign in Password too short - Login details are incorrect. Make sure the correct UONET+ register variation is selected in the field below + Login details are incorrect + %1$s. Make sure the correct UONET+ register variation is selected below Invalid PIN Invalid token Token expired From 2cb11e443cadde38b90b01f6946644a0f4b3a193 Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Sat, 25 Sep 2021 13:46:11 +0200 Subject: [PATCH 012/357] Mark teacher with yellow when new and old are the same (#1529) --- app/build.gradle | 2 +- .../ui/modules/timetable/TimetableAdapter.kt | 2 +- .../ui/modules/timetable/TimetableDialog.kt | 21 +++++++++++++++++-- .../timetablewidget/TimetableWidgetFactory.kt | 2 +- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cea9ae81..435a2f56 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -166,7 +166,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:8f3721f1f9" + implementation "io.github.wulkanowy:sdk:230d2075df" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt index 87b3362d..4a5a0699 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt @@ -360,7 +360,7 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter { + timetableDialogTeacherValue.run { + visibility = GONE + } + timetableDialogTeacherNewValue.run { + visibility = VISIBLE + text = teacher + } + } teacher.isNotBlank() -> timetableDialogTeacherValue.text = teacher else -> { timetableDialogTeacherTitle.visibility = GONE diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt index 45b79b50..c8dda23c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt @@ -173,7 +173,7 @@ class TimetableWidgetFactory( updateNotCanceledLessonNumberColor(this, lesson) updateNotCanceledSubjectColor(this, lesson) - val teacherChange = lesson.teacherOld.isNotBlank() && lesson.teacher != lesson.teacherOld + val teacherChange = lesson.teacherOld.isNotBlank() updateNotCanceledRoom(this, lesson, teacherChange) updateNotCanceledTeacher(this, lesson, teacherChange) } From de6131f4f574540357537429985e27cc8247b6dc Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Sat, 25 Sep 2021 13:46:35 +0200 Subject: [PATCH 013/357] Add transparency to lucky number widget (#1530) --- app/src/main/res/drawable/background_luckynumber_widget.xml | 4 ++-- .../main/res/drawable/background_luckynumber_widget_dark.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/drawable/background_luckynumber_widget.xml b/app/src/main/res/drawable/background_luckynumber_widget.xml index f29744d0..367c5527 100644 --- a/app/src/main/res/drawable/background_luckynumber_widget.xml +++ b/app/src/main/res/drawable/background_luckynumber_widget.xml @@ -1,6 +1,6 @@ - - + + diff --git a/app/src/main/res/drawable/background_luckynumber_widget_dark.xml b/app/src/main/res/drawable/background_luckynumber_widget_dark.xml index fa15fd85..cb094b57 100644 --- a/app/src/main/res/drawable/background_luckynumber_widget_dark.xml +++ b/app/src/main/res/drawable/background_luckynumber_widget_dark.xml @@ -1,6 +1,6 @@ - - + + From 9211baf7ec54d1e4774e53340e43a997bead1e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sat, 25 Sep 2021 14:02:38 +0200 Subject: [PATCH 014/357] Add notification piggyback (#1503) --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 7 ++++ .../repositories/PreferencesRepository.kt | 21 ++++++---- .../VulcanNotificationListenerService.kt | 24 +++++++++++ .../wulkanowy/services/sync/SyncManager.kt | 24 +++++++---- .../notifications/NotificationsFragment.kt | 41 +++++++++++++++++-- .../notifications/NotificationsPresenter.kt | 22 ++++++++++ .../notifications/NotificationsView.kt | 6 +++ app/src/main/res/values-pl/strings.xml | 4 ++ .../main/res/values/preferences_defaults.xml | 1 + app/src/main/res/values/preferences_keys.xml | 1 + app/src/main/res/values/strings.xml | 4 ++ .../xml/scheme_preferences_notifications.xml | 6 +++ 13 files changed, 145 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/services/piggyback/VulcanNotificationListenerService.kt diff --git a/app/build.gradle b/app/build.gradle index 435a2f56..71375658 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -166,7 +166,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:230d2075df" + implementation "io.github.wulkanowy:sdk:49c2071d10" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ad5adaf2..77ce3506 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -93,6 +93,13 @@ + + + + + ? @@ -230,8 +235,10 @@ class PreferencesRepository @Inject constructor( set(value) = sharedPref.edit().putInt(PREF_KEY_IN_APP_REVIEW_COUNT, value).apply() var inAppReviewDate: LocalDate? - get() = sharedPref.getLong(PREF_KEY_IN_APP_REVIEW_DATE, 0).takeIf { it != 0L }?.toLocalDate() - set(value) = sharedPref.edit().putLong(PREF_KEY_IN_APP_REVIEW_DATE, value!!.toTimestamp()).apply() + get() = sharedPref.getLong(PREF_KEY_IN_APP_REVIEW_DATE, 0).takeIf { it != 0L } + ?.toLocalDate() + set(value) = sharedPref.edit().putLong(PREF_KEY_IN_APP_REVIEW_DATE, value!!.toTimestamp()) + .apply() var isAppReviewDone: Boolean get() = sharedPref.getBoolean(PREF_KEY_IN_APP_REVIEW_DONE, false) diff --git a/app/src/main/java/io/github/wulkanowy/services/piggyback/VulcanNotificationListenerService.kt b/app/src/main/java/io/github/wulkanowy/services/piggyback/VulcanNotificationListenerService.kt new file mode 100644 index 00000000..c7df2dbc --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/piggyback/VulcanNotificationListenerService.kt @@ -0,0 +1,24 @@ +package io.github.wulkanowy.services.piggyback + +import android.service.notification.NotificationListenerService +import android.service.notification.StatusBarNotification +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.services.sync.SyncManager +import javax.inject.Inject + +@AndroidEntryPoint +class VulcanNotificationListenerService : NotificationListenerService() { + + @Inject + lateinit var syncManager: SyncManager + + @Inject + lateinit var preferenceRepository: PreferencesRepository + + override fun onNotificationPosted(statusBarNotification: StatusBarNotification?) { + if (statusBarNotification?.packageName == "pl.edu.vulcan.hebe" && preferenceRepository.isNotificationPiggybackEnabled) { + syncManager.startOneTimeSyncWorker() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt index b94d97e3..02d8b964 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt @@ -57,14 +57,20 @@ class SyncManager @Inject constructor( fun startPeriodicSyncWorker(restart: Boolean = false) { if (preferencesRepository.isServiceEnabled && !now().isHolidays) { - workManager.enqueueUniquePeriodicWork(SyncWorker::class.java.simpleName, if (restart) REPLACE else KEEP, - PeriodicWorkRequestBuilder(preferencesRepository.servicesInterval, MINUTES) + val serviceInterval = preferencesRepository.servicesInterval + + workManager.enqueueUniquePeriodicWork( + SyncWorker::class.java.simpleName, if (restart) REPLACE else KEEP, + PeriodicWorkRequestBuilder(serviceInterval, MINUTES) .setInitialDelay(10, MINUTES) .setBackoffCriteria(EXPONENTIAL, 30, MINUTES) - .setConstraints(Constraints.Builder() - .setRequiredNetworkType(if (preferencesRepository.isServicesOnlyWifi) UNMETERED else CONNECTED) - .build()) - .build()) + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(if (preferencesRepository.isServicesOnlyWifi) UNMETERED else CONNECTED) + .build() + ) + .build() + ) } } @@ -77,7 +83,11 @@ class SyncManager @Inject constructor( ) .build() - workManager.enqueueUniqueWork("${SyncWorker::class.java.simpleName}_one_time", ExistingWorkPolicy.REPLACE, work) + workManager.enqueueUniqueWork( + "${SyncWorker::class.java.simpleName}_one_time", + ExistingWorkPolicy.REPLACE, + work + ) return workManager.getWorkInfoByIdLiveData(work.id).asFlow() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt index 0fc7e68e..207d587d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt @@ -10,9 +10,12 @@ import android.provider.Settings import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog +import androidx.core.app.NotificationManagerCompat import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreferenceCompat import androidx.recyclerview.widget.RecyclerView import com.thelittlefireman.appkillermanager.AppKillerManager import com.thelittlefireman.appkillermanager.exceptions.NoActionFoundException @@ -43,6 +46,21 @@ class NotificationsFragment : PreferenceFragmentCompat(), override val titleStringId get() = R.string.pref_settings_notifications_title + override val isNotificationPermissionGranted: Boolean + get() { + val packageNameList = + NotificationManagerCompat.getEnabledListenerPackages(requireContext()) + val appPackageName = requireContext().packageName + + return appPackageName in packageNameList + } + + private val notificationSettingsContract = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + + presenter.onNotificationPermissionResult() + } + override fun initView(showDebugNotificationSwitch: Boolean) { findPreference(getString(R.string.pref_key_notification_debug))?.isVisible = showDebugNotificationSwitch @@ -57,12 +75,11 @@ class NotificationsFragment : PreferenceFragmentCompat(), } } - findPreference(getString(R.string.pref_key_notifications_system_settings))?.run { - setOnPreferenceClickListener { + findPreference(getString(R.string.pref_key_notifications_system_settings)) + ?.setOnPreferenceClickListener { presenter.onOpenSystemSettingsClicked() true } - } } override fun onCreateRecyclerView( @@ -157,6 +174,24 @@ class NotificationsFragment : PreferenceFragmentCompat(), } } + override fun openNotificationPermissionDialog() { + AlertDialog.Builder(requireContext()) + .setTitle(getString(R.string.pref_notification_piggyback_popup_title)) + .setMessage(getString(R.string.pref_notification_piggyback_popup_description)) + .setPositiveButton(getString(R.string.pref_notification_piggyback_popup_positive)) { _, _ -> + notificationSettingsContract.launch(Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS")) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + setNotificationPiggybackPreferenceChecked(false) + } + .show() + } + + override fun setNotificationPiggybackPreferenceChecked(isChecked: Boolean) { + findPreference(getString(R.string.pref_key_notifications_piggyback))?.isChecked = + isChecked + } + override fun onResume() { super.onResume() preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt index 8366d309..19d2f559 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt @@ -31,6 +31,9 @@ class NotificationsPresenter @Inject constructor( ) initView(appInfo.isDebug) } + + checkNotificationPiggybackState() + Timber.i("Settings notifications view was initialized") } @@ -47,6 +50,11 @@ class NotificationsPresenter @Inject constructor( isDebugNotificationEnableKey -> { chuckerCollector.showNotification = isDebugNotificationEnable } + isNotificationPiggybackEnabledKey -> { + if (isNotificationPiggybackEnabled && view?.isNotificationPermissionGranted == false) { + view?.openNotificationPermissionDialog() + } + } } } analytics.logEvent("setting_changed", "name" to key) @@ -59,4 +67,18 @@ class NotificationsPresenter @Inject constructor( fun onOpenSystemSettingsClicked() { view?.openSystemSettings() } + + fun onNotificationPermissionResult() { + view?.run { + setNotificationPiggybackPreferenceChecked(isNotificationPermissionGranted) + } + } + + private fun checkNotificationPiggybackState() { + if (preferencesRepository.isNotificationPiggybackEnabled) { + view?.run { + setNotificationPiggybackPreferenceChecked(isNotificationPermissionGranted) + } + } + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsView.kt index 2ab9b035..42862b38 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsView.kt @@ -4,6 +4,8 @@ import io.github.wulkanowy.ui.base.BaseView interface NotificationsView : BaseView { + val isNotificationPermissionGranted: Boolean + fun initView(showDebugNotificationSwitch: Boolean) fun showFixSyncDialog() @@ -11,4 +13,8 @@ interface NotificationsView : BaseView { fun openSystemSettings() fun enableNotification(notificationKey: String, enable: Boolean) + + fun openNotificationPermissionDialog() + + fun setNotificationPiggybackPreferenceChecked(isChecked: Boolean) } diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 6ac94a4e..d1f44e5c 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -624,6 +624,10 @@ Przejdź do ustawień Pokazuj powiadomienia debugowania Synchronizacja jest wyłączona + Przechwytywanie powiadomień oficjalnej aplikacji + Przechwytywanie powiadomień + Dzięki tej funkcji możesz zyskać namiastkę powiadomień push takich, jak w oficjalnej aplikacji. Wystarczy że zezwolisz Wulkanowemu na odbieranie wszystkich powiadomień w ustawieniach systemowych.\n\nJak to działa?\nGdy dostaniesz powiadomienie w Dzienniczku VULCANa Wulkanowy zostanie o tym powiadomiony (po to te dodatkowe uprawnienia) i uruchomi synchronizację, dzięki czemu będzie mógł wysłać własne powiadomienie.\n\nTYLKO DLA ZAAWANSOWANYCH UŻYTKOWNIKÓW + Przejdź do ustawień Synchronizacja Automatyczna aktualizacja Zawieszona na wakacjach diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index 1ba9359c..9286052d 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -26,6 +26,7 @@ false false 0 + false LUCKY_NUMBER MESSAGES diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index 09dac700..bae6a617 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -32,4 +32,5 @@ message_send_is_draft message_send_recipients last_sync_date + notifications_piggyback diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 57540110..af200603 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -625,6 +625,10 @@ Go to settings Show debug notifications Synchronization is disabled + Capture official app notifications + Capture notifications + With this feature you can gain a substitute of push notifications like in the official app. All you need to do is allow Wulkanowy to receive all notifications in your system settings.\n\nHow it works?\nWhen you get a notification in Dziennik VULCAN, Wulkanowy will be notified (that\'s what these extra permissions are for) and will trigger a sync so that can send its own notification.\n\nFOR ADVANCED USERS ONLY + Go to settings Synchronization Automatic update diff --git a/app/src/main/res/xml/scheme_preferences_notifications.xml b/app/src/main/res/xml/scheme_preferences_notifications.xml index ac88746c..24b83169 100644 --- a/app/src/main/res/xml/scheme_preferences_notifications.xml +++ b/app/src/main/res/xml/scheme_preferences_notifications.xml @@ -14,6 +14,12 @@ app:key="@string/pref_key_notifications_upcoming_lessons_enable" app:singleLineTitle="false" app:title="@string/pref_notify_upcoming_lessons_switch" /> + Date: Sat, 25 Sep 2021 15:18:40 +0200 Subject: [PATCH 015/357] Don't stop loading the timetable when error occurs in upcoming lessons notification scheduling (#1532) --- .../TimetableNotificationSchedulerHelper.kt | 67 +++++++++++-------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt index 98bd93eb..a42a0ab4 100644 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt @@ -31,6 +31,7 @@ import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.toTimestamp import kotlinx.coroutines.withContext import timber.log.Timber +import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalDateTime.now import javax.inject.Inject @@ -57,10 +58,13 @@ class TimetableNotificationSchedulerHelper @Inject constructor( lessons.sortedBy { it.start }.forEachIndexed { index, lesson -> val upcomingTime = getUpcomingLessonTime(index, lessons, lesson) cancelScheduledTo( - upcomingTime..lesson.start, - getRequestCode(upcomingTime, studentId) + range = upcomingTime..lesson.start, + requestCode = getRequestCode(upcomingTime, studentId) + ) + cancelScheduledTo( + range = lesson.start..lesson.end, + requestCode = getRequestCode(lesson.start, studentId) ) - cancelScheduledTo(lesson.start..lesson.end, getRequestCode(lesson.start, studentId)) Timber.d("TimetableNotification canceled: type 1 & 2, subject: ${lesson.subject}, start: ${lesson.start}, student: $studentId") } @@ -82,6 +86,11 @@ class TimetableNotificationSchedulerHelper @Inject constructor( return cancelScheduled(lessons, student) } + if (lessons.firstOrNull()?.date?.isAfter(LocalDate.now().plusDays(2)) == true) { + Timber.d("Timetable notification scheduling skipped - lessons are too far") + return + } + withContext(dispatchersProvider.backgroundThread) { lessons.groupBy { it.date } .map { it.value.sortedBy { lesson -> lesson.start } } @@ -96,26 +105,26 @@ class TimetableNotificationSchedulerHelper @Inject constructor( if (lesson.start > now()) { scheduleBroadcast( - intent, - student.studentId, - NOTIFICATION_TYPE_UPCOMING, - getUpcomingLessonTime(index, active, lesson) + intent = intent, + studentId = student.studentId, + notificationType = NOTIFICATION_TYPE_UPCOMING, + time = getUpcomingLessonTime(index, active, lesson) ) } if (lesson.end > now()) { scheduleBroadcast( - intent, - student.studentId, - NOTIFICATION_TYPE_CURRENT, - lesson.start + intent = intent, + studentId = student.studentId, + notificationType = NOTIFICATION_TYPE_CURRENT, + time = lesson.start ) if (active.lastIndex == index) { scheduleBroadcast( - intent, - student.studentId, - NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION, - lesson.end + intent = intent, + studentId = student.studentId, + notificationType = NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION, + time = lesson.end ) } } @@ -143,17 +152,21 @@ class TimetableNotificationSchedulerHelper @Inject constructor( notificationType: Int, time: LocalDateTime ) { - AlarmManagerCompat.setExactAndAllowWhileIdle( - alarmManager, RTC_WAKEUP, time.toTimestamp(), - PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also { - it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id) - it.putExtra(LESSON_TYPE, notificationType) - }, FLAG_UPDATE_CURRENT) - ) - Timber.d( - "TimetableNotification scheduled: type: $notificationType, subject: ${ - intent.getStringExtra(LESSON_TITLE) - }, start: $time, student: $studentId" - ) + try { + AlarmManagerCompat.setExactAndAllowWhileIdle( + alarmManager, RTC_WAKEUP, time.toTimestamp(), + PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also { + it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id) + it.putExtra(LESSON_TYPE, notificationType) + }, FLAG_UPDATE_CURRENT) + ) + Timber.d( + "TimetableNotification scheduled: type: $notificationType, subject: ${ + intent.getStringExtra(LESSON_TITLE) + }, start: $time, student: $studentId" + ) + } catch (e: IllegalStateException) { + Timber.e(e) + } } } From 7a46ef5f1924dcb7b4a470c9c801bc0a0d07142f Mon Sep 17 00:00:00 2001 From: Piotr Romanowski Date: Sat, 25 Sep 2021 17:19:21 +0200 Subject: [PATCH 016/357] Add calculated average help dialog (#1379) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rafał Borcz --- .../ui/modules/grade/GradeAverageProvider.kt | 10 ++- .../grade/summary/GradeSummaryAdapter.kt | 11 +++- .../grade/summary/GradeSummaryFragment.kt | 28 ++++++++- .../grade/summary/GradeSummaryPresenter.kt | 8 +++ .../modules/grade/summary/GradeSummaryView.kt | 4 ++ .../message/preview/MessagePreviewAdapter.kt | 7 +-- .../github/wulkanowy/utils/GradeExtension.kt | 5 +- app/src/main/res/drawable/ic_help.xml | 10 +++ .../scrollable_header_grade_summary.xml | 61 +++++++++++++++---- .../main/res/values-pl/preferences_values.xml | 2 +- app/src/main/res/values-pl/strings.xml | 6 +- .../main/res/values/preferences_values.xml | 4 +- app/src/main/res/values/strings.xml | 11 ++-- .../wulkanowy/utils/GradeExtensionTest.kt | 16 ++--- 14 files changed, 139 insertions(+), 44 deletions(-) create mode 100644 app/src/main/res/drawable/ic_help.xml diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt index 4a304972..2784f429 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt @@ -131,7 +131,9 @@ class GradeAverageProvider @Inject constructor( val updatedFirstSemesterGrades = firstSemesterSubject?.grades?.updateModifiers(student).orEmpty() - (updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage(isOptionalArithmeticAverage) + (updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage( + isOptionalArithmeticAverage + ) } else { secondSemesterSubject.average } @@ -147,7 +149,8 @@ class GradeAverageProvider @Inject constructor( return if (!isAnyVulcanAverage || isGradeAverageForceCalc) { val secondSemesterAverage = - secondSemesterSubject.grades.updateModifiers(student).calcAverage(isOptionalArithmeticAverage) + secondSemesterSubject.grades.updateModifiers(student) + .calcAverage(isOptionalArithmeticAverage) val firstSemesterAverage = firstSemesterSubject?.grades?.updateModifiers(student) ?.calcAverage(isOptionalArithmeticAverage) ?: secondSemesterAverage @@ -213,7 +216,8 @@ class GradeAverageProvider @Inject constructor( proposedPoints = "", finalPoints = "", pointsSum = "", - average = if (calcAverage) details.updateModifiers(student).calcAverage(isOptionalArithmeticAverage) else .0 + average = if (calcAverage) details.updateModifiers(student) + .calcAverage(isOptionalArithmeticAverage) else .0 ) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt index 0754361c..082c847e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt @@ -10,7 +10,7 @@ import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.databinding.ItemGradeSummaryBinding import io.github.wulkanowy.databinding.ScrollableHeaderGradeSummaryBinding -import io.github.wulkanowy.utils.calcAverage +import io.github.wulkanowy.utils.calcFinalAverage import java.util.Locale import javax.inject.Inject @@ -25,6 +25,10 @@ class GradeSummaryAdapter @Inject constructor( var items = emptyList() + var onCalculatedHelpClickListener: () -> Unit = {} + + var onFinalHelpClickListener: () -> Unit = {} + override fun getItemCount() = items.size + if (items.isNotEmpty()) 1 else 0 override fun getItemViewType(position: Int) = when (position) { @@ -60,7 +64,7 @@ class GradeSummaryAdapter @Inject constructor( val finalItemsCount = items.count { it.finalGrade.matches("[0-6][+-]?".toRegex()) } val calculatedItemsCount = items.count { value -> value.average != 0.0 } val allItemsCount = items.count { !it.subject.equals("zachowanie", true) } - val finalAverage = items.calcAverage( + val finalAverage = items.calcFinalAverage( preferencesRepository.gradePlusModifier, preferencesRepository.gradeMinusModifier ) @@ -83,6 +87,9 @@ class GradeSummaryAdapter @Inject constructor( calculatedItemsCount, allItemsCount ) + + gradeSummaryCalculatedAverageHelp.setOnClickListener { onCalculatedHelpClickListener() } + gradeSummaryFinalAverageHelp.setOnClickListener { onFinalHelpClickListener() } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt index 0ac16fb3..3810902f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt @@ -5,6 +5,7 @@ import android.view.View import android.view.View.GONE import android.view.View.INVISIBLE import android.view.View.VISIBLE +import androidx.appcompat.app.AlertDialog import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -48,6 +49,11 @@ class GradeSummaryFragment : } override fun initView() { + with(gradeSummaryAdapter) { + onCalculatedHelpClickListener = presenter::onCalculatedAverageHelpClick + onFinalHelpClickListener = presenter::onFinalAverageHelpClick + } + with(binding.gradeSummaryRecycler) { layoutManager = LinearLayoutManager(context) adapter = gradeSummaryAdapter @@ -55,7 +61,11 @@ class GradeSummaryFragment : with(binding) { gradeSummarySwipe.setOnRefreshListener(presenter::onSwipeRefresh) gradeSummarySwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) - gradeSummarySwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh)) + gradeSummarySwipe.setProgressBackgroundColorSchemeColor( + requireContext().getThemeAttrColor( + R.attr.colorSwipeRefresh + ) + ) gradeSummaryErrorRetry.setOnClickListener { presenter.onRetry() } gradeSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() } } @@ -107,6 +117,22 @@ class GradeSummaryFragment : binding.gradeSummarySwipe.isRefreshing = show } + override fun showCalculatedAverageHelpDialog() { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.grade_summary_calculated_average_help_dialog_title) + .setMessage(R.string.grade_summary_calculated_average_help_dialog_message) + .setPositiveButton(R.string.all_close) { _, _ -> } + .show() + } + + override fun showFinalAverageHelpDialog() { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.grade_summary_final_average_help_dialog_title) + .setMessage(R.string.grade_summary_final_average_help_dialog_message) + .setPositiveButton(R.string.all_close) { _, _ -> } + .show() + } + override fun onParentLoadData(semesterId: Int, forceRefresh: Boolean) { presenter.onParentViewLoadData(semesterId, forceRefresh) } 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 7adfd7e5..933633dc 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 @@ -135,6 +135,14 @@ class GradeSummaryPresenter @Inject constructor( cancelJobs("load") } + fun onCalculatedAverageHelpClick() { + view?.showCalculatedAverageHelpDialog() + } + + fun onFinalAverageHelpClick() { + view?.showFinalAverageHelpDialog() + } + private fun createGradeSummaryItems(items: List): List { return items .filter { !checkEmpty(it) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt index 974d9141..156731c3 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt @@ -33,6 +33,10 @@ interface GradeSummaryView : BaseView { fun showEmpty(show: Boolean) + fun showCalculatedAverageHelpDialog() + + fun showFinalAverageHelpDialog() + fun notifyParentDataLoaded(semesterId: Int) fun notifyParentRefresh() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt index 421453c9..d75128be 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt @@ -79,12 +79,7 @@ class MessagePreviewAdapter @Inject constructor() : val readText = when { recipientCount > 1 -> { - context.resources.getQuantityString( - R.plurals.message_read_by, - message.readBy, - message.readBy, - recipientCount - ) + context.getString(R.string.message_read_by, message.readBy, recipientCount) } message.readBy == 1 -> { context.getString(R.string.message_read, context.getString(R.string.all_yes)) diff --git a/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt index 820e7f43..1be3093f 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt @@ -3,7 +3,7 @@ package io.github.wulkanowy.utils import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradeSummary -import io.github.wulkanowy.sdk.scrapper.grades.* +import io.github.wulkanowy.sdk.scrapper.grades.isGradeValid fun List.calcAverage(isOptionalArithmeticAverage: Boolean): Double { val isArithmeticAverage = isOptionalArithmeticAverage && !any { it.weightValue != .0 } @@ -18,8 +18,7 @@ fun List.calcAverage(isOptionalArithmeticAverage: Boolean): Double { return if (denominator != 0.0) counter / denominator else 0.0 } -@JvmName("calcSummaryAverage") -fun List.calcAverage(plusModifier: Double, minusModifier: Double) = asSequence() +fun List.calcFinalAverage(plusModifier: Double, minusModifier: Double) = asSequence() .mapNotNull { if (it.finalGrade.matches("[0-6][+-]?".toRegex())) { when { diff --git a/app/src/main/res/drawable/ic_help.xml b/app/src/main/res/drawable/ic_help.xml new file mode 100644 index 00000000..9c6ba292 --- /dev/null +++ b/app/src/main/res/drawable/ic_help.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/scrollable_header_grade_summary.xml b/app/src/main/res/layout/scrollable_header_grade_summary.xml index 29657ba1..049219a9 100644 --- a/app/src/main/res/layout/scrollable_header_grade_summary.xml +++ b/app/src/main/res/layout/scrollable_header_grade_summary.xml @@ -1,5 +1,6 @@ - + android:layout_gravity="center" + android:orientation="horizontal"> + + + + + - + android:layout_gravity="center" + android:orientation="horizontal"> + + + + + Kolory ocen w dzienniku - Średnia ocen z drugiego semestru + Średnia ocen z wybranego semestru Średnia średnich z obu semestrów Średnia wszystkich ocen z całego roku diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index d1f44e5c..d7a49c11 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -91,7 +91,11 @@ Ocena końcowa Przewidywana ocena Obliczona średnia + Jak działa Obliczona średnia? + Obliczona średnia to średnia arytmetyczna wyliczona ze średnich przedmiotów. Pozwala ona na poznanie przybliżonej średniej końcowej. Jest obliczana w sposób wybrany przez użytkownika w ustawieniach aplikacji. Zalecane jest, aby wybrać odpowiednią opcję. Wynika to z tego, że sposoby obliczania średnich w szkołach różnią się. Dodatkowo jeśli twoja szkoła podaje średnią z przedmiotów na stronie dziennika aplikacja pobiera je i nie wylicza tych średnich. Można to zmienić wymuszając liczenie średniej w ustawieniach aplikacji.\n\nŚrednia ocen z aktualnie wybranego semestru:\n1. Obliczanie średniej ważonej dla każdego przedmiotu w danym semestrze\n2. Sumowanie obliczonych średnich\n3. Obliczenie średniej arytmetycznej z zsumowanych średnich\n\nŚrednia średnich z obu semestrów:\n1. Obliczanie średniej ważonej dla każdego przedmiotu w semestrze 1 i 2\n2. Obliczanie średniej arytmetycznej z obliczonych średnich semestrów 1 i 2 dla każdego przedmiotu.\n3. Sumowanie obliczonych średnich\n4. Obliczenie średniej arytmetycznej z zsumowanych średnich\n\nŚrednia wszystkich ocen z całego roku:\n1. Obliczanie średniej ważonej z całego roku dla każdego przedmiotu. Średnia końcowa w 1 semestrze jest bez znaczenia.\n3. Sumowanie obliczonych średnich\n4. Obliczenie średniej arytmetycznej z zsumowanych średnich Końcowa średnia + Jak działa Końcowa średnia? + Końcowa średnia to średnia arytmetyczna wyliczona ze wszystkich aktualnie dostępnych ocen końcowych w danym semestrze.\n\nSchemat obliczania składa się z następujących kroków:\n1. Zsumowanie ocen końcowych wystawionych przez nauczycieli\n2. Podzielenie przez liczbę przedmiotów, z których oceny zostały już wystawione z %1$d na %2$d przedmiotów Podsumowanie Klasa @@ -603,7 +607,7 @@ Wygląd i zachowanie aplikacji Domyślny widok - Obliczanie średniej końcoworocznej + Opcje średniej obliczonej Wymuś obliczanie średniej przez aplikację Pokazuj obecność Motyw diff --git a/app/src/main/res/values/preferences_values.xml b/app/src/main/res/values/preferences_values.xml index 9c1a0421..bd3e8b47 100644 --- a/app/src/main/res/values/preferences_values.xml +++ b/app/src/main/res/values/preferences_values.xml @@ -100,8 +100,8 @@ - Average of grades only from the 2nd semester - Average of grades from both semesters + Average of grades only from selected semester + Average of averages from both semesters Average of grades from the whole year diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index af200603..ed236932 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -101,6 +101,10 @@ Final grade Predicted grade Calculated average + How does Calculated Average work? + The Calculated Average is the arithmetic average calculated from the subjects averages. It allows you to know the approximate final average. It is calculated in a way selected by the user in the application settings. It is recommended that you choose the appropriate option. This is because the calculation of school averages differs. Additionally, if your school reports the average of the subjects on the Vulcan page, the application downloads them and does not calculate these averages. This can be changed by forcing the calculation of the average in the application settings.\n\nAverage of grades only from selected semester:\n1. Calculating the weighted average for each subject in a given semester\n2.Adding calculated averages\n3. Calculation of the arithmetic average of the summed averages\n\nAverage of averages from both semesters:\n1.Calculating the weighted average for each subject in semester 1 and 2\n2. Calculating the arithmetic average of the calculated averages for semesters 1 and 2 for each subject.\n3. Adding calculated averages\n4. Calculation of the arithmetic average of the summed averages\n\nAverage of grades from the whole year:\n1. Calculating weighted average over the year for each subject. The final average in the 1st semester is irrelevant.\n3. Adding calculated averages\n4. Calculating the arithmetic average of summed averages + How does the Final Average work? + The Final Average is the arithmetic average calculated from all currently available final grades in the given semester.\n\nThe calculation scheme consists of the following steps:\n1. Summing up the final grades given by teachers\n2. Divide by the number of subjects that have already been graded Final average from %1$d of %2$d subjects Summary @@ -245,10 +249,7 @@ Only unread Only with attachments Read: %s - - Read by: %1$d of %2$d people - Read by: %1$d of %2$d people - + Read by: %1$d of %2$d people %d message %d messages @@ -603,7 +604,7 @@ App appearance & behavior Default view - Calculation of the end-of-year average + Calculated average options Force average calculation by app Show presence Theme diff --git a/app/src/test/java/io/github/wulkanowy/utils/GradeExtensionTest.kt b/app/src/test/java/io/github/wulkanowy/utils/GradeExtensionTest.kt index 32b1602e..39d4c3bd 100644 --- a/app/src/test/java/io/github/wulkanowy/utils/GradeExtensionTest.kt +++ b/app/src/test/java/io/github/wulkanowy/utils/GradeExtensionTest.kt @@ -33,13 +33,15 @@ class GradeExtensionTest { @Test fun calcSummaryAverage() { - assertEquals(3.5, listOf( - createGradeSummary("4"), - createGradeSummary("5+"), - createGradeSummary("5-"), - createGradeSummary("test"), - createGradeSummary("0") - ).calcAverage(0.5, 0.5), 0.005) + assertEquals( + 3.5, listOf( + createGradeSummary("4"), + createGradeSummary("5+"), + createGradeSummary("5-"), + createGradeSummary("test"), + createGradeSummary("0") + ).calcFinalAverage(0.5, 0.5), 0.005 + ) } @Test From a6a1678b47ebc694d89d47a6b090a1f478cbde75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Sep 2021 15:51:06 +0000 Subject: [PATCH 017/357] Bump core from 1.10.1 to 1.10.2 (#1536) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 71375658..09c9e8e1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -224,7 +224,7 @@ dependencies { playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' - playImplementation 'com.google.android.play:core:1.10.1' + playImplementation 'com.google.android.play:core:1.10.2' playImplementation 'com.google.android.play:core-ktx:1.8.1' hmsImplementation 'com.huawei.hms:hianalytics:6.2.0.301' From b552dbc9048231f5923aec8f509c0de6e75332cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Sep 2021 15:56:11 +0000 Subject: [PATCH 018/357] Bump constraintlayout from 2.1.0 to 2.1.1 (#1535) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 09c9e8e1..8414ff5d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -183,7 +183,7 @@ dependencies { implementation "androidx.recyclerview:recyclerview:1.2.1" implementation "androidx.viewpager:viewpager:1.0.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" - implementation "androidx.constraintlayout:constraintlayout:2.1.0" + implementation "androidx.constraintlayout:constraintlayout:2.1.1" implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0" implementation "com.google.android.material:material:1.4.0" implementation "com.github.wulkanowy:material-chips-input:2.3.1" From dc90549b9da55d606664cfeac516708e4d31c16c Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Mon, 27 Sep 2021 17:56:43 +0200 Subject: [PATCH 019/357] Fix hiding last element in messages (#1538) --- app/src/main/res/layout/fragment_message_tab.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/layout/fragment_message_tab.xml b/app/src/main/res/layout/fragment_message_tab.xml index 4a6948f0..3a30cff8 100644 --- a/app/src/main/res/layout/fragment_message_tab.xml +++ b/app/src/main/res/layout/fragment_message_tab.xml @@ -13,6 +13,8 @@ android:id="@+id/messageTabRecycler" android:layout_width="match_parent" android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingBottom="64dp" tools:listitem="@layout/item_message" /> From d69118b0859640fa8b783b9fb38ca959fba0e3e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 27 Sep 2021 20:58:25 +0200 Subject: [PATCH 020/357] Add notifications center (#1524) --- .../40.json | 2316 +++++++++++++++++ app/src/main/AndroidManifest.xml | 13 + .../github/wulkanowy/data/RepositoryModule.kt | 6 +- .../github/wulkanowy/data/db/AppDatabase.kt | 13 +- .../wulkanowy/data/db/dao/NotificationDao.kt | 15 + .../data/db/entities/Notification.kt | 27 + .../data/db/migrations/Migration40.kt | 23 + .../{Notification.kt => NotificationData.kt} | 10 +- .../repositories/NotificationRepository.kt | 15 + .../notifications/AppNotificationManager.kt | 145 ++ .../sync/notifications/BaseNotification.kt | 102 - .../NewConferenceNotification.kt | 16 +- .../sync/notifications/NewExamNotification.kt | 16 +- .../notifications/NewGradeNotification.kt | 28 +- .../notifications/NewHomeworkNotification.kt | 16 +- .../NewLuckyNumberNotification.kt | 30 +- .../notifications/NewMessageNotification.kt | 16 +- .../sync/notifications/NewNoteNotification.kt | 16 +- .../NewSchoolAnnouncementNotification.kt | 34 +- .../sync/notifications/NotificationType.kt | 4 +- .../services/sync/works/AttendanceWork.kt | 7 +- .../ui/modules/dashboard/DashboardFragment.kt | 6 + .../modules/dashboard/DashboardPresenter.kt | 5 + .../ui/modules/dashboard/DashboardView.kt | 2 + .../NotificationDebugPresenter.kt | 2 +- .../NotificationsCenterAdapter.kt | 62 + .../NotificationsCenterFragment.kt | 108 + .../NotificationsCenterPresenter.kt | 83 + .../NotificationsCenterView.kt | 23 + .../notifications/NotificationsFragment.kt | 1 + .../layout/fragment_notifications_center.xml | 106 + .../res/layout/item_notifications_center.xml | 70 + .../main/res/menu/action_menu_dashboard.xml | 9 +- app/src/main/res/menu/action_menu_main.xml | 2 +- app/src/main/res/values/strings.xml | 1 + .../services/messaging/AppMessagingService.kt | 57 + 36 files changed, 3186 insertions(+), 219 deletions(-) create mode 100644 app/schemas/io.github.wulkanowy.data.db.AppDatabase/40.json create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/dao/NotificationDao.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/entities/Notification.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration40.kt rename app/src/main/java/io/github/wulkanowy/data/pojos/{Notification.kt => NotificationData.kt} (87%) create mode 100644 app/src/main/java/io/github/wulkanowy/data/repositories/NotificationRepository.kt create mode 100644 app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt delete mode 100644 app/src/main/java/io/github/wulkanowy/services/sync/notifications/BaseNotification.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterPresenter.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterView.kt create mode 100644 app/src/main/res/layout/fragment_notifications_center.xml create mode 100644 app/src/main/res/layout/item_notifications_center.xml create mode 100644 app/src/play/java/io/github/wulkanowy/services/messaging/AppMessagingService.kt diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/40.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/40.json new file mode 100644 index 00000000..362c7f0e --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/40.json @@ -0,0 +1,2316 @@ +{ + "formatVersion": 1, + "database": { + "version": 40, + "identityHash": "e2fba6244951713b4e9b217adc5d1a23", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "semester_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `recipient_name` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `removed` INTEGER NOT NULL, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `unread_by` INTEGER NOT NULL, `read_by` INTEGER NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipient", + "columnName": "recipient_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "removed", + "columnName": "removed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `one_drive_id` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`real_id`))", + "fields": [ + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneDriveId", + "columnName": "one_drive_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "real_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ReportingUnits", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `short` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `roles` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roles", + "columnName": "roles", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` TEXT NOT NULL, `name` TEXT NOT NULL, `real_name` TEXT NOT NULL, `login_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `role` INTEGER NOT NULL, `hash` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realName", + "columnName": "real_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e2fba6244951713b4e9b217adc5d1a23')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 77ce3506..84c50523 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -45,6 +45,7 @@ tools:ignore="GoogleAppIndexingWarning,UnusedAttribute"> @@ -74,6 +75,7 @@ @@ -83,6 +85,7 @@ @@ -95,11 +98,20 @@ android:permission="android.permission.BIND_REMOTEVIEWS" /> + + + + + + 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 a1c3cbbb..f1b719e5 100644 --- a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt @@ -8,8 +8,8 @@ import androidx.preference.PreferenceManager import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.RetentionManager -import com.squareup.moshi.Moshi import com.fredporciuncula.flow.preferences.FlowSharedPreferences +import com.squareup.moshi.Moshi import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -202,4 +202,8 @@ internal class RepositoryModule { @Singleton @Provides fun provideSchoolAnnouncementDao(database: AppDatabase) = database.schoolAnnouncementDao + + @Singleton + @Provides + fun provideNotificationDao(database: AppDatabase) = database.notificationDao } 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 0dca9aa1..09aa972f 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 @@ -10,7 +10,6 @@ import io.github.wulkanowy.data.db.dao.AttendanceDao import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao import io.github.wulkanowy.data.db.dao.CompletedLessonsDao import io.github.wulkanowy.data.db.dao.ConferenceDao -import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao import io.github.wulkanowy.data.db.dao.ExamDao import io.github.wulkanowy.data.db.dao.GradeDao import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao @@ -23,8 +22,10 @@ import io.github.wulkanowy.data.db.dao.MessageAttachmentDao import io.github.wulkanowy.data.db.dao.MessagesDao import io.github.wulkanowy.data.db.dao.MobileDeviceDao import io.github.wulkanowy.data.db.dao.NoteDao +import io.github.wulkanowy.data.db.dao.NotificationDao import io.github.wulkanowy.data.db.dao.RecipientDao import io.github.wulkanowy.data.db.dao.ReportingUnitDao +import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao import io.github.wulkanowy.data.db.dao.SchoolDao import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.StudentDao @@ -38,7 +39,6 @@ import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.AttendanceSummary import io.github.wulkanowy.data.db.entities.CompletedLesson import io.github.wulkanowy.data.db.entities.Conference -import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradePartialStatistics @@ -51,9 +51,11 @@ import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageAttachment import io.github.wulkanowy.data.db.entities.MobileDevice import io.github.wulkanowy.data.db.entities.Note +import io.github.wulkanowy.data.db.entities.Notification import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.data.db.entities.ReportingUnit import io.github.wulkanowy.data.db.entities.School +import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentInfo @@ -95,6 +97,7 @@ import io.github.wulkanowy.data.db.migrations.Migration37 import io.github.wulkanowy.data.db.migrations.Migration38 import io.github.wulkanowy.data.db.migrations.Migration39 import io.github.wulkanowy.data.db.migrations.Migration4 +import io.github.wulkanowy.data.db.migrations.Migration40 import io.github.wulkanowy.data.db.migrations.Migration5 import io.github.wulkanowy.data.db.migrations.Migration6 import io.github.wulkanowy.data.db.migrations.Migration7 @@ -134,6 +137,7 @@ import javax.inject.Singleton StudentInfo::class, TimetableHeader::class, SchoolAnnouncement::class, + Notification::class ], version = AppDatabase.VERSION_SCHEMA, exportSchema = true @@ -142,7 +146,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 39 + const val VERSION_SCHEMA = 40 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), @@ -183,6 +187,7 @@ abstract class AppDatabase : RoomDatabase() { Migration37(), Migration38(), Migration39(), + Migration40() ) fun newInstance( @@ -252,4 +257,6 @@ abstract class AppDatabase : RoomDatabase() { abstract val timetableHeaderDao: TimetableHeaderDao abstract val schoolAnnouncementDao: SchoolAnnouncementDao + + abstract val notificationDao: NotificationDao } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/NotificationDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/NotificationDao.kt new file mode 100644 index 00000000..c5ae21bc --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/NotificationDao.kt @@ -0,0 +1,15 @@ +package io.github.wulkanowy.data.db.dao + +import androidx.room.Dao +import androidx.room.Query +import io.github.wulkanowy.data.db.entities.Notification +import kotlinx.coroutines.flow.Flow +import javax.inject.Singleton + +@Singleton +@Dao +interface NotificationDao : BaseDao { + + @Query("SELECT * FROM Notifications WHERE student_id = :studentId OR student_id = -1") + fun loadAll(studentId: Long): Flow> +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Notification.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Notification.kt new file mode 100644 index 00000000..740137f0 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Notification.kt @@ -0,0 +1,27 @@ +package io.github.wulkanowy.data.db.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import io.github.wulkanowy.services.sync.notifications.NotificationType +import java.time.LocalDateTime + +@Entity(tableName = "Notifications") +data class Notification( + + @ColumnInfo(name = "student_id") + val studentId: Long, + + val title: String, + + val content: String, + + val type: NotificationType, + + val date: LocalDateTime, + + val data: String? = null +) { + @PrimaryKey(autoGenerate = true) + var id: Long = 0 +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration40.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration40.kt new file mode 100644 index 00000000..6d2795c7 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration40.kt @@ -0,0 +1,23 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration40 : Migration(39, 40) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS `Notifications` ( + `student_id` INTEGER NOT NULL, + `title` TEXT NOT NULL, + `content` TEXT NOT NULL, + `type` TEXT NOT NULL, + `date` INTEGER NOT NULL, + `data` TEXT, + `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + ) + """ + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/Notification.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt similarity index 87% rename from app/src/main/java/io/github/wulkanowy/data/pojos/Notification.kt rename to app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt index ca274937..0b4603ef 100644 --- a/app/src/main/java/io/github/wulkanowy/data/pojos/Notification.kt +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt @@ -6,7 +6,7 @@ import androidx.annotation.StringRes import io.github.wulkanowy.services.sync.notifications.NotificationType import io.github.wulkanowy.ui.modules.main.MainView -sealed interface Notification { +sealed interface NotificationData { val type: NotificationType val startMenu: MainView.Section val icon: Int @@ -14,7 +14,7 @@ sealed interface Notification { val contentStringRes: Int } -data class MultipleNotifications( +data class MultipleNotificationsData( override val type: NotificationType, override val startMenu: MainView.Section, @DrawableRes override val icon: Int, @@ -23,9 +23,9 @@ data class MultipleNotifications( @PluralsRes val summaryStringRes: Int, val lines: List, -) : Notification +) : NotificationData -data class OneNotification( +data class OneNotificationData( override val type: NotificationType, override val startMenu: MainView.Section, @DrawableRes override val icon: Int, @@ -33,4 +33,4 @@ data class OneNotification( @StringRes override val contentStringRes: Int, val contentValues: List, -) : Notification +) : NotificationData diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/NotificationRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/NotificationRepository.kt new file mode 100644 index 00000000..36bc7c25 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/NotificationRepository.kt @@ -0,0 +1,15 @@ +package io.github.wulkanowy.data.repositories + +import io.github.wulkanowy.data.db.dao.NotificationDao +import io.github.wulkanowy.data.db.entities.Notification +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NotificationRepository @Inject constructor(private val notificationDao: NotificationDao) { + + fun getNotifications(studentId: Long) = notificationDao.loadAll(studentId) + + suspend fun saveNotification(notification: Notification) = + notificationDao.insertAll(listOf(notification)) +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt new file mode 100644 index 00000000..69d0092c --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt @@ -0,0 +1,145 @@ +package io.github.wulkanowy.services.sync.notifications + +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.content.Context +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Notification +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.pojos.MultipleNotificationsData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.data.pojos.OneNotificationData +import io.github.wulkanowy.data.repositories.NotificationRepository +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.AppInfo +import io.github.wulkanowy.utils.getCompatBitmap +import io.github.wulkanowy.utils.getCompatColor +import io.github.wulkanowy.utils.nickOrName +import java.time.LocalDateTime +import javax.inject.Inject +import kotlin.random.Random + +class AppNotificationManager @Inject constructor( + private val notificationManager: NotificationManagerCompat, + @ApplicationContext private val context: Context, + private val appInfo: AppInfo, + private val notificationRepository: NotificationRepository +) { + + suspend fun sendNotification(notificationData: NotificationData, student: Student) = + when (notificationData) { + is OneNotificationData -> sendOneNotification(notificationData, student) + is MultipleNotificationsData -> sendMultipleNotifications(notificationData, student) + } + + private suspend fun sendOneNotification( + notificationData: OneNotificationData, + student: Student + ) { + val content = context.getString( + notificationData.contentStringRes, + *notificationData.contentValues.toTypedArray() + ) + + val title = context.getString(notificationData.titleStringRes) + + val notification = getDefaultNotificationBuilder(notificationData) + .setContentTitle(title) + .setContentText(content) + .setStyle( + NotificationCompat.BigTextStyle() + .setSummaryText(student.nickOrName) + .bigText(content) + ) + .build() + + notificationManager.notify(Random.nextInt(Int.MAX_VALUE), notification) + + saveNotification(title, content, notificationData, student) + } + + private suspend fun sendMultipleNotifications( + notificationData: MultipleNotificationsData, + student: Student + ) { + val groupType = notificationData.type.group ?: return + val group = "${groupType}_${student.id}" + val groupId = student.id * 100 + notificationData.type.ordinal + + notificationData.lines.forEach { item -> + val title = context.resources.getQuantityString(notificationData.titleStringRes, 1) + + val notification = getDefaultNotificationBuilder(notificationData) + .setContentTitle(title) + .setContentText(item) + .setStyle( + NotificationCompat.BigTextStyle() + .setSummaryText(student.nickOrName) + .bigText(item) + ) + .setGroup(group) + .build() + + notificationManager.notify(Random.nextInt(Int.MAX_VALUE), notification) + + saveNotification(title, item, notificationData, student) + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return + + val summaryNotification = getDefaultNotificationBuilder(notificationData) + .setSmallIcon(notificationData.icon) + .setGroup(group) + .setStyle(NotificationCompat.InboxStyle().setSummaryText(student.nickOrName)) + .setGroupSummary(true) + .build() + + notificationManager.notify(groupId.toInt(), summaryNotification) + } + + @SuppressLint("InlinedApi") + private fun getDefaultNotificationBuilder(notificationData: NotificationData): NotificationCompat.Builder { + val pendingIntentsFlags = if (appInfo.systemVersion >= Build.VERSION_CODES.M) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + + return NotificationCompat.Builder(context, notificationData.type.channel) + .setLargeIcon(context.getCompatBitmap(notificationData.icon, R.color.colorPrimary)) + .setSmallIcon(R.drawable.ic_stat_all) + .setAutoCancel(true) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setColor(context.getCompatColor(R.color.colorPrimary)) + .setContentIntent( + PendingIntent.getActivity( + context, + notificationData.startMenu.id, + MainActivity.getStartIntent(context, notificationData.startMenu, true), + pendingIntentsFlags + ) + ) + } + + private suspend fun saveNotification( + title: String, + content: String, + notificationData: NotificationData, + student: Student + ) { + val notificationEntity = Notification( + studentId = student.id, + title = title, + content = content, + type = notificationData.type, + date = LocalDateTime.now() + ) + + notificationRepository.saveNotification(notificationEntity) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/BaseNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/BaseNotification.kt deleted file mode 100644 index 8c9cb471..00000000 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/BaseNotification.kt +++ /dev/null @@ -1,102 +0,0 @@ -package io.github.wulkanowy.services.sync.notifications - -import android.app.PendingIntent -import android.content.Context -import android.os.Build -import androidx.annotation.PluralsRes -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import io.github.wulkanowy.R -import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications -import io.github.wulkanowy.data.pojos.Notification -import io.github.wulkanowy.data.pojos.OneNotification -import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.utils.getCompatBitmap -import io.github.wulkanowy.utils.getCompatColor -import io.github.wulkanowy.utils.nickOrName -import kotlin.random.Random - -abstract class BaseNotification( - private val context: Context, - private val notificationManager: NotificationManagerCompat, -) { - - protected fun sendNotification(notification: Notification, student: Student) = - when (notification) { - is OneNotification -> sendOneNotification(notification, student) - is MultipleNotifications -> sendMultipleNotifications(notification, student) - } - - private fun sendOneNotification(notification: OneNotification, student: Student?) { - notificationManager.notify( - Random.nextInt(Int.MAX_VALUE), - getNotificationBuilder(notification).apply { - val content = context.getString( - notification.contentStringRes, - *notification.contentValues.toTypedArray() - ) - setContentTitle(context.getString(notification.titleStringRes)) - setContentText(content) - setStyle( - NotificationCompat.BigTextStyle() - .setSummaryText(student?.nickOrName) - .bigText(content) - ) - }.build() - ) - } - - private fun sendMultipleNotifications(notification: MultipleNotifications, student: Student) { - val group = notification.type.group + student.id - val groupId = student.id * 100 + notification.type.ordinal - - notification.lines.forEach { item -> - notificationManager.notify( - Random.nextInt(Int.MAX_VALUE), - getNotificationBuilder(notification).apply { - setContentTitle(getQuantityString(notification.titleStringRes, 1)) - setContentText(item) - setStyle( - NotificationCompat.BigTextStyle() - .setSummaryText(student.nickOrName) - .bigText(item) - ) - setGroup(group) - }.build() - ) - } - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return - - notificationManager.notify( - groupId.toInt(), - getNotificationBuilder(notification).apply { - setSmallIcon(notification.icon) - setGroup(group) - setStyle(NotificationCompat.InboxStyle().setSummaryText(student.nickOrName)) - setGroupSummary(true) - }.build() - ) - } - - private fun getNotificationBuilder(notification: Notification) = NotificationCompat - .Builder(context, notification.type.channel) - .setLargeIcon(context.getCompatBitmap(notification.icon, R.color.colorPrimary)) - .setSmallIcon(R.drawable.ic_stat_all) - .setAutoCancel(true) - .setDefaults(NotificationCompat.DEFAULT_ALL) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setColor(context.getCompatColor(R.color.colorPrimary)) - .setContentIntent( - PendingIntent.getActivity( - context, notification.startMenu.id, - MainActivity.getStartIntent(context, notification.startMenu, true), - PendingIntent.FLAG_UPDATE_CURRENT - ) - ) - - private fun getQuantityString(@PluralsRes id: Int, value: Int): String { - return context.resources.getQuantityString(id, value, value) - } -} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt index fda2922f..994cb8d4 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt @@ -1,29 +1,25 @@ package io.github.wulkanowy.services.sync.notifications -import android.content.Context -import androidx.core.app.NotificationManagerCompat -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications +import io.github.wulkanowy.data.pojos.MultipleNotificationsData import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDateTime import javax.inject.Inject class NewConferenceNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager +) { - fun notify(items: List, student: Student) { + suspend fun notify(items: List, student: Student) { val today = LocalDateTime.now() val lines = items.filter { !it.date.isBefore(today) }.map { "${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}" }.ifEmpty { return } - val notification = MultipleNotifications( + val notification = MultipleNotificationsData( type = NotificationType.NEW_CONFERENCE, icon = R.drawable.ic_more_conferences, titleStringRes = R.plurals.conference_notify_new_item_title, @@ -33,6 +29,6 @@ class NewConferenceNotification @Inject constructor( lines = lines ) - sendNotification(notification, student) + appNotificationManager.sendNotification(notification, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt index d493c4d2..f148fa34 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt @@ -1,29 +1,25 @@ package io.github.wulkanowy.services.sync.notifications -import android.content.Context -import androidx.core.app.NotificationManagerCompat -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications +import io.github.wulkanowy.data.pojos.MultipleNotificationsData import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate import javax.inject.Inject class NewExamNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager +) { - fun notify(items: List, student: Student) { + suspend fun notify(items: List, student: Student) { val today = LocalDate.now() val lines = items.filter { !it.date.isBefore(today) }.map { "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}" }.ifEmpty { return } - val notification = MultipleNotifications( + val notification = MultipleNotificationsData( type = NotificationType.NEW_EXAM, icon = R.drawable.ic_main_exam, titleStringRes = R.plurals.exam_notify_new_item_title, @@ -33,6 +29,6 @@ class NewExamNotification @Inject constructor( lines = lines ) - sendNotification(notification, student) + appNotificationManager.sendNotification(notification, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt index 415ba343..52bdff58 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt @@ -1,23 +1,19 @@ package io.github.wulkanowy.services.sync.notifications -import android.content.Context -import androidx.core.app.NotificationManagerCompat -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications +import io.github.wulkanowy.data.pojos.MultipleNotificationsData import io.github.wulkanowy.ui.modules.main.MainView import javax.inject.Inject class NewGradeNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager +) { - fun notifyDetails(items: List, student: Student) { - val notification = MultipleNotifications( + suspend fun notifyDetails(items: List, student: Student) { + val notification = MultipleNotificationsData( type = NotificationType.NEW_GRADE_DETAILS, icon = R.drawable.ic_stat_grade, titleStringRes = R.plurals.grade_new_items, @@ -29,11 +25,11 @@ class NewGradeNotification @Inject constructor( } ) - sendNotification(notification, student) + appNotificationManager.sendNotification(notification, student) } - fun notifyPredicted(items: List, student: Student) { - val notification = MultipleNotifications( + suspend fun notifyPredicted(items: List, student: Student) { + val notification = MultipleNotificationsData( type = NotificationType.NEW_GRADE_PREDICTED, icon = R.drawable.ic_stat_grade, titleStringRes = R.plurals.grade_new_items_predicted, @@ -45,11 +41,11 @@ class NewGradeNotification @Inject constructor( } ) - sendNotification(notification, student) + appNotificationManager.sendNotification(notification, student) } - fun notifyFinal(items: List, student: Student) { - val notification = MultipleNotifications( + suspend fun notifyFinal(items: List, student: Student) { + val notification = MultipleNotificationsData( type = NotificationType.NEW_GRADE_FINAL, icon = R.drawable.ic_stat_grade, titleStringRes = R.plurals.grade_new_items_final, @@ -61,6 +57,6 @@ class NewGradeNotification @Inject constructor( } ) - sendNotification(notification, student) + appNotificationManager.sendNotification(notification, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt index fe973cad..4c34cb8f 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt @@ -1,29 +1,25 @@ package io.github.wulkanowy.services.sync.notifications -import android.content.Context -import androidx.core.app.NotificationManagerCompat -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications +import io.github.wulkanowy.data.pojos.MultipleNotificationsData import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate import javax.inject.Inject class NewHomeworkNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager +) { - fun notify(items: List, student: Student) { + suspend fun notify(items: List, student: Student) { val today = LocalDate.now() val lines = items.filter { !it.date.isBefore(today) }.map { "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}" }.ifEmpty { return } - val notification = MultipleNotifications( + val notification = MultipleNotificationsData( type = NotificationType.NEW_HOMEWORK, icon = R.drawable.ic_more_homework, titleStringRes = R.plurals.homework_notify_new_item_title, @@ -33,6 +29,6 @@ class NewHomeworkNotification @Inject constructor( lines = lines ) - sendNotification(notification, student) + appNotificationManager.sendNotification(notification, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt index 95156c45..08c98510 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt @@ -1,30 +1,26 @@ package io.github.wulkanowy.services.sync.notifications -import android.content.Context -import androidx.core.app.NotificationManagerCompat -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.OneNotification +import io.github.wulkanowy.data.pojos.OneNotificationData import io.github.wulkanowy.ui.modules.main.MainView import javax.inject.Inject class NewLuckyNumberNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager +) { - fun notify(item: LuckyNumber, student: Student) { - val notification = OneNotification( - type = NotificationType.NEW_LUCKY_NUMBER, - icon = R.drawable.ic_stat_luckynumber, - titleStringRes = R.string.lucky_number_notify_new_item_title, - contentStringRes = R.string.lucky_number_notify_new_item, - startMenu = MainView.Section.LUCKY_NUMBER, - contentValues = listOf(item.luckyNumber.toString()) - ) + suspend fun notify(item: LuckyNumber, student: Student) { + val notification = OneNotificationData( + type = NotificationType.NEW_LUCKY_NUMBER, + icon = R.drawable.ic_stat_luckynumber, + titleStringRes = R.string.lucky_number_notify_new_item_title, + contentStringRes = R.string.lucky_number_notify_new_item, + startMenu = MainView.Section.LUCKY_NUMBER, + contentValues = listOf(item.luckyNumber.toString()) + ) - sendNotification(notification, student) + appNotificationManager.sendNotification(notification, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt index fc364198..a6d503aa 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt @@ -1,22 +1,18 @@ package io.github.wulkanowy.services.sync.notifications -import android.content.Context -import androidx.core.app.NotificationManagerCompat -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications +import io.github.wulkanowy.data.pojos.MultipleNotificationsData import io.github.wulkanowy.ui.modules.main.MainView import javax.inject.Inject class NewMessageNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager +) { - fun notify(items: List, student: Student) { - val notification = MultipleNotifications( + suspend fun notify(items: List, student: Student) { + val notification = MultipleNotificationsData( type = NotificationType.NEW_MESSAGE, icon = R.drawable.ic_stat_message, titleStringRes = R.plurals.message_new_items, @@ -28,6 +24,6 @@ class NewMessageNotification @Inject constructor( } ) - sendNotification(notification, student) + appNotificationManager.sendNotification(notification, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt index f355341b..ffa3cc9c 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt @@ -1,23 +1,19 @@ package io.github.wulkanowy.services.sync.notifications -import android.content.Context -import androidx.core.app.NotificationManagerCompat -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications +import io.github.wulkanowy.data.pojos.MultipleNotificationsData import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory import io.github.wulkanowy.ui.modules.main.MainView import javax.inject.Inject class NewNoteNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager +) { - fun notify(items: List, student: Student) { - val notification = MultipleNotifications( + suspend fun notify(items: List, student: Student) { + val notification = MultipleNotificationsData( type = NotificationType.NEW_NOTE, icon = R.drawable.ic_stat_note, titleStringRes = when (NoteCategory.getByValue(items.first().categoryType)) { @@ -41,6 +37,6 @@ class NewNoteNotification @Inject constructor( } ) - sendNotification(notification, student) + appNotificationManager.sendNotification(notification, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt index b38e4f60..990a950b 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt @@ -1,33 +1,29 @@ package io.github.wulkanowy.services.sync.notifications -import android.content.Context -import androidx.core.app.NotificationManagerCompat -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications +import io.github.wulkanowy.data.pojos.MultipleNotificationsData import io.github.wulkanowy.ui.modules.main.MainView import javax.inject.Inject class NewSchoolAnnouncementNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager +) { - fun notify(items: List, student: Student) { - val notification = MultipleNotifications( - type = NotificationType.NEW_ANNOUNCEMENT, - icon = R.drawable.ic_all_about, - titleStringRes = R.plurals.school_announcement_notify_new_item_title, - contentStringRes = R.plurals.school_announcement_notify_new_items, - summaryStringRes = R.plurals.school_announcement_number_item, - startMenu = MainView.Section.SCHOOL_ANNOUNCEMENT, - lines = items.map { - "${it.subject}: ${it.content}" - } + suspend fun notify(items: List, student: Student) { + val notification = MultipleNotificationsData( + type = NotificationType.NEW_ANNOUNCEMENT, + icon = R.drawable.ic_all_about, + titleStringRes = R.plurals.school_announcement_notify_new_item_title, + contentStringRes = R.plurals.school_announcement_notify_new_items, + summaryStringRes = R.plurals.school_announcement_number_item, + startMenu = MainView.Section.SCHOOL_ANNOUNCEMENT, + lines = items.map { + "${it.subject}: ${it.content}" + } ) - sendNotification(notification, student) + appNotificationManager.sendNotification(notification, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt index c3df1960..49cbcfe9 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt @@ -8,8 +8,9 @@ import io.github.wulkanowy.services.sync.channels.NewHomeworkChannel import io.github.wulkanowy.services.sync.channels.NewMessagesChannel import io.github.wulkanowy.services.sync.channels.NewNotesChannel import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel +import io.github.wulkanowy.services.sync.channels.PushChannel -enum class NotificationType(val group: String, val channel: String) { +enum class NotificationType(val group: String?, val channel: String) { NEW_CONFERENCE("new_conferences_group", NewConferencesChannel.CHANNEL_ID), NEW_EXAM("new_exam_group", NewExamChannel.CHANNEL_ID), NEW_GRADE_DETAILS("new_grade_details_group", NewGradesChannel.CHANNEL_ID), @@ -20,4 +21,5 @@ enum class NotificationType(val group: String, val channel: String) { NEW_MESSAGE("new_message_group", NewMessagesChannel.CHANNEL_ID), NEW_NOTE("new_notes_group", NewNotesChannel.CHANNEL_ID), NEW_ANNOUNCEMENT("new_school_announcements_group", NewSchoolAnnouncementsChannel.CHANNEL_ID), + PUSH(null, PushChannel.CHANNEL_ID) } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt index 788e4ea2..4823b2b5 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt @@ -9,9 +9,12 @@ import io.github.wulkanowy.utils.waitForResult import java.time.LocalDate.now import javax.inject.Inject -class AttendanceWork @Inject constructor(private val attendanceRepository: AttendanceRepository) : Work { +class AttendanceWork @Inject constructor( + private val attendanceRepository: AttendanceRepository +) : Work { override suspend fun doWork(student: Student, semester: Semester) { - attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true).waitForResult() + attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true) + .waitForResult() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt index 59d9c8e4..096c1b77 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt @@ -24,6 +24,7 @@ import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.message.MessageFragment +import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.utils.capitalise @@ -120,6 +121,7 @@ class DashboardFragment : BaseFragment(R.layout.fragme override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.dashboard_menu_tiles -> presenter.onDashboardTileSettingsSelected() + R.id.dashboard_menu_notifaction_list -> presenter.onNotificationsCenterSelected() else -> false } } @@ -182,6 +184,10 @@ class DashboardFragment : BaseFragment(R.layout.fragme if (::presenter.isInitialized) presenter.onViewReselected() } + override fun openNotificationsCenterView() { + (requireActivity() as MainActivity).pushView(NotificationsCenterFragment.newInstance()) + } + override fun onDestroyView() { dashboardAdapter.clearTimers() presenter.onDetachView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index 108a086b..c513a9f6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -209,6 +209,11 @@ class DashboardPresenter @Inject constructor( view?.showErrorDetailsDialog(lastError) } + fun onNotificationsCenterSelected(): Boolean { + view?.openNotificationsCenterView() + return true + } + fun onDashboardTileSettingsSelected(): Boolean { view?.showDashboardTileSettings(preferencesRepository.selectedDashboardTiles.toList()) return true diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt index d5c5e5a7..899eb320 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt @@ -23,4 +23,6 @@ interface DashboardView : BaseView { fun resetView() fun popViewToRoot() + + fun openNotificationsCenterView() } \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt index 07468daa..6103317d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt @@ -87,7 +87,7 @@ class NotificationDebugPresenter @Inject constructor( } } - private fun withStudent(block: (Student) -> Unit) { + private fun withStudent(block: suspend (Student) -> Unit) { launch { block(studentRepository.getCurrentStudent(false)) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt new file mode 100644 index 00000000..7ee326f8 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt @@ -0,0 +1,62 @@ +package io.github.wulkanowy.ui.modules.notificationscenter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Notification +import io.github.wulkanowy.databinding.ItemNotificationsCenterBinding +import io.github.wulkanowy.services.sync.notifications.NotificationType +import io.github.wulkanowy.utils.toFormattedString +import javax.inject.Inject + +class NotificationsCenterAdapter @Inject constructor() : + ListAdapter(DiffUtilCallback()) { + + var onItemClickListener: (NotificationType) -> Unit = {} + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + ItemNotificationsCenterBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = getItem(position) + + with(holder.binding) { + notificationsCenterItemTitle.text = item.title + notificationsCenterItemContent.text = item.content + notificationsCenterItemDate.text = item.date.toFormattedString("HH:mm, d MMM") + notificationsCenterItemIcon.setImageResource(item.type.toDrawableResId()) + + root.setOnClickListener { onItemClickListener(item.type) } + } + } + + private fun NotificationType.toDrawableResId() = when (this) { + NotificationType.NEW_CONFERENCE -> R.drawable.ic_more_conferences + NotificationType.NEW_EXAM -> R.drawable.ic_main_exam + NotificationType.NEW_GRADE_DETAILS -> R.drawable.ic_stat_grade + NotificationType.NEW_GRADE_PREDICTED -> R.drawable.ic_stat_grade + NotificationType.NEW_GRADE_FINAL -> R.drawable.ic_stat_grade + NotificationType.NEW_HOMEWORK -> R.drawable.ic_more_homework + NotificationType.NEW_LUCKY_NUMBER -> R.drawable.ic_stat_luckynumber + NotificationType.NEW_MESSAGE -> R.drawable.ic_stat_message + NotificationType.NEW_NOTE -> R.drawable.ic_stat_note + NotificationType.NEW_ANNOUNCEMENT -> R.drawable.ic_all_about + NotificationType.PUSH -> R.drawable.ic_stat_all + } + + class ViewHolder(val binding: ItemNotificationsCenterBinding) : + RecyclerView.ViewHolder(binding.root) + + private class DiffUtilCallback : DiffUtil.ItemCallback() { + + override fun areContentsTheSame(oldItem: Notification, newItem: Notification) = + oldItem == newItem + + override fun areItemsTheSame(oldItem: Notification, newItem: Notification) = + oldItem.id == newItem.id + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt new file mode 100644 index 00000000..b9bfb447 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt @@ -0,0 +1,108 @@ +package io.github.wulkanowy.ui.modules.notificationscenter + +import android.os.Bundle +import android.view.View +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Notification +import io.github.wulkanowy.databinding.FragmentNotificationsCenterBinding +import io.github.wulkanowy.services.sync.notifications.NotificationType +import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.conference.ConferenceFragment +import io.github.wulkanowy.ui.modules.exam.ExamFragment +import io.github.wulkanowy.ui.modules.grade.GradeFragment +import io.github.wulkanowy.ui.modules.homework.HomeworkFragment +import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.modules.message.MessageFragment +import io.github.wulkanowy.ui.modules.note.NoteFragment +import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment +import javax.inject.Inject + +@AndroidEntryPoint +class NotificationsCenterFragment : + BaseFragment(R.layout.fragment_notifications_center), + NotificationsCenterView, MainView.TitledView { + + @Inject + lateinit var presenter: NotificationsCenterPresenter + + @Inject + lateinit var notificationsCenterAdapter: NotificationsCenterAdapter + + companion object { + + fun newInstance() = NotificationsCenterFragment() + } + + override val titleStringId: Int + get() = R.string.notifications_center_title + + override val isViewEmpty: Boolean + get() = notificationsCenterAdapter.itemCount == 0 + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentNotificationsCenterBinding.bind(view) + presenter.onAttachView(this) + } + + override fun initView() { + notificationsCenterAdapter.onItemClickListener = { notificationType -> + notificationType.toDestinationFragment() + ?.let { (requireActivity() as MainActivity).pushView(it) } + } + + with(binding.notificationsCenterRecycler) { + layoutManager = LinearLayoutManager(context) + adapter = notificationsCenterAdapter + } + } + + override fun updateData(data: List) { + notificationsCenterAdapter.submitList(data) + } + + override fun showEmpty(show: Boolean) { + binding.notificationsCenterEmpty.isVisible = show + } + + override fun showProgress(show: Boolean) { + binding.notificationsCenterProgress.isVisible = show + } + + override fun showContent(show: Boolean) { + binding.notificationsCenterRecycler.isVisible = show + } + + override fun showErrorView(show: Boolean) { + binding.notificationCenterError.isVisible = show + } + + override fun setErrorDetails(message: String) { + binding.notificationCenterErrorMessage.text = message + } + + override fun onDestroyView() { + presenter.onDetachView() + super.onDestroyView() + } + + private fun NotificationType.toDestinationFragment(): Fragment? = when (this) { + NotificationType.NEW_CONFERENCE -> ConferenceFragment.newInstance() + NotificationType.NEW_EXAM -> ExamFragment.newInstance() + NotificationType.NEW_GRADE_DETAILS -> GradeFragment.newInstance() + NotificationType.NEW_GRADE_PREDICTED -> GradeFragment.newInstance() + NotificationType.NEW_GRADE_FINAL -> GradeFragment.newInstance() + NotificationType.NEW_HOMEWORK -> HomeworkFragment.newInstance() + NotificationType.NEW_LUCKY_NUMBER -> LuckyNumberFragment.newInstance() + NotificationType.NEW_MESSAGE -> MessageFragment.newInstance() + NotificationType.NEW_NOTE -> NoteFragment.newInstance() + NotificationType.NEW_ANNOUNCEMENT -> SchoolAnnouncementFragment.newInstance() + NotificationType.PUSH -> null + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterPresenter.kt new file mode 100644 index 00000000..394c2310 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterPresenter.kt @@ -0,0 +1,83 @@ +package io.github.wulkanowy.ui.modules.notificationscenter + +import io.github.wulkanowy.data.repositories.NotificationRepository +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.ErrorHandler +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import timber.log.Timber +import javax.inject.Inject + +class NotificationsCenterPresenter @Inject constructor( + private val notificationRepository: NotificationRepository, + errorHandler: ErrorHandler, + studentRepository: StudentRepository +) : BasePresenter(errorHandler, studentRepository) { + + private lateinit var lastError: Throwable + + override fun onAttachView(view: NotificationsCenterView) { + super.onAttachView(view) + view.initView() + Timber.i("Notifications centre view was initialized") + errorHandler.showErrorMessage = ::showErrorViewOnError + loadData() + } + + fun onRetry() { + view?.run { + showErrorView(false) + showProgress(true) + } + loadData() + } + + fun onDetailsClick() { + view?.showErrorDetailsDialog(lastError) + } + + private fun loadData() { + Timber.i("Loading notifications data started") + + flow { + val studentId = studentRepository.getCurrentStudent(false).id + emitAll(notificationRepository.getNotifications(studentId)) + } + .map { notificationList -> notificationList.sortedByDescending { it.date } } + .catch { Timber.i("Loading notifications result: An exception occurred") } + .onEach { + Timber.i("Loading notifications result: Success") + + if (it.isEmpty()) { + view?.run { + showContent(false) + showProgress(false) + showEmpty(true) + } + } else { + view?.run { + showContent(true) + showProgress(false) + showEmpty(false) + updateData(it) + } + } + } + .launch() + } + + private fun showErrorViewOnError(message: String, error: Throwable) { + view?.run { + if (isViewEmpty) { + lastError = error + setErrorDetails(message) + showErrorView(true) + showEmpty(false) + } else showError(message, error) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterView.kt new file mode 100644 index 00000000..1bfbe75e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterView.kt @@ -0,0 +1,23 @@ +package io.github.wulkanowy.ui.modules.notificationscenter + +import io.github.wulkanowy.data.db.entities.Notification +import io.github.wulkanowy.ui.base.BaseView + +interface NotificationsCenterView : BaseView { + + val isViewEmpty: Boolean + + fun initView() + + fun updateData(data: List) + + fun showProgress(show: Boolean) + + fun showEmpty(show: Boolean) + + fun showContent(show: Boolean) + + fun showErrorView(show: Boolean) + + fun setErrorDetails(message: String) +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt index 207d587d..8470d1a5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt @@ -184,6 +184,7 @@ class NotificationsFragment : PreferenceFragmentCompat(), .setNegativeButton(android.R.string.cancel) { _, _ -> setNotificationPiggybackPreferenceChecked(false) } + .setOnDismissListener { setNotificationPiggybackPreferenceChecked(false) } .show() } diff --git a/app/src/main/res/layout/fragment_notifications_center.xml b/app/src/main/res/layout/fragment_notifications_center.xml new file mode 100644 index 00000000..f59ce33c --- /dev/null +++ b/app/src/main/res/layout/fragment_notifications_center.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_notifications_center.xml b/app/src/main/res/layout/item_notifications_center.xml new file mode 100644 index 00000000..ae3f5586 --- /dev/null +++ b/app/src/main/res/layout/item_notifications_center.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/action_menu_dashboard.xml b/app/src/main/res/menu/action_menu_dashboard.xml index dbdd6e81..13565a19 100644 --- a/app/src/main/res/menu/action_menu_dashboard.xml +++ b/app/src/main/res/menu/action_menu_dashboard.xml @@ -1,10 +1,17 @@ + diff --git a/app/src/main/res/menu/action_menu_main.xml b/app/src/main/res/menu/action_menu_main.xml index 21905939..f14d1f74 100644 --- a/app/src/main/res/menu/action_menu_main.xml +++ b/app/src/main/res/menu/action_menu_main.xml @@ -5,7 +5,7 @@ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ed236932..ae11d493 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,6 +24,7 @@ Account details Student info Dashboard + Notifications center diff --git a/app/src/play/java/io/github/wulkanowy/services/messaging/AppMessagingService.kt b/app/src/play/java/io/github/wulkanowy/services/messaging/AppMessagingService.kt new file mode 100644 index 00000000..6e5c7e89 --- /dev/null +++ b/app/src/play/java/io/github/wulkanowy/services/messaging/AppMessagingService.kt @@ -0,0 +1,57 @@ +package io.github.wulkanowy.services.messaging + +import android.annotation.SuppressLint +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.data.db.entities.Notification +import io.github.wulkanowy.data.repositories.NotificationRepository +import io.github.wulkanowy.services.sync.notifications.NotificationType +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import timber.log.Timber +import java.time.LocalDateTime +import javax.inject.Inject + +@SuppressLint("MissingFirebaseInstanceTokenRefresh") +@AndroidEntryPoint +class AppMessagingService : FirebaseMessagingService() { + + @Inject + lateinit var notificationRepository: NotificationRepository + + private val job = Job() + + private val serviceScope = CoroutineScope(Dispatchers.Main + job) + + override fun onMessageReceived(remoteMessage: RemoteMessage) { + val remoteMessageData = remoteMessage.data + val title = remoteMessageData["title"] ?: return + val content = remoteMessageData["content"] ?: return + val customData = remoteMessageData["custom_data"] + + val notification = Notification( + title = title, + content = content, + data = customData, + date = LocalDateTime.now(), + type = NotificationType.PUSH, + studentId = -1 + ) + + serviceScope.launch { + try { + notificationRepository.saveNotification(notification) + } catch (e: Throwable) { + Timber.e(e) + } + } + } + + override fun onDestroy() { + job.cancel() + super.onDestroy() + } +} \ No newline at end of file From e10e530dee7972dbe8e58993e15c904c444259a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 27 Sep 2021 23:03:59 +0200 Subject: [PATCH 021/357] Remove seconds from timetable timer (#1539) --- .../ui/modules/timetable/TimetableAdapter.kt | 41 +++++++------------ 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt index 4a5a0699..2228aaf4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt @@ -1,12 +1,13 @@ package io.github.wulkanowy.ui.modules.timetable import android.graphics.Paint +import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup import android.widget.TextView -import androidx.core.view.ViewCompat import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.R @@ -151,8 +152,8 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter Date: Tue, 28 Sep 2021 11:48:25 +0200 Subject: [PATCH 022/357] Add option to make upcoming lesson notification not persistent (#1537) --- .../repositories/PreferencesRepository.kt | 8 ++ .../alarm/TimetableNotificationReceiver.kt | 77 +++++++++++++------ .../notifications/NotificationsPresenter.kt | 2 +- .../main/res/values/preferences_defaults.xml | 1 + app/src/main/res/values/preferences_keys.xml | 1 + app/src/main/res/values/strings.xml | 2 + .../xml/scheme_preferences_notifications.xml | 8 ++ 7 files changed, 74 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index f0f1c32d..a08045f8 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -107,6 +107,14 @@ class PreferencesRepository @Inject constructor( R.bool.pref_default_notification_upcoming_lessons_enable ) + val isUpcomingLessonsNotificationsPersistentKey = + context.getString(R.string.pref_key_notifications_upcoming_lessons_persistent) + val isUpcomingLessonsNotificationsPersistent: Boolean + get() = getBoolean( + isUpcomingLessonsNotificationsPersistentKey, + R.bool.pref_default_notification_upcoming_lessons_persistent + ) + val isNotificationPiggybackEnabledKey = context.getString(R.string.pref_key_notifications_piggyback) val isNotificationPiggybackEnabled: Boolean diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt index 406d91f5..5e4bad8c 100644 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt @@ -11,6 +11,7 @@ import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.HiltBroadcastReceiver import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel.Companion.CHANNEL_ID @@ -32,6 +33,9 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { @Inject lateinit var studentRepository: StudentRepository + @Inject + lateinit var preferencesRepository: PreferencesRepository + companion object { const val NOTIFICATION_TYPE_CURRENT = 1 const val NOTIFICATION_TYPE_UPCOMING = 2 @@ -68,6 +72,7 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { private fun prepareNotification(context: Context, intent: Intent) { val type = intent.getIntExtra(LESSON_TYPE, 0) val notificationId = intent.getIntExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id) + val isPersistent = preferencesRepository.isUpcomingLessonsNotificationsPersistent if (type == NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION) { return NotificationManagerCompat.from(context).cancel(notificationId) @@ -87,33 +92,57 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { Timber.d("TimetableNotification receive: type: $type, subject: $subject, start: ${start.toLocalDateTime()}, student: $studentId") - showNotification(context, notificationId, studentName, + showNotification(context, notificationId, isPersistent, studentName, if (type == NOTIFICATION_TYPE_CURRENT) end else start, end - start, - context.getString(if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next, "($room) $subject".removePrefix("()")), - nextSubject?.let { context.getString(R.string.timetable_later, "($nextRoom) $nextSubject".removePrefix("()")) } + context.getString( + if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next, + "($room) $subject".removePrefix("()") + ), + nextSubject?.let { + context.getString( + R.string.timetable_later, + "($nextRoom) $nextSubject".removePrefix("()") + ) + } ) } - private fun showNotification(context: Context, notificationId: Int, studentName: String?, countDown: Long, timeout: Long, title: String, next: String?) { - NotificationManagerCompat.from(context).notify(notificationId, NotificationCompat.Builder(context, CHANNEL_ID) - .setContentTitle(title) - .setContentText(next) - .setAutoCancel(false) - .setOngoing(true) - .setWhen(countDown) - .apply { - if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true) - } - .setTimeoutAfter(timeout) - .setSmallIcon(R.drawable.ic_stat_timetable) - .setColor(context.getCompatColor(R.color.colorPrimary)) - .setStyle(NotificationCompat.InboxStyle().also { - it.setSummaryText(studentName) - it.addLine(next) - }) - .setContentIntent(PendingIntent.getActivity(context, MainView.Section.TIMETABLE.id, - MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), FLAG_UPDATE_CURRENT)) - .build() - ) + private fun showNotification( + context: Context, + notificationId: Int, + isPersistent: Boolean, + studentName: String?, + countDown: Long, + timeout: Long, + title: String, + next: String? + ) { + NotificationManagerCompat.from(context) + .notify(notificationId, NotificationCompat.Builder(context, CHANNEL_ID) + .setContentTitle(title) + .setContentText(next) + .setAutoCancel(false) + .setWhen(countDown) + .setOngoing(isPersistent) + .apply { + if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true) + } + .setTimeoutAfter(timeout) + .setSmallIcon(R.drawable.ic_stat_timetable) + .setColor(context.getCompatColor(R.color.colorPrimary)) + .setStyle(NotificationCompat.InboxStyle().also { + it.setSummaryText(studentName) + it.addLine(next) + }) + .setContentIntent( + PendingIntent.getActivity( + context, + MainView.Section.TIMETABLE.id, + MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), + FLAG_UPDATE_CURRENT + ) + ) + .build() + ) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt index 19d2f559..722ee96d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt @@ -42,7 +42,7 @@ class NotificationsPresenter @Inject constructor( preferencesRepository.apply { when (key) { - isUpcomingLessonsNotificationsEnableKey -> { + isUpcomingLessonsNotificationsEnableKey, isUpcomingLessonsNotificationsPersistentKey -> { if (!isUpcomingLessonsNotificationsEnable) { timetableNotificationHelper.cancelNotification() } diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index 9286052d..df84d37d 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -14,6 +14,7 @@ false true false + true false 0.33 0.33 diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index bae6a617..c512a5f2 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -18,6 +18,7 @@ notifications_system_settings notifications_enable notifications_upcoming_lessons_enable + notifications_upcoming_lessons_persistent notification_debug grade_modifier_plus grade_modifier_minus diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ae11d493..63d4fc42 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -621,6 +621,8 @@ Notifications Show notifications Show upcoming lesson notifications + Make upcoming lesson notification persistent + Turn off when notification is not showing in your watch/band Open system notification settings Fix synchronization & notifications issues Your device may have data synchronization issues and with notifications.\n\nTo fix them, you need to add Wulkanowy to the autostart and turn off battery optimization/saving in the phone settings. diff --git a/app/src/main/res/xml/scheme_preferences_notifications.xml b/app/src/main/res/xml/scheme_preferences_notifications.xml index 24b83169..78e91cf0 100644 --- a/app/src/main/res/xml/scheme_preferences_notifications.xml +++ b/app/src/main/res/xml/scheme_preferences_notifications.xml @@ -14,6 +14,14 @@ app:key="@string/pref_key_notifications_upcoming_lessons_enable" app:singleLineTitle="false" app:title="@string/pref_notify_upcoming_lessons_switch" /> + Date: Tue, 28 Sep 2021 21:55:40 +0200 Subject: [PATCH 023/357] Update License (#1542) --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 5dd9cacf..2fb96cee 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2019 Wulkanowy + Copyright 2021 Wulkanowy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From f8cb7599e6f82eaebf2d62470e9736307426b8ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 28 Sep 2021 22:40:43 +0200 Subject: [PATCH 024/357] Add missing auto refresh to recipients, subjects and teachers (#1540) --- app/build.gradle | 2 +- .../data/repositories/AttendanceRepository.kt | 30 ++++++++--- .../AttendanceSummaryRepository.kt | 6 +-- .../CompletedLessonsRepository.kt | 24 +++++++-- .../data/repositories/ConferenceRepository.kt | 6 +-- .../data/repositories/ExamRepository.kt | 6 +-- .../data/repositories/GradeRepository.kt | 14 +++-- .../repositories/GradeStatisticsRepository.kt | 54 +++++++++++++++---- .../data/repositories/HomeworkRepository.kt | 13 +++-- .../repositories/LuckyNumberRepository.kt | 20 ++++--- .../data/repositories/MessageRepository.kt | 25 ++++++--- .../repositories/MobileDeviceRepository.kt | 11 +++- .../data/repositories/NoteRepository.kt | 15 ++++-- .../repositories/NotificationRepository.kt | 6 ++- .../data/repositories/RecipientRepository.kt | 20 +++++-- .../data/repositories/RecoverRepository.kt | 6 +-- .../SchoolAnnouncementRepository.kt | 16 +++--- .../data/repositories/SchoolRepository.kt | 52 +++++++++++------- .../repositories/StudentInfoRepository.kt | 39 +++++++------- .../data/repositories/SubjectRepository.kt | 20 +++++-- .../data/repositories/TeacherRepository.kt | 20 +++++-- .../data/repositories/TimetableRepository.kt | 21 +++++--- .../io/github/wulkanowy/utils/RefreshUtils.kt | 2 +- app/src/main/res/values/api_hosts.xml | 2 +- .../repositories/AttendanceRepositoryTest.kt | 2 +- .../CompletedLessonsRepositoryTest.kt | 2 +- .../data/repositories/ExamRemoteTest.kt | 2 +- .../data/repositories/GradeRepositoryTest.kt | 2 +- .../GradeStatisticsRepositoryTest.kt | 2 +- .../repositories/MessageRepositoryTest.kt | 2 +- .../MobileDeviceRepositoryTest.kt | 2 +- .../data/repositories/RecipientLocalTest.kt | 8 ++- .../repositories/TimetableRepositoryTest.kt | 2 +- 33 files changed, 312 insertions(+), 142 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 8414ff5d..e8680fe2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -166,7 +166,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:49c2071d10" + implementation "io.github.wulkanowy:sdk:f62736adb0" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' 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 ffccb059..d21ffb5f 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 @@ -32,10 +32,23 @@ class AttendanceRepository @Inject constructor( private val cacheKey = "attendance" - fun getAttendance(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( + fun getAttendance( + student: Student, + semester: Semester, + start: LocalDate, + end: LocalDate, + forceRefresh: Boolean, + ) = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, - query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed( + key = getRefreshKey(cacheKey, semester, start, end) + ) + it.isEmpty() || forceRefresh || isExpired + }, + query = { + attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) + }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) .getAttendance(start.monday, end.sunday, semester.semesterId) @@ -50,12 +63,17 @@ class AttendanceRepository @Inject constructor( filterResult = { it.filter { item -> item.date in start..end } } ) - suspend fun excuseForAbsence(student: Student, semester: Semester, absenceList: List, reason: String? = null) { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).excuseForAbsence(absenceList.map { attendance -> + suspend fun excuseForAbsence( + student: Student, semester: Semester, + absenceList: List, reason: String? = null + ) { + val items = absenceList.map { attendance -> Absent( date = LocalDateTime.of(attendance.date, LocalTime.of(0, 0)), timeId = attendance.timeId ) - }, reason) + } + sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) + .excuseForAbsence(items, reason) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt index 58659914..bc1fb234 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt @@ -29,12 +29,12 @@ class AttendanceSummaryRepository @Inject constructor( student: Student, semester: Semester, subjectId: Int, - forceRefresh: Boolean + forceRefresh: Boolean, ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { - it.isEmpty() || forceRefresh - || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) + it.isEmpty() || forceRefresh || isExpired }, query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) }, fetch = { 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 index 99ef56f4..c2e5a721 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt @@ -28,10 +28,28 @@ class CompletedLessonsRepository @Inject constructor( private val cacheKey = "completed" - fun getCompletedLessons(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( + fun getCompletedLessons( + student: Student, + semester: Semester, + start: LocalDate, + end: LocalDate, + forceRefresh: Boolean, + ) = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, - query = { completedLessonsDb.loadAll(semester.studentId, semester.diaryId, start.monday, end.sunday) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed( + key = getRefreshKey(cacheKey, semester, start, end) + ) + it.isEmpty() || forceRefresh || isExpired + }, + query = { + completedLessonsDb.loadAll( + studentId = semester.studentId, + diaryId = semester.diaryId, + from = start.monday, + end = end.sunday + ) + }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) .getCompletedLessons(start.monday, end.sunday) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt index 16d7c3c6..e3227183 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt @@ -35,12 +35,12 @@ class ConferenceRepository @Inject constructor( semester: Semester, forceRefresh: Boolean, notify: Boolean = false, - startDate: LocalDateTime = LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC) + startDate: LocalDateTime = LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC), ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { - it.isEmpty() || forceRefresh - || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) + it.isEmpty() || forceRefresh || isExpired }, query = { conferenceDb.loadAll(semester.diaryId, student.studentId, startDate) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt index 93d5a47c..9bdac065 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt @@ -36,14 +36,14 @@ class ExamRepository @Inject constructor( start: LocalDate, end: LocalDate, forceRefresh: Boolean, - notify: Boolean = false + notify: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { - val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed( + val isExpired = refreshHelper.shouldBeRefreshed( key = getRefreshKey(cacheKey, semester, start, end) ) - it.isEmpty() || forceRefresh || isShouldBeRefreshed + it.isEmpty() || forceRefresh || isExpired }, query = { examDb.loadAll( diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt index d8417f8a..6c574b48 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt @@ -37,13 +37,12 @@ class GradeRepository @Inject constructor( student: Student, semester: Semester, forceRefresh: Boolean, - notify: Boolean = false + notify: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { (details, summaries) -> - val isShouldBeRefreshed = - refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) - details.isEmpty() || summaries.isEmpty() || forceRefresh || isShouldBeRefreshed + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) + details.isEmpty() || summaries.isEmpty() || forceRefresh || isExpired }, query = { val detailsFlow = gradeDb.loadAll(semester.semesterId, semester.studentId) @@ -71,8 +70,8 @@ class GradeRepository @Inject constructor( newDetails: List, notify: Boolean ) { - val notifyBreakDate = - oldGrades.maxByOrNull { it.date }?.date ?: student.registrationDate.toLocalDate() + val notifyBreakDate = oldGrades.maxByOrNull {it.date } + ?.date ?: student.registrationDate.toLocalDate() gradeDb.deleteAll(oldGrades uniqueSubtract newDetails) gradeDb.insertAll((newDetails uniqueSubtract oldGrades).onEach { if (it.date >= notifyBreakDate) it.apply { @@ -89,8 +88,7 @@ class GradeRepository @Inject constructor( ) { gradeSummaryDb.deleteAll(oldSummaries uniqueSubtract newSummary) gradeSummaryDb.insertAll((newSummary uniqueSubtract oldSummaries).onEach { summary -> - val oldSummary = - oldSummaries.find { oldSummary -> oldSummary.subject == summary.subject } + val oldSummary = oldSummaries.find { old -> old.subject == summary.subject } summary.isPredictedGradeNotified = when { summary.predictedGrade.isEmpty() -> true notify && oldSummary?.predictedGrade != summary.predictedGrade -> false diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt index 9cd8e711..6c36f163 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt @@ -39,9 +39,19 @@ class GradeStatisticsRepository @Inject constructor( private val semesterCacheKey = "grade_stats_semester" private val pointsCacheKey = "grade_stats_points" - fun getGradesPartialStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource( + fun getGradesPartialStatistics( + student: Student, + semester: Semester, + subjectName: String, + forceRefresh: Boolean, + ) = networkBoundResource( mutex = partialMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(partialCacheKey, semester)) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed( + key = getRefreshKey(partialCacheKey, semester) + ) + it.isEmpty() || forceRefresh || isExpired + }, query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) @@ -76,9 +86,19 @@ class GradeStatisticsRepository @Inject constructor( } ) - fun getGradesSemesterStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource( + fun getGradesSemesterStatistics( + student: Student, + semester: Semester, + subjectName: String, + forceRefresh: Boolean, + ) = networkBoundResource( mutex = semesterMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(semesterCacheKey, semester)) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed( + key = getRefreshKey(semesterCacheKey, semester) + ) + it.isEmpty() || forceRefresh || isExpired + }, query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) @@ -94,10 +114,12 @@ class GradeStatisticsRepository @Inject constructor( val itemsWithAverage = items.map { item -> item.copy().apply { val denominator = item.amounts.sum() - average = if (denominator == 0) "" else (item.amounts.mapIndexed { gradeValue, amount -> - (gradeValue + 1) * amount - }.sum().toDouble() / denominator).let { - "%.2f".format(Locale.FRANCE, it) + average = if (denominator == 0) "" else { + (item.amounts.mapIndexed { gradeValue, amount -> + (gradeValue + 1) * amount + }.sum().toDouble() / denominator).let { + "%.2f".format(Locale.FRANCE, it) + } } } } @@ -109,7 +131,9 @@ class GradeStatisticsRepository @Inject constructor( amounts = itemsWithAverage.map { it.amounts }.sumGradeAmounts(), studentGrade = 0 ).apply { - average = itemsWithAverage.mapNotNull { it.average.replace(",", ".").toDoubleOrNull() }.average().let { + average = itemsWithAverage.mapNotNull { + it.average.replace(",", ".").toDoubleOrNull() + }.average().let { "%.2f".format(Locale.FRANCE, it) } }).reversed() @@ -118,9 +142,17 @@ class GradeStatisticsRepository @Inject constructor( } ) - fun getGradesPointsStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource( + fun getGradesPointsStatistics( + student: Student, + semester: Semester, + subjectName: String, + forceRefresh: Boolean, + ) = networkBoundResource( mutex = pointsMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(pointsCacheKey, semester)) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(pointsCacheKey, semester)) + it.isEmpty() || forceRefresh || isExpired + }, query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt index 23dd74c2..a04085fb 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt @@ -30,16 +30,19 @@ class HomeworkRepository @Inject constructor( private val cacheKey = "homework" fun getHomework( - student: Student, semester: Semester, - start: LocalDate, end: LocalDate, - forceRefresh: Boolean, notify: Boolean = false + student: Student, + semester: Semester, + start: LocalDate, + end: LocalDate, + forceRefresh: Boolean, + notify: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { - val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed( + val isExpired = refreshHelper.shouldBeRefreshed( key = getRefreshKey(cacheKey, semester, start, end) ) - it.isEmpty() || forceRefresh || isShouldBeRefreshed + it.isEmpty() || forceRefresh || isExpired }, query = { homeworkDb.loadAll( diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt index b904b7db..41e824e5 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt @@ -23,11 +23,17 @@ class LuckyNumberRepository @Inject constructor( private val saveFetchResultMutex = Mutex() - fun getLuckyNumber(student: Student, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( + fun getLuckyNumber( + student: Student, + forceRefresh: Boolean, + notify: Boolean = false, + ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { it == null || forceRefresh }, query = { luckyNumberDb.load(student.studentId, now()) }, - fetch = { sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) }, + fetch = { + sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) + }, saveFetchResult = { old, new -> if (new != old) { old?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) } @@ -41,9 +47,11 @@ class LuckyNumberRepository @Inject constructor( fun getLuckyNumberHistory(student: Student, start: LocalDate, end: LocalDate) = luckyNumberDb.getAll(student.studentId, start, end) - suspend fun getNotNotifiedLuckyNumber(student: Student) = luckyNumberDb.load(student.studentId, now()).map { - if (it?.isNotified == false) it else null - }.first() + suspend fun getNotNotifiedLuckyNumber(student: Student) = + luckyNumberDb.load(student.studentId, now()).map { + if (it?.isNotified == false) it else null + }.first() - suspend fun updateLuckyNumber(luckyNumber: LuckyNumber?) = luckyNumberDb.updateAll(listOfNotNull(luckyNumber)) + suspend fun updateLuckyNumber(luckyNumber: LuckyNumber?) = + luckyNumberDb.updateAll(listOfNotNull(luckyNumber)) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt index 9977e1d5..ee516495 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt @@ -51,14 +51,18 @@ class MessageRepository @Inject constructor( @Suppress("UNUSED_PARAMETER") fun getMessages( - student: Student, semester: Semester, - folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false + student: Student, + semester: Semester, + folder: MessageFolder, + forceRefresh: Boolean, + notify: Boolean = false, ): Flow>> = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { - it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed( - getRefreshKey(cacheKey, student, folder) + val isExpired = refreshHelper.shouldBeRefreshed( + key = getRefreshKey(cacheKey, student, folder) ) + it.isEmpty() || forceRefresh || isExpired }, query = { messagesDb.loadAll(student.id.toInt(), folder.id) }, fetch = { @@ -77,7 +81,8 @@ class MessageRepository @Inject constructor( ) private fun getMessagesWithReadByChange( - old: List, new: List, + old: List, + new: List, setNotified: Boolean ): List { val oldMeta = old.map { Triple(it, it.readBy, it.unreadBy) } @@ -96,7 +101,9 @@ class MessageRepository @Inject constructor( } fun getMessage( - student: Student, message: Message, markAsRead: Boolean = false + student: Student, + message: Message, + markAsRead: Boolean = false, ): Flow> = networkBoundResource( shouldFetch = { checkNotNull(it, { "This message no longer exist!" }) @@ -135,8 +142,10 @@ class MessageRepository @Inject constructor( } suspend fun sendMessage( - student: Student, subject: String, content: String, - recipients: List + student: Student, + subject: String, + content: String, + recipients: List, ): SentMessage = sdk.init(student).sendMessage( subject = subject, content = content, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt index 4b333bc6..bf17cbbc 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt @@ -28,9 +28,16 @@ class MobileDeviceRepository @Inject constructor( private val cacheKey = "devices" - fun getDevices(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( + fun getDevices( + student: Student, + semester: Semester, + forceRefresh: Boolean, + ) = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student)) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) + it.isEmpty() || forceRefresh || isExpired + }, query = { mobileDb.loadAll(student.userLoginId.takeIf { it != 0 } ?: student.studentId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt index d43cdbc0..c1738b36 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt @@ -12,7 +12,6 @@ import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -28,9 +27,19 @@ class NoteRepository @Inject constructor( private val cacheKey = "note" - fun getNotes(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( + fun getNotes( + student: Student, + semester: Semester, + forceRefresh: Boolean, + notify: Boolean = false, + ) = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed( + getRefreshKey(cacheKey, semester) + ) + it.isEmpty() || forceRefresh || isExpired + }, query = { noteDb.loadAll(student.studentId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/NotificationRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/NotificationRepository.kt index 36bc7c25..fca26378 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/NotificationRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/NotificationRepository.kt @@ -6,10 +6,12 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class NotificationRepository @Inject constructor(private val notificationDao: NotificationDao) { +class NotificationRepository @Inject constructor( + private val notificationDao: NotificationDao, +) { fun getNotifications(studentId: Long) = notificationDao.loadAll(studentId) suspend fun saveNotification(notification: Notification) = notificationDao.insertAll(listOf(notification)) -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt index 975a30a2..60e6f248 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt @@ -7,6 +7,8 @@ import io.github.wulkanowy.data.db.entities.ReportingUnit import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.uniqueSubtract import javax.inject.Inject @@ -15,26 +17,34 @@ import javax.inject.Singleton @Singleton class RecipientRepository @Inject constructor( private val recipientDb: RecipientDao, - private val sdk: Sdk + private val sdk: Sdk, + private val refreshHelper: AutoRefreshHelper, ) { + private val cacheKey = "recipient" + suspend fun refreshRecipients(student: Student, unit: ReportingUnit, role: Int) { val new = sdk.init(student).getRecipients(unit.unitId, role).mapToEntities(unit.studentId) val old = recipientDb.loadAll(unit.studentId, unit.unitId, role) recipientDb.deleteAll(old uniqueSubtract new) recipientDb.insertAll(new uniqueSubtract old) + + refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) } suspend fun getRecipients(student: Student, unit: ReportingUnit, role: Int): List { - return recipientDb.loadAll(unit.studentId, unit.unitId, role).ifEmpty { - refreshRecipients(student, unit, role) + val cached = recipientDb.loadAll(unit.studentId, unit.unitId, role) + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) + return if (cached.isEmpty() || isExpired) { + refreshRecipients(student, unit, role) recipientDb.loadAll(unit.studentId, unit.unitId, role) - } + } else cached } suspend fun getMessageRecipients(student: Student, message: Message): List { - return sdk.init(student).getMessageRecipients(message.messageId, message.senderId).mapToEntities(student.studentId) + return sdk.init(student).getMessageRecipients(message.messageId, message.senderId) + .mapToEntities(student.studentId) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/RecoverRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/RecoverRepository.kt index 5e106355..5940f477 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/RecoverRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/RecoverRepository.kt @@ -11,7 +11,7 @@ class RecoverRepository @Inject constructor(private val sdk: Sdk) { return sdk.getPasswordResetCaptchaCode(host, symbol) } - suspend fun sendRecoverRequest(url: String, symbol: String, email: String, reCaptchaResponse: String): String { - return sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse) - } + suspend fun sendRecoverRequest( + url: String, symbol: String, email: String, reCaptchaResponse: String + ): String = sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt index 62d806ac..b6724ed3 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt @@ -2,7 +2,6 @@ package io.github.wulkanowy.data.repositories import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao import io.github.wulkanowy.data.db.entities.SchoolAnnouncement -import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.sdk.Sdk @@ -12,7 +11,6 @@ import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -30,17 +28,15 @@ class SchoolAnnouncementRepository @Inject constructor( fun getSchoolAnnouncements( student: Student, - forceRefresh: Boolean, - notify: Boolean = false + forceRefresh: Boolean, notify: Boolean = false ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { - it.isEmpty() || forceRefresh - || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student)) + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) + it.isEmpty() || forceRefresh || isExpired }, query = { - schoolAnnouncementDb.loadAll( - student.studentId) + schoolAnnouncementDb.loadAll(student.studentId) }, fetch = { sdk.init(student) @@ -57,9 +53,11 @@ class SchoolAnnouncementRepository @Inject constructor( refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) } ) + fun getSchoolAnnouncementFromDatabase(student: Student): Flow> { return schoolAnnouncementDb.loadAll(student.studentId) } - suspend fun updateSchoolAnnouncement(schoolAnnouncement: List) = schoolAnnouncementDb.updateAll(schoolAnnouncement) + suspend fun updateSchoolAnnouncement(schoolAnnouncement: List) = + schoolAnnouncementDb.updateAll(schoolAnnouncement) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt index 8b59cb58..288a1fb6 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt @@ -5,6 +5,8 @@ import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntity import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import kotlinx.coroutines.sync.Mutex @@ -14,29 +16,41 @@ import javax.inject.Singleton @Singleton class SchoolRepository @Inject constructor( private val schoolDb: SchoolDao, - private val sdk: Sdk + private val sdk: Sdk, + private val refreshHelper: AutoRefreshHelper, ) { private val saveFetchResultMutex = Mutex() - fun getSchoolInfo(student: Student, semester: Semester, forceRefresh: Boolean) = - networkBoundResource( - mutex = saveFetchResultMutex, - shouldFetch = { it == null || forceRefresh }, - query = { schoolDb.load(semester.studentId, semester.classId) }, - fetch = { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool() - .mapToEntity(semester) - }, - saveFetchResult = { old, new -> - if (old != null && new != old) { - with(schoolDb) { - deleteAll(listOf(old)) - insertAll(listOf(new)) - } - } else if (old == null) { - schoolDb.insertAll(listOf(new)) + private val cacheKey = "school_info" + + fun getSchoolInfo( + student: Student, + semester: Semester, + forceRefresh: Boolean, + ) = networkBoundResource( + mutex = saveFetchResultMutex, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed( + key = getRefreshKey(cacheKey, student) + ) + it == null || forceRefresh || isExpired + }, + query = { schoolDb.load(semester.studentId, semester.classId) }, + fetch = { + sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool() + .mapToEntity(semester) + }, + saveFetchResult = { old, new -> + if (old != null && new != old) { + with(schoolDb) { + deleteAll(listOf(old)) + insertAll(listOf(new)) } + } else if (old == null) { + schoolDb.insertAll(listOf(new)) } - ) + refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) + } + ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt index de66ad20..e98daedf 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt @@ -19,24 +19,27 @@ class StudentInfoRepository @Inject constructor( private val saveFetchResultMutex = Mutex() - fun getStudentInfo(student: Student, semester: Semester, forceRefresh: Boolean) = - networkBoundResource( - mutex = saveFetchResultMutex, - shouldFetch = { it == null || forceRefresh }, - query = { studentInfoDao.loadStudentInfo(student.studentId) }, - fetch = { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) - .getStudentInfo().mapToEntity(semester) - }, - saveFetchResult = { old, new -> - if (old != null && new != old) { - with(studentInfoDao) { - deleteAll(listOf(old)) - insertAll(listOf(new)) - } - } else if (old == null) { - studentInfoDao.insertAll(listOf(new)) + fun getStudentInfo( + student: Student, + semester: Semester, + forceRefresh: Boolean, + ) = networkBoundResource( + mutex = saveFetchResultMutex, + shouldFetch = { it == null || forceRefresh }, + query = { studentInfoDao.loadStudentInfo(student.studentId) }, + fetch = { + sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) + .getStudentInfo().mapToEntity(semester) + }, + saveFetchResult = { old, new -> + if (old != null && new != old) { + with(studentInfoDao) { + deleteAll(listOf(old)) + insertAll(listOf(new)) } + } else if (old == null) { + studentInfoDao.insertAll(listOf(new)) } - ) + } + ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt index b4bfef18..d81cb7c9 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt @@ -5,6 +5,8 @@ import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract @@ -15,14 +17,24 @@ import javax.inject.Singleton @Singleton class SubjectRepository @Inject constructor( private val subjectDao: SubjectDao, - private val sdk: Sdk + private val sdk: Sdk, + private val refreshHelper: AutoRefreshHelper, ) { private val saveFetchResultMutex = Mutex() - fun getSubjects(student: Student, semester: Semester, forceRefresh: Boolean = false) = networkBoundResource( + private val cacheKey = "subjects" + + fun getSubjects( + student: Student, + semester: Semester, + forceRefresh: Boolean = false, + ) = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) + it.isEmpty() || forceRefresh || isExpired + }, query = { subjectDao.loadAll(semester.diaryId, semester.studentId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) @@ -31,6 +43,8 @@ class SubjectRepository @Inject constructor( saveFetchResult = { old, new -> subjectDao.deleteAll(old uniqueSubtract new) subjectDao.insertAll(new uniqueSubtract old) + + refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) } ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt index 7135edbe..029b2707 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt @@ -5,6 +5,8 @@ import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract @@ -15,14 +17,24 @@ import javax.inject.Singleton @Singleton class TeacherRepository @Inject constructor( private val teacherDb: TeacherDao, - private val sdk: Sdk + private val sdk: Sdk, + private val refreshHelper: AutoRefreshHelper, ) { private val saveFetchResultMutex = Mutex() - fun getTeachers(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( + private val cacheKey = "teachers" + + fun getTeachers( + student: Student, + semester: Semester, + forceRefresh: Boolean, + ) = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) + it.isEmpty() || forceRefresh || isExpired + }, query = { teacherDb.loadAll(semester.studentId, semester.classId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) @@ -32,6 +44,8 @@ class TeacherRepository @Inject constructor( saveFetchResult = { old, new -> teacherDb.deleteAll(old uniqueSubtract new) teacherDb.insertAll(new uniqueSubtract old) + + refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) } ) } 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 index 5495d077..1540d3cc 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt @@ -41,18 +41,22 @@ class TimetableRepository @Inject constructor( private val cacheKey = "timetable" fun getTimetable( - student: Student, semester: Semester, start: LocalDate, end: LocalDate, - forceRefresh: Boolean, refreshAdditional: Boolean = false + student: Student, + semester: Semester, + start: LocalDate, + end: LocalDate, + forceRefresh: Boolean, + refreshAdditional: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { (timetable, additional, headers) -> val refreshKey = getRefreshKey(cacheKey, semester, start, end) - val isShouldRefresh = refreshHelper.isShouldBeRefreshed(refreshKey) + val isExpired = refreshHelper.shouldBeRefreshed(refreshKey) val isRefreshAdditional = additional.isEmpty() && refreshAdditional val isNoData = timetable.isEmpty() || isRefreshAdditional || headers.isEmpty() - isNoData || forceRefresh || isShouldRefresh + isNoData || forceRefresh || isExpired }, query = { getFullTimetableFromDatabase(student, semester, start, end) }, fetch = { @@ -79,8 +83,10 @@ class TimetableRepository @Inject constructor( ) private fun getFullTimetableFromDatabase( - student: Student, semester: Semester, - start: LocalDate, end: LocalDate + student: Student, + semester: Semester, + start: LocalDate, + end: LocalDate, ): Flow { val timetableFlow = timetableDb.loadAll( diaryId = semester.diaryId, @@ -113,7 +119,8 @@ class TimetableRepository @Inject constructor( private suspend fun refreshTimetable( student: Student, - lessonsOld: List, lessonsNew: List + lessonsOld: List, + lessonsNew: List, ) { val lessonsToRemove = lessonsOld uniqueSubtract lessonsNew val lessonsToAdd = (lessonsNew uniqueSubtract lessonsOld).map { new -> diff --git a/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt b/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt index cd59b864..6bf97bae 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt @@ -33,7 +33,7 @@ class AutoRefreshHelper @Inject constructor( private val sharedPref: SharedPrefProvider ) { - fun isShouldBeRefreshed(key: String): Boolean { + fun shouldBeRefreshed(key: String): Boolean { val timestamp = sharedPref.getLong(key, 0).toLocalDateTime() val servicesInterval = sharedPref.getString(context.getString(R.string.pref_key_services_interval), context.getString(R.string.pref_default_services_interval)).toLong() diff --git a/app/src/main/res/values/api_hosts.xml b/app/src/main/res/values/api_hosts.xml index 15849047..dac94c3f 100644 --- a/app/src/main/res/values/api_hosts.xml +++ b/app/src/main/res/values/api_hosts.xml @@ -40,7 +40,7 @@ https://vulcan.net.pl/?login https://vulcan.net.pl/?login https://vulcan.net.pl/?login - http://fakelog.cf/?email + http://fakelog.tk/?email Default diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt index 1c592c09..f3c7fba7 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt @@ -53,7 +53,7 @@ class AttendanceRepositoryTest { @Before fun setUp() { MockKAnnotations.init(this) - every { refreshHelper.isShouldBeRefreshed(any()) } returns false + every { refreshHelper.shouldBeRefreshed(any()) } returns false attendanceRepository = AttendanceRepository(attendanceDb, sdk, refreshHelper) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt index b116a623..fa54522a 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt @@ -53,7 +53,7 @@ class CompletedLessonsRepositoryTest { @Before fun initApi() { MockKAnnotations.init(this) - every { refreshHelper.isShouldBeRefreshed(any()) } returns false + every { refreshHelper.shouldBeRefreshed(any()) } returns false completedLessonRepository = CompletedLessonsRepository(completedLessonDb, sdk, refreshHelper) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt index ead6dc5d..8bf4deee 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt @@ -54,7 +54,7 @@ class ExamRemoteTest { @Before fun setUp() { MockKAnnotations.init(this) - every { refreshHelper.isShouldBeRefreshed(any()) } returns false + every { refreshHelper.shouldBeRefreshed(any()) } returns false examRepository = ExamRepository(examDb, sdk, refreshHelper) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt index 8a19d633..6dd30a57 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt @@ -50,7 +50,7 @@ class GradeRepositoryTest { @Before fun initApi() { MockKAnnotations.init(this) - every { refreshHelper.isShouldBeRefreshed(any()) } returns false + every { refreshHelper.shouldBeRefreshed(any()) } returns false gradeRepository = GradeRepository(gradeDb, gradeSummaryDb, sdk, refreshHelper) diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepositoryTest.kt index 73dd4cfa..cce3794d 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepositoryTest.kt @@ -56,7 +56,7 @@ class GradeStatisticsRepositoryTest { @Before fun setUp() { MockKAnnotations.init(this) - every { refreshHelper.isShouldBeRefreshed(any()) } returns false + every { refreshHelper.shouldBeRefreshed(any()) } returns false gradeStatisticsRepository = GradeStatisticsRepository(gradePartialStatisticsDb, gradePointsStatisticsDb, gradeSemesterStatisticsDb, sdk, refreshHelper) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt index cadc4225..25774d74 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt @@ -69,7 +69,7 @@ class MessageRepositoryTest { @Before fun setUp() { MockKAnnotations.init(this) - every { refreshHelper.isShouldBeRefreshed(any()) } returns false + every { refreshHelper.shouldBeRefreshed(any()) } returns false repository = MessageRepository( messagesDb = messageDb, diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt index e5b3d101..52a076d3 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt @@ -48,7 +48,7 @@ class MobileDeviceRepositoryTest { @Before fun initTest() { MockKAnnotations.init(this) - every { refreshHelper.isShouldBeRefreshed(any()) } returns false + every { refreshHelper.shouldBeRefreshed(any()) } returns false mobileDeviceRepository = MobileDeviceRepository(mobileDeviceDb, sdk, refreshHelper) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/RecipientLocalTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/RecipientLocalTest.kt index 82406ef4..980abac0 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/RecipientLocalTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/RecipientLocalTest.kt @@ -5,10 +5,12 @@ import io.github.wulkanowy.data.db.entities.ReportingUnit import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.utils.AutoRefreshHelper import io.mockk.MockKAnnotations import io.mockk.Runs import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK import io.mockk.just @@ -26,6 +28,9 @@ class RecipientLocalTest { @MockK private lateinit var recipientDb: RecipientDao + @MockK(relaxUnitFun = true) + private lateinit var refreshHelper: AutoRefreshHelper + private val student = getStudentEntity() private lateinit var recipientRepository: RecipientRepository @@ -39,8 +44,9 @@ class RecipientLocalTest { @Before fun setUp() { MockKAnnotations.init(this) + every { refreshHelper.shouldBeRefreshed(any()) } returns false - recipientRepository = RecipientRepository(recipientDb, sdk) + recipientRepository = RecipientRepository(recipientDb, sdk, refreshHelper) } @Test diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt index 2c56a1b6..75c75a66 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt @@ -62,7 +62,7 @@ class TimetableRepositoryTest { @Before fun initApi() { MockKAnnotations.init(this) - every { refreshHelper.isShouldBeRefreshed(any()) } returns false + every { refreshHelper.shouldBeRefreshed(any()) } returns false timetableRepository = TimetableRepository(timetableDb, timetableAdditionalDao, timetableHeaderDao, sdk, timetableNotificationSchedulerHelper, refreshHelper) } From 9711cc868c4583f1cc169b361e229f90631a3ca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Tue, 28 Sep 2021 22:42:06 +0200 Subject: [PATCH 025/357] New Crowdin updates (#1522) --- .../main/res/values-cs/preferences_values.xml | 4 +-- app/src/main/res/values-cs/strings.xml | 28 +++++++++------- .../main/res/values-de/preferences_values.xml | 2 +- app/src/main/res/values-de/strings.xml | 32 ++++++++++++------- .../main/res/values-pl/preferences_values.xml | 4 +-- app/src/main/res/values-pl/strings.xml | 24 +++++++------- .../main/res/values-ru/preferences_values.xml | 4 +-- app/src/main/res/values-ru/strings.xml | 30 ++++++++++------- .../main/res/values-sk/preferences_values.xml | 4 +-- app/src/main/res/values-sk/strings.xml | 28 +++++++++------- .../main/res/values-uk/preferences_values.xml | 4 +-- app/src/main/res/values-uk/strings.xml | 30 ++++++++++------- 12 files changed, 112 insertions(+), 82 deletions(-) diff --git a/app/src/main/res/values-cs/preferences_values.xml b/app/src/main/res/values-cs/preferences_values.xml index fb938f09..5e2f10fb 100644 --- a/app/src/main/res/values-cs/preferences_values.xml +++ b/app/src/main/res/values-cs/preferences_values.xml @@ -41,8 +41,8 @@ Barvy známek v deníku - Průměrná známka od druhého semestru - Průměr známek z obou semestrů + Průměr známek pouze z vybraného semestru + Průměr z průměrů z obou semestrů Průměr známek z celého roku diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 0367efdd..1557ecc1 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -12,7 +12,7 @@ O aplikaci Prohlížeč protokolů Ladění - Ladění oznámení + Ladění upozornění Tvůrci Licence Zprávy @@ -24,6 +24,7 @@ Podrobnosti účtu Informace o žáku Domů + Centrum upozornění Semestr %1$d, %2$d/%3$d @@ -43,7 +44,7 @@ Přihlásit Toto heslo je příliš krátké Přihlašovací údaje jsou nesprávné - %1$s. Ujistěte se, že je v poli níže vybrána správná variace deníku UONET+ + %1$s. Zkontrolujte, zda je níže vybrána správná variace deníku UONET+ Neplatný PIN Neplatný token Token vypršel @@ -91,6 +92,10 @@ Konečná známka Předpokládaná známka Vypočítaný průměr + Jak funguje vypočítaný průměr? + Vypočítaný průměr je aritmetický průměr vypočítaný z průměrů předmětů. Umožňuje vám to znát přibližný konečný průměr. Vypočítává se způsobem zvoleným uživatelem v nastavení aplikaci. Doporučuje se vybrat příslušnou možnost. Důvodem je rozdílný výpočet školních průměrů. Pokud vaše škola navíc uvádí průměr předmětů na stránce deníku Vulcan, aplikace si je stáhne a tyto průměry nepočítá. To lze změnit vynucením výpočtu průměru v nastavení aplikaci.\n\nPrůměr známek pouze z vybraného semestru:\n1. Výpočet váženého průměru pro každý předmět v daném semestru\n2. Sčítání vypočítaných průměrů\n3. Výpočet aritmetického průměru součtených průměrů\n\nPrůměr průměrů z obou semestrů:\n1. Výpočet váženého průměru pro každý předmět v semestru 1 a 2\n2. Výpočet aritmetického průměru vypočítaných průměrů za semestry 1 a 2 pro každý předmět.\n3. Sčítání vypočítaných průměrů\n4. Výpočet aritmetického průměru součtených průměrů\n\nPrůměr známek z celého roku:\n1. Výpočet váženého průměru za rok pro každý předmět. Konečný průměr v 1. semestru je nepodstatný.\n3. Sčítání vypočítaných průměrů\n4. Výpočet aritmetického průměru součtených průměrů + Jak funguje konečný průměr? + Konečný průměr je aritmetický průměr vypočítaný ze všech aktuálně dostupných konečných známek v daném semestru.\n\nSchéma výpočtu se skládá z následujících kroků:\n1. Sčítání konečných známek zadaných učiteli\n2. Děleno počtem předmětů, pro které už byly vydány známky Konečný průměr z %1$d z %2$d předmětů Shrnutí @@ -239,12 +244,7 @@ Pouze nepřečtené Pouze s přílohami Přečtena: %s - - Přečtena přes: %1$d z %2$d osob - Přečtena přes: %1$d z %2$d osob - Přečtena přes: %1$d z %2$d osob - Přečtena přes: %1$d z %2$d osob - + Přečtena přes: %1$d z %2$d osob %d zpráva %d zprávy @@ -403,7 +403,7 @@ Máte %1$d nových setkání Máte %1$d nových setkání - Present at conference + Přítomnost na setkání Agenda Školní oznámení @@ -593,7 +593,7 @@ Ano Ne Uložit - Title + Titul Žádné lekce Vybrat motiv @@ -603,7 +603,7 @@ Vzhled a chování aplikací Výchozí zobrazení - Výpočet koncoročního průměru + Možnosti vypočítaného průměru Vynutit průměrný výpočet podle aplikace Zobrazit přítomnost Motiv @@ -618,12 +618,18 @@ Upozornění Zobrazit upozornění Zobrazit upozornění o nadcházející lekci + Nastavit upozornění o nadcházející lekci jako trvalé + Vypnout, když upozornění není ve vašem hodinkách/náramku viditelné Otevřít systémová nastavení upozornění Opravte problémy se synchronizací a upozorněním Vaše zařízení může mít problémy se synchronizací dat as upozorněními.\n\nChcete-li je opravit, přidejte Wulkanového do funkce Autostart a vypněte optimalizaci/úsporu baterie v nastavení systému telefonu. Přejít do nastavení Zobrazit upozornění o ladění Synchronizace je vypnutá + Zachytit upozornění oficiální aplikací + Zachytit upozornění + S touto funkcí můžete získat náhradu push upozornění jako v oficiální aplikaci. Vše, co musíte udělat, je povolit Wulkanowému číst všechna vaše upozornění v nastaveních systému.\n\nJak to funguje?\nKdyž obdržíte oznámení v Deníčku VULCAN, Wulkanowy bude o tom informován (k tomu je to dodatečné povolení) a spustí synchronizaci, aby mohl zaslat vlastní upozornění.\n\nPOUZE PRO POKROČILÉ UŽIVATELE + Přejít do nastavení Synchronizace Automatická aktualizace Pozastaveno na dovolené diff --git a/app/src/main/res/values-de/preferences_values.xml b/app/src/main/res/values-de/preferences_values.xml index 1e0df8de..699ca824 100644 --- a/app/src/main/res/values-de/preferences_values.xml +++ b/app/src/main/res/values-de/preferences_values.xml @@ -41,8 +41,8 @@ Farben der Bewertungen im Logbuch - Durchschnittsnote für das 2. Semester Durchschnitt der Noten aus beiden Semestern + Durchschnittswert der Durchschnittswerte beider Semester Durchschnitt der Noten aus dem ganzen Jahr diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8c30c7a9..de73e782 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -24,6 +24,7 @@ Kontodetails Schülerinfo Übersicht + Benachrichtigungszentrum Semester %1$d, %2$d/%3$d @@ -43,7 +44,7 @@ Anmelden Passwort ist zu kurz Anmeldedaten sind falsch - %1$s. Stellen Sie sicher, dass die richtige UONET+ Registervariation im unteren Feld ausgewählt ist + %1$s. Stellen Sie sicher, dass die korrekte UONET+ Registervariation unten ausgewählt ist Ungültige PIN Ungültige token Token ist nicht mehr gültig @@ -91,6 +92,10 @@ Finaler Note Vorhergesagte Note Berechnender Durchschnitt + Wie funktioniert der berechnete Durchschnitt? + The Calculated Average is the arithmetic average calculated from the subjects averages. It allows you to know the approximate final average. It is calculated in a way selected by the user in the application settings. It is recommended that you choose the appropriate option. This is because the calculation of school averages differs. Additionally, if your school reports the average of the subjects on the Vulcan page, the application downloads them and does not calculate these averages. This can be changed by forcing the calculation of the average in the application settings.\n\nAverage of grades only from selected semester:\n1. Calculating the weighted average for each subject in a given semester\n2.Adding calculated averages\n3. Calculation of the arithmetic average of the summed averages\n\nAverage of averages from both semesters:\n1.Calculating the weighted average for each subject in semester 1 and 2\n2. Calculating the arithmetic average of the calculated averages for semesters 1 and 2 for each subject.\n3. Adding calculated averages\n4. Calculation of the arithmetic average of the summed averages\n\nAverage of grades from the whole year:\n1. Calculating weighted average over the year for each subject. The final average in the 1st semester is irrelevant.\n3. Adding calculated averages\n4. Calculating the arithmetic average of summed averages + Wie funktioniert der endgültige Durchschnitt? + The Final Average is the arithmetic average calculated from all currently available final grades in the given semester.\n\nThe calculation scheme consists of the following steps:\n1. Summing up the final grades given by teachers\n2. Divide by the number of subjects that have already been graded Finaler Durchschnitt aus %1$d von %2$d Schulfächern Zusammenfassung @@ -156,7 +161,7 @@ Zusätzliche Lektionen Zusätzliche Lektionen anzeigen - Keine Infos zu zusätzlichen Lektionen + Keine Informationen über zusätzlichen Lektionen Übersicht über die Schulbesuch Aus schulischen Gründen abwesend @@ -186,8 +191,8 @@ Neue prüfungen - Du hast %d neue Prüfung erhalten - Sie haben %d neue Prüfungen erhalten + Du hast %d neue Prüfung + Du hast %d neue Prüfungen %d prüfung @@ -213,16 +218,13 @@ Thema Inhalt Nachricht erfolgreich gesendet - Nachricht existiert nicht + Nachricht nicht vorhanden Sie müssen mindestens 1 Empfänger auswählen. Der Inhalt der Nachricht muss mindestens 3 Zeichen lang sein. Nur ungelesen Nur mit Anhängen Lesen: %s - - Lesen von: %1$d von %2$d Personen - Lesen von: %1$d von %2$d Personen - + Lesen von: %1$d von %2$d Personen %d nachricht %d nachrichten @@ -345,7 +347,7 @@ Sie haben %1$d neue konferenz Sie haben %1$d neue konferenzen - Present at conference + Teilnahme an einem Meeting Agenda Schulankündigungen @@ -515,7 +517,7 @@ Ja Nein Speichern - Title + Titel Keine Lektionen Thema wählen @@ -525,7 +527,7 @@ Aussehen & Verhalten Standard Ansicht - Berechnung des Jahresenddurchschnitts + Berechnete Durchschnittsoptionen Mittelwertberechnung durch App erzwingen Anwesendheit zeigen Thema @@ -540,12 +542,18 @@ Benachrichtigungen Benachrichtigungen anzeigen Benachrichtigungen über bevorstehende Lektionen anzeigen + Festlegen einer Benachrichtigung über die bevorstehende Lektion dauerhaft + Deaktivieren wenn die Benachrichtigung nicht in deiner Uhr/Band angezeigt wird Systembenachrichtigungseinstellungen öffnen Synchronisierungs- und Benachrichtigungsprobleme reparieren Ihr Gerät hat möglicherweise Probleme mit der Datensynchronisierung und Benachrichtigungen.\n\nUm diese zu reparieren, fügen Sie Wulkanowy zum Autostart hinzu und deaktivieren Sie die Batterieoptimierung in den Systemeinstellungen des Geräts. Gehe zu den Einstellungen Debug-Benachrichtigungen anzeigen Synchronisierung ist deaktiviert + Offizielle App-Benachrichtigungen erfassen + Benachrichtigungen erfassen + With this feature you can gain a substitute of push notifications like in the official app. All you need to do is allow Wulkanowy to receive all notifications in your system settings.\n\nHow it works?\nWhen you get a notification in Dziennik VULCAN, Wulkanowy will be notified (that\'s what these extra permissions are for) and will trigger a sync so that can send its own notification.\n\nFOR ADVANCED USERS ONLY + Gehe zu Einstellungen Synchronisierung Automatische Aktualisierung An Feiertagen suspendiert diff --git a/app/src/main/res/values-pl/preferences_values.xml b/app/src/main/res/values-pl/preferences_values.xml index 51e185f7..5b0a90c6 100644 --- a/app/src/main/res/values-pl/preferences_values.xml +++ b/app/src/main/res/values-pl/preferences_values.xml @@ -41,8 +41,8 @@ Kolory ocen w dzienniku - Średnia ocen z wybranego semestru - Średnia średnich z obu semestrów + Średnia ocen tylko z wybranego semestru + Średnia ze średnich z obu semestrów Średnia wszystkich ocen z całego roku diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index d7a49c11..0a231606 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -24,6 +24,7 @@ Szczegóły konta Informacje o uczniu Start + Centrum powiadomień Semestr %1$d, %2$d/%3$d @@ -43,7 +44,7 @@ Zaloguj To hasło jest za krótkie Dane logowania są niepoprawne - %1$s. Upewnij się, że poniżej została wybrana odpowiednia odmiana dziennika UONET+ + %1$s. Upewnij się, że wybrano poprawną odmianę dziennika UONET+ poniżej Nieprawidłowy PIN Nieprawidłowy token Token stracił ważność @@ -91,11 +92,11 @@ Ocena końcowa Przewidywana ocena Obliczona średnia - Jak działa Obliczona średnia? - Obliczona średnia to średnia arytmetyczna wyliczona ze średnich przedmiotów. Pozwala ona na poznanie przybliżonej średniej końcowej. Jest obliczana w sposób wybrany przez użytkownika w ustawieniach aplikacji. Zalecane jest, aby wybrać odpowiednią opcję. Wynika to z tego, że sposoby obliczania średnich w szkołach różnią się. Dodatkowo jeśli twoja szkoła podaje średnią z przedmiotów na stronie dziennika aplikacja pobiera je i nie wylicza tych średnich. Można to zmienić wymuszając liczenie średniej w ustawieniach aplikacji.\n\nŚrednia ocen z aktualnie wybranego semestru:\n1. Obliczanie średniej ważonej dla każdego przedmiotu w danym semestrze\n2. Sumowanie obliczonych średnich\n3. Obliczenie średniej arytmetycznej z zsumowanych średnich\n\nŚrednia średnich z obu semestrów:\n1. Obliczanie średniej ważonej dla każdego przedmiotu w semestrze 1 i 2\n2. Obliczanie średniej arytmetycznej z obliczonych średnich semestrów 1 i 2 dla każdego przedmiotu.\n3. Sumowanie obliczonych średnich\n4. Obliczenie średniej arytmetycznej z zsumowanych średnich\n\nŚrednia wszystkich ocen z całego roku:\n1. Obliczanie średniej ważonej z całego roku dla każdego przedmiotu. Średnia końcowa w 1 semestrze jest bez znaczenia.\n3. Sumowanie obliczonych średnich\n4. Obliczenie średniej arytmetycznej z zsumowanych średnich + Jak działa obliczona średnia? + Obliczona średnia jest średnią arytmetyczną obliczoną ze średnich przedmiotów. Pozwala ona na poznanie przybliżonej średniej końcowej. Jest obliczana w sposób wybrany przez użytkownika w ustawieniach aplikacji. Zaleca się wybranie odpowiedniej opcji. Dzieje się tak dlatego, że obliczanie średnich w szkołach różni się. Dodatkowo, jeśli twoja szkoła ma włączone średnie przedmiotów na stronie dziennika Vulcan, aplikacja pobiera je i ich nie oblicza. Można to zmienić, wymuszając obliczanie średniej w ustawieniach aplikacji.\n\nŚrednia ocen tylko z wybranego semestru:\n1. Obliczanie średniej arytmetycznej każdego przedmiotu w danym semestrze\n2. Zsumowanie obliczonych średnich\n3. Obliczanie średniej arytmetycznej zsumowanych średnich\n\nŚrednia ze średnich z obu semestrów:\n1.Obliczanie średniej arytmetycznej każdego przedmiotu w semestrze 1 i 2\n2. Obliczanie średniej arytmetycznej obliczonych średnich w semetrze 1 i 2 każdego przedmiotu.\n3. Zsumowanie obliczonych średnich\n4. Obliczanie średniej arytmetycznej zsumowanych średnich\n\nŚrednia wszystkich ocen z całego roku:\n1. Obliczanie średniej arytmetycznej z każdego przedmiotu w ciągu całego roku. Końcowa ocena w 1 semestrze jest bez znaczenia.\n3. Zsumowanie obliczonych średnich\n4. Obliczanie średniej arytmetycznej z zsumowanych średnich + Jak działa końcowa średnia? + Średnią końcową jest średnia arytmetyczna obliczona na podstawie wszystkich obecnie dostępnych ocen końcowych w danym semestrze.\n\nSchemat obliczeń składa się z następujących kroków:\n1. Sumowanie końcowych ocen wpisanych przez nauczycieli\n2. Dzielenie przez liczbę przedmiotów, z których oceny zostały już wystawione Końcowa średnia - Jak działa Końcowa średnia? - Końcowa średnia to średnia arytmetyczna wyliczona ze wszystkich aktualnie dostępnych ocen końcowych w danym semestrze.\n\nSchemat obliczania składa się z następujących kroków:\n1. Zsumowanie ocen końcowych wystawionych przez nauczycieli\n2. Podzielenie przez liczbę przedmiotów, z których oceny zostały już wystawione z %1$d na %2$d przedmiotów Podsumowanie Klasa @@ -243,12 +244,7 @@ Tylko nieprzeczytane Tylko z załącznikami Przeczytana: %s - - Przeczytana przez: %1$d z %2$d osób - Przeczytana przez: %1$d z %2$d osób - Przeczytana przez: %1$d z %2$d osób - Przeczytana przez: %1$d z %2$d osób - + Przeczytana przez: %1$d z %2$d osób %d wiadomość %d wiadomości @@ -607,7 +603,7 @@ Wygląd i zachowanie aplikacji Domyślny widok - Opcje średniej obliczonej + Opcje obliczonej średniej Wymuś obliczanie średniej przez aplikację Pokazuj obecność Motyw @@ -622,6 +618,8 @@ Powiadomienia Pokazuj powiadomienia Pokazuj powiadomienia o nadchodzących lekcjach + Ustaw powiadomienie o nadchodzącej lekcji jako trwałe + Wyłącz, gdy powiadomienie nie jest widoczne na zegarku/opasce Otwórz systemowe ustawienia powiadomień Napraw problemy z synchronizacją i powiadomieniami Na twoim urządzeniu mogą występować problemy z synchronizacją danych i powiadomieniami.\n\nBy je naprawić, dodaj Wulkanowego do autostartu i wyłącz optymalizację/oszczędzanie baterii w ustawieniach systemowych telefonu. @@ -630,7 +628,7 @@ Synchronizacja jest wyłączona Przechwytywanie powiadomień oficjalnej aplikacji Przechwytywanie powiadomień - Dzięki tej funkcji możesz zyskać namiastkę powiadomień push takich, jak w oficjalnej aplikacji. Wystarczy że zezwolisz Wulkanowemu na odbieranie wszystkich powiadomień w ustawieniach systemowych.\n\nJak to działa?\nGdy dostaniesz powiadomienie w Dzienniczku VULCANa Wulkanowy zostanie o tym powiadomiony (po to te dodatkowe uprawnienia) i uruchomi synchronizację, dzięki czemu będzie mógł wysłać własne powiadomienie.\n\nTYLKO DLA ZAAWANSOWANYCH UŻYTKOWNIKÓW + Dzięki tej funkcji możesz uzyskać namiastkę powiadomień push, takich jak w oficjalnej aplikacji. Wszystko, co musisz zrobić, to zezwolić Wulkanowemu na odczytywanie wszystkich powiadomień w ustawieniach systemowych.\n\nJak to działa?\nKiedy otrzymasz powiadomienie w Dzienniczku VULCAN, Wulkanowy zostanie o tym powiadomiony (do tego jest to dodatkowe uprawnienie) i uruchomi synchronizację, aby mógł wysłać własne powiadomienie.\n\nWYŁĄCZNIE DLA ZAAWANSOWANYCH UŻYTKOWNIKÓW Przejdź do ustawień Synchronizacja Automatyczna aktualizacja diff --git a/app/src/main/res/values-ru/preferences_values.xml b/app/src/main/res/values-ru/preferences_values.xml index 7c4d14df..4920068e 100644 --- a/app/src/main/res/values-ru/preferences_values.xml +++ b/app/src/main/res/values-ru/preferences_values.xml @@ -41,8 +41,8 @@ Цвета оценок в дневнике - Средняя оценка со 2 семестра - Средняя оценка с двух семестров + Средние оценки только с выбранного семестра + Средние значения для обоих семестров Средняя оценок со всего года diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 54ae5e1b..48d8a21a 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -24,6 +24,7 @@ Данные аккаунта Информация о студенте Панель + Центр уведомлений %1$d семестр, %2$d/%3$d @@ -42,8 +43,8 @@ Symbol Войти Слишком короткий пароль - Данные для входа неверны - %1$s. Убедитесь, что в поле ниже выбран правильный вариант регистра UONET+ + Данные для входа указаны неверно + %1$s. Убедитесь, что ниже выбран правильный UONET+ вариант регистра Неправильный PIN Неверный token Token просрочен @@ -91,6 +92,10 @@ Итоговая оценка Ожидаемая оценка Рассчитанная средняя оценка + Как рассчитывается средняя работа? + Расчетное среднее - это среднее арифметическое, рассчитанное на основе средних значений испытуемых. Это позволяет узнать приблизительное итоговое среднее значение. Он рассчитывается способом, выбранным пользователем в настройках приложения. Рекомендуется выбрать подходящий вариант. Это потому, что расчет средних показателей школы отличается. Кроме того, если ваша школа сообщает среднее значение по предметам на странице Vulcan, приложение загружает их и не вычисляет эти средние значения. Это можно изменить, принудительно вычисляя среднее значение в настройках приложения. \ N \ n Среднее значение только за выбранный семестр : \ n1. Вычисление средневзвешенного значения по каждому предмету за семестр \ n2.Добавление вычисленных средних \ n3. Вычисление среднего арифметического суммарных средних \ n \ n Среднее из средних значений за оба семестра : \ n1.Расчет средневзвешенного значения для каждого предмета в семестрах 1 и 2 \ n2. Вычисление среднего арифметического рассчитанных средних значений для семестров 1 и 2 по каждому предмету. \ N3. Добавление вычисленных средних \ n4. Расчет среднего арифметического суммированных средних \ n \ n Среднее значение оценок за весь год: \ n1. Расчет средневзвешенного значения за год по каждому предмету. Итоговое среднее значение за 1 семестр не имеет значения. \ N3. Добавление вычисленных средних \ n4. Расчет среднего арифметического + Как работает окончательный средний показатель? + Среднее арифметическое - это среднее арифметическое, рассчитанное по всем имеющимся на данный момент итоговым классам данного семестра.\n\nСхема расчета состоит из следующих шагов:\n1. Суммирование итоговых классов преподавателей\n2. Деление по количеству уже оцененных предметов Итоговая средняя оценка от %1$d из %2$d субъектов Итоги @@ -239,12 +244,7 @@ Только непрочитанные Только с вложениями Чтение: %s - - Прочитано: %1$d из %2$d человек - Прочитано: %1$d из %2$d человек - Прочитано: %1$d из %2$d человек - Прочитано: %1$d из %2$d человек - + Прочитано: %1$d из %2$d человек %d сообщение %d сообщения @@ -403,8 +403,8 @@ У вас %1$d новая конференция У вас %1$d новых конференций - Present at conference - Agenda + Присутствует на конференции + Повестка дня Объявления школ Нет объявлений о школе @@ -593,7 +593,7 @@ Да Нет Сохранить - Title + Тема Нет уроков Выбрать тему @@ -603,7 +603,7 @@ Внешний вид приложения & поведение Окно по умолчанию - Способ определения средней годовой оценки + Рассчитанные средние параметры Принудительно высчитать среднюю оценку через приложение Показать присутствие Тема @@ -618,12 +618,18 @@ Уведомления Показывать уведомления Показывать уведомления о будущих уроках + Сделать уведомления о предстоящем уроке постоянным + Выключить, когда уведомление не отображается в чата/полосе Открыть настройки уведомлений системы Исправить проблемы с синхронизацией и уведомлениями На вашем устройстве могут быть проблемы с синхронизацией данных и уведомлениями.\n\nЧтобы их исправить, вам необходимо добавить Wulkanowy в авто-старт и выключить оптимизацию/экономию батареи в настройках устройства. Перейти в настройски Показывать дебаг-уведомления Синхронизация отключена + Записывать официальные уведомления + Показывать push-уведомления + С помощью этой функции вы можете получить замену push-уведомлений, как в официальном приложении. Все, что вам нужно сделать, это разрешить Wulkanowy получать все уведомления в настройках системы.\n\nКак это работает?\nКогда вы получаете уведомление в Dziennik VULCAN, Wulkanowy будет уведомлен (это требует дополнительных прав) и запустит синхронизацию, чтобы отправить свое уведомление.\n\nТОЛЬКО ДЛЯ ПОЛЬЗОВАТЕЛЯ + Перейти к настройкам Синхронизация Автоматическая синхронизация Приостановить синхронизации во время каникул diff --git a/app/src/main/res/values-sk/preferences_values.xml b/app/src/main/res/values-sk/preferences_values.xml index 108af555..b8974f23 100644 --- a/app/src/main/res/values-sk/preferences_values.xml +++ b/app/src/main/res/values-sk/preferences_values.xml @@ -41,8 +41,8 @@ Farby známok v denníku - Priemer známok až od druhého semestra - Priemer známok z oboch semestrov + Priemer známok iba z vybraného semestra + Priemer z priemerov z oboch semestrov Priemer známok z celého roka diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index b1fd78ae..19e9735f 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -12,7 +12,7 @@ O aplikácii Prehliadač protokolov Ladenie - Ladenie oznámení + Ladenie upozornení Prispievatelia Licencie Správy @@ -24,6 +24,7 @@ Podrobnosti účtu Informácie o žiakovi Domov + Centrum upozornení Semester %1$d, %2$d/%3$d @@ -43,7 +44,7 @@ Prihlásiť Toto heslo je príliš krátke Prihlasovacie údaje sú nesprávne - %1$s. Uistite sa, že je v poli nižšie vybraná správna variácie denníka UONET+ + %1$s. Skontrolujte, či je nižšie vybratá správna variácie denníka UONET+ Neplatný PIN Neplatný token Platnosť tokenu vypršala @@ -91,6 +92,10 @@ Konečná známka Predpokladaná známka Vypočítaný priemer + Ako funguje vypočítaný priemer? + Vypočítaný priemer je aritmetický priemer vypočítaný z priemerov predmetov. Umožňuje vám to poznať približný konečný priemer. Vypočítava sa spôsobom zvoleným užívateľom v nastaveniach aplikácii. Odporúča sa vybrať príslušnú možnosť. Dôvodom je rozdielny výpočet školských priemerov. Ak vaša škola navyše uvádza priemer predmetov na stránke denníka Vulcan, aplikácia si ich stiahne a tieto priemery nepočíta. To možno zmeniť vynútením výpočtu priemeru v nastavení aplikácii.\n\nPriemer známok iba z vybraného semestra:\n1. Výpočet váženého priemeru pre každý predmet v danom semestri\n2. Sčítanie vypočítaných priemerov\n3. Výpočet aritmetického priemeru součtených priemerov\n\nPriemer priemerov z oboch semestrov:\n1. Výpočet váženého priemeru pre každý predmet v semestri 1 a 2\n2. Výpočet aritmetického priemeru vypočítaných priemerov za semestre 1 a 2 pre každý predmet.\n3. Sčítanie vypočítaných priemerov\n4. Výpočet aritmetického priemeru součtených priemerov\n\nPriemer známok z celého roka:\n1. Výpočet váženého priemeru za rok pre každý predmet. Konečný priemer v 1. semestri je nepodstatný.\n3. Sčítanie vypočítaných priemerov\n4. Výpočet aritmetického priemeru součtených priemerov + Ako funguje konečný priemer? + Konečný priemer je aritmetický priemer vypočítaný zo všetkých aktuálne dostupných konečných známok v danom semestri.\n\nSchéma výpočtu sa skladá z nasledujúcich krokov:\n1. Sčítanie konečných známok zadaných učiteľmi\n2. Delené počtom predmetov, pre ktoré už boli vydané známky Konečný priemer z %1$d z %2$d predmetov Zhrnutie @@ -239,12 +244,7 @@ Iba neprečítané Iba s prílohami Prečítaná: %s - - Prečítaná cez: %1$d z %2$d osôb - Prečítaná cez: %1$d z %2$d osôb - Prečítaná cez: %1$d z %2$d osôb - Prečítaná cez: %1$d z %2$d osôb - + Prečítaná cez: %1$d z %2$d osôb %d správa %d správy @@ -403,7 +403,7 @@ Máte %1$d nových stretnutí Máte %1$d nových stretnutí - Present at conference + Prítomnosť na stretnutí Agenda Školské oznámenia @@ -593,7 +593,7 @@ Áno Nie Uložiť - Title + Titul Žiadne lekcie Vybrať motív @@ -603,7 +603,7 @@ Vzhľad a správanie aplikácií Predvolené zobrazenie - Výpočet koncoročního priemeru + Možnosti vypočítaného priemeru Vynútiť priemerný výpočet podľa aplikácie Zobraziť prítomnosť Motív @@ -618,12 +618,18 @@ Upozornenia Zobraziť upozornenia Zobraziť upozornenia o nadchádzajúcej lekciu + Nastaviť upozornenia o nadchádzajúcej lekcii ako trvalé + Vypnúť, keď upozornenia nie je vo vašom hodinkách/náramku viditeľné Otvoriť systémové nastavenia upozornení Opravte problémy so synchronizáciou a upozornením Vaše zariadenie môže mať problémy so synchronizáciou dát as upozorneniami.\n\nAk ich chcete opraviť, pridajte Wulkanového do funkcie Autostart a vypnite optimalizáciu/úsporu batérie v nastavení systému telefóne. Prejsť do nastavení Zobraziť upozornenia o ladení Synchronizácia je vypnutá + Zachytiť upozornenia oficiálnej aplikácie + Zachytiť upozornenia + S touto funkciou môžete získať náhradu push upozornení ako v oficiálnej aplikácii. Všetko, čo musíte urobiť, je povoliť Wulkanowému čítať všetky vaše upozornenia v nastaveniach systému.\n\nAko to funguje?\nKeď dostanete oznámenie v Deníčku VULCAN, Wulkanowy bude o tom informovaný (k tomu je to dodatočné povolenie) a spustí synchronizáciu, aby mohol zaslať vlastné upozornenie.\n\nLEN PRE POKROČILÝCH POUŽĺVATEĹOV + Prejsť do nastavení Synchronizácia Automatická aktualizácia Pozastavený počas dovolenky diff --git a/app/src/main/res/values-uk/preferences_values.xml b/app/src/main/res/values-uk/preferences_values.xml index f6f5b984..f21ad819 100644 --- a/app/src/main/res/values-uk/preferences_values.xml +++ b/app/src/main/res/values-uk/preferences_values.xml @@ -41,8 +41,8 @@ Кольори оцінок в щоденнику - Середня оцінка з 2 семестру - Середнє оцінювання за обидва семестри + Середні оцінки тільки від обраного семестру + Середнє значення для обох семестів Середнє оцінювання за весь рік diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 70f40b66..58fed988 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -24,6 +24,7 @@ Деталі облікового запису Інформація про учня Дошка + Журнал сповіщень %1$d семестр, %2$d/%3$d @@ -42,8 +43,8 @@ Symbol Увійти Занадто короткий пароль - Дані для входу неправильні - %1$s. Переконайтеся, що у полі нижче вказано правильний варіант реєстрації UONET+ + Вказані невірні дані + %1$s. Переконайтеся, що обрано правильну варіацію запису UONET+ Неправильний PIN Неправильний token Минув термін дії токену @@ -91,6 +92,10 @@ Підсумкова оцінка Очікувана оцінка Розрахована середня оцінка + Як розраховується середньо? + Розрахункове середнє - це середнє арифметичне, обчислене з середніх показників для випробуваних. Це дозволяє дізнатися приблизну кінцеву середню величину. Він розраховується способом, обраним користувачем у налаштуваннях програми. Рекомендується вибрати відповідний варіант. Це пояснюється тим, що розрахунок середніх показників за школою відрізняється. Крім того, якщо у вашій школі повідомляється середнє значення предметів на сторінці Вулкан, програма завантажує їх і не обчислює ці середні значення. Це можна змінити шляхом примусового розрахунку середнього значення в налаштуваннях програми. \ N \ n Середні оцінки лише за вибраний семестр : \ n1. Розрахунок середньозваженого для кожного предмета в даному семестрі \ n2.Додавання розрахункових середніх \ n3. Розрахунок середнього арифметичного підсумованих середніх значень \ n \ n Середнє значення середніх показників за обидва семестри : \ n1.Обчислення середньозваженого значення для кожного предмета у 1 та 2 семестрах \ n2. Обчислення середнього арифметичного розрахункових середніх показників за 1 та 2 семестри для кожного предмета. \ N3. Додавання розрахункових середніх \ n4. Розрахунок середнього арифметичного підсумованих середніх значень \ n \ n Середнє значення оцінок за весь рік: \ n1. Розрахунок середньозваженого показника за рік для кожного предмета. Остаточний середній показник у 1 -му семестрі не має значення. \ N3. Додавання розрахункових середніх \ n4. Обчислення середнього арифметичного середніх суммованих середніх + Як працює кінцевий середній показник? + Підсумкове середнє значення - це середнє арифметичне, обчислене з усіх наявних наразі підсумкових оцінок у даному семестрі. \ N \ nСхема обчислення складається з таких кроків: \ n1. Підбиття підсумкових оцінок викладачів \ n2. Поділіть на кількість предметів, які вже оцінені Підсумкова середня оцінка з %1$d із %2$d тем Підсумок @@ -239,12 +244,7 @@ Лише непрочитані Тільки з вкладеннями Читання: %s - - Прочитані: %1$d з %2$d осіб - Прочитані: %1$d з %2$d осіб - Прочитані: %1$d з %2$d осіб - Прочитані: %1$d з %2$d осіб - + Прочитанно:%1$d через %2$d людей %d повідомлення %d повідомлення @@ -403,8 +403,8 @@ У вас є %1$d нова конференція У вас є %1$d нових конференцій - Present at conference - Agenda + Присутність на конференції + Порядок денний Оголошення школи Жодних навчальних оголошень @@ -593,7 +593,7 @@ Так Ні Зберегти - Title + Титул Брак уроків Увібрати тему @@ -603,7 +603,7 @@ Поява додатка & amp; поведінки Вікно за замовчуванням - Спосіб облічування оцінки на кінець року + Розрахункові середні параметри Примусово розрахувати середню оцінку через додаток Показати присутність Тема @@ -618,12 +618,18 @@ Повідомлення Показувати повідомлення Показувати повідомлення о наступних уроках + Зробити сповіщення майбутнього уроку нестійкими + Вимкнути коли сповіщення не показуються у відстежувачі/темпі Відкрити налаштування сповіщень системи Виправити помилки з синхронізацією і повідомленнями На вашому пристрої можуть бути помилки з синхронізацією і повідомленнями\n\nЩоб виправити іх, вам необхідно додати Wulkanowy в авто-старт и вимкнути оптимізацію/экономію батареї в налаштуваннях пристрою. Перейти до налаштувань Показувати дебаг-повідомлення Синхронізація вимкнена + Захоплювати офіційні сповіщення програм + Показувати push-повідомлення + За допомогою цієї функції ви можете отримати заміну push -повідомлень, як у офіційному додатку. Все, що вам потрібно зробити, це дозволити Wulkanowy отримувати всі сповіщення у налаштуваннях вашої системи. \ N \ nЯк це працює? \ NКоли ви отримаєте сповіщення у Dziennik VULCAN, Wulkanowy отримає сповіщення (для цього призначені ці додаткові дозволи) і запустить синхронізація, яка може надсилати власне сповіщення. \ n \ n ТІЛЬКИ ДЛЯ РОЗШИРЕНИХ КОРИСТУВАЧІВ + Перейти до налаштувань Синхронізація Автоматична синхронізація Призупинено на час канікул From 0b83a66b85a16d5e590078825fee4f6cf461959f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 28 Sep 2021 23:10:11 +0200 Subject: [PATCH 026/357] Remove disappearing teachers workaround from timetable repository (#1545) --- .../data/repositories/TimetableRepository.kt | 12 +- .../repositories/TimetableRepositoryTest.kt | 129 +----------------- 2 files changed, 3 insertions(+), 138 deletions(-) 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 index 1540d3cc..769fa0f0 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt @@ -123,17 +123,7 @@ class TimetableRepository @Inject constructor( lessonsNew: List, ) { val lessonsToRemove = lessonsOld uniqueSubtract lessonsNew - val lessonsToAdd = (lessonsNew uniqueSubtract lessonsOld).map { new -> - val matchingOld = lessonsOld.singleOrNull { new.start == it.start } - if (matchingOld != null) { - val useOldTeacher = new.teacher.isEmpty() && !new.changes && !matchingOld.changes - new.copy( - room = if (new.room.isEmpty()) matchingOld.room else new.room, - teacher = if (useOldTeacher) matchingOld.teacher - else new.teacher - ) - } else new - } + val lessonsToAdd = lessonsNew uniqueSubtract lessonsOld timetableDb.deleteAll(lessonsToRemove) timetableDb.insertAll(lessonsToAdd) diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt index 75c75a66..edb3125e 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt @@ -67,136 +67,11 @@ class TimetableRepositoryTest { timetableRepository = TimetableRepository(timetableDb, timetableAdditionalDao, timetableHeaderDao, sdk, timetableNotificationSchedulerHelper, refreshHelper) } - @Test - fun copyRoomToCompletedFromPrevious() { - // prepare - val remoteList = listOf( - createTimetableRemote(of(2021, 1, 4, 8, 0), 1, "", "Przyroda"), - createTimetableRemote(of(2021, 1, 4, 8, 50), 2, "", "Religia"), - createTimetableRemote(of(2021, 1, 4, 9, 40), 3, "", "W-F"), - createTimetableRemote(of(2021, 1, 4, 10, 30), 4, "", "W-F") - ) - coEvery { sdk.getTimetableFull(any(), any()) } returns TimetableFull(emptyList(), remoteList, emptyList()) - - val localList = listOf( - createTimetableRemote(of(2021, 1, 4, 8, 0), 1, "123", "Przyroda"), - createTimetableRemote(of(2021, 1, 4, 8, 50), 2, "321", "Religia"), - createTimetableRemote(of(2021, 1, 4, 9, 40), 3, "213", "W-F"), - createTimetableRemote(of(2021, 1, 4, 10, 30), 3, "213", "W-F", "Jan Kowalski") - ) - coEvery { timetableDb.loadAll(semester.diaryId, semester.studentId, startDate, endDate) } returns flowOf(localList.mapToEntities(semester)) - coEvery { timetableDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { timetableDb.deleteAll(any()) } just Runs - - coEvery { timetableAdditionalDao.loadAll(1, 1, startDate, endDate) } returns flowOf(listOf()) - coEvery { timetableAdditionalDao.insertAll(emptyList()) } returns listOf(1, 2, 3) - coEvery { timetableAdditionalDao.deleteAll(emptyList()) } just Runs - - coEvery { timetableHeaderDao.loadAll(1, 1, startDate, endDate) } returns flowOf(listOf()) - coEvery { timetableHeaderDao.insertAll(emptyList()) } returns listOf(1, 2, 3) - coEvery { timetableHeaderDao.deleteAll(emptyList()) } just Runs - - // execute - val res = runBlocking { - timetableRepository.getTimetable(student, semester, startDate, endDate, true).toFirstResult() - } - - // verify - assertEquals(4, res.data?.lessons.orEmpty().size) - coVerify { - timetableDb.insertAll(withArg { - assertEquals(4, it.size) - assertEquals("123", it[0].room) - assertEquals("321", it[1].room) - assertEquals("213", it[2].room) - }) - } - coVerify { timetableDb.deleteAll(match { it.size == 4 }) } - } - - @Test - fun copyTeacherToCompletedFromPrevious() { - // prepare - val remoteList = listOf( - createTimetableRemote(of(2021, 1, 4, 8, 0), 1, "123", "Matematyka", "Paweł Poniedziałkowski", false), // skip - createTimetableRemote(of(2021, 1, 4, 8, 50), 2, "124", "Matematyka", "Jakub Wtorkowski", true), - createTimetableRemote(of(2021, 1, 4, 9, 40), 3, "125", "Język polski", "Joanna Poniedziałkowska", false), - createTimetableRemote(of(2021, 1, 4, 10, 40), 4, "126", "Język polski", "Joanna Wtorkowska", true), // skip - - createTimetableRemote(of(2021, 1, 5, 8, 0), 1, "123", "Język polski", "", false), - createTimetableRemote(of(2021, 1, 5, 8, 50), 2, "124", "Język polski", "", true), - createTimetableRemote(of(2021, 1, 5, 9, 40), 3, "125", "Język polski", "", false), - createTimetableRemote(of(2021, 1, 5, 10, 40), 4, "126", "Język polski", "", true), - - createTimetableRemote(of(2021, 1, 6, 8, 0), 1, "123", "Matematyka", "Paweł Środowski", false), - createTimetableRemote(of(2021, 1, 6, 8, 50), 2, "124", "Matematyka", "Paweł Czwartkowski", true), - createTimetableRemote(of(2021, 1, 6, 9, 40), 3, "125", "Matematyka", "Paweł Środowski", false), - createTimetableRemote(of(2021, 1, 6, 10, 40), 4, "126", "Matematyka", "Paweł Czwartkowski", true) - ) - coEvery { sdk.getTimetableFull(startDate, endDate) } returns TimetableFull(emptyList(), remoteList, emptyList()) - - val localList = listOf( - createTimetableRemote(of(2021, 1, 4, 8, 0), 1, "123", "Matematyka", "Paweł Poniedziałkowski", false), - createTimetableRemote(of(2021, 1, 4, 8, 50), 2, "124", "Matematyka", "Paweł Poniedziałkowski", false), - createTimetableRemote(of(2021, 1, 4, 9, 40), 3, "125", "Język polski", "Joanna Wtorkowska", true), - createTimetableRemote(of(2021, 1, 4, 10, 40), 4, "126", "Język polski", "Joanna Wtorkowska", true), - - createTimetableRemote(of(2021, 1, 5, 8, 0), 1, "123", "Język polski", "Joanna Wtorkowska", false), - createTimetableRemote(of(2021, 1, 5, 8, 50), 2, "124", "Język polski", "Joanna Wtorkowska", false), - createTimetableRemote(of(2021, 1, 5, 9, 40), 3, "125", "Język polski", "Joanna Środowska", true), - createTimetableRemote(of(2021, 1, 5, 10, 40), 4, "126", "Język polski", "Joanna Środowska", true), - - createTimetableRemote(of(2021, 1, 6, 8, 0), 1, "123", "Matematyka", "", false), - createTimetableRemote(of(2021, 1, 6, 8, 50), 2, "124", "Matematyka", "", false), - createTimetableRemote(of(2021, 1, 6, 9, 40), 3, "125", "Matematyka", "", true), - createTimetableRemote(of(2021, 1, 6, 10, 40), 4, "126", "Matematyka", "", true) - ) - coEvery { timetableDb.loadAll(semester.diaryId, semester.studentId, startDate, endDate) } returns flowOf(localList.mapToEntities(semester)) - coEvery { timetableDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { timetableDb.deleteAll(any()) } just Runs - - coEvery { timetableAdditionalDao.loadAll(1, 1, startDate, endDate) } returns flowOf(listOf()) - coEvery { timetableAdditionalDao.insertAll(emptyList()) } returns listOf(1, 2, 3) - coEvery { timetableAdditionalDao.deleteAll(emptyList()) } just Runs - - coEvery { timetableHeaderDao.loadAll(1, 1, startDate, endDate) } returns flowOf(listOf()) - coEvery { timetableHeaderDao.insertAll(emptyList()) } returns listOf(1, 2, 3) - coEvery { timetableHeaderDao.deleteAll(emptyList()) } just Runs - - // execute - val res = runBlocking { timetableRepository.getTimetable(student, semester, startDate, endDate, true).toFirstResult() } - - // verify - assertEquals(null, res.error) - assertEquals(12, res.data!!.lessons.size) - - coVerify { - timetableDb.insertAll(withArg { - assertEquals(10, it.size) -// assertEquals("Paweł Poniedziałkowski", it[0].teacher) // skip - assertEquals("Jakub Wtorkowski", it[0].teacher) - assertEquals("Joanna Poniedziałkowska", it[1].teacher) -// assertEquals("Joanna Wtorkowska", it[3].teacher) // skip - - assertEquals("Joanna Wtorkowska", it[2].teacher) - assertEquals("", it[3].teacher) - assertEquals("", it[4].teacher) - assertEquals("", it[5].teacher) - - assertEquals("Paweł Środowski", it[6].teacher) - assertEquals("Paweł Czwartkowski", it[7].teacher) - assertEquals("Paweł Środowski", it[8].teacher) - assertEquals("Paweł Czwartkowski", it[9].teacher) - }) - } - coVerify { timetableDb.deleteAll(match { it.size == 10 }) } - } - @Test fun `force refresh without difference`() { val remoteList = listOf( - createTimetableRemote(of(2021, 1, 4, 8, 0), 1, "123", "Język polski", "", false), - createTimetableRemote(of(2021, 1, 4, 8, 50), 2, "124", "Język polski", "", true) + createTimetableRemote(of(2021, 1, 4, 8, 0), 1, "123", "Język polski", "Jan Kowalski", false), + createTimetableRemote(of(2021, 1, 4, 8, 50), 2, "124", "Język niemiecki", "Joanna Czarniecka", true) ) // prepare From 9c8bcbfdd38ae9c1289aa59efb218b126b6cde89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Tue, 28 Sep 2021 23:11:59 +0200 Subject: [PATCH 027/357] New Crowdin updates (#1544) --- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 48d8a21a..92af176f 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -93,7 +93,7 @@ Ожидаемая оценка Рассчитанная средняя оценка Как рассчитывается средняя работа? - Расчетное среднее - это среднее арифметическое, рассчитанное на основе средних значений испытуемых. Это позволяет узнать приблизительное итоговое среднее значение. Он рассчитывается способом, выбранным пользователем в настройках приложения. Рекомендуется выбрать подходящий вариант. Это потому, что расчет средних показателей школы отличается. Кроме того, если ваша школа сообщает среднее значение по предметам на странице Vulcan, приложение загружает их и не вычисляет эти средние значения. Это можно изменить, принудительно вычисляя среднее значение в настройках приложения. \ N \ n Среднее значение только за выбранный семестр : \ n1. Вычисление средневзвешенного значения по каждому предмету за семестр \ n2.Добавление вычисленных средних \ n3. Вычисление среднего арифметического суммарных средних \ n \ n Среднее из средних значений за оба семестра : \ n1.Расчет средневзвешенного значения для каждого предмета в семестрах 1 и 2 \ n2. Вычисление среднего арифметического рассчитанных средних значений для семестров 1 и 2 по каждому предмету. \ N3. Добавление вычисленных средних \ n4. Расчет среднего арифметического суммированных средних \ n \ n Среднее значение оценок за весь год: \ n1. Расчет средневзвешенного значения за год по каждому предмету. Итоговое среднее значение за 1 семестр не имеет значения. \ N3. Добавление вычисленных средних \ n4. Расчет среднего арифметического + Расчетное среднее - это среднее арифметическое, рассчитанное на основе средних значений испытуемых. Это позволяет узнать приблизительное итоговое среднее значение. Он рассчитывается способом, выбранным пользователем в настройках приложения. Рекомендуется выбрать подходящий вариант. Это потому, что расчет средних показателей школы отличается. Кроме того, если ваша школа сообщает среднее значение по предметам на странице Vulcan, приложение загружает их и не вычисляет эти средние значения. Это можно изменить, принудительно вычисляя среднее значение в настройках приложения.\n\nСреднее значение только за выбранный семестр :\n1. Вычисление средневзвешенного значения по каждому предмету за семестр\n2.Добавление вычисленных средних\n3. Вычисление среднего арифметического суммарных средних\n\nСреднее из средних значений за оба семестра:\n1.Расчет средневзвешенного значения для каждого предмета в семестрах 1 и 2\n2. Вычисление среднего арифметического рассчитанных средних значений для семестров 1 и 2 по каждому предмету.\n3. Добавление вычисленных средних\n4. Расчет среднего арифметического суммированных средних\n\nСреднее значение оценок за весь год: \n1. Расчет средневзвешенного значения за год по каждому предмету. Итоговое среднее значение за 1 семестр не имеет значения.\n3. Добавление вычисленных средних\n4. Расчет среднего арифметического Как работает окончательный средний показатель? Среднее арифметическое - это среднее арифметическое, рассчитанное по всем имеющимся на данный момент итоговым классам данного семестра.\n\nСхема расчета состоит из следующих шагов:\n1. Суммирование итоговых классов преподавателей\n2. Деление по количеству уже оцененных предметов Итоговая средняя оценка diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 58fed988..74175548 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -93,7 +93,7 @@ Очікувана оцінка Розрахована середня оцінка Як розраховується середньо? - Розрахункове середнє - це середнє арифметичне, обчислене з середніх показників для випробуваних. Це дозволяє дізнатися приблизну кінцеву середню величину. Він розраховується способом, обраним користувачем у налаштуваннях програми. Рекомендується вибрати відповідний варіант. Це пояснюється тим, що розрахунок середніх показників за школою відрізняється. Крім того, якщо у вашій школі повідомляється середнє значення предметів на сторінці Вулкан, програма завантажує їх і не обчислює ці середні значення. Це можна змінити шляхом примусового розрахунку середнього значення в налаштуваннях програми. \ N \ n Середні оцінки лише за вибраний семестр : \ n1. Розрахунок середньозваженого для кожного предмета в даному семестрі \ n2.Додавання розрахункових середніх \ n3. Розрахунок середнього арифметичного підсумованих середніх значень \ n \ n Середнє значення середніх показників за обидва семестри : \ n1.Обчислення середньозваженого значення для кожного предмета у 1 та 2 семестрах \ n2. Обчислення середнього арифметичного розрахункових середніх показників за 1 та 2 семестри для кожного предмета. \ N3. Додавання розрахункових середніх \ n4. Розрахунок середнього арифметичного підсумованих середніх значень \ n \ n Середнє значення оцінок за весь рік: \ n1. Розрахунок середньозваженого показника за рік для кожного предмета. Остаточний середній показник у 1 -му семестрі не має значення. \ N3. Додавання розрахункових середніх \ n4. Обчислення середнього арифметичного середніх суммованих середніх + Розрахункове середнє - це середнє арифметичне, обчислене з середніх показників для випробуваних. Це дозволяє дізнатися приблизну кінцеву середню величину. Він розраховується способом, обраним користувачем у налаштуваннях програми. Рекомендується вибрати відповідний варіант. Це пояснюється тим, що розрахунок середніх показників за школою відрізняється. Крім того, якщо у вашій школі повідомляється середнє значення предметів на сторінці Вулкан, програма завантажує їх і не обчислює ці середні значення. Це можна змінити шляхом примусового розрахунку середнього значення в налаштуваннях програми.\n\nСередні оцінки лише за вибраний семестр :\n1. Розрахунок середньозваженого для кожного предмета в даному семестрі\n2.Додавання розрахункових середніх\n3. Розрахунок середнього арифметичного підсумованих середніх значень\n\nСереднє значення середніх показників за обидва семестри :\n1.Обчислення середньозваженого значення для кожного предмета у 1 та 2 семестрах\n2. Обчислення середнього арифметичного розрахункових середніх показників за 1 та 2 семестри для кожного предмета.\n3. Додавання розрахункових середніх\n4. Розрахунок середнього арифметичного підсумованих середніх значень\n\nСереднє значення оцінок за весь рік: \n1. Розрахунок середньозваженого показника за рік для кожного предмета. Остаточний середній показник у 1 -му семестрі не має значення.\n3. Додавання розрахункових середніх \n4. Обчислення середнього арифметичного середніх суммованих середніх Як працює кінцевий середній показник? Підсумкове середнє значення - це середнє арифметичне, обчислене з усіх наявних наразі підсумкових оцінок у даному семестрі. \ N \ nСхема обчислення складається з таких кроків: \ n1. Підбиття підсумкових оцінок викладачів \ n2. Поділіть на кількість предметів, які вже оцінені Підсумкова середня оцінка From 6cdcf927824380da83700e79cf88da5e903ecc97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 28 Sep 2021 23:26:10 +0200 Subject: [PATCH 028/357] Version 1.3.0 --- app/build.gradle | 6 +++--- app/src/main/play/release-notes/pl-PL/default.txt | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e8680fe2..1d6e56b1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,8 +21,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 30 - versionCode 96 - versionName "1.2.3" + versionCode 97 + versionName "1.3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -166,7 +166,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:f62736adb0" + implementation "io.github.wulkanowy:sdk:1.3.0" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 7de10a26..fc9fab88 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,8 +1,9 @@ -Wersja 1.2.3 +Wersja 1.3.0 -- naprawiliśmy pomieszane imiona nauczycieli z salami w planie lekcji -- dodaliśmy brakujące okienka ze szczegółami na ekranie zebrań -- klikając w kafelek z lekcjami na jutro aplikacja teraz przekierowuje na ekran z planem na jutro -- naprawiliśmy błąd przy wylogowywaniu innego niż bieżący uczeń +- naprawiliśmy logowanie na platformę Opolskiej eSzkoły +- dodaliśmy centrum powiadomień i opcję odbierania pushy z oficjalnej aplikacji (dla zaawansowanych) +- dodaliśmy objaśnienie do informacji o obliczonych średnich w podsumowaniu +- poprawiliśmy wyświetlanie zmian w planie lekcji +- dokonaliśmy też kilka innych zmian i kosmetycznych poprawek poprawiających komfort używania aplikacji Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases From e02d93f9797c8ada9bb48372e9363d697e298140 Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Sun, 3 Oct 2021 01:07:45 +0200 Subject: [PATCH 029/357] Add Czech and Slovak listings (#1555) --- .../main/play/listings/cs-CZ/full-description.txt | 14 ++++++++++++++ .../main/play/listings/cs-CZ/short-description.txt | 1 + app/src/main/play/listings/cs-CZ/title.txt | 1 + app/src/main/play/listings/sk/full-description.txt | 14 ++++++++++++++ .../main/play/listings/sk/short-description.txt | 1 + app/src/main/play/listings/sk/title.txt | 1 + 6 files changed, 32 insertions(+) create mode 100644 app/src/main/play/listings/cs-CZ/full-description.txt create mode 100644 app/src/main/play/listings/cs-CZ/short-description.txt create mode 100644 app/src/main/play/listings/cs-CZ/title.txt create mode 100644 app/src/main/play/listings/sk/full-description.txt create mode 100644 app/src/main/play/listings/sk/short-description.txt create mode 100644 app/src/main/play/listings/sk/title.txt diff --git a/app/src/main/play/listings/cs-CZ/full-description.txt b/app/src/main/play/listings/cs-CZ/full-description.txt new file mode 100644 index 00000000..1420f5d6 --- /dev/null +++ b/app/src/main/play/listings/cs-CZ/full-description.txt @@ -0,0 +1,14 @@ +Aplikace je určena pro uživatele deníku VULCAN UONET+. + +Zvýrazněné vlastnosti a funkce: +- výpočet váženého průměru, +- procentuální zobrazení docházky, +- šťastné číslo, +- náhled na další a dokončené lekce, +- tmavý motiv, +- žádné reklamy, +- offline režim, +- upozornění. + +GitHub: https://github.com/wulkanowy/wulkanowy +Discord: https://discord.gg/vccAQBr diff --git a/app/src/main/play/listings/cs-CZ/short-description.txt b/app/src/main/play/listings/cs-CZ/short-description.txt new file mode 100644 index 00000000..0f29ab1b --- /dev/null +++ b/app/src/main/play/listings/cs-CZ/short-description.txt @@ -0,0 +1 @@ +Neoficiální aplikace žáka a rodiče pro deníku VULCAN UONET+ diff --git a/app/src/main/play/listings/cs-CZ/title.txt b/app/src/main/play/listings/cs-CZ/title.txt new file mode 100644 index 00000000..b7f42a5b --- /dev/null +++ b/app/src/main/play/listings/cs-CZ/title.txt @@ -0,0 +1 @@ +Wulkanowy Deníček diff --git a/app/src/main/play/listings/sk/full-description.txt b/app/src/main/play/listings/sk/full-description.txt new file mode 100644 index 00000000..2a4787d2 --- /dev/null +++ b/app/src/main/play/listings/sk/full-description.txt @@ -0,0 +1,14 @@ +Aplikácia je určená pre užívateľov denníka VULCAN UONET+. + +Zvýraznené vlastnosti a funkcie: +- výpočet váženého priemeru, +- percentuálne zobrazenie dochádzky, +- šťastné číslo, +- náhľad na ďalšie a dokončené lekcie, +- tmavý motív, +- žiadne reklamy, +- offline režim, +- upozornenia. + +GitHub: https://github.com/wulkanowy/wulkanowy +Discord: https://discord.gg/vccAQBr diff --git a/app/src/main/play/listings/sk/short-description.txt b/app/src/main/play/listings/sk/short-description.txt new file mode 100644 index 00000000..645ebbb6 --- /dev/null +++ b/app/src/main/play/listings/sk/short-description.txt @@ -0,0 +1 @@ +Neoficiálna aplikácia žiaka a rodiča pre denníka VULCAN UONET+ diff --git a/app/src/main/play/listings/sk/title.txt b/app/src/main/play/listings/sk/title.txt new file mode 100644 index 00000000..aa50ce77 --- /dev/null +++ b/app/src/main/play/listings/sk/title.txt @@ -0,0 +1 @@ +Wulkanowy Denníček From 8e607d48f7388db76e359d44192e9d01118a5f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sun, 3 Oct 2021 10:36:17 +0200 Subject: [PATCH 030/357] Add coroutines scope to presenter (#1554) --- .../github/wulkanowy/ui/base/BasePresenter.kt | 26 +++++++++---------- .../modules/dashboard/DashboardPresenter.kt | 2 +- .../NotificationDebugPresenter.kt | 2 +- .../ui/modules/message/MessagePresenter.kt | 2 +- .../message/send/SendMessagePresenter.kt | 4 +-- .../message/tab/MessageTabPresenter.kt | 4 +-- .../mobiledevice/MobileDevicePresenter.kt | 3 +-- .../ui/modules/note/NotePresenter.kt | 19 +++++++------- .../SchoolAndTeachersPresenter.kt | 2 +- 9 files changed, 31 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt index 6f363bfc..5cd5d010 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt @@ -6,29 +6,27 @@ import io.github.wulkanowy.utils.flowWithResource import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import timber.log.Timber -import kotlin.coroutines.CoroutineContext open class BasePresenter( protected val errorHandler: ErrorHandler, protected val studentRepository: StudentRepository -) : CoroutineScope { +) { + private val job = SupervisorJob() - private var job = Job() + protected val presenterScope = CoroutineScope(job + Dispatchers.Main) - private val jobs = mutableMapOf() - - override val coroutineContext: CoroutineContext - get() = Dispatchers.Main + job + private val childrenJobs = mutableMapOf() var view: T? = null open fun onAttachView(view: T) { - job = Job() this.view = view errorHandler.apply { showErrorMessage = view::showError @@ -64,22 +62,22 @@ open class BasePresenter( } fun Flow.launch(individualJobTag: String = "load"): Job { - jobs[individualJobTag]?.cancel() - val job = catch { errorHandler.dispatch(it) }.launchIn(this@BasePresenter) - jobs[individualJobTag] = job + childrenJobs[individualJobTag]?.cancel() + val job = catch { errorHandler.dispatch(it) }.launchIn(presenterScope) + childrenJobs[individualJobTag] = job Timber.d("Job $individualJobTag launched in ${this@BasePresenter.javaClass.simpleName}: $job") return job } fun cancelJobs(vararg names: String) { names.forEach { - jobs[it]?.cancel() + childrenJobs[it]?.cancel() } } open fun onDetachView() { - view = null - job.cancel() + job.cancelChildren() errorHandler.clear() + view = null } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index c513a9f6..d6d533e2 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -149,7 +149,7 @@ class DashboardPresenter @Inject constructor( tileList: List, forceRefresh: Boolean ) { - launch { + presenterScope.launch { Timber.i("Loading dashboard account data started") val student = runCatching { studentRepository.getCurrentStudent(true) } .onFailure { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt index 6103317d..26eca18f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt @@ -88,7 +88,7 @@ class NotificationDebugPresenter @Inject constructor( } private fun withStudent(block: suspend (Student) -> Unit) { - launch { + presenterScope.launch { block(studentRepository.getCurrentStudent(false)) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt index 7b8c3d0f..be694052 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt @@ -15,7 +15,7 @@ class MessagePresenter @Inject constructor( override fun onAttachView(view: MessageView) { super.onAttachView(view) - launch { + presenterScope.launch { delay(150) view.initView() Timber.i("Message view was initialized") diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt index 60a23e58..77fa8231 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt @@ -224,14 +224,14 @@ class SendMessagePresenter @Inject constructor( } fun onMessageContentChange() { - launch { + presenterScope.launch { messageUpdateChannel.send(Unit) } } @OptIn(FlowPreview::class) private fun initializeSubjectStream() { - launch { + presenterScope.launch { messageUpdateChannel.consumeAsFlow() .debounce(250) .catch { Timber.e(it) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt index a24f9b79..4571390b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt @@ -173,14 +173,14 @@ class MessageTabPresenter @Inject constructor( } fun onSearchQueryTextChange(query: String) { - launch { + presenterScope.launch { searchChannel.send(query) } } @OptIn(FlowPreview::class) private fun initializeSearchStream() { - launch { + presenterScope.launch { searchChannel.consumeAsFlow() .debounce(250) .map { query -> diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt index 9591867d..53049891 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt @@ -11,7 +11,6 @@ import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResourceIn -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -148,6 +147,6 @@ class MobileDevicePresenter @Inject constructor( errorHandler.dispatch(it.error!!) } } - }.launchIn(this) + }.launch("unregister") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt index c441231e..10a39182 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt @@ -11,7 +11,6 @@ import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResourceIn -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -123,15 +122,17 @@ class NotePresenter @Inject constructor( } private fun updateNote(note: Note) { - flowWithResource { noteRepository.updateNote(note) }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Attempt to update note ${note.id}") - Status.SUCCESS -> Timber.i("Update note result: Success") - Status.ERROR -> { - Timber.i("Update note result: An exception occurred") - errorHandler.dispatch(it.error!!) + flowWithResource { noteRepository.updateNote(note) } + .onEach { + when (it.status) { + Status.LOADING -> Timber.i("Attempt to update note ${note.id}") + Status.SUCCESS -> Timber.i("Update note result: Success") + Status.ERROR -> { + Timber.i("Update note result: An exception occurred") + errorHandler.dispatch(it.error!!) + } } } - }.launchIn(this) + .launch("update_note") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt index 915cc421..c14272b9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt @@ -15,7 +15,7 @@ class SchoolAndTeachersPresenter @Inject constructor( override fun onAttachView(view: SchoolAndTeachersView) { super.onAttachView(view) - launch { + presenterScope.launch { delay(150) view.initView() Timber.i("Message view was initialized") From 60501fcd728bae993c110a4ed500b5dd66012ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 3 Oct 2021 14:13:42 +0200 Subject: [PATCH 031/357] Set buildTimestamp through manifest meta (#1556) --- app/build.gradle | 4 ++-- app/src/main/AndroidManifest.xml | 10 ++++++++-- .../java/io/github/wulkanowy/utils/AppInfo.kt | 16 +++++++++++++--- .../data/db/migrations/AbstractMigrationTest.kt | 5 +++-- .../data/db/migrations/Migration35Test.kt | 6 +++--- .../wulkanowy/data/repositories/StudentTest.kt | 14 +++++++------- 6 files changed, 36 insertions(+), 19 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1d6e56b1..e8762dda 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,10 +27,10 @@ android { vectorDrawables.useSupportLibrary = true resValue "string", "app_name", "Wulkanowy" - buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis()) manifestPlaceholders = [ - firebase_enabled: project.hasProperty("enableFirebase") + firebase_enabled: project.hasProperty("enableFirebase"), + buildTimestamp: String.valueOf(System.currentTimeMillis()) ] javaCompileOptions { annotationProcessorOptions { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 84c50523..bebe33c6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -106,7 +106,8 @@ + android:exported="false" + tools:ignore="MissingClass"> @@ -152,6 +153,10 @@ android:resource="@xml/provider_paths" /> + + @@ -162,7 +167,8 @@ android:name="com.google.firebase.provider.FirebaseInitProvider" android:authorities="${applicationId}.firebaseinitprovider" android:enabled="${firebase_enabled}" - android:exported="false" /> + android:exported="false" + tools:ignore="MissingClass" /> () + @get:Rule val helper: MigrationTestHelper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), @@ -24,7 +26,6 @@ abstract class AbstractMigrationTest { ) fun getMigratedRoomDatabase(): AppDatabase { - val context = ApplicationProvider.getApplicationContext() val database = Room.databaseBuilder( ApplicationProvider.getApplicationContext(), AppDatabase::class.java, @@ -32,7 +33,7 @@ abstract class AbstractMigrationTest { ).addMigrations( *AppDatabase.getMigrations( SharedPrefProvider(PreferenceManager.getDefaultSharedPreferences(context)), - AppInfo() + AppInfo(context) ) ).build() // close the database and release any stream resources when the test finishes diff --git a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt index 883cdb81..9a38f428 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt @@ -29,15 +29,15 @@ class Migration35Test : AbstractMigrationTest() { close() } - helper.runMigrationsAndValidate(dbName, 35, true, Migration35(AppInfo())) + helper.runMigrationsAndValidate(dbName, 35, true, Migration35(AppInfo(context))) val db = getMigratedRoomDatabase() val students = runBlocking { db.studentDao.loadAll() } assertEquals(2, students.size) - assertTrue { students[0].avatarColor in AppInfo().defaultColorsForAvatar } - assertTrue { students[1].avatarColor in AppInfo().defaultColorsForAvatar } + assertTrue { students[0].avatarColor in AppInfo(context).defaultColorsForAvatar } + assertTrue { students[1].avatarColor in AppInfo(context).defaultColorsForAvatar } } private fun createStudent(db: SupportSQLiteDatabase, id: Long) { diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt index b34bbf1b..39ebb4a1 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt @@ -32,13 +32,13 @@ class StudentTest { fun initApi() { MockKAnnotations.init(this) studentRepository = StudentRepository( - mockk(), - TestDispatchersProvider(), - studentDb, - semesterDb, - mockSdk, - AppInfo(), - mockk() + context = mockk(), + dispatchers = TestDispatchersProvider(), + studentDb = studentDb, + semesterDb = semesterDb, + sdk = mockSdk, + appInfo = AppInfo(mockk()), + appDatabase = mockk() ) } From 031a17ea502159fc33e1e8564abd503ba38ae08a Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Mon, 4 Oct 2021 16:35:37 +0200 Subject: [PATCH 032/357] Change text to bold in notifications center (#1546) --- app/src/main/res/layout/item_notifications_center.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/item_notifications_center.xml b/app/src/main/res/layout/item_notifications_center.xml index ae3f5586..16a7ae0c 100644 --- a/app/src/main/res/layout/item_notifications_center.xml +++ b/app/src/main/res/layout/item_notifications_center.xml @@ -33,6 +33,7 @@ android:layout_marginStart="16dp" android:layout_marginTop="6dp" android:layout_marginEnd="16dp" + android:fontFamily="sans-serif-medium" android:textSize="15sp" app:layout_constraintEnd_toStartOf="@id/notifications_center_item_icon" app:layout_constraintStart_toStartOf="parent" From 04393e60bb860566442712a08893400169eb160a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 14:43:46 +0000 Subject: [PATCH 033/357] Bump agconnect-crash from 1.6.0.300 to 1.6.1.200 (#1563) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index e8762dda..9cfa670e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -228,7 +228,7 @@ dependencies { playImplementation 'com.google.android.play:core-ktx:1.8.1' hmsImplementation 'com.huawei.hms:hianalytics:6.2.0.301' - hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.0.300' + hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.200' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 8315759c83b967ddbd82cfb5a9e39f1261e03039 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 14:44:15 +0000 Subject: [PATCH 034/357] Bump agcp from 1.6.0.300 to 1.6.1.200 (#1562) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 15d3c792..370fe296 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ buildscript { classpath 'com.android.tools.build:gradle:7.0.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.10' - classpath 'com.huawei.agconnect:agcp:1.6.0.300' + classpath 'com.huawei.agconnect:agcp:1.6.1.200' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.3.0" From 426379ec17f2bb6c74d583d2d2185f242f1b5b01 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 14:45:09 +0000 Subject: [PATCH 035/357] Bump about_libraries from 8.9.1 to 8.9.3 (#1560) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 370fe296..fbd12719 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlin_version = '1.5.31' - about_libraries = '8.9.1' + about_libraries = '8.9.3' hilt_version = "2.38.1" } repositories { From 2d84b0775a9586a9119bbed6133a254497e3a20f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 14:51:31 +0000 Subject: [PATCH 036/357] Bump hilt_version from 2.38.1 to 2.39.1 (#1561) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index fbd12719..118efd39 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { kotlin_version = '1.5.31' about_libraries = '8.9.3' - hilt_version = "2.38.1" + hilt_version = "2.39.1" } repositories { mavenCentral() From 1839d7cb8f4b4dd4f0bb99dcfecde86ed6d6d9bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 4 Oct 2021 17:13:31 +0200 Subject: [PATCH 037/357] Migrate from moshi to kotlinx serialization (#1557) --- app/build.gradle | 12 ++-- .../github/wulkanowy/data/RepositoryModule.kt | 6 +- .../io/github/wulkanowy/data/db/Converters.kt | 29 ++++---- .../data/db/adapters/PairAdapterFactory.kt | 68 ------------------- .../wulkanowy/data/db/entities/Recipient.kt | 3 +- .../wulkanowy/data/pojos/Contributor.kt | 4 +- .../wulkanowy/data/pojos/MessageDraft.kt | 4 +- .../data/repositories/AppCreatorRepository.kt | 15 ++-- .../data/repositories/MessageRepository.kt | 11 +-- .../repositories/PreferencesRepository.kt | 20 +++--- .../modules/message/send/RecipientChipItem.kt | 4 +- .../wulkanowy/data/db/ConvertersTest.kt | 4 +- .../repositories/MessageRepositoryTest.kt | 7 +- build.gradle | 1 + 14 files changed, 56 insertions(+), 132 deletions(-) delete mode 100644 app/src/main/java/io/github/wulkanowy/data/db/adapters/PairAdapterFactory.kt diff --git a/app/build.gradle b/app/build.gradle index 9cfa670e..91ce5f24 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-kapt' apply plugin: 'dagger.hilt.android.plugin' @@ -162,7 +163,7 @@ ext { room = "2.3.0" chucker = "3.5.2" mockk = "1.12.0" - moshi = "1.12.0" + coroutines = "1.5.2" } dependencies { @@ -170,7 +171,8 @@ dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" implementation "androidx.core:core-ktx:1.6.0" implementation "androidx.activity:activity-ktx:1.3.1" @@ -207,10 +209,6 @@ dependencies { implementation 'com.github.ncapdevi:FragNav:3.3.0' implementation "com.github.YarikSOffice:lingver:1.3.0" - implementation "com.squareup.moshi:moshi:$moshi" - implementation "com.squareup.moshi:moshi-adapters:$moshi" - kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi" - implementation "com.jakewharton.timber:timber:5.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1" implementation 'com.github.bastienpaulfr:Treessence:1.0.4' @@ -237,7 +235,7 @@ dependencies { testImplementation "junit:junit:4.13.2" testImplementation "io.mockk:mockk:$mockk" - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2' + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testImplementation 'org.robolectric:robolectric:4.6.1' 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 f1b719e5..863bb595 100644 --- a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt @@ -9,7 +9,6 @@ import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.RetentionManager import com.fredporciuncula.flow.preferences.FlowSharedPreferences -import com.squareup.moshi.Moshi import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -21,6 +20,7 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AppInfo import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.serialization.json.Json import timber.log.Timber import javax.inject.Singleton @@ -88,7 +88,9 @@ internal class RepositoryModule { @Singleton @Provides - fun provideMoshi() = Moshi.Builder().build() + fun provideJson() = Json { + ignoreUnknownKeys = true + } @Singleton @Provides 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 def0b371..1993c433 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 @@ -1,9 +1,10 @@ package io.github.wulkanowy.data.db import androidx.room.TypeConverter -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types -import io.github.wulkanowy.data.db.adapters.PairAdapterFactory +import kotlinx.serialization.SerializationException +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime @@ -13,15 +14,7 @@ import java.util.Date class Converters { - private val moshi by lazy { Moshi.Builder().add(PairAdapterFactory).build() } - - private val integerListAdapter by lazy { - moshi.adapter>(Types.newParameterizedType(List::class.java, Integer::class.java)) - } - - private val stringListPairAdapter by lazy { - moshi.adapter>>(Types.newParameterizedType(List::class.java, Pair::class.java, String::class.java, String::class.java)) - } + private val json = Json @TypeConverter fun timestampToDate(value: Long?): LocalDate? = value?.run { @@ -51,21 +44,25 @@ class Converters { @TypeConverter fun intListToJson(list: List): String { - return integerListAdapter.toJson(list) + return json.encodeToString(list) } @TypeConverter fun jsonToIntList(value: String): List { - return integerListAdapter.fromJson(value).orEmpty() + return json.decodeFromString(value) } @TypeConverter fun stringPairListToJson(list: List>): String { - return stringListPairAdapter.toJson(list) + return json.encodeToString(list) } @TypeConverter fun jsonToStringPairList(value: String): List> { - return stringListPairAdapter.fromJson(value).orEmpty() + return try { + json.decodeFromString(value) + } catch (e: SerializationException) { + emptyList() // handle errors from old gson Pair serialized data + } } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/adapters/PairAdapterFactory.kt b/app/src/main/java/io/github/wulkanowy/data/db/adapters/PairAdapterFactory.kt deleted file mode 100644 index 4a9b168d..00000000 --- a/app/src/main/java/io/github/wulkanowy/data/db/adapters/PairAdapterFactory.kt +++ /dev/null @@ -1,68 +0,0 @@ -package io.github.wulkanowy.data.db.adapters - -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.JsonReader -import com.squareup.moshi.JsonWriter -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type - -object PairAdapterFactory : JsonAdapter.Factory { - - override fun create(type: Type, annotations: MutableSet, moshi: Moshi): JsonAdapter<*>? { - if (type !is ParameterizedType || List::class.java != type.rawType) return null - if (type.actualTypeArguments[0] != Pair::class.java) return null - - val listType = Types.newParameterizedType(List::class.java, Map::class.java, String::class.java) - val listAdapter = moshi.adapter>>(listType) - - val mapType = Types.newParameterizedType(MutableMap::class.java, String::class.java, String::class.java) - val mapAdapter = moshi.adapter>(mapType) - - return PairAdapter(listAdapter, mapAdapter) - } - - private class PairAdapter( - private val listAdapter: JsonAdapter>>, - private val mapAdapter: JsonAdapter>, - ) : JsonAdapter>>() { - - override fun toJson(writer: JsonWriter, value: List>?) { - writer.beginArray() - value?.forEach { - writer.beginObject() - writer.name("first").value(it.first) - writer.name("second").value(it.second) - writer.endObject() - } - writer.endArray() - } - - override fun fromJson(reader: JsonReader): List>? { - return if (reader.peek() == JsonReader.Token.BEGIN_OBJECT) deserializeMoshiMap(reader) - else deserializeGsonPair(reader) - } - - // for compatibility with 0.21.0 - private fun deserializeMoshiMap(reader: JsonReader): List>? { - val map = mapAdapter.fromJson(reader) ?: return null - - return map.entries.map { - it.key to it.value - } - } - - private fun deserializeGsonPair(reader: JsonReader): List>? { - val list = listAdapter.fromJson(reader) ?: return null - - return list.map { - require(it.size == 2) { - "pair with more or less than two elements: $list" - } - - it["first"].orEmpty() to it["second"].orEmpty() - } - } - } -} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Recipient.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Recipient.kt index 60e67d32..22332270 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Recipient.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Recipient.kt @@ -3,10 +3,9 @@ package io.github.wulkanowy.data.db.entities import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import com.squareup.moshi.JsonClass import java.io.Serializable -@JsonClass(generateAdapter = true) +@kotlinx.serialization.Serializable @Entity(tableName = "Recipients") data class Recipient( diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/Contributor.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/Contributor.kt index d2338c28..4165b3f1 100644 --- a/app/src/main/java/io/github/wulkanowy/data/pojos/Contributor.kt +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/Contributor.kt @@ -1,8 +1,8 @@ package io.github.wulkanowy.data.pojos -import com.squareup.moshi.JsonClass +import kotlinx.serialization.Serializable -@JsonClass(generateAdapter = true) +@Serializable class Contributor( val displayName: String, val githubUsername: String diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/MessageDraft.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/MessageDraft.kt index a79b70cd..2e568e37 100644 --- a/app/src/main/java/io/github/wulkanowy/data/pojos/MessageDraft.kt +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/MessageDraft.kt @@ -1,9 +1,9 @@ package io.github.wulkanowy.data.pojos -import com.squareup.moshi.JsonClass import io.github.wulkanowy.ui.modules.message.send.RecipientChipItem +import kotlinx.serialization.Serializable -@JsonClass(generateAdapter = true) +@Serializable data class MessageDraft( val recipients: List, val subject: String, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt index 71b7ea94..f1fdbfed 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt @@ -1,25 +1,26 @@ package io.github.wulkanowy.data.repositories import android.content.res.AssetManager -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types import io.github.wulkanowy.data.pojos.Contributor import io.github.wulkanowy.utils.DispatchersProvider import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream import javax.inject.Inject import javax.inject.Singleton @Singleton class AppCreatorRepository @Inject constructor( private val assets: AssetManager, - private val dispatchers: DispatchersProvider + private val dispatchers: DispatchersProvider, + private val json: Json, ) { + @OptIn(ExperimentalSerializationApi::class) @Suppress("BlockingMethodInNonBlockingContext") suspend fun getAppCreators() = withContext(dispatchers.backgroundThread) { - val moshi = Moshi.Builder().build() - val type = Types.newParameterizedType(List::class.java, Contributor::class.java) - val adapter = moshi.adapter>(type) - adapter.fromJson(assets.open("contributors.json").bufferedReader().use { it.readText() }) + val inputStream = assets.open("contributors.json").buffered() + json.decodeFromStream>(inputStream) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt index ee516495..224c69bd 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.data.repositories import android.content.Context -import com.squareup.moshi.Moshi import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.Resource @@ -18,7 +17,6 @@ import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED import io.github.wulkanowy.data.mappers.mapFromEntities import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.pojos.MessageDraft -import io.github.wulkanowy.data.pojos.MessageDraftJsonAdapter import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Folder import io.github.wulkanowy.sdk.pojo.SentMessage @@ -29,6 +27,9 @@ import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import timber.log.Timber import java.time.LocalDateTime.now import javax.inject.Inject @@ -42,7 +43,7 @@ class MessageRepository @Inject constructor( @ApplicationContext private val context: Context, private val refreshHelper: AutoRefreshHelper, private val sharedPrefProvider: SharedPrefProvider, - private val moshi: Moshi, + private val json: Json, ) { private val saveFetchResultMutex = Mutex() @@ -168,9 +169,9 @@ class MessageRepository @Inject constructor( var draftMessage: MessageDraft? get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft)) - ?.let { MessageDraftJsonAdapter(moshi).fromJson(it) } + ?.let { json.decodeFromString(it) } set(value) = sharedPrefProvider.putString( context.getString(R.string.pref_key_message_send_draft), - value?.let { MessageDraftJsonAdapter(moshi).toJson(it) } + value?.let { json.encodeToString(it) } ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index a08045f8..65edd53f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -5,9 +5,6 @@ import android.content.SharedPreferences import androidx.core.content.edit import com.fredporciuncula.flow.preferences.FlowSharedPreferences import com.fredporciuncula.flow.preferences.Preference -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Moshi -import com.squareup.moshi.adapter import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.sdk.toLocalDate @@ -19,6 +16,9 @@ import io.github.wulkanowy.utils.toTimestamp import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import java.time.LocalDate import java.time.LocalDateTime import javax.inject.Inject @@ -27,16 +27,12 @@ import javax.inject.Singleton @OptIn(ExperimentalCoroutinesApi::class) @Singleton class PreferencesRepository @Inject constructor( + @ApplicationContext val context: Context, private val sharedPref: SharedPreferences, private val flowSharedPref: FlowSharedPreferences, - @ApplicationContext val context: Context, - moshi: Moshi + private val json: Json, ) { - @OptIn(ExperimentalStdlibApi::class) - private val dashboardItemsPositionAdapter: JsonAdapter> = - moshi.adapter() - val startMenuIndex: Int get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt() @@ -197,14 +193,14 @@ class PreferencesRepository @Inject constructor( var dashboardItemsPosition: Map? get() { - val json = sharedPref.getString(PREF_KEY_DASHBOARD_ITEMS_POSITION, null) ?: return null + val value = sharedPref.getString(PREF_KEY_DASHBOARD_ITEMS_POSITION, null) ?: return null - return dashboardItemsPositionAdapter.fromJson(json) + return json.decodeFromString(value) } set(value) = sharedPref.edit { putString( PREF_KEY_DASHBOARD_ITEMS_POSITION, - dashboardItemsPositionAdapter.toJson(value) + json.encodeToString(value) ) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/RecipientChipItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/RecipientChipItem.kt index 26ab7f48..bd14bc89 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/RecipientChipItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/RecipientChipItem.kt @@ -1,10 +1,10 @@ package io.github.wulkanowy.ui.modules.message.send -import com.squareup.moshi.JsonClass import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.materialchipsinput.ChipItem +import kotlinx.serialization.Serializable -@JsonClass(generateAdapter = true) +@Serializable data class RecipientChipItem( override val title: String, diff --git a/app/src/test/java/io/github/wulkanowy/data/db/ConvertersTest.kt b/app/src/test/java/io/github/wulkanowy/data/db/ConvertersTest.kt index 3b0d7c84..a044a429 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/ConvertersTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/ConvertersTest.kt @@ -23,8 +23,8 @@ class ConvertersTest { @Test fun jsonToStringPairList_0210() { - assertEquals(Converters().jsonToStringPairList("{\"aaa\":\"bbb\",\"ccc\":\"ddd\"}"), listOf("aaa" to "bbb", "ccc" to "ddd")) - assertEquals(Converters().jsonToStringPairList("{\"aaa\":\"bbb\"}"), listOf("aaa" to "bbb")) + assertEquals(Converters().jsonToStringPairList("{\"aaa\":\"bbb\",\"ccc\":\"ddd\"}"), listOf>()) + assertEquals(Converters().jsonToStringPairList("{\"aaa\":\"bbb\"}"), listOf>()) assertEquals(Converters().jsonToStringPairList("{}"), listOf>()) } } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt index 25774d74..052f08f0 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.data.repositories import android.content.Context -import com.squareup.moshi.Moshi import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.dao.MessageAttachmentDao @@ -30,6 +29,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -63,9 +63,6 @@ class MessageRepositoryTest { private lateinit var repository: MessageRepository - @MockK - private lateinit var moshi: Moshi - @Before fun setUp() { MockKAnnotations.init(this) @@ -78,7 +75,7 @@ class MessageRepositoryTest { context = context, refreshHelper = refreshHelper, sharedPrefProvider = sharedPrefProvider, - moshi = moshi, + json = Json, ) } diff --git a/build.gradle b/build.gradle index 118efd39..153088a6 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ buildscript { } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath 'com.android.tools.build:gradle:7.0.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.10' From e8075e30e4f3d6203d66e0dccd8921df97fd71a7 Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Fri, 8 Oct 2021 11:19:49 +0200 Subject: [PATCH 038/357] Add "add homework" feature (#1564) --- .../41.json | 2322 +++++++++++++++++ .../github/wulkanowy/data/db/AppDatabase.kt | 6 +- .../wulkanowy/data/db/entities/Homework.kt | 3 + .../data/db/migrations/Migration41.kt | 11 + .../data/repositories/HomeworkRepository.kt | 7 +- .../ui/modules/homework/HomeworkFragment.kt | 9 +- .../ui/modules/homework/HomeworkPresenter.kt | 6 +- .../ui/modules/homework/HomeworkView.kt | 4 +- .../modules/homework/add/HomeworkAddDialog.kt | 124 + .../homework/add/HomeworkAddPresenter.kt | 92 + .../modules/homework/add/HomeworkAddView.kt | 21 + .../details/HomeworkDetailsAdapter.kt | 28 +- .../homework/details/HomeworkDetailsDialog.kt | 8 + .../details/HomeworkDetailsPresenter.kt | 19 + .../homework/details/HomeworkDetailsView.kt | 4 + .../login/advanced/LoginAdvancedFragment.kt | 10 +- .../modules/login/form/LoginFormFragment.kt | 4 +- .../login/recover/LoginRecoverFragment.kt | 2 +- .../login/symbol/LoginSymbolFragment.kt | 2 +- app/src/main/res/layout/dialog_homework.xml | 17 +- .../main/res/layout/dialog_homework_add.xml | 134 + app/src/main/res/layout/fragment_homework.xml | 22 +- .../layout/item_homework_dialog_details.xml | 26 +- app/src/main/res/values/strings.xml | 16 +- 24 files changed, 2850 insertions(+), 47 deletions(-) create mode 100644 app/schemas/io.github.wulkanowy.data.db.AppDatabase/41.json create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddDialog.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddPresenter.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddView.kt create mode 100644 app/src/main/res/layout/dialog_homework_add.xml diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/41.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/41.json new file mode 100644 index 00000000..9d008060 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/41.json @@ -0,0 +1,2322 @@ +{ + "formatVersion": 1, + "database": { + "version": 41, + "identityHash": "d9ce44a78495a358606612bd91603c0f", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "semester_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `recipient_name` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `removed` INTEGER NOT NULL, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `unread_by` INTEGER NOT NULL, `read_by` INTEGER NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipient", + "columnName": "recipient_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "removed", + "columnName": "removed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `one_drive_id` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`real_id`))", + "fields": [ + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneDriveId", + "columnName": "one_drive_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "real_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `is_added_by_user` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ReportingUnits", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `short` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `roles` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roles", + "columnName": "roles", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` TEXT NOT NULL, `name` TEXT NOT NULL, `real_name` TEXT NOT NULL, `login_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `role` INTEGER NOT NULL, `hash` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realName", + "columnName": "real_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd9ce44a78495a358606612bd91603c0f')" + ] + } +} \ No newline at end of file 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 09aa972f..b93d0088 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 @@ -97,6 +97,7 @@ import io.github.wulkanowy.data.db.migrations.Migration37 import io.github.wulkanowy.data.db.migrations.Migration38 import io.github.wulkanowy.data.db.migrations.Migration39 import io.github.wulkanowy.data.db.migrations.Migration4 +import io.github.wulkanowy.data.db.migrations.Migration41 import io.github.wulkanowy.data.db.migrations.Migration40 import io.github.wulkanowy.data.db.migrations.Migration5 import io.github.wulkanowy.data.db.migrations.Migration6 @@ -146,7 +147,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 40 + const val VERSION_SCHEMA = 41 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), @@ -187,7 +188,8 @@ abstract class AppDatabase : RoomDatabase() { Migration37(), Migration38(), Migration39(), - Migration40() + Migration40(), + Migration41() ) fun newInstance( diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Homework.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Homework.kt index 04ee1e8c..4538cf31 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Homework.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Homework.kt @@ -40,4 +40,7 @@ data class Homework( @ColumnInfo(name = "is_notified") var isNotified: Boolean = true + + @ColumnInfo(name = "is_added_by_user") + var isAddedByUser: Boolean = false } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt new file mode 100644 index 00000000..17773efd --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt @@ -0,0 +1,11 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration41 : Migration(40, 41) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE Homework ADD COLUMN is_added_by_user INTEGER NOT NULL DEFAULT 0") + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt index a04085fb..95a375a4 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt @@ -61,8 +61,9 @@ class HomeworkRepository @Inject constructor( val homeWorkToSave = (new uniqueSubtract old).onEach { if (notify) it.isNotified = false } + val filteredOld = old.filterNot { it.isAddedByUser } - homeworkDb.deleteAll(old uniqueSubtract new) + homeworkDb.deleteAll(filteredOld uniqueSubtract new) homeworkDb.insertAll(homeWorkToSave) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) @@ -79,4 +80,8 @@ class HomeworkRepository @Inject constructor( homeworkDb.loadAll(semester.semesterId, semester.studentId, start.monday, end.sunday) suspend fun updateHomework(homework: List) = homeworkDb.updateAll(homework) + + suspend fun saveHomework(homework: Homework) = homeworkDb.insertAll(listOf(homework)) + + suspend fun deleteHomework(homework: Homework) = homeworkDb.deleteAll(listOf(homework)) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt index 1d9434dc..a723b048 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt @@ -10,6 +10,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.databinding.FragmentHomeworkBinding import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.homework.add.HomeworkAddDialog import io.github.wulkanowy.ui.modules.homework.details.HomeworkDetailsDialog import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView @@ -64,6 +65,8 @@ class HomeworkFragment : BaseFragment(R.layout.fragment homeworkPreviousButton.setOnClickListener { presenter.onPreviousDay() } homeworkNextButton.setOnClickListener { presenter.onNextDay() } + openAddHomeworkButton.setOnClickListener { presenter.onHomeworkAddButtonClicked() } + homeworkNavContainer.setElevationCompat(requireContext().dpToPx(8f)) } } @@ -122,10 +125,14 @@ class HomeworkFragment : BaseFragment(R.layout.fragment binding.homeworkNextButton.visibility = if (show) VISIBLE else View.INVISIBLE } - override fun showTimetableDialog(homework: Homework) { + override fun showHomeworkDialog(homework: Homework) { (activity as? MainActivity)?.showDialogFragment(HomeworkDetailsDialog.newInstance(homework)) } + override fun showAddHomeworkDialog() { + (activity as? MainActivity)?.showDialogFragment(HomeworkAddDialog()) + } + 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/homework/HomeworkPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt index 11c54dc2..d7d5d7cb 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt @@ -78,7 +78,11 @@ class HomeworkPresenter @Inject constructor( fun onHomeworkItemSelected(homework: Homework) { Timber.i("Select homework item ${homework.id}") - view?.showTimetableDialog(homework) + view?.showHomeworkDialog(homework) + } + + fun onHomeworkAddButtonClicked() { + view?.showAddHomeworkDialog() } private fun setBaseDateOnHolidays() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt index a1d6a04a..7c05ab86 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt @@ -33,5 +33,7 @@ interface HomeworkView : BaseView { fun showNextButton(show: Boolean) - fun showTimetableDialog(homework: Homework) + fun showHomeworkDialog(homework: Homework) + + fun showAddHomeworkDialog() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddDialog.kt new file mode 100644 index 00000000..12168f14 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddDialog.kt @@ -0,0 +1,124 @@ +package io.github.wulkanowy.ui.modules.homework.add + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.widget.doOnTextChanged +import com.google.android.material.datepicker.CalendarConstraints +import com.google.android.material.datepicker.MaterialDatePicker +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.DialogHomeworkAddBinding +import io.github.wulkanowy.ui.base.BaseDialogFragment +import io.github.wulkanowy.utils.toFormattedString +import io.github.wulkanowy.utils.toLocalDateTime +import io.github.wulkanowy.utils.toTimestamp +import java.time.LocalDate +import javax.inject.Inject + +@AndroidEntryPoint +class HomeworkAddDialog : BaseDialogFragment(), HomeworkAddView { + + @Inject + lateinit var presenter: HomeworkAddPresenter + + private var date: LocalDate? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_TITLE, 0) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DialogHomeworkAddBinding.inflate(inflater).apply { binding = this }.root + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.onAttachView(this) + } + + override fun initView() { + with(binding) { + homeworkDialogSubjectEdit.doOnTextChanged { _, _, _, _ -> + homeworkDialogSubject.error = null + homeworkDialogSubject.isErrorEnabled = false + } + homeworkDialogDateEdit.doOnTextChanged { _, _, _, _ -> + homeworkDialogDate.error = null + homeworkDialogDate.isErrorEnabled = false + } + homeworkDialogContentEdit.doOnTextChanged { _, _, _, _ -> + homeworkDialogContent.error = null + homeworkDialogContent.isErrorEnabled = false + } + homeworkDialogClose.setOnClickListener { dismiss() } + homeworkDialogDateEdit.setOnClickListener { presenter.showDatePicker(date) } + homeworkDialogAdd.setOnClickListener { + presenter.onAddHomeworkClicked( + subject = homeworkDialogSubjectEdit.text?.toString(), + teacher = homeworkDialogTeacherEdit.text?.toString(), + date = homeworkDialogDateEdit.text?.toString(), + content = homeworkDialogContentEdit.text?.toString() + ) + } + } + } + + override fun showSuccessMessage() { + showMessage(getString(R.string.homework_add_success)) + } + + override fun setErrorSubjectRequired() { + with(binding.homeworkDialogSubject) { + isErrorEnabled = true + error = getString(R.string.error_field_required) + } + } + + override fun setErrorDateRequired() { + with(binding.homeworkDialogDate) { + isErrorEnabled = true + error = getString(R.string.error_field_required) + } + } + + override fun setErrorContentRequired() { + with(binding.homeworkDialogContent) { + isErrorEnabled = true + error = getString(R.string.error_field_required) + } + } + + override fun closeDialog() { + dismiss() + } + + override fun showDatePickerDialog(currentDate: LocalDate) { + val constraintsBuilder = CalendarConstraints.Builder().apply { + setStart(LocalDate.now().toEpochDay()) + } + val datePicker = + MaterialDatePicker.Builder.datePicker() + .setCalendarConstraints(constraintsBuilder.build()) + .setSelection(currentDate.toTimestamp()) + .build() + + datePicker.addOnPositiveButtonClickListener { + date = it.toLocalDateTime().toLocalDate() + binding.homeworkDialogDate.editText?.setText(date!!.toFormattedString()) + } + + if (!parentFragmentManager.isStateSaved) { + datePicker.show(this.parentFragmentManager, null) + } + } + + override fun onDestroyView() { + presenter.onDetachView() + super.onDestroyView() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddPresenter.kt new file mode 100644 index 00000000..3639c2fe --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddPresenter.kt @@ -0,0 +1,92 @@ +package io.github.wulkanowy.ui.modules.homework.add + +import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.db.entities.Homework +import io.github.wulkanowy.data.repositories.HomeworkRepository +import io.github.wulkanowy.data.repositories.SemesterRepository +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.ErrorHandler +import io.github.wulkanowy.utils.flowWithResource +import io.github.wulkanowy.utils.toLocalDate +import kotlinx.coroutines.flow.onEach +import timber.log.Timber +import java.time.LocalDate +import javax.inject.Inject + +class HomeworkAddPresenter @Inject constructor( + errorHandler: ErrorHandler, + studentRepository: StudentRepository, + private val homeworkRepository: HomeworkRepository, + private val semesterRepository: SemesterRepository +) : BasePresenter(errorHandler, studentRepository) { + + override fun onAttachView(view: HomeworkAddView) { + super.onAttachView(view) + view.initView() + Timber.i("Homework details view was initialized") + } + + fun showDatePicker(date: LocalDate?) { + view?.showDatePickerDialog(date ?: LocalDate.now()) + } + + fun onAddHomeworkClicked(subject: String?, teacher: String?, date: String?, content: String?) { + var isError = false + + if (subject.isNullOrBlank()) { + view?.setErrorSubjectRequired() + isError = true + } + + if (date.isNullOrBlank()) { + view?.setErrorDateRequired() + isError = true + } + + if (content.isNullOrBlank()) { + view?.setErrorContentRequired() + isError = true + } + + if (!isError) { + saveHomework(subject!!, teacher.orEmpty(), date!!.toLocalDate(), content!!) + } + } + + private fun saveHomework(subject: String, teacher: String, date: LocalDate, content: String) { + flowWithResource { + val student = studentRepository.getCurrentStudent() + val semester = semesterRepository.getCurrentSemester(student) + val entryDate = LocalDate.now() + homeworkRepository.saveHomework( + Homework( + semesterId = semester.semesterId, + studentId = student.studentId, + date = date, + entryDate = entryDate, + subject = subject, + content = content, + teacher = teacher, + teacherSymbol = "", + attachments = emptyList(), + ).apply { isAddedByUser = true } + ) + }.onEach { + when (it.status) { + Status.LOADING -> Timber.i("Homework insert start") + Status.SUCCESS -> { + Timber.i("Homework insert: Success") + view?.run { + showSuccessMessage() + closeDialog() + } + } + Status.ERROR -> { + Timber.i("Homework insert result: An exception occurred") + errorHandler.dispatch(it.error!!) + } + } + }.launch("add_homework") + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddView.kt new file mode 100644 index 00000000..3bb304d9 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddView.kt @@ -0,0 +1,21 @@ +package io.github.wulkanowy.ui.modules.homework.add + +import io.github.wulkanowy.ui.base.BaseView +import java.time.LocalDate + +interface HomeworkAddView : BaseView { + + fun initView() + + fun showSuccessMessage() + + fun setErrorSubjectRequired() + + fun setErrorDateRequired() + + fun setErrorContentRequired() + + fun closeDialog() + + fun showDatePickerDialog(currentDate: LocalDate) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt index cd9a7e85..e03707a5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt @@ -5,10 +5,12 @@ import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.databinding.ItemHomeworkDialogAttachmentBinding import io.github.wulkanowy.databinding.ItemHomeworkDialogAttachmentsHeaderBinding import io.github.wulkanowy.databinding.ItemHomeworkDialogDetailsBinding +import io.github.wulkanowy.utils.ifNullOrBlank import io.github.wulkanowy.utils.toFormattedString import javax.inject.Inject @@ -37,6 +39,8 @@ class HomeworkDetailsAdapter @Inject constructor() : var onFullScreenExitClickListener = {} + var onDeleteClickListener: (homework: Homework) -> Unit = {} + override fun getItemCount() = 1 + if (attachments.isNotEmpty()) attachments.size + 1 else 0 override fun getItemViewType(position: Int) = when (position) { @@ -49,9 +53,15 @@ class HomeworkDetailsAdapter @Inject constructor() : val inflater = LayoutInflater.from(parent.context) return when (viewType) { - ViewType.ATTACHMENTS_HEADER.id -> AttachmentsHeaderViewHolder(ItemHomeworkDialogAttachmentsHeaderBinding.inflate(inflater, parent, false)) - ViewType.ATTACHMENT.id -> AttachmentViewHolder(ItemHomeworkDialogAttachmentBinding.inflate(inflater, parent, false)) - else -> DetailsViewHolder(ItemHomeworkDialogDetailsBinding.inflate(inflater, parent, false)) + ViewType.ATTACHMENTS_HEADER.id -> AttachmentsHeaderViewHolder( + ItemHomeworkDialogAttachmentsHeaderBinding.inflate(inflater, parent, false) + ) + ViewType.ATTACHMENT.id -> AttachmentViewHolder( + ItemHomeworkDialogAttachmentBinding.inflate(inflater, parent, false) + ) + else -> DetailsViewHolder( + ItemHomeworkDialogDetailsBinding.inflate(inflater, parent, false) + ) } } @@ -63,12 +73,15 @@ class HomeworkDetailsAdapter @Inject constructor() : } private fun bindDetailsViewHolder(holder: DetailsViewHolder) { + val noDataString = holder.binding.root.context.getString(R.string.all_no_data) + with(holder.binding) { homeworkDialogDate.text = homework?.date?.toFormattedString() homeworkDialogEntryDate.text = homework?.entryDate?.toFormattedString() - homeworkDialogSubject.text = homework?.subject - homeworkDialogTeacher.text = homework?.teacher - homeworkDialogContent.text = homework?.content + homeworkDialogSubject.text = homework?.subject.ifNullOrBlank { noDataString } + homeworkDialogTeacher.text = homework?.teacher.ifNullOrBlank { noDataString } + homeworkDialogContent.text = homework?.content.ifNullOrBlank { noDataString } + homeworkDialogDelete.visibility = if (homework?.isAddedByUser == true) VISIBLE else GONE homeworkDialogFullScreen.visibility = if (isHomeworkFullscreen) GONE else VISIBLE homeworkDialogFullScreenExit.visibility = if (isHomeworkFullscreen) VISIBLE else GONE homeworkDialogFullScreen.setOnClickListener { @@ -81,6 +94,9 @@ class HomeworkDetailsAdapter @Inject constructor() : homeworkDialogFullScreenExit.visibility = GONE onFullScreenExitClickListener() } + homeworkDialogDelete.setOnClickListener { + onDeleteClickListener(homework!!) + } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt index 93045a48..f9d46351 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt @@ -25,6 +25,9 @@ class HomeworkDetailsDialog : BaseDialogFragment(), Homew @Inject lateinit var detailsAdapter: HomeworkDetailsAdapter + override val homeworkDeleteSuccess: String + get() = getString(R.string.homework_delete_success) + private lateinit var homework: Homework companion object { @@ -82,12 +85,17 @@ class HomeworkDetailsDialog : BaseDialogFragment(), Homew dialog?.window?.setLayout(WRAP_CONTENT, WRAP_CONTENT) presenter.isHomeworkFullscreen = false } + onDeleteClickListener = { homework -> presenter.deleteHomework(homework) } isHomeworkFullscreen = presenter.isHomeworkFullscreen homework = this@HomeworkDetailsDialog.homework } } } + override fun closeDialog() { + dismiss() + } + override fun updateMarkAsDoneLabel(isDone: Boolean) { binding.homeworkDialogRead.text = view?.context?.getString(if (isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsPresenter.kt index ca6fc71e..ea9b47a0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsPresenter.kt @@ -33,6 +33,25 @@ class HomeworkDetailsPresenter @Inject constructor( Timber.i("Homework details view was initialized") } + fun deleteHomework(homework: Homework) { + flowWithResource { homeworkRepository.deleteHomework(homework) }.onEach { + when (it.status) { + Status.LOADING -> Timber.i("Homework delete start") + Status.SUCCESS -> { + Timber.i("Homework delete: Success") + view?.run { + showMessage(homeworkDeleteSuccess) + closeDialog() + } + } + Status.ERROR -> { + Timber.i("Homework delete result: An exception occurred") + errorHandler.dispatch(it.error!!) + } + } + }.launch("delete") + } + fun toggleDone(homework: Homework) { flowWithResource { homeworkRepository.toggleDone(homework) }.onEach { when (it.status) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsView.kt index 697f2233..4a47de43 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsView.kt @@ -4,7 +4,11 @@ import io.github.wulkanowy.ui.base.BaseView interface HomeworkDetailsView : BaseView { + val homeworkDeleteSuccess: String + fun initView() + fun closeDialog() + fun updateMarkAsDoneLabel(isDone: Boolean) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt index 0672d75f..4a93d4ca 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt @@ -170,7 +170,7 @@ class LoginAdvancedFragment : override fun setErrorUsernameRequired() { with(binding.loginFormUsernameLayout) { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -191,7 +191,7 @@ class LoginAdvancedFragment : override fun setErrorPassRequired(focus: Boolean) { with(binding.loginFormPassLayout) { if (focus) requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -212,7 +212,7 @@ class LoginAdvancedFragment : override fun setErrorPinRequired() { with(binding.loginFormPinLayout) { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -226,7 +226,7 @@ class LoginAdvancedFragment : override fun setErrorSymbolRequired() { with(binding.loginFormSymbolLayout) { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -240,7 +240,7 @@ class LoginAdvancedFragment : override fun setErrorTokenRequired() { with(binding.loginFormTokenLayout) { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt index 6e0294a4..78cc6db0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt @@ -123,7 +123,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme override fun setErrorUsernameRequired() { with(binding.loginFormUsernameLayout) { - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -141,7 +141,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme override fun setErrorPassRequired(focus: Boolean) { with(binding.loginFormPassLayout) { - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverFragment.kt index 2e2f9f5c..a91dfb61 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverFragment.kt @@ -99,7 +99,7 @@ class LoginRecoverFragment : override fun setErrorNameRequired() { with(bindingLocal.loginRecoverNameLayout) { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt index e2c37db6..ae0f75a5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt @@ -77,7 +77,7 @@ class LoginSymbolFragment : override fun setErrorSymbolRequire() { binding.loginSymbolNameLayout.apply { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } diff --git a/app/src/main/res/layout/dialog_homework.xml b/app/src/main/res/layout/dialog_homework.xml index 22a03cb2..341cec54 100644 --- a/app/src/main/res/layout/dialog_homework.xml +++ b/app/src/main/res/layout/dialog_homework.xml @@ -4,6 +4,7 @@ android:id="@+id/parent" android:layout_width="match_parent" android:layout_height="match_parent" + android:minWidth="300dp" android:orientation="vertical"> + android:layout_height="1dp" + android:background="@drawable/ic_all_divider" /> @@ -52,6 +53,9 @@ style="@style/Widget.MaterialComponents.Button.TextButton.Dialog" android:layout_width="wrap_content" android:layout_height="36dp" + android:layout_alignParentEnd="true" + android:layout_alignParentBottom="true" + android:layout_gravity="center_vertical" android:layout_marginTop="8dp" android:layout_marginEnd="8dp" android:layout_marginBottom="8dp" @@ -60,9 +64,6 @@ android:insetRight="0dp" android:insetBottom="0dp" android:minWidth="88dp" - android:layout_gravity="center_vertical" - android:layout_alignParentEnd="true" - android:layout_alignParentBottom="true" android:text="@string/all_close" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> diff --git a/app/src/main/res/layout/dialog_homework_add.xml b/app/src/main/res/layout/dialog_homework_add.xml new file mode 100644 index 00000000..b9b8d1a2 --- /dev/null +++ b/app/src/main/res/layout/dialog_homework_add.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_homework.xml b/app/src/main/res/layout/fragment_homework.xml index c0b5698d..e1f0a16b 100644 --- a/app/src/main/res/layout/fragment_homework.xml +++ b/app/src/main/res/layout/fragment_homework.xml @@ -26,6 +26,8 @@ android:id="@+id/homeworkRecycler" android:layout_width="match_parent" android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingBottom="70dp" tools:listitem="@layout/item_homework" /> @@ -105,6 +107,18 @@ android:text="@string/all_retry" /> + + + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/item_homework_dialog_details.xml b/app/src/main/res/layout/item_homework_dialog_details.xml index abc371da..9d560ba5 100644 --- a/app/src/main/res/layout/item_homework_dialog_details.xml +++ b/app/src/main/res/layout/item_homework_dialog_details.xml @@ -10,26 +10,37 @@ + android:orientation="horizontal"> + + - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 63d4fc42..b662d4ad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,6 +17,7 @@ Licenses Messages New message + New homework Notes and achievements Homework Accounts manager @@ -57,7 +58,6 @@ Use the assigned login or email in @%1$s Invalid symbol Student not found. Validate the symbol and the chosen variation of the UONET+ register - This field is required Selected student is already logged in The symbol can be found on the register page in Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne.\n\nMake sure that you have set the appropriate register variant in the UONET+ register variant field on the previous screen. Wulkanowy does not detect pre-school students at the moment Select students to log in to the application @@ -316,6 +316,9 @@ No info about homework Mark as done Mark as undone + Add homework + Homework added successfully + Homework deleted successfully Attachments New homework @@ -592,6 +595,10 @@ No Save Title + Add + Copied + Undo + Change @@ -697,12 +704,6 @@ No color - - Copied - Undo - Change - - Download of updates has started… An update has just been downloaded. @@ -721,4 +722,5 @@ An unexpected error occurred Feature disabled by your school Feature not available. Login in a mode other than Mobile API + This field is required From ebf9e741c2cc19d0cb8287878e9d789cf480575e Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Sat, 9 Oct 2021 01:04:04 +0200 Subject: [PATCH 039/357] Fix homework last item padding (#1568) --- app/src/main/res/layout/fragment_homework.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/fragment_homework.xml b/app/src/main/res/layout/fragment_homework.xml index e1f0a16b..ae8270ab 100644 --- a/app/src/main/res/layout/fragment_homework.xml +++ b/app/src/main/res/layout/fragment_homework.xml @@ -27,7 +27,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" - android:paddingBottom="70dp" + android:paddingBottom="64dp" tools:listitem="@layout/item_homework" /> From 2ab0a57a4168b172f21a5f686c12feeb95756eb2 Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Sat, 9 Oct 2021 01:55:00 +0200 Subject: [PATCH 040/357] Separate calculated average settings (#1558) --- app/src/main/res/values/strings.xml | 1 + .../main/res/xml/scheme_preferences_advanced.xml | 16 ++++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b662d4ad..d8674705 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -668,6 +668,7 @@ Attendance Timetable Grades + Calculated average Messages Appearance & Behavior diff --git a/app/src/main/res/xml/scheme_preferences_advanced.xml b/app/src/main/res/xml/scheme_preferences_advanced.xml index 46103787..95f6f383 100644 --- a/app/src/main/res/xml/scheme_preferences_advanced.xml +++ b/app/src/main/res/xml/scheme_preferences_advanced.xml @@ -19,18 +19,16 @@ app:key="@string/pref_key_grade_modifier_minus" app:title="@string/pref_other_grade_modifier_minus" app:useSimpleSummaryProvider="true" /> + + - + Date: Sat, 9 Oct 2021 17:36:22 +0200 Subject: [PATCH 041/357] Change text when there are no lessons today and tomorrow in dashboard (#1571) --- .../ui/modules/dashboard/DashboardAdapter.kt | 9 +++++++-- app/src/main/res/layout/item_dashboard_lessons.xml | 13 +++++++++++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt index 11b575c1..7bd6ed0c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt @@ -301,26 +301,31 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { dateToNavigate = tomorrowDate updateLessonView(item, tomorrowTimetable, binding) binding.dashboardLessonsItemTitleTomorrow.isVisible = true + binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false } currentDayHeader != null && currentDayHeader.content.isNotBlank() -> { dateToNavigate = currentDate updateLessonView(item, emptyList(), binding, currentDayHeader) binding.dashboardLessonsItemTitleTomorrow.isVisible = false + binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false } tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> { dateToNavigate = tomorrowDate updateLessonView(item, emptyList(), binding, tomorrowDayHeader) binding.dashboardLessonsItemTitleTomorrow.isVisible = true + binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false } else -> { - dateToNavigate = tomorrowDate + dateToNavigate = currentDate updateLessonView(item, emptyList(), binding) - binding.dashboardLessonsItemTitleTomorrow.isVisible = + binding.dashboardLessonsItemTitleTomorrow.isVisible = false + binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = !(item.isLoading && item.error == null) } } diff --git a/app/src/main/res/layout/item_dashboard_lessons.xml b/app/src/main/res/layout/item_dashboard_lessons.xml index a2a92c54..39bd317a 100644 --- a/app/src/main/res/layout/item_dashboard_lessons.xml +++ b/app/src/main/res/layout/item_dashboard_lessons.xml @@ -42,6 +42,19 @@ app:layout_constraintBottom_toBottomOf="@id/dashboard_lessons_item_title" app:layout_constraintStart_toEndOf="@id/dashboard_lessons_item_title" /> + + Lessons (Tomorrow) + (Today and tomorrow) In a moment: Soon: First: From a240fd5d5f1cbe1bdf90f39c6bebdeeb0a05a1c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sat, 9 Oct 2021 18:37:27 +0200 Subject: [PATCH 042/357] Add build timestamp as build config field (#1567) --- app/build.gradle | 11 +++++++---- app/src/main/AndroidManifest.xml | 12 ++++-------- .../java/io/github/wulkanowy/utils/AppInfo.kt | 16 +++------------- .../data/db/migrations/AbstractMigrationTest.kt | 4 ++-- .../data/db/migrations/Migration35Test.kt | 6 +++--- .../wulkanowy/data/repositories/StudentTest.kt | 2 +- 6 files changed, 20 insertions(+), 31 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 91ce5f24..031385fe 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,10 +29,7 @@ android { resValue "string", "app_name", "Wulkanowy" - manifestPlaceholders = [ - firebase_enabled: project.hasProperty("enableFirebase"), - buildTimestamp: String.valueOf(System.currentTimeMillis()) - ] + manifestPlaceholders = [firebase_enabled: project.hasProperty("enableFirebase")] javaCompileOptions { annotationProcessorOptions { arguments += [ @@ -41,6 +38,12 @@ android { ] } } + + if (System.env.SET_BUILD_TIMESTAMP) { + buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis()) + } else { + buildConfigField "long", "BUILD_TIMESTAMP", "1486235849000" + } } sourceSets { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bebe33c6..8c7eb852 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -153,14 +153,6 @@ android:resource="@xml/provider_paths" /> - - - - + + diff --git a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt index 4875425f..a3961aed 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt @@ -1,12 +1,10 @@ package io.github.wulkanowy.utils -import android.content.Context -import android.content.pm.PackageManager import android.content.res.Resources import android.os.Build.MANUFACTURER import android.os.Build.MODEL import android.os.Build.VERSION.SDK_INT -import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.BuildConfig.BUILD_TIMESTAMP import io.github.wulkanowy.BuildConfig.DEBUG import io.github.wulkanowy.BuildConfig.FLAVOR import io.github.wulkanowy.BuildConfig.VERSION_CODE @@ -15,21 +13,13 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -open class AppInfo @Inject constructor( - @ApplicationContext private val context: Context, -) { +open class AppInfo @Inject constructor() { open val isDebug get() = DEBUG open val versionCode get() = VERSION_CODE - open val buildTimestamp: Long - get() { - val info = context.packageManager.getApplicationInfo( - context.packageName, PackageManager.GET_META_DATA, - ) - return info.metaData?.getFloat("buildTimestamp")?.toLong() ?: 0 - } + open val buildTimestamp get() = BUILD_TIMESTAMP open val buildFlavor get() = FLAVOR diff --git a/app/src/test/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt b/app/src/test/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt index 5897af73..cc31d893 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt @@ -16,7 +16,7 @@ abstract class AbstractMigrationTest { val dbName = "migration-test" - val context: Context get() = ApplicationProvider.getApplicationContext() + val context: Context get() = ApplicationProvider.getApplicationContext() @get:Rule val helper: MigrationTestHelper = MigrationTestHelper( @@ -33,7 +33,7 @@ abstract class AbstractMigrationTest { ).addMigrations( *AppDatabase.getMigrations( SharedPrefProvider(PreferenceManager.getDefaultSharedPreferences(context)), - AppInfo(context) + AppInfo() ) ).build() // close the database and release any stream resources when the test finishes diff --git a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt index 9a38f428..883cdb81 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt @@ -29,15 +29,15 @@ class Migration35Test : AbstractMigrationTest() { close() } - helper.runMigrationsAndValidate(dbName, 35, true, Migration35(AppInfo(context))) + helper.runMigrationsAndValidate(dbName, 35, true, Migration35(AppInfo())) val db = getMigratedRoomDatabase() val students = runBlocking { db.studentDao.loadAll() } assertEquals(2, students.size) - assertTrue { students[0].avatarColor in AppInfo(context).defaultColorsForAvatar } - assertTrue { students[1].avatarColor in AppInfo(context).defaultColorsForAvatar } + assertTrue { students[0].avatarColor in AppInfo().defaultColorsForAvatar } + assertTrue { students[1].avatarColor in AppInfo().defaultColorsForAvatar } } private fun createStudent(db: SupportSQLiteDatabase, id: Long) { diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt index 39ebb4a1..9d3d7a2e 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt @@ -37,7 +37,7 @@ class StudentTest { studentDb = studentDb, semesterDb = semesterDb, sdk = mockSdk, - appInfo = AppInfo(mockk()), + appInfo = AppInfo(), appDatabase = mockk() ) } From 539be586ce402d74bdfc2ce195a175c02726007d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 11 Oct 2021 14:39:54 +0200 Subject: [PATCH 043/357] Fix text color of time left indicator on dashboard timetable card (#1572) --- app/src/main/res/layout/item_dashboard_lessons.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout/item_dashboard_lessons.xml b/app/src/main/res/layout/item_dashboard_lessons.xml index 39bd317a..9156c1a2 100644 --- a/app/src/main/res/layout/item_dashboard_lessons.xml +++ b/app/src/main/res/layout/item_dashboard_lessons.xml @@ -50,10 +50,10 @@ android:text="@string/dashboard_timetable_title_today_and_tomorrow" android:textColor="?colorOnSurface" android:textSize="14sp" - tools:visibility="invisible" app:layout_constraintBaseline_toBaselineOf="@id/dashboard_lessons_item_title" app:layout_constraintBottom_toBottomOf="@id/dashboard_lessons_item_title" - app:layout_constraintStart_toEndOf="@id/dashboard_lessons_item_title" /> + app:layout_constraintStart_toEndOf="@id/dashboard_lessons_item_title" + tools:visibility="invisible" /> Date: Mon, 11 Oct 2021 15:43:29 +0000 Subject: [PATCH 044/357] Bump hianalytics from 6.2.0.301 to 6.3.0.300 (#1576) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 031385fe..fc7d50d5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -228,7 +228,7 @@ dependencies { playImplementation 'com.google.android.play:core:1.10.2' playImplementation 'com.google.android.play:core-ktx:1.8.1' - hmsImplementation 'com.huawei.hms:hianalytics:6.2.0.301' + hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.300' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.200' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 3d0cd11ba45042b612d3c04f146f364852cac373 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 15:44:03 +0000 Subject: [PATCH 045/357] Bump Treessence from 1.0.4 to 1.0.5 (#1575) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index fc7d50d5..ff40c7c8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -214,7 +214,7 @@ dependencies { implementation "com.jakewharton.timber:timber:5.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1" - implementation 'com.github.bastienpaulfr:Treessence:1.0.4' + implementation 'com.github.bastienpaulfr:Treessence:1.0.5' implementation "com.mikepenz:aboutlibraries-core:$about_libraries" implementation "io.coil-kt:coil:1.3.2" implementation "io.github.wulkanowy:AppKillerManager:3.0.0" From 9d0366d010433994a296b69bb05ea2e3e644396d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 15:44:19 +0000 Subject: [PATCH 046/357] Bump firebase-bom from 28.4.1 to 28.4.2 (#1574) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index ff40c7c8..ce641606 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -221,7 +221,7 @@ dependencies { implementation 'me.xdrop:fuzzywuzzy:1.3.1' implementation 'com.fredporciuncula:flow-preferences:1.5.0' - playImplementation platform('com.google.firebase:firebase-bom:28.4.1') + playImplementation platform('com.google.firebase:firebase-bom:28.4.2') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From d6918077bff00d91f51d7cdd5c6a5bd201a1a5e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Jelnicki?= Date: Wed, 13 Oct 2021 21:47:03 +0200 Subject: [PATCH 047/357] Set upcoming lesson notification visibility to public (#1581) --- .../wulkanowy/services/alarm/TimetableNotificationReceiver.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt index 5e4bad8c..9ce96ef3 100644 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt @@ -124,6 +124,7 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { .setAutoCancel(false) .setWhen(countDown) .setOngoing(isPersistent) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .apply { if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true) } From e3122127c0912fe0854c21a8fc140becec01982b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Wed, 13 Oct 2021 23:58:24 +0200 Subject: [PATCH 048/357] Add admin messages (#1553) --- app/build.gradle | 10 +- .../42.json | 2396 +++++++++++++++++ .../{RepositoryModule.kt => DataModule.kt} | 88 +- .../wulkanowy/data/api/AdminMessageService.kt | 12 + .../github/wulkanowy/data/db/AppDatabase.kt | 15 +- .../wulkanowy/data/db/dao/AdminMessageDao.kt | 25 + .../data/db/entities/AdminMessage.kt | 37 + .../data/db/migrations/Migration42.kt | 24 + .../repositories/AdminMessageRepository.kt | 53 + .../data/repositories/AppCreatorRepository.kt | 7 +- .../repositories/PreferencesRepository.kt | 2 + .../github/wulkanowy/ui/base/ErrorHandler.kt | 7 +- .../ui/modules/dashboard/DashboardAdapter.kt | 40 + .../ui/modules/dashboard/DashboardFragment.kt | 13 + .../ui/modules/dashboard/DashboardItem.kt | 13 + .../dashboard/DashboardItemMoveCallback.kt | 7 +- .../modules/dashboard/DashboardPresenter.kt | 59 +- .../ui/modules/dashboard/DashboardView.kt | 2 + .../ui/modules/login/LoginErrorHandler.kt | 7 +- .../login/recover/RecoverErrorHandler.kt | 9 +- .../completed/CompletedLessonsErrorHandler.kt | 6 +- .../java/io/github/wulkanowy/utils/AppInfo.kt | 28 +- .../res/drawable/ic_dashboard_warning.xml | 10 + .../layout/item_dashboard_admin_message.xml | 60 + app/src/main/res/values-night/styles.xml | 1 + app/src/main/res/values/api_hosts.xml | 2 +- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/colors.xml | 3 + app/src/main/res/values/styles.xml | 1 + 29 files changed, 2868 insertions(+), 70 deletions(-) create mode 100644 app/schemas/io.github.wulkanowy.data.db.AppDatabase/42.json rename app/src/main/java/io/github/wulkanowy/data/{RepositoryModule.kt => DataModule.kt} (72%) create mode 100644 app/src/main/java/io/github/wulkanowy/data/api/AdminMessageService.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration42.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt create mode 100644 app/src/main/res/drawable/ic_dashboard_warning.xml create mode 100644 app/src/main/res/layout/item_dashboard_admin_message.xml diff --git a/app/build.gradle b/app/build.gradle index ce641606..227423e4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -66,12 +66,14 @@ android { shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release + buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" } debug { resValue "string", "app_name", "Wulkanowy DEV " + defaultConfig.versionCode applicationIdSuffix ".dev" versionNameSuffix "-dev" ext.enableCrashlytics = project.hasProperty("enableFirebase") + buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" } } @@ -170,7 +172,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:1.3.0" + implementation "io.github.wulkanowy:sdk:4efd64264b" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' @@ -212,6 +214,10 @@ dependencies { implementation 'com.github.ncapdevi:FragNav:3.3.0' implementation "com.github.YarikSOffice:lingver:1.3.0" + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" + implementation "com.squareup.okhttp3:logging-interceptor:4.9.2" + implementation "com.jakewharton.timber:timber:5.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1" implementation 'com.github.bastienpaulfr:Treessence:1.0.5' @@ -234,7 +240,7 @@ dependencies { releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" debugImplementation "com.github.ChuckerTeam.Chucker:library:$chucker" - debugImplementation 'com.github.amitshekhariitbhu.Android-Debug-Database:debug-db:v1.0.6' + debugImplementation 'com.github.amitshekhariitbhu.Android-Debug-Database:debug-db:1.0.6' testImplementation "junit:junit:4.13.2" testImplementation "io.mockk:mockk:$mockk" diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/42.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/42.json new file mode 100644 index 00000000..a5faa57b --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/42.json @@ -0,0 +1,2396 @@ +{ + "formatVersion": 1, + "database": { + "version": 42, + "identityHash": "5c8b7f9409294ecdebf9f74a44f8e883", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "semester_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `recipient_name` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `removed` INTEGER NOT NULL, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `unread_by` INTEGER NOT NULL, `read_by` INTEGER NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipient", + "columnName": "recipient_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "removed", + "columnName": "removed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `one_drive_id` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`real_id`))", + "fields": [ + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneDriveId", + "columnName": "one_drive_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "real_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `is_added_by_user` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ReportingUnits", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `short` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `roles` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roles", + "columnName": "roles", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` TEXT NOT NULL, `name` TEXT NOT NULL, `real_name` TEXT NOT NULL, `login_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `role` INTEGER NOT NULL, `hash` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realName", + "columnName": "real_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AdminMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `version_name` INTEGER, `version_max` INTEGER, `target_register_host` TEXT, `target_flavor` TEXT, `destination_url` TEXT, `priority` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionMin", + "columnName": "version_name", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMax", + "columnName": "version_max", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "targetRegisterHost", + "columnName": "target_register_host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetFlavor", + "columnName": "target_flavor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "destinationUrl", + "columnName": "destination_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5c8b7f9409294ecdebf9f74a44f8e883')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt similarity index 72% rename from app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt rename to app/src/main/java/io/github/wulkanowy/data/DataModule.kt index 863bb595..cac3ffc2 100644 --- a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt @@ -2,62 +2,100 @@ package io.github.wulkanowy.data import android.content.Context import android.content.SharedPreferences -import android.content.res.AssetManager -import android.content.res.Resources import androidx.preference.PreferenceManager import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.RetentionManager import com.fredporciuncula.flow.preferences.FlowSharedPreferences +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import io.github.wulkanowy.data.api.AdminMessageService import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AppInfo import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.create import timber.log.Timber +import java.util.concurrent.TimeUnit import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal class RepositoryModule { +internal class DataModule { @Singleton @Provides - fun provideSdk(chuckerCollector: ChuckerCollector, @ApplicationContext context: Context): Sdk { - return Sdk().apply { + fun provideSdk(chuckerInterceptor: ChuckerInterceptor) = + Sdk().apply { androidVersion = android.os.Build.VERSION.RELEASE buildTag = android.os.Build.MODEL setSimpleHttpLogger { Timber.d(it) } // for debug only - addInterceptor( - ChuckerInterceptor.Builder(context) - .collector(chuckerCollector) - .alwaysReadResponseBody(true) - .build(), network = true - ) + addInterceptor(chuckerInterceptor, network = true) } - } @Singleton @Provides fun provideChuckerCollector( @ApplicationContext context: Context, prefRepository: PreferencesRepository - ): ChuckerCollector { - return ChuckerCollector( - context = context, - showNotification = prefRepository.isDebugNotificationEnable, - retentionPeriod = RetentionManager.Period.ONE_HOUR - ) - } + ) = ChuckerCollector( + context = context, + showNotification = prefRepository.isDebugNotificationEnable, + retentionPeriod = RetentionManager.Period.ONE_HOUR + ) + + @Singleton + @Provides + fun provideChuckerInterceptor( + @ApplicationContext context: Context, + chuckerCollector: ChuckerCollector + ) = ChuckerInterceptor.Builder(context) + .collector(chuckerCollector) + .alwaysReadResponseBody(true) + .build() + + @Singleton + @Provides + fun provideOkHttpClient(chuckerInterceptor: ChuckerInterceptor): OkHttpClient = + OkHttpClient.Builder() + .addNetworkInterceptor(chuckerInterceptor) + .addInterceptor(HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BASIC + }) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + + @OptIn(ExperimentalSerializationApi::class) + @Singleton + @Provides + fun provideRetrofit( + okHttpClient: OkHttpClient, + json: Json, + appInfo: AppInfo + ): Retrofit = Retrofit.Builder() + .baseUrl(appInfo.messagesBaseUrl) + .client(okHttpClient) + .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) + .build() + + @Singleton + @Provides + fun provideAdminMessageService(retrofit: Retrofit): AdminMessageService = retrofit.create() @Singleton @Provides @@ -67,14 +105,6 @@ internal class RepositoryModule { appInfo: AppInfo ) = AppDatabase.newInstance(context, sharedPrefProvider, appInfo) - @Singleton - @Provides - fun provideResources(@ApplicationContext context: Context): Resources = context.resources - - @Singleton - @Provides - fun provideAssets(@ApplicationContext context: Context): AssetManager = context.assets - @Singleton @Provides fun provideSharedPref(@ApplicationContext context: Context): SharedPreferences = @@ -208,4 +238,8 @@ internal class RepositoryModule { @Singleton @Provides fun provideNotificationDao(database: AppDatabase) = database.notificationDao + + @Singleton + @Provides + fun provideAdminMessageDao(database: AppDatabase) = database.adminMessagesDao } diff --git a/app/src/main/java/io/github/wulkanowy/data/api/AdminMessageService.kt b/app/src/main/java/io/github/wulkanowy/data/api/AdminMessageService.kt new file mode 100644 index 00000000..23f5af24 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/api/AdminMessageService.kt @@ -0,0 +1,12 @@ +package io.github.wulkanowy.data.api + +import io.github.wulkanowy.data.db.entities.AdminMessage +import retrofit2.http.GET +import javax.inject.Singleton + +@Singleton +interface AdminMessageService { + + @GET("/v1.json") + suspend fun getAdminMessages(): List +} \ No newline at end of file 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 b93d0088..b8831acd 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 @@ -6,6 +6,7 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.RoomDatabase.JournalMode.TRUNCATE import androidx.room.TypeConverters +import io.github.wulkanowy.data.db.dao.AdminMessageDao import io.github.wulkanowy.data.db.dao.AttendanceDao import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao import io.github.wulkanowy.data.db.dao.CompletedLessonsDao @@ -35,6 +36,7 @@ import io.github.wulkanowy.data.db.dao.TeacherDao import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao import io.github.wulkanowy.data.db.dao.TimetableDao import io.github.wulkanowy.data.db.dao.TimetableHeaderDao +import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.AttendanceSummary import io.github.wulkanowy.data.db.entities.CompletedLesson @@ -97,8 +99,9 @@ import io.github.wulkanowy.data.db.migrations.Migration37 import io.github.wulkanowy.data.db.migrations.Migration38 import io.github.wulkanowy.data.db.migrations.Migration39 import io.github.wulkanowy.data.db.migrations.Migration4 -import io.github.wulkanowy.data.db.migrations.Migration41 import io.github.wulkanowy.data.db.migrations.Migration40 +import io.github.wulkanowy.data.db.migrations.Migration41 +import io.github.wulkanowy.data.db.migrations.Migration42 import io.github.wulkanowy.data.db.migrations.Migration5 import io.github.wulkanowy.data.db.migrations.Migration6 import io.github.wulkanowy.data.db.migrations.Migration7 @@ -138,7 +141,8 @@ import javax.inject.Singleton StudentInfo::class, TimetableHeader::class, SchoolAnnouncement::class, - Notification::class + Notification::class, + AdminMessage::class ], version = AppDatabase.VERSION_SCHEMA, exportSchema = true @@ -147,7 +151,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 41 + const val VERSION_SCHEMA = 42 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), @@ -189,7 +193,8 @@ abstract class AppDatabase : RoomDatabase() { Migration38(), Migration39(), Migration40(), - Migration41() + Migration41(), + Migration42() ) fun newInstance( @@ -261,4 +266,6 @@ abstract class AppDatabase : RoomDatabase() { abstract val schoolAnnouncementDao: SchoolAnnouncementDao abstract val notificationDao: NotificationDao + + abstract val adminMessagesDao: AdminMessageDao } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt new file mode 100644 index 00000000..87f4812d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt @@ -0,0 +1,25 @@ +package io.github.wulkanowy.data.db.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import io.github.wulkanowy.data.db.entities.AdminMessage +import kotlinx.coroutines.flow.Flow +import javax.inject.Singleton + +@Singleton +@Dao +abstract class AdminMessageDao : BaseDao { + + @Query("SELECT * FROM AdminMessages") + abstract fun loadAll(): Flow> + + @Transaction + open suspend fun removeOldAndSaveNew( + oldMessages: List, + newMessages: List + ) { + deleteAll(oldMessages) + insertAll(newMessages) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt new file mode 100644 index 00000000..b7b0cf58 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt @@ -0,0 +1,37 @@ +package io.github.wulkanowy.data.db.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.serialization.Serializable + +@Serializable +@Entity(tableName = "AdminMessages") +data class AdminMessage( + + @PrimaryKey + val id: Int, + + val title: String, + + val content: String, + + @ColumnInfo(name = "version_name") + val versionMin: Int? = null, + + @ColumnInfo(name = "version_max") + val versionMax: Int? = null, + + @ColumnInfo(name = "target_register_host") + val targetRegisterHost: String? = null, + + @ColumnInfo(name = "target_flavor") + val targetFlavor: String? = null, + + @ColumnInfo(name = "destination_url") + val destinationUrl: String? = null, + + val priority: String, + + val type: String +) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration42.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration42.kt new file mode 100644 index 00000000..3d66f301 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration42.kt @@ -0,0 +1,24 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration42 : Migration(41, 42) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + """CREATE TABLE IF NOT EXISTS `AdminMessages` ( + `id` INTEGER NOT NULL, + `title` TEXT NOT NULL, + `content` TEXT NOT NULL, + `version_name` INTEGER, + `version_max` INTEGER, + `target_register_host` TEXT, + `target_flavor` TEXT, + `destination_url` TEXT, + `priority` TEXT NOT NULL, + `type` TEXT NOT NULL, + PRIMARY KEY(`id`))""" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt new file mode 100644 index 00000000..a620caad --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt @@ -0,0 +1,53 @@ +package io.github.wulkanowy.data.repositories + +import io.github.wulkanowy.data.api.AdminMessageService +import io.github.wulkanowy.data.db.dao.AdminMessageDao +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.utils.AppInfo +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey +import io.github.wulkanowy.utils.networkBoundResource +import kotlinx.coroutines.sync.Mutex +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AdminMessageRepository @Inject constructor( + private val adminMessageService: AdminMessageService, + private val adminMessageDao: AdminMessageDao, + private val appInfo: AppInfo, + private val refreshHelper: AutoRefreshHelper, +) { + private val saveFetchResultMutex = Mutex() + + private val cacheKey = "admin_messages" + + suspend fun getAdminMessages(student: Student, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, + query = { adminMessageDao.loadAll() }, + fetch = { adminMessageService.getAdminMessages() }, + shouldFetch = { + refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) || forceRefresh + }, + saveFetchResult = { oldItems, newItems -> + adminMessageDao.removeOldAndSaveNew(oldItems, newItems) + refreshHelper.updateLastRefreshTimestamp(cacheKey) + }, + showSavedOnLoading = false, + mapResult = { adminMessages -> + adminMessages.filter { adminMessage -> + val isCorrectRegister = adminMessage.targetRegisterHost?.let { + student.scrapperBaseUrl.contains(it, true) + } ?: true + val isCorrectFlavor = + adminMessage.targetFlavor?.equals(appInfo.buildFlavor, true) ?: true + val isCorrectMaxVersion = + adminMessage.versionMax?.let { it >= appInfo.versionCode } ?: true + val isCorrectMinVersion = + adminMessage.versionMin?.let { it <= appInfo.versionCode } ?: true + + isCorrectRegister && isCorrectFlavor && isCorrectMaxVersion && isCorrectMinVersion + }.maxByOrNull { it.id } + } + ) +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt index f1fdbfed..3d83f718 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.data.repositories -import android.content.res.AssetManager +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.pojos.Contributor import io.github.wulkanowy.utils.DispatchersProvider import kotlinx.coroutines.withContext @@ -12,7 +13,7 @@ import javax.inject.Singleton @Singleton class AppCreatorRepository @Inject constructor( - private val assets: AssetManager, + @ApplicationContext private val context: Context, private val dispatchers: DispatchersProvider, private val json: Json, ) { @@ -20,7 +21,7 @@ class AppCreatorRepository @Inject constructor( @OptIn(ExperimentalSerializationApi::class) @Suppress("BlockingMethodInNonBlockingContext") suspend fun getAppCreators() = withContext(dispatchers.backgroundThread) { - val inputStream = assets.open("contributors.json").buffered() + val inputStream = context.assets.open("contributors.json").buffered() json.decodeFromStream>(inputStream) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 65edd53f..b0991bbc 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -209,6 +209,7 @@ class PreferencesRepository @Inject constructor( .map { set -> set.map { DashboardItem.Tile.valueOf(it) } .plus(DashboardItem.Tile.ACCOUNT) + .plus(DashboardItem.Tile.ADMIN_MESSAGE) .toSet() } @@ -216,6 +217,7 @@ class PreferencesRepository @Inject constructor( get() = selectedDashboardTilesPreference.get() .map { DashboardItem.Tile.valueOf(it) } .plus(DashboardItem.Tile.ACCOUNT) + .plus(DashboardItem.Tile.ADMIN_MESSAGE) .toSet() set(value) { val filteredValue = value.filterNot { it == DashboardItem.Tile.ACCOUNT } 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 7c32ef18..fbc994e2 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 @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.base -import android.content.res.Resources +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException @@ -9,7 +10,7 @@ import io.github.wulkanowy.utils.security.ScramblerException import timber.log.Timber import javax.inject.Inject -open class ErrorHandler @Inject constructor(protected val resources: Resources) { +open class ErrorHandler @Inject constructor(@ApplicationContext protected val context: Context) { var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> } @@ -25,7 +26,7 @@ open class ErrorHandler @Inject constructor(protected val resources: Resources) } protected open fun proceed(error: Throwable) { - showErrorMessage(resources.getString(error), error) + showErrorMessage(context.resources.getString(error), error) when (error) { is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl) is ScramblerException, is BadCredentialsException -> onSessionExpired() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt index 7bd6ed0c..405bfbc5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt @@ -1,6 +1,8 @@ package io.github.wulkanowy.ui.modules.dashboard import android.annotation.SuppressLint +import android.content.res.ColorStateList +import android.graphics.Color import android.graphics.Typeface import android.os.Handler import android.os.Looper @@ -18,6 +20,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.TimetableHeader import io.github.wulkanowy.databinding.ItemDashboardAccountBinding +import io.github.wulkanowy.databinding.ItemDashboardAdminMessageBinding import io.github.wulkanowy.databinding.ItemDashboardAnnouncementsBinding import io.github.wulkanowy.databinding.ItemDashboardConferencesBinding import io.github.wulkanowy.databinding.ItemDashboardExamsBinding @@ -63,6 +66,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter Unit = {} + var onAdminMessageClickListener: (String?) -> Unit = {} + val items = mutableListOf() fun submitList(newItems: List) { @@ -109,6 +114,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter ConferencesViewHolder( ItemDashboardConferencesBinding.inflate(inflater, parent, false) ) + DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder( + ItemDashboardAdminMessageBinding.inflate(inflater, parent, false) + ) else -> throw IllegalArgumentException() } } @@ -123,6 +131,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter bindAnnouncementsViewHolder(holder, position) is ExamsViewHolder -> bindExamsViewHolder(holder, position) is ConferencesViewHolder -> bindConferencesViewHolder(holder, position) + is AdminMessageViewHolder -> bindAdminMessage(holder, position) } } @@ -697,6 +706,34 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { + context.getThemeAttrColor(R.attr.colorPrimary) to + context.getThemeAttrColor(R.attr.colorOnPrimary) + } + "MEDIUM" -> { + context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK + } + else -> null to context.getThemeAttrColor(R.attr.colorOnSurface) + } + + with(adminMessageViewHolder.binding) { + dashboardAdminMessageItemTitle.text = item.title + dashboardAdminMessageItemTitle.setTextColor(textColor) + dashboardAdminMessageItemDescription.text = item.content + dashboardAdminMessageItemDescription.setTextColor(textColor) + dashboardAdminMessageItemIcon.setColorFilter(textColor) + + root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) }) + item.destinationUrl?.let { url -> + root.setOnClickListener { onAdminMessageClickListener(url) } + } + } + } + class AccountViewHolder(val binding: ItemDashboardAccountBinding) : RecyclerView.ViewHolder(binding.root) @@ -736,6 +773,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter, private val oldList: List diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt index 096c1b77..775b7b55 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt @@ -10,6 +10,7 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.databinding.FragmentDashboardBinding @@ -29,6 +30,7 @@ import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragm import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate import javax.inject.Inject @@ -97,6 +99,13 @@ class DashboardFragment : BaseFragment(R.layout.fragme onConferencesTileClickListener = { mainActivity.pushView(ConferenceFragment.newInstance()) } + onAdminMessageClickListener = presenter::onAdminMessageSelected + + registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + binding.dashboardRecycler.scrollToPosition(0) + } + }) } with(binding) { @@ -188,6 +197,10 @@ class DashboardFragment : BaseFragment(R.layout.fragme (requireActivity() as MainActivity).pushView(NotificationsCenterFragment.newInstance()) } + override fun openInternetBrowser(url: String) { + requireContext().openInternetBrowser(url) + } + override fun onDestroyView() { dashboardAdapter.clearTimers() presenter.onDetachView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt index cf99f0c9..92665857 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.dashboard +import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Grade @@ -16,6 +17,15 @@ sealed class DashboardItem(val type: Type) { abstract val isDataLoaded: Boolean + data class AdminMessages( + val adminMessage: AdminMessage? = null, + override val error: Throwable? = null, + override val isLoading: Boolean = false + ) : DashboardItem(Type.ADMIN_MESSAGE) { + + override val isDataLoaded get() = adminMessage != null + } + data class Account( val student: Student? = null, override val error: Throwable? = null, @@ -96,6 +106,7 @@ sealed class DashboardItem(val type: Type) { } enum class Type { + ADMIN_MESSAGE, ACCOUNT, HORIZONTAL_GROUP, LESSONS, @@ -108,6 +119,7 @@ sealed class DashboardItem(val type: Type) { } enum class Tile { + ADMIN_MESSAGE, ACCOUNT, LUCKY_NUMBER, MESSAGES, @@ -123,6 +135,7 @@ sealed class DashboardItem(val type: Type) { } fun DashboardItem.Tile.toDashboardItemType() = when (this) { + DashboardItem.Tile.ADMIN_MESSAGE -> DashboardItem.Type.ADMIN_MESSAGE DashboardItem.Tile.ACCOUNT -> DashboardItem.Type.ACCOUNT DashboardItem.Tile.LUCKY_NUMBER -> DashboardItem.Type.HORIZONTAL_GROUP DashboardItem.Tile.MESSAGES -> DashboardItem.Type.HORIZONTAL_GROUP diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt index cf4097a4..b9625570 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt @@ -21,7 +21,7 @@ class DashboardItemMoveCallback( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder ): Int { - val dragFlags = if (viewHolder.bindingAdapterPosition != 0) { + val dragFlags = if (!viewHolder.isAdminMessageOrAccountItem) { ItemTouchHelper.UP or ItemTouchHelper.DOWN } else 0 @@ -32,7 +32,7 @@ class DashboardItemMoveCallback( recyclerView: RecyclerView, current: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder - ) = target.bindingAdapterPosition != 0 + ) = !target.isAdminMessageOrAccountItem override fun onMove( recyclerView: RecyclerView, @@ -52,4 +52,7 @@ class DashboardItemMoveCallback( onUserInteractionEndListener(dashboardAdapter.items.toList()) } + + private val RecyclerView.ViewHolder.isAdminMessageOrAccountItem: Boolean + get() = this is DashboardAdapter.AdminMessageViewHolder || this is DashboardAdapter.AccountViewHolder } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index d6d533e2..5ff7f2ce 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -5,6 +5,7 @@ import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.enums.MessageFolder +import io.github.wulkanowy.data.repositories.AdminMessageRepository import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository import io.github.wulkanowy.data.repositories.ConferenceRepository import io.github.wulkanowy.data.repositories.ExamRepository @@ -50,7 +51,8 @@ class DashboardPresenter @Inject constructor( private val examRepository: ExamRepository, private val conferenceRepository: ConferenceRepository, private val preferencesRepository: PreferencesRepository, - private val schoolAnnouncementRepository: SchoolAnnouncementRepository + private val schoolAnnouncementRepository: SchoolAnnouncementRepository, + private val adminMessageRepository: AdminMessageRepository ) : BasePresenter(errorHandler, studentRepository) { private val dashboardItemLoadedList = mutableListOf() @@ -179,6 +181,7 @@ class DashboardPresenter @Inject constructor( loadConferences(student, forceRefresh) } DashboardItem.Type.ADS -> TODO() + DashboardItem.Type.ADMIN_MESSAGE -> loadAdminMessage(student, forceRefresh) } } } @@ -225,6 +228,10 @@ class DashboardPresenter @Inject constructor( }.toSet() } + fun onAdminMessageSelected(url: String?) { + url?.let { view?.openInternetBrowser(it) } + } + private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) { flow { val semester = semesterRepository.getCurrentSemester(student) @@ -567,6 +574,38 @@ class DashboardPresenter @Inject constructor( }.launch("dashboard_conferences") } + private fun loadAdminMessage(student: Student, forceRefresh: Boolean) { + flowWithResourceIn { adminMessageRepository.getAdminMessages(student, forceRefresh) } + .onEach { + when (it.status) { + Status.LOADING -> { + Timber.i("Loading dashboard admin message data started") + if (forceRefresh) return@onEach + updateData(DashboardItem.AdminMessages(), forceRefresh) + } + Status.SUCCESS -> { + Timber.i("Loading dashboard admin message result: Success") + updateData( + dashboardItem = DashboardItem.AdminMessages(adminMessage = it.data), + forceRefresh = forceRefresh + ) + } + Status.ERROR -> { + Timber.i("Loading dashboard admin message result: An exception occurred") + errorHandler.dispatch(it.error!!) + updateData( + dashboardItem = DashboardItem.AdminMessages( + adminMessage = it.data, + error = it.error + ), + forceRefresh = forceRefresh + ) + } + } + } + .launch("dashboard_admin_messages") + } + private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) { val isForceRefreshError = forceRefresh && dashboardItem.error != null val isFirstRunDataLoadedError = @@ -579,6 +618,11 @@ class DashboardPresenter @Inject constructor( sortDashboardItems() + if (dashboardItem is DashboardItem.AdminMessages && !dashboardItem.isDataLoaded) { + dashboardItemsToLoad = dashboardItemsToLoad - DashboardItem.Type.ADMIN_MESSAGE + dashboardItemLoadedList.removeAll { it.type == DashboardItem.Type.ADMIN_MESSAGE } + } + if (forceRefresh) { updateForceRefreshData(dashboardItem) } else { @@ -644,7 +688,9 @@ class DashboardPresenter @Inject constructor( itemsLoadedList: List, forceRefresh: Boolean ) { - val filteredItems = itemsLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT } + val filteredItems = itemsLoadedList.filterNot { + it.type == DashboardItem.Type.ACCOUNT || it.type == DashboardItem.Type.ADMIN_MESSAGE + } val isAccountItemError = itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null val isGeneralError = @@ -676,10 +722,13 @@ class DashboardPresenter @Inject constructor( val dashboardItemsPosition = preferencesRepository.dashboardItemsPosition dashboardItemLoadedList.sortBy { tile -> - dashboardItemsPosition?.getOrDefault( - tile.type, + val defaultPosition = if (tile is DashboardItem.AdminMessages) { + -1 + } else { tile.type.ordinal + 100 - ) ?: tile.type.ordinal + } + + dashboardItemsPosition?.getOrDefault(tile.type, defaultPosition) ?: tile.type.ordinal } } } \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt index 899eb320..730e19a3 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt @@ -25,4 +25,6 @@ interface DashboardView : BaseView { fun popViewToRoot() fun openNotificationsCenterView() + + fun openInternetBrowser(url: String) } \ No newline at end of file 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 2f76cd51..ea7215ce 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 @@ -1,7 +1,8 @@ package io.github.wulkanowy.ui.modules.login -import android.content.res.Resources +import android.content.Context import android.database.sqlite.SQLiteConstraintException +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.sdk.mobile.exception.InvalidPinException import io.github.wulkanowy.sdk.mobile.exception.InvalidSymbolException @@ -11,7 +12,8 @@ import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException import io.github.wulkanowy.ui.base.ErrorHandler import javax.inject.Inject -class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) { +class LoginErrorHandler @Inject constructor(@ApplicationContext context: Context) : + ErrorHandler(context) { var onBadCredentials: (String?) -> Unit = {} @@ -24,6 +26,7 @@ class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler var onStudentDuplicate: (String) -> Unit = {} override fun proceed(error: Throwable) { + val resources = context.resources when (error) { is BadCredentialsException -> onBadCredentials(error.message) is SQLiteConstraintException -> onStudentDuplicate(resources.getString(R.string.login_duplicate_student)) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/RecoverErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/RecoverErrorHandler.kt index 8619369d..ac4c0313 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/RecoverErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/RecoverErrorHandler.kt @@ -1,13 +1,15 @@ package io.github.wulkanowy.ui.modules.login.recover -import android.content.res.Resources +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.sdk.scrapper.exception.InvalidCaptchaException import io.github.wulkanowy.sdk.scrapper.exception.InvalidEmailException import io.github.wulkanowy.sdk.scrapper.exception.NoAccountFoundException import io.github.wulkanowy.ui.base.ErrorHandler import javax.inject.Inject -class RecoverErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) { +class RecoverErrorHandler @Inject constructor(@ApplicationContext context: Context) : + ErrorHandler(context) { var onInvalidUsername: (String) -> Unit = {} @@ -15,7 +17,8 @@ class RecoverErrorHandler @Inject constructor(resources: Resources) : ErrorHandl override fun proceed(error: Throwable) { when (error) { - is InvalidEmailException, is NoAccountFoundException -> onInvalidUsername(error.localizedMessage.orEmpty()) + is InvalidEmailException, + is NoAccountFoundException -> onInvalidUsername(error.localizedMessage.orEmpty()) is InvalidCaptchaException -> onInvalidCaptcha(error.localizedMessage.orEmpty(), error) else -> super.proceed(error) } 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 index 00ba0bad..36e38fb9 100644 --- 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 @@ -1,11 +1,13 @@ package io.github.wulkanowy.ui.modules.timetable.completed -import android.content.res.Resources +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException import io.github.wulkanowy.ui.base.ErrorHandler import javax.inject.Inject -class CompletedLessonsErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) { +class CompletedLessonsErrorHandler @Inject constructor(@ApplicationContext context: Context) : + ErrorHandler(context) { var onFeatureDisabled: () -> Unit = {} diff --git a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt index a3961aed..962e5b20 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt @@ -1,35 +1,31 @@ package io.github.wulkanowy.utils import android.content.res.Resources -import android.os.Build.MANUFACTURER -import android.os.Build.MODEL -import android.os.Build.VERSION.SDK_INT -import io.github.wulkanowy.BuildConfig.BUILD_TIMESTAMP -import io.github.wulkanowy.BuildConfig.DEBUG -import io.github.wulkanowy.BuildConfig.FLAVOR -import io.github.wulkanowy.BuildConfig.VERSION_CODE -import io.github.wulkanowy.BuildConfig.VERSION_NAME +import android.os.Build +import io.github.wulkanowy.BuildConfig import javax.inject.Inject import javax.inject.Singleton @Singleton open class AppInfo @Inject constructor() { - open val isDebug get() = DEBUG + open val isDebug get() = BuildConfig.DEBUG - open val versionCode get() = VERSION_CODE + open val versionCode get() = BuildConfig.VERSION_CODE - open val buildTimestamp get() = BUILD_TIMESTAMP + open val buildTimestamp get() = BuildConfig.BUILD_TIMESTAMP - open val buildFlavor get() = FLAVOR + open val buildFlavor get() = BuildConfig.FLAVOR - open val versionName get() = VERSION_NAME + open val versionName get() = BuildConfig.VERSION_NAME - open val systemVersion get() = SDK_INT + open val systemVersion get() = Build.VERSION.SDK_INT - open val systemManufacturer: String get() = MANUFACTURER + open val systemManufacturer: String get() = Build.MANUFACTURER - open val systemModel: String get() = MODEL + open val systemModel: String get() = Build.MODEL + + open val messagesBaseUrl = BuildConfig.MESSAGES_BASE_URL @Suppress("DEPRECATION") open val systemLanguage: String diff --git a/app/src/main/res/drawable/ic_dashboard_warning.xml b/app/src/main/res/drawable/ic_dashboard_warning.xml new file mode 100644 index 00000000..e7a5dc5a --- /dev/null +++ b/app/src/main/res/drawable/ic_dashboard_warning.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/item_dashboard_admin_message.xml b/app/src/main/res/layout/item_dashboard_admin_message.xml new file mode 100644 index 00000000..265cc14e --- /dev/null +++ b/app/src/main/res/layout/item_dashboard_admin_message.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index 5eca4680..881d5bd4 100644 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -8,6 +8,7 @@ @color/colorErrorLight @color/colorDividerInverse @color/colorSwipeRefreshDark + @color/dashboard_message_medium_light ?colorSurface ?android:textColorPrimary @color/colorNavigationBarLight diff --git a/app/src/main/res/values/api_hosts.xml b/app/src/main/res/values/api_hosts.xml index dac94c3f..15849047 100644 --- a/app/src/main/res/values/api_hosts.xml +++ b/app/src/main/res/values/api_hosts.xml @@ -40,7 +40,7 @@ https://vulcan.net.pl/?login https://vulcan.net.pl/?login https://vulcan.net.pl/?login - http://fakelog.tk/?email + http://fakelog.cf/?email Default diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 49ef39ab..d4ed6e97 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -3,4 +3,5 @@ + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index a68e2710..f3112b10 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -12,6 +12,9 @@ #1C1C1C #0D0D0D + #FFD980 + #ffd54f + #ffd54f #ff8f00 diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 628aa297..45382e6d 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -11,6 +11,7 @@ @color/colorError @color/colorDivider @color/colorSwipeRefresh + @color/dashboard_message_medium_dark @android:color/darker_gray ?android:textColorPrimary @style/PreferenceThemeOverlay From ac86737050fbeb95d593b8747279cc593eb025ad Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Thu, 14 Oct 2021 01:44:34 +0200 Subject: [PATCH 049/357] Fix NPE in SyncPresenter (#1582) --- .../java/io/github/wulkanowy/services/sync/SyncManager.kt | 2 +- .../wulkanowy/ui/modules/settings/sync/SyncPresenter.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt index 02d8b964..32ca20af 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt @@ -74,7 +74,7 @@ class SyncManager @Inject constructor( } } - fun startOneTimeSyncWorker(): Flow { + fun startOneTimeSyncWorker(): Flow { val work = OneTimeWorkRequestBuilder() .setInputData( Data.Builder() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt index 63e86a47..0d404a13 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt @@ -46,7 +46,7 @@ class SyncPresenter @Inject constructor( fun onSyncNowClicked() { view?.run { syncManager.startOneTimeSyncWorker().onEach { workInfo -> - when (workInfo.state) { + when (workInfo?.state) { WorkInfo.State.ENQUEUED -> { setSyncInProgress(true) Timber.i("Setting sync now started") @@ -63,9 +63,9 @@ class SyncPresenter @Inject constructor( ) analytics.logEvent("sync_now", "status" to "failed") } - else -> Timber.d("Sync now state: ${workInfo.state}") + else -> Timber.d("Sync now state: ${workInfo?.state}") } - if (workInfo.state.isFinished) { + if (workInfo?.state?.isFinished == true) { setSyncInProgress(false) setSyncDateInView() } From e7550f7a43a74b02df366501481eb679be7a9979 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Oct 2021 00:10:22 +0000 Subject: [PATCH 050/357] Bump gradle from 7.0.2 to 7.0.3 (#1586) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 153088a6..ad27aba0 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" - classpath 'com.android.tools.build:gradle:7.0.2' + classpath 'com.android.tools.build:gradle:7.0.3' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.10' classpath 'com.huawei.agconnect:agcp:1.6.1.200' From 4c8d9c8f7fa2fb347d467ccc08a0f864ed5b07c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 14 Oct 2021 16:25:09 +0200 Subject: [PATCH 051/357] Fix infinite refresh when admin messages list is empty (#1587) --- .../wulkanowy/ui/modules/dashboard/DashboardPresenter.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index 5ff7f2ce..f791fa0d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -620,6 +620,8 @@ class DashboardPresenter @Inject constructor( if (dashboardItem is DashboardItem.AdminMessages && !dashboardItem.isDataLoaded) { dashboardItemsToLoad = dashboardItemsToLoad - DashboardItem.Type.ADMIN_MESSAGE + dashboardTileLoadedList = dashboardTileLoadedList - DashboardItem.Tile.ADMIN_MESSAGE + dashboardItemLoadedList.removeAll { it.type == DashboardItem.Type.ADMIN_MESSAGE } } @@ -654,9 +656,12 @@ class DashboardPresenter @Inject constructor( } private fun updateForceRefreshData(dashboardItem: DashboardItem) { + val isNotLoadedAdminMessage = + dashboardItem is DashboardItem.AdminMessages && !dashboardItem.isDataLoaded + with(dashboardItemRefreshLoadedList) { removeAll { it.type == dashboardItem.type } - add(dashboardItem) + if (!isNotLoadedAdminMessage) add(dashboardItem) } val isRefreshItemLoaded = From 54e9ea6478226f40363c279dddc993406aae7220 Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Sat, 16 Oct 2021 11:17:00 +0200 Subject: [PATCH 052/357] Use FloatingActionButton.{show,hide} instead of using setVisibility (#1591) --- .../wulkanowy/ui/modules/attendance/AttendanceFragment.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt index 3fbdaec5..092c9d0d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt @@ -218,7 +218,13 @@ class AttendanceFragment : BaseFragment(R.layout.frag } override fun showExcuseButton(show: Boolean) { - binding.attendanceExcuseButton.visibility = if (show) VISIBLE else GONE + with(binding.attendanceExcuseButton) { + if (show) { + show() + } else { + hide() + } + } } override fun showAttendanceDialog(lesson: Attendance) { From 84e4167dbdaeb7a419693f1ba0978601b247a0bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 18 Oct 2021 00:11:46 +0200 Subject: [PATCH 053/357] Mi Band notification improvements (#1579) --- .../notifications/AppNotificationManager.kt | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt index 69d0092c..ddad9bf2 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Context import android.os.Build +import androidx.annotation.PluralsRes import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.qualifiers.ApplicationContext @@ -68,7 +69,8 @@ class AppNotificationManager @Inject constructor( ) { val groupType = notificationData.type.group ?: return val group = "${groupType}_${student.id}" - val groupId = student.id * 100 + notificationData.type.ordinal + + notificationData.sendSummaryNotification(group, student) notificationData.lines.forEach { item -> val title = context.resources.getQuantityString(notificationData.titleStringRes, 1) @@ -88,16 +90,26 @@ class AppNotificationManager @Inject constructor( saveNotification(title, item, notificationData, student) } + } + private fun MultipleNotificationsData.sendSummaryNotification(group: String, student: Student) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return - val summaryNotification = getDefaultNotificationBuilder(notificationData) - .setSmallIcon(notificationData.icon) + val summaryNotification = getDefaultNotificationBuilder(this) + .setSmallIcon(icon) + .setContentTitle(getQuantityString(titleStringRes, lines.size)) + .setContentText(getQuantityString(contentStringRes, lines.size)) + .setStyle( + NotificationCompat.InboxStyle() + .setSummaryText(student.nickOrName) + .also { builder -> lines.forEach { builder.addLine(it) } } + ) + .setLocalOnly(true) .setGroup(group) - .setStyle(NotificationCompat.InboxStyle().setSummaryText(student.nickOrName)) .setGroupSummary(true) .build() + val groupId = student.id * 100 + type.ordinal notificationManager.notify(groupId.toInt(), summaryNotification) } @@ -116,6 +128,7 @@ class AppNotificationManager @Inject constructor( .setDefaults(NotificationCompat.DEFAULT_ALL) .setPriority(NotificationCompat.PRIORITY_HIGH) .setColor(context.getCompatColor(R.color.colorPrimary)) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) .setContentIntent( PendingIntent.getActivity( context, @@ -142,4 +155,8 @@ class AppNotificationManager @Inject constructor( notificationRepository.saveNotification(notificationEntity) } -} \ No newline at end of file + + private fun getQuantityString(@PluralsRes res: Int, arg: Int): String { + return context.resources.getQuantityString(res, arg, arg) + } +} From 58ea2c530e55991843e47ecbb9824e46678921a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Oct 2021 06:36:46 +0000 Subject: [PATCH 054/357] Bump hianalytics from 6.3.0.300 to 6.3.0.301 (#1593) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 227423e4..90b4cdb9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -234,7 +234,7 @@ dependencies { playImplementation 'com.google.android.play:core:1.10.2' playImplementation 'com.google.android.play:core-ktx:1.8.1' - hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.300' + hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.301' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.200' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 09a134d442c26d3e2a3e242ef5765b348b8a2c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 21 Oct 2021 10:47:37 +0200 Subject: [PATCH 055/357] Fix last sync date to save only successful sync (#1595) --- .../data/repositories/AppCreatorRepository.kt | 2 +- .../data/repositories/LoggerRepository.kt | 23 +++++++++--------- .../data/repositories/SemesterRepository.kt | 4 ++-- .../data/repositories/StudentRepository.kt | 8 +++---- .../TimetableNotificationSchedulerHelper.kt | 4 ++-- .../wulkanowy/services/sync/SyncWorker.kt | 24 +++++++++++-------- .../modules/about/license/LicensePresenter.kt | 2 +- .../wulkanowy/utils/DispatchersProvider.kt | 4 +--- .../wulkanowy/TestDispatchersProvider.kt | 4 +--- 9 files changed, 37 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt index 3d83f718..cbaa12bd 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt @@ -20,7 +20,7 @@ class AppCreatorRepository @Inject constructor( @OptIn(ExperimentalSerializationApi::class) @Suppress("BlockingMethodInNonBlockingContext") - suspend fun getAppCreators() = withContext(dispatchers.backgroundThread) { + suspend fun getAppCreators() = withContext(dispatchers.io) { val inputStream = context.assets.open("contributors.json").buffered() json.decodeFromStream>(inputStream) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/LoggerRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/LoggerRepository.kt index 6d509b02..1a8cd6ea 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/LoggerRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/LoggerRepository.kt @@ -15,24 +15,23 @@ class LoggerRepository @Inject constructor( suspend fun getLastLogLines() = getLastModified().readText().split("\n") - suspend fun getLogFiles() = withContext(dispatchers.backgroundThread) { - File(context.filesDir.absolutePath).listFiles(File::isFile)?.filter { - it.name.endsWith(".log") - }!! + suspend fun getLogFiles() = withContext(dispatchers.io) { + File(context.filesDir.absolutePath).listFiles(File::isFile) + ?.filter { it.name.endsWith(".log") }!! } - private suspend fun getLastModified(): File { - return withContext(dispatchers.backgroundThread) { - var lastModifiedTime = Long.MIN_VALUE - var chosenFile: File? = null - File(context.filesDir.absolutePath).listFiles(File::isFile)?.forEach { file -> + private suspend fun getLastModified() = withContext(dispatchers.io) { + var lastModifiedTime = Long.MIN_VALUE + var chosenFile: File? = null + + File(context.filesDir.absolutePath).listFiles(File::isFile) + ?.forEach { file -> if (file.lastModified() > lastModifiedTime) { lastModifiedTime = file.lastModified() chosenFile = file } } - if (chosenFile == null) throw FileNotFoundException("Log file not found") - chosenFile!! - } + + chosenFile ?: throw FileNotFoundException("Log file not found") } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt index 4336877a..cc954558 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt @@ -26,7 +26,7 @@ class SemesterRepository @Inject constructor( student: Student, forceRefresh: Boolean = false, refreshOnNoCurrent: Boolean = false - ) = withContext(dispatchers.backgroundThread) { + ) = withContext(dispatchers.io) { val semesters = semesterDb.loadAll(student.studentId, student.classId) if (isShouldFetch(student, semesters, forceRefresh, refreshOnNoCurrent)) { @@ -64,7 +64,7 @@ class SemesterRepository @Inject constructor( } suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) = - withContext(dispatchers.backgroundThread) { + withContext(dispatchers.io) { getSemesters(student, forceRefresh).getCurrentOrLast() } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt index 2ac892d0..9e4a1aab 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt @@ -66,7 +66,7 @@ class StudentRepository @Inject constructor( .map { it.apply { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { - student.password = withContext(dispatchers.backgroundThread) { + student.password = withContext(dispatchers.io) { decrypt(student.password) } } @@ -77,7 +77,7 @@ class StudentRepository @Inject constructor( val student = studentDb.loadById(id) ?: throw NoCurrentStudentException() if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { - student.password = withContext(dispatchers.backgroundThread) { + student.password = withContext(dispatchers.io) { decrypt(student.password) } } @@ -88,7 +88,7 @@ class StudentRepository @Inject constructor( val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException() if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { - student.password = withContext(dispatchers.backgroundThread) { + student.password = withContext(dispatchers.io) { decrypt(student.password) } } @@ -101,7 +101,7 @@ class StudentRepository @Inject constructor( .map { it.apply { if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) { - password = withContext(dispatchers.backgroundThread) { + password = withContext(dispatchers.io) { encrypt(password, context) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt index a42a0ab4..2ec4e527 100644 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt @@ -54,7 +54,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor( suspend fun cancelScheduled(lessons: List, student: Student) { val studentId = student.studentId - withContext(dispatchersProvider.backgroundThread) { + withContext(dispatchersProvider.io) { lessons.sortedBy { it.start }.forEachIndexed { index, lesson -> val upcomingTime = getUpcomingLessonTime(index, lessons, lesson) cancelScheduledTo( @@ -91,7 +91,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor( return } - withContext(dispatchersProvider.backgroundThread) { + withContext(dispatchersProvider.io) { lessons.groupBy { it.date } .map { it.value.sortedBy { lesson -> lesson.start } } .map { it.filter { lesson -> lesson.isStudentPlan } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/SyncWorker.kt b/app/src/main/java/io/github/wulkanowy/services/sync/SyncWorker.kt index ea1f79cb..52979e63 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/SyncWorker.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/SyncWorker.kt @@ -19,11 +19,11 @@ import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException import io.github.wulkanowy.services.sync.channels.DebugChannel import io.github.wulkanowy.services.sync.works.Work +import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.getCompatColor -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext import timber.log.Timber import java.time.LocalDateTime -import java.time.ZoneId import kotlin.random.Random @HiltWorker @@ -34,13 +34,14 @@ class SyncWorker @AssistedInject constructor( private val semesterRepository: SemesterRepository, private val works: Set<@JvmSuppressWildcards Work>, private val preferencesRepository: PreferencesRepository, - private val notificationManager: NotificationManagerCompat + private val notificationManager: NotificationManagerCompat, + private val dispatchersProvider: DispatchersProvider ) : CoroutineWorker(appContext, workerParameters) { - override suspend fun doWork() = coroutineScope { + override suspend fun doWork() = withContext(dispatchersProvider.io) { Timber.i("SyncWorker is starting") - if (!studentRepository.isCurrentStudentSet()) return@coroutineScope Result.failure() + if (!studentRepository.isCurrentStudentSet()) return@withContext Result.failure() val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student, true) @@ -50,12 +51,12 @@ class SyncWorker @AssistedInject constructor( Timber.i("${work::class.java.simpleName} is starting") work.doWork(student, semester) Timber.i("${work::class.java.simpleName} result: Success") - preferencesRepository.lasSyncDate = LocalDateTime.now(ZoneId.systemDefault()) null } catch (e: Throwable) { Timber.w("${work::class.java.simpleName} result: An exception ${e.message} occurred") - if (e is FeatureDisabledException || e is FeatureNotAvailableException) null - else { + if (e is FeatureDisabledException || e is FeatureNotAvailableException) { + null + } else { Timber.e(e) e } @@ -70,13 +71,16 @@ class SyncWorker @AssistedInject constructor( ) } exceptions.isNotEmpty() -> Result.retry() - else -> Result.success() + else -> { + preferencesRepository.lasSyncDate = LocalDateTime.now() + Result.success() + } } if (preferencesRepository.isDebugNotificationEnable) notify(result) Timber.i("SyncWorker result: $result") - result + return@withContext result } private fun notify(result: Result) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicensePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicensePresenter.kt index cc430fc2..5368cc19 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicensePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicensePresenter.kt @@ -31,7 +31,7 @@ class LicensePresenter @Inject constructor( private fun loadData() { flowWithResource { - withContext(dispatchers.backgroundThread) { + withContext(dispatchers.io) { view?.appLibraries.orEmpty() } }.onEach { diff --git a/app/src/main/java/io/github/wulkanowy/utils/DispatchersProvider.kt b/app/src/main/java/io/github/wulkanowy/utils/DispatchersProvider.kt index ecc8e05e..8aaa57f4 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/DispatchersProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/DispatchersProvider.kt @@ -1,10 +1,8 @@ package io.github.wulkanowy.utils -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers open class DispatchersProvider { - open val backgroundThread: CoroutineDispatcher - get() = Dispatchers.IO + open val io get() = Dispatchers.IO } diff --git a/app/src/test/java/io/github/wulkanowy/TestDispatchersProvider.kt b/app/src/test/java/io/github/wulkanowy/TestDispatchersProvider.kt index e60b1d7a..d4cba7b5 100644 --- a/app/src/test/java/io/github/wulkanowy/TestDispatchersProvider.kt +++ b/app/src/test/java/io/github/wulkanowy/TestDispatchersProvider.kt @@ -1,11 +1,9 @@ package io.github.wulkanowy import io.github.wulkanowy.utils.DispatchersProvider -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers class TestDispatchersProvider : DispatchersProvider() { - override val backgroundThread: CoroutineDispatcher - get() = Dispatchers.Unconfined + override val io get() = Dispatchers.Unconfined } From 94fd303f8ea6edcdd0f8e528cf623615c8aa3619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 21 Oct 2021 10:51:00 +0200 Subject: [PATCH 056/357] Add single support advert (#1484) --- app/build.gradle | 20 ++-- app/src/main/AndroidManifest.xml | 13 ++- .../ui/modules/about/AboutPresenter.kt | 26 ++--- .../ui/modules/dashboard/DashboardAdapter.kt | 3 +- .../settings/advanced/AdvancedFragment.kt | 4 - .../settings/appearance/AppearanceFragment.kt | 4 - .../notifications/NotificationsFragment.kt | 4 - .../ui/modules/settings/sync/SyncFragment.kt | 4 - app/src/main/res/drawable/ic_settings_ads.xml | 22 +++++ app/src/main/res/values/preferences_keys.xml | 1 + app/src/main/res/values/strings.xml | 10 ++ app/src/main/res/xml/scheme_preferences.xml | 30 +++--- .../ui/modules/settings/ads/AdsFragment.kt | 94 +++++++++++++++++++ .../ui/modules/settings/ads/AdsPresenter.kt | 41 ++++++++ .../ui/modules/settings/ads/AdsView.kt | 17 ++++ .../io/github/wulkanowy/utils/AdsHelper.kt | 39 ++++++++ app/src/play/res/xml/scheme_preferences.xml | 39 ++++++++ .../play/res/xml/scheme_preferences_ads.xml | 12 +++ 18 files changed, 324 insertions(+), 59 deletions(-) create mode 100644 app/src/main/res/drawable/ic_settings_ads.xml create mode 100644 app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt create mode 100644 app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsPresenter.kt create mode 100644 app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsView.kt create mode 100644 app/src/play/java/io/github/wulkanowy/utils/AdsHelper.kt create mode 100644 app/src/play/res/xml/scheme_preferences.xml create mode 100644 app/src/play/res/xml/scheme_preferences_ads.xml diff --git a/app/build.gradle b/app/build.gradle index 90b4cdb9..42177406 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,7 +29,10 @@ android { resValue "string", "app_name", "Wulkanowy" - manifestPlaceholders = [firebase_enabled: project.hasProperty("enableFirebase")] + manifestPlaceholders = [ + firebase_enabled: project.hasProperty("enableFirebase"), + admob_project_id: "" + ] javaCompileOptions { annotationProcessorOptions { arguments += [ @@ -39,6 +42,8 @@ android { } } + buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null" + if (System.env.SET_BUILD_TIMESTAMP) { buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis()) } else { @@ -82,23 +87,21 @@ android { productFlavors { hms { dimension "platform" - manifestPlaceholders = [ - install_channel: "AppGallery" - ] + manifestPlaceholders = [install_channel: "AppGallery"] } play { dimension "platform" manifestPlaceholders = [ - install_channel: "Google Play" + install_channel : "Google Play", + admob_project_id: System.getenv("ADMOB_PROJECT_ID") ?: "ca-app-pub-3940256099942544~3347511713" ] + buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "\"${System.getenv("SINGLE_SUPPORT_AD_ID") ?: "ca-app-pub-3940256099942544/5354046379"}\"" } fdroid { dimension "platform" - manifestPlaceholders = [ - install_channel: "F-Droid" - ] + manifestPlaceholders = [install_channel: "F-Droid"] } } @@ -233,6 +236,7 @@ dependencies { playImplementation 'com.google.firebase:firebase-crashlytics:' playImplementation 'com.google.android.play:core:1.10.2' playImplementation 'com.google.android.play:core-ktx:1.8.1' + playImplementation 'com.google.android.gms:play-services-ads:20.4.0' hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.301' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.200' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8c7eb852..810d469f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -165,33 +165,32 @@ - - - - - - - + + 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 6bcf5f77..55274934 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 @@ -82,18 +82,20 @@ class AboutPresenter @Inject constructor( private fun loadData() { view?.run { - updateData(listOfNotNull( - versionRes, - creatorsRes, - feedbackRes, - faqRes, - discordRes, - facebookRes, - twitterRes, - homepageRes, - licensesRes, - privacyRes - )) + updateData( + listOfNotNull( + versionRes, + creatorsRes, + feedbackRes, + faqRes, + discordRes, + facebookRes, + twitterRes, + homepageRes, + licensesRes, + privacyRes + ) + ) } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt index 405bfbc5..440bbd5d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt @@ -299,7 +299,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter + + + + + + diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index c512a5f2..1aba7d85 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -34,4 +34,5 @@ message_send_recipients last_sync_date notifications_piggyback + single_ad_support diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 48c7712d..13030366 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -658,10 +658,19 @@ Reply with message history Show arithmetic average when no weights provided + Support + Watch single ad to support project + Consent to data processing + To view an advertisement you must agree to the data processing terms of our Privacy Policy + Agree + Privacy policy + Ad is loading + Advanced Appearance & Behavior Notifications Synchronization + Advertisements Grades Dashboard @@ -681,6 +690,7 @@ Plus and minus values, average calculation Advanced App version, contributors, social portals, licenses + Displaying advertisements, project support diff --git a/app/src/main/res/xml/scheme_preferences.xml b/app/src/main/res/xml/scheme_preferences.xml index 08621492..5bf7ad8a 100644 --- a/app/src/main/res/xml/scheme_preferences.xml +++ b/app/src/main/res/xml/scheme_preferences.xml @@ -1,33 +1,33 @@ + app:title="@string/pref_appearance_category" /> + app:title="@string/pref_notifications_category" /> + app:title="@string/pref_sync_category" /> + app:title="@string/pref_advanced_category" /> + app:title="@string/about_title" /> diff --git a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt new file mode 100644 index 00000000..960a54b8 --- /dev/null +++ b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt @@ -0,0 +1,94 @@ +package io.github.wulkanowy.ui.modules.settings.ads + +import android.os.Bundle +import android.view.View +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAd +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.ui.base.BaseActivity +import io.github.wulkanowy.ui.base.ErrorDialog +import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.utils.openInternetBrowser +import javax.inject.Inject + +@AndroidEntryPoint +class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView { + + @Inject + lateinit var presenter: AdsPresenter + + override val titleStringId = R.string.pref_settings_ads_title + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.scheme_preferences_ads, rootKey) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.onAttachView(this) + } + + override fun initView() { + findPreference(getString(R.string.pref_key_ads_single_support))?.setOnPreferenceClickListener { + presenter.onWatchSingleAdSelected() + true + } + } + + override fun showAd(ad: RewardedInterstitialAd) { + if (isVisible) { + ad.show(requireActivity()) {} + } + } + + override fun showPrivacyPolicyDialog() { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.pref_ads_privacy_title)) + .setMessage(getString(R.string.pref_ads_privacy_description)) + .setPositiveButton(getString(R.string.pref_ads_privacy_agree)) { _, _ -> presenter.onAgreedPrivacy() } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .setNeutralButton(getString(R.string.pref_ads_privacy_link)) { _, _ -> presenter.onPrivacySelected() } + .show() + } + + override fun openPrivacyPolicy() { + requireContext().openInternetBrowser( + "https://wulkanowy.github.io/polityka-prywatnosci.html", + ::showMessage + ) + } + + override fun showLoadingSupportAd(show: Boolean) { + findPreference(getString(R.string.pref_key_ads_single_support))?.run { + isEnabled = !show + summary = if (show) getString(R.string.pref_ads_loading) else null + } + } + + override fun showError(text: String, error: Throwable) { + (activity as? BaseActivity<*, *>)?.showError(text, error) + } + + override fun showMessage(text: String) { + (activity as? BaseActivity<*, *>)?.showMessage(text) + } + + override fun showExpiredDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredDialog() + } + + override fun showChangePasswordSnackbar(redirectUrl: String) { + (activity as? BaseActivity<*, *>)?.showChangePasswordSnackbar(redirectUrl) + } + + override fun openClearLoginView() { + (activity as? BaseActivity<*, *>)?.openClearLoginView() + } + + override fun showErrorDetailsDialog(error: Throwable) { + ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) + } +} \ No newline at end of file diff --git a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsPresenter.kt b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsPresenter.kt new file mode 100644 index 00000000..fd5cc9b6 --- /dev/null +++ b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsPresenter.kt @@ -0,0 +1,41 @@ +package io.github.wulkanowy.ui.modules.settings.ads + +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.ErrorHandler +import io.github.wulkanowy.utils.AdsHelper +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +class AdsPresenter @Inject constructor( + errorHandler: ErrorHandler, + studentRepository: StudentRepository, + private val adsHelper: AdsHelper +) : BasePresenter(errorHandler, studentRepository) { + + override fun onAttachView(view: AdsView) { + super.onAttachView(view) + view.initView() + Timber.i("Settings ads view was initialized") + } + + fun onWatchSingleAdSelected() { + view?.showPrivacyPolicyDialog() + } + + fun onPrivacySelected() { + view?.openPrivacyPolicy() + } + + fun onAgreedPrivacy() { + view?.showLoadingSupportAd(true) + presenterScope.launch { + runCatching { adsHelper.getSupportAd() } + .onFailure(errorHandler::dispatch) + .onSuccess { it?.let { view?.showAd(it) } } + + view?.showLoadingSupportAd(false) + } + } +} \ No newline at end of file diff --git a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsView.kt b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsView.kt new file mode 100644 index 00000000..25eeaaec --- /dev/null +++ b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsView.kt @@ -0,0 +1,17 @@ +package io.github.wulkanowy.ui.modules.settings.ads + +import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAd +import io.github.wulkanowy.ui.base.BaseView + +interface AdsView : BaseView { + + fun initView() + + fun showAd(ad: RewardedInterstitialAd) + + fun showPrivacyPolicyDialog() + + fun openPrivacyPolicy() + + fun showLoadingSupportAd(show: Boolean) +} \ No newline at end of file diff --git a/app/src/play/java/io/github/wulkanowy/utils/AdsHelper.kt b/app/src/play/java/io/github/wulkanowy/utils/AdsHelper.kt new file mode 100644 index 00000000..f363c13f --- /dev/null +++ b/app/src/play/java/io/github/wulkanowy/utils/AdsHelper.kt @@ -0,0 +1,39 @@ +package io.github.wulkanowy.utils + +import android.content.Context +import com.google.android.gms.ads.AdRequest +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.ads.MobileAds +import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAd +import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAdLoadCallback +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.BuildConfig +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class AdsHelper @Inject constructor(@ApplicationContext private val context: Context) { + + suspend fun getSupportAd(): RewardedInterstitialAd? { + MobileAds.initialize(context) + + val adRequest = AdRequest.Builder().build() + + return suspendCoroutine { + RewardedInterstitialAd.load( + context, + BuildConfig.SINGLE_SUPPORT_AD_ID, + adRequest, + object : RewardedInterstitialAdLoadCallback() { + override fun onAdLoaded(rewardedInterstitialAd: RewardedInterstitialAd) { + it.resume(rewardedInterstitialAd) + } + + override fun onAdFailedToLoad(loadAdError: LoadAdError) { + it.resumeWithException(IllegalArgumentException(loadAdError.message)) + } + }) + } + } +} \ No newline at end of file diff --git a/app/src/play/res/xml/scheme_preferences.xml b/app/src/play/res/xml/scheme_preferences.xml new file mode 100644 index 00000000..05b0bf64 --- /dev/null +++ b/app/src/play/res/xml/scheme_preferences.xml @@ -0,0 +1,39 @@ + + + + + + + + + diff --git a/app/src/play/res/xml/scheme_preferences_ads.xml b/app/src/play/res/xml/scheme_preferences_ads.xml new file mode 100644 index 00000000..6b3625ca --- /dev/null +++ b/app/src/play/res/xml/scheme_preferences_ads.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file From 0f800b61f63f8b7ae303316354ae371eafc9799d Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Sun, 24 Oct 2021 01:23:36 +0200 Subject: [PATCH 057/357] Allow expanding multiple subject' grades at once (#1584) --- .../github/wulkanowy/data/db/AppDatabase.kt | 2 +- .../data/db/migrations/Migration41.kt | 12 +- .../repositories/PreferencesRepository.kt | 15 +- .../ui/modules/grade/GradeExpandMode.kt | 9 ++ .../grade/details/GradeDetailsAdapter.kt | 146 +++++++++++++----- .../grade/details/GradeDetailsFragment.kt | 5 +- .../grade/details/GradeDetailsPresenter.kt | 33 ++-- .../modules/grade/details/GradeDetailsView.kt | 3 +- .../main/res/values/preferences_defaults.xml | 2 +- app/src/main/res/values/preferences_keys.xml | 3 +- .../main/res/values/preferences_values.xml | 11 ++ .../res/xml/scheme_preferences_appearance.xml | 11 +- 12 files changed, 187 insertions(+), 65 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeExpandMode.kt 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 b8831acd..d9685606 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 @@ -193,7 +193,7 @@ abstract class AppDatabase : RoomDatabase() { Migration38(), Migration39(), Migration40(), - Migration41(), + Migration41(sharedPrefProvider), Migration42() ) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt index 17773efd..0080e057 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt @@ -2,10 +2,20 @@ package io.github.wulkanowy.data.db.migrations import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import io.github.wulkanowy.data.db.SharedPrefProvider +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode -class Migration41 : Migration(40, 41) { +class Migration41(private val sharedPrefProvider: SharedPrefProvider) : Migration(40, 41) { override fun migrate(database: SupportSQLiteDatabase) { + migrateSharedPreferences() database.execSQL("ALTER TABLE Homework ADD COLUMN is_added_by_user INTEGER NOT NULL DEFAULT 0") } + + private fun migrateSharedPreferences() { + if (sharedPrefProvider.getBoolean("pref_key_expand_grade", false)) { + sharedPrefProvider.putString("pref_key_expand_grade_mode", GradeExpandMode.ALWAYS_EXPANDED.value) + } + sharedPrefProvider.delete("pref_key_expand_grade") + } } \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index b0991bbc..696ffd63 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -10,6 +10,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.sdk.toLocalDate import io.github.wulkanowy.ui.modules.dashboard.DashboardItem import io.github.wulkanowy.ui.modules.grade.GradeAverageMode +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode import io.github.wulkanowy.ui.modules.grade.GradeSortingMode import io.github.wulkanowy.utils.toLocalDateTime import io.github.wulkanowy.utils.toTimestamp @@ -19,6 +20,8 @@ import kotlinx.coroutines.flow.map import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import java.lang.ClassCastException +import java.lang.IllegalStateException import java.time.LocalDate import java.time.LocalDateTime import javax.inject.Inject @@ -56,8 +59,13 @@ class PreferencesRepository @Inject constructor( R.bool.pref_default_grade_average_force_calc ) - val isGradeExpandable: Boolean - get() = !getBoolean(R.string.pref_key_expand_grade, R.bool.pref_default_expand_grade) + val gradeExpandMode: GradeExpandMode + get() = GradeExpandMode.getByValue( + getString( + R.string.pref_key_expand_grade_mode, + R.string.pref_default_expand_grade_mode + ) + ) val showAllSubjectsOnStatisticsList: Boolean get() = getBoolean( @@ -265,6 +273,9 @@ class PreferencesRepository @Inject constructor( private fun getBoolean(id: String, default: Int) = sharedPref.getBoolean(id, context.resources.getBoolean(default)) + private fun getBoolean(id: Int, default: Boolean) = + sharedPref.getBoolean(context.getString(id), default) + private companion object { private const val PREF_KEY_DASHBOARD_ITEMS_POSITION = "dashboard_items_position" diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeExpandMode.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeExpandMode.kt new file mode 100644 index 00000000..722e986e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeExpandMode.kt @@ -0,0 +1,9 @@ +package io.github.wulkanowy.ui.modules.grade + +enum class GradeExpandMode(val value: String) { + ONE("one"), UNLIMITED("any"), ALWAYS_EXPANDED("always"); + + companion object { + fun getByValue(value: String) = values().firstOrNull { it.value == value } ?: ONE + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt index 01631140..d96ac092 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt @@ -5,6 +5,7 @@ import android.content.res.Resources import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.NO_POSITION @@ -13,9 +14,11 @@ import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.databinding.HeaderGradeDetailsBinding import io.github.wulkanowy.databinding.ItemGradeDetailsBinding import io.github.wulkanowy.ui.base.BaseExpandableAdapter +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode import io.github.wulkanowy.utils.getBackgroundColor import io.github.wulkanowy.utils.toFormattedString import timber.log.Timber +import java.util.BitSet import javax.inject.Inject class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter() { @@ -24,19 +27,20 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter() - private var expandedPosition = NO_POSITION + private val expandedPositions = BitSet(items.size) - private var isExpandable = false + private var expandMode = GradeExpandMode.ONE var onClickListener: (Grade, position: Int) -> Unit = { _, _ -> } var colorTheme = "" - fun setDataItems(data: List, isExpanded: Boolean = isExpandable) { + fun setDataItems(data: List, expandMode: GradeExpandMode = this.expandMode) { headers = data.filter { it.viewType == ViewType.HEADER }.toMutableList() - items = if (isExpanded) headers else data.toMutableList() - isExpandable = isExpanded - expandedPosition = NO_POSITION + items = + (if (expandMode != GradeExpandMode.ALWAYS_EXPANDED) headers else data).toMutableList() + this.expandMode = expandMode + expandedPositions.clear() } fun updateDetailsItem(position: Int, grade: Grade) { @@ -48,7 +52,7 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter 1) { - Timber.e("Header with subject $subject found ${candidates.size} times! Expanded: $expandedPosition. Items: $candidates") + Timber.e("Header with subject $subject found ${candidates.size} times! Expanded: $expandedPositions. Items: $candidates") } return candidates.first() @@ -64,9 +68,9 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter HeaderViewHolder(HeaderGradeDetailsBinding.inflate(inflater, parent, false)) - ViewType.ITEM.id -> ItemViewHolder(ItemGradeDetailsBinding.inflate(inflater, parent, false)) + ViewType.HEADER.id -> HeaderViewHolder( + HeaderGradeDetailsBinding.inflate(inflater, parent, false) + ) + ViewType.ITEM.id -> ItemViewHolder( + ItemGradeDetailsBinding.inflate(inflater, parent, false) + ) else -> throw IllegalStateException() } } @@ -106,46 +114,91 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter 0) View.VISIBLE else View.GONE - if (header.newGrades > 0) gradeHeaderNote.text = header.newGrades.toString(10) + gradeHeaderPointsSum.text = + context.getString(R.string.grade_points_sum, header.pointsSum) + gradeHeaderPointsSum.isVisible = !header.pointsSum.isNullOrEmpty() + gradeHeaderNumber.text = context.resources.getQuantityString( + R.plurals.grade_number_item, + header.grades.size, + header.grades.size + ) + gradeHeaderNote.isVisible = header.newGrades > 0 - gradeHeaderContainer.isEnabled = isExpandable + if (header.newGrades > 0) { + gradeHeaderNote.text = header.newGrades.toString() + } + + gradeHeaderContainer.isEnabled = expandMode != GradeExpandMode.ALWAYS_EXPANDED gradeHeaderContainer.setOnClickListener { - expandedPosition = if (expandedPosition == adapterPosition) -1 else adapterPosition - - if (expandedPosition != NO_POSITION) { - refreshList(headers.toMutableList().apply { - addAll(headerPosition + 1, header.grades) - }) - scrollToHeaderWithSubItems(headerPosition, header.grades.size) - } else { - refreshList(headers) - } + expandGradeHeader(headerPosition, header, holder) } } } - private fun formatAverage(average: Double?, resources: Resources): String { - return if (average == null || average == .0) resources.getString(R.string.grade_no_average) - else resources.getString(R.string.grade_average, average) + private fun expandGradeHeader( + headerPosition: Int, + header: GradeDetailsHeader, + holder: HeaderViewHolder + ) { + if (expandMode == GradeExpandMode.ONE) { + val isHeaderExpanded = expandedPositions[headerPosition] + + expandedPositions.clear() + + if (!isHeaderExpanded) { + val updatedItemList = headers.toMutableList() + .apply { addAll(headerPosition + 1, header.grades) } + + expandedPositions.set(headerPosition) + refreshList(updatedItemList) + scrollToHeaderWithSubItems(headerPosition, header.grades.size) + } else { + refreshList(headers.toMutableList()) + } + } else if (expandMode == GradeExpandMode.UNLIMITED) { + val headerAdapterPosition = holder.bindingAdapterPosition + val isHeaderExpanded = expandedPositions[headerPosition] + + expandedPositions.flip(headerPosition) + + if (!isHeaderExpanded) { + val updatedList = items.toMutableList() + .apply { addAll(headerAdapterPosition + 1, header.grades) } + + refreshList(updatedList) + scrollToHeaderWithSubItems(headerAdapterPosition, header.grades.size) + } else { + val startPosition = headerAdapterPosition + 1 + val updatedList = items.toMutableList() + .apply { + subList(startPosition, startPosition + header.grades.size).clear() + } + + refreshList(updatedList) + } + } } @SuppressLint("SetTextI18n") private fun bindItemViewHolder(holder: ItemViewHolder, grade: Grade) { + val context = holder.binding.root.context + with(holder.binding) { gradeItemValue.run { text = grade.entry @@ -154,26 +207,37 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter grade.description grade.gradeSymbol.isNotBlank() -> grade.gradeSymbol - else -> root.context.getString(R.string.all_no_description) + else -> context.getString(R.string.all_no_description) } gradeItemDate.text = grade.date.toFormattedString() - gradeItemWeight.text = "${root.context.getString(R.string.grade_weight)}: ${grade.weight}" + gradeItemWeight.text = "${context.getString(R.string.grade_weight)}: ${grade.weight}" gradeItemNote.visibility = if (!grade.isRead) View.VISIBLE else View.GONE root.setOnClickListener { - holder.bindingAdapterPosition.let { if (it != NO_POSITION) onClickListener(grade, it) } + holder.bindingAdapterPosition.let { + if (it != NO_POSITION) onClickListener(grade, it) + } } } } + private fun formatAverage(average: Double?, resources: Resources) = + if (average == null || average == .0) { + resources.getString(R.string.grade_no_average) + } else { + resources.getString(R.string.grade_average, average) + } + private class HeaderViewHolder(val binding: HeaderGradeDetailsBinding) : RecyclerView.ViewHolder(binding.root) private class ItemViewHolder(val binding: ItemGradeDetailsBinding) : RecyclerView.ViewHolder(binding.root) - class GradeDetailsDiffUtil(private val old: List, private val new: List) : - DiffUtil.Callback() { + private class GradeDetailsDiffUtil( + private val old: List, + private val new: List + ) : DiffUtil.Callback() { override fun getOldListSize() = old.size diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt index 9d4da767..c93600d4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt @@ -12,6 +12,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode import io.github.wulkanowy.databinding.FragmentGradeDetailsBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment @@ -79,10 +80,10 @@ class GradeDetailsFragment : else false } - override fun updateData(data: List, isGradeExpandable: Boolean, gradeColorTheme: String) { + override fun updateData(data: List, expandMode: GradeExpandMode, gradeColorTheme: String) { with(gradeDetailsAdapter) { colorTheme = gradeColorTheme - setDataItems(data, isGradeExpandable) + setDataItems(data, expandMode) notifyDataSetChanged() } } 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 7544d2aa..9b9651e1 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 @@ -9,6 +9,7 @@ import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode import io.github.wulkanowy.ui.modules.grade.GradeSortingMode.ALPHABETIC import io.github.wulkanowy.ui.modules.grade.GradeSortingMode.DATE import io.github.wulkanowy.ui.modules.grade.GradeSubject @@ -113,7 +114,7 @@ class GradeDetailsPresenter @Inject constructor( fun onParentViewReselected() { view?.run { if (!isViewEmpty) { - if (preferencesRepository.isGradeExpandable) collapseAllItems() + if (preferencesRepository.gradeExpandMode != GradeExpandMode.ALWAYS_EXPANDED) collapseAllItems() scrollToStart() } } @@ -157,7 +158,7 @@ class GradeDetailsPresenter @Inject constructor( showContent(true) updateData( data = items, - isGradeExpandable = preferencesRepository.isGradeExpandable, + expandMode = preferencesRepository.gradeExpandMode, gradeColorTheme = preferencesRepository.gradeColorTheme ) notifyParentDataLoaded(semesterId) @@ -175,7 +176,7 @@ class GradeDetailsPresenter @Inject constructor( showContent(items.isNotEmpty()) updateData( data = items, - isGradeExpandable = preferencesRepository.isGradeExpandable, + expandMode = preferencesRepository.gradeExpandMode, gradeColorTheme = preferencesRepository.gradeColorTheme ) } @@ -235,14 +236,24 @@ class GradeDetailsPresenter @Inject constructor( .sortedByDescending { it.date } .map { GradeDetailsItem(it, ViewType.ITEM) } - listOf(GradeDetailsItem(GradeDetailsHeader( - subject = subject, - average = average, - pointsSum = points, - grades = subItems - ).apply { - newGrades = grades.filter { grade -> !grade.isRead }.size - }, ViewType.HEADER)) + if (preferencesRepository.isGradeExpandable) emptyList() else subItems + val gradeDetailsItems = listOf( + GradeDetailsItem( + GradeDetailsHeader( + subject = subject, + average = average, + pointsSum = points, + grades = subItems + ).apply { + newGrades = grades.filter { grade -> !grade.isRead }.size + }, ViewType.HEADER + ) + ) + + if (preferencesRepository.gradeExpandMode == GradeExpandMode.ALWAYS_EXPANDED) { + gradeDetailsItems + subItems + } else { + gradeDetailsItems + } }.flatten() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsView.kt index e71fcc3c..55633229 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsView.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.modules.grade.details import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode import io.github.wulkanowy.ui.base.BaseView interface GradeDetailsView : BaseView { @@ -9,7 +10,7 @@ interface GradeDetailsView : BaseView { fun initView() - fun updateData(data: List, isGradeExpandable: Boolean, gradeColorTheme: String) + fun updateData(data: List, expandMode: GradeExpandMode, gradeColorTheme: String) fun updateItem(item: Grade, position: Int) diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index df84d37d..7fb3d5c0 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -4,7 +4,7 @@ true only_one_semester false - false + one false light vulcan diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index 1aba7d85..fef062dd 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -5,7 +5,8 @@ app_theme dashboard_tiles grade_color_scheme - expand_grade + expand_grade + expand_grade_mode grade_average_mode grade_average_always_calc grade_statistics_list diff --git a/app/src/main/res/values/preferences_values.xml b/app/src/main/res/values/preferences_values.xml index bd3e8b47..1d777bdb 100644 --- a/app/src/main/res/values/preferences_values.xml +++ b/app/src/main/res/values/preferences_values.xml @@ -99,6 +99,17 @@ grade_color + + Up to 1 at once + Always expanded + Unlimited expansions + + + one + always + any + + Average of grades only from selected semester Average of averages from both semesters diff --git a/app/src/main/res/xml/scheme_preferences_appearance.xml b/app/src/main/res/xml/scheme_preferences_appearance.xml index b34fd417..b2da0287 100644 --- a/app/src/main/res/xml/scheme_preferences_appearance.xml +++ b/app/src/main/res/xml/scheme_preferences_appearance.xml @@ -50,11 +50,14 @@ app:iconSpaceReserved="false" app:key="@string/pref_key_grade_color_scheme" app:title="@string/pref_view_grade_color_scheme" /> - + app:key="@string/pref_key_expand_grade_mode" + app:title="@string/pref_view_expand_grade" + app:useSimpleSummaryProvider="true" /> Date: Tue, 26 Oct 2021 09:51:36 +0000 Subject: [PATCH 058/357] Bump about_libraries from 8.9.3 to 8.9.4 (#1604) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ad27aba0..6a563d2e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlin_version = '1.5.31' - about_libraries = '8.9.3' + about_libraries = '8.9.4' hilt_version = "2.39.1" } repositories { From 621db49fbf860bad00910f906226e68fe6f02f32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Oct 2021 09:52:55 +0000 Subject: [PATCH 059/357] Bump agcp from 1.6.1.200 to 1.6.1.300 (#1606) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6a563d2e..c345e4c7 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { classpath 'com.android.tools.build:gradle:7.0.3' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.10' - classpath 'com.huawei.agconnect:agcp:1.6.1.200' + classpath 'com.huawei.agconnect:agcp:1.6.1.300' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.3.0" From de11644e9b6b599c45d204dd30a777ab40bc47ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Oct 2021 09:53:16 +0000 Subject: [PATCH 060/357] Bump agconnect-crash from 1.6.1.200 to 1.6.1.300 (#1605) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 42177406..94dfc166 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -239,7 +239,7 @@ dependencies { playImplementation 'com.google.android.gms:play-services-ads:20.4.0' hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.301' - hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.200' + hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 1d910f8d661ba218ab674447528d764071fa7ae1 Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Wed, 27 Oct 2021 10:07:04 +0200 Subject: [PATCH 061/357] Fix excuse button showing up despite no lessons available to excuse (#1607) This was happening when there was an unexcused lesson that you excused until the teacher sent a response (accepted or denied it) --- .../wulkanowy/ui/modules/attendance/AttendanceAdapter.kt | 2 +- .../wulkanowy/ui/modules/attendance/AttendancePresenter.kt | 5 ++--- .../java/io/github/wulkanowy/utils/AttendanceExtension.kt | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt index 6cee2396..bb4f7022 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt @@ -46,7 +46,7 @@ class AttendanceAdapter @Inject constructor() : onExcuseCheckboxSelect(item, checked) } - when (if (item.excuseStatus != null) SentExcuseStatus.valueOf(item.excuseStatus) else null) { + when (item.excuseStatus?.let { SentExcuseStatus.valueOf(it)}) { SentExcuseStatus.WAITING -> { attendanceItemExcuseInfo.setImageResource(R.drawable.ic_excuse_waiting) attendanceItemExcuseInfo.visibility = View.VISIBLE 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 03545b25..5ed14f67 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 @@ -259,9 +259,8 @@ class AttendancePresenter @Inject constructor( showEmpty(filteredAttendance.isEmpty()) showErrorView(false) showContent(filteredAttendance.isNotEmpty()) - showExcuseButton(filteredAttendance.any { item -> - (!isParent && isVulcanExcusedFunctionEnabled) || (isParent && item.isExcusableOrNotExcused) - }) + val anyExcusables = filteredAttendance.any { it.isExcusableOrNotExcused } + showExcuseButton(anyExcusables && (isParent || isVulcanExcusedFunctionEnabled)) } analytics.logEvent( "load_data", diff --git a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt index 479cc518..b89ad57d 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt @@ -17,7 +17,7 @@ private inline val AttendanceSummary.allAbsences: Double get() = absence.toDouble() + absenceExcused inline val Attendance.isExcusableOrNotExcused: Boolean - get() = excusable || ((absence || lateness) && !excused) + get() = (excusable || ((absence || lateness) && !excused)) && excuseStatus == null fun AttendanceSummary.calculatePercentage() = calculatePercentage(allPresences, allAbsences) From 26c749c219b8d35e302fd12cd82641015d1dba9a Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Fri, 29 Oct 2021 20:30:27 +0200 Subject: [PATCH 062/357] Fix about header layout to support long app names (for DEV builds) (#1602) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mikołaj Pich --- app/build.gradle | 2 +- app/src/main/res/layout/scrollable_header_about.xml | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 94dfc166..1d541195 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -74,7 +74,7 @@ android { buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" } debug { - resValue "string", "app_name", "Wulkanowy DEV " + defaultConfig.versionCode + resValue "string", "app_name", "Wulkanowy DEV" applicationIdSuffix ".dev" versionNameSuffix "-dev" ext.enableCrashlytics = project.hasProperty("enableFirebase") diff --git a/app/src/main/res/layout/scrollable_header_about.xml b/app/src/main/res/layout/scrollable_header_about.xml index e203d98d..5a7669fd 100644 --- a/app/src/main/res/layout/scrollable_header_about.xml +++ b/app/src/main/res/layout/scrollable_header_about.xml @@ -6,6 +6,7 @@ android:layout_height="wrap_content" android:minHeight="104dp" android:orientation="vertical" + android:paddingHorizontal="20dp" tools:context=".ui.modules.about.AboutAdapter"> + app:layout_constraintTop_toTopOf="@id/aboutScrollableHeaderIcon" + app:layout_constraintWidth_max="wrap" /> From 8ed8b5a33cdf10431ecb582f319e59c9f7d4ab66 Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Sun, 31 Oct 2021 20:28:01 +0100 Subject: [PATCH 063/357] Error messages content wrap in error dialog (#1577) --- .../io/github/wulkanowy/ui/base/ErrorDialog.kt | 17 ++++++++--------- app/src/main/res/layout/dialog_error.xml | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt index 4ce97770..4c279d81 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt @@ -11,6 +11,7 @@ import android.widget.Toast import android.widget.Toast.LENGTH_LONG import androidx.appcompat.app.AlertDialog import androidx.core.content.getSystemService +import androidx.core.view.isGone import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.databinding.DialogErrorBinding @@ -24,8 +25,6 @@ import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openInternetBrowser import okhttp3.internal.http2.StreamResetException import java.io.InterruptedIOException -import java.io.PrintWriter -import java.io.StringWriter import java.net.ConnectException import java.net.SocketTimeoutException import java.net.UnknownHostException @@ -64,26 +63,26 @@ class ErrorDialog : BaseDialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val stringWriter = StringWriter().apply { - error.printStackTrace(PrintWriter(this)) - } + val errorStacktrace = error.stackTraceToString() with(binding) { - errorDialogContent.text = stringWriter.toString() + errorDialogContent.text = errorStacktrace.replace(": ${error.localizedMessage}", "") with(errorDialogHorizontalScroll) { post { fullScroll(HorizontalScrollView.FOCUS_LEFT) } } errorDialogCopy.setOnClickListener { - val clip = ClipData.newPlainText("wulkanowy", stringWriter.toString()) + val clip = ClipData.newPlainText("Error details", errorStacktrace) activity?.getSystemService()?.setPrimaryClip(clip) Toast.makeText(context, R.string.all_copied, LENGTH_LONG).show() } errorDialogCancel.setOnClickListener { dismiss() } errorDialogReport.setOnClickListener { - openConfirmDialog { openEmailClient(stringWriter.toString()) } + openConfirmDialog { openEmailClient(errorStacktrace) } } - errorDialogMessage.text = resources.getString(error) + errorDialogHumanizedMessage.text = resources.getString(error) + errorDialogErrorMessage.text = error.localizedMessage + errorDialogErrorMessage.isGone = error.localizedMessage.isNullOrBlank() errorDialogReport.isEnabled = when (error) { is UnknownHostException, is InterruptedIOException, diff --git a/app/src/main/res/layout/dialog_error.xml b/app/src/main/res/layout/dialog_error.xml index a78790bc..75abcf0f 100644 --- a/app/src/main/res/layout/dialog_error.xml +++ b/app/src/main/res/layout/dialog_error.xml @@ -4,7 +4,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:minWidth="300dp" - android:orientation="vertical"> + android:orientation="vertical" + tools:context=".ui.base.ErrorDialog"> + + From 36a570eeb0120a8211e62a6ab9a39a04bfbc6e49 Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Mon, 1 Nov 2021 01:46:23 +0100 Subject: [PATCH 064/357] Allow selecting text in error dialog (#1615) --- app/src/main/res/layout/dialog_error.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/dialog_error.xml b/app/src/main/res/layout/dialog_error.xml index 75abcf0f..b52c99ac 100644 --- a/app/src/main/res/layout/dialog_error.xml +++ b/app/src/main/res/layout/dialog_error.xml @@ -32,6 +32,7 @@ android:layout_marginBottom="5dp" android:paddingHorizontal="20dp" android:paddingTop="10dp" + android:textIsSelectable="true" android:textColor="?android:textColorSecondary" tools:text="@tools:sample/lorem" /> From a62ed54d07728c4949464e7619c3eddf83b0e398 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 16:54:10 +0000 Subject: [PATCH 065/357] Bump hianalytics from 6.3.0.301 to 6.3.0.302 (#1621) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 1d541195..b5eb455c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -238,7 +238,7 @@ dependencies { playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.gms:play-services-ads:20.4.0' - hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.301' + hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.302' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 1a3d58011650afefd16e73633457120a4a293287 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 16:54:43 +0000 Subject: [PATCH 066/357] Bump firebase-bom from 28.4.2 to 29.0.0 (#1619) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index b5eb455c..53814dae 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -230,7 +230,7 @@ dependencies { implementation 'me.xdrop:fuzzywuzzy:1.3.1' implementation 'com.fredporciuncula:flow-preferences:1.5.0' - playImplementation platform('com.google.firebase:firebase-bom:28.4.2') + playImplementation platform('com.google.firebase:firebase-bom:29.0.0') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From 8be605629a4ace0411b5598eb4f098f650771d72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 16:56:18 +0000 Subject: [PATCH 067/357] Bump firebase-crashlytics-gradle from 2.7.1 to 2.8.0 (#1623) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c345e4c7..575293aa 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.10' classpath 'com.huawei.agconnect:agcp:1.6.1.300' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.0' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.3.0" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3" From b7134221cb5d1d7b862c57512025685b93ea9b67 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 17:05:57 +0000 Subject: [PATCH 068/357] Bump hilt_version from 2.39.1 to 2.40 (#1617) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 575293aa..99c4820e 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { kotlin_version = '1.5.31' about_libraries = '8.9.4' - hilt_version = "2.39.1" + hilt_version = "2.40" } repositories { mavenCentral() From eea20ced57e2d06b79cc04c4a24711866b084c4a Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Thu, 4 Nov 2021 03:06:54 +0100 Subject: [PATCH 069/357] Notifications settings reorganize and strings update (#1616) --- app/src/main/res/values/strings.xml | 7 ++++--- .../res/xml/scheme_preferences_notifications.xml | 16 ++++++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13030366..1e88ee1a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -611,13 +611,13 @@ - App appearance & behavior + App Default view Calculated average options Force average calculation by app Show presence Theme - Expand grades + Grades expanding Mark current lesson Show groups next to subjects Show chart list in class grades @@ -627,6 +627,7 @@ Language Notifications + Other Show notifications Show upcoming lesson notifications Make upcoming lesson notification persistent @@ -689,7 +690,7 @@ Automatic update, synchronization interval Plus and minus values, average calculation Advanced - App version, contributors, social portals, licenses + App version, contributors, social portals Displaying advertisements, project support diff --git a/app/src/main/res/xml/scheme_preferences_notifications.xml b/app/src/main/res/xml/scheme_preferences_notifications.xml index 78e91cf0..442581bf 100644 --- a/app/src/main/res/xml/scheme_preferences_notifications.xml +++ b/app/src/main/res/xml/scheme_preferences_notifications.xml @@ -22,18 +22,22 @@ app:singleLineTitle="false" app:summary="@string/pref_notify_upcoming_lessons_persistent_summary" app:title="@string/pref_notify_upcoming_lessons_persistent_switch" /> - + + + Date: Thu, 4 Nov 2021 03:09:47 +0100 Subject: [PATCH 070/357] Add support for user ca in debug flavor(#1624) --- app/src/main/AndroidManifest.xml | 1 + app/src/main/res/xml/network_security_config.xml | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 app/src/main/res/xml/network_security_config.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 810d469f..e43a0ec0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,6 +38,7 @@ android:allowBackup="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="false" android:theme="@style/WulkanowyTheme" diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 00000000..84ff05a0 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,9 @@ + + + + + + + + + From 4401df6203bb408e1aa8cb63cf0976fe4a6f2d01 Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Sat, 6 Nov 2021 19:07:26 +0100 Subject: [PATCH 071/357] Migrate from ViewPager to ViewPager2 (#1601) --- .../ui/base/BaseFragmentPagerAdapter.kt | 37 ++++++++-------- .../ui/modules/grade/GradeFragment.kt | 42 ++++++++++++------ .../ui/modules/grade/GradePresenter.kt | 1 - .../ui/modules/login/LoginActivity.kt | 42 +++++++++++------- .../ui/modules/message/MessageFragment.kt | 40 ++++++++++++----- .../ui/modules/message/MessagePresenter.kt | 1 - .../SchoolAndTeachersFragment.kt | 44 ++++++++++++++----- .../SchoolAndTeachersPresenter.kt | 1 - .../ui/widgets/SwipeDisabledViewPager.kt | 19 -------- .../wulkanowy/utils/ViewPagerExtension.kt | 8 ++-- app/src/main/res/layout/activity_login.xml | 2 +- app/src/main/res/layout/fragment_grade.xml | 2 +- app/src/main/res/layout/fragment_message.xml | 2 +- .../res/layout/fragment_schoolandteachers.xml | 2 +- 14 files changed, 142 insertions(+), 101 deletions(-) delete mode 100644 app/src/main/java/io/github/wulkanowy/ui/widgets/SwipeDisabledViewPager.kt diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragmentPagerAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragmentPagerAdapter.kt index bd735535..6bca87f1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragmentPagerAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragmentPagerAdapter.kt @@ -2,32 +2,33 @@ package io.github.wulkanowy.ui.base import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentPagerAdapter +import androidx.lifecycle.Lifecycle +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayoutMediator -//TODO Use ViewPager2 -class BaseFragmentPagerAdapter(private val fragmentManager: FragmentManager) : - FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { +class BaseFragmentPagerAdapter( + private val fragmentManager: FragmentManager, + private val pagesCount: Int, + lifecycle: Lifecycle, +) : FragmentStateAdapter(fragmentManager, lifecycle), TabLayoutMediator.TabConfigurationStrategy { - private val pages = mutableMapOf() + lateinit var itemFactory: (position: Int) -> Fragment + + var titleFactory: (position: Int) -> String? = { "" } var containerId = 0 fun getFragmentInstance(position: Int): Fragment? { require(containerId != 0) { "Container id is 0" } - return fragmentManager.findFragmentByTag("android:switcher:$containerId:$position") + return fragmentManager.findFragmentByTag("f$position") } - fun addFragments(fragments: List) { - fragments.forEach { pages[it] = null } + override fun createFragment(position: Int): Fragment = itemFactory(position) + + override fun getItemCount() = pagesCount + + override fun onConfigureTab(tab: TabLayout.Tab, position: Int) { + tab.text = titleFactory(position) } - - fun addFragmentsWithTitle(pages: Map) { - this.pages.putAll(pages) - } - - override fun getItem(position: Int) = pages.keys.elementAt(position) - - override fun getCount() = pages.size - - override fun getPageTitle(position: Int) = pages.values.elementAt(position) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt index b3ef3037..f9ecb681 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt @@ -8,6 +8,7 @@ import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE import androidx.appcompat.app.AlertDialog +import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Semester @@ -29,7 +30,13 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade @Inject lateinit var presenter: GradePresenter - private val pagerAdapter by lazy { BaseFragmentPagerAdapter(childFragmentManager) } + private val pagerAdapter by lazy { + BaseFragmentPagerAdapter( + fragmentManager = childFragmentManager, + pagesCount = 3, + lifecycle = lifecycle, + ) + } private var semesterSwitchMenu: MenuItem? = null @@ -62,25 +69,34 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade } override fun initView() { - with(pagerAdapter) { - containerId = binding.gradeViewPager.id - addFragmentsWithTitle( - mapOf( - GradeDetailsFragment.newInstance() to getString(R.string.all_details), - GradeSummaryFragment.newInstance() to getString(R.string.grade_menu_summary), - GradeStatisticsFragment.newInstance() to getString(R.string.grade_menu_statistics) - ) - ) - } - with(binding.gradeViewPager) { adapter = pagerAdapter offscreenPageLimit = 3 setOnSelectPageListener(presenter::onPageSelected) } + with(pagerAdapter) { + containerId = binding.gradeViewPager.id + titleFactory = { + when (it) { + 0 -> getString(R.string.all_details) + 1 -> getString(R.string.grade_menu_summary) + 2 -> getString(R.string.grade_menu_statistics) + else -> throw IllegalStateException() + } + } + itemFactory = { + when (it) { + 0 -> GradeDetailsFragment.newInstance() + 1 -> GradeSummaryFragment.newInstance() + 2 -> GradeStatisticsFragment.newInstance() + else -> throw IllegalStateException() + } + } + TabLayoutMediator(binding.gradeTabLayout, binding.gradeViewPager, this).attach() + } + with(binding.gradeTabLayout) { - setupWithViewPager(binding.gradeViewPager) setElevationCompat(context.dpToPx(4f)) } 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 504c730d..76e88bcd 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 @@ -101,7 +101,6 @@ class GradePresenter @Inject constructor( private fun loadData() { flowWithResource { val student = studentRepository.getCurrentStudent() - delay(200) semesterRepository.getSemesters(student, refreshOnNoCurrent = true) }.onEach { when (it.status) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt index 10f6c073..f7bab526 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt @@ -24,7 +24,13 @@ class LoginActivity : BaseActivity(), Logi @Inject override lateinit var presenter: LoginPresenter - private val loginAdapter = BaseFragmentPagerAdapter(supportFragmentManager) + private val pagerAdapter by lazy { + BaseFragmentPagerAdapter( + fragmentManager = supportFragmentManager, + pagesCount = 5, + lifecycle = lifecycle, + ) + } @Inject lateinit var updateHelper: UpdateHelper @@ -65,24 +71,26 @@ class LoginActivity : BaseActivity(), Logi setDisplayShowTitleEnabled(false) } - with(loginAdapter) { - containerId = binding.loginViewpager.id - addFragments( - listOf( - LoginFormFragment.newInstance(), - LoginSymbolFragment.newInstance(), - LoginStudentSelectFragment.newInstance(), - LoginAdvancedFragment.newInstance(), - LoginRecoverFragment.newInstance() - ) - ) - } - with(binding.loginViewpager) { offscreenPageLimit = 2 - adapter = loginAdapter + adapter = pagerAdapter + isUserInputEnabled = false setOnSelectPageListener(presenter::onViewSelected) } + + with(pagerAdapter) { + containerId = binding.loginViewpager.id + itemFactory = { + when (it) { + 0 -> LoginFormFragment.newInstance() + 1 -> LoginSymbolFragment.newInstance() + 2 -> LoginStudentSelectFragment.newInstance() + 3 -> LoginAdvancedFragment.newInstance() + 4 -> LoginRecoverFragment.newInstance() + else -> throw IllegalStateException() + } + } + } } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -103,12 +111,12 @@ class LoginActivity : BaseActivity(), Logi } override fun notifyInitSymbolFragment(loginData: Triple) { - (loginAdapter.getFragmentInstance(1) as? LoginSymbolFragment) + (pagerAdapter.getFragmentInstance(1) as? LoginSymbolFragment) ?.onParentInitSymbolFragment(loginData) } override fun notifyInitStudentSelectFragment(studentsWithSemesters: List) { - (loginAdapter.getFragmentInstance(2) as? LoginStudentSelectFragment) + (pagerAdapter.getFragmentInstance(2) as? LoginStudentSelectFragment) ?.onParentInitStudentSelectFragment(studentsWithSemesters) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt index 72fc627f..51076c64 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE +import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED @@ -26,7 +27,13 @@ class MessageFragment : BaseFragment(R.layout.fragment_m @Inject lateinit var presenter: MessagePresenter - private val pagerAdapter by lazy { BaseFragmentPagerAdapter(childFragmentManager) } + private val pagerAdapter by lazy { + BaseFragmentPagerAdapter( + fragmentManager = childFragmentManager, + pagesCount = 3, + lifecycle = lifecycle, + ) + } companion object { fun newInstance() = MessageFragment() @@ -43,23 +50,34 @@ class MessageFragment : BaseFragment(R.layout.fragment_m } override fun initView() { - with(pagerAdapter) { - containerId = binding.messageViewPager.id - addFragmentsWithTitle(mapOf( - MessageTabFragment.newInstance(RECEIVED) to getString(R.string.message_inbox), - MessageTabFragment.newInstance(SENT) to getString(R.string.message_sent), - MessageTabFragment.newInstance(TRASHED) to getString(R.string.message_trash) - )) - } - with(binding.messageViewPager) { adapter = pagerAdapter offscreenPageLimit = 2 setOnSelectPageListener(presenter::onPageSelected) } + with(pagerAdapter) { + containerId = binding.messageViewPager.id + titleFactory = { + when (it) { + 0 -> getString(R.string.message_inbox) + 1 -> getString(R.string.message_sent) + 2 -> getString(R.string.message_trash) + else -> throw IllegalStateException() + } + } + itemFactory = { + when (it) { + 0 -> MessageTabFragment.newInstance(RECEIVED) + 1 -> MessageTabFragment.newInstance(SENT) + 2 -> MessageTabFragment.newInstance(TRASHED) + else -> throw IllegalStateException() + } + } + TabLayoutMediator(binding.messageTabLayout, binding.messageViewPager, this).attach() + } + with(binding.messageTabLayout) { - setupWithViewPager(binding.messageViewPager) setElevationCompat(context.dpToPx(4f)) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt index be694052..9e19517b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt @@ -16,7 +16,6 @@ class MessagePresenter @Inject constructor( override fun onAttachView(view: MessageView) { super.onAttachView(view) presenterScope.launch { - delay(150) view.initView() Timber.i("Message view was initialized") loadData() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt index c1c56961..9892b77a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE +import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.databinding.FragmentSchoolandteachersBinding @@ -24,7 +25,13 @@ class SchoolAndTeachersFragment : @Inject lateinit var presenter: SchoolAndTeachersPresenter - private val pagerAdapter by lazy { BaseFragmentPagerAdapter(childFragmentManager) } + private val pagerAdapter by lazy { + BaseFragmentPagerAdapter( + fragmentManager = childFragmentManager, + pagesCount = 2, + lifecycle = lifecycle, + ) + } companion object { fun newInstance() = SchoolAndTeachersFragment() @@ -41,22 +48,36 @@ class SchoolAndTeachersFragment : } override fun initView() { - with(pagerAdapter) { - containerId = binding.schoolandteachersViewPager.id - addFragmentsWithTitle(mapOf( - SchoolFragment.newInstance() to getString(R.string.school_title), - TeacherFragment.newInstance() to getString(R.string.teachers_title) - )) - } - with(binding.schoolandteachersViewPager) { adapter = pagerAdapter offscreenPageLimit = 2 setOnSelectPageListener(presenter::onPageSelected) } + with(pagerAdapter) { + containerId = binding.schoolandteachersViewPager.id + titleFactory = { + when (it) { + 0 -> getString(R.string.school_title) + 1 -> getString(R.string.teachers_title) + else -> throw IllegalStateException() + } + } + itemFactory = { + when (it) { + 0 -> SchoolFragment.newInstance() + 1 -> TeacherFragment.newInstance() + else -> throw IllegalStateException() + } + } + TabLayoutMediator( + binding.schoolandteachersTabLayout, + binding.schoolandteachersViewPager, + this + ).attach() + } + with(binding.schoolandteachersTabLayout) { - setupWithViewPager(binding.schoolandteachersViewPager) setElevationCompat(context.dpToPx(4f)) } } @@ -77,7 +98,8 @@ class SchoolAndTeachersFragment : } override fun notifyChildLoadData(index: Int, forceRefresh: Boolean) { - (pagerAdapter.getFragmentInstance(index) as? SchoolAndTeachersChildView)?.onParentLoadData(forceRefresh) + (pagerAdapter.getFragmentInstance(index) as? SchoolAndTeachersChildView) + ?.onParentLoadData(forceRefresh) } override fun onDestroyView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt index c14272b9..43823d6b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt @@ -16,7 +16,6 @@ class SchoolAndTeachersPresenter @Inject constructor( override fun onAttachView(view: SchoolAndTeachersView) { super.onAttachView(view) presenterScope.launch { - delay(150) view.initView() Timber.i("Message view was initialized") loadData() diff --git a/app/src/main/java/io/github/wulkanowy/ui/widgets/SwipeDisabledViewPager.kt b/app/src/main/java/io/github/wulkanowy/ui/widgets/SwipeDisabledViewPager.kt deleted file mode 100644 index eb5cae4f..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/widgets/SwipeDisabledViewPager.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.github.wulkanowy.ui.widgets - -import android.annotation.SuppressLint -import android.content.Context -import android.util.AttributeSet -import android.view.MotionEvent -import androidx.viewpager.widget.ViewPager - -class SwipeDisabledViewPager : ViewPager { - - constructor(context: Context) : super(context) - - constructor(context: Context, attr: AttributeSet) : super(context, attr) - - @SuppressLint("ClickableViewAccessibility") - override fun onTouchEvent(ev: MotionEvent) = false - - override fun onInterceptTouchEvent(ev: MotionEvent) = false -} diff --git a/app/src/main/java/io/github/wulkanowy/utils/ViewPagerExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ViewPagerExtension.kt index 6a5ad880..700ac2f1 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ViewPagerExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ViewPagerExtension.kt @@ -1,13 +1,11 @@ package io.github.wulkanowy.utils -import androidx.viewpager.widget.ViewPager +import androidx.viewpager2.widget.ViewPager2 -inline fun ViewPager.setOnSelectPageListener(crossinline selectListener: (position: Int) -> Unit) { - addOnPageChangeListener(object : ViewPager.OnPageChangeListener { +inline fun ViewPager2.setOnSelectPageListener(crossinline selectListener: (position: Int) -> Unit) { + registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { selectListener(position) } - override fun onPageScrollStateChanged(state: Int) {} - override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} }) } diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index e55ea8b9..1d5b5280 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -10,7 +10,7 @@ android:layout_height="wrap_content" android:background="@android:color/transparent" /> - diff --git a/app/src/main/res/layout/fragment_grade.xml b/app/src/main/res/layout/fragment_grade.xml index ed0447fb..989929d4 100644 --- a/app/src/main/res/layout/fragment_grade.xml +++ b/app/src/main/res/layout/fragment_grade.xml @@ -17,7 +17,7 @@ tools:ignore="UnusedAttribute" tools:visibility="visible" /> - - - Date: Sat, 6 Nov 2021 22:21:34 +0100 Subject: [PATCH 072/357] Add timetable changes, attendance notifications and refactor notification deeplinks (#1547) --- .../43.json | 2408 +++++++++++++++++ .../github/wulkanowy/data/db/AppDatabase.kt | 6 +- .../wulkanowy/data/db/dao/AttendanceDao.kt | 9 +- .../wulkanowy/data/db/entities/Attendance.kt | 3 + .../wulkanowy/data/db/entities/Timetable.kt | 3 + .../data/db/migrations/Migration43.kt | 12 + .../wulkanowy/data/pojos/NotificationData.kt | 43 +- .../data/repositories/AttendanceRepository.kt | 19 +- .../data/repositories/TimetableRepository.kt | 20 +- .../wulkanowy/services/ServicesModule.kt | 10 + .../alarm/TimetableNotificationReceiver.kt | 17 +- .../TimetableNotificationSchedulerHelper.kt | 4 +- .../services/shortcuts/ShortcutsHelper.kt | 90 + .../sync/channels/NewAttendanceChannel.kt | 36 + .../sync/channels/TimetableChangeChannel.kt | 36 + .../notifications/AppNotificationManager.kt | 220 +- .../ChangeTimetableNotification.kt | 125 + .../NewAttendanceNotification.kt | 55 + .../NewConferenceNotification.kt | 48 +- .../sync/notifications/NewExamNotification.kt | 48 +- .../notifications/NewGradeNotification.kt | 98 +- .../notifications/NewHomeworkNotification.kt | 46 +- .../NewLuckyNumberNotification.kt | 34 +- .../notifications/NewMessageNotification.kt | 38 +- .../sync/notifications/NewNoteNotification.kt | 48 +- .../NewSchoolAnnouncementNotification.kt | 57 +- .../sync/notifications/NotificationType.kt | 85 +- .../services/sync/works/AttendanceWork.kt | 30 +- .../services/sync/works/TimetableWork.kt | 31 +- .../wulkanowy/ui/modules/Destination.kt | 132 + .../modules/attendance/AttendanceAdapter.kt | 4 +- .../ui/modules/attendance/AttendanceDialog.kt | 4 +- .../NotificationDebugPresenter.kt | 12 + .../debug/notification/mock/attendance.kt | 35 + .../debug/notification/mock/timetable.kt | 39 + .../LoginStudentSelectFragment.kt | 9 +- .../LuckyNumberWidgetProvider.kt | 59 +- .../wulkanowy/ui/modules/main/MainActivity.kt | 191 +- .../ui/modules/main/MainPresenter.kt | 61 +- .../wulkanowy/ui/modules/main/MainView.kt | 32 +- .../NotificationsCenterAdapter.kt | 19 +- .../NotificationsCenterFragment.kt | 6 +- .../ui/modules/settings/SettingsFragment.kt | 14 +- .../ui/modules/settings/SettingsView.kt | 5 + .../TimetableWidgetProvider.kt | 8 +- .../wulkanowy/utils/AttendanceExtension.kt | 2 +- .../wulkanowy/utils/ContextExtension.kt | 4 + .../utils/FragNavControlerExtension.kt | 13 +- .../wulkanowy/utils/FragmentExtension.kt | 44 - app/src/main/res/values/strings.xml | 34 + .../ui/modules/main/MainPresenterTest.kt | 14 +- 51 files changed, 3819 insertions(+), 601 deletions(-) create mode 100644 app/schemas/io.github.wulkanowy.data.db.AppDatabase/43.json create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration43.kt create mode 100644 app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt create mode 100644 app/src/main/java/io/github/wulkanowy/services/sync/channels/NewAttendanceChannel.kt create mode 100644 app/src/main/java/io/github/wulkanowy/services/sync/channels/TimetableChangeChannel.kt create mode 100644 app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt create mode 100644 app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/attendance.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/timetable.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt delete mode 100644 app/src/main/java/io/github/wulkanowy/utils/FragmentExtension.kt diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/43.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/43.json new file mode 100644 index 00000000..22c0d812 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/43.json @@ -0,0 +1,2408 @@ +{ + "formatVersion": 1, + "database": { + "version": 43, + "identityHash": "66946510bb620ae82686a5a1a31aba18", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "semester_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `recipient_name` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `removed` INTEGER NOT NULL, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `unread_by` INTEGER NOT NULL, `read_by` INTEGER NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipient", + "columnName": "recipient_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "removed", + "columnName": "removed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `one_drive_id` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`real_id`))", + "fields": [ + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneDriveId", + "columnName": "one_drive_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "real_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `is_added_by_user` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ReportingUnits", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `short` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `roles` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roles", + "columnName": "roles", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` TEXT NOT NULL, `name` TEXT NOT NULL, `real_name` TEXT NOT NULL, `login_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `role` INTEGER NOT NULL, `hash` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realName", + "columnName": "real_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AdminMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `version_name` INTEGER, `version_max` INTEGER, `target_register_host` TEXT, `target_flavor` TEXT, `destination_url` TEXT, `priority` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionMin", + "columnName": "version_name", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMax", + "columnName": "version_max", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "targetRegisterHost", + "columnName": "target_register_host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetFlavor", + "columnName": "target_flavor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "destinationUrl", + "columnName": "destination_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '66946510bb620ae82686a5a1a31aba18')" + ] + } +} \ No newline at end of file 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 d9685606..7e6d609f 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 @@ -102,6 +102,7 @@ import io.github.wulkanowy.data.db.migrations.Migration4 import io.github.wulkanowy.data.db.migrations.Migration40 import io.github.wulkanowy.data.db.migrations.Migration41 import io.github.wulkanowy.data.db.migrations.Migration42 +import io.github.wulkanowy.data.db.migrations.Migration43 import io.github.wulkanowy.data.db.migrations.Migration5 import io.github.wulkanowy.data.db.migrations.Migration6 import io.github.wulkanowy.data.db.migrations.Migration7 @@ -151,7 +152,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 42 + const val VERSION_SCHEMA = 43 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), @@ -194,7 +195,8 @@ abstract class AppDatabase : RoomDatabase() { Migration39(), Migration40(), Migration41(sharedPrefProvider), - Migration42() + Migration42(), + Migration43() ) fun newInstance( diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/AttendanceDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/AttendanceDao.kt index 8ef3fd44..c6c255a1 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/AttendanceDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/AttendanceDao.kt @@ -11,6 +11,11 @@ import javax.inject.Singleton @Dao interface AttendanceDao : BaseDao { - @Query("SELECT * FROM Attendance WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end") - fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Flow> + @Query("SELECT * FROM Attendance WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :start AND date <= :end") + fun loadAll( + diaryId: Int, + studentId: Int, + start: LocalDate, + end: LocalDate + ): Flow> } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Attendance.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Attendance.kt index f141d5d5..b40dd52e 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Attendance.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Attendance.kt @@ -47,4 +47,7 @@ data class Attendance( @PrimaryKey(autoGenerate = true) var id: Long = 0 + + @ColumnInfo(name = "is_notified") + var isNotified: Boolean = true } 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 index 1bf159ef..29b3737b 100644 --- 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 @@ -50,4 +50,7 @@ data class Timetable( @PrimaryKey(autoGenerate = true) var id: Long = 0 + + @ColumnInfo(name = "is_notified") + var isNotified: Boolean = true } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration43.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration43.kt new file mode 100644 index 00000000..68c2834d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration43.kt @@ -0,0 +1,12 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration43 : Migration(42, 43) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE Timetable ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1") + database.execSQL("ALTER TABLE Attendance ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1") + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt index 0b4603ef..0748ba64 100644 --- a/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt @@ -1,36 +1,19 @@ package io.github.wulkanowy.data.pojos -import androidx.annotation.DrawableRes -import androidx.annotation.PluralsRes -import androidx.annotation.StringRes +import android.content.Intent import io.github.wulkanowy.services.sync.notifications.NotificationType -import io.github.wulkanowy.ui.modules.main.MainView -sealed interface NotificationData { +data class NotificationData( + val intentToStart: Intent, + val title: String, + val content: String +) + +data class GroupNotificationData( + val notificationDataList: List, + val title: String, + val content: String, + val intentToStart: Intent, val type: NotificationType - val startMenu: MainView.Section - val icon: Int - val titleStringRes: Int - val contentStringRes: Int -} +) -data class MultipleNotificationsData( - override val type: NotificationType, - override val startMenu: MainView.Section, - @DrawableRes override val icon: Int, - @PluralsRes override val titleStringRes: Int, - @PluralsRes override val contentStringRes: Int, - - @PluralsRes val summaryStringRes: Int, - val lines: List, -) : NotificationData - -data class OneNotificationData( - override val type: NotificationType, - override val startMenu: MainView.Section, - @DrawableRes override val icon: Int, - @StringRes override val titleStringRes: Int, - @StringRes override val contentStringRes: Int, - - val contentValues: List, -) : NotificationData 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 d21ffb5f..ec919817 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 @@ -14,6 +14,7 @@ import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex import java.time.LocalDate import java.time.LocalDateTime @@ -38,6 +39,7 @@ class AttendanceRepository @Inject constructor( start: LocalDate, end: LocalDate, forceRefresh: Boolean, + notify: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { @@ -56,13 +58,28 @@ class AttendanceRepository @Inject constructor( }, saveFetchResult = { old, new -> attendanceDb.deleteAll(old uniqueSubtract new) - attendanceDb.insertAll(new uniqueSubtract old) + val attendanceToAdd = (new uniqueSubtract old).map { newAttendance -> + newAttendance.apply { if (notify) isNotified = false } + } + attendanceDb.insertAll(attendanceToAdd) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) }, filterResult = { it.filter { item -> item.date in start..end } } ) + fun getAttendanceFromDatabase( + semester: Semester, + start: LocalDate, + end: LocalDate + ): Flow> { + return attendanceDb.loadAll(semester.diaryId, semester.studentId, start, end) + } + + suspend fun updateTimetable(timetable: List) { + return attendanceDb.updateAll(timetable) + } + suspend fun excuseForAbsence( student: Student, semester: Semester, absenceList: List, reason: String? = null 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 index 769fa0f0..8be62112 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt @@ -47,6 +47,7 @@ class TimetableRepository @Inject constructor( end: LocalDate, forceRefresh: Boolean, refreshAdditional: Boolean = false, + notify: Boolean = false ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { (timetable, additional, headers) -> @@ -67,7 +68,7 @@ class TimetableRepository @Inject constructor( timetableFull.mapToEntities(semester) }, saveFetchResult = { timetableOld, timetableNew -> - refreshTimetable(student, timetableOld.lessons, timetableNew.lessons) + refreshTimetable(student, timetableOld.lessons, timetableNew.lessons, notify) refreshAdditional(timetableOld.additional, timetableNew.additional) refreshDayHeaders(timetableOld.headers, timetableNew.headers) @@ -117,13 +118,28 @@ class TimetableRepository @Inject constructor( } } + fun getTimetableFromDatabase( + semester: Semester, + from: LocalDate, + end: LocalDate + ): Flow> { + return timetableDb.loadAll(semester.diaryId, semester.studentId, from, end) + } + + suspend fun updateTimetable(timetable: List) { + return timetableDb.updateAll(timetable) + } + private suspend fun refreshTimetable( student: Student, lessonsOld: List, lessonsNew: List, + notify: Boolean ) { val lessonsToRemove = lessonsOld uniqueSubtract lessonsNew - val lessonsToAdd = lessonsNew uniqueSubtract lessonsOld + val lessonsToAdd = (lessonsNew uniqueSubtract lessonsOld).map { new -> + new.apply { if (notify) isNotified = false } + } timetableDb.deleteAll(lessonsToRemove) timetableDb.insertAll(lessonsToAdd) diff --git a/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt b/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt index cdf0c26a..1729f100 100644 --- a/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt +++ b/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt @@ -15,6 +15,7 @@ import dagger.multibindings.IntoSet import io.github.wulkanowy.services.sync.channels.Channel import io.github.wulkanowy.services.sync.channels.DebugChannel import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel +import io.github.wulkanowy.services.sync.channels.NewAttendanceChannel import io.github.wulkanowy.services.sync.channels.NewConferencesChannel import io.github.wulkanowy.services.sync.channels.NewExamChannel import io.github.wulkanowy.services.sync.channels.NewGradesChannel @@ -23,6 +24,7 @@ import io.github.wulkanowy.services.sync.channels.NewMessagesChannel import io.github.wulkanowy.services.sync.channels.NewNotesChannel import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel import io.github.wulkanowy.services.sync.channels.PushChannel +import io.github.wulkanowy.services.sync.channels.TimetableChangeChannel import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel import io.github.wulkanowy.services.sync.works.AttendanceSummaryWork import io.github.wulkanowy.services.sync.works.AttendanceWork @@ -167,4 +169,12 @@ abstract class ServicesModule { @Binds @IntoSet abstract fun provideUpcomingLessonsChannel(channel: UpcomingLessonsChannel): Channel + + @Binds + @IntoSet + abstract fun provideChangeTimetableChannel(channel: TimetableChangeChannel): Channel + + @Binds + @IntoSet + abstract fun provideNewAttendanceChannel(channel: NewAttendanceChannel): Channel } diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt index 9ce96ef3..38ae7884 100644 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt @@ -15,8 +15,8 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.HiltBroadcastReceiver import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel.Companion.CHANNEL_ID +import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.toLocalDateTime @@ -41,7 +41,7 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { const val NOTIFICATION_TYPE_UPCOMING = 2 const val NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION = 3 - const val NOTIFICATION_ID = "id" + const val NOTIFICATION_ID = 2137 const val STUDENT_NAME = "student_name" const val STUDENT_ID = "student_id" @@ -71,11 +71,10 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { private fun prepareNotification(context: Context, intent: Intent) { val type = intent.getIntExtra(LESSON_TYPE, 0) - val notificationId = intent.getIntExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id) val isPersistent = preferencesRepository.isUpcomingLessonsNotificationsPersistent if (type == NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION) { - return NotificationManagerCompat.from(context).cancel(notificationId) + return NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID) } val studentId = intent.getIntExtra(STUDENT_ID, 0) @@ -92,7 +91,8 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { Timber.d("TimetableNotification receive: type: $type, subject: $subject, start: ${start.toLocalDateTime()}, student: $studentId") - showNotification(context, notificationId, isPersistent, studentName, + showNotification( + context, isPersistent, studentName, if (type == NOTIFICATION_TYPE_CURRENT) end else start, end - start, context.getString( if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next, @@ -109,7 +109,6 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { private fun showNotification( context: Context, - notificationId: Int, isPersistent: Boolean, studentName: String?, countDown: Long, @@ -118,7 +117,7 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { next: String? ) { NotificationManagerCompat.from(context) - .notify(notificationId, NotificationCompat.Builder(context, CHANNEL_ID) + .notify(NOTIFICATION_ID, NotificationCompat.Builder(context, CHANNEL_ID) .setContentTitle(title) .setContentText(next) .setAutoCancel(false) @@ -138,8 +137,8 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { .setContentIntent( PendingIntent.getActivity( context, - MainView.Section.TIMETABLE.id, - MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), + NOTIFICATION_ID, + MainActivity.getStartIntent(context, Destination.Timetable(), true), FLAG_UPDATE_CURRENT ) ) diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt index 2ec4e527..28a9b373 100644 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt @@ -25,7 +25,6 @@ import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companio import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.NOTIFICATION_TYPE_UPCOMING import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_ID import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_NAME -import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.toTimestamp @@ -79,7 +78,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor( } fun cancelNotification() = - NotificationManagerCompat.from(context).cancel(MainView.Section.TIMETABLE.id) + NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID) suspend fun scheduleNotifications(lessons: List, student: Student) { if (!preferencesRepository.isUpcomingLessonsNotificationsEnable) { @@ -156,7 +155,6 @@ class TimetableNotificationSchedulerHelper @Inject constructor( AlarmManagerCompat.setExactAndAllowWhileIdle( alarmManager, RTC_WAKEUP, time.toTimestamp(), PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also { - it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id) it.putExtra(LESSON_TYPE, notificationType) }, FLAG_UPDATE_CURRENT) ) diff --git a/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt b/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt new file mode 100644 index 00000000..d1c215f2 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt @@ -0,0 +1,90 @@ +package io.github.wulkanowy.services.shortcuts + +import android.content.Context +import android.content.Intent +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ShortcutsHelper @Inject constructor(@ApplicationContext private val context: Context) { + + private val destinations = mapOf( + "grade" to Destination.Grade, + "attendance" to Destination.Attendance, + "exam" to Destination.Exam, + "timetable" to Destination.Timetable() + ) + + init { + initializeShortcuts() + } + + fun getDestination(intent: Intent) = + destinations[intent.getStringExtra(EXTRA_SHORTCUT_DESTINATION_ID)] + + private fun initializeShortcuts() { + val shortcutsInfo = listOf( + ShortcutInfoCompat.Builder(context, "grade_shortcut") + .setShortLabel(context.getString(R.string.grade_title)) + .setLongLabel(context.getString(R.string.grade_title)) + .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_grade)) + .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .apply { + action = Intent.ACTION_VIEW + putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "grade") + } + ) + .build(), + + ShortcutInfoCompat.Builder(context, "attendance_shortcut") + .setShortLabel(context.getString(R.string.attendance_title)) + .setLongLabel(context.getString(R.string.attendance_title)) + .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_attendance)) + .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .apply { + action = Intent.ACTION_VIEW + putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "attendance") + } + ) + .build(), + + ShortcutInfoCompat.Builder(context, "exam_shortcut") + .setShortLabel(context.getString(R.string.exam_title)) + .setLongLabel(context.getString(R.string.exam_title)) + .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_exam)) + .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .apply { + action = Intent.ACTION_VIEW + putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "exam") + } + ) + .build(), + + ShortcutInfoCompat.Builder(context, "timetable_shortcut") + .setShortLabel(context.getString(R.string.timetable_title)) + .setLongLabel(context.getString(R.string.timetable_title)) + .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_timetable)) + .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .apply { + action = Intent.ACTION_VIEW + putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "timetable") + } + ) + .build() + ) + + shortcutsInfo.forEach { ShortcutManagerCompat.pushDynamicShortcut(context, it) } + } + + private companion object { + + private const val EXTRA_SHORTCUT_DESTINATION_ID = "shortcut_destination_id" + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/channels/NewAttendanceChannel.kt b/app/src/main/java/io/github/wulkanowy/services/sync/channels/NewAttendanceChannel.kt new file mode 100644 index 00000000..3110099e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/channels/NewAttendanceChannel.kt @@ -0,0 +1,36 @@ +package io.github.wulkanowy.services.sync.channels + +import android.annotation.TargetApi +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import androidx.core.app.NotificationManagerCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import javax.inject.Inject + +@TargetApi(26) +class NewAttendanceChannel @Inject constructor( + private val notificationManager: NotificationManagerCompat, + @ApplicationContext private val context: Context +) : Channel { + + companion object { + const val CHANNEL_ID = "new_attendance_channel" + } + + override fun create() { + notificationManager.createNotificationChannel( + NotificationChannel( + CHANNEL_ID, + context.getString(R.string.channel_new_attendance), + NotificationManager.IMPORTANCE_HIGH + ) + .apply { + enableLights(true) + enableVibration(true) + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + }) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/channels/TimetableChangeChannel.kt b/app/src/main/java/io/github/wulkanowy/services/sync/channels/TimetableChangeChannel.kt new file mode 100644 index 00000000..10dd3e00 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/channels/TimetableChangeChannel.kt @@ -0,0 +1,36 @@ +package io.github.wulkanowy.services.sync.channels + +import android.annotation.TargetApi +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import androidx.core.app.NotificationManagerCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import javax.inject.Inject + +@TargetApi(26) +class TimetableChangeChannel @Inject constructor( + private val notificationManager: NotificationManagerCompat, + @ApplicationContext private val context: Context +) : Channel { + + companion object { + const val CHANNEL_ID = "change_timetable_channel" + } + + override fun create() { + notificationManager.createNotificationChannel( + NotificationChannel( + CHANNEL_ID, + context.getString(R.string.channel_change_timetable), + NotificationManager.IMPORTANCE_HIGH + ) + .apply { + enableLights(true) + enableVibration(true) + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + }) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt index ddad9bf2..542b9346 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt @@ -4,19 +4,15 @@ import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Context import android.os.Build -import androidx.annotation.PluralsRes import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Notification import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData +import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData -import io.github.wulkanowy.data.pojos.OneNotificationData import io.github.wulkanowy.data.repositories.NotificationRepository -import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.getCompatBitmap import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.nickOrName @@ -27,102 +23,17 @@ import kotlin.random.Random class AppNotificationManager @Inject constructor( private val notificationManager: NotificationManagerCompat, @ApplicationContext private val context: Context, - private val appInfo: AppInfo, private val notificationRepository: NotificationRepository ) { - suspend fun sendNotification(notificationData: NotificationData, student: Student) = - when (notificationData) { - is OneNotificationData -> sendOneNotification(notificationData, student) - is MultipleNotificationsData -> sendMultipleNotifications(notificationData, student) - } - - private suspend fun sendOneNotification( - notificationData: OneNotificationData, - student: Student - ) { - val content = context.getString( - notificationData.contentStringRes, - *notificationData.contentValues.toTypedArray() - ) - - val title = context.getString(notificationData.titleStringRes) - - val notification = getDefaultNotificationBuilder(notificationData) - .setContentTitle(title) - .setContentText(content) - .setStyle( - NotificationCompat.BigTextStyle() - .setSummaryText(student.nickOrName) - .bigText(content) - ) - .build() - - notificationManager.notify(Random.nextInt(Int.MAX_VALUE), notification) - - saveNotification(title, content, notificationData, student) - } - - private suspend fun sendMultipleNotifications( - notificationData: MultipleNotificationsData, - student: Student - ) { - val groupType = notificationData.type.group ?: return - val group = "${groupType}_${student.id}" - - notificationData.sendSummaryNotification(group, student) - - notificationData.lines.forEach { item -> - val title = context.resources.getQuantityString(notificationData.titleStringRes, 1) - - val notification = getDefaultNotificationBuilder(notificationData) - .setContentTitle(title) - .setContentText(item) - .setStyle( - NotificationCompat.BigTextStyle() - .setSummaryText(student.nickOrName) - .bigText(item) - ) - .setGroup(group) - .build() - - notificationManager.notify(Random.nextInt(Int.MAX_VALUE), notification) - - saveNotification(title, item, notificationData, student) - } - } - - private fun MultipleNotificationsData.sendSummaryNotification(group: String, student: Student) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return - - val summaryNotification = getDefaultNotificationBuilder(this) - .setSmallIcon(icon) - .setContentTitle(getQuantityString(titleStringRes, lines.size)) - .setContentText(getQuantityString(contentStringRes, lines.size)) - .setStyle( - NotificationCompat.InboxStyle() - .setSummaryText(student.nickOrName) - .also { builder -> lines.forEach { builder.addLine(it) } } - ) - .setLocalOnly(true) - .setGroup(group) - .setGroupSummary(true) - .build() - - val groupId = student.id * 100 + type.ordinal - notificationManager.notify(groupId.toInt(), summaryNotification) - } - @SuppressLint("InlinedApi") - private fun getDefaultNotificationBuilder(notificationData: NotificationData): NotificationCompat.Builder { - val pendingIntentsFlags = if (appInfo.systemVersion >= Build.VERSION_CODES.M) { - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - } else { - PendingIntent.FLAG_UPDATE_CURRENT - } - - return NotificationCompat.Builder(context, notificationData.type.channel) - .setLargeIcon(context.getCompatBitmap(notificationData.icon, R.color.colorPrimary)) + suspend fun sendSingleNotification( + notificationData: NotificationData, + notificationType: NotificationType, + student: Student + ) { + val notification = NotificationCompat.Builder(context, notificationType.channel) + .setLargeIcon(context.getCompatBitmap(notificationType.icon, R.color.colorPrimary)) .setSmallIcon(R.drawable.ic_stat_all) .setAutoCancel(true) .setDefaults(NotificationCompat.DEFAULT_ALL) @@ -132,31 +43,122 @@ class AppNotificationManager @Inject constructor( .setContentIntent( PendingIntent.getActivity( context, - notificationData.startMenu.id, - MainActivity.getStartIntent(context, notificationData.startMenu, true), - pendingIntentsFlags + Random.nextInt(), + notificationData.intentToStart, + PendingIntent.FLAG_UPDATE_CURRENT ) ) + .setContentTitle(notificationData.title) + .setContentText(notificationData.content) + .setStyle( + NotificationCompat.BigTextStyle() + .setSummaryText(student.nickOrName) + .bigText(notificationData.content) + ) + .build() + + notificationManager.notify(Random.nextInt(), notification) + saveNotification(notificationData, notificationType, student) + } + + @SuppressLint("InlinedApi") + suspend fun sendMultipleNotifications( + groupNotificationData: GroupNotificationData, + student: Student + ) { + val notificationType = groupNotificationData.type + val groupType = notificationType.group ?: return + val group = "${groupType}_${student.id}" + + sendSummaryNotification(groupNotificationData, group, student) + + groupNotificationData.notificationDataList.forEach { notificationData -> + val notification = NotificationCompat.Builder(context, notificationType.channel) + .setLargeIcon(context.getCompatBitmap(notificationType.icon, R.color.colorPrimary)) + .setSmallIcon(R.drawable.ic_stat_all) + .setAutoCancel(true) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setColor(context.getCompatColor(R.color.colorPrimary)) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + .setContentIntent( + PendingIntent.getActivity( + context, + Random.nextInt(), + notificationData.intentToStart, + PendingIntent.FLAG_UPDATE_CURRENT + ) + ) + .setContentTitle(notificationData.title) + .setContentText(notificationData.content) + .setStyle( + NotificationCompat.BigTextStyle() + .setSummaryText(student.nickOrName) + .bigText(notificationData.content) + ) + .setGroup(group) + .build() + + notificationManager.notify(Random.nextInt(), notification) + saveNotification(notificationData, groupNotificationData.type, student) + } + } + + private fun sendSummaryNotification( + groupNotificationData: GroupNotificationData, + group: String, + student: Student + ) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return + + val summaryNotification = + NotificationCompat.Builder(context, groupNotificationData.type.channel) + .setContentTitle(groupNotificationData.title) + .setContentText(groupNotificationData.content) + .setSmallIcon(groupNotificationData.type.icon) + .setAutoCancel(true) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setColor(context.getCompatColor(R.color.colorPrimary)) + .setStyle( + NotificationCompat.InboxStyle() + .setSummaryText(student.nickOrName) + .also { builder -> + groupNotificationData.notificationDataList.forEach { + builder.addLine(it.content) + } + } + ) + .setContentIntent( + PendingIntent.getActivity( + context, + Random.nextInt(), + groupNotificationData.intentToStart, + PendingIntent.FLAG_UPDATE_CURRENT + ) + ) + .setLocalOnly(true) + .setGroup(group) + .setGroupSummary(true) + .build() + + val groupId = student.id * 100 + groupNotificationData.type.ordinal + notificationManager.notify(groupId.toInt(), summaryNotification) } private suspend fun saveNotification( - title: String, - content: String, notificationData: NotificationData, + notificationType: NotificationType, student: Student ) { val notificationEntity = Notification( studentId = student.id, - title = title, - content = content, - type = notificationData.type, + title = notificationData.title, + content = notificationData.content, + type = notificationType, date = LocalDateTime.now() ) notificationRepository.saveNotification(notificationEntity) } - - private fun getQuantityString(@PluralsRes res: Int, arg: Int): String { - return context.resources.getQuantityString(res, arg, arg) - } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt new file mode 100644 index 00000000..6d2d3a59 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt @@ -0,0 +1,125 @@ +package io.github.wulkanowy.services.sync.notifications + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural +import io.github.wulkanowy.utils.toFormattedString +import java.time.LocalDate +import java.time.LocalDateTime +import javax.inject.Inject + +class ChangeTimetableNotification @Inject constructor( + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context, +) { + + suspend fun notify(items: List, student: Student) { + val currentTime = LocalDateTime.now() + val changedLessons = items.filter { (it.canceled || it.changes) && it.start > currentTime } + val notificationDataList = changedLessons.groupBy { it.date } + .map { (date, lessons) -> + getNotificationContents(date, lessons).map { + NotificationData( + title = context.getPlural( + R.plurals.timetable_notify_new_items_title, + 1 + ), + content = it, + intentToStart = MainActivity.getStartIntent( + context = context, + destination = Destination.Timetable(date), + startNewTask = true + ) + ) + } + } + .flatten() + .ifEmpty { return } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural( + R.plurals.timetable_notify_new_items_title, + changedLessons.size + ), + content = context.getPlural( + R.plurals.timetable_notify_new_items_group, + changedLessons.size, + changedLessons.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Timetable(), true), + type = NotificationType.CHANGE_TIMETABLE + ) + + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) + } + + private fun getNotificationContents(date: LocalDate, lessons: List): List { + val formattedDate = date.toFormattedString("EEE dd.MM") + + return if (lessons.size > 2) { + listOf( + context.getPlural( + R.plurals.timetable_notify_new_items, + lessons.size, + formattedDate, + lessons.size, + ) + ) + } else { + lessons.map { + buildString { + append( + context.getString( + R.string.timetable_notify_lesson, + formattedDate, + it.number, + it.subject + ) + ) + if (it.roomOld.isNotBlank()) { + appendLine() + append( + context.getString( + R.string.timetable_notify_change_room, + it.roomOld, + it.room + ) + ) + } + if (it.teacherOld.isNotBlank() && it.teacher != it.teacherOld) { + appendLine() + append( + context.getString( + R.string.timetable_notify_change_teacher, + it.teacherOld, + it.teacher + ) + ) + } + if (it.subjectOld.isNotBlank()) { + appendLine() + append( + context.getString( + R.string.timetable_notify_change_subject, + it.subjectOld, + it.subject + ) + ) + } + if (it.info.isNotBlank()) { + appendLine() + append(it.info) + } + } + } + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt new file mode 100644 index 00000000..69555272 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt @@ -0,0 +1,55 @@ +package io.github.wulkanowy.services.sync.notifications + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Attendance +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.descriptionRes +import io.github.wulkanowy.utils.getPlural +import io.github.wulkanowy.utils.toFormattedString +import javax.inject.Inject + +class NewAttendanceNotification @Inject constructor( + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context +) { + + suspend fun notify(items: List, student: Student) { + val lines = items.filterNot { it.presence || it.name == "UNKNOWN" } + .map { + val description = context.getString(it.descriptionRes) + "${it.date.toFormattedString("dd.MM")} - ${it.subject}: $description" + } + .ifEmpty { return } + + val notificationDataList = lines.map { + NotificationData( + title = context.getPlural(R.plurals.attendance_notify_new_items_title, 1), + content = it, + intentToStart = MainActivity.getStartIntent(context, Destination.Attendance, true) + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural( + R.plurals.attendance_notify_new_items_title, + notificationDataList.size + ), + content = context.getPlural( + R.plurals.attendance_notify_new_items, + notificationDataList.size, + notificationDataList.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Attendance, true), + type = NotificationType.NEW_ATTENDANCE + ) + + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt index 994cb8d4..97b1332d 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt @@ -1,34 +1,52 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDateTime import javax.inject.Inject class NewConferenceNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { suspend fun notify(items: List, student: Student) { val today = LocalDateTime.now() - val lines = items.filter { !it.date.isBefore(today) }.map { - "${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}" - }.ifEmpty { return } + val lines = items.filter { !it.date.isBefore(today) } + .map { + "${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}" + } + .ifEmpty { return } - val notification = MultipleNotificationsData( - type = NotificationType.NEW_CONFERENCE, - icon = R.drawable.ic_more_conferences, - titleStringRes = R.plurals.conference_notify_new_item_title, - contentStringRes = R.plurals.conference_notify_new_items, - summaryStringRes = R.plurals.conference_number_item, - startMenu = MainView.Section.CONFERENCE, - lines = lines + val notificationDataList = lines.map { + NotificationData( + title = context.getPlural(R.plurals.conference_notify_new_item_title, 1), + content = it, + intentToStart = MainActivity.getStartIntent(context, Destination.Conference, true) + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.conference_notify_new_item_title, lines.size), + content = context.getPlural( + R.plurals.conference_notify_new_items, + lines.size, + lines.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Conference, true), + type = NotificationType.NEW_CONFERENCE ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt index f148fa34..6f8ed896 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt @@ -1,34 +1,52 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate import javax.inject.Inject class NewExamNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { suspend fun notify(items: List, student: Student) { val today = LocalDate.now() - val lines = items.filter { !it.date.isBefore(today) }.map { - "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}" - }.ifEmpty { return } + val lines = items.filter { !it.date.isBefore(today) } + .map { + "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}" + } + .ifEmpty { return } - val notification = MultipleNotificationsData( - type = NotificationType.NEW_EXAM, - icon = R.drawable.ic_main_exam, - titleStringRes = R.plurals.exam_notify_new_item_title, - contentStringRes = R.plurals.exam_notify_new_item_content, - summaryStringRes = R.plurals.exam_number_item, - startMenu = MainView.Section.EXAM, - lines = lines + val notificationDataList = lines.map { + NotificationData( + title = context.getPlural(R.plurals.exam_notify_new_item_title, 1), + content = it, + intentToStart = MainActivity.getStartIntent(context, Destination.Exam, true), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.exam_notify_new_item_title, lines.size), + content = context.getPlural( + R.plurals.exam_notify_new_item_content, + lines.size, + lines.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Exam, true), + type = NotificationType.NEW_EXAM ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt index 52bdff58..09692fb7 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt @@ -1,62 +1,88 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import javax.inject.Inject class NewGradeNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { suspend fun notifyDetails(items: List, student: Student) { - val notification = MultipleNotificationsData( - type = NotificationType.NEW_GRADE_DETAILS, - icon = R.drawable.ic_stat_grade, - titleStringRes = R.plurals.grade_new_items, - contentStringRes = R.plurals.grade_notify_new_items, - summaryStringRes = R.plurals.grade_number_item, - startMenu = MainView.Section.GRADE, - lines = items.map { - "${it.subject}: ${it.entry}" - } + val notificationDataList = items.map { + NotificationData( + title = context.getPlural(R.plurals.grade_new_items, 1), + content = "${it.subject}: ${it.entry}", + intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.grade_new_items, items.size), + content = context.getPlural(R.plurals.grade_notify_new_items, items.size, items.size), + intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + type = NotificationType.NEW_GRADE_DETAILS ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } suspend fun notifyPredicted(items: List, student: Student) { - val notification = MultipleNotificationsData( - type = NotificationType.NEW_GRADE_PREDICTED, - icon = R.drawable.ic_stat_grade, - titleStringRes = R.plurals.grade_new_items_predicted, - contentStringRes = R.plurals.grade_notify_new_items_predicted, - summaryStringRes = R.plurals.grade_number_item, - startMenu = MainView.Section.GRADE, - lines = items.map { - "${it.subject}: ${it.predictedGrade}" - } + val notificationDataList = items.map { + NotificationData( + title = context.getPlural(R.plurals.grade_new_items_predicted, 1), + content = "${it.subject}: ${it.predictedGrade}", + intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.grade_new_items_predicted, items.size), + content = context.getPlural( + R.plurals.grade_notify_new_items_predicted, + items.size, + items.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + type = NotificationType.NEW_GRADE_PREDICTED ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } suspend fun notifyFinal(items: List, student: Student) { - val notification = MultipleNotificationsData( - type = NotificationType.NEW_GRADE_FINAL, - icon = R.drawable.ic_stat_grade, - titleStringRes = R.plurals.grade_new_items_final, - contentStringRes = R.plurals.grade_notify_new_items_final, - summaryStringRes = R.plurals.grade_number_item, - startMenu = MainView.Section.GRADE, - lines = items.map { - "${it.subject}: ${it.finalGrade}" - } + val notificationDataList = items.map { + NotificationData( + title = context.getPlural(R.plurals.grade_new_items_final, 1), + content = "${it.subject}: ${it.finalGrade}", + intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.grade_new_items_final, items.size), + content = context.getPlural( + R.plurals.grade_notify_new_items_final, + items.size, + items.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + type = NotificationType.NEW_GRADE_FINAL ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt index 4c34cb8f..cdada844 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt @@ -1,34 +1,52 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate import javax.inject.Inject class NewHomeworkNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { suspend fun notify(items: List, student: Student) { val today = LocalDate.now() - val lines = items.filter { !it.date.isBefore(today) }.map { - "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}" - }.ifEmpty { return } + val lines = items.filter { !it.date.isBefore(today) } + .map { + "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}" + } + .ifEmpty { return } - val notification = MultipleNotificationsData( + val notificationDataList = lines.map { + NotificationData( + title = context.getPlural(R.plurals.homework_notify_new_item_title, 1), + content = it, + intentToStart = MainActivity.getStartIntent(context, Destination.Homework, true), + ) + } + + val groupNotificationData = GroupNotificationData( + title = context.getPlural(R.plurals.homework_notify_new_item_title, lines.size), + content = context.getPlural( + R.plurals.homework_notify_new_item_content, + lines.size, + lines.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Homework, true), type = NotificationType.NEW_HOMEWORK, - icon = R.drawable.ic_more_homework, - titleStringRes = R.plurals.homework_notify_new_item_title, - contentStringRes = R.plurals.homework_notify_new_item_content, - summaryStringRes = R.plurals.homework_number_item, - startMenu = MainView.Section.HOMEWORK, - lines = lines + notificationDataList = notificationDataList ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt index 08c98510..d9f138b5 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt @@ -1,26 +1,34 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.OneNotificationData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity import javax.inject.Inject class NewLuckyNumberNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { - suspend fun notify(item: LuckyNumber, student: Student) { - val notification = OneNotificationData( - type = NotificationType.NEW_LUCKY_NUMBER, - icon = R.drawable.ic_stat_luckynumber, - titleStringRes = R.string.lucky_number_notify_new_item_title, - contentStringRes = R.string.lucky_number_notify_new_item, - startMenu = MainView.Section.LUCKY_NUMBER, - contentValues = listOf(item.luckyNumber.toString()) - ) + suspend fun notify(item: LuckyNumber, student: Student) { + val notificationData = NotificationData( + title = context.getString(R.string.lucky_number_notify_new_item_title), + content = context.getString( + R.string.lucky_number_notify_new_item, + item.luckyNumber.toString() + ), + intentToStart = MainActivity.getStartIntent(context, Destination.LuckyNumber, true) + ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendSingleNotification( + notificationData = notificationData, + notificationType = NotificationType.NEW_LUCKY_NUMBER, + student = student + ) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt index a6d503aa..cdb5ab9c 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt @@ -1,29 +1,39 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import javax.inject.Inject class NewMessageNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { suspend fun notify(items: List, student: Student) { - val notification = MultipleNotificationsData( - type = NotificationType.NEW_MESSAGE, - icon = R.drawable.ic_stat_message, - titleStringRes = R.plurals.message_new_items, - contentStringRes = R.plurals.message_notify_new_items, - summaryStringRes = R.plurals.message_number_item, - startMenu = MainView.Section.MESSAGE, - lines = items.map { - "${it.sender}: ${it.subject}" - } + val notificationDataList = items.map { + NotificationData( + title = context.getPlural(R.plurals.message_new_items, 1), + content = "${it.sender}: ${it.subject}", + intentToStart = MainActivity.getStartIntent(context, Destination.Message, true), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.message_new_items, items.size), + content = context.getPlural(R.plurals.message_notify_new_items, items.size, items.size), + intentToStart = MainActivity.getStartIntent(context, Destination.Message, true), + type = NotificationType.NEW_MESSAGE ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt index ffa3cc9c..16be1ca5 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt @@ -1,42 +1,46 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import javax.inject.Inject class NewNoteNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { suspend fun notify(items: List, student: Student) { - val notification = MultipleNotificationsData( - type = NotificationType.NEW_NOTE, - icon = R.drawable.ic_stat_note, - titleStringRes = when (NoteCategory.getByValue(items.first().categoryType)) { + val notificationDataList = items.map { + val titleRes = when (NoteCategory.getByValue(it.categoryType)) { NoteCategory.POSITIVE -> R.plurals.praise_new_items NoteCategory.NEUTRAL -> R.plurals.neutral_note_new_items else -> R.plurals.note_new_items - }, - contentStringRes = when (NoteCategory.getByValue(items.first().categoryType)) { - NoteCategory.POSITIVE -> R.plurals.praise_notify_new_items - NoteCategory.NEUTRAL -> R.plurals.neutral_note_notify_new_items - else -> R.plurals.note_notify_new_items - }, - summaryStringRes = when (NoteCategory.getByValue(items.first().categoryType)) { - NoteCategory.POSITIVE -> R.plurals.praise_number_item - NoteCategory.NEUTRAL -> R.plurals.neutral_note_number_item - else -> R.plurals.note_number_item - }, - startMenu = MainView.Section.NOTE, - lines = items.map { - "${it.teacher}: ${it.category}" } + + NotificationData( + title = context.getPlural(titleRes, 1), + content = "${it.teacher}: ${it.category}", + intentToStart = MainActivity.getStartIntent(context, Destination.Note, true), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + intentToStart = MainActivity.getStartIntent(context, Destination.Note, true), + title = context.getPlural(R.plurals.note_new_items, items.size), + content = context.getPlural(R.plurals.note_notify_new_items, items.size, items.size), + type = NotificationType.NEW_NOTE ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt index 990a950b..1f603624 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt @@ -1,29 +1,56 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import javax.inject.Inject class NewSchoolAnnouncementNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { - suspend fun notify(items: List, student: Student) { - val notification = MultipleNotificationsData( - type = NotificationType.NEW_ANNOUNCEMENT, - icon = R.drawable.ic_all_about, - titleStringRes = R.plurals.school_announcement_notify_new_item_title, - contentStringRes = R.plurals.school_announcement_notify_new_items, - summaryStringRes = R.plurals.school_announcement_number_item, - startMenu = MainView.Section.SCHOOL_ANNOUNCEMENT, - lines = items.map { - "${it.subject}: ${it.content}" - } + suspend fun notify(items: List, student: Student) { + val notificationDataList = items.map { + NotificationData( + intentToStart = MainActivity.getStartIntent( + context = context, + destination = Destination.SchoolAnnouncement, + startNewTask = true + ), + title = context.getPlural( + R.plurals.school_announcement_notify_new_item_title, + 1 + ), + content = "${it.subject}: ${it.content}" + ) + } + val groupNotificationData = GroupNotificationData( + type = NotificationType.NEW_ANNOUNCEMENT, + intentToStart = MainActivity.getStartIntent( + context = context, + destination = Destination.SchoolAnnouncement, + startNewTask = true + ), + title = context.getPlural( + R.plurals.school_announcement_notify_new_item_title, + items.size + ), + content = context.getPlural( + R.plurals.school_announcement_notify_new_items, + items.size, + items.size + ), + notificationDataList = notificationDataList ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt index 49cbcfe9..af79fcd2 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt @@ -1,6 +1,8 @@ package io.github.wulkanowy.services.sync.notifications +import io.github.wulkanowy.R import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel +import io.github.wulkanowy.services.sync.channels.NewAttendanceChannel import io.github.wulkanowy.services.sync.channels.NewConferencesChannel import io.github.wulkanowy.services.sync.channels.NewExamChannel import io.github.wulkanowy.services.sync.channels.NewGradesChannel @@ -9,17 +11,76 @@ import io.github.wulkanowy.services.sync.channels.NewMessagesChannel import io.github.wulkanowy.services.sync.channels.NewNotesChannel import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel import io.github.wulkanowy.services.sync.channels.PushChannel +import io.github.wulkanowy.services.sync.channels.TimetableChangeChannel -enum class NotificationType(val group: String?, val channel: String) { - NEW_CONFERENCE("new_conferences_group", NewConferencesChannel.CHANNEL_ID), - NEW_EXAM("new_exam_group", NewExamChannel.CHANNEL_ID), - NEW_GRADE_DETAILS("new_grade_details_group", NewGradesChannel.CHANNEL_ID), - NEW_GRADE_PREDICTED("new_grade_predicted_group", NewGradesChannel.CHANNEL_ID), - NEW_GRADE_FINAL("new_grade_final_group", NewGradesChannel.CHANNEL_ID), - NEW_HOMEWORK("new_homework_group", NewHomeworkChannel.CHANNEL_ID), - NEW_LUCKY_NUMBER("lucky_number_group", LuckyNumberChannel.CHANNEL_ID), - NEW_MESSAGE("new_message_group", NewMessagesChannel.CHANNEL_ID), - NEW_NOTE("new_notes_group", NewNotesChannel.CHANNEL_ID), - NEW_ANNOUNCEMENT("new_school_announcements_group", NewSchoolAnnouncementsChannel.CHANNEL_ID), - PUSH(null, PushChannel.CHANNEL_ID) +enum class NotificationType( + val group: String?, + val channel: String, + val icon: Int +) { + NEW_CONFERENCE( + group = "new_conferences_group", + channel = NewConferencesChannel.CHANNEL_ID, + icon = R.drawable.ic_more_conferences, + ), + NEW_EXAM( + group = "new_exam_group", + channel = NewExamChannel.CHANNEL_ID, + icon = R.drawable.ic_main_exam + ), + NEW_GRADE_DETAILS( + group = "new_grade_details_group", + channel = NewGradesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_grade, + ), + NEW_GRADE_PREDICTED( + group = "new_grade_predicted_group", + channel = NewGradesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_grade, + ), + NEW_GRADE_FINAL( + group = "new_grade_final_group", + channel = NewGradesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_grade, + ), + NEW_HOMEWORK( + group = "new_homework_group", + channel = NewHomeworkChannel.CHANNEL_ID, + icon = R.drawable.ic_more_homework, + ), + NEW_LUCKY_NUMBER( + group = null, + channel = LuckyNumberChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_luckynumber, + ), + NEW_MESSAGE( + group = "new_message_group", + channel = NewMessagesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_message, + ), + NEW_NOTE( + group = "new_notes_group", + channel = NewNotesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_note + ), + NEW_ANNOUNCEMENT( + group = "new_school_announcements_group", + channel = NewSchoolAnnouncementsChannel.CHANNEL_ID, + icon = R.drawable.ic_all_about + ), + CHANGE_TIMETABLE( + group = "change_timetable_group", + channel = TimetableChangeChannel.CHANNEL_ID, + icon = R.drawable.ic_main_timetable + ), + NEW_ATTENDANCE( + group = "new_attendance_group", + channel = NewAttendanceChannel.CHANNEL_ID, + icon = R.drawable.ic_main_attendance + ), + PUSH( + group = null, + channel = PushChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_all + ) } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt index 4823b2b5..f7b680e3 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt @@ -3,18 +3,40 @@ package io.github.wulkanowy.services.sync.works import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.AttendanceRepository -import io.github.wulkanowy.utils.monday -import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.services.sync.notifications.NewAttendanceNotification +import io.github.wulkanowy.utils.previousOrSameSchoolDay import io.github.wulkanowy.utils.waitForResult +import kotlinx.coroutines.flow.first import java.time.LocalDate.now import javax.inject.Inject class AttendanceWork @Inject constructor( - private val attendanceRepository: AttendanceRepository + private val attendanceRepository: AttendanceRepository, + private val newAttendanceNotification: NewAttendanceNotification, + private val preferencesRepository: PreferencesRepository ) : Work { override suspend fun doWork(student: Student, semester: Semester) { - attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true) + attendanceRepository.getAttendance( + student = student, + semester = semester, + start = now().previousOrSameSchoolDay, + end = now().previousOrSameSchoolDay, + forceRefresh = true, + notify = preferencesRepository.isNotificationsEnable + ) .waitForResult() + + attendanceRepository.getAttendanceFromDatabase(semester, now().minusDays(7), now()) + .first() + .filterNot { it.isNotified } + .let { + if (it.isNotEmpty()) newAttendanceNotification.notify(it, student) + + attendanceRepository.updateTimetable(it.onEach { attendance -> + attendance.isNotified = true + }) + } } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/TimetableWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/TimetableWork.kt index 2df2c9dc..fcc33063 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/TimetableWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/TimetableWork.kt @@ -2,18 +2,41 @@ package io.github.wulkanowy.services.sync.works import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.TimetableRepository -import io.github.wulkanowy.utils.monday -import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification +import io.github.wulkanowy.utils.nextOrSameSchoolDay import io.github.wulkanowy.utils.waitForResult +import kotlinx.coroutines.flow.first import java.time.LocalDate.now import javax.inject.Inject class TimetableWork @Inject constructor( - private val timetableRepository: TimetableRepository + private val timetableRepository: TimetableRepository, + private val changeTimetableNotification: ChangeTimetableNotification, + private val preferencesRepository: PreferencesRepository ) : Work { override suspend fun doWork(student: Student, semester: Semester) { - timetableRepository.getTimetable(student, semester, now().monday, now().sunday, true).waitForResult() + timetableRepository.getTimetable( + student = student, + semester = semester, + start = now().nextOrSameSchoolDay, + end = now().nextOrSameSchoolDay, + forceRefresh = true, + notify = preferencesRepository.isNotificationsEnable + ) + .waitForResult() + + timetableRepository.getTimetableFromDatabase(semester, now(), now().plusDays(7)) + .first() + .filterNot { it.isNotified } + .let { + if (it.isNotEmpty()) changeTimetableNotification.notify(it, student) + + timetableRepository.updateTimetable(it.onEach { timetable -> + timetable.isNotified = true + }) + } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt new file mode 100644 index 00000000..0406afa4 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt @@ -0,0 +1,132 @@ +package io.github.wulkanowy.ui.modules + +import androidx.fragment.app.Fragment +import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment +import io.github.wulkanowy.ui.modules.conference.ConferenceFragment +import io.github.wulkanowy.ui.modules.dashboard.DashboardFragment +import io.github.wulkanowy.ui.modules.exam.ExamFragment +import io.github.wulkanowy.ui.modules.grade.GradeFragment +import io.github.wulkanowy.ui.modules.homework.HomeworkFragment +import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment +import io.github.wulkanowy.ui.modules.message.MessageFragment +import io.github.wulkanowy.ui.modules.more.MoreFragment +import io.github.wulkanowy.ui.modules.note.NoteFragment +import io.github.wulkanowy.ui.modules.schoolandteachers.school.SchoolFragment +import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment +import io.github.wulkanowy.ui.modules.timetable.TimetableFragment +import java.io.Serializable +import java.time.LocalDate + +sealed interface Destination : Serializable { + + val type: Type + + val fragment: Fragment + + enum class Type(val defaultDestination: Destination) { + DASHBOARD(Dashboard), + GRADE(Grade), + ATTENDANCE(Attendance), + EXAM(Exam), + TIMETABLE(Timetable()), + HOMEWORK(Homework), + NOTE(Note), + CONFERENCE(Conference), + SCHOOL_ANNOUNCEMENT(SchoolAnnouncement), + SCHOOL(School), + LUCKY_NUMBER(More), + MORE(More), + MESSAGE(Message); + } + + object Dashboard : Destination { + + override val type = Type.DASHBOARD + + override val fragment get() = DashboardFragment.newInstance() + } + + object Grade : Destination { + + override val type = Type.GRADE + + override val fragment get() = GradeFragment.newInstance() + } + + object Attendance : Destination { + + override val type = Type.ATTENDANCE + + override val fragment get() = AttendanceFragment.newInstance() + } + + object Exam : Destination { + + override val type = Type.EXAM + + override val fragment get() = ExamFragment.newInstance() + } + + data class Timetable(val date: LocalDate? = null) : Destination { + + override val type = Type.TIMETABLE + + override val fragment get() = TimetableFragment.newInstance(date) + } + + object Homework : Destination { + + override val type = Type.HOMEWORK + + override val fragment get() = HomeworkFragment.newInstance() + } + + object Note : Destination { + + override val type = Type.NOTE + + override val fragment get() = NoteFragment.newInstance() + } + + object Conference : Destination { + + override val type = Type.CONFERENCE + + override val fragment get() = ConferenceFragment.newInstance() + } + + object SchoolAnnouncement : Destination { + + override val type = Type.SCHOOL_ANNOUNCEMENT + + override val fragment get() = SchoolAnnouncementFragment.newInstance() + } + + object School : Destination { + + override val type = Type.SCHOOL + + override val fragment get() = SchoolFragment.newInstance() + } + + object LuckyNumber : Destination { + + override val type = Type.LUCKY_NUMBER + + override val fragment get() = LuckyNumberFragment.newInstance() + } + + object More : Destination { + + override val type = Type.MORE + + override val fragment get() = MoreFragment.newInstance() + } + + object Message : Destination { + + override val type = Type.MESSAGE + + override val fragment get() = MessageFragment.newInstance() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt index bb4f7022..5d5ed504 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt @@ -9,7 +9,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.enums.SentExcuseStatus import io.github.wulkanowy.databinding.ItemAttendanceBinding -import io.github.wulkanowy.utils.description +import io.github.wulkanowy.utils.descriptionRes import io.github.wulkanowy.utils.isExcusableOrNotExcused import javax.inject.Inject @@ -36,7 +36,7 @@ class AttendanceAdapter @Inject constructor() : with(holder.binding) { attendanceItemNumber.text = item.number.toString() attendanceItemSubject.text = item.subject - attendanceItemDescription.setText(item.description) + attendanceItemDescription.setText(item.descriptionRes) attendanceItemAlert.visibility = item.run { if (absence && !excused) View.VISIBLE else View.INVISIBLE } attendanceItemNumber.visibility = View.GONE attendanceItemExcuseInfo.visibility = View.GONE diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt index d816d8f0..9b5c63e4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt @@ -7,7 +7,7 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.databinding.DialogAttendanceBinding -import io.github.wulkanowy.utils.description +import io.github.wulkanowy.utils.descriptionRes import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.toFormattedString @@ -45,7 +45,7 @@ class AttendanceDialog : DialogFragment() { with(binding) { attendanceDialogSubjectValue.text = attendance.subject - attendanceDialogDescriptionValue.setText(attendance.description) + attendanceDialogDescriptionValue.setText(attendance.descriptionRes) attendanceDialogDateValue.text = attendance.date.toFormattedString() attendanceDialogNumberValue.text = attendance.number.toString() attendanceDialogClose.setOnClickListener { dismiss() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt index 26eca18f..d0dfcd69 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt @@ -3,6 +3,8 @@ package io.github.wulkanowy.ui.modules.debug.notification import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification +import io.github.wulkanowy.services.sync.notifications.NewAttendanceNotification import io.github.wulkanowy.services.sync.notifications.NewConferenceNotification import io.github.wulkanowy.services.sync.notifications.NewExamNotification import io.github.wulkanowy.services.sync.notifications.NewGradeNotification @@ -13,6 +15,7 @@ import io.github.wulkanowy.services.sync.notifications.NewNoteNotification import io.github.wulkanowy.services.sync.notifications.NewSchoolAnnouncementNotification import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler +import io.github.wulkanowy.ui.modules.debug.notification.mock.debugAttendanceItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugConferenceItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugExamItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeDetailsItems @@ -22,6 +25,7 @@ import io.github.wulkanowy.ui.modules.debug.notification.mock.debugLuckyNumber import io.github.wulkanowy.ui.modules.debug.notification.mock.debugMessageItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugNoteItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugSchoolAnnouncementItems +import io.github.wulkanowy.ui.modules.debug.notification.mock.debugTimetableItems import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -37,6 +41,8 @@ class NotificationDebugPresenter @Inject constructor( private val newNoteNotification: NewNoteNotification, private val newSchoolAnnouncementNotification: NewSchoolAnnouncementNotification, private val newLuckyNumberNotification: NewLuckyNumberNotification, + private val changeTimetableNotification: ChangeTimetableNotification, + private val newAttendanceNotification: NewAttendanceNotification, ) : BasePresenter(errorHandler, studentRepository) { private val items = listOf( @@ -64,6 +70,12 @@ class NotificationDebugPresenter @Inject constructor( NotificationDebugItem(R.string.note_title) { n -> withStudent { newNoteNotification.notify(debugNoteItems.take(n), it) } }, + NotificationDebugItem(R.string.attendance_title) { n -> + withStudent { newAttendanceNotification.notify(debugAttendanceItems.take(n), it) } + }, + NotificationDebugItem(R.string.timetable_title) { n -> + withStudent { changeTimetableNotification.notify(debugTimetableItems.take(n), it) } + }, NotificationDebugItem(R.string.school_announcement_title) { n -> withStudent { newSchoolAnnouncementNotification.notify(debugSchoolAnnouncementItems.take(n), it) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/attendance.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/attendance.kt new file mode 100644 index 00000000..042cf07e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/attendance.kt @@ -0,0 +1,35 @@ +package io.github.wulkanowy.ui.modules.debug.notification.mock + +import io.github.wulkanowy.data.db.entities.Attendance +import java.time.LocalDate + +val debugAttendanceItems = listOf( + generateAttendance("Matematyka", "PRESENCE"), + generateAttendance("Język angielski", "UNEXCUSED_LATENESS"), + generateAttendance("Geografia", "ABSENCE_UNEXCUSED"), + generateAttendance("Sieci komputerowe", "ABSENCE_EXCUSED"), + generateAttendance("Systemy operacyjne", "EXCUSED_LATENESS"), + generateAttendance("Język niemiecki", "ABSENCE_UNEXCUSED"), + generateAttendance("Biologia", "ABSENCE_UNEXCUSED"), + generateAttendance("Chemia", "ABSENCE_EXCUSED"), + generateAttendance("Fizyka", "ABSENCE_UNEXCUSED"), + generateAttendance("Matematyka", "ABSENCE_EXCUSED"), +) + +private fun generateAttendance(subject: String, name: String) = Attendance( + subject = subject, + studentId = 0, + diaryId = 0, + date = LocalDate.now(), + timeId = 0, + number = 1, + name = name, + presence = false, + absence = false, + exemption = false, + lateness = false, + excused = false, + deleted = false, + excusable = false, + excuseStatus = "" +) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/timetable.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/timetable.kt new file mode 100644 index 00000000..428c001d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/timetable.kt @@ -0,0 +1,39 @@ +package io.github.wulkanowy.ui.modules.debug.notification.mock + +import io.github.wulkanowy.data.db.entities.Timetable +import java.time.LocalDate +import java.time.LocalDateTime +import kotlin.random.Random + +val debugTimetableItems = listOf( + generateTimetable("Matematyka", "12", "01"), + generateTimetable("Język angielski", "23", "12"), + generateTimetable("Geografia", "34", "23"), + generateTimetable("Sieci komputerowe", "45", "34"), + generateTimetable("Systemy operacyjne", "56", "45"), + generateTimetable("Język niemiecki", "67", "56"), + generateTimetable("Biologia", "78", "67"), + generateTimetable("Chemia", "89", "78"), + generateTimetable("Fizyka", "90", "89"), + generateTimetable("Matematyka", "01", "90"), +) + +private fun generateTimetable(subject: String, room: String, roomOld: String) = Timetable( + subject = subject, + studentId = 0, + diaryId = 0, + date = LocalDate.now().minusDays(Random.nextLong(0, 8)), + number = 1, + start = LocalDateTime.now().plusHours(1), + end = LocalDateTime.now(), + subjectOld = "", + group = "", + room = room, + roomOld = roomOld, + teacher = "Wtorkowska Renata", + teacherOld = "", + info = "", + isStudentPlan = true, + changes = true, + canceled = true +) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt index e71fc0f6..04e35d31 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt @@ -66,7 +66,14 @@ class LoginStudentSelectFragment : } override fun openMainView() { - activity?.let { startActivity(MainActivity.getStartIntent(context = it, clear = true)) } + activity?.let { + startActivity( + MainActivity.getStartIntent( + context = it, + startNewTask = true + ) + ) + } } override fun showProgress(show: Boolean) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt index 49a19943..51585ac5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt @@ -18,8 +18,8 @@ import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.repositories.LuckyNumberRepository import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.toFirstResult import kotlinx.coroutines.runBlocking import timber.log.Timber @@ -39,6 +39,8 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { companion object { + const val LUCKY_NUMBER_PENDING_INTENT_ID = 200 + fun getStudentWidgetKey(appWidgetId: Int) = "lucky_number_widget_student_$appWidgetId" fun getThemeWidgetKey(appWidgetId: Int) = "lucky_number_widget_theme_$appWidgetId" @@ -48,18 +50,31 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { fun getWidthWidgetKey(appWidgetId: Int) = "lucky_number_widget_width_$appWidgetId" } - override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray?) { + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray? + ) { super.onUpdate(context, appWidgetManager, appWidgetIds) appWidgetIds?.forEach { appWidgetId -> + val luckyNumber = + getLuckyNumber(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId) + val appIntent = PendingIntent.getActivity( + context, + LUCKY_NUMBER_PENDING_INTENT_ID, + MainActivity.getStartIntent(context, Destination.LuckyNumber, true), + FLAG_UPDATE_CURRENT + ) - val luckyNumber = getLuckyNumber(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId) - val appIntent = PendingIntent.getActivity(context, MainView.Section.LUCKY_NUMBER.id, - MainActivity.getStartIntent(context, MainView.Section.LUCKY_NUMBER, true), FLAG_UPDATE_CURRENT) - - val remoteView = RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)).apply { - setTextViewText(R.id.luckyNumberWidgetNumber, luckyNumber?.luckyNumber?.toString() ?: "#") - setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent) - } + val remoteView = + RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)) + .apply { + setTextViewText( + R.id.luckyNumberWidgetNumber, + luckyNumber?.luckyNumber?.toString() ?: "#" + ) + setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent) + } setStyles(remoteView, appWidgetId) appWidgetManager.updateAppWidget(appWidgetId, remoteView) @@ -78,7 +93,12 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { } } - override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle?) { + override fun onAppWidgetOptionsChanged( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int, + newOptions: Bundle? + ) { super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions) val remoteView = RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)) @@ -88,8 +108,12 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { } private fun setStyles(views: RemoteViews, appWidgetId: Int, options: Bundle? = null) { - val width = options?.getInt(OPTION_APPWIDGET_MIN_WIDTH) ?: sharedPref.getLong(getWidthWidgetKey(appWidgetId), 74).toInt() - val height = options?.getInt(OPTION_APPWIDGET_MAX_HEIGHT) ?: sharedPref.getLong(getHeightWidgetKey(appWidgetId), 74).toInt() + val width = options?.getInt(OPTION_APPWIDGET_MIN_WIDTH) ?: sharedPref.getLong( + getWidthWidgetKey(appWidgetId), 74 + ).toInt() + val height = options?.getInt(OPTION_APPWIDGET_MAX_HEIGHT) ?: sharedPref.getLong( + getHeightWidgetKey(appWidgetId), 74 + ).toInt() with(sharedPref) { putLong(getWidthWidgetKey(appWidgetId), width.toLong()) @@ -112,7 +136,11 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { } } - private fun RemoteViews.setVisibility(imageTop: Boolean, imageLeft: Boolean, title: Boolean = false) { + private fun RemoteViews.setVisibility( + imageTop: Boolean, + imageLeft: Boolean, + title: Boolean = false + ) { setViewVisibility(R.id.luckyNumberWidgetImageTop, if (imageTop) VISIBLE else GONE) setViewVisibility(R.id.luckyNumberWidgetImageLeft, if (imageLeft) VISIBLE else GONE) setViewVisibility(R.id.luckyNumberWidgetTitle, if (title) VISIBLE else GONE) @@ -152,7 +180,8 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { private fun getCorrectLayoutId(appWidgetId: Int, context: Context): Int { val savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0) - val isSystemDarkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + val isSystemDarkMode = + context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES return if (savedTheme == 1L || (savedTheme == 2L && isSystemDarkMode)) { R.layout.widget_luckynumber_dark 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 d758ac0d..92bc36bc 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 @@ -5,16 +5,10 @@ import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK -import android.content.pm.ShortcutInfo -import android.content.pm.ShortcutManager -import android.graphics.drawable.Icon -import android.os.Build import android.os.Build.VERSION_CODES.P import android.os.Bundle import android.view.Menu import android.view.MenuItem -import androidx.annotation.RequiresApi -import androidx.core.content.getSystemService import androidx.core.view.ViewCompat import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment @@ -29,20 +23,10 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.databinding.ActivityMainBinding +import io.github.wulkanowy.services.shortcuts.ShortcutsHelper import io.github.wulkanowy.ui.base.BaseActivity +import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog -import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment -import io.github.wulkanowy.ui.modules.conference.ConferenceFragment -import io.github.wulkanowy.ui.modules.dashboard.DashboardFragment -import io.github.wulkanowy.ui.modules.exam.ExamFragment -import io.github.wulkanowy.ui.modules.grade.GradeFragment -import io.github.wulkanowy.ui.modules.homework.HomeworkFragment -import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment -import io.github.wulkanowy.ui.modules.message.MessageFragment -import io.github.wulkanowy.ui.modules.more.MoreFragment -import io.github.wulkanowy.ui.modules.note.NoteFragment -import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment -import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.InAppReviewHelper @@ -75,6 +59,9 @@ class MainActivity : BaseActivity(), MainVie @Inject lateinit var appInfo: AppInfo + @Inject + lateinit var shortcutsHelper: ShortcutsHelper + private var accountMenu: MenuItem? = null private val overlayProvider by lazy { ElevationOverlayProvider(this) } @@ -83,15 +70,19 @@ class MainActivity : BaseActivity(), MainVie FragNavController(supportFragmentManager, R.id.main_fragment_container) companion object { - const val EXTRA_START_MENU = "extraStartMenu" + + private const val EXTRA_START_DESTINATION = "start_destination" fun getStartIntent( context: Context, - startMenu: MainView.Section? = null, - clear: Boolean = false + destination: Destination? = null, + startNewTask: Boolean = false ) = Intent(context, MainActivity::class.java).apply { - if (clear) flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK - startMenu?.let { putExtra(EXTRA_START_MENU, it.id) } + putExtra(EXTRA_START_DESTINATION, destination) + + if (startNewTask) { + flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK + } } } @@ -106,42 +97,21 @@ class MainActivity : BaseActivity(), MainVie override val currentViewSubtitle get() = (navController.currentFrag as? MainView.TitledView)?.subtitleString - override var startMenuIndex = 0 - - override var startMenuMoreIndex = -1 - - private val moreMenuFragments = mapOf( - MainView.Section.MESSAGE.id to MessageFragment.newInstance(), - MainView.Section.EXAM.id to ExamFragment.newInstance(), - MainView.Section.HOMEWORK.id to HomeworkFragment.newInstance(), - MainView.Section.NOTE.id to NoteFragment.newInstance(), - MainView.Section.CONFERENCE.id to ConferenceFragment.newInstance(), - MainView.Section.SCHOOL_ANNOUNCEMENT.id to SchoolAnnouncementFragment.newInstance(), - MainView.Section.LUCKY_NUMBER.id to LuckyNumberFragment.newInstance(), - ) + private var savedInstanceState: Bundle? = null @SuppressLint("NewApi") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityMainBinding.inflate(layoutInflater).apply { binding = this }.root) setSupportActionBar(binding.mainToolbar) + this.savedInstanceState = savedInstanceState messageContainer = binding.mainMessageContainer updateHelper.messageContainer = binding.mainFragmentContainer - val section = MainView.Section.values() - .singleOrNull { it.id == intent.getIntExtra(EXTRA_START_MENU, -1) } - - presenter.onAttachView(this, section) - - with(navController) { - initialize(startMenuIndex, savedInstanceState) - pushFragment(moreMenuFragments[startMenuMoreIndex]) - } - - if (appInfo.systemVersion >= Build.VERSION_CODES.N_MR1) { - initShortcuts() - } + val destination = intent.getSerializableExtra(EXTRA_START_DESTINATION) as Destination? + ?: shortcutsHelper.getDestination(intent) + presenter.onAttachView(this, destination) updateHelper.checkAndInstallUpdates(this) } @@ -157,54 +127,6 @@ class MainActivity : BaseActivity(), MainVie updateHelper.onActivityResult(requestCode, resultCode) } - @RequiresApi(Build.VERSION_CODES.N_MR1) - fun initShortcuts() { - val shortcutsList = mutableListOf() - - listOf( - Triple( - getString(R.string.grade_title), - R.drawable.ic_shortcut_grade, - MainView.Section.GRADE - ), - Triple( - getString(R.string.attendance_title), - R.drawable.ic_shortcut_attendance, - MainView.Section.ATTENDANCE - ), - Triple( - getString(R.string.exam_title), - R.drawable.ic_shortcut_exam, - MainView.Section.EXAM - ), - Triple( - getString(R.string.timetable_title), - R.drawable.ic_shortcut_timetable, - MainView.Section.TIMETABLE - ) - ).forEach { (title, icon, enum) -> - shortcutsList.add( - ShortcutInfo.Builder(applicationContext, title) - .setShortLabel(title) - .setLongLabel(title) - .setIcon(Icon.createWithResource(applicationContext, icon)) - .setIntents( - arrayOf( - Intent(applicationContext, MainActivity::class.java) - .setAction(Intent.ACTION_VIEW), - Intent(applicationContext, MainActivity::class.java) - .putExtra(EXTRA_START_MENU, enum.id) - .setAction(Intent.ACTION_VIEW) - .addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK) - ) - ) - .build() - ) - } - - getSystemService()?.dynamicShortcuts = shortcutsList - } - override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.action_menu_main, menu) accountMenu = menu?.findItem(R.id.mainMenuAccount) @@ -213,15 +135,38 @@ class MainActivity : BaseActivity(), MainVie return true } - @SuppressLint("NewApi") - override fun initView() { + override fun initView(startMenuIndex: Int, rootDestinations: List) { + initializeToolbar() + initializeBottomNavigation(startMenuIndex) + initializeNavController(startMenuIndex, rootDestinations) + } + + private fun initializeNavController(startMenuIndex: Int, rootDestinations: List) { + with(navController) { + setOnViewChangeListener { destinationView -> + presenter.onViewChange(destinationView) + analytics.setCurrentScreen( + this@MainActivity, + destinationView::class.java.simpleName + ) + } + fragmentHideStrategy = HIDE + rootFragments = rootDestinations.map { it.fragment } + + initialize(startMenuIndex, savedInstanceState) + } + } + + private fun initializeToolbar() { with(binding.mainToolbar) { stateListAnimator = null setBackgroundColor( overlayProvider.compositeOverlayWithThemeSurfaceColorIfNeeded(dpToPx(4f)) ) } + } + private fun initializeBottomNavigation(startMenuIndex: Int) { with(binding.mainBottomNav) { with(menu) { add(Menu.NONE, 0, Menu.NONE, R.string.dashboard_title) @@ -239,36 +184,6 @@ class MainActivity : BaseActivity(), MainVie setOnItemSelectedListener { presenter.onTabSelected(it.itemId, false) } setOnItemReselectedListener { presenter.onTabSelected(it.itemId, true) } } - - with(navController) { - setOnViewChangeListener { section, name -> - if (section == MainView.Section.ACCOUNT || section == MainView.Section.STUDENT_INFO) { - binding.mainBottomNav.isVisible = false - - if (appInfo.systemVersion >= P) { - window.navigationBarColor = getThemeAttrColor(R.attr.colorSurface) - } - } else { - binding.mainBottomNav.isVisible = true - - if (appInfo.systemVersion >= P) { - window.navigationBarColor = - getThemeAttrColor(android.R.attr.navigationBarColor) - } - } - - analytics.setCurrentScreen(this@MainActivity, name) - presenter.onViewChange(section) - } - fragmentHideStrategy = HIDE - rootFragments = listOf( - DashboardFragment.newInstance(), - GradeFragment.newInstance(), - AttendanceFragment.newInstance(), - TimetableFragment.newInstance(), - MoreFragment.newInstance() - ) - } } override fun onPreferenceStartFragment( @@ -317,6 +232,22 @@ class MainActivity : BaseActivity(), MainVie ViewCompat.setElevation(binding.mainToolbar, if (show) dpToPx(4f) else 0f) } + override fun showBottomNavigation(show: Boolean) { + binding.mainBottomNav.isVisible = show + + if (appInfo.systemVersion >= P) { + window.navigationBarColor = if (show) { + getThemeAttrColor(android.R.attr.navigationBarColor) + } else { + getThemeAttrColor(R.attr.colorSurface) + } + } + } + + override fun openMoreDestination(destination: Destination) { + pushView(destination.fragment) + } + override fun notifyMenuViewReselected() { (navController.currentStack?.getOrNull(0) as? MainView.MainChildView)?.onFragmentReselected() } @@ -373,6 +304,6 @@ class MainActivity : BaseActivity(), MainVie override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) navController.onSaveInstanceState(outState) - intent.removeExtra(EXTRA_START_MENU) + intent.removeExtra(EXTRA_START_DESTINATION) } } 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 4805b5a1..0aca51af 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 @@ -6,10 +6,15 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.ErrorHandler -import io.github.wulkanowy.ui.modules.main.MainView.Section.GRADE -import io.github.wulkanowy.ui.modules.main.MainView.Section.MESSAGE -import io.github.wulkanowy.ui.modules.main.MainView.Section.SCHOOL +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.account.AccountView +import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsView +import io.github.wulkanowy.ui.modules.grade.GradeView +import io.github.wulkanowy.ui.modules.message.MessageView +import io.github.wulkanowy.ui.modules.schoolandteachers.school.SchoolView +import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.flowWithResource import kotlinx.coroutines.flow.onEach @@ -27,19 +32,40 @@ class MainPresenter @Inject constructor( private var studentsWitSemesters: List? = null - fun onAttachView(view: MainView, initMenu: MainView.Section?) { - super.onAttachView(view) - view.apply { - getProperViewIndexes(initMenu).let { (main, more) -> - startMenuIndex = main - startMenuMoreIndex = more + private val rootDestinationTypeList = listOf( + Destination.Type.DASHBOARD, + Destination.Type.GRADE, + Destination.Type.ATTENDANCE, + Destination.Type.TIMETABLE, + Destination.Type.MORE + ) + + private val Destination?.startMenuIndex + get() = when { + this == null -> prefRepository.startMenuIndex + type in rootDestinationTypeList -> { + rootDestinationTypeList.indexOf(type) } - initView() - Timber.i("Main view was initialized with $startMenuIndex menu index and $startMenuMoreIndex more index") + else -> 4 + } + + fun onAttachView(view: MainView, initDestination: Destination?) { + super.onAttachView(view) + + val startMenuIndex = initDestination.startMenuIndex + val destinations = rootDestinationTypeList.map { + if (it == initDestination?.type) initDestination else it.defaultDestination + } + + view.initView(startMenuIndex, destinations) + if (initDestination != null && startMenuIndex == 4) { + view.openMoreDestination(initDestination) } syncManager.startPeriodicSyncWorker() - analytics.logEvent("app_open", "destination" to initMenu?.name) + + analytics.logEvent("app_open", "destination" to initDestination.toString()) + Timber.i("Main view was initialized with $initDestination") } fun onActionMenuCreated() { @@ -64,9 +90,10 @@ class MainPresenter @Inject constructor( }.launch("avatar") } - fun onViewChange(section: MainView.Section?) { + fun onViewChange(destinationView: BaseView) { view?.apply { - showActionBarElevation(section != GRADE && section != MESSAGE && section != SCHOOL) + showBottomNavigation(destinationView !is AccountView && destinationView !is StudentInfoView && destinationView !is AccountDetailsView) + showActionBarElevation(destinationView !is GradeView && destinationView !is MessageView && destinationView !is SchoolView) currentViewTitle?.let { setViewTitle(it) } currentViewSubtitle?.let { setViewSubTitle(it.ifBlank { null }) } currentStackSize?.let { @@ -134,10 +161,4 @@ class MainPresenter @Inject constructor( view?.showStudentAvatar(currentStudent) } - - private fun getProperViewIndexes(initMenu: MainView.Section?) = when (initMenu?.id) { - in 0..3 -> initMenu!!.id to -1 - in 4..100 -> 4 to initMenu!!.id - else -> prefRepository.startMenuIndex to -1 - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt index 8851f587..3a57fcc6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt @@ -3,13 +3,10 @@ package io.github.wulkanowy.ui.modules.main import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.ui.base.BaseView +import io.github.wulkanowy.ui.modules.Destination interface MainView : BaseView { - var startMenuIndex: Int - - var startMenuMoreIndex: Int - val isRootView: Boolean val currentViewTitle: String? @@ -18,7 +15,7 @@ interface MainView : BaseView { val currentStackSize: Int? - fun initView() + fun initView(startMenuIndex: Int, rootDestinations: List) fun switchMenuView(position: Int) @@ -28,6 +25,8 @@ interface MainView : BaseView { fun showActionBarElevation(show: Boolean) + fun showBottomNavigation(show: Boolean) + fun notifyMenuViewReselected() fun notifyMenuViewChanged() @@ -42,6 +41,8 @@ interface MainView : BaseView { fun showInAppReview() + fun openMoreDestination(destination: Destination) + interface MainChildView { fun onFragmentReselected() @@ -57,25 +58,4 @@ interface MainView : BaseView { get() = "" set(_) {} } - - enum class Section { - DASHBOARD, - GRADE, - ATTENDANCE, - TIMETABLE, - MORE, - MESSAGE, - EXAM, - HOMEWORK, - NOTE, - CONFERENCE, - SCHOOL_ANNOUNCEMENT, - SCHOOL, - LUCKY_NUMBER, - ACCOUNT, - STUDENT_INFO, - SETTINGS; - - val id get() = ordinal - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt index 7ee326f8..27b3637a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt @@ -5,7 +5,6 @@ import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Notification import io.github.wulkanowy.databinding.ItemNotificationsCenterBinding import io.github.wulkanowy.services.sync.notifications.NotificationType @@ -28,26 +27,12 @@ class NotificationsCenterAdapter @Inject constructor() : notificationsCenterItemTitle.text = item.title notificationsCenterItemContent.text = item.content notificationsCenterItemDate.text = item.date.toFormattedString("HH:mm, d MMM") - notificationsCenterItemIcon.setImageResource(item.type.toDrawableResId()) + notificationsCenterItemIcon.setImageResource(item.type.icon) root.setOnClickListener { onItemClickListener(item.type) } } } - private fun NotificationType.toDrawableResId() = when (this) { - NotificationType.NEW_CONFERENCE -> R.drawable.ic_more_conferences - NotificationType.NEW_EXAM -> R.drawable.ic_main_exam - NotificationType.NEW_GRADE_DETAILS -> R.drawable.ic_stat_grade - NotificationType.NEW_GRADE_PREDICTED -> R.drawable.ic_stat_grade - NotificationType.NEW_GRADE_FINAL -> R.drawable.ic_stat_grade - NotificationType.NEW_HOMEWORK -> R.drawable.ic_more_homework - NotificationType.NEW_LUCKY_NUMBER -> R.drawable.ic_stat_luckynumber - NotificationType.NEW_MESSAGE -> R.drawable.ic_stat_message - NotificationType.NEW_NOTE -> R.drawable.ic_stat_note - NotificationType.NEW_ANNOUNCEMENT -> R.drawable.ic_all_about - NotificationType.PUSH -> R.drawable.ic_stat_all - } - class ViewHolder(val binding: ItemNotificationsCenterBinding) : RecyclerView.ViewHolder(binding.root) @@ -59,4 +44,4 @@ class NotificationsCenterAdapter @Inject constructor() : override fun areItemsTheSame(oldItem: Notification, newItem: Notification) = oldItem.id == newItem.id } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt index b9bfb447..f3bbc42d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt @@ -11,6 +11,7 @@ import io.github.wulkanowy.data.db.entities.Notification import io.github.wulkanowy.databinding.FragmentNotificationsCenterBinding import io.github.wulkanowy.services.sync.notifications.NotificationType import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment import io.github.wulkanowy.ui.modules.conference.ConferenceFragment import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment @@ -21,6 +22,7 @@ import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.message.MessageFragment import io.github.wulkanowy.ui.modules.note.NoteFragment import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment +import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import javax.inject.Inject @AndroidEntryPoint @@ -104,5 +106,7 @@ class NotificationsCenterFragment : NotificationType.NEW_NOTE -> NoteFragment.newInstance() NotificationType.NEW_ANNOUNCEMENT -> SchoolAnnouncementFragment.newInstance() NotificationType.PUSH -> null + NotificationType.CHANGE_TIMETABLE -> TimetableFragment.newInstance() + NotificationType.NEW_ATTENDANCE -> AttendanceFragment.newInstance() } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt index 2612fab3..d56cdfa7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt @@ -6,7 +6,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.ui.modules.main.MainView import timber.log.Timber -class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView { +class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, SettingsView { companion object { @@ -19,4 +19,16 @@ class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView { setPreferencesFromResource(R.xml.scheme_preferences, rootKey) Timber.i("Settings view was initialized") } + + override fun showError(text: String, error: Throwable) {} + + override fun showMessage(text: String) {} + + override fun showExpiredDialog() {} + + override fun openClearLoginView() {} + + override fun showErrorDetailsDialog(error: Throwable) {} + + override fun showChangePasswordSnackbar(redirectUrl: String) {} } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt new file mode 100644 index 00000000..79f91bc5 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt @@ -0,0 +1,5 @@ +package io.github.wulkanowy.ui.modules.settings + +import io.github.wulkanowy.ui.base.BaseView + +interface SettingsView : BaseView \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt index f9079b5f..63fc8496 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt @@ -24,8 +24,8 @@ import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.HiltBroadcastReceiver import io.github.wulkanowy.services.widgets.TimetableWidgetService +import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.createNameInitialsDrawable @@ -60,6 +60,8 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { companion object { + private const val TIMETABLE_PENDING_INTENT_ID = 201 + private const val EXTRA_TOGGLED_WIDGET_ID = "extraToggledWidget" private const val EXTRA_BUTTON_TYPE = "extraButtonType" @@ -174,8 +176,8 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { ) val appIntent = PendingIntent.getActivity( context, - MainView.Section.TIMETABLE.id, - MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), + TIMETABLE_PENDING_INTENT_ID, + MainActivity.getStartIntent(context, Destination.Timetable(), true), FLAG_UPDATE_CURRENT ) diff --git a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt index b89ad57d..397c9595 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt @@ -29,7 +29,7 @@ private fun calculatePercentage(presence: Double, absence: Double): Double { return if ((presence + absence) == 0.0) 0.0 else (presence / (presence + absence)) * 100 } -inline val Attendance.description +inline val Attendance.descriptionRes get() = when (AttendanceCategory.getCategoryByName(name)) { AttendanceCategory.PRESENCE -> R.string.attendance_present AttendanceCategory.ABSENCE_UNEXCUSED -> R.string.attendance_absence_unexcused diff --git a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt index 2cd4459e..ecd982a1 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt @@ -18,6 +18,7 @@ import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.annotation.DrawableRes +import androidx.annotation.PluralsRes import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils import androidx.core.graphics.applyCanvas @@ -57,6 +58,9 @@ fun Context.getCompatDrawable(@DrawableRes drawableRes: Int, @ColorRes colorRes: fun Context.getCompatBitmap(@DrawableRes drawableRes: Int, @ColorRes colorRes: Int) = getCompatDrawable(drawableRes, colorRes)?.toBitmap() +fun Context.getPlural(@PluralsRes pluralRes: Int, quantity: Int, vararg arguments: Any) = + resources.getQuantityString(pluralRes, quantity, *arguments) + fun Context.openInternetBrowser(uri: String, onActivityNotFound: (uri: String) -> Unit = {}) { Intent.parseUri(uri, 0).let { try { diff --git a/app/src/main/java/io/github/wulkanowy/utils/FragNavControlerExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/FragNavControlerExtension.kt index 9dc1e18a..01c876dd 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/FragNavControlerExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/FragNavControlerExtension.kt @@ -2,16 +2,19 @@ package io.github.wulkanowy.utils import androidx.fragment.app.Fragment import com.ncapdevi.fragnav.FragNavController -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.base.BaseView -inline fun FragNavController.setOnViewChangeListener(crossinline listener: (section: MainView.Section?, name: String?) -> Unit) { +inline fun FragNavController.setOnViewChangeListener(crossinline listener: (view: BaseView) -> Unit) { transactionListener = object : FragNavController.TransactionListener { - override fun onFragmentTransaction(fragment: Fragment?, transactionType: FragNavController.TransactionType) { - listener(fragment?.toSection(), fragment?.let { it::class.java.simpleName }) + override fun onFragmentTransaction( + fragment: Fragment?, + transactionType: FragNavController.TransactionType + ) { + fragment?.let { listener(it as BaseView) } } override fun onTabTransaction(fragment: Fragment?, index: Int) { - listener(fragment?.toSection(), fragment?.let { it::class.java.simpleName }) + fragment?.let { listener(it as BaseView) } } } } diff --git a/app/src/main/java/io/github/wulkanowy/utils/FragmentExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/FragmentExtension.kt deleted file mode 100644 index 210a6209..00000000 --- a/app/src/main/java/io/github/wulkanowy/utils/FragmentExtension.kt +++ /dev/null @@ -1,44 +0,0 @@ -package io.github.wulkanowy.utils - -import androidx.fragment.app.Fragment -import io.github.wulkanowy.ui.modules.account.AccountFragment -import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment -import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment -import io.github.wulkanowy.ui.modules.conference.ConferenceFragment -import io.github.wulkanowy.ui.modules.dashboard.DashboardFragment -import io.github.wulkanowy.ui.modules.exam.ExamFragment -import io.github.wulkanowy.ui.modules.grade.GradeFragment -import io.github.wulkanowy.ui.modules.homework.HomeworkFragment -import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment -import io.github.wulkanowy.ui.modules.main.MainView -import io.github.wulkanowy.ui.modules.message.MessageFragment -import io.github.wulkanowy.ui.modules.more.MoreFragment -import io.github.wulkanowy.ui.modules.note.NoteFragment -import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment -import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment -import io.github.wulkanowy.ui.modules.settings.SettingsFragment -import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoFragment -import io.github.wulkanowy.ui.modules.timetable.TimetableFragment - -fun Fragment.toSection(): MainView.Section? { - return when (this) { - is GradeFragment -> MainView.Section.GRADE - is AttendanceFragment -> MainView.Section.ATTENDANCE - is ExamFragment -> MainView.Section.EXAM - is TimetableFragment -> MainView.Section.TIMETABLE - is MoreFragment -> MainView.Section.MORE - is MessageFragment -> MainView.Section.MESSAGE - is HomeworkFragment -> MainView.Section.HOMEWORK - is NoteFragment -> MainView.Section.NOTE - is LuckyNumberFragment -> MainView.Section.LUCKY_NUMBER - is SettingsFragment -> MainView.Section.SETTINGS - is SchoolAndTeachersFragment -> MainView.Section.SCHOOL - is AccountFragment -> MainView.Section.ACCOUNT - is AccountDetailsFragment -> MainView.Section.ACCOUNT - is StudentInfoFragment -> MainView.Section.STUDENT_INFO - is ConferenceFragment -> MainView.Section.CONFERENCE - is SchoolAnnouncementFragment -> MainView.Section.SCHOOL_ANNOUNCEMENT - is DashboardFragment -> MainView.Section.DASHBOARD - else -> null - } -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e88ee1a..00806956 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -163,6 +163,26 @@ Now: %s Next: %s Later: %s + %1$s lesson %2$d - %3$s + Change of room from %1$s to %2$s + Change of teacher from %1$s to %2$s + Change of subject from %1$s to %2$s + + Timetable change + Timetable changes + + + %1$s - %2$d change in timetable + %1$s - %2$d changes in timetable + + + %1$d change in timetable + %1$d changes in timetable + + + %d change + %d changes + @@ -200,6 +220,18 @@ Excuse z powodu Dzień dobry,\nProszę o usprawiedliwienie mojego dziecka w dniu %s z lekcji %s%s%s.\n\nPozdrawiam. + + New attendance + New attendance + + + %1$d new attendance + %1$d attendance + + + %d attendance + %d attendance + @@ -706,6 +738,8 @@ Push notifications Upcoming lessons Debug + Timetable change + New attendance diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt index 2c44e9fa..7557d745 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt @@ -42,20 +42,12 @@ class MainPresenterTest { MockKAnnotations.init(this) clearMocks(mainView) - every { mainView.startMenuIndex = any() } just Runs - every { mainView.startMenuMoreIndex = any() } just Runs - every { mainView.startMenuIndex } returns 1 - every { mainView.startMenuMoreIndex } returns 1 - every { mainView.initView() } just Runs - presenter = MainPresenter(errorHandler, studentRepository, prefRepository, syncManager, analytics) + every { mainView.initView(any(), any()) } just Runs + presenter = + MainPresenter(errorHandler, studentRepository, prefRepository, syncManager, analytics) presenter.onAttachView(mainView, null) } - @Test - fun initMenuTest() { - verify { mainView.initView() } - } - @Test fun onTabSelectedTest() { every { mainView.notifyMenuViewChanged() } just Runs From 007d62e61d79c89ab5d6f22563280f354086e32b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 11 Nov 2021 15:23:20 +0100 Subject: [PATCH 073/357] Update project to Android SDK 31 (#1570) --- app/build.gradle | 11 ++--- app/src/main/AndroidManifest.xml | 1 - .../wulkanowy/data/db/SharedPrefProvider.kt | 9 ++-- .../alarm/TimetableNotificationReceiver.kt | 8 ++-- .../TimetableNotificationSchedulerHelper.kt | 12 +++-- .../services/shortcuts/ShortcutsHelper.kt | 10 ++-- .../notifications/AppNotificationManager.kt | 7 +-- .../ChangeTimetableNotification.kt | 6 +-- .../NewAttendanceNotification.kt | 6 +-- .../NewConferenceNotification.kt | 6 +-- .../sync/notifications/NewExamNotification.kt | 6 +-- .../notifications/NewGradeNotification.kt | 14 +++--- .../notifications/NewHomeworkNotification.kt | 6 +-- .../NewLuckyNumberNotification.kt | 4 +- .../notifications/NewMessageNotification.kt | 6 +-- .../sync/notifications/NewNoteNotification.kt | 6 +-- .../NewSchoolAnnouncementNotification.kt | 6 +-- .../github/wulkanowy/ui/base/BaseActivity.kt | 8 +--- .../github/wulkanowy/ui/base/BasePresenter.kt | 2 +- .../github/wulkanowy/ui/base/ThemeManager.kt | 11 +++-- .../wulkanowy/ui/modules/Destination.kt | 30 ++++++------ .../accountedit/AccountEditColorAdapter.kt | 32 +++---------- .../modules/attendance/AttendanceFragment.kt | 6 +-- .../summary/AttendanceSummaryFragment.kt | 2 +- .../wulkanowy/ui/modules/exam/ExamFragment.kt | 2 +- .../ui/modules/grade/GradeFragment.kt | 4 +- .../statistics/GradeStatisticsFragment.kt | 2 +- .../ui/modules/homework/HomeworkFragment.kt | 2 +- .../ui/modules/login/LoginActivity.kt | 4 +- .../LoginStudentSelectFragment.kt | 13 ++---- .../history/LuckyNumberHistoryFragment.kt | 2 +- .../LuckyNumberWidgetProvider.kt | 8 ++-- .../wulkanowy/ui/modules/main/MainActivity.kt | 12 ----- .../ui/modules/message/MessageFragment.kt | 5 +- .../message/preview/MessagePreviewFragment.kt | 10 +--- .../preview/MessagePreviewPresenter.kt | 24 +++++----- .../message/preview/MessagePreviewView.kt | 7 +-- .../message/tab/MessageTabPresenter.kt | 9 ++-- .../SchoolAndTeachersFragment.kt | 4 +- .../notifications/NotificationsFragment.kt | 2 - .../ui/modules/splash/SplashActivity.kt | 43 +++++++++++++++--- .../ui/modules/splash/SplashPresenter.kt | 27 ++++++----- .../wulkanowy/ui/modules/splash/SplashView.kt | 3 +- .../ui/modules/timetable/TimetableFragment.kt | 2 +- .../additional/AdditionalLessonsFragment.kt | 2 +- .../completed/CompletedLessonsFragment.kt | 2 +- .../TimetableWidgetProvider.kt | 34 +++++++------- .../ui/widgets/FittedScrollableTabLayout.kt | 12 ++--- .../ui/widgets/MaterialLinearLayout.kt | 25 +++------- .../wulkanowy/ui/widgets/MaterialTabLayout.kt | 25 ---------- .../wulkanowy/utils/PendingIntentCompat.kt | 11 +++++ .../github/wulkanowy/utils/StringExtension.kt | 4 +- .../wulkanowy/utils/security/Scrambler.kt | 3 -- ...ompat_splash_screen_no_icon_background.xml | 24 ++++++++++ .../main/res/drawable-v23/img_splash_logo.png | Bin 19474 -> 0 bytes .../drawable-v23/layer_splash_background.xml | 12 ----- app/src/main/res/drawable/ic_splash_logo.xml | 12 +++++ app/src/main/res/drawable/img_splash_logo.png | Bin 20340 -> 0 bytes .../res/drawable/layer_splash_background.xml | 12 ----- .../main/res/layout/fragment_attendance.xml | 8 ++-- app/src/main/res/layout/fragment_exam.xml | 8 ++-- .../layout/fragment_lucky_number_history.xml | 8 ++-- app/src/main/res/layout/fragment_message.xml | 2 +- app/src/main/res/layout/fragment_school.xml | 8 ++-- .../res/layout/fragment_schoolandteachers.xml | 2 +- .../main/res/layout/fragment_timetable.xml | 8 ++-- .../layout/fragment_timetable_additional.xml | 8 ++-- .../layout/fragment_timetable_completed.xml | 8 ++-- .../layout/subitem_dashboard_conferences.xml | 7 +-- app/src/main/res/values-v23/styles.xml | 8 +--- app/src/main/res/values-v26/styles.xml | 7 --- app/src/main/res/values-v28/styles.xml | 8 ---- app/src/main/res/values-v29/styles.xml | 8 ---- app/src/main/res/values/styles.xml | 12 ++--- .../wulkanowy/utils/CrashlyticsUtils.kt | 6 --- .../ui/modules/splash/SplashPresenterTest.kt | 6 +-- 76 files changed, 316 insertions(+), 384 deletions(-) delete mode 100644 app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialTabLayout.kt create mode 100644 app/src/main/java/io/github/wulkanowy/utils/PendingIntentCompat.kt create mode 100644 app/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml delete mode 100644 app/src/main/res/drawable-v23/img_splash_logo.png delete mode 100644 app/src/main/res/drawable-v23/layer_splash_background.xml create mode 100644 app/src/main/res/drawable/ic_splash_logo.xml delete mode 100644 app/src/main/res/drawable/img_splash_logo.png delete mode 100644 app/src/main/res/drawable/layer_splash_background.xml diff --git a/app/build.gradle b/app/build.gradle index 53814dae..bef984aa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,17 +15,16 @@ apply from: 'sonarqube.gradle' apply from: 'hooks.gradle' android { - compileSdkVersion 30 + compileSdkVersion 31 defaultConfig { applicationId "io.github.wulkanowy" testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 - targetSdkVersion 30 + targetSdkVersion 31 versionCode 97 versionName "1.3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables.useSupportLibrary = true resValue "string", "app_name", "Wulkanowy" @@ -166,7 +165,7 @@ huaweiPublish { } ext { - work_manager = "2.6.0" + work_manager = "2.7.0" android_hilt = "1.0.0" room = "2.3.0" chucker = "3.5.2" @@ -183,9 +182,9 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" implementation "androidx.core:core-ktx:1.6.0" + implementation 'androidx.core:core-splashscreen:1.0.0-alpha02' implementation "androidx.activity:activity-ktx:1.3.1" - implementation "androidx.appcompat:appcompat:1.3.1" - implementation "androidx.appcompat:appcompat-resources:1.3.1" + implementation "androidx.appcompat:appcompat:1.4.0-beta01" implementation "androidx.fragment:fragment-ktx:1.3.6" implementation "androidx.annotation:annotation:1.2.0" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e43a0ec0..5928c23a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -42,7 +42,6 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="false" android:theme="@style/WulkanowyTheme" - android:usesCleartextTraffic="true" tools:ignore="GoogleAppIndexingWarning,UnusedAttribute"> , requestCode: Int) { if (now() in range) cancelNotification() + alarmManager.cancel( - PendingIntent.getBroadcast(context, requestCode, Intent(), FLAG_UPDATE_CURRENT) + PendingIntent.getBroadcast( + context, + requestCode, + Intent(), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) ) } @@ -156,7 +162,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor( alarmManager, RTC_WAKEUP, time.toTimestamp(), PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also { it.putExtra(LESSON_TYPE, notificationType) - }, FLAG_UPDATE_CURRENT) + }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) ) Timber.d( "TimetableNotification scheduled: type: $notificationType, subject: ${ diff --git a/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt b/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt index d1c215f2..bb09434b 100644 --- a/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt +++ b/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt @@ -8,7 +8,7 @@ import androidx.core.graphics.drawable.IconCompat import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import javax.inject.Inject import javax.inject.Singleton @@ -35,7 +35,7 @@ class ShortcutsHelper @Inject constructor(@ApplicationContext private val contex .setShortLabel(context.getString(R.string.grade_title)) .setLongLabel(context.getString(R.string.grade_title)) .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_grade)) - .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .setIntent(SplashActivity.getStartIntent(context, startNewTask = true) .apply { action = Intent.ACTION_VIEW putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "grade") @@ -47,7 +47,7 @@ class ShortcutsHelper @Inject constructor(@ApplicationContext private val contex .setShortLabel(context.getString(R.string.attendance_title)) .setLongLabel(context.getString(R.string.attendance_title)) .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_attendance)) - .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .setIntent(SplashActivity.getStartIntent(context, startNewTask = true) .apply { action = Intent.ACTION_VIEW putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "attendance") @@ -59,7 +59,7 @@ class ShortcutsHelper @Inject constructor(@ApplicationContext private val contex .setShortLabel(context.getString(R.string.exam_title)) .setLongLabel(context.getString(R.string.exam_title)) .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_exam)) - .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .setIntent(SplashActivity.getStartIntent(context, startNewTask = true) .apply { action = Intent.ACTION_VIEW putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "exam") @@ -71,7 +71,7 @@ class ShortcutsHelper @Inject constructor(@ApplicationContext private val contex .setShortLabel(context.getString(R.string.timetable_title)) .setLongLabel(context.getString(R.string.timetable_title)) .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_timetable)) - .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .setIntent(SplashActivity.getStartIntent(context, startNewTask = true) .apply { action = Intent.ACTION_VIEW putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "timetable") diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt index 542b9346..2848fe30 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt @@ -13,6 +13,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.data.repositories.NotificationRepository +import io.github.wulkanowy.utils.PendingIntentCompat import io.github.wulkanowy.utils.getCompatBitmap import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.nickOrName @@ -45,7 +46,7 @@ class AppNotificationManager @Inject constructor( context, Random.nextInt(), notificationData.intentToStart, - PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) ) .setContentTitle(notificationData.title) @@ -86,7 +87,7 @@ class AppNotificationManager @Inject constructor( context, Random.nextInt(), notificationData.intentToStart, - PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) ) .setContentTitle(notificationData.title) @@ -134,7 +135,7 @@ class AppNotificationManager @Inject constructor( context, Random.nextInt(), groupNotificationData.intentToStart, - PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) ) .setLocalOnly(true) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt index 6d2d3a59..f8860b7f 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate @@ -32,7 +32,7 @@ class ChangeTimetableNotification @Inject constructor( 1 ), content = it, - intentToStart = MainActivity.getStartIntent( + intentToStart = SplashActivity.getStartIntent( context = context, destination = Destination.Timetable(date), startNewTask = true @@ -54,7 +54,7 @@ class ChangeTimetableNotification @Inject constructor( changedLessons.size, changedLessons.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Timetable(), true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Timetable(), true), type = NotificationType.CHANGE_TIMETABLE ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt index 69555272..3792725c 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.descriptionRes import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString @@ -31,7 +31,7 @@ class NewAttendanceNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.attendance_notify_new_items_title, 1), content = it, - intentToStart = MainActivity.getStartIntent(context, Destination.Attendance, true) + intentToStart = SplashActivity.getStartIntent(context, Destination.Attendance, true) ) } @@ -46,7 +46,7 @@ class NewAttendanceNotification @Inject constructor( notificationDataList.size, notificationDataList.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Attendance, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Attendance, true), type = NotificationType.NEW_ATTENDANCE ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt index 97b1332d..4ec359b0 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDateTime @@ -31,7 +31,7 @@ class NewConferenceNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.conference_notify_new_item_title, 1), content = it, - intentToStart = MainActivity.getStartIntent(context, Destination.Conference, true) + intentToStart = SplashActivity.getStartIntent(context, Destination.Conference, true) ) } @@ -43,7 +43,7 @@ class NewConferenceNotification @Inject constructor( lines.size, lines.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Conference, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Conference, true), type = NotificationType.NEW_CONFERENCE ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt index 6f8ed896..feb7c320 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate @@ -31,7 +31,7 @@ class NewExamNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.exam_notify_new_item_title, 1), content = it, - intentToStart = MainActivity.getStartIntent(context, Destination.Exam, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Exam, true), ) } @@ -43,7 +43,7 @@ class NewExamNotification @Inject constructor( lines.size, lines.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Exam, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Exam, true), type = NotificationType.NEW_EXAM ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt index 09692fb7..425a57fc 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt @@ -9,7 +9,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import javax.inject.Inject @@ -23,7 +23,7 @@ class NewGradeNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.grade_new_items, 1), content = "${it.subject}: ${it.entry}", - intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade, true), ) } @@ -31,7 +31,7 @@ class NewGradeNotification @Inject constructor( notificationDataList = notificationDataList, title = context.getPlural(R.plurals.grade_new_items, items.size), content = context.getPlural(R.plurals.grade_notify_new_items, items.size, items.size), - intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade, true), type = NotificationType.NEW_GRADE_DETAILS ) @@ -43,7 +43,7 @@ class NewGradeNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.grade_new_items_predicted, 1), content = "${it.subject}: ${it.predictedGrade}", - intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade, true), ) } @@ -55,7 +55,7 @@ class NewGradeNotification @Inject constructor( items.size, items.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade, true), type = NotificationType.NEW_GRADE_PREDICTED ) @@ -67,7 +67,7 @@ class NewGradeNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.grade_new_items_final, 1), content = "${it.subject}: ${it.finalGrade}", - intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade, true), ) } @@ -79,7 +79,7 @@ class NewGradeNotification @Inject constructor( items.size, items.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade, true), type = NotificationType.NEW_GRADE_FINAL ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt index cdada844..fcaed10f 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate @@ -31,7 +31,7 @@ class NewHomeworkNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.homework_notify_new_item_title, 1), content = it, - intentToStart = MainActivity.getStartIntent(context, Destination.Homework, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Homework, true), ) } @@ -42,7 +42,7 @@ class NewHomeworkNotification @Inject constructor( lines.size, lines.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Homework, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Homework, true), type = NotificationType.NEW_HOMEWORK, notificationDataList = notificationDataList ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt index d9f138b5..ab2c1ca7 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt @@ -7,7 +7,7 @@ import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import javax.inject.Inject class NewLuckyNumberNotification @Inject constructor( @@ -22,7 +22,7 @@ class NewLuckyNumberNotification @Inject constructor( R.string.lucky_number_notify_new_item, item.luckyNumber.toString() ), - intentToStart = MainActivity.getStartIntent(context, Destination.LuckyNumber, true) + intentToStart = SplashActivity.getStartIntent(context, Destination.LuckyNumber, true) ) appNotificationManager.sendSingleNotification( diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt index cdb5ab9c..1a06cf6f 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import javax.inject.Inject @@ -22,7 +22,7 @@ class NewMessageNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.message_new_items, 1), content = "${it.sender}: ${it.subject}", - intentToStart = MainActivity.getStartIntent(context, Destination.Message, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Message, true), ) } @@ -30,7 +30,7 @@ class NewMessageNotification @Inject constructor( notificationDataList = notificationDataList, title = context.getPlural(R.plurals.message_new_items, items.size), content = context.getPlural(R.plurals.message_notify_new_items, items.size, items.size), - intentToStart = MainActivity.getStartIntent(context, Destination.Message, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Message, true), type = NotificationType.NEW_MESSAGE ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt index 16be1ca5..533d7f38 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt @@ -9,7 +9,7 @@ import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import javax.inject.Inject @@ -29,13 +29,13 @@ class NewNoteNotification @Inject constructor( NotificationData( title = context.getPlural(titleRes, 1), content = "${it.teacher}: ${it.category}", - intentToStart = MainActivity.getStartIntent(context, Destination.Note, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Note, true), ) } val groupNotificationData = GroupNotificationData( notificationDataList = notificationDataList, - intentToStart = MainActivity.getStartIntent(context, Destination.Note, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Note, true), title = context.getPlural(R.plurals.note_new_items, items.size), content = context.getPlural(R.plurals.note_notify_new_items, items.size, items.size), type = NotificationType.NEW_NOTE diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt index 1f603624..1efe13ae 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import javax.inject.Inject @@ -20,7 +20,7 @@ class NewSchoolAnnouncementNotification @Inject constructor( suspend fun notify(items: List, student: Student) { val notificationDataList = items.map { NotificationData( - intentToStart = MainActivity.getStartIntent( + intentToStart = SplashActivity.getStartIntent( context = context, destination = Destination.SchoolAnnouncement, startNewTask = true @@ -34,7 +34,7 @@ class NewSchoolAnnouncementNotification @Inject constructor( } val groupNotificationData = GroupNotificationData( type = NotificationType.NEW_ANNOUNCEMENT, - intentToStart = MainActivity.getStartIntent( + intentToStart = SplashActivity.getStartIntent( context = context, destination = Destination.SchoolAnnouncement, startNewTask = true diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt index 0521b4a0..075557a5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt @@ -1,14 +1,11 @@ package io.github.wulkanowy.ui.base import android.app.ActivityManager -import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.os.Bundle import android.view.View import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.app.AppCompatDelegate import androidx.viewbinding.ViewBinding import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_LONG @@ -40,7 +37,6 @@ abstract class BaseActivity, VB : ViewBinding> : themeManager.applyActivityTheme(this) super.onCreate(savedInstanceState) supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true) - AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) @Suppress("DEPRECATION") setTaskDescription( @@ -83,8 +79,8 @@ abstract class BaseActivity, VB : ViewBinding> : } override fun openClearLoginView() { - startActivity(LoginActivity.getStartIntent(this) - .apply { addFlags(FLAG_ACTIVITY_CLEAR_TASK or FLAG_ACTIVITY_NEW_TASK) }) + startActivity(LoginActivity.getStartIntent(this)) + finishAffinity() } override fun onDestroy() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt index 5cd5d010..40d54860 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt @@ -20,7 +20,7 @@ open class BasePresenter( ) { private val job = SupervisorJob() - protected val presenterScope = CoroutineScope(job + Dispatchers.Main) + protected val presenterScope = CoroutineScope(job + Dispatchers.Main.immediate) private val childrenJobs = mutableMapOf() diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt index b560ed2e..e934a182 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt @@ -41,14 +41,15 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer ) } - private fun isThemeApplicable(activity: AppCompatActivity): Boolean { - return activity.packageManager + private fun isThemeApplicable(activity: AppCompatActivity) = + activity.packageManager .getPackageInfo(activity.packageName, GET_ACTIVITIES) - .activities.singleOrNull { it.name == activity::class.java.canonicalName } - ?.theme.let { + .activities + .singleOrNull { it.name == activity::class.java.canonicalName } + ?.theme + .let { it == R.style.WulkanowyTheme_Black || it == R.style.WulkanowyTheme_NoActionBar || it == R.style.WulkanowyTheme_Login || it == R.style.WulkanowyTheme_Login_Black || it == R.style.WulkanowyTheme_MessageSend || it == R.style.WulkanowyTheme_MessageSend_Black } - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt index 0406afa4..43d4b5f9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt @@ -19,6 +19,10 @@ import java.time.LocalDate sealed interface Destination : Serializable { + /* + Type in children classes have to be as getter to avoid null in enums + https://stackoverflow.com/questions/68866453/kotlin-enum-val-is-returning-null-despite-being-set-at-compile-time + */ val type: Type val fragment: Fragment @@ -41,91 +45,91 @@ sealed interface Destination : Serializable { object Dashboard : Destination { - override val type = Type.DASHBOARD + override val type get() = Type.DASHBOARD override val fragment get() = DashboardFragment.newInstance() } object Grade : Destination { - override val type = Type.GRADE + override val type get() = Type.GRADE override val fragment get() = GradeFragment.newInstance() } object Attendance : Destination { - override val type = Type.ATTENDANCE + override val type get() = Type.ATTENDANCE override val fragment get() = AttendanceFragment.newInstance() } object Exam : Destination { - override val type = Type.EXAM + override val type get() = Type.EXAM override val fragment get() = ExamFragment.newInstance() } data class Timetable(val date: LocalDate? = null) : Destination { - override val type = Type.TIMETABLE + override val type get() = Type.TIMETABLE override val fragment get() = TimetableFragment.newInstance(date) } object Homework : Destination { - override val type = Type.HOMEWORK + override val type get() = Type.HOMEWORK override val fragment get() = HomeworkFragment.newInstance() } object Note : Destination { - override val type = Type.NOTE + override val type get() = Type.NOTE override val fragment get() = NoteFragment.newInstance() } object Conference : Destination { - override val type = Type.CONFERENCE + override val type get() = Type.CONFERENCE override val fragment get() = ConferenceFragment.newInstance() } object SchoolAnnouncement : Destination { - override val type = Type.SCHOOL_ANNOUNCEMENT + override val type get() = Type.SCHOOL_ANNOUNCEMENT override val fragment get() = SchoolAnnouncementFragment.newInstance() } object School : Destination { - override val type = Type.SCHOOL + override val type get() = Type.SCHOOL override val fragment get() = SchoolFragment.newInstance() } object LuckyNumber : Destination { - override val type = Type.LUCKY_NUMBER + override val type get() = Type.LUCKY_NUMBER override val fragment get() = LuckyNumberFragment.newInstance() } object More : Destination { - override val type = Type.MORE + override val type get() = Type.MORE override val fragment get() = MoreFragment.newInstance() } object Message : Destination { - override val type = Type.MESSAGE + override val type get() = Type.MESSAGE override val fragment get() = MessageFragment.newInstance() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt index ab6eec41..66e39fc7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt @@ -3,12 +3,9 @@ package io.github.wulkanowy.ui.modules.account.accountedit import android.annotation.SuppressLint import android.content.res.ColorStateList import android.graphics.Color -import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import android.graphics.drawable.RippleDrawable -import android.graphics.drawable.StateListDrawable -import android.os.Build import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.view.isVisible @@ -52,30 +49,13 @@ class AccountEditColorAdapter @Inject constructor() : } } - private fun Int.createForegroundDrawable(): Drawable = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - val mask = GradientDrawable().apply { - shape = GradientDrawable.OVAL - setColor(Color.BLACK) - } - RippleDrawable(ColorStateList.valueOf(this.rippleColor), null, mask) - } else { - val foreground = StateListDrawable().apply { - alpha = 80 - setEnterFadeDuration(250) - setExitFadeDuration(250) - } - - val mask = GradientDrawable().apply { - shape = GradientDrawable.OVAL - setColor(this@createForegroundDrawable.rippleColor) - } - - foreground.apply { - addState(intArrayOf(android.R.attr.state_pressed), mask) - addState(intArrayOf(), ColorDrawable(Color.TRANSPARENT)) - } + private fun Int.createForegroundDrawable(): Drawable { + val mask = GradientDrawable().apply { + shape = GradientDrawable.OVAL + setColor(Color.BLACK) } + return RippleDrawable(ColorStateList.valueOf(this.rippleColor), null, mask) + } private inline val Int.rippleColor: Int get() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt index 092c9d0d..c6ee60ee 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt @@ -121,9 +121,7 @@ class AttendanceFragment : BaseFragment(R.layout.frag attendanceSwipe.setOnRefreshListener(presenter::onSwipeRefresh) attendanceSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) attendanceSwipe.setProgressBackgroundColorSchemeColor( - requireContext().getThemeAttrColor( - R.attr.colorSwipeRefresh - ) + requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh) ) attendanceErrorRetry.setOnClickListener { presenter.onRetry() } attendanceErrorDetails.setOnClickListener { presenter.onDetailsClick() } @@ -134,7 +132,7 @@ class AttendanceFragment : BaseFragment(R.layout.frag attendanceExcuseButton.setOnClickListener { presenter.onExcuseButtonClick() } - attendanceNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + attendanceNavContainer.elevation = requireContext().dpToPx(8f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryFragment.kt index 118971e6..dd1644a9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryFragment.kt @@ -71,7 +71,7 @@ class AttendanceSummaryFragment : setOnItemSelectedListener { presenter.onSubjectSelected(it?.text?.toString()) } } - binding.attendanceSummarySubjectsContainer.setElevationCompat(requireContext().dpToPx(1f)) + binding.attendanceSummarySubjectsContainer.elevation = requireContext().dpToPx(1f) } override fun updateSubjects(data: ArrayList) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt index fb7939bc..ddd0e4a1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt @@ -64,7 +64,7 @@ class ExamFragment : BaseFragment(R.layout.fragment_exam), examPreviousButton.setOnClickListener { presenter.onPreviousWeek() } examNextButton.setOnClickListener { presenter.onNextWeek() } - examNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + examNavContainer.elevation = requireContext().dpToPx(8f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt index f9ecb681..0a8561ee 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt @@ -96,9 +96,7 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade TabLayoutMediator(binding.gradeTabLayout, binding.gradeViewPager, this).attach() } - with(binding.gradeTabLayout) { - setElevationCompat(context.dpToPx(4f)) - } + binding.gradeTabLayout.elevation = requireContext().dpToPx(4f) with(binding) { gradeErrorRetry.setOnClickListener { presenter.onRetry() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt index 0adac300..dbc4c10c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt @@ -68,7 +68,7 @@ class GradeStatisticsFragment : } with(binding) { - gradeStatisticsSubjectsContainer.setElevationCompat(requireContext().dpToPx(1f)) + gradeStatisticsSubjectsContainer.elevation = requireContext().dpToPx(1f) gradeStatisticsSwipe.setOnRefreshListener(presenter::onSwipeRefresh) gradeStatisticsSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt index a723b048..d4eaade2 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt @@ -67,7 +67,7 @@ class HomeworkFragment : BaseFragment(R.layout.fragment openAddHomeworkButton.setOnClickListener { presenter.onHomeworkAddButtonClicked() } - homeworkNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + homeworkNavContainer.elevation = requireContext().dpToPx(8f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt index f7bab526..70b54c49 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt @@ -35,13 +35,13 @@ class LoginActivity : BaseActivity(), Logi @Inject lateinit var updateHelper: UpdateHelper + override val currentViewIndex get() = binding.loginViewpager.currentItem + companion object { fun getStartIntent(context: Context) = Intent(context, LoginActivity::class.java) } - override val currentViewIndex get() = binding.loginViewpager.currentItem - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityLoginBinding.inflate(layoutInflater).apply { binding = this }.root) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt index 04e35d31..87cb505c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt @@ -66,14 +66,8 @@ class LoginStudentSelectFragment : } override fun openMainView() { - activity?.let { - startActivity( - MainActivity.getStartIntent( - context = it, - startNewTask = true - ) - ) - } + startActivity(MainActivity.getStartIntent(requireContext())) + requireActivity().finish() } override fun showProgress(show: Boolean) { @@ -115,7 +109,8 @@ class LoginStudentSelectFragment : chooserTitle = requireContext().getString(R.string.login_email_intent_title), email = "wulkanowyinc@gmail.com", subject = requireContext().getString(R.string.login_email_subject), - body = requireContext().getString(R.string.login_email_text, appInfo.systemModel, + body = requireContext().getString( + R.string.login_email_text, appInfo.systemModel, appInfo.systemVersion.toString(), appInfo.versionName, "Select users to log in", diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt index 3a84b2dd..49d094b7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt @@ -65,7 +65,7 @@ class LuckyNumberHistoryFragment : luckyNumberHistoryPreviousButton.setOnClickListener { presenter.onPreviousWeek() } luckyNumberHistoryNextButton.setOnClickListener { presenter.onNextWeek() } - luckyNumberHistoryNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + luckyNumberHistoryNavContainer.elevation = requireContext().dpToPx(8f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt index 51585ac5..9ef41068 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.ui.modules.luckynumberwidget import android.app.PendingIntent -import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH @@ -19,7 +18,8 @@ import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.repositories.LuckyNumberRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity +import io.github.wulkanowy.utils.PendingIntentCompat import io.github.wulkanowy.utils.toFirstResult import kotlinx.coroutines.runBlocking import timber.log.Timber @@ -62,8 +62,8 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { val appIntent = PendingIntent.getActivity( context, LUCKY_NUMBER_PENDING_INTENT_ID, - MainActivity.getStartIntent(context, Destination.LuckyNumber, true), - FLAG_UPDATE_CURRENT + SplashActivity.getStartIntent(context, Destination.LuckyNumber, true), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) val remoteView = 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 92bc36bc..3969e1d3 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 @@ -3,8 +3,6 @@ package io.github.wulkanowy.ui.modules.main import android.annotation.SuppressLint import android.content.Context import android.content.Intent -import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.os.Build.VERSION_CODES.P import android.os.Bundle import android.view.Menu @@ -23,7 +21,6 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.databinding.ActivityMainBinding -import io.github.wulkanowy.services.shortcuts.ShortcutsHelper import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog @@ -59,9 +56,6 @@ class MainActivity : BaseActivity(), MainVie @Inject lateinit var appInfo: AppInfo - @Inject - lateinit var shortcutsHelper: ShortcutsHelper - private var accountMenu: MenuItem? = null private val overlayProvider by lazy { ElevationOverlayProvider(this) } @@ -76,13 +70,8 @@ class MainActivity : BaseActivity(), MainVie fun getStartIntent( context: Context, destination: Destination? = null, - startNewTask: Boolean = false ) = Intent(context, MainActivity::class.java).apply { putExtra(EXTRA_START_DESTINATION, destination) - - if (startNewTask) { - flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK - } } } @@ -109,7 +98,6 @@ class MainActivity : BaseActivity(), MainVie updateHelper.messageContainer = binding.mainFragmentContainer val destination = intent.getSerializableExtra(EXTRA_START_DESTINATION) as Destination? - ?: shortcutsHelper.getDestination(intent) presenter.onAttachView(this, destination) updateHelper.checkAndInstallUpdates(this) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt index 51076c64..a45ab623 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt @@ -77,9 +77,8 @@ class MessageFragment : BaseFragment(R.layout.fragment_m TabLayoutMediator(binding.messageTabLayout, binding.messageViewPager, this).attach() } - with(binding.messageTabLayout) { - setElevationCompat(context.dpToPx(4f)) - } + binding.messageTabLayout.elevation = requireContext().dpToPx(4f) + binding.openSendMessageButton.setOnClickListener { presenter.onSendMessageButtonClicked() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt index 74f8f57e..e1cc2e37 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.message.preview -import android.os.Build import android.os.Bundle import android.print.PrintAttributes import android.print.PrintManager @@ -13,7 +12,6 @@ import android.view.View.VISIBLE import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient -import androidx.annotation.RequiresApi import androidx.core.content.getSystemService import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint @@ -25,7 +23,6 @@ import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.shareText import javax.inject.Inject @@ -40,9 +37,6 @@ class MessagePreviewFragment : @Inject lateinit var previewAdapter: MessagePreviewAdapter - @Inject - lateinit var appInfo: AppInfo - private var menuReplyButton: MenuItem? = null private var menuForwardButton: MenuItem? = null @@ -140,7 +134,7 @@ class MessagePreviewFragment : menuForwardButton?.isVisible = show menuDeleteButton?.isVisible = show menuShareButton?.isVisible = show - menuPrintButton?.isVisible = show && appInfo.systemVersion >= Build.VERSION_CODES.LOLLIPOP + menuPrintButton?.isVisible = show } override fun setDeletedOptionsLabels() { @@ -175,7 +169,6 @@ class MessagePreviewFragment : context?.shareText(text, subject) } - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) override fun printDocument(html: String, jobName: String) { val webView = WebView(requireContext()) webView.webViewClient = object : WebViewClient() { @@ -190,7 +183,6 @@ class MessagePreviewFragment : webView.loadDataWithBaseURL("file:///android_asset/", html, "text/HTML", "UTF-8", null) } - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private fun createWebPrintJob(webView: WebView, jobName: String) { activity?.getSystemService()?.let { printManager -> val printAdapter = webView.createPrintDocumentAdapter(jobName) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt index 702e5467..eb33ee6e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.ui.modules.message.preview import android.annotation.SuppressLint -import android.os.Build import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageAttachment @@ -11,7 +10,6 @@ import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResourceIn @@ -24,8 +22,7 @@ class MessagePreviewPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, private val messageRepository: MessageRepository, - private val analytics: AnalyticsHelper, - private var appInfo: AppInfo + private val analytics: AnalyticsHelper ) : BasePresenter(errorHandler, studentRepository) { var message: Message? = null @@ -112,10 +109,11 @@ class MessagePreviewPresenter @Inject constructor( fun onShare(): Boolean { message?.let { - var text = "Temat: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}\n" + when (it.sender.isNotEmpty()) { - true -> "Od: ${it.sender}\n" - false -> "Do: ${it.recipient}\n" - } + "Data: ${it.date.toFormattedString("yyyy-MM-dd HH:mm:ss")}\n\n${it.content}" + var text = + "Temat: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}\n" + when (it.sender.isNotEmpty()) { + true -> "Od: ${it.sender}\n" + false -> "Do: ${it.recipient}\n" + } + "Data: ${it.date.toFormattedString("yyyy-MM-dd HH:mm:ss")}\n\n${it.content}" attachments?.let { attachments -> if (attachments.isNotEmpty()) { @@ -127,7 +125,10 @@ class MessagePreviewPresenter @Inject constructor( } } - view?.shareText(text, "FW: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}") + view?.shareText( + text, + "FW: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}" + ) return true } return false @@ -135,7 +136,6 @@ class MessagePreviewPresenter @Inject constructor( @SuppressLint("NewApi") fun onPrint(): Boolean { - if (appInfo.systemVersion < Build.VERSION_CODES.LOLLIPOP) return false message?.let { val dateString = it.date.toFormattedString("yyyy-MM-dd HH:mm:ss") val infoContent = "

Data wysłania

$dateString
" + when { @@ -154,7 +154,9 @@ class MessagePreviewPresenter @Inject constructor( view?.apply { val html = printHTML - .replace("%SUBJECT%", it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }) + .replace( + "%SUBJECT%", + it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }) .replace("%CONTENT%", messageContent) .replace("%INFO%", infoContent) printDocument(html, jobName) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt index 583ba687..88fe77d9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt @@ -1,7 +1,5 @@ package io.github.wulkanowy.ui.modules.message.preview -import android.os.Build -import androidx.annotation.RequiresApi import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.ui.base.BaseView @@ -42,8 +40,7 @@ interface MessagePreviewView : BaseView { fun shareText(text: String, subject: String) - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - fun printDocument(html: String, jobName: String) - fun popView() + + fun printDocument(html: String, jobName: String) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt index 4571390b..9b76a7f0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt @@ -127,10 +127,13 @@ class MessageTabPresenter @Inject constructor( onlyUnread, onlyWithAttachments ) - val newItems = listOf(MessageTabDataItem.Header) + filteredData.map { - MessageTabDataItem.MessageItem(it) + val messageItems = filteredData.map { message -> + MessageTabDataItem.MessageItem(message) } - updateData(newItems, folder.id == MessageFolder.SENT.id) + val messageItemsWithHeader = + listOf(MessageTabDataItem.Header) + messageItems + + updateData(messageItemsWithHeader, folder.id == MessageFolder.SENT.id) notifyParentDataLoaded() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt index 9892b77a..f4fa8e01 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt @@ -77,9 +77,7 @@ class SchoolAndTeachersFragment : ).attach() } - with(binding.schoolandteachersTabLayout) { - setElevationCompat(context.dpToPx(4f)) - } + binding.schoolandteachersTabLayout.elevation = requireContext().dpToPx(4f) } override fun showContent(show: Boolean) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt index 43920588..dd6cf0ec 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.settings.notifications -import android.annotation.SuppressLint import android.content.Intent import android.content.SharedPreferences import android.net.Uri @@ -152,7 +151,6 @@ class NotificationsFragment : PreferenceFragmentCompat(), .show() } - @SuppressLint("InlinedApi") override fun openSystemSettings() { val intent = if (appInfo.systemVersion >= Build.VERSION_CODES.O) { Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt index 376ef374..b27f06ef 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt @@ -1,29 +1,60 @@ package io.github.wulkanowy.ui.modules.splash +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent import android.os.Bundle import android.widget.Toast import android.widget.Toast.LENGTH_LONG +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.viewbinding.ViewBinding import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.services.shortcuts.ShortcutsHelper import io.github.wulkanowy.ui.base.BaseActivity +import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.openInternetBrowser import javax.inject.Inject +@SuppressLint("CustomSplashScreen") @AndroidEntryPoint class SplashActivity : BaseActivity(), SplashView { @Inject - lateinit var appInfo: AppInfo + override lateinit var presenter: SplashPresenter @Inject - override lateinit var presenter: SplashPresenter + lateinit var shortcutsHelper: ShortcutsHelper + + companion object { + + private const val EXTRA_START_DESTINATION = "start_destination" + + private const val EXTRA_EXTERNAL_URL = "external_url" + + fun getStartIntent( + context: Context, + destination: Destination? = null, + startNewTask: Boolean = false + ) = Intent(context, SplashActivity::class.java).apply { + putExtra(EXTRA_START_DESTINATION, destination) + + if (startNewTask) { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - presenter.onAttachView(this, intent?.getStringExtra("external_url")) + installSplashScreen().setKeepVisibleCondition { true } + + val externalLink = intent?.getStringExtra(EXTRA_EXTERNAL_URL) + val startDestination = intent?.getSerializableExtra(EXTRA_START_DESTINATION) as Destination? + ?: shortcutsHelper.getDestination(intent) + + presenter.onAttachView(this, externalLink, startDestination) } override fun openLoginView() { @@ -31,8 +62,8 @@ class SplashActivity : BaseActivity(), SplashView finish() } - override fun openMainView() { - startActivity(MainActivity.getStartIntent(this)) + override fun openMainView(destination: Destination?) { + startActivity(MainActivity.getStartIntent(this, destination)) finish() } 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 03e43efa..0b740902 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 @@ -1,12 +1,10 @@ package io.github.wulkanowy.ui.modules.splash -import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler -import io.github.wulkanowy.utils.flowWithResource -import kotlinx.coroutines.flow.onEach -import timber.log.Timber +import io.github.wulkanowy.ui.modules.Destination +import kotlinx.coroutines.launch import javax.inject.Inject class SplashPresenter @Inject constructor( @@ -14,7 +12,7 @@ class SplashPresenter @Inject constructor( studentRepository: StudentRepository, ) : BasePresenter(errorHandler, studentRepository) { - fun onAttachView(view: SplashView, externalUrl: String?) { + fun onAttachView(view: SplashView, externalUrl: String?, startDestination: Destination?) { super.onAttachView(view) if (!externalUrl.isNullOrBlank()) { @@ -22,15 +20,16 @@ class SplashPresenter @Inject constructor( return } - flowWithResource { studentRepository.isCurrentStudentSet() }.onEach { - when (it.status) { - Status.LOADING -> Timber.d("Is current user set check started") - Status.SUCCESS -> { - if (it.data!!) view.openMainView() - else view.openLoginView() + presenterScope.launch { + runCatching { studentRepository.isCurrentStudentSet() } + .onFailure(errorHandler::dispatch) + .onSuccess { + if (it) { + view.openMainView(startDestination) + } else { + view.openLoginView() + } } - Status.ERROR -> errorHandler.dispatch(it.error!!) - } - }.launch() + } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashView.kt index a5aa1409..1c5d8bfd 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashView.kt @@ -1,12 +1,13 @@ package io.github.wulkanowy.ui.modules.splash import io.github.wulkanowy.ui.base.BaseView +import io.github.wulkanowy.ui.modules.Destination interface SplashView : BaseView { fun openLoginView() - fun openMainView() + fun openMainView(destination: Destination?) fun openExternalUrlAndFinish(url: String) } 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 4478a2a6..07a9f6c7 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 @@ -97,7 +97,7 @@ class TimetableFragment : BaseFragment(R.layout.fragme timetableNavDate.setOnClickListener { presenter.onPickDate() } timetableNextButton.setOnClickListener { presenter.onNextDay() } - timetableNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + timetableNavContainer.elevation = requireContext().dpToPx(8f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt index 47bee1e3..e8fb9e44 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt @@ -72,7 +72,7 @@ class AdditionalLessonsFragment : additionalLessonsNavDate.setOnClickListener { presenter.onPickDate() } additionalLessonsNextButton.setOnClickListener { presenter.onNextDay() } - additionalLessonsNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + additionalLessonsNavContainer.elevation = requireContext().dpToPx(8f) } } 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 index b8da1c0f..a6b12644 100644 --- 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 @@ -79,7 +79,7 @@ class CompletedLessonsFragment : completedLessonsNavDate.setOnClickListener { presenter.onPickDate() } completedLessonsNextButton.setOnClickListener { presenter.onNextDay() } - completedLessonsNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + completedLessonsNavContainer.elevation = requireContext().dpToPx(8f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt index 63fc8496..2c8c5a67 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt @@ -2,7 +2,6 @@ package io.github.wulkanowy.ui.modules.timetablewidget import android.annotation.SuppressLint import android.app.PendingIntent -import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_DELETED import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE @@ -25,8 +24,9 @@ import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.HiltBroadcastReceiver import io.github.wulkanowy.services.widgets.TimetableWidgetService import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.AnalyticsHelper +import io.github.wulkanowy.utils.PendingIntentCompat import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.createNameInitialsDrawable import io.github.wulkanowy.utils.getCompatColor @@ -167,18 +167,20 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { action = appWidgetId.toString() } val accountIntent = PendingIntent.getActivity( - context, -Int.MAX_VALUE + appWidgetId, + context, + -Int.MAX_VALUE + appWidgetId, Intent(context, TimetableWidgetConfigureActivity::class.java).apply { addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK) putExtra(EXTRA_APPWIDGET_ID, appWidgetId) putExtra(EXTRA_FROM_PROVIDER, true) - }, FLAG_UPDATE_CURRENT + }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) val appIntent = PendingIntent.getActivity( context, TIMETABLE_PENDING_INTENT_ID, - MainActivity.getStartIntent(context, Destination.Timetable(), true), - FLAG_UPDATE_CURRENT + SplashActivity.getStartIntent(context, Destination.Timetable(), true), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) val remoteView = RemoteViews(context.packageName, layoutId).apply { @@ -222,16 +224,16 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { code: Int, appWidgetId: Int, buttonType: String - ): PendingIntent { - return PendingIntent.getBroadcast( - context, code, - Intent(context, TimetableWidgetProvider::class.java).apply { - action = ACTION_APPWIDGET_UPDATE - putExtra(EXTRA_BUTTON_TYPE, buttonType) - putExtra(EXTRA_TOGGLED_WIDGET_ID, appWidgetId) - }, FLAG_UPDATE_CURRENT - ) - } + ) = PendingIntent.getBroadcast( + context, + code, + Intent(context, TimetableWidgetProvider::class.java).apply { + action = ACTION_APPWIDGET_UPDATE + putExtra(EXTRA_BUTTON_TYPE, buttonType) + putExtra(EXTRA_TOGGLED_WIDGET_ID, appWidgetId) + }, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) private suspend fun getStudent(studentId: Long, appWidgetId: Int) = try { val students = studentRepository.getSavedStudents(false) diff --git a/app/src/main/java/io/github/wulkanowy/ui/widgets/FittedScrollableTabLayout.kt b/app/src/main/java/io/github/wulkanowy/ui/widgets/FittedScrollableTabLayout.kt index 0f121dc5..6b7fb4aa 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/widgets/FittedScrollableTabLayout.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/widgets/FittedScrollableTabLayout.kt @@ -3,17 +3,15 @@ package io.github.wulkanowy.ui.widgets import android.content.Context import android.util.AttributeSet import android.view.ViewGroup +import com.google.android.material.tabs.TabLayout /** * @see Tabs don't fit to screen with tabmode=scrollable, Even with a Custom Tab Layout */ -class FittedScrollableTabLayout : MaterialTabLayout { - - constructor(context: Context) : super(context) - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) +class FittedScrollableTabLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : TabLayout(context, attrs) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { setMeasuredDimension(widthMeasureSpec, heightMeasureSpec) diff --git a/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialLinearLayout.kt b/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialLinearLayout.kt index a04922e5..4e1ca1a9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialLinearLayout.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialLinearLayout.kt @@ -1,24 +1,19 @@ package io.github.wulkanowy.ui.widgets import android.content.Context -import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.LOLLIPOP import android.util.AttributeSet import android.widget.LinearLayout import androidx.core.view.ViewCompat -import com.google.android.material.elevation.ElevationOverlayProvider import com.google.android.material.shape.MaterialShapeDrawable -class MaterialLinearLayout : LinearLayout { - - constructor(context: Context) : super(context) - - constructor(context: Context, attr: AttributeSet) : super(context, attr) - - constructor(context: Context, attr: AttributeSet, defStyleAttr: Int) : super(context, attr, defStyleAttr) +class MaterialLinearLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : LinearLayout(context, attrs) { init { - val drawable = MaterialShapeDrawable.createWithElevationOverlay(context, ViewCompat.getElevation(this)) + val drawable = + MaterialShapeDrawable.createWithElevationOverlay(context, ViewCompat.getElevation(this)) ViewCompat.setBackground(this, drawable) } @@ -28,12 +23,4 @@ class MaterialLinearLayout : LinearLayout { (background as MaterialShapeDrawable).elevation = elevation } } - - fun setElevationCompat(elevation: Float) { - if (SDK_INT >= LOLLIPOP) { - setElevation(elevation) - } else { - setBackgroundColor(ElevationOverlayProvider(context).compositeOverlayWithThemeSurfaceColorIfNeeded(elevation)) - } - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialTabLayout.kt b/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialTabLayout.kt deleted file mode 100644 index e19d0111..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialTabLayout.kt +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.wulkanowy.ui.widgets - -import android.content.Context -import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.LOLLIPOP -import android.util.AttributeSet -import com.google.android.material.elevation.ElevationOverlayProvider -import com.google.android.material.tabs.TabLayout - -open class MaterialTabLayout : TabLayout { - - constructor(context: Context) : super(context) - - constructor(context: Context, attr: AttributeSet) : super(context, attr) - - constructor(context: Context, attr: AttributeSet, defStyleAttr: Int) : super(context, attr, defStyleAttr) - - fun setElevationCompat(elevation: Float) { - if (SDK_INT >= LOLLIPOP) { - setElevation(elevation) - } else { - setBackgroundColor(ElevationOverlayProvider(context).compositeOverlayWithThemeSurfaceColorIfNeeded(elevation)) - } - } -} diff --git a/app/src/main/java/io/github/wulkanowy/utils/PendingIntentCompat.kt b/app/src/main/java/io/github/wulkanowy/utils/PendingIntentCompat.kt new file mode 100644 index 00000000..45ee431a --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/PendingIntentCompat.kt @@ -0,0 +1,11 @@ +package io.github.wulkanowy.utils + +import android.app.PendingIntent +import android.os.Build + +object PendingIntentCompat { + + val FLAG_IMMUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.FLAG_IMMUTABLE + } else 0 +} diff --git a/app/src/main/java/io/github/wulkanowy/utils/StringExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/StringExtension.kt index 5c888f30..bddd7df4 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/StringExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/StringExtension.kt @@ -4,6 +4,4 @@ inline fun String?.ifNullOrBlank(defaultValue: () -> String) = if (isNullOrBlank()) defaultValue() else this fun String.capitalise() = - replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } - -fun String.decapitalise() = replaceFirstChar { it.lowercase() } + replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt b/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt index 74ae1932..c994ebab 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt @@ -2,10 +2,8 @@ package io.github.wulkanowy.utils.security -import android.annotation.TargetApi import android.content.Context import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.JELLY_BEAN_MR2 import android.os.Build.VERSION_CODES.M import android.security.KeyPairGeneratorSpec import android.security.keystore.KeyGenParameterSpec @@ -116,7 +114,6 @@ fun decrypt(cipherText: String): String { } } -@TargetApi(JELLY_BEAN_MR2) private fun generateKeyPair(context: Context) { (if (SDK_INT >= M) { KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT) diff --git a/app/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml b/app/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml new file mode 100644 index 00000000..dad56a17 --- /dev/null +++ b/app/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v23/img_splash_logo.png b/app/src/main/res/drawable-v23/img_splash_logo.png deleted file mode 100644 index 61489d81b973aae9ed13341ea9bc0f0902168b82..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19474 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4mJh`hA$OYelajGa29w(7Bet#3xhBt!>lJ4}r;Fna-R}9C&vQ4QXFtHfGtI*|(6oVPrIVcCgXZH$jy-&$=g`Qd zQotaExux3<0_f3F`IFE;BUD{V}teks*4DqA25n zi3|$DsU0p18P*JI&WClmGDH|Kh@^QRv1GWR%V6Lcp60=@VLQWta{}C(7&v$s3<5h^ zH5pi{84^w?D_1bI%w!O8`xI{TQG1=hb1epjikX{icB)vqHb!&k7P5zj>&a=H^Ay(> zF-v4hoaLZ*sB)&HQKJd}gU@Fe7#2(v6@1YA`ESKJzIEr$ne}bsi`RX%pY6X=Qqtpp zPtUI|aA06~SW!-AK$!ao;)!p3#s zgq=5UK7I1!hSw3N=)_auwg1u&#UBX&oAa~I=Fj)95B5D$X*7y$bZ7bT;Lw>bK1*fJ zPvUGzy7R2x{m*vp|9@mJD;@Dn+8LoG@;Feau+L>y&u62hN}W!VG>@uY`k(UXzRquE zyZfdL8?x*et0(HNh)rhx=B+LeQF7qXcLs(}yYmlT(coZi$Y_3ezWo1t`@ilBR2dr5 zJSP=0FvPj2=!KlxY=4}Cfx%@#qt*sT=`RN*bPlk|9Av$7ko!-9S&Ng(kwZ=bPFyJs zGDQiRYnq%i+Rko})k@I6;vj2sATXnCasi*-q0k+CwgvpZ5;^7_)Y#L=$>Ml|LpV|K zRtIBJSA=pOhxp9aABqP(ScJM&6i#-C1S)w?H1#kH>IzW^^^pB!Tf}799^kJ-ZK7glHR-|Dw> zK5lT}g#^3tksJfPV~3qKTIg_2PCmU+X${ZnV^SN!)-dnx{F;1zV>ydZqw4`qGnR5E zg+w_=j|iU#zZ)Ja6l7FP1aEPgb27IC9#U7Dyuy2n+AhJLME}D8-DnmTud*=EVCA$RgPL=bUej)tQ>6h*=roUkR zQqIHMCfaNQnusv-zs_doG3wOJe@6TKQZpOTQaSii)@|WMM-242OOs)RkkiWUVmVb5UiD28z zqr!HZ=bVJiQPaYF8(ID1w?76t#~yZYUYa;Har(oSi90XaEWYa2>&D-cc`W9!%H!N) zxyKf(v8gGldHcTdeK#w}cjq$AW!65{KF80VIlJiWwV9@~Z4Iv*J)ix2_VwBKwv291H=cXr7%Oc-x+mqKCWf{3NA~Y&?v)kIaYgMo9Ub}m_;PRb*ll@lDyOw@< z!^dqWx2@bRx#8&zw%hBrnRA0@Uv^IyPv2SG{JEpM)4S=k z-gG;?+0w`Ep4j`utK@58?8B-{U+2!=Jp0hw6}Q*FGkcf$uKmvZGbZyj=S3P9UpRci z`9a{*i}u{}ry6A6UsLDr?BAHa^|`Wsvi`*IwbwUnUs}HEd*XX>yKiziwlTkF zbT2agjQN4>C$^vVKb$`=zV*Iyz4HH*|5g7lGAwUUWlU%8Y;S?Vr%gfn!|Igdc?T_X@;(lB$K12URY(eG1bBDgPE*D#=C!}LF z&B3R`&tl34M**!BVm+##gv(X?xGE3H9!gizzi4)`CP7Ql@s>(@cP`HZ8cA9R+&Psk*(EU!{$j7ClorZQuLj?E&Wycv$V3)Yf@V` z{X3f1yPFp=s`|R5@n@`=I zU_R&l{Qqx-4hcP+v~6jdcet)>#IwC?vMe{*6jt8mdMwJS-4@2S#%4#?Hm%&N{;S`H zPhVHJSFOr7yD{$IsiuQl8M9w=hjKk_t$lsx`rBCl2>rmX)iI>`XbIb}8-UvwOPpqWPw+n*MA$UmowxTU&VVhi!TL z^_%N${pn{lZtrg1=Dc(9ew!V8QkS1Rch=fIWJ$=EkY6EY?=Iclnt1(3-oR2DUt<2tuFSXX z`pzp?WiQ!=1odCje(lb8Tz=obx$nz$dCho3d*>W`q+H-q`Q+ty&imixc&%=g^;EBU z$bMPAOg3FsKg(&>r&(^ZgJ=DYPKw^;f5z_OwO9A5{1XyJxN28I!&9UDe*L)oI^fO3yPtclXQQ%8z3IuU(yI zX?u6?=c@egN56l)FLt-CNd51*)4Ok$tG`>j$A3@!7l*fwH=XacU$u|^cjw0YJM~!y z$`^dTc*p%aTWj03=E|P3=FRl7#OT(FffScPl`Y4#=zic>*?YcQgQ3e-SQo>p;sF|exG!Y znUk~S0@Fg376B_2k(aT%6gWJl_Hk$|h;R^LWDOCSut*^^)3Iuz-qnDXfF<9jwX}SB zQdj@xWIBiD_j$$3-pu^|-KNsvjPHNzb5XY%2BBn8VnnvHoJ- z+xX65_sxIrQWAJR2!1g8p!GrML)54D@=|_V!zOb!sH85^HDLK+wLx!#(gvTox94!} z;pt&}$C~FU#B!3wVN#KqzzwMz(h<3R>c8E7GwtBsq4o2*#9@752G3Lfj@4UiFy4^D z_q$$d2h$H>gL{V+g&90&ZI?SK{EH_+C*U-Hiv0tP2i7OQeflO;!1f{hkT&y47Kfmx zTUpJRwy}mWiOtnzlwjV&cZc^5Tg|2>dpOMGz$Mo-Q;&jf2B{uUmh%<`$ z{o5P;>3nOWo`d=V@{pPv;NGzc0<&HF+w(DbHo1M zYz1-!iVtmn&k%QiFYk)+rW;peX-qdm9%XP&m}_{h!1AC;#V9Ww??Y zRpk78ih=FBmVEcjm#{BCW-?}%x zoqQzO_t5UIqDnSt=8V5pJ*W6EsH{A?i+MIfw7{;5?F@6d|J2@RxaU1-a@AbkKdcg* zI-(y9_w*X%B{nVJ%-NuF)?YtCHbMQ+ft^PimT?_R47xOXgAeDeBHj)9`}hnPCoI`1 zG2@QPHpVu|zDqwf6Tdb2FsN`g2p(W;$$NCJZSj^gfvS>s1|n(r{_K9MFy?ROJl_~RR!u8AAlP!qqFb`>RF!zd z)PNXIkm!NQ_hk>P-}^KCp3niiwu3AVK|kN}-{Gt|8=$dKdEYNX#t9)&a~q2ryE(ZZ zcx+z7#$(2##xUvBa_a)i#FJ5(ezpyvJk6X9ONt_A1pLfRcV75YB8$U^L1n6Y`U9>6 zZRdro5q(+HD$E!sgjl_8xG(7S(~JAk^bJck`!J}uzAJbY%@lrh(siSAMvN1#IJ{<9 zF3Gm_5YGXvLe+Hfev$pHjY~N%|J)_`tiYf77~^T?R$i zJdK7-grX-s=ic{qVdLDT*|lOIc%naHV#a%T{~`fuar#=E?&MHgIV zm>RKF2+YXx@Tyucd-mLyilQgj4!9oZYICxj&3T9U4o3`UkEx0p!=y_axeGYU_OGhA z$EK&Eqx5yzLj%SMS1hcV>)Ccmaf#k|ec>H$K8M# z3?8e_7PWj|<1NkQnr2nN_aV8O*@r=8>%FxHuC+M|?&i0UsJIGJ_G?uuhik2va>>{2 z$~`~qk6u{8636X>t1BD0pEM~=DHRHM=^y37`qSR}07K|2h z+&?@v$MC&LmhfR%vMFj#z_#*eXT{p1?+%oL5{%aD)JgK&O?g{Q!WTF6HWs!mnjvPy z614g&gWl(7`e(0a9q0oqn>?RET9PfZ@!P@F=iH~m8G>H(u)k9{^K!aL{bkO}iScG4 z-xxds)z2LGckj{z8)3)AC$c|kHd#y$qn76*uU&yYurA?QpP<2m*`#$OVAnT=^o)06un z6L)$vo7DepcrWC|pr2&1M6BAtb4DtYf@^yGgX0G_wK-)?eR)9hq&-L|S@MBTLb$Wy z)h9j=7!L>J=&|e6-rRPeGU8kKs;hGk=pA5j4$R?Wn)0t)w?=-)z1jOiHU}PHy!Yqv z18oLQE2ifR?Gjuo)6O&Re!RtAm_bu$Zo}zG@_L?ID;a)>eVDeeqkTO?NNQ}O_MV^K zr8`A#T%5h1vq7Z0?0|gZT;A4@$^8w7Io|&{F46bn2bV+WSMCJ;!wOfQSUpfbz}==4 z=f@l(D$5bWVIs-3GE|Q79D^IvJsTf}B`emj-r+4V7YXXV>nWjA&3o;eK5snZ3NJCX zy7y<}vnG}|?B?)Ju;=tKQ&D3GTC;Ed7Y2`6yRUO}|G48HapHNhb5Ptqrhw2jvu)0n zn2KDQ?!&Cdu#V{-!#!h_Zwwx(7w2s-nrUADa%x9L5AD zg{c>QIrxJFE*>xjC(y-L3wOq6=Jm1bneuEB*>GwF^M*x<6ZWMD7049qTR6d%L2I>Z z!&BboFaPH$o$31cOuw2TBL6b%vl&TaG(SF>Ey{ z7AZ^%R=vpP2oa1I(E3@+XEXcG4|z7X70RFA~Zs&mb?wHj}~QzwteR6-;p~_l$Uq7$^LD zF6_L3YX`IV=ZTyRU#7AwxM{HBh#kYf9ElVrh1#1H50Vcr5VB_H{&RxG;aAuBhU>h| zUatZlFdbG9X2^VJ%b+jGHuZw~frkeooE3!`Uap?SnHsp%=qHnfz>n<OC%F$89z-9g1_f5o>t6qPiE6L!yn@m@z&qV zH@JAS?Gml6_gXT8|MIqSWoZ>~7l@U8KP|_wGSSX+-l`=^_kQFvY-1|}N5PU~ueohl zbn-Pq80HJHt%zf@$+JjdQds)h;epeFlK~p+#*OR^^#ad6@6Y#0VNyt4?ELBR?v)Dz z+?Fx@XZ^=^$8p9)qoW&6usEE$x9Y%O2Y=^8ZTwmPU-x@HfYF6Sz&W0)bl^Z|v zG)tu@C9eDT_kexl^9J!fFHf^LXf3m6{w=|_HOTk_+rsGoQ=fSL4?J|~vN2D;55tm2 z(K9aC?%ucX#M_mXLKiq+cx+szY%YB7vcKoNH}2~GU-Z??|Ce)4{k7-eX%>gBD_ovJ z1{VTS-pL<`bFfcf*dVe&WrM{Ajtj~^FY~_ODq*y`K2yU)-SfEk$@%9e+dpnFJivay z`atx_-haJM_E#?s&)jOtvTDV2PVY5w^Y7G5nae*(`u&b4C#(G4PM%#q{mJG|Ih1|C?|^HY zl4-iosRv$0j1#z0WeN-m7!nuEl6qe9faS1)X!~V`r8}cJf3R*?yl}$4+6*B8z*6wU3^6atN||#~!B@IeVA! z_p9GkW-@rpIy+_3)^s6*%ZIf8Dy1z-_vCzjn;4MA z$1=1|e4oKtETw@RYRf8ztE<&-?t7=j+x&NyVI&LGaf&urVc&!K$5<(&F-BZD@B z8SiBuls-^?z@0Fw-E|UY!<2RD!8e{o9`NT~yTq+fYQdEXZ-HfuW%(Dr?PCaIyl1dw zlGITrhoHx`3_UpyzWsZ7HhvOkLr9HrgFVA!sSRsa9$3%2Qbm|SlaF8B(_yb3>pSx~ zlcdfXG6YraV!F;4ef7uB-;L{qwZ3{zvSkQTQDgYcV1E6>P3y+|iqF$im=un_OimDg zkaFnGAJ)o#gS+|GmsYAPcs8tOSfZmlUxj%#V}_>Zs0ECiHFKV|x_ z5WFxkz^p>ZKyLbjsVPhfo|7)6XeM~NZZg@xxk2$l{{`z$HV?m^aNe=35fo#WEJc3Q zO=3B7_U*~~`fCB6Y7CPeUFWutzOkLP{=061`Gdm4pk7JWb*TfcEs=t{Uu6vD7!(8r z^{!!7(A2-r^e*G+S@{Hx4f`KXe#1B+?K<-f&Isujyguq8*LTkTz!&-RHNzhni)jYD zo5b{0_cF|v@;k}ma7+A$;D$Rf|8AQ+V0=*V;C`Ey`Dw=etlRPqZ0lpGOZQL}W(d9A zdElk)k7{{_-9llngSTyCImgasl2N^w)7G|+bwbty35KZ$x9RTr$*jW`!=saR!|3uf zgWC7c*zWyd3}gCcv}Y1$!>ZRy!v6ggZIqYV=JSP7kGalMqw!8gyWlA;ZzINlqdbgZ zYabT3GfPYF3t7v0XWFOWUjm0Vot)&u5Mr3k9GmqcM*2aIl>KVf+x$C3BW#;%W%)|X zo`B3vmDuX98a?|$>zZRPx&E;2Xffb5=62@1%BVV%VTF>F;I4+(g#rsj*1p=#WFTa* ztZ&JhFn87G%__nSp$8rv&??PKR@ks-;hNig2j)I8Uzb#R=!>7H8pEoFr;V;s8|Ln7 z+qCZf1bK$*M(;!aY${^8aM4+5Cc}z9*Rn5^*_lr=)_lv zT(=kR;%qCe^843+NxR8#LuSX(0FTel_}h|9w=mwWVEAEhXxsl8A6_M8AL422i#r`( zD6{a^Vvr#x4qdVTFaKZ_$L)W$57gs!bDJ>TIA-9X#-KI*=*oJA2eS_>p18I8>64f7 z+}GMRxH(umE6!wCVH39S-}nFj6?_~U%G^fFvCoSGoRQB@>6o}iAP*l)VLrvCD)EYTxZqsZCCDRgGyRU>9FUQ?`%F`EVy;M z_=jBn>eyQh$EDd$vN#mZU+?_t&N2Ir?63P{8%1rtl-?G(aRKC=u3-6ut5$aQk~?%4 zeJfMgvp?*w6{CAXsl!PYhrp;wzt|Z1)Yd(+w+^oFZ~vFBen?6PJ+Va zuN2Q8aXGYlsW?yT8^#G;=?n#CTl+iqb603j+9vn#tbOp@#>G6%pq7jz;~kf`SO3rA z^#1SHzhLcYhTWAXjJHc0+%P!7;-J;`w4r$N^6#OqZk%&Juy*2>N`I%7$*I|b4+IV` z&?@3-=vor~DY?et!g|KryE_eT?S3J~HqWn$amwC}EgKouOL5&~R!CjVTww4);KOIO zyMGwh^(^^YUHSQZsQ;lK+$ZH2G?m{r@ zyS01~JetA zLmfVu6i^fVYMEay?;Y=o>-)Wq{jO)N&}y=fb%}W5addP2Bn}TXh9GWU^)-9Sv-|wF zD{Iz$|8H=J$z5xiD#vOgFExfsOgt=lEP2djCqGvEFuvzSD^Sh zzXflF&d-ByrhWci*%}bC{Kd78S2Xvia;|4m5oX9VyW5~VdG_0rEm7ZG3#=1+zbj=k zmwXc0B<^6u7~nIDWA(MW{Bzvjp8m3Iq2vy=CW&(lcHcv~4sAMcugy`7Vb!j)4ZAsm z-?WK-_-)I%N5WI*{pXUdV~H)Q&7j)iT-1TkmQz);MSfJ@6x_fe$#SffKeNPcRp`~&wEy|2m|=NR_?tdy%z z+GG*$5WyRGbi2`p-H8)88&0hvI?a#iCeb3{}$O`AixxCG% z7OmX5^QK0Kd;J3mr|>jMH`98q9tOAR3v{ammPN0+F;Cg2cW#nK3V%b=29Cr7Hy@w8 zoEEvv#5Qu$Z2JROTjC@|Zwr9>-@*){vF=WSli3@MHSSA2zdm)1t-9d%indih_y z1*f8gH`KY_sw#E8RVnqMXL;E|S@%1wTT+-72)*E06W&<1&w0kR2-alIWGl6*D)DPo z5wZbl45w~7I4#}3Nt&}~<%Pn%@)pN0@4m2qVY^UPo`)L4siOs_?u%OXe#qOC^>uZ< z--<^ol5UCII0EuX8k}m|GtDL+yva z2W|hahi-A)0DC?%nlWM-)7w4k9$h~b>8Z5fQ6{%c+f_!*mr+!Ug@H&h4g6ZmhQ|xIehM{eH#p zN8-l(o#J7$IPQ3?*!3gqSfK>#oLOJkRz%F=NM`h55IN3y`_7#lkw@3`DwTAK1s8Z8 z2s=5s{MIg}>5RL#Ja})%pToarqVuCV?XZQ~i&nOD%~502vN_IhWWBh-mM6J2f)zT8 zYiqZ+n%6gkHhex&$M>VeiFH@M&0p^2wgoNw*BUVfh+SLqhkMc;r;7h`x21AS{T`KE z{VB5SXXK$({aW+ht~jY}zQ`;A>WgnR6_dC60lF0&P zGdIb=N84&0%ohLe&nytSI&p(i%)NEY_r3^ePT6~+yr(0DDL`m0M=-`Jj-vf=I4mGxeU%71R3 zUQ)2namFs~nG7q^w1cA0{*tI+x-rZ#OHQAug-H0(@ zHIHweLFyNQ4~1Q~MAu)hk5f@msOk;xugV6 zjajWn8yHn{Z}T45A+mjcIiLTN-Opq$_ZM0vOswUOI0lL)-Hk#ULO5@mitbQ8`nLT1 zkC*k+N*n4dkCjRpoI1U1!!O3)pvrHBYTE?AueL+K3CrBuc^+v$8hi4 zC(S=w_bvz3RnJl@Jo~Tn9(W>>yv>#I){N&l4Vn>x25jz(dwH9E7);DxPv77x+^{?7 z&Obxe%;#w>nsX{8pFRz)_}j<~YG#=%FKdxxNd2ty!KI7y@-x=V=Zl`~c6tzOAz;A9 z{%nrMDHaD-6M=|bEpLCZ8yGLmm1il@3W(mWzF%|BdB(|mdbsi{A4r3W>sfad4wWo4 zn#uOZc4;m@TbXu5wYg7UvfMES{VGYT*e~MPBm@+-cgilQ;PIV?j zNDTj}?Mx4rAK0szTV3zOc~2>-?+=^WKBhAtinADMqfX?9ZTS-~m)8ha zoIb57r7bv(dzue}R?gf>Tb~yn`m*2cHJgl-8O=v=!>bqmGq^&;-e3Ij$Uc`-`TuQ~s<%%AH-5Mu z7ZqsL7a$s9#JHlN)Vr)EPff?`@(YPvxk8_9tX1iI&WCdZGl@>$+PGetZ6-s=;SQm# zscZ$&T(@ntJ}qBlcfe2b{b%0?vHI&ZWL@jLy-u?@NM?H5T|RQ@lUCx_{ccihIpIw= zYHS~?Jd<6+A+h>li=qA`PKQoiex2Aw-|n#YXDFfFc<%EE?-?Q2FJW&;U z`~GYB-@DT|9d@quc_F9Qoh`LzuP8_Nt0|7*o8F(SIOeSrB`tKU&qIwtb!${--`Va1 zk9qGsJ2)k~^+>)@-0zg9no{w-fj2Xh{%zDZV!U9pMe$ta+s{SH1{)i1M>hq0-|rK~ zvTs^pkl8tJ-6-ja=bM6z7z0j;=>;+wNbXpC@U~4y(O$WkQ@_0RE6bMbVW0Oos5{IW z+&yae6e$_8L?ySf@mSga?~LDcUl!hPy0Cj<>&yrNjk4K{vq2@sl+$dgS*LRkEMd#b z=TDe9N$&g7hF;@4^O+eR|9@*YE$4XO2^I&<==CaZGnQ@K`hT~;sejWVrzK0rF)pjv z>6&|ak}W8ST@q8g!l2RY&e?ocD%JngR%ZQF`WY#YDW6iufb<6tinSU;wId!jKleCSNFvHS}f+FFLyAcoGvE}6ppW1Hn zX-^J&$@v6(5ULJ84U@NblUVZNaVQ8WSbwv7cko@WH`a1Q1m*e&vI}7r5_ico)0&Wykpq>*XKwTL+XY(iXa=@ zrYw>Dzx3nXQ_9*Ec5kLQI^E#0d9+k~Z(U2#rAeF)OO7W-c;1?HOe?iNXs_uTbCub< z*sn9sujX{iEj$%$7o)~7)wto)ng8+}Tk=1c*|WV{bHnnY@C~cxzdcOnJ_+^MF}_Ha zNMRCKe({;8U%iJ$=!OB84{Y}@mHZBdm z@vb@Kz3G93O)J0JFrGea*gVHLb!8TV?;dE7%oZ%9y0r?Jh!nS zxG`G0qVLVz$NTnsRI#)%-#--2?7EfbX@rN58pG60b)2`iM0{$V(k9(_UaPDx$LHVE zxaExD0&$*iSZ!V|jn;^5NCnjyk1kIAz_lQ!YGPb$v-!$<9$(j`SW6Vt9b*nx>wI0O z#?*RI?mEi^PKS^-2Ja~wVskyV+x>hhY7@Rqaqe}kuU|O+NZ#PNF+Cu_#uO%jiC4YvY;!c(r}9BZ zo@smNo0v@gH&GAXn@jExxglaP;nms)5iH)1d>CdjxGXe&I{UwUI@5$_E1t~exO*XV z6ZeI+5AGMr6c}#cE8y5Cb>dQz13##wd-U4dO7wGAQsAZ72QTCvT>(@W1-0Yol{q2mKRV;lB^FCkl_mu~S zci@c$Ou0V0+aI&7+TL{6?TBS*3Dbw_2iXV88^r%|X6)G3q7*ZU)1lE+)s`W3yVt4P z^^LEtu6oa!Bl+?Be~}#x2LGOV2sgGBt2OvAaOGuQiZt4u@oVxshXW@=wwn~Zx&Dsj zL3xW@uX1DfpU8|3QJzL0hOS(W#3Ok(b=`I`GcvAI@3C1w>1so%_oZq5lU)*YA4tgP zM67SL5DidcX!U2f6}Dh*_oO=izluK(BO z!*HcR{{2?1E7uq$-#lM0YSX_>!ADhhc7J2>r0gdZ4^r!mqL#8h)zQ}zZnzY)AUk}; zUhUKk{l6Q_waVmpm(20@JpW0uKx{+eCFKvh>}7Q?DO#GV2sbcEIc#0_ibv^h75jU} z@~}5HSxoyP8_ViueBoHbJEvtmo7fUwJuVgDhC@<;8^7_XtqGmCZT8AI--fd*kL3t8 z_&3B0yh@+r@Kvliix)I(H-kas(4!RN#}D-$^mZNHCjFteYwJ(j2R?@+8YgR2oQY17 zbXE{oRWto>1rcATp21Zhb=2*52ti!i5@x z8y+6;Z{XLu_K$HMlM$mvpKIen(cJ3zP0};}Fnw@gP2RJ6`i+>qtbarz*ng;Atu#$A zRG7&ia^h9X_pPB<`f`@5#l{_!$Y)*iGS%$9%LA8$+6T`MxPyz4FtZgEzcf8Dk z%sFg-6kkg2n06udjrHU8RZKUge$Y}i`&$6Yvqh#2Z&p1x`oU$=)72C78|o##{d~pr zhUpLIj{Xm`9z>^XZYkEe=T*g==Jsvx??;Oqj2KT`{nhrs+kc*S+Me^X8#vqcx&C5~ zXa6_J=Hoxs8^RHXE`0Dmls)-I6@w2$3e$we58EDia~?2~eE(VUfu{5Qti6-wHE1`o zbADwlW2*``ku&aCZ@zLiXM+#Jr^^%Hu<06X>PZ2A6+PL+ zRz}F=sqbrY^sfGOibX-bnd5H3ulb@yH=cL5?M?h8n8yD{wt{8H41=J3>8!W@aeXLB zxqowIgAwD2lLC+Q)h4+GT~T{nyUp;;$_T+5@;fFQ+5xGkNnz}@Bkc5X4H$(#<)!n%LB>*YpwaPW?{>>p3l-spo*o01&$U`-UcJ0uD$$iG`{4P>vtI-s zv>!O-@S9`b@7)KI1&{uEec;}_qFIwqGcJEV|A1Rt;yO9?5RW+%AKC2^U|Ss7t|({R zz|O(U`F{D0;CE%p>m(T;bE?jN|H)*sEyK$w9`1rO{_>aB_ir%QtCpT^)^f0EzZ%b$ z-Qqv=5+7t-blYUlutz>HXSd)gt98#a)r1>Lcv;2Fc|QA2U|X`|@4N$nKkgZ)?M?G$ zi&N1FD&g1=x;zQ%>h|fYNr{(C)axB&148sTee!djn!+lN&&yq_Kl}j$}jx&;O>E0mi5!C z8_bn^9tu2IA5xyEK9ga|GV2Z75+fE&^?bhf-M@%`TezmNuT#9g)yz(@hqdEk)d5w9 zaDP={2GesvOMchw)?vH&!-{in<6++1R33)D^FOU)L<|3HW7kvYTRGi`vE$ILwgpH1 z3v8wLoUP?FeHkiiJs~&qrKK&~9>Kyt`#9&k%}ilhaO}nffh``oUb;&BT%Q#w zb;IKL-+sj_x*azKTNe2kF$T!qN`5%=T1Lv6ea;ns7iDEK=S=%tzJqy&Sfz|VQ{UGT zBgTNYVNAcI+r*@AEqMEdna^y_)UC2V_pwz7RIFtw{3BY>uzWISL)K=k4I2_OHoDnv zlz(u!e(6{B`!mhw3olbWwvM+zPH%oL(}G!R+O9s2O0H_2_fPicdfTFV|F5ccCZ|78 zauRi$$*`i;>d~paKXS|TbN<1Prh(N z*i1)_CqezGoUQdsR2zMZ?)T|^aH6PnJJfCF8cQ zH9jOp`9yixiHrxGla7_eKPd4x6lSP3W!zA`CRLBoZuR5O%?4Ye4J@V^`wej=)EvZeX*MTB1YhIBWL+E8b zc7|<7gb&1)*3|hXlpkIZ7SUA0&g{qR7N&B|TS$yQ>e}UVEDOBY)a*@W?f2#gy_EIU zc23&LYne)Gcqcsnbmu!$vX;<$M^B*#w`9~9rfyXGc}=eW;Oi^fU)?WAa6i04D#vr* zX2yB!^Hv4yxz4P2xN(;ngXo26e@^!ud*FZId8y4&*~gdnXI+^dAZg%#JzVEmG=sQM zT>mMS1zWx`ZZqTg{FO1WVS@kvQ{MMBGv=}GTM=>QJ42oO3@r`ehD(whbJ8TlUZfq; zZit%e@FH_!)>gTn>(c+5edTTFy?XG#%6WBa45^pJEV$Ws>zpuorTu5Kq?lyjUorLj zHyMl@qqU@d3pdJFaLr!aO0^-&MFFLbmb!lIT}VS^9rA%YHC* zUcq*{`qaDF1ItDG>ir*_Z@Hyu#Mp5v;DN~D3rg#Qe&2cC*HZp}-t??3TTgD!dVah` zvoUlVOM&i&Mavg+I_Ow2JZG}yaqW&_=~@4A^Xz2vg_kx;{QUOdtdCH`Oi_Ej(@f!$ z-(T^W$*|={;gkRW48-Iau3s(G`}Hn9^HjCjfxj)g7(%y4T@t=2I8`rfRVp-6!i)laGF6l0@nOuJs_C?62 za!lQH@4zw3`u1#Fp8xsL5BekCA7)w5u+LvZdDXO?jT?SNuC-@5%)8L&L+_mL{~12? zwLkp-)^)|2tdse|4TmHgBbwuM>woQEa_syz?@79v+}VuYPj~z`Y%w+ZV(-qVF3l#~ zp!C8~;_LmB0b6b)AJ|)J!^mUYa>_&g)B5Ly>;`w8KdRa@_L=agF;xCm+^{P#V`_KS zAIH~z8XHtnGmAgPuKHHzRGCnFbLIha-eyjRxNRTWu4L&m*02A#*_t_9!YMT;IN_ws zcl*9yUa8YV*&ie&R68d!P1qXO68K-1*LC&$f9lsh?DRYEvCZEyV#c4-zShzS@2r@n z3m!{6$)aFv&&np<7FId?7We-*?h&zpr=0JtzHICF!uE2wuG0#!7dI4y8+>xOWpX5< z-nueTBpLxxxi?dobZK<5eaOX+ljOD76R1y{}G+^|&{HP_hPS@?C)#S#{*?-=c zZ`|(s^Of(v&5Z9CfJ2oDN!kN3X8TDr7d;)pov=z0b5ptKsa)?F?rZ zo5neZZW3FVQsD2L$aEoNH{<e>T{1LhnF6DBP4yRL%b{wPnpJ zQrx;@0aQns6dl9dKk8Z9y z=lL>a4(|o2AHpWryp}&*M-f?ru{bkssA>INnj>3N3&ZBQ@>F*jbeePTM>N8(TT>C6AidFFGi(f)a! z?T56%Vdsllwj8h7*5qpVFg#l>`$76p)Pq%LO|OLQ{-f}C9mkWsW(TwnILoz#z2=VL zT$2M*^;E!VZ~fJOU+Ob?=V$ZhWc(0myvngO{j(A8j+=)ACO_p^uRh&KM2(?X$|>Td z|JG`biUn=wzbg3rpWKxv_-Q_G0sn`Y?Tgf!8&^+ytRc*xaOJ1n?a7Ps#F@^T+FkWA zuIV~5ttD>t9vgoRo+Y=|%vWK&%hSrqaKNy|>e8zB?wKjQ^JU#Dwrd_uQ2rFFRKX_3 zy6U>vNoH?$=fJlt38fEG_zUm;bC6&T=zjKueeF#HEvC7RXN&&vnO`m3t9he2Kz+f< zfDmKG7c6tSy*;kIE`4y!YI~vbhgAzhR6fN?KhQlC!Ep7t!GrX+g|j#hd~XnxySMAF zY{mL>xmT>39Znq%_%t`jtoc^_&X=eC)b_B2wF)(SZ4kEIQu>?eM{@JFQro_~FqtZ* z+fV+;zGJof5^!a0rvXQey>nnM%L48KzK5~%UKQ(pxX+gRI`u)B^D2)V&wb4&;uzy-8t|d%L5f1?wcIJ4c<~a_NG11k9ZYQc!NP=!sqfIyW1BodRnu=(}z2C;tcWy!W2nJz(4xCiuc*hTpX1w;Rseitt`${-JGfCSZy_;|o8Duld*O zlVdo1@;{iRGt?Vx={mCRiFNOr<|l1Sk45%uSa#B@ZBY#8fqu@#<)QokGi+b7unETbm(-IMiZ z2dobSn(Q-Q_o|Jt{+Q6mi|Pz}IZprC_Hj<$jPl@n|Kba38IFD~m{{(4b9UqXDh;VT z;Ri`h7cGPvI7|h#YcU0L)#Yh4PHK)c$Z7dM{T<_BLiZC*aFzd@R3tI`H8gCz?0 zpVr8@%$_!%(R~(!$o;&C+m^YJJ$3tJW+bc#Ex60EUb;IW>(iEB+uBt{_p^O7W7Jsp z??B{~a?!(!BAUO%Gyh?qZW}6NrXIqs(_}tL<-kmn-s{s0Gzy}H8M>!{nNR@uOTGmHM6$Ji{F1_*)J_yYMuD=(?d0eqnpe>_?|!SW5PH)ZNvQEje%{^ ztk?N&>`J)*(&fQ@xmh7PUK)q@ba6Tu>Tv%#^w}u*_Jq2LJs%I;2l>JH=nP(i-&>1S z{CIs%UpjF0q`fag(Ybx+S$sABuQQ$`|rM`%H_@h-Wj$B7R@_G=($5 z2b9~QS-0zLNMhW7{q=#37K_z+SK~IYYu~q-@ns^VX@_hL{@C^Wm;!=M{CD6 z)_=acq?WTSdz~9D#L5$F!fb7#7niq$ zGOJh5xKy3NR_7VvdVxiO)qR4mpww0$^@P(s-@b-B?JZt;;=1$1cwak@le<=OSKa@^ zoEX!<>A<|U&pS>mFf8=KJk>o#6S{JYo%~Y+|FZ44e0a(#z5_=OT=!v+TI^l)sO;k% ztvRMA(sVRasPc6r>O_{Z9`VRn)oMfZ8lTD{>Kfxw#&IPGdESguT=gJ z@4Svl`%_LsWE`8{f*H-Cr3R zt{WP$r&T;1N?x;xC^EkA)7X%3y?$e^ciDT}^^C_Pi$rgDwS+Ueni*GasJeJ~>aMGk zTC`UkDs}Z>@Kc$+^?7LhwAokr@12ggA9M5?r-anO?{iE|3%c5MN|hVBld5K%wPw6A zBj8X_;g6~|f4%ynUl~JkHePAlFFif@{HeYJ@|=%dd>EcBHZL%{aIaWR>hQ^TrU%|0 zsOQqP)mpXH!k$YH*a$bs2krT~)wI63G1U0h^^ynvZ85B-b1&?b>ppoP z@xXDpT|P3*f6lNZyxZpE=XP{y@g?pFQzz~?v$88w%fM{Y`#(yFXQpndK4E)`<-zGH zLASr#KmX~tR3;_lgUuZKzaO)&+JOE zzWkN@g~qsJiED`Y!=$`Q2YS1+^I7oav6x1|QyGp#d!qcy?EXSuY$#8SIshiA^5%CYp^?+{hSh^_#Ug(s8arF;)O zlYIZV=+V2P(493wlgezGgPAvP3%ew|p+HoqAzsz9;Z&;jg6Ov2)nYr&Z&fH5$TyvjV$=bbTNr- zi(^yluOgj=E8I3O3_oY5nK(O-vF`1{vv&$x9{DWwWZ+ZUy)=31e9@D2j>}IsF6Z6r z!w~c__<EZeP$oKKuU@q3!SQAMm$1Gc%h%hAT%fo2ft~v1-Pu zrwg?grhKs4sLJ5n7;IBF^|IMZ))Tk(9`J38 zx0LXX;9l^i@37FF%7`fe8W|osoDS6wWVixudhEPmwfyfqzlwCtnG7cPULV+az=yx` z*)-2d4I$kxOtlz)=xx||e9fjdhSN_s+z&hu+7>VQd(Nl*Of#OU9eQArnlJFM)FCVR zmDEIrHO_v;5}^`*PwdlDI63*``hp{yHkr0%_1gc`{IH|lR$Q84`oy4{U#o;0`dSVN z+S>X}-5%-pyS6nTIQrBk_61p-=K~M0@Ym{XW2*D#+PwCaFGC%}n)e?C6J;FYIv-db z_$|kjTH(BA*71S~<@5Hr&X^fEgC)VDV9#;a&V2`}EZ_dNt?^&^re|%&?t1>7FCU&i z3y_LQS$~aXLH4KS+c%RJ-F!3U@xK2a6|xFT-)`>OR;^W_@?qU_hrGHgC)nRd2{*_y z9kvmhby#c3{v%)Z^e~?LB%qOMyz<+E%jTTH%v0xIZp?o^l_AGZLO|*4O67%WE^+Hv z=6x~Ha1AcFc5>^1pYynOW^(`GyP^7;Wr6y_YxU~=jn5u^e!^~~FFfgUu-EAjVFm_2 zwt0p;J74-6 zRe$><)L?c%@&H5fs>+I~N&HMzjY&K{Q*0SR3yeO@Nj!0PUyJK6u^Wf&_wV0eyZ+59_b${=KCOLb91Qh%8)_W&L4!#amM=JoXy9ku2(yBX3gO&Plh_yFUD=N&i6m)?@YgN{%Ff}QBe3=bMH8AAadyD zndR#Bd7KCS9Wd{i{p_(#LWs-#@5&EiWR|Y^WyzLsP)*%`-oC2S@$L_l5>`)BVPNLa z=JoviiRZ&?j{V=49{4P0clD$dgYIXe#HtOSuCxBoHb|=DbZCFlzIXk{OEx}?bqqcI zA9mL_zE=yK?`d&ht*M^(KaKis0WQ|HM(bG;>JzLVFZsDcYKQTo_w~vj7PJSJ+Wei( zu;m=*0q?ej0>6aCyrb*Ru_Oe1D%mo@@RCgp#~+Rvl7-UC1YU{xd@oqVmh8h2^)~rX z1;gng@xuy-UN_in7Ha5g37pL>Eq(U5f8%!V3w^g6t!?J4+Q^EUYOsW-Qx@N>TtN%XJf33*bX+qa_7I;mMI^4SS z)n^@D4gb|=)@&5GaMB_}E>1C|RcG;YPVaOt-PA)7`@1={*|qBDiZR_Y<(t(mAM{Rd zxADd;tE1k@ItOe|dr;xDP~qw8yR8kodz+s*nW@+ZVyzb7X o% - - - - - - - diff --git a/app/src/main/res/drawable/ic_splash_logo.xml b/app/src/main/res/drawable/ic_splash_logo.xml new file mode 100644 index 00000000..e2e74731 --- /dev/null +++ b/app/src/main/res/drawable/ic_splash_logo.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/img_splash_logo.png b/app/src/main/res/drawable/img_splash_logo.png deleted file mode 100644 index fb521bf65ee01a4c75b85550fd214c7c606f4444..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20340 zcmeAS@N?(olHy`uVBq!ia0y~yV7kD-z{YW;PTIOb`A*0$S=t+&d4uN@N{-oC@9KL%gjk-V5qn?H#j{c_@$Wb_j_NQygM4E zc;^R+awr5jbvKAiRMS%A6!7X$TzFG7@SvcpD~t4r1s%+NeGNULfjT^0TsPRCC@$)2 zUfj`j>i5Iy#o5>Pe1CTK-`4AP&)0mOyZJo(0S=yN9>#&D4LmEI8^l^Gd+)Y;f*D;;3p$G})|nVW;*fbZ-B1~Tpc`CmEBjA3kOVLVVW z(ZZXfL4?7fs?TW|gM%Am!`vBa!3+yn7!pn=Cp~5e&}B$auYY!#p<(WinbnL8(Nh#f z85c}sP!LY-aAC->W>|ARtjm=l!hk^}&HIQY!wp>q1JCd@4~7lf84jEi;NHZ*!NXt> z*wL!Vz*5bSa6(zRf}v$5gNWOwaGQ_X>ja)_F)&oj++?#;#nQDgnnSmcJv>}bPUD=X zxVDH{B2(fl2fag;GbN20P52*tKEuGUV4|qtgXYhFE6(w)J9o~kZyR5{?yLQ5|CN%G z9{+oKeszHZ1H;3Tx{LpG^fya2ShF?c{#&H_jwK_GdqL^z#w#e-g}EoK%h+auRUj zN@d<#}gdF ziHf&67?Zjpl>0cuXSV)OJm|q9)UBd$vO^?L$$O%yhhb1xh(f4`>?hkICd>91w_Ovh zPcRrY{b}3dZaAfJQ=sh?o~_P1SC~t+xGmzokWyket6yxv>5WQjcvc^i+7Pyed3Wd6i_@Hwxh3$By3*to-dogm3I04Hkz{1p9^s**d`+ld zSYM#J!}*B0icqJsk^0UFOr9H;gj^E3yx=p4nHw|BK<`1Q#9AyB!|K!KY|}=1f9zAl6)%g)Z{5!>I@^t(8aTY?yguEB52wl z<+OI;wxIQa?JI;$N@rP~Z5H$NUb=SC)XP#C;u+sF*S{#)C3ttLoZs{d;g?Robbm4Z z1@o739^N+5=Hngd8j`am*d@{>%O%#&@L48#xm)shh=IA`>lx2y^3Ti>SC5BnVUcU}^>U}OB_J;xNdlO;D-=qP6Mu5K%BG(DoW@$Ai- zGOgV+{>tv|wq3Pyp{292mvQ*!pwCm%?AagBDEu*}#E$>iVyw`lBDZR|Z zXQyFmO!lg2)2BtO_E;Ub+WqxjvvqHm&t10Hb$8_NMn1>mUg|OG?&-1J-FO^=n+ogPe-tu=d=Ix7XnCFwf{9fhW=f7lX_5X(a&Hc6ft2<8w z+h!gWw%a`CBy5hF7UtW?>L0)TG0-{ou!Hl`#HoqXAGS=~dC_L^RkvO@{+`TZF^^Rq z=N`*Fwpfi#O;OF;_m%IvSwX%#muW7u_ObRke)i1SMQ5+gG@Wg0c-`pv?B}zu&%VDs zA*v!uXWNQxJ5oJ2J&9Tt`EJ{uyw)ho$fXgXQL&rd*3Mn4dTsaG-OB}+@ARAOw|d^S z^urrIZacYc<#x#pPj9f@UbiiGTWw-`l6d;|qw5a8o4j{&?XKG1zqahW?Kk;W%SOr8 z6>ljLu?n-gH|yM-clNbMzf60npB`~u;QS5a$J>NYYaYp*8$A26d%Ae~&f@0J9o?PY zO{evy+v&}gK6dxS-X~rqUkhU&R$cl!clPGlhu*HZz5boqyUcg(cjlilnXfr7(zy7- z;SSmx-;3LQvn!ha*zU27 z`8}h1k@08D4{SfN{j~q#{CV-M_nqsN|EK(~`hSsOd4norI&){EV`DVa?;lk)iu?QQ zt!}4&+jR4-W6y$}j-rnJO>3K;9$c%KapKK{wTj=L&p0>nTqa*@K;4UL7uXY@C3YXK z<56#IZw+s=?2hUf0W1%Wjokmd!2mT_(Hi zkLjgFp1B$^H^MW=TgFmPYn@qM&c6G9-hOU>H1`qr<7)94`X^!wDi@wR^rdyV*h)Pi z9jj>$J{^7*Q$9EfXsrIbBr|m!6dCGFy>Ur8{-=5ig z>h=WlIq&EHe=BrI=;5SoOWVA|b!8)-?Ol^)xyh!m@;29FQC97?Ft#-|JG!=MuGE4>pR!q#`;I-N8aC>_^WNMw4G>Y*E*5g z;?qUzZ)n`9cq-kVe`^2CJ5#o-TzzC`%K5ZQX)mAM)14R1H*MAQXVdxecyHd?!h1h# z%iFKtTyN`7KdW(jck?#qos0L|?AVjK{N%Z_*7hMwLcWCj3Nd?k>F(CV>p$`?zLT!r z^UdVV)!Wf~zW=p5x$kx;W2o=dw5zGVJ%4ZhZojT`)yp5#e&@!n^~)`Mdz|?a^Ivvl zzHQfcUb!lJ$u=aY|C;t|cfRBD`~J;+U#`n*#v9r@=h!3V0+-4sFTZo%|1QUCb*rqW zdd)-j%kpKi>9YD+PP0DEa+@7I>vwcg^e+E1b{DU`x>xnb{kA;+921+!O0Ul$Co^xo zY`Q!-{nPoLxl`xX*6IDclW=>{w(94}=k90m=fB@2Q1bAS>;LZO)_bha#eCXP@@doU zr^4Yo!e_0wiqCyj^2+AlO4!^yL&%Z<$pi={p)?PyLCnCf6txXeY0Hs-P%3=d*Z)1ymh?ke7F6oee}OOH{Rc= z&pJ@P;Pb^h?%&y3o6rB7__d$knYDjr|LHc~w%7A4>?|!G*%$qN`DeNF`IPf&^Thux z`geMrxR`$3kE^e*pSw8Y;=<{lrcd6VTeI}abLoIzdcxf zr(E{^;`{vmPihqEHU4M(-1s&4!t&pfB_~h*&%!+8^Pgm6-P;Td3<}8}LB0$ORjLdO z4b2P;KmRi@G`wVBC^cYUc$L7wU^Rn*K|Fs_{82Xs2ABDsE{-7;x8B?>pOKp$yW!#U zf=kX?igQ&aFfmTL;N--w;_zaD!d`L32Bk84Q4v=GQI|lLOeGE_CPhxwFUu4-Jl?nH zae8_v+CKlC{MN&J=BDpw^uE`|K4&UPo4>WbdhX2hYis`QlQzq}CBne4!6e+6iGe}j z0t17>0!9W02WEzb1~vu;MqUO6rdu2gXd+S!3@j!L3>+EC3{W+25tt#p3@|k?5ty-N zqZW+@-DqL}j7%f!cwa>sO2JU=gWrl>dobb27cX#Z{E4}cd{=M0+ z|G8_-Ld8Rthu>efFQ#_sFPE=JUqv6|SaYn+cedF~CWg!JmR*fqwRn~D>g;t74E1DM zzqGI39-S{`Vi@JRl-MQpf$k$h|mR{xWkrueX;M06}mF?>6 zo`2t8UM+X8Uw3BV)%E!yy12HtNGUF8U1>1?f2pRpNkrc?4$O+S68a|ty=ew_3GbM{2}Ytyfb4h zw%J^jbXZrGfk`TXFLwK`;?m=R2JzXl{ZAiF+wbz~`Resy<#QL!oEK`lr)u$KZh;F7 zpC7)uD!Yo^{GX_nM9ubH>4&eDGBENQEM6CQcX?GxsrTan7rJ+y)iI4_r`BEFcZ`7}qv7SIvnE6 zWO&Le$nZMxc2;}V=~s(hU3#_e>f_$Dtk|g71duZ$q^m>P!?&+n7ytHJf%gHit49}G z+}?jHBP96soL45v3@j!GQugn=x2Y=Nuu#TtiIYCR<4U^>K>=yhx2A6EuhzEJXU^=~ zT+AvZ@lnmG;p5FkzxKX5@`~|M<+?iys*<+O4P*c{15>ydR&THV+SIoC+?n;~P1aog zyX)2FV~k8v4O4IKt_m%UUK}BDKktU_C)TUsy`XgHy*>Er`kR&ofpdfYhJG#Q)n3KG zB-OC`!=hIkk8SvHFTi%|uU&x+4QvmV=LW|vP840dc=i7CU2Z&f(YEV;Eda?xq+DN> zcWVRZDPL(b$DO}4W|glrGgqoR^nF!($a@nmfeQ@X&z4bh*c_~ zVDGE@tDAe93@sP$O8r{7xZ&p`E|UWVw~u{3YMi_+_;BqPP-3v=U->SB$LGLZscCjB zEG7r;+}KsM#Lz&oI^5iZ4V2!VG`tE(PH-yAiSU*YxWG`Hy#I~Li6=Q{bY&D4Fg`nT z)a?LEZc$%j1KSMq__caQY-&&Lyb4JMrAhyNVSDdO%$N|o`n&`)M@Be~4uby7DH@&lqkt3sF=C<^&eXkN0C-m$K`I}zqbeK)^xdf+<9?1M>3W zdc}P8e@MKs(1Msowr8i;O0VO~OBY#lY1ylA>1hruKQzN0GCX_v`eyJE0<^K zlMNc{GFfyEM)GD`*Khyj@pa#;4z-@7-l z`dx7kC~4WhXTG}h*ntzPSG9-SHwGCJv#;Fj&Z&b**2{m*ZPPx#T#8*`0pq$G=Bp<6 zCNA8&dsp<=H?No;GpJ=AUgjHC&-Coj+EwMD(Go|UewKVa^y=x=oyR~io?2tOIxhV0 zqClM%{V@G?agl%57lKNm)V$EWTX*dWJhODObeYJX=@ZF@t zxZTZy{@hIOg#SQ-&H-Zc+dKa^I->qvJ z%kC}Zjow(l_?OSuiLdNeoezC34NB41&gGlxWF9-edqtw;<=*T+-}AB}=Knl>b-LTd zKlWFzuL^ck%orgc0 z^eXe}Vd-~AV?)lb{&(#qsJlMx)zz!EQqy)stE7kRzrDfeq^lou!_|#k>>E1F!uVI$ zt+h(&oB#a#>h0n6p|y8T^MOJ>dwZLxm0XalS@ZGwmoMzsU)#WRY$xw)o%(Hwr(bV3 zU$cV|lqOf(uCfl}Uz2x#L&|NPA30x_FD|fKpV-=Kyz2cE-pQco;Eh?ktFCnWt7(h> z*Ueq_{jbvf6@8gEuJSW5@@{zdZPmNG1yjr0HEs6!Uj4iJdgV%nYki>>4C`)eUo9T; z-*V~k?F(<+Jk6r8fN|N`nZY$V&#NmLSWFIV`T1*#-1VKh-;No8?C{!K?bZL<`)c|g z8xZ$a&5B z%IR|-_;u?H2gAAxrmJ7CeSH7_;#aa)xmRzmumrpG_QtLEwSGT(b^2=bYIBnu<+_E_ znLrWSw0i5W=)32#c1+q+y0!9fu43r)o;4t5`riNV zw|I|mf?Y%_^MkI~TLto;mWkH9o4)$Hlp?4-XQFPlre$MHefZz?9~sZg=bdeqYsA{1 zUH(7bUFWF%En9Jlq=i^F=#t+Hs9GH#&ny%SZTAFZJH}n3Ad%<(_ zO~SLvEt&zWJ~HbHi&l{W|^ z$1_sv?%fabH?srPAIqFqo0lE6W}bI*!^e}+J+GGE*H=ynpO)UVYzS-M7pe zvLE|buMPhh@jZVBE2y=2X2Rj$A8pTvKb^k%w%&&|eXH)p+|Bpt-=6)oW7@Z4&Y-lW z$`^g?cwY41u&srR&r4#y{%X_y`1s3%tor4Opt||+u~*AZ|Cyt_%2qtX%{uIVz}^M7 zDw+ECyML8ft^`)Fb=N0z{+|WK zn?ny@{ghk(==`_{tp|0-?qjk{?g$8Yx>s(gL==lRTSYp>~z@m-9iU!(cEAQR> z6gKsTPwDDc?XPRzxQFPk$umh`e^PVt&$`<$Idz1-J1}of+W*Sx^q)Dx%M|AE=iFfY z;8y%&)|+VCJ1aRs&FPZnY1?NV+sPZg=KAf)tD2wvD6PynJev!YPo`KV&6NND@>TNd znlr!6b8L*xe_Jj0^b4p#_oa4iRG!2B)mF80`+r}&YAW{NYh}vepej%<^pd&p$t+;! zf`|W}f8A34^#E&Ct=t0>Ru+>3Uv>l@o^{;&Yv-%|*M82Bzf(|scu|GV;h`@m28a ziaq7)@*bML{&=;|FW$j{dCgwgRnF5te$3DO8+QN3qN<9mJEyO8E*6y%xWMrG(Kg=i z4*ExDu6_SX+I;=HN2t+Dl8D6}QfMSGML&Tz%odoSPdmFRKMJT)Snu>UfC%if^$` zlWW)Rs(oF|TJ^qd?cwhqC&jJg4=rDzXTX&pw}Xjy&C<8h+p6mF4|7TX?t5r>|NgJL z`#uVM+@oT?{vEhy7%{IpJbzW(tqr!{ckIcWJMX;S)x~?)cyEe4+$D3Zm5s&ZfY6;y zRnA{;wS}4;d%61R?&~irZ8umQIK4Zid_|ubsLF}i=)79obdTeQd%V%H{kJ=>O1}PZ zr!1@Dvag+&fBCBW^^ZGmZL9ui7PdcVZ|N6ifdf88-0T~oZkw!W z{nPhqXST?B)0}xlx2$ZRUtaxr&ll~Y+pi)(jo}R|-bKmA?_Kq9Tie#6cNz<>pRVuy zx1lE5bPeM>E<1$nH?Ou|d9GY*%@6kV(bAyyvunZO zSL>zCoOaf4eQG6p{Pt@3b)R+o*8RJrxlt)P#D9&M1gk-J&Qo5Uqj|jA**3d&U9*xu zee~+uRsB8(W6IXt3zB^)61lBgD(p$c_CN+EsTu0lW_uhzXkKj$F7lCIl@}jtp?bXd z_N%kJx2J5j$zSmfKDMddqJgH{*BD{M_#c8i<}Qt_X)HK-@E>o_VP1bcUFFU z@5iaIpwYD4=kO`1|5rBfemAk){WZ4sahJ{Vs{O&~H@B^ov;Nh{3(A)JZ)f#Q z4P{t2N&CWv8-1(h*(e|Xb^q$}GzFIWVv*lE@7ep+(_=G0E_$G6{H64P@7ZHT>GRjj zdv+yiTlKbWw-17%-Zd@N`l<2i$!Qtu^rcwsy|>qXJq_v?B+PmnE^DBC{8!(rDU!3^ zx?X)BdcSOz?rHrnb0bjlyZq`yR=cF-waK&g-hE{kZF@L#n~t?vjwHXrg2t649+!7s z`+WXa+UDJ!C%ac)x4Js*Y2)HzVFw51;MC8rju{uNdB1+2QRDHwVWpQZ&bL)Bc{APY z&Z&>3%kNCejh9Ual?=&VHWV%UjpL`h?3wXW8L2g?X#*Ss$y|7j-!7c6k8OGuUpMN@u^V+xc7BpIx zB^*}My8LeKRsLgNBFxvn+t$->zbtX-B+nm@UlscuR=uejzL1fT*Qe2W)!OBMB=qwl zWRqj$Lf-D_>9=2WtK8}QTkEU&=5NAIgzm0i=fun;6_M_K=73?{vR@}=$h~d|bkIL8 z$NSyK|J3Z&`^&ZTPTE~P+{-9%;Xu{RBsZpCcdvdw{w1P6bBAcc&Ucm%pZ_@XD%tO( zYGumg-@Tw&x^-7drOO`7P7^m-6KKm<9m*f~@cF%}FFz-L+JE)(X078v@yFf4J>Rbd zkF$!RekO>?@u=*e>~(DxNtzms)X&|-T4u7 zt=o>@T>SL@??02xLds*BpZ``|{&dotP}^G)42-;MmacVYv^}f0?w-Z6KTh6cP%)k3z1t(X`c`H@-bbsR1{$hv8^_QPMU-fzJJny1} z)xXy=H8ikId%7U3&i+GGWp?R33AyL;qm>!;OM%jeGX{x&1499)ix zz+)Je|&sZ zfB(b6cae8kbKJV)zCYSQeTFT?tLvTbF4?>G>V57#rH8McER4_G zf4gP=@8xqBoi3XD#*c#|BjFHV-mdEx{w9`Ql8}3T;+3DU#XjrM@c9ou|4%BNcDdxF zSqT5yGH{1_a?#>tzyCe&H<@=^LM}Ylvhnfl^}Eip%>S=hsr;}=HtOzmPyqUz(p~oF z$Gw2usHe$y*6h08(wF~PZMXKr|9wico>n?-oeWB)65Dg7&riymT$J_oo1x9QLf-EV z{-<))B(lYy^|ojiHoy7@PW5;Aa(7)nTkHR|*U;wpZ=W9){F!@JSN~aae*3T0AG%-c z$Z`h-oPpo<^<^_2J+6)}y{IMsSTI{4Fxvd)Rf~ViA9|UbE^dCs2sUg^-kn$5x6GT8 zANoG_Y4V%#LmpS>KI&8bA0`$&*iJ-_8vQKbMiK;-McBV z8`aJCB#JpWFt0A!o7JyryDdNTeazEjn>Dx6d;5Qz@qWMScj)=tP1I{QiF%KFLem<>uwcNH|)SzoRu-s&?J44_bEh$6rl+{loN4M_V-8 zv9~?1y8Z5k*7{eMOuiey?%;4(#(K%iyADrduYM2z__I8xcKy2i(7VM<{nd_NBbUEh zb!XeIZcuDRbV`RyFaNMZZ;fBx)7|fK*UYnEjo<$+@~u(h@xNPw%R|jV_DALBiGb4k z>uJGNG4bo{c66P$-)ja-{Gt@0$UsTk|fbl=rf*m>9H|QpnR+ekwei*ZH6M!2&DQKO^bHp+&-}BUD`t|pP~YLS ztB;F8t<6T&8v%Dyy!zj^yxJpL_b&13-sK-G?=JnAuc*7=_~RF^_&`bJ!KybGe&lOQ zw0~YL9RBfV|H^H%4?Qnk9eAhz@^R@f?Zi^E_4ATI)y29~cEM5?C=-q!}r zx!*z)0w8b85w!c zG@e~G+wWoFU;nQcMeE-EUHvyL;oIXl;a|m9m-@Zjcw<$S3%EeuA@kex%>?t@zV&)h z2cN%< zRQCAO-dD5zUT)lHw;S9@nqOLR*y_UNC-zc#?h&2I={f((b8Dpc&WWqNsA(I8dBd$%M(Hj+MRkYvR zjW>7S$^ZqvUh&~J?FE-po@X5oWqYzb{Y~kPxX01LzgAzpZ@Kk&+WoZm+Y}ren1wfO z+;vh!?z-RFch{e#hp3->@cLuJvF~Z;(nITQX0fi`oc;K&p^(6Z13bIk9>+FU%n7TG z$zOYJ*2e3}ce0)A*Cy`%5FYy6`fhm6>bBb!3@j!W?5=)Y{^dsAjaTcA)ouCpqi3&N zwONk5t)KiFxoaB~PpoeJ(#3pBnv;={H>7m&vhvxFnxpRSz4?6mr{0Kbm+!hCH1nGt z_CL~g$FAhBx68>-c628i$DvYxn*5F?RFS_^-d;Xh@`=yw+Rp`$GHEx>t%vKgO}Z7O*BKmL0qZT#()%#{zz_HX@i?o}P{_OdJ=bdpGV%IQai;PFU>i>3j|jhaXujNUVO8 zxjlI6e+jwgf3HqYe`9)UvsD#W-{aiBKd$weo9T3JUy-wn3FL70(CV`X3g6wRxOU%m zZPo9dy>8O0s<-Xhy5acKN3V3R>W3aTd-G{WNBENOnLC7d6&55i-mP%^CmX-^o#$-icOUieDb z_*+!&)n9vNepnR~wRfld{aYWWv2wjw{aXm6*k*S~`SY| zrT1)j$UE!%s&^L)W}mUSe`{m%Vd-c_789Kre`l|~%JMDKM!V32YmagIo3Fd_viGZf z@v5`1T(~Q{^naW7rP?7d&Ny$c)k6&o7bjuxMmw7z#%>U>a{>76)(?r`_~>p8JMZSqAfm9MT_SXKUc z^X7+j`&vMC*=fG3U(-MA$hSClysG-^T*>Nx)#iV<7skx%US%FqzEUn`Z!+IM-nUmj zxWzCq@^U|_;{7giewMV!zTD&c@4UHtY~#X*y;ad?UR0Jle!bOp+v;8P-u$-zyl?Qh=EBe zqVcWt`@Ki9Yv26t{rJ%@|HjOMYc>mRmG+%GDIdB%FXHvD#;f%vfAc@w(aKk0VKLF^ z`73=tODsFrx%9#e`Oinv@-O!@uSz$s-~DiI=-y=y&8D`Szmc+h`+ph(i-}GH^X#|P z=ErNl{@L$dpLSN+ZDZx}w%bNGyC5` z-fXA-(~?(jrZNF;Y>-1~k)#Jo2zXK%k})U`Eoi*KpR*Y>OKQhA;~=U)7uyV}w+|NN?dI}?>>9uJA1G3oIw zTkS#y?G1^Sm-#MZ*!(zZ@rhftZSxOXUfsL8yzir-dG?Knr{9XdM9sfhkbNpzdfTOc zDVw){ek5wSAn|M8Y~O=D?`?jWvu;RP=WsY@=e?fe#k~1*q`!;4=$?J{s>Ql9eOF~y z*{{E6+Vvtna|e4}w`6@@{JMEpHdGz2x>fhZS+k*`&9dyYWu@Hpxd)QxUD&YZuFb8L zR=a+^ezjlfUd@k|Hr~>-tL@Txdn4b!Z@m67l~v$^!F|5@lb;tlZ2dVy{_|?y@T})| z`FNu<|HqaFeO>md&{(P|e_m7ajmy(T9UKn-xwZG^eWqVVzfJ!9s=4zo{ouLIy;5fS zKik_(r^mX_f;`J3wxPkj+? zu5Wi;MUtc}*69#!>XWHlD|DRKj?QA;2cXjeyweT$G!&k4l zo9+4d;mIpj-h7Xk^V*k}o7~yIciub`u1p3--r|(~ZBwRw?Elem-Mg-C<^*33E9T&208m{hVXFJa|=f$o<%-kKbG> zkY9V-Jna3WSKYk3<>v*|uCmJI{dX^RRln&!-m+=0C3&M)F~?|zFGw`IlfyUv@;ct| zYvHfOk9@w76BfI_D(i6AYxR)&YXw`YCi$(tXU2N||97dfY8HVD29LL`aPubk%R$e58^eJ0c4dcoFWQ@N`@ z!aqNF{x){+l9O>S{}+7SsadfuI^_KNcMnt9SxnBXbDnj-G%LI~`#|2+zjW&%WPwX$Ljq}XFthY-B9~u$)2^l61?WU ze#a|);m(=Ud>k1wbfu-s6y}8QHp_XvH>NtYzqo46&TP}0t2fE7+Gpi@zI^|yR9~cJN&rMhmZc*5tFNC-Pur;`uKj8oWO+xEiv1_ z-c`-JWM-x_cbm-a)Rh~*mi$($mOj4U_hIqA%CC=JNyXJUI2``6+jHwx8T!smK-%z`2pTaXDv!g@nqwijQ3Ccr#7CR5lvD>~rQ|Ka2L z)%VPriz_3(rh`?c#BQ$g`@HklG52YLj?!-SE^nSpnIY+JvZpgI=-#!QyT=M=#;^Q$ z<$ILCg#%A)_Wq3h{ZsWStM{K9v!A^ByyoWaT~Ar${}*4aKmQ`9Zgo{xFsR#A_kEq5 z_44y?xp`mz)VmriU6zzmmJ@b2v*~`Y-nx4Rhc{Q29j=yUVC3yNd5`zISDxMS^Y2x4 z|6YH;UGlHN!8ex664}ps&b)ftLa+P9bL;Jn4$O}~e|c7XbJwc;EnjkXn}_+IIK{d- zYx}Eh!Zm-^hrgfo@Mli=-W^t_Rx&Y3Ma=yj-aqT{^Y3l1w7vh_h;Jdye$qUGwW|R`&9vKGv&F-aGcO@zrf( zyT!Gu>R0g{U(>K4$T--4)Jb$&_t|R?>ODFHGI(chHTIq96yMN-V_|G4l!zbtZ zrOr=M;KG4Rwi^-`w^o*Be&s*?!|IFl)oFg+Gq)#-T$udpUXf?%p*d$>b-Z%Ddd^Q; z;KG4W>jgjWFMMNr_4l+N8_P2z4mSSeJnVUI-iN+dyNiuhTbu3Kcwtko!h%NajZZb^ zeV_ID`OishZ+A~O>OcPU)n}WVt54>w`gHcy-(r*X@hj%NzR$ps@u16g(XRJ%?2q?v z{c=_4UBKP5nTF-}!__LK-LJlWo|D#}x9@%c50g~H#@FHXI~dM?F1?!R{bPsjs`%`P z|1*CUm27>_BLAQH>h)(iY5r^L?yqEKlKSy7;?L)-^Op7V?yrs8d!*mCOeE^e{A`my zWr>rX-`xGYAkRPhh7X6p1&6Ok#2$yW)@SZ;|GFwC^6#$oj>T`~glpdT@^0shHA*+W z9r4%otHN@Aj*N&B^ObewjPssH^6p-vdvxz=<+)PDZ>RZ|&Hc=8R=>UQ%7&?b@0)vPnnEEU9=B~u2ykdtR`j!Px{T?1a|M5@R3mcw(XY^AQxUhkvEMrG$@^RM6 zed|}9i#~MxuK9O6{YiUQMf-mE=%4>GyYKueX_GtWHZH%+$zpP5_wo>T-4n%seZID- z*4!z(I(d#1cdGN@u(y-{#C?8$_I<9*^qrxHr`^A8z0|?s@Sa_zhlO&xA054F>HTBJ z?Qrv=DxEotSN&D{@nil9yR8hxd$PYCYP-DvRB@-azgjP~Zkt|He#n1NVk`^07vW+Z zT0ZN+3vHN$T0Lwl;HKxqS}je_rL?eMtA{Of3D^IK40(Z@>S*5p<Mc zE5LT`(W>Sfbq03JVg&?ryF6 z(`5Vnd&v9PgU|PF|1~2xP4Vs?Q2L(OrYb6avW!=|Smx~)CPv=PDgABcUvhRjoUaW2 z%71mK_K!QCSBG2Wid}lg>uuwA>L+jbLh-Zbcz285TEM|#a;C?A4PWer=YKA~dJ)f}Fyy1(!7td;JXv^H1e|Yx;^XLuF|NedTcXdsV@74XG&#ijJ z&VA#JuJ%85^r~@G&sdv(7&p*qeU*=;f$;VGqvqC$Czo{-=CS?BBGn zds*jwUT?BSWXnNKb|$H7r=3^*mUcGJs*z6m6J6RiQE!g&D(34C-v2E5TFq_u`u_TP zZ>BIa^1gn!piSHI-`OkMuI3*95-~rkG~3H~c3Ec4cBa^=)3^S~eg6FI??3S&{KdB# z8``#Rsrjb^O2nisjFKKmV`Px9hypHyN0ut{q91 zx_9+lzigze!Ls948-LB2s5d9t?B`Vrz0Y%BnM<7)>e-xiyv=k`f6=SPhBi;@H4k5j z+UM?mbuPJTP4TL5>6eFYZrk-+>R)Ys?#AicR^KWWx!7*`*TLcNlAOrDo?Dj(#;m*D z_c8m#YpJlO2^(c@7gQgNsx13Dk9Gdft?u?4;;z4BXOdET$hr2d=%&cx%g4&&e}u0x zmwtK3a*I_8@Be+ptCxq}kNvPSet(tc*7jfl7Ly)pzW)a#)?KONJw37a^nQ~Z(Q}V( z?A&@Y&Az+C55?{J{)|dd~8=iU*?U6 zMY?;pRP|mg_xbw$>eXH*7LzsCgjZWj9-bs~b%W_=i$5z8lSRc&KkUs?{vPvg$+sWW;y>)Uy=#9o6^(&{ry$*-qM9{BJL*2{=1hSw*PKH{Fg=5 zIbWa8Vqh^@bENEQ@v6B!Nhi&+_q;w;x4UZbM!h-J>n?v^Y#v^p{wMVBtZsIN1&OM4 zdv|R$)VUoi{eFG@|CPHAM2Meyxc2<5BePf8ud;ihn&JF)`PIiDm+D-7yE@*mO>}2o zX}6)>J@wU-drKGQT>eqEEoNQu>g6HJt0ZsT*bw$jUy_w0V~3V?SpBqU^RTSd7x@() z7X==kRHoDCyY_v)?7!7jZPOP@g(?1Of7R#4$RxGy%>V2?uO;;tMfW>Z#rWmz5IUQ* zV}Vue-2R8GSNB(Gu6nrW&}pe_U2H5Sf952we%zCIl`nfwH1qsfb2-IN{=6#rx`t=E z*`CgK+HrlWzO9P8w;@_ZVZp=yZ?1h*x?(yZGVR zBjQz_s~+zA5*~KlkW1h~!OZ))d$J|;cUha>yJ}%K*<4t!D{}`^?M!piJLe8QxX?CN zy4)0`uK(5a-o!P1IX||(x7fR9*O83p_ip-Lztnnu&A-E^Kh@`j|4rW79N5s1R`@p3 zcJ;@7FMAW$ZI}3*T({uYjEm>?UX}`bG-KK0%B<39)1Upnv-sERwzHER9Hbe!)11Gy zU;Wyfq|_(-`SIM)z1JdGzgYb^|LSFMMa=7$53d$Wzh4U~R@v?>uWB*$IrCQPf7Px0 zb^B7c6u)H;u8PrLbML{^pPK$_{vD7IxUj)uUFhG+txE%a&fHtvA-7yVG~e*n&azum z^5ATbwVxeq7r2d!ha( zqrV|8_N%LwfE4b#yT7Wza80MO^#5F$`FRmu-9Js<+RN7EtV~kR zF4tX^@89>pxUxOOe8z#t|9s!I&I^Bj=76Ey;$I!pUoC%gW5d($7u^~g+J0_|{QBnA zEw_zn+t&8YI`Ft|Z`H()=jL{=mOpo(aNew?Z}ram@$Q$kx}za*!N6WF?(TVsYj@^K zy=#}-UcY8v#-8G*b0j%mtl|%=f2I1{@^{GpnAr0mrN8qoe8~IA>$dS(p2@=eW%tjT zo&NmnjwIKMRnJXz)}HXUUXl3x!rmTG)sx-b7Ha;@u&Z=q?Bt!ji`%O8>=Ta^Kc3U} z^ikT;)~oqp%PU^iofEvOy($*0{^iB1dwUaO@1{ESSC@U2c|3P@!@h!JeLuN*XLrUt zdivYaJ-j|VH&+DImuxp%v+LlmU74j1TI7EJcy-s!#+pFh95KJn_D zv%Mf!JbcB={pYvnqhGg|?z+md`1Ma?sklE|HqY9tePQ~~zpp089XkE>#l`E7&Vt-< z`TfVM8;=>7#>bbrb?`@n+1+06Ew1c4+<0AKNFSJ>3_( zxA;)~@!zY%rN5qG|FWh!?`xR;Q_-rjuMWwej_S{=uf82K2%Q&STgv-y-gDDGT61rf z9FwtW|Ga8>%Icrmn{vM%dbJJ|!7mG6w^hHLAt#>|aVkr>HO7`*?-FGR#t7icz(|3S?fi0^j6hv|FvcMH?G)fk&En7 zAjQWGdB58k+wQyeLAvp4MCe1-#rx;qEUf!n%DM}*c6G|!$4~bk>scG9s+^2g@e%1V3HU zcH8{gU-KvMatuZ@~hx!|{ft3DVotMHB{M7hrywAmte=@!}i(P)d<<%q0`ab=W zm&&~V-un0YSVKeG$KBaqGm>X$iTCaMIM*`ucxg~+)AaOLbN4Bn|IF`hpHcMm-wLa1 z5&{o*1!t7q4)Zr-V=?Ku zuG^Eas%CXn+U4JEd&|S}KNz)|NPa?%!0(Pw|QURIoGs*#lFX>v#tI|)mAXh|9*Vc{qLXR*ZD49|5;%{ zV$Z#hzXvrQM941>_g`ga;C%eg)K{Fm-*b#*{p9`%Y+E6HZ_kb@x6;nwhs)~>{vUo- zu{c32=f;+kthQ - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_attendance.xml b/app/src/main/res/layout/fragment_attendance.xml index 8016081b..6f6f648a 100644 --- a/app/src/main/res/layout/fragment_attendance.xml +++ b/app/src/main/res/layout/fragment_attendance.xml @@ -142,8 +142,8 @@ android:paddingRight="12dp" android:paddingBottom="8dp" android:scaleType="fitStart" - android:tint="?colorPrimary" - app:srcCompat="@drawable/ic_chevron_left" /> + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/fragment_exam.xml b/app/src/main/res/layout/fragment_exam.xml index ca88849c..0c62aab5 100644 --- a/app/src/main/res/layout/fragment_exam.xml +++ b/app/src/main/res/layout/fragment_exam.xml @@ -128,8 +128,8 @@ android:paddingRight="12dp" android:paddingBottom="8dp" android:scaleType="fitStart" - android:tint="?colorPrimary" - app:srcCompat="@drawable/ic_chevron_left" /> + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/fragment_lucky_number_history.xml b/app/src/main/res/layout/fragment_lucky_number_history.xml index 5f50d126..a5698e2e 100644 --- a/app/src/main/res/layout/fragment_lucky_number_history.xml +++ b/app/src/main/res/layout/fragment_lucky_number_history.xml @@ -118,8 +118,8 @@ android:paddingRight="12dp" android:paddingBottom="8dp" android:scaleType="fitStart" - android:tint="?colorPrimary" - app:srcCompat="@drawable/ic_chevron_left" /> + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/fragment_message.xml b/app/src/main/res/layout/fragment_message.xml index f6fe39dd..5269d95e 100644 --- a/app/src/main/res/layout/fragment_message.xml +++ b/app/src/main/res/layout/fragment_message.xml @@ -5,7 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + android:layout_gravity="center_vertical" + app:tint="?colorPrimary" /> + android:layout_gravity="center_vertical" + app:tint="?colorPrimary" /> - + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/fragment_timetable_additional.xml b/app/src/main/res/layout/fragment_timetable_additional.xml index 61eb4445..a71f7545 100644 --- a/app/src/main/res/layout/fragment_timetable_additional.xml +++ b/app/src/main/res/layout/fragment_timetable_additional.xml @@ -131,8 +131,8 @@ android:paddingRight="12dp" android:paddingBottom="8dp" android:scaleType="fitStart" - android:tint="?colorPrimary" - app:srcCompat="@drawable/ic_chevron_left" /> + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/fragment_timetable_completed.xml b/app/src/main/res/layout/fragment_timetable_completed.xml index 1a890fe1..e089275d 100644 --- a/app/src/main/res/layout/fragment_timetable_completed.xml +++ b/app/src/main/res/layout/fragment_timetable_completed.xml @@ -130,8 +130,8 @@ android:paddingRight="12dp" android:paddingBottom="8dp" android:scaleType="fitStart" - android:tint="?colorPrimary" - app:srcCompat="@drawable/ic_chevron_left" /> + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/subitem_dashboard_conferences.xml b/app/src/main/res/layout/subitem_dashboard_conferences.xml index e8080936..8da2e19b 100644 --- a/app/src/main/res/layout/subitem_dashboard_conferences.xml +++ b/app/src/main/res/layout/subitem_dashboard_conferences.xml @@ -2,7 +2,8 @@ + android:layout_height="wrap_content" + xmlns:tools="http://schemas.android.com/tools"> + - - \ No newline at end of file diff --git a/app/src/main/res/values-v26/styles.xml b/app/src/main/res/values-v26/styles.xml index 55413c05..3fb0a5dd 100644 --- a/app/src/main/res/values-v26/styles.xml +++ b/app/src/main/res/values-v26/styles.xml @@ -5,11 +5,4 @@ false @android:color/darker_gray - - diff --git a/app/src/main/res/values-v28/styles.xml b/app/src/main/res/values-v28/styles.xml index ee77091d..a936566f 100644 --- a/app/src/main/res/values-v28/styles.xml +++ b/app/src/main/res/values-v28/styles.xml @@ -6,12 +6,4 @@ true @android:color/white - - \ No newline at end of file diff --git a/app/src/main/res/values-v29/styles.xml b/app/src/main/res/values-v29/styles.xml index ee77091d..a936566f 100644 --- a/app/src/main/res/values-v29/styles.xml +++ b/app/src/main/res/values-v29/styles.xml @@ -6,12 +6,4 @@ true @android:color/white - - \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 45382e6d..7cd0f725 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -23,10 +23,11 @@ true - @@ -50,8 +50,6 @@ 11sp -