1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2024-09-20 01:49:09 -05:00

Add timetable changes, attendance notifications and refactor notification deeplinks (#1547)

This commit is contained in:
Mateusz Idziejczak 2021-11-06 22:21:34 +01:00 committed by GitHub
parent 4401df6203
commit f88d44f0ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 3819 additions and 601 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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.Migration40
import io.github.wulkanowy.data.db.migrations.Migration41 import io.github.wulkanowy.data.db.migrations.Migration41
import io.github.wulkanowy.data.db.migrations.Migration42 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.Migration5
import io.github.wulkanowy.data.db.migrations.Migration6 import io.github.wulkanowy.data.db.migrations.Migration6
import io.github.wulkanowy.data.db.migrations.Migration7 import io.github.wulkanowy.data.db.migrations.Migration7
@ -151,7 +152,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val VERSION_SCHEMA = 42 const val VERSION_SCHEMA = 43
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(), Migration2(),
@ -194,7 +195,8 @@ abstract class AppDatabase : RoomDatabase() {
Migration39(), Migration39(),
Migration40(), Migration40(),
Migration41(sharedPrefProvider), Migration41(sharedPrefProvider),
Migration42() Migration42(),
Migration43()
) )
fun newInstance( fun newInstance(

View File

@ -11,6 +11,11 @@ import javax.inject.Singleton
@Dao @Dao
interface AttendanceDao : BaseDao<Attendance> { interface AttendanceDao : BaseDao<Attendance> {
@Query("SELECT * FROM Attendance WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end") @Query("SELECT * FROM Attendance WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :start AND date <= :end")
fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Flow<List<Attendance>> fun loadAll(
diaryId: Int,
studentId: Int,
start: LocalDate,
end: LocalDate
): Flow<List<Attendance>>
} }

View File

@ -47,4 +47,7 @@ data class Attendance(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
var id: Long = 0 var id: Long = 0
@ColumnInfo(name = "is_notified")
var isNotified: Boolean = true
} }

View File

@ -50,4 +50,7 @@ data class Timetable(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
var id: Long = 0 var id: Long = 0
@ColumnInfo(name = "is_notified")
var isNotified: Boolean = true
} }

View File

@ -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")
}
}

View File

@ -1,36 +1,19 @@
package io.github.wulkanowy.data.pojos package io.github.wulkanowy.data.pojos
import androidx.annotation.DrawableRes import android.content.Intent
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import io.github.wulkanowy.services.sync.notifications.NotificationType 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<NotificationData>,
val title: String,
val content: String,
val intentToStart: Intent,
val type: NotificationType 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<String>,
) : 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<String>,
) : NotificationData

View File

@ -14,6 +14,7 @@ import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
@ -38,6 +39,7 @@ class AttendanceRepository @Inject constructor(
start: LocalDate, start: LocalDate,
end: LocalDate, end: LocalDate,
forceRefresh: Boolean, forceRefresh: Boolean,
notify: Boolean = false,
) = networkBoundResource( ) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { shouldFetch = {
@ -56,13 +58,28 @@ class AttendanceRepository @Inject constructor(
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
attendanceDb.deleteAll(old uniqueSubtract 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)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
}, },
filterResult = { it.filter { item -> item.date in start..end } } filterResult = { it.filter { item -> item.date in start..end } }
) )
fun getAttendanceFromDatabase(
semester: Semester,
start: LocalDate,
end: LocalDate
): Flow<List<Attendance>> {
return attendanceDb.loadAll(semester.diaryId, semester.studentId, start, end)
}
suspend fun updateTimetable(timetable: List<Attendance>) {
return attendanceDb.updateAll(timetable)
}
suspend fun excuseForAbsence( suspend fun excuseForAbsence(
student: Student, semester: Semester, student: Student, semester: Semester,
absenceList: List<Attendance>, reason: String? = null absenceList: List<Attendance>, reason: String? = null

View File

@ -47,6 +47,7 @@ class TimetableRepository @Inject constructor(
end: LocalDate, end: LocalDate,
forceRefresh: Boolean, forceRefresh: Boolean,
refreshAdditional: Boolean = false, refreshAdditional: Boolean = false,
notify: Boolean = false
) = networkBoundResource( ) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { (timetable, additional, headers) -> shouldFetch = { (timetable, additional, headers) ->
@ -67,7 +68,7 @@ class TimetableRepository @Inject constructor(
timetableFull.mapToEntities(semester) timetableFull.mapToEntities(semester)
}, },
saveFetchResult = { timetableOld, timetableNew -> saveFetchResult = { timetableOld, timetableNew ->
refreshTimetable(student, timetableOld.lessons, timetableNew.lessons) refreshTimetable(student, timetableOld.lessons, timetableNew.lessons, notify)
refreshAdditional(timetableOld.additional, timetableNew.additional) refreshAdditional(timetableOld.additional, timetableNew.additional)
refreshDayHeaders(timetableOld.headers, timetableNew.headers) refreshDayHeaders(timetableOld.headers, timetableNew.headers)
@ -117,13 +118,28 @@ class TimetableRepository @Inject constructor(
} }
} }
fun getTimetableFromDatabase(
semester: Semester,
from: LocalDate,
end: LocalDate
): Flow<List<Timetable>> {
return timetableDb.loadAll(semester.diaryId, semester.studentId, from, end)
}
suspend fun updateTimetable(timetable: List<Timetable>) {
return timetableDb.updateAll(timetable)
}
private suspend fun refreshTimetable( private suspend fun refreshTimetable(
student: Student, student: Student,
lessonsOld: List<Timetable>, lessonsOld: List<Timetable>,
lessonsNew: List<Timetable>, lessonsNew: List<Timetable>,
notify: Boolean
) { ) {
val lessonsToRemove = lessonsOld uniqueSubtract lessonsNew 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.deleteAll(lessonsToRemove)
timetableDb.insertAll(lessonsToAdd) timetableDb.insertAll(lessonsToAdd)

View File

@ -15,6 +15,7 @@ import dagger.multibindings.IntoSet
import io.github.wulkanowy.services.sync.channels.Channel import io.github.wulkanowy.services.sync.channels.Channel
import io.github.wulkanowy.services.sync.channels.DebugChannel import io.github.wulkanowy.services.sync.channels.DebugChannel
import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel 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.NewConferencesChannel
import io.github.wulkanowy.services.sync.channels.NewExamChannel import io.github.wulkanowy.services.sync.channels.NewExamChannel
import io.github.wulkanowy.services.sync.channels.NewGradesChannel 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.NewNotesChannel
import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel
import io.github.wulkanowy.services.sync.channels.PushChannel 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.channels.UpcomingLessonsChannel
import io.github.wulkanowy.services.sync.works.AttendanceSummaryWork import io.github.wulkanowy.services.sync.works.AttendanceSummaryWork
import io.github.wulkanowy.services.sync.works.AttendanceWork import io.github.wulkanowy.services.sync.works.AttendanceWork
@ -167,4 +169,12 @@ abstract class ServicesModule {
@Binds @Binds
@IntoSet @IntoSet
abstract fun provideUpcomingLessonsChannel(channel: UpcomingLessonsChannel): Channel abstract fun provideUpcomingLessonsChannel(channel: UpcomingLessonsChannel): Channel
@Binds
@IntoSet
abstract fun provideChangeTimetableChannel(channel: TimetableChangeChannel): Channel
@Binds
@IntoSet
abstract fun provideNewAttendanceChannel(channel: NewAttendanceChannel): Channel
} }

View File

@ -15,8 +15,8 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.HiltBroadcastReceiver import io.github.wulkanowy.services.HiltBroadcastReceiver
import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel.Companion.CHANNEL_ID 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.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResource
import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.toLocalDateTime import io.github.wulkanowy.utils.toLocalDateTime
@ -41,7 +41,7 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
const val NOTIFICATION_TYPE_UPCOMING = 2 const val NOTIFICATION_TYPE_UPCOMING = 2
const val NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION = 3 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_NAME = "student_name"
const val STUDENT_ID = "student_id" const val STUDENT_ID = "student_id"
@ -71,11 +71,10 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
private fun prepareNotification(context: Context, intent: Intent) { private fun prepareNotification(context: Context, intent: Intent) {
val type = intent.getIntExtra(LESSON_TYPE, 0) val type = intent.getIntExtra(LESSON_TYPE, 0)
val notificationId = intent.getIntExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
val isPersistent = preferencesRepository.isUpcomingLessonsNotificationsPersistent val isPersistent = preferencesRepository.isUpcomingLessonsNotificationsPersistent
if (type == NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION) { 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) 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") 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, if (type == NOTIFICATION_TYPE_CURRENT) end else start, end - start,
context.getString( context.getString(
if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next, if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next,
@ -109,7 +109,6 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
private fun showNotification( private fun showNotification(
context: Context, context: Context,
notificationId: Int,
isPersistent: Boolean, isPersistent: Boolean,
studentName: String?, studentName: String?,
countDown: Long, countDown: Long,
@ -118,7 +117,7 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
next: String? next: String?
) { ) {
NotificationManagerCompat.from(context) NotificationManagerCompat.from(context)
.notify(notificationId, NotificationCompat.Builder(context, CHANNEL_ID) .notify(NOTIFICATION_ID, NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(title) .setContentTitle(title)
.setContentText(next) .setContentText(next)
.setAutoCancel(false) .setAutoCancel(false)
@ -138,8 +137,8 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
.setContentIntent( .setContentIntent(
PendingIntent.getActivity( PendingIntent.getActivity(
context, context,
MainView.Section.TIMETABLE.id, NOTIFICATION_ID,
MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), MainActivity.getStartIntent(context, Destination.Timetable(), true),
FLAG_UPDATE_CURRENT FLAG_UPDATE_CURRENT
) )
) )

View File

@ -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.NOTIFICATION_TYPE_UPCOMING
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_ID import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_ID
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_NAME 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.DispatchersProvider
import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.nickOrName
import io.github.wulkanowy.utils.toTimestamp import io.github.wulkanowy.utils.toTimestamp
@ -79,7 +78,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
} }
fun cancelNotification() = fun cancelNotification() =
NotificationManagerCompat.from(context).cancel(MainView.Section.TIMETABLE.id) NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID)
suspend fun scheduleNotifications(lessons: List<Timetable>, student: Student) { suspend fun scheduleNotifications(lessons: List<Timetable>, student: Student) {
if (!preferencesRepository.isUpcomingLessonsNotificationsEnable) { if (!preferencesRepository.isUpcomingLessonsNotificationsEnable) {
@ -156,7 +155,6 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
AlarmManagerCompat.setExactAndAllowWhileIdle( AlarmManagerCompat.setExactAndAllowWhileIdle(
alarmManager, RTC_WAKEUP, time.toTimestamp(), alarmManager, RTC_WAKEUP, time.toTimestamp(),
PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also { PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also {
it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
it.putExtra(LESSON_TYPE, notificationType) it.putExtra(LESSON_TYPE, notificationType)
}, FLAG_UPDATE_CURRENT) }, FLAG_UPDATE_CURRENT)
) )

View File

@ -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"
}
}

View File

@ -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
})
}
}

View File

@ -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
})
}
}

