1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2025-01-31 15:08:21 +01:00

Add notification about upcoming lesson (#578)

This commit is contained in:
Mikołaj Pich 2020-05-20 15:11:01 +02:00 committed by GitHub
parent 115da64167
commit 29226dd93e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 333 additions and 8 deletions

View File

@ -8,12 +8,15 @@ import androidx.test.filters.SdkSuppress
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.TestInternetObservingStrategy
import io.github.wulkanowy.data.repositories.getStudent
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
import io.github.wulkanowy.sdk.Sdk
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.reactivex.Single
import org.junit.After
import org.junit.Before
@ -34,11 +37,17 @@ class TimetableRepositoryTest {
.strategy(TestInternetObservingStrategy())
.build()
@MockK
private lateinit var studentMock: Student
private val student = getStudent()
@MockK
private lateinit var semesterMock: Semester
@MockK
private lateinit var timetableNotificationSchedulerHelper: TimetableNotificationSchedulerHelper
private lateinit var timetableRemote: TimetableRemote
private lateinit var timetableLocal: TimetableLocal
@ -52,10 +61,17 @@ class TimetableRepositoryTest {
timetableLocal = TimetableLocal(testDb.timetableDao)
timetableRemote = TimetableRemote(mockSdk)
every { timetableNotificationSchedulerHelper.scheduleNotifications(any(), any()) } returns mockk()
every { timetableNotificationSchedulerHelper.cancelScheduled(any(), any()) } returns mockk()
every { studentMock.studentId } returns 1
every { studentMock.studentName } returns "Jan Kowalski"
every { semesterMock.studentId } returns 1
every { semesterMock.diaryId } returns 2
every { semesterMock.schoolYear } returns 2019
every { semesterMock.semesterId } returns 1
every { mockSdk.switchDiary(any(), any()) } returns mockSdk
}
@ -80,7 +96,7 @@ class TimetableRepositoryTest {
createTimetableRemote(of(2019, 3, 5, 10, 30), 4, "", "W-F")
))
val lessons = TimetableRepository(settings, timetableLocal, timetableRemote)
val lessons = TimetableRepository(settings, timetableLocal, timetableRemote, timetableNotificationSchedulerHelper)
.getTimetable(student, semesterMock, LocalDate.of(2019, 3, 5), LocalDate.of(2019, 3, 5), true)
.blockingGet()
@ -126,7 +142,7 @@ class TimetableRepositoryTest {
createTimetableRemote(of(2019, 12, 25, 10, 40), 4, "126", "Matematyka", "Paweł Czwartkowski", true)
))
val lessons = TimetableRepository(settings, timetableLocal, timetableRemote)
val lessons = TimetableRepository(settings, timetableLocal, timetableRemote, timetableNotificationSchedulerHelper)
.getTimetable(student, semesterMock, LocalDate.of(2019, 12, 23), LocalDate.of(2019, 12, 25), true)
.blockingGet()

View File

@ -92,6 +92,8 @@
android:resource="@xml/provider_widget_lucky_number" />
</receiver>
<receiver android:name=".services.alarm.TimetableNotificationReceiver" />
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"

View File

@ -1,9 +1,11 @@
package io.github.wulkanowy.data
import android.app.AlarmManager
import android.content.Context
import android.content.SharedPreferences
import android.content.res.AssetManager
import android.content.res.Resources
import androidx.core.content.getSystemService
import androidx.preference.PreferenceManager
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.strategy.WalledGardenInternetObservingStrategy

View File

