Add notifications center (#1524)

This commit is contained in:
Rafał Borcz 2021-09-27 20:58:25 +02:00 committed by GitHub
parent dc90549b9d
commit d69118b085
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 3186 additions and 219 deletions

File diff suppressed because it is too large Load Diff

View File

@ -45,6 +45,7 @@
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute"> tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
<activity <activity
android:name=".ui.modules.splash.SplashActivity" android:name=".ui.modules.splash.SplashActivity"
android:exported="true"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/WulkanowyTheme.SplashScreen" android:theme="@style/WulkanowyTheme.SplashScreen"
tools:ignore="LockedOrientationActivity"> tools:ignore="LockedOrientationActivity">
@ -74,6 +75,7 @@
<activity <activity
android:name=".ui.modules.timetablewidget.TimetableWidgetConfigureActivity" android:name=".ui.modules.timetablewidget.TimetableWidgetConfigureActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true"
android:noHistory="true" android:noHistory="true"
android:theme="@style/WulkanowyTheme.WidgetAccountSwitcher"> android:theme="@style/WulkanowyTheme.WidgetAccountSwitcher">
<intent-filter> <intent-filter>
@ -83,6 +85,7 @@
<activity <activity
android:name=".ui.modules.luckynumberwidget.LuckyNumberWidgetConfigureActivity" android:name=".ui.modules.luckynumberwidget.LuckyNumberWidgetConfigureActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true"
android:noHistory="true" android:noHistory="true"
android:theme="@style/WulkanowyTheme.WidgetAccountSwitcher"> android:theme="@style/WulkanowyTheme.WidgetAccountSwitcher">
<intent-filter> <intent-filter>
@ -95,11 +98,20 @@
android:permission="android.permission.BIND_REMOTEVIEWS" /> android:permission="android.permission.BIND_REMOTEVIEWS" />
<service <service
android:name=".services.piggyback.VulcanNotificationListenerService" android:name=".services.piggyback.VulcanNotificationListenerService"
android:exported="true"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"> android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter> <intent-filter>
<action android:name="android.service.notification.NotificationListenerService" /> <action android:name="android.service.notification.NotificationListenerService" />
</intent-filter> </intent-filter>
</service> </service>
<service
android:name=".services.messaging.AppMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<receiver <receiver
android:name=".ui.modules.timetablewidget.TimetableWidgetProvider" android:name=".ui.modules.timetablewidget.TimetableWidgetProvider"
@ -114,6 +126,7 @@
</receiver> </receiver>
<receiver <receiver
android:name=".ui.modules.luckynumberwidget.LuckyNumberWidgetProvider" android:name=".ui.modules.luckynumberwidget.LuckyNumberWidgetProvider"
android:exported="true"
android:label="@string/lucky_number_title"> android:label="@string/lucky_number_title">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />

View File

@ -8,8 +8,8 @@ import androidx.preference.PreferenceManager
import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.chuckerteam.chucker.api.RetentionManager import com.chuckerteam.chucker.api.RetentionManager
import com.squareup.moshi.Moshi
import com.fredporciuncula.flow.preferences.FlowSharedPreferences import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import com.squareup.moshi.Moshi
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -202,4 +202,8 @@ internal class RepositoryModule {
@Singleton @Singleton
@Provides @Provides
fun provideSchoolAnnouncementDao(database: AppDatabase) = database.schoolAnnouncementDao fun provideSchoolAnnouncementDao(database: AppDatabase) = database.schoolAnnouncementDao
@Singleton
@Provides
fun provideNotificationDao(database: AppDatabase) = database.notificationDao
} }

View File

@ -10,7 +10,6 @@ import io.github.wulkanowy.data.db.dao.AttendanceDao
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
import io.github.wulkanowy.data.db.dao.CompletedLessonsDao import io.github.wulkanowy.data.db.dao.CompletedLessonsDao
import io.github.wulkanowy.data.db.dao.ConferenceDao import io.github.wulkanowy.data.db.dao.ConferenceDao
import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao
import io.github.wulkanowy.data.db.dao.ExamDao import io.github.wulkanowy.data.db.dao.ExamDao
import io.github.wulkanowy.data.db.dao.GradeDao import io.github.wulkanowy.data.db.dao.GradeDao
import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao
@ -23,8 +22,10 @@ import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.dao.MobileDeviceDao import io.github.wulkanowy.data.db.dao.MobileDeviceDao
import io.github.wulkanowy.data.db.dao.NoteDao import io.github.wulkanowy.data.db.dao.NoteDao
import io.github.wulkanowy.data.db.dao.NotificationDao
import io.github.wulkanowy.data.db.dao.RecipientDao import io.github.wulkanowy.data.db.dao.RecipientDao
import io.github.wulkanowy.data.db.dao.ReportingUnitDao import io.github.wulkanowy.data.db.dao.ReportingUnitDao
import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao
import io.github.wulkanowy.data.db.dao.SchoolDao import io.github.wulkanowy.data.db.dao.SchoolDao
import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao import io.github.wulkanowy.data.db.dao.StudentDao
@ -38,7 +39,6 @@ import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.AttendanceSummary import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.data.db.entities.CompletedLesson import io.github.wulkanowy.data.db.entities.CompletedLesson
import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradePartialStatistics import io.github.wulkanowy.data.db.entities.GradePartialStatistics
@ -51,9 +51,11 @@ import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.db.entities.MobileDevice import io.github.wulkanowy.data.db.entities.MobileDevice
import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.ReportingUnit import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.School import io.github.wulkanowy.data.db.entities.School
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentInfo import io.github.wulkanowy.data.db.entities.StudentInfo
@ -95,6 +97,7 @@ import io.github.wulkanowy.data.db.migrations.Migration37
import io.github.wulkanowy.data.db.migrations.Migration38 import io.github.wulkanowy.data.db.migrations.Migration38
import io.github.wulkanowy.data.db.migrations.Migration39 import io.github.wulkanowy.data.db.migrations.Migration39
import io.github.wulkanowy.data.db.migrations.Migration4 import io.github.wulkanowy.data.db.migrations.Migration4
import io.github.wulkanowy.data.db.migrations.Migration40
import io.github.wulkanowy.data.db.migrations.Migration5 import io.github.wulkanowy.data.db.migrations.Migration5
import io.github.wulkanowy.data.db.migrations.Migration6 import io.github.wulkanowy.data.db.migrations.Migration6
import io.github.wulkanowy.data.db.migrations.Migration7 import io.github.wulkanowy.data.db.migrations.Migration7
@ -134,6 +137,7 @@ import javax.inject.Singleton
StudentInfo::class, StudentInfo::class,
TimetableHeader::class, TimetableHeader::class,
SchoolAnnouncement::class, SchoolAnnouncement::class,
Notification::class
], ],
version = AppDatabase.VERSION_SCHEMA, version = AppDatabase.VERSION_SCHEMA,
exportSchema = true exportSchema = true
@ -142,7 +146,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val VERSION_SCHEMA = 39 const val VERSION_SCHEMA = 40
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(), Migration2(),
@ -183,6 +187,7 @@ abstract class AppDatabase : RoomDatabase() {
Migration37(), Migration37(),
Migration38(), Migration38(),
Migration39(), Migration39(),
Migration40()
) )
fun newInstance( fun newInstance(
@ -252,4 +257,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract val timetableHeaderDao: TimetableHeaderDao abstract val timetableHeaderDao: TimetableHeaderDao
abstract val schoolAnnouncementDao: SchoolAnnouncementDao abstract val schoolAnnouncementDao: SchoolAnnouncementDao
abstract val notificationDao: NotificationDao
} }