View File

@ -4,19 +4,15 @@ import android.annotation.SuppressLint
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import androidx.annotation.PluralsRes
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Notification import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.data.db.entities.Student 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.NotificationData
import io.github.wulkanowy.data.pojos.OneNotificationData
import io.github.wulkanowy.data.repositories.NotificationRepository 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.getCompatBitmap
import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.nickOrName
@ -27,102 +23,17 @@ import kotlin.random.Random
class AppNotificationManager @Inject constructor( class AppNotificationManager @Inject constructor(
private val notificationManager: NotificationManagerCompat, private val notificationManager: NotificationManagerCompat,
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val appInfo: AppInfo,
private val notificationRepository: NotificationRepository 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") @SuppressLint("InlinedApi")
private fun getDefaultNotificationBuilder(notificationData: NotificationData): NotificationCompat.Builder { suspend fun sendSingleNotification(
val pendingIntentsFlags = if (appInfo.systemVersion >= Build.VERSION_CODES.M) { notificationData: NotificationData,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE notificationType: NotificationType,
} else { student: Student
PendingIntent.FLAG_UPDATE_CURRENT ) {
} val notification = NotificationCompat.Builder(context, notificationType.channel)
.setLargeIcon(context.getCompatBitmap(notificationType.icon, R.color.colorPrimary))
return NotificationCompat.Builder(context, notificationData.type.channel)
.setLargeIcon(context.getCompatBitmap(notificationData.icon, R.color.colorPrimary))
.setSmallIcon(R.drawable.ic_stat_all) .setSmallIcon(R.drawable.ic_stat_all)
.setAutoCancel(true) .setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL) .setDefaults(NotificationCompat.DEFAULT_ALL)
@ -132,31 +43,122 @@ class AppNotificationManager @Inject constructor(
.setContentIntent( .setContentIntent(
PendingIntent.getActivity( PendingIntent.getActivity(
context, context,
notificationData.startMenu.id, Random.nextInt(),
MainActivity.getStartIntent(context, notificationData.startMenu, true), notificationData.intentToStart,
pendingIntentsFlags 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( private suspend fun saveNotification(
title: String,
content: String,
notificationData: NotificationData, notificationData: NotificationData,
notificationType: NotificationType,
student: Student student: Student
) { ) {
val notificationEntity = Notification( val notificationEntity = Notification(
studentId = student.id, studentId = student.id,
title = title, title = notificationData.title,
content = content, content = notificationData.content,
type = notificationData.type, type = notificationType,
date = LocalDateTime.now() date = LocalDateTime.now()
) )
notificationRepository.saveNotification(notificationEntity) notificationRepository.saveNotification(notificationEntity)
} }
private fun getQuantityString(@PluralsRes res: Int, arg: Int): String {
return context.resources.getQuantityString(res, arg, arg)
}
} }

View File

@ -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<Timetable>, 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<Timetable>): List<String> {
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)
}
}
}
}
}
}