@ -55,6 +55,10 @@ class PreferencesRepository @Inject constructor(
val isNotificationsEnable: Boolean
get() = getBoolean(R.string.pref_key_notifications_enable, R.bool.pref_default_notifications_enable)
val isUpcomingLessonsNotificationsEnableKey = context.getString(R.string.pref_key_notifications_upcoming_lessons_enable)
val isUpcomingLessonsNotificationsEnable: Boolean
get() = getBoolean(isUpcomingLessonsNotificationsEnableKey, R.bool.pref_default_notification_upcoming_lessons_enable)
val isDebugNotificationEnableKey = context.getString(R.string.pref_key_notification_debug)
val isDebugNotificationEnable: Boolean
get() = getBoolean(isDebugNotificationEnableKey, R.bool.pref_default_notification_debug)

View File

@ -5,6 +5,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.uniqueSubtract
@ -18,7 +19,8 @@ import javax.inject.Singleton
class TimetableRepository @Inject constructor(
private val settings: InternetObservingSettings,
private val local: TimetableLocal,
private val remote: TimetableRemote
private val remote: TimetableRemote,
private val schedulerHelper: TimetableNotificationSchedulerHelper
) {
fun getTimetable(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean = false): Single<List<Timetable>> {
@ -31,8 +33,8 @@ class TimetableRepository @Inject constructor(
local.getTimetable(semester, monday, friday)
.toSingle(emptyList())
.doOnSuccess { old ->
local.deleteTimetable(old.uniqueSubtract(new))
local.saveTimetable(new.uniqueSubtract(old).map { item ->
local.deleteTimetable(old.uniqueSubtract(new).also { schedulerHelper.cancelScheduled(it) })
local.saveTimetable(new.uniqueSubtract(old).also { schedulerHelper.scheduleNotifications(it, student) }.map { item ->
item.also { new ->
old.singleOrNull { new.start == it.start }?.let { old ->
return@map new.copy(
@ -45,7 +47,7 @@ class TimetableRepository @Inject constructor(
}
}.flatMap {
local.getTimetable(semester, monday, friday).toSingle(emptyList())
}).map { list -> list.filter { it.date in start..end } }
}).map { list -> list.filter { it.date in start..end }.also { schedulerHelper.scheduleNotifications(it, student) } }
}
}
}

View File

@ -4,6 +4,7 @@ import dagger.Module
import dagger.android.ContributesAndroidInjector
import io.github.wulkanowy.di.scopes.PerActivity
import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginModule
import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetConfigureActivity
@ -48,4 +49,7 @@ internal abstract class BindingModule {
@ContributesAndroidInjector
abstract fun bindLuckyNumberWidgetProvider(): LuckyNumberWidgetProvider
@ContributesAndroidInjector
abstract fun bindTimetableNotificationReceiver(): TimetableNotificationReceiver
}

View File

@ -1,7 +1,9 @@
package io.github.wulkanowy.services
import android.app.AlarmManager
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.getSystemService
import androidx.work.WorkManager
import com.squareup.inject.assisted.dagger2.AssistedModule
import dagger.Binds
@ -15,6 +17,7 @@ import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel
import io.github.wulkanowy.services.sync.channels.NewGradesChannel
import io.github.wulkanowy.services.sync.channels.NewMessagesChannel
import io.github.wulkanowy.services.sync.channels.NewNotesChannel
import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel
import io.github.wulkanowy.services.sync.channels.PushChannel
import io.github.wulkanowy.services.sync.works.AttendanceSummaryWork
import io.github.wulkanowy.services.sync.works.AttendanceWork
@ -46,6 +49,10 @@ abstract class ServicesModule {
@Singleton
@Provides
fun provideNotificationManager(context: Context) = NotificationManagerCompat.from(context)
@Singleton
@Provides
fun provideAlarmManager(context: Context): AlarmManager = context.getSystemService()!!
}
@ContributesAndroidInjector
@ -126,4 +133,8 @@ abstract class ServicesModule {
@Binds
@IntoSet
abstract fun providePushChannel(channel: PushChannel): Channel
@Binds
@IntoSet
abstract fun provideUpcomingLessonsChannel(channel: UpcomingLessonsChannel): Channel
}

View File

@ -0,0 +1,117 @@
package io.github.wulkanowy.services.alarm
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Build.VERSION_CODES.N
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import dagger.android.AndroidInjection
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel.Companion.CHANNEL_ID
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.toLocalDateTime
import timber.log.Timber
import javax.inject.Inject
class TimetableNotificationReceiver : BroadcastReceiver() {
@Inject
lateinit var studentRepository: StudentRepository
@Inject
lateinit var schedulers: SchedulersProvider
companion object {
const val NOTIFICATION_TYPE_CURRENT = 1
const val NOTIFICATION_TYPE_UPCOMING = 2
const val NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION = 3
const val NOTIFICATION_ID = "id"
const val STUDENT_NAME = "student_name"
const val STUDENT_ID = "student_id"
const val LESSON_TYPE = "type"
const val LESSON_TITLE = "title"
const val LESSON_ROOM = "room"
const val LESSON_NEXT_TITLE = "next_title"
const val LESSON_NEXT_ROOM = "next_room"
const val LESSON_START = "start_timestamp"
const val LESSON_END = "end_timestamp"
}
@SuppressLint("CheckResult")
override fun onReceive(context: Context, intent: Intent) {
Timber.d("Receiving intent... ${intent.toUri(0)}")
AndroidInjection.inject(this, context)
studentRepository.getCurrentStudent(false)
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({
val studentId = intent.getIntExtra(STUDENT_ID, 0)
if (it.studentId == studentId) prepareNotification(context, intent)
else Timber.d("Notification studentId($studentId) differs from current(${it.studentId})")
}, { Timber.e(it) })
}
private fun prepareNotification(context: Context, intent: Intent) {
val type = intent.getIntExtra(LESSON_TYPE, 0)
val notificationId = intent.getIntExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
if (type == NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION) {
return NotificationManagerCompat.from(context).cancel(notificationId)
}
val studentId = intent.getIntExtra(STUDENT_ID, 0)
val studentName = intent.getStringExtra(STUDENT_NAME)
val subject = intent.getStringExtra(LESSON_TITLE)
val room = intent.getStringExtra(LESSON_ROOM)
val start = intent.getLongExtra(LESSON_START, 0)
val end = intent.getLongExtra(LESSON_END, 0)
val nextSubject = intent.getStringExtra(LESSON_NEXT_TITLE)
val nextRoom = intent.getStringExtra(LESSON_NEXT_ROOM)
Timber.d("TimetableNotification receive: type: $type, subject: $subject, start: ${start.toLocalDateTime()}, student: $studentId")
showNotification(context, notificationId, 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("()")) }
)
}
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()
)
}
}

View File

@ -0,0 +1,109 @@
package io.github.wulkanowy.services.alarm
import android.app.AlarmManager
import android.app.AlarmManager.RTC_WAKEUP
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_CANCEL_CURRENT
import android.content.Context
import android.content.Intent
import androidx.core.app.AlarmManagerCompat
import androidx.core.app.NotificationManagerCompat
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_END
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_NEXT_ROOM
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_NEXT_TITLE
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_ROOM
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_START
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_TITLE
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_TYPE
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.NOTIFICATION_ID
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.NOTIFICATION_TYPE_CURRENT
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION
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.toTimestamp
import org.threeten.bp.LocalDateTime
import org.threeten.bp.LocalDateTime.now
import timber.log.Timber
import javax.inject.Inject
class TimetableNotificationSchedulerHelper @Inject constructor(
private val context: Context,
private val alarmManager: AlarmManager,
private val preferencesRepository: PreferencesRepository
) {
private fun getRequestCode(time: LocalDateTime, studentId: Int) = (time.toTimestamp() * studentId).toInt()
private fun getUpcomingLessonTime(index: Int, day: List<Timetable>, lesson: Timetable): LocalDateTime {
return day.getOrNull(index - 1)?.end ?: lesson.start.minusMinutes(30)
}
fun cancelScheduled(lessons: List<Timetable>, studentId: Int = 1) {
lessons.sortedBy { it.start }.forEachIndexed { index, lesson ->
val upcomingTime = getUpcomingLessonTime(index, lessons, lesson)
cancelScheduledTo(upcomingTime..lesson.start, getRequestCode(upcomingTime, 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")
}
}
private fun cancelScheduledTo(range: ClosedRange<LocalDateTime>, requestCode: Int) {
if (now() in range) cancelNotification()
alarmManager.cancel(PendingIntent.getBroadcast(context, requestCode, Intent(), FLAG_CANCEL_CURRENT))
}
fun cancelNotification() = NotificationManagerCompat.from(context).cancel(MainView.Section.TIMETABLE.id)
fun scheduleNotifications(lessons: List<Timetable>, student: Student) {
if (!preferencesRepository.isUpcomingLessonsNotificationsEnable) return cancelScheduled(lessons, student.studentId)
lessons.groupBy { it.date }
.map { it.value.sortedBy { lesson -> lesson.start } }
.map { it.filter { lesson -> !lesson.canceled && lesson.isStudentPlan } }
.map { day ->
day.forEachIndexed { index, lesson ->
val intent = createIntent(student, lesson, day.getOrNull(index + 1))
if (lesson.start > now()) {
scheduleBroadcast(intent, student.studentId, NOTIFICATION_TYPE_UPCOMING, getUpcomingLessonTime(index, day, lesson))
}
if (lesson.end > now()) {
scheduleBroadcast(intent, student.studentId, NOTIFICATION_TYPE_CURRENT, lesson.start)
if (day.lastIndex == index) {
scheduleBroadcast(intent, student.studentId, NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION, lesson.end)
}
}
}
}
}
private fun createIntent(student: Student, lesson: Timetable, nextLesson: Timetable?): Intent {
return Intent(context, TimetableNotificationReceiver::class.java).apply {
putExtra(STUDENT_ID, student.studentId)
putExtra(STUDENT_NAME, student.studentName)
putExtra(LESSON_ROOM, lesson.room)
putExtra(LESSON_START, lesson.start.toTimestamp())
putExtra(LESSON_END, lesson.end.toTimestamp())
putExtra(LESSON_TITLE, lesson.subject)
putExtra(LESSON_NEXT_TITLE, nextLesson?.subject)
putExtra(LESSON_NEXT_ROOM, nextLesson?.room)
}
}
private fun scheduleBroadcast(intent: Intent, studentId: Int, 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_CANCEL_CURRENT)
)
Timber.d("TimetableNotification scheduled: type: $notificationType, subject: ${intent.getStringExtra(LESSON_TITLE)}, start: $time, student: $studentId")
}
}

View File

@ -0,0 +1,31 @@
package io.github.wulkanowy.services.sync.channels
import android.annotation.TargetApi
import android.app.Notification.VISIBILITY_PUBLIC
import android.app.NotificationChannel
import android.app.NotificationManager.IMPORTANCE_DEFAULT
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import io.github.wulkanowy.R
import javax.inject.Inject
@TargetApi(26)
class UpcomingLessonsChannel @Inject constructor(
private val notificationManager: NotificationManagerCompat,
private val context: Context
) : Channel {
companion object {
const val CHANNEL_ID = "lesson_channel"
}
override fun create() {
notificationManager.createNotificationChannel(
NotificationChannel(CHANNEL_ID, context.getString(R.string.channel_upcoming_lessons), IMPORTANCE_DEFAULT).apply {
lockscreenVisibility = VISIBILITY_PUBLIC
setShowBadge(false)
enableVibration(false)
}
)
}
}

View File

@ -4,6 +4,7 @@ import androidx.work.WorkInfo
import com.chuckerteam.chucker.api.ChuckerCollector
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
@ -20,6 +21,7 @@ class SettingsPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val preferencesRepository: PreferencesRepository,
private val timetableNotificationHelper: TimetableNotificationSchedulerHelper,
private val analytics: FirebaseAnalyticsHelper,
private val syncManager: SyncManager,
private val chuckerCollector: ChuckerCollector,
@ -36,17 +38,17 @@ class SettingsPresenter @Inject constructor(
fun onSharedPreferenceChanged(key: String) {
Timber.i("Change settings $key")
with(preferencesRepository) {
preferencesRepository.apply {
when (key) {
serviceEnableKey -> with(syncManager) { if (isServiceEnabled) startPeriodicSyncWorker() else stopSyncWorker() }
servicesIntervalKey, servicesOnlyWifiKey -> syncManager.startPeriodicSyncWorker(true)
isDebugNotificationEnableKey -> chuckerCollector.showNotification = isDebugNotificationEnable
appThemeKey -> view?.recreateView()
isUpcomingLessonsNotificationsEnableKey -> if (!isUpcomingLessonsNotificationsEnable) timetableNotificationHelper.cancelNotification()
appLanguageKey -> view?.run {
updateLanguage(if (appLanguage == "system") appInfo.systemLanguage else appLanguage)
recreateView()
}
else -> Unit
}
}
analytics.logEvent("setting_changed", "name" to key)

View File

@ -4,9 +4,13 @@ import org.threeten.bp.DayOfWeek.FRIDAY
import org.threeten.bp.DayOfWeek.MONDAY
import org.threeten.bp.DayOfWeek.SATURDAY
import org.threeten.bp.DayOfWeek.SUNDAY
import org.threeten.bp.Instant.ofEpochMilli
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime
import org.threeten.bp.LocalDateTime.ofInstant
import org.threeten.bp.Month
import org.threeten.bp.ZoneId
import org.threeten.bp.ZoneOffset
import org.threeten.bp.format.DateTimeFormatter.ofPattern
import org.threeten.bp.format.TextStyle.FULL_STANDALONE
import org.threeten.bp.temporal.TemporalAdjusters.firstInMonth
@ -18,6 +22,10 @@ private const val DATE_PATTERN = "dd.MM.yyyy"
fun String.toLocalDate(format: String = DATE_PATTERN): LocalDate = LocalDate.parse(this, ofPattern(format))
fun LocalDateTime.toTimestamp() = atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneOffset.UTC).toInstant().toEpochMilli()
fun Long.toLocalDateTime() = ofInstant(ofEpochMilli(this), ZoneId.systemDefault())
fun LocalDate.toFormattedString(format: String = DATE_PATTERN): String = format(ofPattern(format))
fun LocalDateTime.toFormattedString(format: String = DATE_PATTERN): String = format(ofPattern(format))

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

View File

@ -121,6 +121,9 @@
<string name="timetable_time">Godziny</string>
<string name="timetable_changes">Zmiany</string>
<string name="timetable_no_items">Brak lekcji w tym dniu</string>
<string name="timetable_now">Teraz: %s</string>
<string name="timetable_next">Za chwilę: %s</string>
<string name="timetable_later">Później: %s</string>
<!--Completed lessons-->
<string name="completed_lessons_title">Lekcje zrealizowane</string>
<string name="completed_lessons_button">Zobacz lekcje zrealizowane</string>
@ -319,6 +322,7 @@
<string name="pref_view_app_language">Język aplikacji</string>
<string name="pref_notify_header">Powiadomienia</string>
<string name="pref_notify_switch">Pokazuj powiadomienia</string>
<string name="pref_notify_upcoming_lessons_switch">Pokazuj powiadomienia o następnych lekcjach</string>
<string name="pref_notify_debug_switch">Pokazuj powiadomienia debugowania</string>
<string name="pref_services_header">Synchronizacja</string>
<string name="pref_services_switch">Automatyczna aktualizacja</string>
@ -344,6 +348,7 @@
<string name="channel_new_message">Nowe wiadomości</string>
<string name="channel_new_notes">Nowe uwagi</string>
<string name="channel_push">Powiadomienia push</string>
<string name="channel_upcoming_lessons">Nadchodzące lekcje</string>
<string name="channel_debug">Debugowanie</string>
<!--Colors-->
<string name="all_black">Czarny</string>

View File

@ -13,6 +13,7 @@
<string name="pref_default_services_interval">60</string>
<bool name="pref_default_services_wifi_only">false</bool>
<bool name="pref_default_notifications_enable">true</bool>
<bool name="pref_default_notification_upcoming_lessons_enable">false</bool>
<bool name="pref_default_notification_debug">false</bool>
<string name="pref_default_grade_modifier_plus">0.33</string>
<string name="pref_default_grade_modifier_minus">0.33</string>

View File

@ -14,6 +14,7 @@
<string name="pref_key_services_wifi_only">services_disable_wifi_only</string>
<string name="pref_key_services_force_sync">services_force_sync</string>
<string name="pref_key_notifications_enable">notifications_enable</string>
<string name="pref_key_notifications_upcoming_lessons_enable">notifications_upcoming_lessons_enable</string>
<string name="pref_key_notification_debug">notification_debug</string>
<string name="pref_key_grade_modifier_plus">grade_modifier_plus</string>
<string name="pref_key_grade_modifier_minus">grade_modifier_minus</string>

View File

@ -130,6 +130,9 @@
<string name="timetable_time">Hours</string>
<string name="timetable_changes">Changes</string>
<string name="timetable_no_items">No lessons this day</string>
<string name="timetable_now">Now: %s</string>
<string name="timetable_next">Next: %s</string>
<string name="timetable_later">Later: %s</string>
<!--Completed lessons-->
@ -356,6 +359,7 @@
<string name="pref_notify_header">Notifications</string>
<string name="pref_notify_switch">Show notifications</string>
<string name="pref_notify_upcoming_lessons_switch">Show upcoming lesson notifications</string>
<string name="pref_notify_debug_switch">Show debug notifications</string>
<string name="pref_services_header">Synchronization</string>
@ -386,6 +390,7 @@
<string name="channel_new_message">New messages</string>
<string name="channel_new_notes">New notes</string>
<string name="channel_push">Push notifications</string>
<string name="channel_upcoming_lessons">Upcoming lessons</string>
<string name="channel_debug">Debug</string>

View File

@ -96,6 +96,11 @@
app:iconSpaceReserved="false"
app:key="@string/pref_key_notifications_enable"
app:title="@string/pref_notify_switch" />
<SwitchPreferenceCompat
app:defaultValue="@bool/pref_default_notification_upcoming_lessons_enable"
app:iconSpaceReserved="false"
app:key="@string/pref_key_notifications_upcoming_lessons_enable"
app:title="@string/pref_notify_upcoming_lessons_switch" />
<SwitchPreferenceCompat
app:defaultValue="@bool/pref_default_notification_debug"
app:iconSpaceReserved="false"