From 29226dd93e9dfcf72ec80e476331d1bcc43c6e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= <m.pich@outlook.com> Date: Wed, 20 May 2020 15:11:01 +0200 Subject: [PATCH] Add notification about upcoming lesson (#578) --- .../timetable/TimetableRepositoryTest.kt | 20 ++- app/src/main/AndroidManifest.xml | 2 + .../github/wulkanowy/data/RepositoryModule.kt | 2 + .../preferences/PreferencesRepository.kt | 4 + .../timetable/TimetableRepository.kt | 10 +- .../io/github/wulkanowy/di/BindingModule.kt | 4 + .../wulkanowy/services/ServicesModule.kt | 11 ++ .../alarm/TimetableNotificationReceiver.kt | 117 ++++++++++++++++++ .../TimetableNotificationSchedulerHelper.kt | 109 ++++++++++++++++ .../sync/channels/UpcomingLessonsChannel.kt | 31 +++++ .../ui/modules/settings/SettingsPresenter.kt | 6 +- .../github/wulkanowy/utils/TimeExtension.kt | 8 ++ .../res/drawable-hdpi/ic_stat_timetable.png | Bin 0 -> 312 bytes .../res/drawable-mdpi/ic_stat_timetable.png | Bin 0 -> 275 bytes .../res/drawable-xhdpi/ic_stat_timetable.png | Bin 0 -> 358 bytes .../res/drawable-xxhdpi/ic_stat_timetable.png | Bin 0 -> 459 bytes .../drawable-xxxhdpi/ic_stat_timetable.png | Bin 0 -> 659 bytes app/src/main/res/values-pl/strings.xml | 5 + .../main/res/values/preferences_defaults.xml | 1 + app/src/main/res/values/preferences_keys.xml | 1 + app/src/main/res/values/strings.xml | 5 + app/src/main/res/xml/scheme_preferences.xml | 5 + 22 files changed, 333 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt create mode 100644 app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt create mode 100644 app/src/main/java/io/github/wulkanowy/services/sync/channels/UpcomingLessonsChannel.kt create mode 100644 app/src/main/res/drawable-hdpi/ic_stat_timetable.png create mode 100644 app/src/main/res/drawable-mdpi/ic_stat_timetable.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_stat_timetable.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_stat_timetable.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_stat_timetable.png diff --git a/app/src/androidTest/java/io/github/wulkanowy/data/repositories/timetable/TimetableRepositoryTest.kt b/app/src/androidTest/java/io/github/wulkanowy/data/repositories/timetable/TimetableRepositoryTest.kt index fdf193a26..75f2f0b83 100644 --- a/app/src/androidTest/java/io/github/wulkanowy/data/repositories/timetable/TimetableRepositoryTest.kt +++ b/app/src/androidTest/java/io/github/wulkanowy/data/repositories/timetable/TimetableRepositoryTest.kt @@ -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() diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4dd70721e..4ec2f7816 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -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" 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 43c27c529..9540372fc 100644 --- a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt @@ -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 diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/preferences/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/preferences/PreferencesRepository.kt index b7a539925..b916bf96f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/preferences/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/preferences/PreferencesRepository.kt @@ -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) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/timetable/TimetableRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/timetable/TimetableRepository.kt index 42812b30f..082b03cf8 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/timetable/TimetableRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/timetable/TimetableRepository.kt @@ -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) } } } } } diff --git a/app/src/main/java/io/github/wulkanowy/di/BindingModule.kt b/app/src/main/java/io/github/wulkanowy/di/BindingModule.kt index ba8c78d3f..1b462964d 100644 --- a/app/src/main/java/io/github/wulkanowy/di/BindingModule.kt +++ b/app/src/main/java/io/github/wulkanowy/di/BindingModule.kt @@ -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 } 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 c7c573e27..b87f0e683 100644 --- a/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt +++ b/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt @@ -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 } 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 new file mode 100644 index 000000000..0130f4673 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt @@ -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() + ) + } +} 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 new file mode 100644 index 000000000..5374c4767 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt @@ -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") + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/channels/UpcomingLessonsChannel.kt b/app/src/main/java/io/github/wulkanowy/services/sync/channels/UpcomingLessonsChannel.kt new file mode 100644 index 000000000..a292c8b53 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/channels/UpcomingLessonsChannel.kt @@ -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) + } + ) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsPresenter.kt index c8545ac0e..09fc2d707 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsPresenter.kt @@ -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) diff --git a/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt index a91f823fa..8d022fc5d 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt @@ -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)) diff --git a/app/src/main/res/drawable-hdpi/ic_stat_timetable.png b/app/src/main/res/drawable-hdpi/ic_stat_timetable.png new file mode 100644 index 0000000000000000000000000000000000000000..201419d5d48501c657d592ea41c27763c5e6eb7f GIT binary patch literal 312 zcmV-80muG{P)<h;3K|Lk000e1NJLTq001Na001Ni1^@s6;Q*MJ00030Nkl<Zc-rll zF$%&k6oy;$5cL8sF2O<QR-8OW@1O_hRUABm4m$K6YU$Xqi%?K7{()EMBJ_nsOVS^{ z!QuUU!%Nc|1Y^ehC{juR5(RTEA%tn6TvC{GZF0ASP%r^Yumu??JLMUJHYNSuffbl? zh1TE>p3-bypiuo>p^LPeGgqjf!K!moOi)p8!4+zErZjvw^m>OZWGn6nHS|z4MdREK z4OfV6{7eI3ODNS`;M5zTNb|ag{8;KCp0d6>lz1z&>3KZ}KX>TgLN^+$HLr_r)YN<B z3LU`P(z}rA-E)N&-~=9qWmMl0%(+4#n1R^vS4T{};rNe<6Zrrz1^oezplKxl0000< KMNUMnLSTY^3Wab0 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_stat_timetable.png b/app/src/main/res/drawable-mdpi/ic_stat_timetable.png new file mode 100644 index 0000000000000000000000000000000000000000..dcfe95f26582f0cb2cee7b00e660fd2f50f7523e GIT binary patch literal 275 zcmV+u0qp*XP)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv0002mNkl<Zc-muN z7`EX5|No*utPR9`c+~Jg)rgW~LBoFt2*sl&6so3y6oVyzH~@%OK{fY54Z?=|plVh@ z^+*yEEXRTP?|;$(NY4qp!3C7Gg(^G+4I)zTDX3msd=@xBT{D%8IGYO9>p+YJcc9@) z3cf>%1(X1yELa9j-=yGWq*y@nIGBp22_KkBi3K%4e3byMA=?6a1d$PrBEp^4ad4Gd z7I@((7P6>f0WIRd3W$GEP*i~QTH&*RABfjN6_N@-dRF7B0b!Q#0I>!cMFmI?&+uzJ Z007MEy7feVSabjY002ovPDHLkV1gACY-j)g literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_timetable.png b/app/src/main/res/drawable-xhdpi/ic_stat_timetable.png new file mode 100644 index 0000000000000000000000000000000000000000..7264bd92a947d264f2b576b17645a1346557cf53 GIT binary patch literal 358 zcmV-s0h#`ZP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm0003kNkl<Zc-rlm zF%AJi7=<-NqHqBhkgYTlH$ZE-gQ&K!Dv49LgKa1k4na$~t!OE*-Xv3OCK`S_lU?s6 zU!(Zne3Q(ut5jkb=Kqo;$uLDnFsGBAWm6!XNH8Z#528RJ1u(Y;24D$RW+@Wu&%Plx zH$!)*t`VsJV)0XO1Xu7(vXAPxffJaiHAn>d;9zOt??m>XClLts&bx~04kZE$W1wf@ zg7A#a1qF&E5EwrrvLMJ(z(pufD1qFc0bZjN_?5t$B1t?7{G&ia@FZr^;31|!4zJN~ z&Ong&?}3N{6yPmHfscmG!NcJbnd+WP1Sa6(@Ok(Q#u9-x*nzu)#jDOOXh{UB`VI9) z?}n|{>#A!Fx}YLGR29(FBEE$);Oj2cS>|7=3{&vD05k7d?=*zzl>h($07*qoM6N<$ Ef`ze{s{jB1 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_stat_timetable.png b/app/src/main/res/drawable-xxhdpi/ic_stat_timetable.png new file mode 100644 index 0000000000000000000000000000000000000000..1fb37b092c4c26e76a349ab679d42edf8797c551 GIT binary patch literal 459 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@Zgyv2V4URX;uunK>+S6QUWWrD+?j8A zZkYV&(VJD>>uw%lFL?i8$?W9i9sLSB$_hkXSMbGddd>ZVedRl)Z3~)Y*X`#nzr->1 zQ)~50%YtV$wwXow3MY+#Ml;NicpjtIJzq+0{R)GA%L(!y#M_RqUZfz<$l<bJCGQf( zXHPcSOx^zJ?$Pe~Z~f+-7QMNp&sg6))!pu-xn-Bdg!#D&^PhI!`LzF_(S=zyQ=F&E z9X9yADs=&K<>O0FZ+(#yUH8=SP~)FB>C+mkM8D7ZCm;P!;!XIS$yL)DPjzh$a@+HE zMR^>PV21-smq9?50FYL?5W?#9GO#&yUM<KG3QWC)2WD+&Q3bOFU(`27nc4+yiFkIC z9i&SsV9V(mM+Jxp-vgD?XQs;o^#G-W<v)1Usr`9$sQpb8o7J>_PWD~X-KRB{9p0ZY zC1H+!rDC}b^Uw1OzCF!uYK%L?8MlGQM0@&^wmnn$AMWb=Iqz}$n^UDbHPY9Fx3%S* v%H64cTX4U_RX*cik$<=6_40s%?${&gU0d$GUCOb13yACK>gTe~DWM4f-M7mX literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_stat_timetable.png b/app/src/main/res/drawable-xxxhdpi/ic_stat_timetable.png new file mode 100644 index 0000000000000000000000000000000000000000..a95cc4f5d9b0dbfcf5513827f0309aa1e2bf89e7 GIT binary patch literal 659 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V2bi|aSW-r^>(hUcZ{Qi+w=|= z7SBXMj%-2QOYT7$9hoAEQ5^61H}<V<<q-Ps*m6LKbBT~^P7VuaV9pi$2kM4DBp>VT zo}B)Hmu2E!mn((o=Nd0_-YY-<^xc_V-)Eg>1X=_FN7{??mbJ}g>|4J1n&DB21!AoX z$AedtI<P1(FmgCBFbOm;%xt!q==-Tv_ifeF2{PLhxg!sEoy@lXE#Q|nSLDA%j`7!% zdNZe=t^0OdU_s!vQ;t7f*PRrOEc+O+ZRWM7;oP6R?0Ek^vY(t=<Wo0Ax+pyEl$Q&m zmf7Wbcc#pL(vtIiz2gane`o5o`Jb-zZa4mUIW3=8G9kq8=FLDq4Tqa){}k@G^E_Dp zlUpzLS#w?9lK6ef{6c&i^tU^+S!{frxa!CEy~$?bEAwta9Se4M0rP3e`z<Tfr@uCI zFar7j=9vvVsmjOPdf<GZ)OKkN0alPNVQP-ZDZH2aw``Bj=bR%qIt1Yo(-wZ;v*pR- z7YAl*BP-t^n5^su4km~J4gak+#D6M3Wd1Vt#ODc&6&dr3-4&U2cJxGBD5ra#YvXw! zpR#vGL4)N_%M^zHEFbI2zsfwFqUZYW(r@Q)cdpL2&6j?#^v$&UPo~*yGgte+<ztqM ze#O<aT^l6-20sdaBK_0mi_qKm|M?|0h#p=go1(dhL3Q@a-TR-MDF1e}>DciNZg~@Q zH#zs6D74h=7Swg_HoLsz?2lV%c_&~IeuclDVSCvhefvfa4NwB`boFyt=akR{00hbj ALI3~& literal 0 HcmV?d00001 diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index bff422927..e6fe6da64 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -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> diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index 29e8751e4..c8704a50b 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -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> diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index 6d683f3f8..1d43f79fd 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -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> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0a837c46b..bfaece677 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -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> diff --git a/app/src/main/res/xml/scheme_preferences.xml b/app/src/main/res/xml/scheme_preferences.xml index b4adabb98..d890fdb24 100644 --- a/app/src/main/res/xml/scheme_preferences.xml +++ b/app/src/main/res/xml/scheme_preferences.xml @@ -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"