View File

@ -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<Attendance>, 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)
}
}

View File

@ -1,34 +1,52 @@
package io.github.wulkanowy.services.sync.notifications 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.R
import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.db.entities.Student 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.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 io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDateTime import java.time.LocalDateTime
import javax.inject.Inject import javax.inject.Inject
class NewConferenceNotification @Inject constructor( class NewConferenceNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager private val appNotificationManager: AppNotificationManager,
@ApplicationContext private val context: Context
) { ) {
suspend fun notify(items: List<Conference>, student: Student) { suspend fun notify(items: List<Conference>, student: Student) {
val today = LocalDateTime.now() val today = LocalDateTime.now()
val lines = items.filter { !it.date.isBefore(today) }.map { val lines = items.filter { !it.date.isBefore(today) }
.map {
"${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}" "${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}"
}.ifEmpty { return } }
.ifEmpty { return }
val notification = MultipleNotificationsData( val notificationDataList = lines.map {
type = NotificationType.NEW_CONFERENCE, NotificationData(
icon = R.drawable.ic_more_conferences, title = context.getPlural(R.plurals.conference_notify_new_item_title, 1),
titleStringRes = R.plurals.conference_notify_new_item_title, content = it,
contentStringRes = R.plurals.conference_notify_new_items, intentToStart = MainActivity.getStartIntent(context, Destination.Conference, true)
summaryStringRes = R.plurals.conference_number_item, )
startMenu = MainView.Section.CONFERENCE, }
lines = lines
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)
} }
} }

View File

@ -1,34 +1,52 @@
package io.github.wulkanowy.services.sync.notifications 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.R
import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Student 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.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 io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
class NewExamNotification @Inject constructor( class NewExamNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager private val appNotificationManager: AppNotificationManager,
@ApplicationContext private val context: Context
) { ) {
suspend fun notify(items: List<Exam>, student: Student) { suspend fun notify(items: List<Exam>, student: Student) {
val today = LocalDate.now() val today = LocalDate.now()
val lines = items.filter { !it.date.isBefore(today) }.map { val lines = items.filter { !it.date.isBefore(today) }
.map {
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}" "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}"
}.ifEmpty { return } }
.ifEmpty { return }
val notification = MultipleNotificationsData( val notificationDataList = lines.map {
type = NotificationType.NEW_EXAM, NotificationData(
icon = R.drawable.ic_main_exam, title = context.getPlural(R.plurals.exam_notify_new_item_title, 1),
titleStringRes = R.plurals.exam_notify_new_item_title, content = it,
contentStringRes = R.plurals.exam_notify_new_item_content, intentToStart = MainActivity.getStartIntent(context, Destination.Exam, true),
summaryStringRes = R.plurals.exam_number_item, )
startMenu = MainView.Section.EXAM, }
lines = lines
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)
} }
} }

View File

@ -1,62 +1,88 @@
package io.github.wulkanowy.services.sync.notifications 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.R
import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Student 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.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 io.github.wulkanowy.utils.getPlural
import javax.inject.Inject import javax.inject.Inject
class NewGradeNotification @Inject constructor( class NewGradeNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager private val appNotificationManager: AppNotificationManager,
@ApplicationContext private val context: Context
) { ) {
suspend fun notifyDetails(items: List<Grade>, student: Student) { suspend fun notifyDetails(items: List<Grade>, student: Student) {
val notification = MultipleNotificationsData( val notificationDataList = items.map {
type = NotificationType.NEW_GRADE_DETAILS, NotificationData(
icon = R.drawable.ic_stat_grade, title = context.getPlural(R.plurals.grade_new_items, 1),
titleStringRes = R.plurals.grade_new_items, content = "${it.subject}: ${it.entry}",
contentStringRes = R.plurals.grade_notify_new_items, intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true),
summaryStringRes = R.plurals.grade_number_item, )
startMenu = MainView.Section.GRADE,
lines = items.map {
"${it.subject}: ${it.entry}"
} }
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<GradeSummary>, student: Student) { suspend fun notifyPredicted(items: List<GradeSummary>, student: Student) {
val notification = MultipleNotificationsData( val notificationDataList = items.map {
type = NotificationType.NEW_GRADE_PREDICTED, NotificationData(
icon = R.drawable.ic_stat_grade, title = context.getPlural(R.plurals.grade_new_items_predicted, 1),
titleStringRes = R.plurals.grade_new_items_predicted, content = "${it.subject}: ${it.predictedGrade}",
contentStringRes = R.plurals.grade_notify_new_items_predicted, intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true),
summaryStringRes = R.plurals.grade_number_item, )
startMenu = MainView.Section.GRADE,
lines = items.map {
"${it.subject}: ${it.predictedGrade}"
} }
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<GradeSummary>, student: Student) { suspend fun notifyFinal(items: List<GradeSummary>, student: Student) {
val notification = MultipleNotificationsData( val notificationDataList = items.map {
type = NotificationType.NEW_GRADE_FINAL, NotificationData(
icon = R.drawable.ic_stat_grade, title = context.getPlural(R.plurals.grade_new_items_final, 1),
titleStringRes = R.plurals.grade_new_items_final, content = "${it.subject}: ${it.finalGrade}",
contentStringRes = R.plurals.grade_notify_new_items_final, intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true),
summaryStringRes = R.plurals.grade_number_item, )
startMenu = MainView.Section.GRADE,
lines = items.map {
"${it.subject}: ${it.finalGrade}"
} }
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)
} }
} }

View File

@ -1,34 +1,52 @@
package io.github.wulkanowy.services.sync.notifications 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.R
import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.Student 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.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 io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
class NewHomeworkNotification @Inject constructor( class NewHomeworkNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager private val appNotificationManager: AppNotificationManager,
@ApplicationContext private val context: Context
) { ) {
suspend fun notify(items: List<Homework>, student: Student) { suspend fun notify(items: List<Homework>, student: Student) {
val today = LocalDate.now() val today = LocalDate.now()
val lines = items.filter { !it.date.isBefore(today) }.map { val lines = items.filter { !it.date.isBefore(today) }
.map {
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}" "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}"
}.ifEmpty { return } }
.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, type = NotificationType.NEW_HOMEWORK,
icon = R.drawable.ic_more_homework, notificationDataList = notificationDataList
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
) )
appNotificationManager.sendNotification(notification, student) appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
} }
} }

View File

