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

View File

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

View File

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

View File

@ -50,4 +50,7 @@ data class Timetable(
@PrimaryKey(autoGenerate = true)
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
import androidx.annotation.DrawableRes
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import android.content.Intent
import io.github.wulkanowy.services.sync.notifications.NotificationType
import io.github.wulkanowy.ui.modules.main.MainView
sealed interface NotificationData {
data class NotificationData(
val intentToStart: Intent,
val title: String,
val content: String
)
data class GroupNotificationData(
val notificationDataList: List<NotificationData>,
val title: String,
val content: String,
val intentToStart: Intent,
val type: NotificationType
val startMenu: MainView.Section
val icon: Int
val titleStringRes: Int
val contentStringRes: Int
}
)
data class MultipleNotificationsData(
override val type: NotificationType,
override val startMenu: MainView.Section,
@DrawableRes override val icon: Int,
@PluralsRes override val titleStringRes: Int,
@PluralsRes override val contentStringRes: Int,
@PluralsRes val summaryStringRes: Int,
val lines: List<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.sunday
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import java.time.LocalDateTime
@ -38,6 +39,7 @@ class AttendanceRepository @Inject constructor(
start: LocalDate,
end: LocalDate,
forceRefresh: Boolean,
notify: Boolean = false,
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
@ -56,13 +58,28 @@ class AttendanceRepository @Inject constructor(
},
saveFetchResult = { old, new ->
attendanceDb.deleteAll(old uniqueSubtract new)
attendanceDb.insertAll(new uniqueSubtract old)
val attendanceToAdd = (new uniqueSubtract old).map { newAttendance ->
newAttendance.apply { if (notify) isNotified = false }
}
attendanceDb.insertAll(attendanceToAdd)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
},
filterResult = { it.filter { item -> item.date in start..end } }
)
fun getAttendanceFromDatabase(
semester: Semester,
start: LocalDate,
end: LocalDate
): Flow<List<Attendance>> {
return attendanceDb.loadAll(semester.diaryId, semester.studentId, start, end)
}
suspend fun updateTimetable(timetable: List<Attendance>) {
return attendanceDb.updateAll(timetable)
}
suspend fun excuseForAbsence(
student: Student, semester: Semester,
absenceList: List<Attendance>, reason: String? = null

View File

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

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

View File

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

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

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.content.Context
import android.os.Build
import androidx.annotation.PluralsRes
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.data.pojos.GroupNotificationData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.data.pojos.OneNotificationData
import io.github.wulkanowy.data.repositories.NotificationRepository
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.getCompatBitmap
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.nickOrName
@ -27,102 +23,17 @@ import kotlin.random.Random
class AppNotificationManager @Inject constructor(
private val notificationManager: NotificationManagerCompat,
@ApplicationContext private val context: Context,
private val appInfo: AppInfo,
private val notificationRepository: NotificationRepository
) {
suspend fun sendNotification(notificationData: NotificationData, student: Student) =
when (notificationData) {
is OneNotificationData -> sendOneNotification(notificationData, student)
is MultipleNotificationsData -> sendMultipleNotifications(notificationData, student)
}
private suspend fun sendOneNotification(
notificationData: OneNotificationData,
student: Student
) {
val content = context.getString(
notificationData.contentStringRes,
*notificationData.contentValues.toTypedArray()
)
val title = context.getString(notificationData.titleStringRes)
val notification = getDefaultNotificationBuilder(notificationData)
.setContentTitle(title)
.setContentText(content)
.setStyle(
NotificationCompat.BigTextStyle()
.setSummaryText(student.nickOrName)
.bigText(content)
)
.build()
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), notification)
saveNotification(title, content, notificationData, student)
}
private suspend fun sendMultipleNotifications(
notificationData: MultipleNotificationsData,
student: Student
) {
val groupType = notificationData.type.group ?: return
val group = "${groupType}_${student.id}"
notificationData.sendSummaryNotification(group, student)
notificationData.lines.forEach { item ->
val title = context.resources.getQuantityString(notificationData.titleStringRes, 1)
val notification = getDefaultNotificationBuilder(notificationData)
.setContentTitle(title)
.setContentText(item)
.setStyle(
NotificationCompat.BigTextStyle()
.setSummaryText(student.nickOrName)
.bigText(item)
)
.setGroup(group)
.build()
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), notification)
saveNotification(title, item, notificationData, student)
}
}
private fun MultipleNotificationsData.sendSummaryNotification(group: String, student: Student) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
val summaryNotification = getDefaultNotificationBuilder(this)
.setSmallIcon(icon)
.setContentTitle(getQuantityString(titleStringRes, lines.size))
.setContentText(getQuantityString(contentStringRes, lines.size))
.setStyle(
NotificationCompat.InboxStyle()
.setSummaryText(student.nickOrName)
.also { builder -> lines.forEach { builder.addLine(it) } }
)
.setLocalOnly(true)
.setGroup(group)
.setGroupSummary(true)
.build()
val groupId = student.id * 100 + type.ordinal
notificationManager.notify(groupId.toInt(), summaryNotification)
}
@SuppressLint("InlinedApi")
private fun getDefaultNotificationBuilder(notificationData: NotificationData): NotificationCompat.Builder {
val pendingIntentsFlags = if (appInfo.systemVersion >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
return NotificationCompat.Builder(context, notificationData.type.channel)
.setLargeIcon(context.getCompatBitmap(notificationData.icon, R.color.colorPrimary))
suspend fun sendSingleNotification(
notificationData: NotificationData,
notificationType: NotificationType,
student: Student
) {
val notification = NotificationCompat.Builder(context, notificationType.channel)
.setLargeIcon(context.getCompatBitmap(notificationType.icon, R.color.colorPrimary))
.setSmallIcon(R.drawable.ic_stat_all)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
@ -132,31 +43,122 @@ class AppNotificationManager @Inject constructor(
.setContentIntent(
PendingIntent.getActivity(
context,
notificationData.startMenu.id,
MainActivity.getStartIntent(context, notificationData.startMenu, true),
pendingIntentsFlags
Random.nextInt(),
notificationData.intentToStart,
PendingIntent.FLAG_UPDATE_CURRENT
)
)
.setContentTitle(notificationData.title)
.setContentText(notificationData.content)
.setStyle(
NotificationCompat.BigTextStyle()
.setSummaryText(student.nickOrName)
.bigText(notificationData.content)
)
.build()
notificationManager.notify(Random.nextInt(), notification)
saveNotification(notificationData, notificationType, student)
}
@SuppressLint("InlinedApi")
suspend fun sendMultipleNotifications(
groupNotificationData: GroupNotificationData,
student: Student
) {
val notificationType = groupNotificationData.type
val groupType = notificationType.group ?: return
val group = "${groupType}_${student.id}"
sendSummaryNotification(groupNotificationData, group, student)
groupNotificationData.notificationDataList.forEach { notificationData ->
val notification = NotificationCompat.Builder(context, notificationType.channel)
.setLargeIcon(context.getCompatBitmap(notificationType.icon, R.color.colorPrimary))
.setSmallIcon(R.drawable.ic_stat_all)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
.setContentIntent(
PendingIntent.getActivity(
context,
Random.nextInt(),
notificationData.intentToStart,
PendingIntent.FLAG_UPDATE_CURRENT
)
)
.setContentTitle(notificationData.title)
.setContentText(notificationData.content)
.setStyle(
NotificationCompat.BigTextStyle()
.setSummaryText(student.nickOrName)
.bigText(notificationData.content)
)
.setGroup(group)
.build()
notificationManager.notify(Random.nextInt(), notification)
saveNotification(notificationData, groupNotificationData.type, student)
}
}
private fun sendSummaryNotification(
groupNotificationData: GroupNotificationData,
group: String,
student: Student
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
val summaryNotification =
NotificationCompat.Builder(context, groupNotificationData.type.channel)
.setContentTitle(groupNotificationData.title)
.setContentText(groupNotificationData.content)
.setSmallIcon(groupNotificationData.type.icon)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setStyle(
NotificationCompat.InboxStyle()
.setSummaryText(student.nickOrName)
.also { builder ->
groupNotificationData.notificationDataList.forEach {
builder.addLine(it.content)
}
}
)
.setContentIntent(
PendingIntent.getActivity(
context,
Random.nextInt(),
groupNotificationData.intentToStart,
PendingIntent.FLAG_UPDATE_CURRENT
)
)
.setLocalOnly(true)
.setGroup(group)
.setGroupSummary(true)
.build()
val groupId = student.id * 100 + groupNotificationData.type.ordinal
notificationManager.notify(groupId.toInt(), summaryNotification)
}
private suspend fun saveNotification(
title: String,
content: String,
notificationData: NotificationData,
notificationType: NotificationType,
student: Student
) {
val notificationEntity = Notification(
studentId = student.id,
title = title,
content = content,
type = notificationData.type,
title = notificationData.title,
content = notificationData.content,
type = notificationType,
date = LocalDateTime.now()
)
notificationRepository.saveNotification(notificationEntity)
}
private fun getQuantityString(@PluralsRes res: Int, arg: Int): String {
return context.resources.getQuantityString(res, arg, arg)
}
}

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

View File

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

View File

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

View File

@ -1,34 +1,52 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.data.pojos.GroupNotificationData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDate
import javax.inject.Inject
class NewHomeworkNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager
private val appNotificationManager: AppNotificationManager,
@ApplicationContext private val context: Context
) {
suspend fun notify(items: List<Homework>, student: Student) {
val today = LocalDate.now()
val lines = items.filter { !it.date.isBefore(today) }.map {
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}"
}.ifEmpty { return }
val lines = items.filter { !it.date.isBefore(today) }
.map {
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}"
}
.ifEmpty { return }
val notification = MultipleNotificationsData(
val notificationDataList = lines.map {
NotificationData(
title = context.getPlural(R.plurals.homework_notify_new_item_title, 1),
content = it,
intentToStart = MainActivity.getStartIntent(context, Destination.Homework, true),
)
}
val groupNotificationData = GroupNotificationData(
title = context.getPlural(R.plurals.homework_notify_new_item_title, lines.size),
content = context.getPlural(
R.plurals.homework_notify_new_item_content,
lines.size,
lines.size
),
intentToStart = MainActivity.getStartIntent(context, Destination.Homework, true),
type = NotificationType.NEW_HOMEWORK,
icon = R.drawable.ic_more_homework,
titleStringRes = R.plurals.homework_notify_new_item_title,
contentStringRes = R.plurals.homework_notify_new_item_content,
summaryStringRes = R.plurals.homework_number_item,
startMenu = MainView.Section.HOMEWORK,
lines = lines
notificationDataList = notificationDataList
)
appNotificationManager.sendNotification(notification, student)
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
}
}