View File

@ -0,0 +1,15 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.Notification
import kotlinx.coroutines.flow.Flow
import javax.inject.Singleton
@Singleton
@Dao
interface NotificationDao : BaseDao<Notification> {
@Query("SELECT * FROM Notifications WHERE student_id = :studentId OR student_id = -1")
fun loadAll(studentId: Long): Flow<List<Notification>>
}

View File

@ -0,0 +1,27 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import io.github.wulkanowy.services.sync.notifications.NotificationType
import java.time.LocalDateTime
@Entity(tableName = "Notifications")
data class Notification(
@ColumnInfo(name = "student_id")
val studentId: Long,
val title: String,
val content: String,
val type: NotificationType,
val date: LocalDateTime,
val data: String? = null
) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

@ -0,0 +1,23 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration40 : Migration(39, 40) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `Notifications` (
`student_id` INTEGER NOT NULL,
`title` TEXT NOT NULL,
`content` TEXT NOT NULL,
`type` TEXT NOT NULL,
`date` INTEGER NOT NULL,
`data` TEXT,
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
)
"""
)
}
}

View File

@ -6,7 +6,7 @@ import androidx.annotation.StringRes
import io.github.wulkanowy.services.sync.notifications.NotificationType import io.github.wulkanowy.services.sync.notifications.NotificationType
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
sealed interface Notification { sealed interface NotificationData {
val type: NotificationType val type: NotificationType
val startMenu: MainView.Section val startMenu: MainView.Section
val icon: Int val icon: Int
@ -14,7 +14,7 @@ sealed interface Notification {
val contentStringRes: Int val contentStringRes: Int
} }
data class MultipleNotifications( data class MultipleNotificationsData(
override val type: NotificationType, override val type: NotificationType,
override val startMenu: MainView.Section, override val startMenu: MainView.Section,
@DrawableRes override val icon: Int, @DrawableRes override val icon: Int,
@ -23,9 +23,9 @@ data class MultipleNotifications(
@PluralsRes val summaryStringRes: Int, @PluralsRes val summaryStringRes: Int,
val lines: List<String>, val lines: List<String>,
) : Notification ) : NotificationData
data class OneNotification( data class OneNotificationData(
override val type: NotificationType, override val type: NotificationType,
override val startMenu: MainView.Section, override val startMenu: MainView.Section,
@DrawableRes override val icon: Int, @DrawableRes override val icon: Int,
@ -33,4 +33,4 @@ data class OneNotification(
@StringRes override val contentStringRes: Int, @StringRes override val contentStringRes: Int,
val contentValues: List<String>, val contentValues: List<String>,
) : Notification ) : NotificationData

View File

@ -0,0 +1,15 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.NotificationDao
import io.github.wulkanowy.data.db.entities.Notification
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class NotificationRepository @Inject constructor(private val notificationDao: NotificationDao) {
fun getNotifications(studentId: Long) = notificationDao.loadAll(studentId)
suspend fun saveNotification(notification: Notification) =
notificationDao.insertAll(listOf(notification))
}

View File

@ -0,0 +1,145 @@
package io.github.wulkanowy.services.sync.notifications
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.data.pojos.OneNotificationData
import io.github.wulkanowy.data.repositories.NotificationRepository
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.getCompatBitmap
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.nickOrName
import java.time.LocalDateTime
import javax.inject.Inject
import kotlin.random.Random
class AppNotificationManager @Inject constructor(
private val notificationManager: NotificationManagerCompat,
@ApplicationContext private val context: Context,
private val appInfo: AppInfo,
private val notificationRepository: NotificationRepository
) {
suspend fun sendNotification(notificationData: NotificationData, student: Student) =
when (notificationData) {
is OneNotificationData -> sendOneNotification(notificationData, student)
is MultipleNotificationsData -> sendMultipleNotifications(notificationData, student)
}
private suspend fun sendOneNotification(
notificationData: OneNotificationData,
student: Student
) {
val content = context.getString(
notificationData.contentStringRes,
*notificationData.contentValues.toTypedArray()
)
val title = context.getString(notificationData.titleStringRes)
val notification = getDefaultNotificationBuilder(notificationData)
.setContentTitle(title)
.setContentText(content)
.setStyle(
NotificationCompat.BigTextStyle()
.setSummaryText(student.nickOrName)
.bigText(content)
)
.build()
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), notification)
saveNotification(title, content, notificationData, student)
}
private suspend fun sendMultipleNotifications(
notificationData: MultipleNotificationsData,
student: Student
) {
val groupType = notificationData.type.group ?: return
val group = "${groupType}_${student.id}"
val groupId = student.id * 100 + notificationData.type.ordinal
notificationData.lines.forEach { item ->
val title = context.resources.getQuantityString(notificationData.titleStringRes, 1)
val notification = getDefaultNotificationBuilder(notificationData)
.setContentTitle(title)
.setContentText(item)
.setStyle(
NotificationCompat.BigTextStyle()
.setSummaryText(student.nickOrName)
.bigText(item)
)
.setGroup(group)
.build()
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), notification)
saveNotification(title, item, notificationData, student)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
val summaryNotification = getDefaultNotificationBuilder(notificationData)
.setSmallIcon(notificationData.icon)
.setGroup(group)
.setStyle(NotificationCompat.InboxStyle().setSummaryText(student.nickOrName))
.setGroupSummary(true)
.build()
notificationManager.notify(groupId.toInt(), summaryNotification)
}
@SuppressLint("InlinedApi")
private fun getDefaultNotificationBuilder(notificationData: NotificationData): NotificationCompat.Builder {
val pendingIntentsFlags = if (appInfo.systemVersion >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
return NotificationCompat.Builder(context, notificationData.type.channel)
.setLargeIcon(context.getCompatBitmap(notificationData.icon, R.color.colorPrimary))
.setSmallIcon(R.drawable.ic_stat_all)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setContentIntent(
PendingIntent.getActivity(
context,
notificationData.startMenu.id,
MainActivity.getStartIntent(context, notificationData.startMenu, true),
pendingIntentsFlags
)
)
}
private suspend fun saveNotification(
title: String,
content: String,
notificationData: NotificationData,
student: Student
) {
val notificationEntity = Notification(
studentId = student.id,
title = title,
content = content,
type = notificationData.type,
date = LocalDateTime.now()
)
notificationRepository.saveNotification(notificationEntity)
}
}

View File

@ -1,102 +0,0 @@
package io.github.wulkanowy.services.sync.notifications
import android.app.PendingIntent
import android.content.Context
import android.os.Build
import androidx.annotation.PluralsRes
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.Notification
import io.github.wulkanowy.data.pojos.OneNotification
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.utils.getCompatBitmap
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.nickOrName
import kotlin.random.Random
abstract class BaseNotification(
private val context: Context,
private val notificationManager: NotificationManagerCompat,
) {
protected fun sendNotification(notification: Notification, student: Student) =
when (notification) {
is OneNotification -> sendOneNotification(notification, student)
is MultipleNotifications -> sendMultipleNotifications(notification, student)
}
private fun sendOneNotification(notification: OneNotification, student: Student?) {
notificationManager.notify(
Random.nextInt(Int.MAX_VALUE),
getNotificationBuilder(notification).apply {
val content = context.getString(
notification.contentStringRes,
*notification.contentValues.toTypedArray()
)
setContentTitle(context.getString(notification.titleStringRes))
setContentText(content)
setStyle(
NotificationCompat.BigTextStyle()
.setSummaryText(student?.nickOrName)
.bigText(content)
)
}.build()
)
}
private fun sendMultipleNotifications(notification: MultipleNotifications, student: Student) {
val group = notification.type.group + student.id
val groupId = student.id * 100 + notification.type.ordinal
notification.lines.forEach { item ->
notificationManager.notify(
Random.nextInt(Int.MAX_VALUE),
getNotificationBuilder(notification).apply {
setContentTitle(getQuantityString(notification.titleStringRes, 1))
setContentText(item)
setStyle(
NotificationCompat.BigTextStyle()
.setSummaryText(student.nickOrName)
.bigText(item)
)
setGroup(group)
}.build()
)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
notificationManager.notify(
groupId.toInt(),
getNotificationBuilder(notification).apply {
setSmallIcon(notification.icon)
setGroup(group)
setStyle(NotificationCompat.InboxStyle().setSummaryText(student.nickOrName))
setGroupSummary(true)
}.build()
)
}
private fun getNotificationBuilder(notification: Notification) = NotificationCompat
.Builder(context, notification.type.channel)
.setLargeIcon(context.getCompatBitmap(notification.icon, R.color.colorPrimary))
.setSmallIcon(R.drawable.ic_stat_all)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setContentIntent(
PendingIntent.getActivity(
context, notification.startMenu.id,
MainActivity.getStartIntent(context, notification.startMenu, true),
PendingIntent.FLAG_UPDATE_CURRENT
)
)
private fun getQuantityString(@PluralsRes id: Int, value: Int): String {
return context.resources.getQuantityString(id, value, value)
}
}

View File

@ -1,29 +1,25 @@
package io.github.wulkanowy.services.sync.notifications package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDateTime import java.time.LocalDateTime
import javax.inject.Inject import javax.inject.Inject
class NewConferenceNotification @Inject constructor( class NewConferenceNotification @Inject constructor(
@ApplicationContext private val context: Context, private val appNotificationManager: AppNotificationManager
notificationManager: NotificationManagerCompat, ) {
) : BaseNotification(context, notificationManager) {
fun notify(items: List<Conference>, student: Student) { suspend fun notify(items: List<Conference>, student: Student) {
val today = LocalDateTime.now() val today = LocalDateTime.now()
val lines = items.filter { !it.date.isBefore(today) }.map { val lines = items.filter { !it.date.isBefore(today) }.map {
"${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}" "${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}"
}.ifEmpty { return } }.ifEmpty { return }
val notification = MultipleNotifications( val notification = MultipleNotificationsData(
type = NotificationType.NEW_CONFERENCE, type = NotificationType.NEW_CONFERENCE,
icon = R.drawable.ic_more_conferences, icon = R.drawable.ic_more_conferences,
titleStringRes = R.plurals.conference_notify_new_item_title, titleStringRes = R.plurals.conference_notify_new_item_title,
@ -33,6 +29,6 @@ class NewConferenceNotification @Inject constructor(
lines = lines lines = lines
) )
sendNotification(notification, student) appNotificationManager.sendNotification(notification, student)
} }
} }

View File

@ -1,29 +1,25 @@
package io.github.wulkanowy.services.sync.notifications package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
class NewExamNotification @Inject constructor( class NewExamNotification @Inject constructor(
@ApplicationContext private val context: Context, private val appNotificationManager: AppNotificationManager
notificationManager: NotificationManagerCompat, ) {
) : BaseNotification(context, notificationManager) {
fun notify(items: List<Exam>, student: Student) { suspend fun notify(items: List<Exam>, student: Student) {
val today = LocalDate.now() val today = LocalDate.now()
val lines = items.filter { !it.date.isBefore(today) }.map { val lines = items.filter { !it.date.isBefore(today) }.map {
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}" "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}"
}.ifEmpty { return } }.ifEmpty { return }
val notification = MultipleNotifications( val notification = MultipleNotificationsData(
type = NotificationType.NEW_EXAM, type = NotificationType.NEW_EXAM,
icon = R.drawable.ic_main_exam, icon = R.drawable.ic_main_exam,
titleStringRes = R.plurals.exam_notify_new_item_title, titleStringRes = R.plurals.exam_notify_new_item_title,
@ -33,6 +29,6 @@ class NewExamNotification @Inject constructor(
lines = lines lines = lines
) )
sendNotification(notification, student) appNotificationManager.sendNotification(notification, student)
} }
} }

View File

@ -1,23 +1,19 @@
package io.github.wulkanowy.services.sync.notifications package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject import javax.inject.Inject
class NewGradeNotification @Inject constructor( class NewGradeNotification @Inject constructor(
@ApplicationContext private val context: Context, private val appNotificationManager: AppNotificationManager
notificationManager: NotificationManagerCompat, ) {
) : BaseNotification(context, notificationManager) {
fun notifyDetails(items: List<Grade>, student: Student) { suspend fun notifyDetails(items: List<Grade>, student: Student) {
val notification = MultipleNotifications( val notification = MultipleNotificationsData(
type = NotificationType.NEW_GRADE_DETAILS, type = NotificationType.NEW_GRADE_DETAILS,
icon = R.drawable.ic_stat_grade, icon = R.drawable.ic_stat_grade,
titleStringRes = R.plurals.grade_new_items, titleStringRes = R.plurals.grade_new_items,
@ -29,11 +25,11 @@ class NewGradeNotification @Inject constructor(
} }
) )
sendNotification(notification, student) appNotificationManager.sendNotification(notification, student)
} }
fun notifyPredicted(items: List<GradeSummary>, student: Student) { suspend fun notifyPredicted(items: List<GradeSummary>, student: Student) {
val notification = MultipleNotifications( val notification = MultipleNotificationsData(
type = NotificationType.NEW_GRADE_PREDICTED, type = NotificationType.NEW_GRADE_PREDICTED,
icon = R.drawable.ic_stat_grade, icon = R.drawable.ic_stat_grade,
titleStringRes = R.plurals.grade_new_items_predicted, titleStringRes = R.plurals.grade_new_items_predicted,
@ -45,11 +41,11 @@ class NewGradeNotification @Inject constructor(
} }
) )
sendNotification(notification, student) appNotificationManager.sendNotification(notification, student)
} }
fun notifyFinal(items: List<GradeSummary>, student: Student) { suspend fun notifyFinal(items: List<GradeSummary>, student: Student) {
val notification = MultipleNotifications( val notification = MultipleNotificationsData(
type = NotificationType.NEW_GRADE_FINAL, type = NotificationType.NEW_GRADE_FINAL,
icon = R.drawable.ic_stat_grade, icon = R.drawable.ic_stat_grade,
titleStringRes = R.plurals.grade_new_items_final, titleStringRes = R.plurals.grade_new_items_final,
@ -61,6 +57,6 @@ class NewGradeNotification @Inject constructor(
} }
) )
sendNotification(notification, student) appNotificationManager.sendNotification(notification, student)
} }
} }

View File

@ -1,29 +1,25 @@
package io.github.wulkanowy.services.sync.notifications package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
class NewHomeworkNotification @Inject constructor( class NewHomeworkNotification @Inject constructor(
@ApplicationContext private val context: Context, private val appNotificationManager: AppNotificationManager
notificationManager: NotificationManagerCompat, ) {
) : BaseNotification(context, notificationManager) {
fun notify(items: List<Homework>, student: Student) { suspend fun notify(items: List<Homework>, student: Student) {
val today = LocalDate.now() val today = LocalDate.now()
val lines = items.filter { !it.date.isBefore(today) }.map { val lines = items.filter { !it.date.isBefore(today) }.map {
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}" "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}"
}.ifEmpty { return } }.ifEmpty { return }
val notification = MultipleNotifications( val notification = MultipleNotificationsData(
type = NotificationType.NEW_HOMEWORK, type = NotificationType.NEW_HOMEWORK,
icon = R.drawable.ic_more_homework, icon = R.drawable.ic_more_homework,
titleStringRes = R.plurals.homework_notify_new_item_title, titleStringRes = R.plurals.homework_notify_new_item_title,
@ -33,6 +29,6 @@ class NewHomeworkNotification @Inject constructor(
lines = lines lines = lines
) )
sendNotification(notification, student) appNotificationManager.sendNotification(notification, student)
} }
} }

View File

@ -1,30 +1,26 @@
package io.github.wulkanowy.services.sync.notifications package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.OneNotification import io.github.wulkanowy.data.pojos.OneNotificationData
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject import javax.inject.Inject
class NewLuckyNumberNotification @Inject constructor( class NewLuckyNumberNotification @Inject constructor(
@ApplicationContext private val context: Context, private val appNotificationManager: AppNotificationManager
notificationManager: NotificationManagerCompat, ) {
) : BaseNotification(context, notificationManager) {
fun notify(item: LuckyNumber, student: Student) { suspend fun notify(item: LuckyNumber, student: Student) {
val notification = OneNotification( val notification = OneNotificationData(
type = NotificationType.NEW_LUCKY_NUMBER, type = NotificationType.NEW_LUCKY_NUMBER,
icon = R.drawable.ic_stat_luckynumber, icon = R.drawable.ic_stat_luckynumber,
titleStringRes = R.string.lucky_number_notify_new_item_title, titleStringRes = R.string.lucky_number_notify_new_item_title,
contentStringRes = R.string.lucky_number_notify_new_item, contentStringRes = R.string.lucky_number_notify_new_item,
startMenu = MainView.Section.LUCKY_NUMBER, startMenu = MainView.Section.LUCKY_NUMBER,
contentValues = listOf(item.luckyNumber.toString()) contentValues = listOf(item.luckyNumber.toString())
) )
sendNotification(notification, student) appNotificationManager.sendNotification(notification, student)
} }
} }

View File

@ -1,22 +1,18 @@
package io.github.wulkanowy.services.sync.notifications package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject import javax.inject.Inject
class NewMessageNotification @Inject constructor( class NewMessageNotification @Inject constructor(
@ApplicationContext private val context: Context, private val appNotificationManager: AppNotificationManager
notificationManager: NotificationManagerCompat, ) {
) : BaseNotification(context, notificationManager) {
fun notify(items: List<Message>, student: Student) { suspend fun notify(items: List<Message>, student: Student) {
val notification = MultipleNotifications( val notification = MultipleNotificationsData(
type = NotificationType.NEW_MESSAGE, type = NotificationType.NEW_MESSAGE,
icon = R.drawable.ic_stat_message, icon = R.drawable.ic_stat_message,
titleStringRes = R.plurals.message_new_items, titleStringRes = R.plurals.message_new_items,
@ -28,6 +24,6 @@ class NewMessageNotification @Inject constructor(
} }
) )
sendNotification(notification, student) appNotificationManager.sendNotification(notification, student)
} }
} }

View File

@ -1,23 +1,19 @@
package io.github.wulkanowy.services.sync.notifications package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject import javax.inject.Inject
class NewNoteNotification @Inject constructor( class NewNoteNotification @Inject constructor(
@ApplicationContext private val context: Context, private val appNotificationManager: AppNotificationManager
notificationManager: NotificationManagerCompat, ) {
) : BaseNotification(context, notificationManager) {
fun notify(items: List<Note>, student: Student) { suspend fun notify(items: List<Note>, student: Student) {
val notification = MultipleNotifications( val notification = MultipleNotificationsData(
type = NotificationType.NEW_NOTE, type = NotificationType.NEW_NOTE,
icon = R.drawable.ic_stat_note, icon = R.drawable.ic_stat_note,
titleStringRes = when (NoteCategory.getByValue(items.first().categoryType)) { titleStringRes = when (NoteCategory.getByValue(items.first().categoryType)) {
@ -41,6 +37,6 @@ class NewNoteNotification @Inject constructor(
} }
) )
sendNotification(notification, student) appNotificationManager.sendNotification(notification, student)
} }
} }

View File

@ -1,33 +1,29 @@
package io.github.wulkanowy.services.sync.notifications package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject import javax.inject.Inject
class NewSchoolAnnouncementNotification @Inject constructor( class NewSchoolAnnouncementNotification @Inject constructor(
@ApplicationContext private val context: Context, private val appNotificationManager: AppNotificationManager
notificationManager: NotificationManagerCompat, ) {
) : BaseNotification(context, notificationManager) {
fun notify(items: List<SchoolAnnouncement>, student: Student) { suspend fun notify(items: List<SchoolAnnouncement>, student: Student) {
val notification = MultipleNotifications( val notification = MultipleNotificationsData(
type = NotificationType.NEW_ANNOUNCEMENT, type = NotificationType.NEW_ANNOUNCEMENT,
icon = R.drawable.ic_all_about, icon = R.drawable.ic_all_about,
titleStringRes = R.plurals.school_announcement_notify_new_item_title, titleStringRes = R.plurals.school_announcement_notify_new_item_title,
contentStringRes = R.plurals.school_announcement_notify_new_items, contentStringRes = R.plurals.school_announcement_notify_new_items,
summaryStringRes = R.plurals.school_announcement_number_item, summaryStringRes = R.plurals.school_announcement_number_item,
startMenu = MainView.Section.SCHOOL_ANNOUNCEMENT, startMenu = MainView.Section.SCHOOL_ANNOUNCEMENT,
lines = items.map { lines = items.map {
"${it.subject}: ${it.content}" "${it.subject}: ${it.content}"
} }
) )
sendNotification(notification, student) appNotificationManager.sendNotification(notification, student)
} }
} }

View File

@ -8,8 +8,9 @@ import io.github.wulkanowy.services.sync.channels.NewHomeworkChannel
import io.github.wulkanowy.services.sync.channels.NewMessagesChannel import io.github.wulkanowy.services.sync.channels.NewMessagesChannel
import io.github.wulkanowy.services.sync.channels.NewNotesChannel import io.github.wulkanowy.services.sync.channels.NewNotesChannel
import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel
import io.github.wulkanowy.services.sync.channels.PushChannel
enum class NotificationType(val group: String, val channel: String) { enum class NotificationType(val group: String?, val channel: String) {
NEW_CONFERENCE("new_conferences_group", NewConferencesChannel.CHANNEL_ID), NEW_CONFERENCE("new_conferences_group", NewConferencesChannel.CHANNEL_ID),
NEW_EXAM("new_exam_group", NewExamChannel.CHANNEL_ID), NEW_EXAM("new_exam_group", NewExamChannel.CHANNEL_ID),
NEW_GRADE_DETAILS("new_grade_details_group", NewGradesChannel.CHANNEL_ID), NEW_GRADE_DETAILS("new_grade_details_group", NewGradesChannel.CHANNEL_ID),
@ -20,4 +21,5 @@ enum class NotificationType(val group: String, val channel: String) {
NEW_MESSAGE("new_message_group", NewMessagesChannel.CHANNEL_ID), NEW_MESSAGE("new_message_group", NewMessagesChannel.CHANNEL_ID),
NEW_NOTE("new_notes_group", NewNotesChannel.CHANNEL_ID), NEW_NOTE("new_notes_group", NewNotesChannel.CHANNEL_ID),
NEW_ANNOUNCEMENT("new_school_announcements_group", NewSchoolAnnouncementsChannel.CHANNEL_ID), NEW_ANNOUNCEMENT("new_school_announcements_group", NewSchoolAnnouncementsChannel.CHANNEL_ID),
PUSH(null, PushChannel.CHANNEL_ID)
} }

View File

@ -9,9 +9,12 @@ import io.github.wulkanowy.utils.waitForResult
import java.time.LocalDate.now import java.time.LocalDate.now
import javax.inject.Inject import javax.inject.Inject
class AttendanceWork @Inject constructor(private val attendanceRepository: AttendanceRepository) : Work { class AttendanceWork @Inject constructor(
private val attendanceRepository: AttendanceRepository
) : Work {
override suspend fun doWork(student: Student, semester: Semester) { override suspend fun doWork(student: Student, semester: Semester) {
attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true).waitForResult() attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true)
.waitForResult()
} }
} }

View File

@ -24,6 +24,7 @@ import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment
import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.MessageFragment import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.capitalise
@ -120,6 +121,7 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.dashboard_menu_tiles -> presenter.onDashboardTileSettingsSelected() R.id.dashboard_menu_tiles -> presenter.onDashboardTileSettingsSelected()
R.id.dashboard_menu_notifaction_list -> presenter.onNotificationsCenterSelected()
else -> false else -> false
} }
} }
@ -182,6 +184,10 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
if (::presenter.isInitialized) presenter.onViewReselected() if (::presenter.isInitialized) presenter.onViewReselected()
} }
override fun openNotificationsCenterView() {
(requireActivity() as MainActivity).pushView(NotificationsCenterFragment.newInstance())
}
override fun onDestroyView() { override fun onDestroyView() {
dashboardAdapter.clearTimers() dashboardAdapter.clearTimers()
presenter.onDetachView() presenter.onDetachView()

View File

@ -209,6 +209,11 @@ class DashboardPresenter @Inject constructor(
view?.showErrorDetailsDialog(lastError) view?.showErrorDetailsDialog(lastError)
} }
fun onNotificationsCenterSelected(): Boolean {
view?.openNotificationsCenterView()
return true
}
fun onDashboardTileSettingsSelected(): Boolean { fun onDashboardTileSettingsSelected(): Boolean {
view?.showDashboardTileSettings(preferencesRepository.selectedDashboardTiles.toList()) view?.showDashboardTileSettings(preferencesRepository.selectedDashboardTiles.toList())
return true return true

View File

@ -23,4 +23,6 @@ interface DashboardView : BaseView {
fun resetView() fun resetView()
fun popViewToRoot() fun popViewToRoot()
fun openNotificationsCenterView()
} }

View File

@ -87,7 +87,7 @@ class NotificationDebugPresenter @Inject constructor(
} }
} }
private fun withStudent(block: (Student) -> Unit) { private fun withStudent(block: suspend (Student) -> Unit) {
launch { launch {
block(studentRepository.getCurrentStudent(false)) block(studentRepository.getCurrentStudent(false))
} }

View File

@ -0,0 +1,62 @@
package io.github.wulkanowy.ui.modules.notificationscenter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.databinding.ItemNotificationsCenterBinding
import io.github.wulkanowy.services.sync.notifications.NotificationType
import io.github.wulkanowy.utils.toFormattedString
import javax.inject.Inject
class NotificationsCenterAdapter @Inject constructor() :
ListAdapter<Notification, NotificationsCenterAdapter.ViewHolder>(DiffUtilCallback()) {
var onItemClickListener: (NotificationType) -> Unit = {}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemNotificationsCenterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
with(holder.binding) {
notificationsCenterItemTitle.text = item.title
notificationsCenterItemContent.text = item.content
notificationsCenterItemDate.text = item.date.toFormattedString("HH:mm, d MMM")
notificationsCenterItemIcon.setImageResource(item.type.toDrawableResId())
root.setOnClickListener { onItemClickListener(item.type) }
}
}
private fun NotificationType.toDrawableResId() = when (this) {
NotificationType.NEW_CONFERENCE -> R.drawable.ic_more_conferences
NotificationType.NEW_EXAM -> R.drawable.ic_main_exam
NotificationType.NEW_GRADE_DETAILS -> R.drawable.ic_stat_grade
NotificationType.NEW_GRADE_PREDICTED -> R.drawable.ic_stat_grade
NotificationType.NEW_GRADE_FINAL -> R.drawable.ic_stat_grade
NotificationType.NEW_HOMEWORK -> R.drawable.ic_more_homework
NotificationType.NEW_LUCKY_NUMBER -> R.drawable.ic_stat_luckynumber
NotificationType.NEW_MESSAGE -> R.drawable.ic_stat_message
NotificationType.NEW_NOTE -> R.drawable.ic_stat_note
NotificationType.NEW_ANNOUNCEMENT -> R.drawable.ic_all_about
NotificationType.PUSH -> R.drawable.ic_stat_all
}
class ViewHolder(val binding: ItemNotificationsCenterBinding) :
RecyclerView.ViewHolder(binding.root)
private class DiffUtilCallback : DiffUtil.ItemCallback<Notification>() {
override fun areContentsTheSame(oldItem: Notification, newItem: Notification) =
oldItem == newItem
override fun areItemsTheSame(oldItem: Notification, newItem: Notification) =
oldItem.id == newItem.id
}
}

View File

@ -0,0 +1,108 @@
package io.github.wulkanowy.ui.modules.notificationscenter
import android.os.Bundle
import android.view.View
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.databinding.FragmentNotificationsCenterBinding
import io.github.wulkanowy.services.sync.notifications.NotificationType
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.conference.ConferenceFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.homework.HomeworkFragment
import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.note.NoteFragment
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
import javax.inject.Inject
@AndroidEntryPoint
class NotificationsCenterFragment :
BaseFragment<FragmentNotificationsCenterBinding>(R.layout.fragment_notifications_center),
NotificationsCenterView, MainView.TitledView {
@Inject
lateinit var presenter: NotificationsCenterPresenter
@Inject
lateinit var notificationsCenterAdapter: NotificationsCenterAdapter
companion object {
fun newInstance() = NotificationsCenterFragment()
}
override val titleStringId: Int
get() = R.string.notifications_center_title
override val isViewEmpty: Boolean
get() = notificationsCenterAdapter.itemCount == 0
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentNotificationsCenterBinding.bind(view)
presenter.onAttachView(this)
}
override fun initView() {
notificationsCenterAdapter.onItemClickListener = { notificationType ->
notificationType.toDestinationFragment()
?.let { (requireActivity() as MainActivity).pushView(it) }
}
with(binding.notificationsCenterRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = notificationsCenterAdapter
}
}
override fun updateData(data: List<Notification>) {
notificationsCenterAdapter.submitList(data)
}
override fun showEmpty(show: Boolean) {
binding.notificationsCenterEmpty.isVisible = show
}
override fun showProgress(show: Boolean) {
binding.notificationsCenterProgress.isVisible = show
}
override fun showContent(show: Boolean) {
binding.notificationsCenterRecycler.isVisible = show
}
override fun showErrorView(show: Boolean) {
binding.notificationCenterError.isVisible = show
}
override fun setErrorDetails(message: String) {
binding.notificationCenterErrorMessage.text = message
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
private fun NotificationType.toDestinationFragment(): Fragment? = when (this) {
NotificationType.NEW_CONFERENCE -> ConferenceFragment.newInstance()
NotificationType.NEW_EXAM -> ExamFragment.newInstance()
NotificationType.NEW_GRADE_DETAILS -> GradeFragment.newInstance()
NotificationType.NEW_GRADE_PREDICTED -> GradeFragment.newInstance()
NotificationType.NEW_GRADE_FINAL -> GradeFragment.newInstance()
NotificationType.NEW_HOMEWORK -> HomeworkFragment.newInstance()
NotificationType.NEW_LUCKY_NUMBER -> LuckyNumberFragment.newInstance()
NotificationType.NEW_MESSAGE -> MessageFragment.newInstance()
NotificationType.NEW_NOTE -> NoteFragment.newInstance()
NotificationType.NEW_ANNOUNCEMENT -> SchoolAnnouncementFragment.newInstance()
NotificationType.PUSH -> null
}
}

View File

@ -0,0 +1,83 @@
package io.github.wulkanowy.ui.modules.notificationscenter
import io.github.wulkanowy.data.repositories.NotificationRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import javax.inject.Inject
class NotificationsCenterPresenter @Inject constructor(
private val notificationRepository: NotificationRepository,
errorHandler: ErrorHandler,
studentRepository: StudentRepository
) : BasePresenter<NotificationsCenterView>(errorHandler, studentRepository) {
private lateinit var lastError: Throwable
override fun onAttachView(view: NotificationsCenterView) {
super.onAttachView(view)
view.initView()
Timber.i("Notifications centre view was initialized")
errorHandler.showErrorMessage = ::showErrorViewOnError
loadData()
}
fun onRetry() {
view?.run {
showErrorView(false)
showProgress(true)
}
loadData()
}
fun onDetailsClick() {
view?.showErrorDetailsDialog(lastError)
}
private fun loadData() {
Timber.i("Loading notifications data started")
flow {
val studentId = studentRepository.getCurrentStudent(false).id
emitAll(notificationRepository.getNotifications(studentId))
}
.map { notificationList -> notificationList.sortedByDescending { it.date } }
.catch { Timber.i("Loading notifications result: An exception occurred") }
.onEach {
Timber.i("Loading notifications result: Success")
if (it.isEmpty()) {
view?.run {
showContent(false)
showProgress(false)
showEmpty(true)
}
} else {
view?.run {
showContent(true)
showProgress(false)
showEmpty(false)
updateData(it)
}
}
}
.launch()
}
private fun showErrorViewOnError(message: String, error: Throwable) {
view?.run {
if (isViewEmpty) {
lastError = error
setErrorDetails(message)
showErrorView(true)
showEmpty(false)
} else showError(message, error)
}
}
}

View File

@ -0,0 +1,23 @@
package io.github.wulkanowy.ui.modules.notificationscenter
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.ui.base.BaseView
interface NotificationsCenterView : BaseView {
val isViewEmpty: Boolean
fun initView()
fun updateData(data: List<Notification>)
fun showProgress(show: Boolean)
fun showEmpty(show: Boolean)
fun showContent(show: Boolean)
fun showErrorView(show: Boolean)
fun setErrorDetails(message: String)
}

View File

@ -184,6 +184,7 @@ class NotificationsFragment : PreferenceFragmentCompat(),
.setNegativeButton(android.R.string.cancel) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
setNotificationPiggybackPreferenceChecked(false) setNotificationPiggybackPreferenceChecked(false)
} }
.setOnDismissListener { setNotificationPiggybackPreferenceChecked(false) }
.show() .show()
} }

View File

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/notifications_center_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="16dp"
android:visibility="gone"
tools:itemCount="4"
tools:listitem="@layout/item_notifications_center"
tools:visibility="visible" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/notifications_center_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="gone" />
<LinearLayout
android:id="@+id/notifications_center_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="10dp"
android:visibility="gone"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/ic_settings_notifications"
app:tint="?colorOnBackground"
tools:ignore="contentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:text="@string/all_no_data"
android:textSize="20sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/notification_center_error"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="invisible"
tools:ignore="UseCompoundDrawables"
tools:visibility="invisible">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/ic_error"
app:tint="?colorOnBackground"
tools:ignore="contentDescription" />
<TextView
android:id="@+id/notification_center_error_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:padding="8dp"
android:text="@string/error_unknown"
android:textSize="20sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/notification_center_error_details"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="@string/all_details" />
<com.google.android.material.button.MaterialButton
android:id="@+id/notification_center_error_retry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/all_retry" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="12dp"
app:cardElevation="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/notifications_center_item_date"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:textColor="?android:textColorSecondary"
android:textSize="14sp"
app:layout_constraintEnd_toStartOf="@id/notifications_center_item_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/date/ddmmyy" />
<TextView
android:id="@+id/notifications_center_item_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="16dp"
android:textSize="15sp"
app:layout_constraintEnd_toStartOf="@id/notifications_center_item_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/notifications_center_item_date"
tools:text="@tools:sample/lorem" />
<TextView
android:id="@+id/notifications_center_item_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:ellipsize="end"
android:maxLines="5"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/notifications_center_item_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/notifications_center_item_title"
tools:text="@tools:sample/lorem/random" />
<ImageView
android:id="@+id/notifications_center_item_icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?colorPrimary"
tools:ignore="ContentDescription"
tools:src="@tools:sample/avatars" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -1,10 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/dashboard_menu_notifaction_list"
android:icon="@drawable/ic_settings_notifications"
android:orderInCategory="1"
android:title="@string/notifications_center_title"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
<item <item
android:id="@+id/dashboard_menu_tiles" android:id="@+id/dashboard_menu_tiles"
android:icon="@drawable/ic_more_settings" android:icon="@drawable/ic_more_settings"
android:orderInCategory="1" android:orderInCategory="2"
android:title="@string/pref_dashboard_appearance_tiles_title" android:title="@string/pref_dashboard_appearance_tiles_title"
app:iconTint="@color/material_on_surface_emphasis_medium" app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />

View File

@ -5,7 +5,7 @@
<item <item
android:id="@+id/mainMenuAccount" android:id="@+id/mainMenuAccount"
android:orderInCategory="2" android:orderInCategory="10"
app:showAsAction="always" app:showAsAction="always"
tools:ignore="MenuTitle" /> tools:ignore="MenuTitle" />
</menu> </menu>

View File

@ -24,6 +24,7 @@
<string name="account_details_title">Account details</string> <string name="account_details_title">Account details</string>
<string name="student_info_title">Student info</string> <string name="student_info_title">Student info</string>
<string name="dashboard_title">Dashboard</string> <string name="dashboard_title">Dashboard</string>
<string name="notifications_center_title">Notifications center</string>
<!--Subtitles--> <!--Subtitles-->

View File

@ -0,0 +1,57 @@
package io.github.wulkanowy.services.messaging
import android.annotation.SuppressLint
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.data.repositories.NotificationRepository
import io.github.wulkanowy.services.sync.notifications.NotificationType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import timber.log.Timber
import java.time.LocalDateTime
import javax.inject.Inject
@SuppressLint("MissingFirebaseInstanceTokenRefresh")
@AndroidEntryPoint
class AppMessagingService : FirebaseMessagingService() {
@Inject
lateinit var notificationRepository: NotificationRepository
private val job = Job()
private val serviceScope = CoroutineScope(Dispatchers.Main + job)
override fun onMessageReceived(remoteMessage: RemoteMessage) {
val remoteMessageData = remoteMessage.data
val title = remoteMessageData["title"] ?: return
val content = remoteMessageData["content"] ?: return
val customData = remoteMessageData["custom_data"]
val notification = Notification(
title = title,
content = content,
data = customData,
date = LocalDateTime.now(),
type = NotificationType.PUSH,
studentId = -1
)
serviceScope.launch {
try {
notificationRepository.saveNotification(notification)
} catch (e: Throwable) {
Timber.e(e)
}
}
}
override fun onDestroy() {
job.cancel()
super.onDestroy()
}
}