@ -1,26 +1,34 @@
package io.github.wulkanowy.services.sync.notifications 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.R
import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.OneNotificationData import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.main.MainActivity
import javax.inject.Inject import javax.inject.Inject
class NewLuckyNumberNotification @Inject constructor( class NewLuckyNumberNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager private val appNotificationManager: AppNotificationManager,
@ApplicationContext private val context: Context
) { ) {
suspend fun notify(item: LuckyNumber, student: Student) { suspend fun notify(item: LuckyNumber, student: Student) {
val notification = OneNotificationData( val notificationData = NotificationData(
type = NotificationType.NEW_LUCKY_NUMBER, title = context.getString(R.string.lucky_number_notify_new_item_title),
icon = R.drawable.ic_stat_luckynumber, content = context.getString(
titleStringRes = R.string.lucky_number_notify_new_item_title, R.string.lucky_number_notify_new_item,
contentStringRes = R.string.lucky_number_notify_new_item, item.luckyNumber.toString()
startMenu = MainView.Section.LUCKY_NUMBER, ),
contentValues = listOf(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
)
} }
} }

View File

@ -1,29 +1,39 @@
package io.github.wulkanowy.services.sync.notifications 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.R
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Student 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.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 io.github.wulkanowy.utils.getPlural
import javax.inject.Inject import javax.inject.Inject
class NewMessageNotification @Inject constructor( class NewMessageNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager private val appNotificationManager: AppNotificationManager,
@ApplicationContext private val context: Context
) { ) {
suspend fun notify(items: List<Message>, student: Student) { suspend fun notify(items: List<Message>, student: Student) {
val notification = MultipleNotificationsData( val notificationDataList = items.map {
type = NotificationType.NEW_MESSAGE, NotificationData(
icon = R.drawable.ic_stat_message, title = context.getPlural(R.plurals.message_new_items, 1),
titleStringRes = R.plurals.message_new_items, content = "${it.sender}: ${it.subject}",
contentStringRes = R.plurals.message_notify_new_items, intentToStart = MainActivity.getStartIntent(context, Destination.Message, true),
summaryStringRes = R.plurals.message_number_item, )
startMenu = MainView.Section.MESSAGE,
lines = items.map {
"${it.sender}: ${it.subject}"
} }
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)
} }
} }

View File

@ -1,42 +1,46 @@
package io.github.wulkanowy.services.sync.notifications 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.R
import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Student 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.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 import javax.inject.Inject
class NewNoteNotification @Inject constructor( class NewNoteNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager private val appNotificationManager: AppNotificationManager,
@ApplicationContext private val context: Context
) { ) {
suspend fun notify(items: List<Note>, student: Student) { suspend fun notify(items: List<Note>, student: Student) {
val notification = MultipleNotificationsData( val notificationDataList = items.map {
type = NotificationType.NEW_NOTE, val titleRes = when (NoteCategory.getByValue(it.categoryType)) {
icon = R.drawable.ic_stat_note,
titleStringRes = when (NoteCategory.getByValue(items.first().categoryType)) {
NoteCategory.POSITIVE -> R.plurals.praise_new_items NoteCategory.POSITIVE -> R.plurals.praise_new_items
NoteCategory.NEUTRAL -> R.plurals.neutral_note_new_items NoteCategory.NEUTRAL -> R.plurals.neutral_note_new_items
else -> R.plurals.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)
} }
} }

View File

@ -1,29 +1,56 @@
package io.github.wulkanowy.services.sync.notifications 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.R
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Student 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.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 io.github.wulkanowy.utils.getPlural
import javax.inject.Inject import javax.inject.Inject
class NewSchoolAnnouncementNotification @Inject constructor( class NewSchoolAnnouncementNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager private val appNotificationManager: AppNotificationManager,
@ApplicationContext private val context: Context
) { ) {
suspend fun notify(items: List<SchoolAnnouncement>, student: Student) { suspend fun notify(items: List<SchoolAnnouncement>, student: Student) {
val notification = MultipleNotificationsData( val notificationDataList = items.map {
type = NotificationType.NEW_ANNOUNCEMENT, NotificationData(
icon = R.drawable.ic_all_about, intentToStart = MainActivity.getStartIntent(
titleStringRes = R.plurals.school_announcement_notify_new_item_title, context = context,
contentStringRes = R.plurals.school_announcement_notify_new_items, destination = Destination.SchoolAnnouncement,
summaryStringRes = R.plurals.school_announcement_number_item, startNewTask = true
startMenu = MainView.Section.SCHOOL_ANNOUNCEMENT, ),
lines = items.map { title = context.getPlural(
"${it.subject}: ${it.content}" 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)
} }
} }

View File

@ -1,6 +1,8 @@
package io.github.wulkanowy.services.sync.notifications 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.LuckyNumberChannel
import io.github.wulkanowy.services.sync.channels.NewAttendanceChannel
import io.github.wulkanowy.services.sync.channels.NewConferencesChannel import io.github.wulkanowy.services.sync.channels.NewConferencesChannel
import io.github.wulkanowy.services.sync.channels.NewExamChannel import io.github.wulkanowy.services.sync.channels.NewExamChannel
import io.github.wulkanowy.services.sync.channels.NewGradesChannel 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.NewNotesChannel
import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel
import io.github.wulkanowy.services.sync.channels.PushChannel 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) { enum class NotificationType(
NEW_CONFERENCE("new_conferences_group", NewConferencesChannel.CHANNEL_ID), val group: String?,
NEW_EXAM("new_exam_group", NewExamChannel.CHANNEL_ID), val channel: String,
NEW_GRADE_DETAILS("new_grade_details_group", NewGradesChannel.CHANNEL_ID), val icon: Int
NEW_GRADE_PREDICTED("new_grade_predicted_group", NewGradesChannel.CHANNEL_ID), ) {
NEW_GRADE_FINAL("new_grade_final_group", NewGradesChannel.CHANNEL_ID), NEW_CONFERENCE(
NEW_HOMEWORK("new_homework_group", NewHomeworkChannel.CHANNEL_ID), group = "new_conferences_group",
NEW_LUCKY_NUMBER("lucky_number_group", LuckyNumberChannel.CHANNEL_ID), channel = NewConferencesChannel.CHANNEL_ID,
NEW_MESSAGE("new_message_group", NewMessagesChannel.CHANNEL_ID), icon = R.drawable.ic_more_conferences,
NEW_NOTE("new_notes_group", NewNotesChannel.CHANNEL_ID), ),
NEW_ANNOUNCEMENT("new_school_announcements_group", NewSchoolAnnouncementsChannel.CHANNEL_ID), NEW_EXAM(
PUSH(null, PushChannel.CHANNEL_ID) 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
)
} }

View File

@ -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.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.AttendanceRepository import io.github.wulkanowy.data.repositories.AttendanceRepository
import io.github.wulkanowy.utils.monday import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.services.sync.notifications.NewAttendanceNotification
import io.github.wulkanowy.utils.previousOrSameSchoolDay
import io.github.wulkanowy.utils.waitForResult import io.github.wulkanowy.utils.waitForResult
import kotlinx.coroutines.flow.first
import java.time.LocalDate.now import java.time.LocalDate.now
import javax.inject.Inject import javax.inject.Inject
class AttendanceWork @Inject constructor( class AttendanceWork @Inject constructor(
private val attendanceRepository: AttendanceRepository private val attendanceRepository: AttendanceRepository,
private val newAttendanceNotification: NewAttendanceNotification,
private val preferencesRepository: PreferencesRepository
) : Work { ) : Work {
override suspend fun doWork(student: Student, semester: Semester) { 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() .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
})
}
} }
} }