View File

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

View File

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

View File

@ -1,42 +1,46 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.data.pojos.GroupNotificationData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.utils.getPlural
import javax.inject.Inject
class NewNoteNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager
private val appNotificationManager: AppNotificationManager,
@ApplicationContext private val context: Context
) {
suspend fun notify(items: List<Note>, student: Student) {
val notification = MultipleNotificationsData(
type = NotificationType.NEW_NOTE,
icon = R.drawable.ic_stat_note,
titleStringRes = when (NoteCategory.getByValue(items.first().categoryType)) {
val notificationDataList = items.map {
val titleRes = when (NoteCategory.getByValue(it.categoryType)) {
NoteCategory.POSITIVE -> R.plurals.praise_new_items
NoteCategory.NEUTRAL -> R.plurals.neutral_note_new_items
else -> R.plurals.note_new_items
},
contentStringRes = when (NoteCategory.getByValue(items.first().categoryType)) {
NoteCategory.POSITIVE -> R.plurals.praise_notify_new_items
NoteCategory.NEUTRAL -> R.plurals.neutral_note_notify_new_items
else -> R.plurals.note_notify_new_items
},
summaryStringRes = when (NoteCategory.getByValue(items.first().categoryType)) {
NoteCategory.POSITIVE -> R.plurals.praise_number_item
NoteCategory.NEUTRAL -> R.plurals.neutral_note_number_item
else -> R.plurals.note_number_item
},
startMenu = MainView.Section.NOTE,
lines = items.map {
"${it.teacher}: ${it.category}"
}
NotificationData(
title = context.getPlural(titleRes, 1),
content = "${it.teacher}: ${it.category}",
intentToStart = MainActivity.getStartIntent(context, Destination.Note, true),
)
}
val groupNotificationData = GroupNotificationData(
notificationDataList = notificationDataList,
intentToStart = MainActivity.getStartIntent(context, Destination.Note, true),
title = context.getPlural(R.plurals.note_new_items, items.size),
content = context.getPlural(R.plurals.note_notify_new_items, items.size, items.size),
type = NotificationType.NEW_NOTE
)
appNotificationManager.sendNotification(notification, student)
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
}
}

View File

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

View File

@ -1,6 +1,8 @@
package io.github.wulkanowy.services.sync.notifications
import io.github.wulkanowy.R
import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel
import io.github.wulkanowy.services.sync.channels.NewAttendanceChannel
import io.github.wulkanowy.services.sync.channels.NewConferencesChannel
import io.github.wulkanowy.services.sync.channels.NewExamChannel
import io.github.wulkanowy.services.sync.channels.NewGradesChannel
@ -9,17 +11,76 @@ import io.github.wulkanowy.services.sync.channels.NewMessagesChannel
import io.github.wulkanowy.services.sync.channels.NewNotesChannel
import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel
import io.github.wulkanowy.services.sync.channels.PushChannel
import io.github.wulkanowy.services.sync.channels.TimetableChangeChannel
enum class NotificationType(val group: String?, val channel: String) {
NEW_CONFERENCE("new_conferences_group", NewConferencesChannel.CHANNEL_ID),
NEW_EXAM("new_exam_group", NewExamChannel.CHANNEL_ID),
NEW_GRADE_DETAILS("new_grade_details_group", NewGradesChannel.CHANNEL_ID),
NEW_GRADE_PREDICTED("new_grade_predicted_group", NewGradesChannel.CHANNEL_ID),
NEW_GRADE_FINAL("new_grade_final_group", NewGradesChannel.CHANNEL_ID),
NEW_HOMEWORK("new_homework_group", NewHomeworkChannel.CHANNEL_ID),
NEW_LUCKY_NUMBER("lucky_number_group", LuckyNumberChannel.CHANNEL_ID),
NEW_MESSAGE("new_message_group", NewMessagesChannel.CHANNEL_ID),
NEW_NOTE("new_notes_group", NewNotesChannel.CHANNEL_ID),
NEW_ANNOUNCEMENT("new_school_announcements_group", NewSchoolAnnouncementsChannel.CHANNEL_ID),
PUSH(null, PushChannel.CHANNEL_ID)
enum class NotificationType(
val group: String?,
val channel: String,
val icon: Int
) {
NEW_CONFERENCE(
group = "new_conferences_group",
channel = NewConferencesChannel.CHANNEL_ID,
icon = R.drawable.ic_more_conferences,
),
NEW_EXAM(
group = "new_exam_group",
channel = NewExamChannel.CHANNEL_ID,
icon = R.drawable.ic_main_exam
),
NEW_GRADE_DETAILS(
group = "new_grade_details_group",
channel = NewGradesChannel.CHANNEL_ID,
icon = R.drawable.ic_stat_grade,
),
NEW_GRADE_PREDICTED(
group = "new_grade_predicted_group",
channel = NewGradesChannel.CHANNEL_ID,
icon = R.drawable.ic_stat_grade,
),
NEW_GRADE_FINAL(
group = "new_grade_final_group",
channel = NewGradesChannel.CHANNEL_ID,
icon = R.drawable.ic_stat_grade,
),
NEW_HOMEWORK(
group = "new_homework_group",
channel = NewHomeworkChannel.CHANNEL_ID,
icon = R.drawable.ic_more_homework,
),
NEW_LUCKY_NUMBER(
group = null,
channel = LuckyNumberChannel.CHANNEL_ID,
icon = R.drawable.ic_stat_luckynumber,
),
NEW_MESSAGE(
group = "new_message_group",
channel = NewMessagesChannel.CHANNEL_ID,
icon = R.drawable.ic_stat_message,
),
NEW_NOTE(
group = "new_notes_group",
channel = NewNotesChannel.CHANNEL_ID,
icon = R.drawable.ic_stat_note
),
NEW_ANNOUNCEMENT(
group = "new_school_announcements_group",
channel = NewSchoolAnnouncementsChannel.CHANNEL_ID,
icon = R.drawable.ic_all_about
),
CHANGE_TIMETABLE(
group = "change_timetable_group",
channel = TimetableChangeChannel.CHANNEL_ID,
icon = R.drawable.ic_main_timetable
),
NEW_ATTENDANCE(
group = "new_attendance_group",
channel = NewAttendanceChannel.CHANNEL_ID,
icon = R.drawable.ic_main_attendance
),
PUSH(
group = null,
channel = PushChannel.CHANNEL_ID,
icon = R.drawable.ic_stat_all
)
}

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.Student
import io.github.wulkanowy.data.repositories.AttendanceRepository
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.services.sync.notifications.NewAttendanceNotification
import io.github.wulkanowy.utils.previousOrSameSchoolDay
import io.github.wulkanowy.utils.waitForResult
import kotlinx.coroutines.flow.first
import java.time.LocalDate.now
import javax.inject.Inject
class AttendanceWork @Inject constructor(
private val attendanceRepository: AttendanceRepository
private val attendanceRepository: AttendanceRepository,
private val newAttendanceNotification: NewAttendanceNotification,
private val preferencesRepository: PreferencesRepository
) : Work {
override suspend fun doWork(student: Student, semester: Semester) {
attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true)
attendanceRepository.getAttendance(
student = student,
semester = semester,
start = now().previousOrSameSchoolDay,
end = now().previousOrSameSchoolDay,
forceRefresh = true,
notify = preferencesRepository.isNotificationsEnable
)
.waitForResult()
attendanceRepository.getAttendanceFromDatabase(semester, now().minusDays(7), now())
.first()
.filterNot { it.isNotified }
.let {
if (it.isNotEmpty()) newAttendanceNotification.notify(it, student)
attendanceRepository.updateTimetable(it.onEach { attendance ->
attendance.isNotified = true
})
}
}
}

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.Student
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification
import io.github.wulkanowy.utils.nextOrSameSchoolDay
import io.github.wulkanowy.utils.waitForResult
import kotlinx.coroutines.flow.first
import java.time.LocalDate.now
import javax.inject.Inject
class TimetableWork @Inject constructor(
private val timetableRepository: TimetableRepository
private val timetableRepository: TimetableRepository,
private val changeTimetableNotification: ChangeTimetableNotification,
private val preferencesRepository: PreferencesRepository
) : Work {
override suspend fun doWork(student: Student, semester: Semester) {
timetableRepository.getTimetable(student, semester, now().monday, now().sunday, true).waitForResult()
timetableRepository.getTimetable(
student = student,
semester = semester,
start = now().nextOrSameSchoolDay,
end = now().nextOrSameSchoolDay,
forceRefresh = true,
notify = preferencesRepository.isNotificationsEnable
)
.waitForResult()
timetableRepository.getTimetableFromDatabase(semester, now(), now().plusDays(7))
.first()
.filterNot { it.isNotified }
.let {
if (it.isNotEmpty()) changeTimetableNotification.notify(it, student)
timetableRepository.updateTimetable(it.onEach { timetable ->
timetable.isNotified = true
})
}
}
}

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

View File

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

View File

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

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() {
activity?.let { startActivity(MainActivity.getStartIntent(context = it, clear = true)) }
activity?.let {
startActivity(
MainActivity.getStartIntent(
context = it,
startNewTask = true
)
)
}
}
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.repositories.LuckyNumberRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.toFirstResult
import kotlinx.coroutines.runBlocking
import timber.log.Timber
@ -39,6 +39,8 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
companion object {
const val LUCKY_NUMBER_PENDING_INTENT_ID = 200
fun getStudentWidgetKey(appWidgetId: Int) = "lucky_number_widget_student_$appWidgetId"
fun getThemeWidgetKey(appWidgetId: Int) = "lucky_number_widget_theme_$appWidgetId"
@ -48,18 +50,31 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
fun getWidthWidgetKey(appWidgetId: Int) = "lucky_number_widget_width_$appWidgetId"
}
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray?) {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray?
) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
appWidgetIds?.forEach { appWidgetId ->
val luckyNumber =
getLuckyNumber(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId)
val appIntent = PendingIntent.getActivity(
context,
LUCKY_NUMBER_PENDING_INTENT_ID,
MainActivity.getStartIntent(context, Destination.LuckyNumber, true),
FLAG_UPDATE_CURRENT
)
val luckyNumber = getLuckyNumber(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId)
val appIntent = PendingIntent.getActivity(context, MainView.Section.LUCKY_NUMBER.id,
MainActivity.getStartIntent(context, MainView.Section.LUCKY_NUMBER, true), FLAG_UPDATE_CURRENT)
val remoteView = RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)).apply {
setTextViewText(R.id.luckyNumberWidgetNumber, luckyNumber?.luckyNumber?.toString() ?: "#")
setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent)
}
val remoteView =
RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context))
.apply {
setTextViewText(
R.id.luckyNumberWidgetNumber,
luckyNumber?.luckyNumber?.toString() ?: "#"
)
setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent)
}
setStyles(remoteView, appWidgetId)
appWidgetManager.updateAppWidget(appWidgetId, remoteView)
@ -78,7 +93,12 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
}
}
override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle?) {
override fun onAppWidgetOptionsChanged(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
newOptions: Bundle?
) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
val remoteView = RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context))
@ -88,8 +108,12 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
}
private fun setStyles(views: RemoteViews, appWidgetId: Int, options: Bundle? = null) {
val width = options?.getInt(OPTION_APPWIDGET_MIN_WIDTH) ?: sharedPref.getLong(getWidthWidgetKey(appWidgetId), 74).toInt()
val height = options?.getInt(OPTION_APPWIDGET_MAX_HEIGHT) ?: sharedPref.getLong(getHeightWidgetKey(appWidgetId), 74).toInt()
val width = options?.getInt(OPTION_APPWIDGET_MIN_WIDTH) ?: sharedPref.getLong(
getWidthWidgetKey(appWidgetId), 74
).toInt()
val height = options?.getInt(OPTION_APPWIDGET_MAX_HEIGHT) ?: sharedPref.getLong(
getHeightWidgetKey(appWidgetId), 74
).toInt()
with(sharedPref) {
putLong(getWidthWidgetKey(appWidgetId), width.toLong())
@ -112,7 +136,11 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
}
}
private fun RemoteViews.setVisibility(imageTop: Boolean, imageLeft: Boolean, title: Boolean = false) {
private fun RemoteViews.setVisibility(
imageTop: Boolean,
imageLeft: Boolean,
title: Boolean = false
) {
setViewVisibility(R.id.luckyNumberWidgetImageTop, if (imageTop) VISIBLE else GONE)
setViewVisibility(R.id.luckyNumberWidgetImageLeft, if (imageLeft) VISIBLE else GONE)
setViewVisibility(R.id.luckyNumberWidgetTitle, if (title) VISIBLE else GONE)
@ -152,7 +180,8 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
private fun getCorrectLayoutId(appWidgetId: Int, context: Context): Int {
val savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0)
val isSystemDarkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
val isSystemDarkMode =
context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
return if (savedTheme == 1L || (savedTheme == 2L && isSystemDarkMode)) {
R.layout.widget_luckynumber_dark

View File

@ -5,16 +5,10 @@ import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.graphics.drawable.Icon
import android.os.Build
import android.os.Build.VERSION_CODES.P
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
@ -29,20 +23,10 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.ActivityMainBinding
import io.github.wulkanowy.services.shortcuts.ShortcutsHelper
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog
import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment
import io.github.wulkanowy.ui.modules.conference.ConferenceFragment
import io.github.wulkanowy.ui.modules.dashboard.DashboardFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.homework.HomeworkFragment
import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.more.MoreFragment
import io.github.wulkanowy.ui.modules.note.NoteFragment
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.InAppReviewHelper
@ -75,6 +59,9 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
@Inject
lateinit var appInfo: AppInfo
@Inject
lateinit var shortcutsHelper: ShortcutsHelper
private var accountMenu: MenuItem? = null
private val overlayProvider by lazy { ElevationOverlayProvider(this) }
@ -83,15 +70,19 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
FragNavController(supportFragmentManager, R.id.main_fragment_container)
companion object {
const val EXTRA_START_MENU = "extraStartMenu"
private const val EXTRA_START_DESTINATION = "start_destination"
fun getStartIntent(
context: Context,
startMenu: MainView.Section? = null,
clear: Boolean = false
destination: Destination? = null,
startNewTask: Boolean = false
) = Intent(context, MainActivity::class.java).apply {
if (clear) flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK
startMenu?.let { putExtra(EXTRA_START_MENU, it.id) }
putExtra(EXTRA_START_DESTINATION, destination)
if (startNewTask) {
flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK
}
}
}
@ -106,42 +97,21 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
override val currentViewSubtitle get() = (navController.currentFrag as? MainView.TitledView)?.subtitleString
override var startMenuIndex = 0
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(),
)
private var savedInstanceState: Bundle? = null
@SuppressLint("NewApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityMainBinding.inflate(layoutInflater).apply { binding = this }.root)
setSupportActionBar(binding.mainToolbar)
this.savedInstanceState = savedInstanceState
messageContainer = binding.mainMessageContainer
updateHelper.messageContainer = binding.mainFragmentContainer
val section = MainView.Section.values()
.singleOrNull { it.id == intent.getIntExtra(EXTRA_START_MENU, -1) }
presenter.onAttachView(this, section)
with(navController) {
initialize(startMenuIndex, savedInstanceState)
pushFragment(moreMenuFragments[startMenuMoreIndex])
}
if (appInfo.systemVersion >= Build.VERSION_CODES.N_MR1) {
initShortcuts()
}
val destination = intent.getSerializableExtra(EXTRA_START_DESTINATION) as Destination?
?: shortcutsHelper.getDestination(intent)
presenter.onAttachView(this, destination)
updateHelper.checkAndInstallUpdates(this)
}
@ -157,54 +127,6 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
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 {
menuInflater.inflate(R.menu.action_menu_main, menu)
accountMenu = menu?.findItem(R.id.mainMenuAccount)
@ -213,15 +135,38 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
return true
}
@SuppressLint("NewApi")
override fun initView() {
override fun initView(startMenuIndex: Int, rootDestinations: List<Destination>) {
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) {
stateListAnimator = null
setBackgroundColor(
overlayProvider.compositeOverlayWithThemeSurfaceColorIfNeeded(dpToPx(4f))
)
}
}
private fun initializeBottomNavigation(startMenuIndex: Int) {
with(binding.mainBottomNav) {
with(menu) {
add(Menu.NONE, 0, Menu.NONE, R.string.dashboard_title)
@ -239,36 +184,6 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
setOnItemSelectedListener { presenter.onTabSelected(it.itemId, false) }
setOnItemReselectedListener { presenter.onTabSelected(it.itemId, true) }
}
with(navController) {
setOnViewChangeListener { section, name ->
if (section == MainView.Section.ACCOUNT || section == MainView.Section.STUDENT_INFO) {
binding.mainBottomNav.isVisible = false
if (appInfo.systemVersion >= P) {
window.navigationBarColor = getThemeAttrColor(R.attr.colorSurface)
}
} else {
binding.mainBottomNav.isVisible = true
if (appInfo.systemVersion >= P) {
window.navigationBarColor =
getThemeAttrColor(android.R.attr.navigationBarColor)
}
}
analytics.setCurrentScreen(this@MainActivity, name)
presenter.onViewChange(section)
}
fragmentHideStrategy = HIDE
rootFragments = listOf(
DashboardFragment.newInstance(),
GradeFragment.newInstance(),
AttendanceFragment.newInstance(),
TimetableFragment.newInstance(),
MoreFragment.newInstance()
)
}
}
override fun onPreferenceStartFragment(
@ -317,6 +232,22 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
ViewCompat.setElevation(binding.mainToolbar, if (show) dpToPx(4f) else 0f)
}
override fun showBottomNavigation(show: Boolean) {
binding.mainBottomNav.isVisible = show
if (appInfo.systemVersion >= P) {
window.navigationBarColor = if (show) {
getThemeAttrColor(android.R.attr.navigationBarColor)
} else {
getThemeAttrColor(R.attr.colorSurface)
}
}
}
override fun openMoreDestination(destination: Destination) {
pushView(destination.fragment)
}
override fun notifyMenuViewReselected() {
(navController.currentStack?.getOrNull(0) as? MainView.MainChildView)?.onFragmentReselected()
}
@ -373,6 +304,6 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
override fun onSaveInstanceState(outState: Bundle) {
super.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.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.main.MainView.Section.GRADE
import io.github.wulkanowy.ui.modules.main.MainView.Section.MESSAGE
import io.github.wulkanowy.ui.modules.main.MainView.Section.SCHOOL
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.account.AccountView
import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsView
import io.github.wulkanowy.ui.modules.grade.GradeView
import io.github.wulkanowy.ui.modules.message.MessageView
import io.github.wulkanowy.ui.modules.schoolandteachers.school.SchoolView
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.flowWithResource
import kotlinx.coroutines.flow.onEach
@ -27,19 +32,40 @@ class MainPresenter @Inject constructor(
private var studentsWitSemesters: List<StudentWithSemesters>? = null
fun onAttachView(view: MainView, initMenu: MainView.Section?) {
super.onAttachView(view)
view.apply {
getProperViewIndexes(initMenu).let { (main, more) ->
startMenuIndex = main
startMenuMoreIndex = more
private val rootDestinationTypeList = listOf(
Destination.Type.DASHBOARD,
Destination.Type.GRADE,
Destination.Type.ATTENDANCE,
Destination.Type.TIMETABLE,
Destination.Type.MORE
)
private val Destination?.startMenuIndex
get() = when {
this == null -> prefRepository.startMenuIndex
type in rootDestinationTypeList -> {
rootDestinationTypeList.indexOf(type)
}
initView()
Timber.i("Main view was initialized with $startMenuIndex menu index and $startMenuMoreIndex more index")
else -> 4
}
fun onAttachView(view: MainView, initDestination: Destination?) {
super.onAttachView(view)
val startMenuIndex = initDestination.startMenuIndex
val destinations = rootDestinationTypeList.map {
if (it == initDestination?.type) initDestination else it.defaultDestination
}
view.initView(startMenuIndex, destinations)
if (initDestination != null && startMenuIndex == 4) {
view.openMoreDestination(initDestination)
}
syncManager.startPeriodicSyncWorker()
analytics.logEvent("app_open", "destination" to initMenu?.name)
analytics.logEvent("app_open", "destination" to initDestination.toString())
Timber.i("Main view was initialized with $initDestination")
}
fun onActionMenuCreated() {
@ -64,9 +90,10 @@ class MainPresenter @Inject constructor(
}.launch("avatar")
}
fun onViewChange(section: MainView.Section?) {
fun onViewChange(destinationView: BaseView) {
view?.apply {
showActionBarElevation(section != GRADE && section != MESSAGE && section != SCHOOL)
showBottomNavigation(destinationView !is AccountView && destinationView !is StudentInfoView && destinationView !is AccountDetailsView)
showActionBarElevation(destinationView !is GradeView && destinationView !is MessageView && destinationView !is SchoolView)
currentViewTitle?.let { setViewTitle(it) }
currentViewSubtitle?.let { setViewSubTitle(it.ifBlank { null }) }
currentStackSize?.let {
@ -134,10 +161,4 @@ class MainPresenter @Inject constructor(
view?.showStudentAvatar(currentStudent)
}
private fun getProperViewIndexes(initMenu: MainView.Section?) = when (initMenu?.id) {
in 0..3 -> initMenu!!.id to -1
in 4..100 -> 4 to initMenu!!.id
else -> prefRepository.startMenuIndex to -1
}
}

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.StudentWithSemesters
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.Destination
interface MainView : BaseView {
var startMenuIndex: Int
var startMenuMoreIndex: Int
val isRootView: Boolean
val currentViewTitle: String?
@ -18,7 +15,7 @@ interface MainView : BaseView {
val currentStackSize: Int?
fun initView()
fun initView(startMenuIndex: Int, rootDestinations: List<Destination>)
fun switchMenuView(position: Int)
@ -28,6 +25,8 @@ interface MainView : BaseView {
fun showActionBarElevation(show: Boolean)
fun showBottomNavigation(show: Boolean)
fun notifyMenuViewReselected()
fun notifyMenuViewChanged()
@ -42,6 +41,8 @@ interface MainView : BaseView {
fun showInAppReview()
fun openMoreDestination(destination: Destination)
interface MainChildView {
fun onFragmentReselected()
@ -57,25 +58,4 @@ interface MainView : BaseView {
get() = ""
set(_) {}
}
enum class Section {
DASHBOARD,
GRADE,
ATTENDANCE,
TIMETABLE,
MORE,
MESSAGE,
EXAM,
HOMEWORK,
NOTE,
CONFERENCE,
SCHOOL_ANNOUNCEMENT,
SCHOOL,
LUCKY_NUMBER,
ACCOUNT,
STUDENT_INFO,
SETTINGS;
val id get() = ordinal
}
}

View File

@ -5,7 +5,6 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.databinding.ItemNotificationsCenterBinding
import io.github.wulkanowy.services.sync.notifications.NotificationType
@ -28,26 +27,12 @@ class NotificationsCenterAdapter @Inject constructor() :
notificationsCenterItemTitle.text = item.title
notificationsCenterItemContent.text = item.content
notificationsCenterItemDate.text = item.date.toFormattedString("HH:mm, d MMM")
notificationsCenterItemIcon.setImageResource(item.type.toDrawableResId())
notificationsCenterItemIcon.setImageResource(item.type.icon)
root.setOnClickListener { onItemClickListener(item.type) }
}
}
private fun NotificationType.toDrawableResId() = when (this) {
NotificationType.NEW_CONFERENCE -> R.drawable.ic_more_conferences
NotificationType.NEW_EXAM -> R.drawable.ic_main_exam
NotificationType.NEW_GRADE_DETAILS -> R.drawable.ic_stat_grade
NotificationType.NEW_GRADE_PREDICTED -> R.drawable.ic_stat_grade
NotificationType.NEW_GRADE_FINAL -> R.drawable.ic_stat_grade
NotificationType.NEW_HOMEWORK -> R.drawable.ic_more_homework
NotificationType.NEW_LUCKY_NUMBER -> R.drawable.ic_stat_luckynumber
NotificationType.NEW_MESSAGE -> R.drawable.ic_stat_message
NotificationType.NEW_NOTE -> R.drawable.ic_stat_note
NotificationType.NEW_ANNOUNCEMENT -> R.drawable.ic_all_about
NotificationType.PUSH -> R.drawable.ic_stat_all
}
class ViewHolder(val binding: ItemNotificationsCenterBinding) :
RecyclerView.ViewHolder(binding.root)
@ -59,4 +44,4 @@ class NotificationsCenterAdapter @Inject constructor() :
override fun areItemsTheSame(oldItem: Notification, newItem: Notification) =
oldItem.id == newItem.id
}
}
}

View File

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

View File

@ -6,7 +6,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.ui.modules.main.MainView
import timber.log.Timber
class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView {
class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, SettingsView {
companion object {
@ -19,4 +19,16 @@ class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView {
setPreferencesFromResource(R.xml.scheme_preferences, rootKey)
Timber.i("Settings view was initialized")
}
override fun showError(text: String, error: Throwable) {}
override fun showMessage(text: String) {}
override fun showExpiredDialog() {}
override fun openClearLoginView() {}
override fun showErrorDetailsDialog(error: Throwable) {}
override fun showChangePasswordSnackbar(redirectUrl: String) {}
}

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

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
}
inline val Attendance.description
inline val Attendance.descriptionRes
get() = when (AttendanceCategory.getCategoryByName(name)) {
AttendanceCategory.PRESENCE -> R.string.attendance_present
AttendanceCategory.ABSENCE_UNEXCUSED -> R.string.attendance_absence_unexcused

View File

@ -18,6 +18,7 @@ import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.PluralsRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.applyCanvas
@ -57,6 +58,9 @@ fun Context.getCompatDrawable(@DrawableRes drawableRes: Int, @ColorRes colorRes:
fun Context.getCompatBitmap(@DrawableRes drawableRes: Int, @ColorRes colorRes: Int) =
getCompatDrawable(drawableRes, colorRes)?.toBitmap()
fun Context.getPlural(@PluralsRes pluralRes: Int, quantity: Int, vararg arguments: Any) =
resources.getQuantityString(pluralRes, quantity, *arguments)
fun Context.openInternetBrowser(uri: String, onActivityNotFound: (uri: String) -> Unit = {}) {
Intent.parseUri(uri, 0).let {
try {

View File

@ -2,16 +2,19 @@ package io.github.wulkanowy.utils
import androidx.fragment.app.Fragment
import com.ncapdevi.fragnav.FragNavController
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.base.BaseView
inline fun FragNavController.setOnViewChangeListener(crossinline listener: (section: MainView.Section?, name: String?) -> Unit) {
inline fun FragNavController.setOnViewChangeListener(crossinline listener: (view: BaseView) -> Unit) {
transactionListener = object : FragNavController.TransactionListener {
override fun onFragmentTransaction(fragment: Fragment?, transactionType: FragNavController.TransactionType) {
listener(fragment?.toSection(), fragment?.let { it::class.java.simpleName })
override fun onFragmentTransaction(
fragment: Fragment?,
transactionType: FragNavController.TransactionType
) {
fragment?.let { listener(it as BaseView) }
}
override fun onTabTransaction(fragment: Fragment?, index: Int) {
listener(fragment?.toSection(), fragment?.let { it::class.java.simpleName })
fragment?.let { listener(it as BaseView) }
}
}
}

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_next">Next: %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-->
@ -200,6 +220,18 @@
<string name="attendance_excuse_title">Excuse</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>
<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-->
@ -706,6 +738,8 @@
<string name="channel_push">Push notifications</string>
<string name="channel_upcoming_lessons">Upcoming lessons</string>
<string name="channel_debug">Debug</string>
<string name="channel_change_timetable">Timetable change</string>
<string name="channel_new_attendance">New attendance</string>
<!--Colors-->

View File

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