View File

@ -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.Semester
import io.github.wulkanowy.data.db.entities.Student 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.data.repositories.TimetableRepository
import io.github.wulkanowy.utils.monday import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification
import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.nextOrSameSchoolDay
import io.github.wulkanowy.utils.waitForResult import io.github.wulkanowy.utils.waitForResult
import kotlinx.coroutines.flow.first
import java.time.LocalDate.now import java.time.LocalDate.now
import javax.inject.Inject import javax.inject.Inject
class TimetableWork @Inject constructor( class TimetableWork @Inject constructor(
private val timetableRepository: TimetableRepository private val timetableRepository: TimetableRepository,
private val changeTimetableNotification: ChangeTimetableNotification,
private val preferencesRepository: PreferencesRepository
) : Work { ) : Work {
override suspend fun doWork(student: Student, semester: Semester) { 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
})
}
} }
} }

View File

@ -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()
}
}

View File

@ -9,7 +9,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.enums.SentExcuseStatus import io.github.wulkanowy.data.enums.SentExcuseStatus
import io.github.wulkanowy.databinding.ItemAttendanceBinding 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 io.github.wulkanowy.utils.isExcusableOrNotExcused
import javax.inject.Inject import javax.inject.Inject
@ -36,7 +36,7 @@ class AttendanceAdapter @Inject constructor() :
with(holder.binding) { with(holder.binding) {
attendanceItemNumber.text = item.number.toString() attendanceItemNumber.text = item.number.toString()
attendanceItemSubject.text = item.subject attendanceItemSubject.text = item.subject
attendanceItemDescription.setText(item.description) attendanceItemDescription.setText(item.descriptionRes)
attendanceItemAlert.visibility = item.run { if (absence && !excused) View.VISIBLE else View.INVISIBLE } attendanceItemAlert.visibility = item.run { if (absence && !excused) View.VISIBLE else View.INVISIBLE }
attendanceItemNumber.visibility = View.GONE attendanceItemNumber.visibility = View.GONE
attendanceItemExcuseInfo.visibility = View.GONE attendanceItemExcuseInfo.visibility = View.GONE

View File

@ -7,7 +7,7 @@ import android.view.ViewGroup
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.databinding.DialogAttendanceBinding 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.lifecycleAwareVariable
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
@ -45,7 +45,7 @@ class AttendanceDialog : DialogFragment() {
with(binding) { with(binding) {
attendanceDialogSubjectValue.text = attendance.subject attendanceDialogSubjectValue.text = attendance.subject
attendanceDialogDescriptionValue.setText(attendance.description) attendanceDialogDescriptionValue.setText(attendance.descriptionRes)
attendanceDialogDateValue.text = attendance.date.toFormattedString() attendanceDialogDateValue.text = attendance.date.toFormattedString()
attendanceDialogNumberValue.text = attendance.number.toString() attendanceDialogNumberValue.text = attendance.number.toString()
attendanceDialogClose.setOnClickListener { dismiss() } attendanceDialogClose.setOnClickListener { dismiss() }

View File

@ -3,6 +3,8 @@ package io.github.wulkanowy.ui.modules.debug.notification
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.StudentRepository 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.NewConferenceNotification
import io.github.wulkanowy.services.sync.notifications.NewExamNotification import io.github.wulkanowy.services.sync.notifications.NewExamNotification
import io.github.wulkanowy.services.sync.notifications.NewGradeNotification 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.services.sync.notifications.NewSchoolAnnouncementNotification
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler 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.debugConferenceItems
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugExamItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugExamItems
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeDetailsItems 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.debugMessageItems
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugNoteItems 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.debugSchoolAnnouncementItems
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugTimetableItems
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -37,6 +41,8 @@ class NotificationDebugPresenter @Inject constructor(
private val newNoteNotification: NewNoteNotification, private val newNoteNotification: NewNoteNotification,
private val newSchoolAnnouncementNotification: NewSchoolAnnouncementNotification, private val newSchoolAnnouncementNotification: NewSchoolAnnouncementNotification,
private val newLuckyNumberNotification: NewLuckyNumberNotification, private val newLuckyNumberNotification: NewLuckyNumberNotification,
private val changeTimetableNotification: ChangeTimetableNotification,
private val newAttendanceNotification: NewAttendanceNotification,
) : BasePresenter<NotificationDebugView>(errorHandler, studentRepository) { ) : BasePresenter<NotificationDebugView>(errorHandler, studentRepository) {
private val items = listOf( private val items = listOf(
@ -64,6 +70,12 @@ class NotificationDebugPresenter @Inject constructor(
NotificationDebugItem(R.string.note_title) { n -> NotificationDebugItem(R.string.note_title) { n ->
withStudent { newNoteNotification.notify(debugNoteItems.take(n), it) } 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 -> NotificationDebugItem(R.string.school_announcement_title) { n ->
withStudent { withStudent {
newSchoolAnnouncementNotification.notify(debugSchoolAnnouncementItems.take(n), it) newSchoolAnnouncementNotification.notify(debugSchoolAnnouncementItems.take(n), it)

View File

@ -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 = ""
)

View File

@ -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
)

View File

@ -66,7 +66,14 @@ class LoginStudentSelectFragment :
} }
override fun openMainView() { 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) { override fun showProgress(show: Boolean) {

View File

@ -18,8 +18,8 @@ import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.data.repositories.LuckyNumberRepository import io.github.wulkanowy.data.repositories.LuckyNumberRepository
import io.github.wulkanowy.data.repositories.StudentRepository 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.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.toFirstResult import io.github.wulkanowy.utils.toFirstResult
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import timber.log.Timber import timber.log.Timber
@ -39,6 +39,8 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
companion object { companion object {
const val LUCKY_NUMBER_PENDING_INTENT_ID = 200
fun getStudentWidgetKey(appWidgetId: Int) = "lucky_number_widget_student_$appWidgetId" fun getStudentWidgetKey(appWidgetId: Int) = "lucky_number_widget_student_$appWidgetId"
fun getThemeWidgetKey(appWidgetId: Int) = "lucky_number_widget_theme_$appWidgetId" fun getThemeWidgetKey(appWidgetId: Int) = "lucky_number_widget_theme_$appWidgetId"
@ -48,16 +50,29 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
fun getWidthWidgetKey(appWidgetId: Int) = "lucky_number_widget_width_$appWidgetId" 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) super.onUpdate(context, appWidgetManager, appWidgetIds)
appWidgetIds?.forEach { appWidgetId -> 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 remoteView =
val appIntent = PendingIntent.getActivity(context, MainView.Section.LUCKY_NUMBER.id, RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context))
MainActivity.getStartIntent(context, MainView.Section.LUCKY_NUMBER, true), FLAG_UPDATE_CURRENT) .apply {
setTextViewText(
val remoteView = RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)).apply { R.id.luckyNumberWidgetNumber,
setTextViewText(R.id.luckyNumberWidgetNumber, luckyNumber?.luckyNumber?.toString() ?: "#") luckyNumber?.luckyNumber?.toString() ?: "#"
)
setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent) setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent)
} }
@ -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) super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
val remoteView = RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)) 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) { 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 width = options?.getInt(OPTION_APPWIDGET_MIN_WIDTH) ?: sharedPref.getLong(
val height = options?.getInt(OPTION_APPWIDGET_MAX_HEIGHT) ?: sharedPref.getLong(getHeightWidgetKey(appWidgetId), 74).toInt() getWidthWidgetKey(appWidgetId), 74
).toInt()
val height = options?.getInt(OPTION_APPWIDGET_MAX_HEIGHT) ?: sharedPref.getLong(
getHeightWidgetKey(appWidgetId), 74
).toInt()
with(sharedPref) { with(sharedPref) {
putLong(getWidthWidgetKey(appWidgetId), width.toLong()) 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.luckyNumberWidgetImageTop, if (imageTop) VISIBLE else GONE)
setViewVisibility(R.id.luckyNumberWidgetImageLeft, if (imageLeft) VISIBLE else GONE) setViewVisibility(R.id.luckyNumberWidgetImageLeft, if (imageLeft) VISIBLE else GONE)
setViewVisibility(R.id.luckyNumberWidgetTitle, if (title) 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 { private fun getCorrectLayoutId(appWidgetId: Int, context: Context): Int {
val savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0) 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)) { return if (savedTheme == 1L || (savedTheme == 2L && isSystemDarkMode)) {
R.layout.widget_luckynumber_dark R.layout.widget_luckynumber_dark

View File

@ -5,16 +5,10 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
import android.content.Intent.FLAG_ACTIVITY_NEW_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.Build.VERSION_CODES.P
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment 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.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.ActivityMainBinding 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.base.BaseActivity
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog 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.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.InAppReviewHelper import io.github.wulkanowy.utils.InAppReviewHelper
@ -75,6 +59,9 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
@Inject @Inject
lateinit var appInfo: AppInfo lateinit var appInfo: AppInfo
@Inject
lateinit var shortcutsHelper: ShortcutsHelper
private var accountMenu: MenuItem? = null private var accountMenu: MenuItem? = null
private val overlayProvider by lazy { ElevationOverlayProvider(this) } private val overlayProvider by lazy { ElevationOverlayProvider(this) }
@ -83,15 +70,19 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
FragNavController(supportFragmentManager, R.id.main_fragment_container) FragNavController(supportFragmentManager, R.id.main_fragment_container)
companion object { companion object {
const val EXTRA_START_MENU = "extraStartMenu"
private const val EXTRA_START_DESTINATION = "start_destination"
fun getStartIntent( fun getStartIntent(
context: Context, context: Context,
startMenu: MainView.Section? = null, destination: Destination? = null,
clear: Boolean = false startNewTask: Boolean = false
) = Intent(context, MainActivity::class.java).apply { ) = Intent(context, MainActivity::class.java).apply {
if (clear) flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK putExtra(EXTRA_START_DESTINATION, destination)
startMenu?.let { putExtra(EXTRA_START_MENU, it.id) }
if (startNewTask) {
flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK
}
} }
} }
@ -106,42 +97,21 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
override val currentViewSubtitle get() = (navController.currentFrag as? MainView.TitledView)?.subtitleString override val currentViewSubtitle get() = (navController.currentFrag as? MainView.TitledView)?.subtitleString
override var startMenuIndex = 0 private var savedInstanceState: Bundle? = null
override var startMenuMoreIndex = -1
private val moreMenuFragments = mapOf<Int, Fragment>(
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(),
)
@SuppressLint("NewApi") @SuppressLint("NewApi")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityMainBinding.inflate(layoutInflater).apply { binding = this }.root) setContentView(ActivityMainBinding.inflate(layoutInflater).apply { binding = this }.root)
setSupportActionBar(binding.mainToolbar) setSupportActionBar(binding.mainToolbar)
this.savedInstanceState = savedInstanceState
messageContainer = binding.mainMessageContainer messageContainer = binding.mainMessageContainer
updateHelper.messageContainer = binding.mainFragmentContainer updateHelper.messageContainer = binding.mainFragmentContainer
val section = MainView.Section.values() val destination = intent.getSerializableExtra(EXTRA_START_DESTINATION) as Destination?
.singleOrNull { it.id == intent.getIntExtra(EXTRA_START_MENU, -1) } ?: shortcutsHelper.getDestination(intent)
presenter.onAttachView(this, section)
with(navController) {
initialize(startMenuIndex, savedInstanceState)
pushFragment(moreMenuFragments[startMenuMoreIndex])
}
if (appInfo.systemVersion >= Build.VERSION_CODES.N_MR1) {
initShortcuts()
}
presenter.onAttachView(this, destination)
updateHelper.checkAndInstallUpdates(this) updateHelper.checkAndInstallUpdates(this)
} }
@ -157,54 +127,6 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
updateHelper.onActivityResult(requestCode, resultCode) updateHelper.onActivityResult(requestCode, resultCode)
} }
@RequiresApi(Build.VERSION_CODES.N_MR1)
fun initShortcuts() {
val shortcutsList = mutableListOf<ShortcutInfo>()
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<ShortcutManager>()?.dynamicShortcuts = shortcutsList
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.action_menu_main, menu) menuInflater.inflate(R.menu.action_menu_main, menu)
accountMenu = menu?.findItem(R.id.mainMenuAccount) accountMenu = menu?.findItem(R.id.mainMenuAccount)
@ -213,15 +135,38 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
return true return true
} }
@SuppressLint("NewApi") override fun initView(startMenuIndex: Int, rootDestinations: List<Destination>) {
override fun initView() { initializeToolbar()
initializeBottomNavigation(startMenuIndex)
initializeNavController(startMenuIndex, rootDestinations)
}
private fun initializeNavController(startMenuIndex: Int, rootDestinations: List<Destination>) {
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) { with(binding.mainToolbar) {
stateListAnimator = null stateListAnimator = null
setBackgroundColor( setBackgroundColor(
overlayProvider.compositeOverlayWithThemeSurfaceColorIfNeeded(dpToPx(4f)) overlayProvider.compositeOverlayWithThemeSurfaceColorIfNeeded(dpToPx(4f))
) )
} }
}
private fun initializeBottomNavigation(startMenuIndex: Int) {
with(binding.mainBottomNav) { with(binding.mainBottomNav) {
with(menu) { with(menu) {
add(Menu.NONE, 0, Menu.NONE, R.string.dashboard_title) add(Menu.NONE, 0, Menu.NONE, R.string.dashboard_title)
@ -239,36 +184,6 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
setOnItemSelectedListener { presenter.onTabSelected(it.itemId, false) } setOnItemSelectedListener { presenter.onTabSelected(it.itemId, false) }
setOnItemReselectedListener { presenter.onTabSelected(it.itemId, true) } 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( override fun onPreferenceStartFragment(
@ -317,6 +232,22 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
ViewCompat.setElevation(binding.mainToolbar, if (show) dpToPx(4f) else 0f) 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() { override fun notifyMenuViewReselected() {
(navController.currentStack?.getOrNull(0) as? MainView.MainChildView)?.onFragmentReselected() (navController.currentStack?.getOrNull(0) as? MainView.MainChildView)?.onFragmentReselected()
} }
@ -373,6 +304,6 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
navController.onSaveInstanceState(outState) navController.onSaveInstanceState(outState)
intent.removeExtra(EXTRA_START_MENU) intent.removeExtra(EXTRA_START_DESTINATION)
} }
} }

View File

@ -6,10 +6,15 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter 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.base.ErrorHandler
import io.github.wulkanowy.ui.modules.main.MainView.Section.GRADE import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.main.MainView.Section.MESSAGE import io.github.wulkanowy.ui.modules.account.AccountView
import io.github.wulkanowy.ui.modules.main.MainView.Section.SCHOOL 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.AnalyticsHelper
import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResource
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -27,19 +32,40 @@ class MainPresenter @Inject constructor(
private var studentsWitSemesters: List<StudentWithSemesters>? = null private var studentsWitSemesters: List<StudentWithSemesters>? = null
fun onAttachView(view: MainView, initMenu: MainView.Section?) { private val rootDestinationTypeList = listOf(
super.onAttachView(view) Destination.Type.DASHBOARD,
view.apply { Destination.Type.GRADE,
getProperViewIndexes(initMenu).let { (main, more) -> Destination.Type.ATTENDANCE,
startMenuIndex = main Destination.Type.TIMETABLE,
startMenuMoreIndex = more Destination.Type.MORE
)
private val Destination?.startMenuIndex
get() = when {
this == null -> prefRepository.startMenuIndex
type in rootDestinationTypeList -> {
rootDestinationTypeList.indexOf(type)
} }
initView() else -> 4
Timber.i("Main view was initialized with $startMenuIndex menu index and $startMenuMoreIndex more index") }
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() 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() { fun onActionMenuCreated() {
@ -64,9 +90,10 @@ class MainPresenter @Inject constructor(
}.launch("avatar") }.launch("avatar")
} }
fun onViewChange(section: MainView.Section?) { fun onViewChange(destinationView: BaseView) {
view?.apply { 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) } currentViewTitle?.let { setViewTitle(it) }
currentViewSubtitle?.let { setViewSubTitle(it.ifBlank { null }) } currentViewSubtitle?.let { setViewSubTitle(it.ifBlank { null }) }
currentStackSize?.let { currentStackSize?.let {
@ -134,10 +161,4 @@ class MainPresenter @Inject constructor(
view?.showStudentAvatar(currentStudent) 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
}
} }

View File

@ -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.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.Destination
interface MainView : BaseView { interface MainView : BaseView {
var startMenuIndex: Int
var startMenuMoreIndex: Int
val isRootView: Boolean val isRootView: Boolean
val currentViewTitle: String? val currentViewTitle: String?
@ -18,7 +15,7 @@ interface MainView : BaseView {
val currentStackSize: Int? val currentStackSize: Int?
fun initView() fun initView(startMenuIndex: Int, rootDestinations: List<Destination>)
fun switchMenuView(position: Int) fun switchMenuView(position: Int)
@ -28,6 +25,8 @@ interface MainView : BaseView {
fun showActionBarElevation(show: Boolean) fun showActionBarElevation(show: Boolean)
fun showBottomNavigation(show: Boolean)
fun notifyMenuViewReselected() fun notifyMenuViewReselected()
fun notifyMenuViewChanged() fun notifyMenuViewChanged()
@ -42,6 +41,8 @@ interface MainView : BaseView {
fun showInAppReview() fun showInAppReview()
fun openMoreDestination(destination: Destination)
interface MainChildView { interface MainChildView {
fun onFragmentReselected() fun onFragmentReselected()
@ -57,25 +58,4 @@ interface MainView : BaseView {
get() = "" get() = ""
set(_) {} 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
}
} }

View File

@ -5,7 +5,6 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Notification import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.databinding.ItemNotificationsCenterBinding import io.github.wulkanowy.databinding.ItemNotificationsCenterBinding
import io.github.wulkanowy.services.sync.notifications.NotificationType import io.github.wulkanowy.services.sync.notifications.NotificationType
@ -28,26 +27,12 @@ class NotificationsCenterAdapter @Inject constructor() :
notificationsCenterItemTitle.text = item.title notificationsCenterItemTitle.text = item.title
notificationsCenterItemContent.text = item.content notificationsCenterItemContent.text = item.content
notificationsCenterItemDate.text = item.date.toFormattedString("HH:mm, d MMM") notificationsCenterItemDate.text = item.date.toFormattedString("HH:mm, d MMM")
notificationsCenterItemIcon.setImageResource(item.type.toDrawableResId()) notificationsCenterItemIcon.setImageResource(item.type.icon)
root.setOnClickListener { onItemClickListener(item.type) } 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) : class ViewHolder(val binding: ItemNotificationsCenterBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)

View File

@ -11,6 +11,7 @@ import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.databinding.FragmentNotificationsCenterBinding import io.github.wulkanowy.databinding.FragmentNotificationsCenterBinding
import io.github.wulkanowy.services.sync.notifications.NotificationType import io.github.wulkanowy.services.sync.notifications.NotificationType
import io.github.wulkanowy.ui.base.BaseFragment 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.conference.ConferenceFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment 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.message.MessageFragment
import io.github.wulkanowy.ui.modules.note.NoteFragment import io.github.wulkanowy.ui.modules.note.NoteFragment
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -104,5 +106,7 @@ class NotificationsCenterFragment :
NotificationType.NEW_NOTE -> NoteFragment.newInstance() NotificationType.NEW_NOTE -> NoteFragment.newInstance()
NotificationType.NEW_ANNOUNCEMENT -> SchoolAnnouncementFragment.newInstance() NotificationType.NEW_ANNOUNCEMENT -> SchoolAnnouncementFragment.newInstance()
NotificationType.PUSH -> null NotificationType.PUSH -> null
NotificationType.CHANGE_TIMETABLE -> TimetableFragment.newInstance()
NotificationType.NEW_ATTENDANCE -> AttendanceFragment.newInstance()
} }
} }

View File

@ -6,7 +6,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import timber.log.Timber import timber.log.Timber
class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView { class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, SettingsView {
companion object { companion object {
@ -19,4 +19,16 @@ class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView {
setPreferencesFromResource(R.xml.scheme_preferences, rootKey) setPreferencesFromResource(R.xml.scheme_preferences, rootKey)
Timber.i("Settings view was initialized") 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) {}
} }

View File

@ -0,0 +1,5 @@
package io.github.wulkanowy.ui.modules.settings
import io.github.wulkanowy.ui.base.BaseView
interface SettingsView : BaseView

View File

@ -24,8 +24,8 @@ import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.HiltBroadcastReceiver import io.github.wulkanowy.services.HiltBroadcastReceiver
import io.github.wulkanowy.services.widgets.TimetableWidgetService 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.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.capitalise
import io.github.wulkanowy.utils.createNameInitialsDrawable import io.github.wulkanowy.utils.createNameInitialsDrawable
@ -60,6 +60,8 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() {
companion object { companion object {
private const val TIMETABLE_PENDING_INTENT_ID = 201
private const val EXTRA_TOGGLED_WIDGET_ID = "extraToggledWidget" private const val EXTRA_TOGGLED_WIDGET_ID = "extraToggledWidget"
private const val EXTRA_BUTTON_TYPE = "extraButtonType" private const val EXTRA_BUTTON_TYPE = "extraButtonType"
@ -174,8 +176,8 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() {
) )
val appIntent = PendingIntent.getActivity( val appIntent = PendingIntent.getActivity(
context, context,
MainView.Section.TIMETABLE.id, TIMETABLE_PENDING_INTENT_ID,
MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), MainActivity.getStartIntent(context, Destination.Timetable(), true),
FLAG_UPDATE_CURRENT FLAG_UPDATE_CURRENT
) )

View File

@ -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 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)) { get() = when (AttendanceCategory.getCategoryByName(name)) {
AttendanceCategory.PRESENCE -> R.string.attendance_present AttendanceCategory.PRESENCE -> R.string.attendance_present
AttendanceCategory.ABSENCE_UNEXCUSED -> R.string.attendance_absence_unexcused AttendanceCategory.ABSENCE_UNEXCUSED -> R.string.attendance_absence_unexcused

View File

@ -18,6 +18,7 @@ import androidx.annotation.AttrRes
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.PluralsRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.graphics.applyCanvas 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) = fun Context.getCompatBitmap(@DrawableRes drawableRes: Int, @ColorRes colorRes: Int) =
getCompatDrawable(drawableRes, colorRes)?.toBitmap() 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 = {}) { fun Context.openInternetBrowser(uri: String, onActivityNotFound: (uri: String) -> Unit = {}) {
Intent.parseUri(uri, 0).let { Intent.parseUri(uri, 0).let {
try { try {

View File

@ -2,16 +2,19 @@ package io.github.wulkanowy.utils
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.ncapdevi.fragnav.FragNavController 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 { transactionListener = object : FragNavController.TransactionListener {
override fun onFragmentTransaction(fragment: Fragment?, transactionType: FragNavController.TransactionType) { override fun onFragmentTransaction(
listener(fragment?.toSection(), fragment?.let { it::class.java.simpleName }) fragment: Fragment?,
transactionType: FragNavController.TransactionType
) {
fragment?.let { listener(it as BaseView) }
} }
override fun onTabTransaction(fragment: Fragment?, index: Int) { override fun onTabTransaction(fragment: Fragment?, index: Int) {
listener(fragment?.toSection(), fragment?.let { it::class.java.simpleName }) fragment?.let { listener(it as BaseView) }
} }
} }
} }

View File

@ -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
}
}

View File

@ -163,6 +163,26 @@
<string name="timetable_now">Now: %s</string> <string name="timetable_now">Now: %s</string>
<string name="timetable_next">Next: %s</string> <string name="timetable_next">Next: %s</string>
<string name="timetable_later">Later: %s</string> <string name="timetable_later">Later: %s</string>
<string name="timetable_notify_lesson">%1$s lesson %2$d - %3$s</string>
<string name="timetable_notify_change_room">Change of room from %1$s to %2$s</string>
<string name="timetable_notify_change_teacher">Change of teacher from %1$s to %2$s</string>
<string name="timetable_notify_change_subject">Change of subject from %1$s to %2$s</string>
<plurals name="timetable_notify_new_items_title">
<item quantity="one">Timetable change</item>
<item quantity="other">Timetable changes</item>
</plurals>
<plurals name="timetable_notify_new_items">
<item quantity="one">%1$s - %2$d change in timetable</item>
<item quantity="other">%1$s - %2$d changes in timetable</item>
</plurals>
<plurals name="timetable_notify_new_items_group">
<item quantity="one">%1$d change in timetable</item>
<item quantity="other">%1$d changes in timetable</item>
</plurals>
<plurals name="timetable_number_item">
<item quantity="one">%d change</item>
<item quantity="other">%d changes</item>
</plurals>
<!--Completed lessons--> <!--Completed lessons-->
@ -200,6 +220,18 @@
<string name="attendance_excuse_title">Excuse</string> <string name="attendance_excuse_title">Excuse</string>
<string name="attendance_excuse_reason" translatable="false">z powodu</string> <string name="attendance_excuse_reason" translatable="false">z powodu</string>
<string name="attendance_excuse_formula" translatable="false">Dzień dobry,\nProszę o usprawiedliwienie mojego dziecka w dniu %s z lekcji %s%s%s.\n\nPozdrawiam.</string> <string name="attendance_excuse_formula" translatable="false">Dzień dobry,\nProszę o usprawiedliwienie mojego dziecka w dniu %s z lekcji %s%s%s.\n\nPozdrawiam.</string>
<plurals name="attendance_notify_new_items_title">
<item quantity="one">New attendance</item>
<item quantity="other">New attendance</item>
</plurals>
<plurals name="attendance_notify_new_items">
<item quantity="one">%1$d new attendance</item>
<item quantity="other">%1$d attendance</item>
</plurals>
<plurals name="attendance_number_item">
<item quantity="one">%d attendance</item>
<item quantity="other">%d attendance</item>
</plurals>
<!--Attendance summary--> <!--Attendance summary-->
@ -706,6 +738,8 @@
<string name="channel_push">Push notifications</string> <string name="channel_push">Push notifications</string>
<string name="channel_upcoming_lessons">Upcoming lessons</string> <string name="channel_upcoming_lessons">Upcoming lessons</string>
<string name="channel_debug">Debug</string> <string name="channel_debug">Debug</string>
<string name="channel_change_timetable">Timetable change</string>
<string name="channel_new_attendance">New attendance</string>
<!--Colors--> <!--Colors-->

View File

@ -42,20 +42,12 @@ class MainPresenterTest {
MockKAnnotations.init(this) MockKAnnotations.init(this)
clearMocks(mainView) clearMocks(mainView)
every { mainView.startMenuIndex = any() } just Runs every { mainView.initView(any(), any()) } just Runs
every { mainView.startMenuMoreIndex = any() } just Runs presenter =
every { mainView.startMenuIndex } returns 1 MainPresenter(errorHandler, studentRepository, prefRepository, syncManager, analytics)
every { mainView.startMenuMoreIndex } returns 1
every { mainView.initView() } just Runs
presenter = MainPresenter(errorHandler, studentRepository, prefRepository, syncManager, analytics)
presenter.onAttachView(mainView, null) presenter.onAttachView(mainView, null)
} }
@Test
fun initMenuTest() {
verify { mainView.initView() }
}
@Test @Test
fun onTabSelectedTest() { fun onTabSelectedTest() {
every { mainView.notifyMenuViewChanged() } just Runs every { mainView.notifyMenuViewChanged() } just Runs