1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2024-09-19 23:09:09 -05:00

Add conferences and announcements notifications (#1330)

This commit is contained in:
Damian Czupryn 2021-05-17 15:19:39 +02:00 committed by GitHub
parent 59cf4fb222
commit 983dcd8656
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 2609 additions and 18 deletions

View File

@ -151,7 +151,7 @@ huaweiPublish {
ext {
work_manager = "2.5.0"
work_hilt = "1.0.0"
android_hilt = "1.0.0"
room = "2.3.0"
chucker = "3.4.0"
mockk = "1.11.0"
@ -195,7 +195,8 @@ dependencies {
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
implementation "androidx.hilt:hilt-work:$work_hilt"
kapt "androidx.hilt:hilt-compiler:$android_hilt"
implementation "androidx.hilt:hilt-work:$android_hilt"
implementation "com.ncapdevi:frag-nav:3.3.0"
implementation "com.github.YarikSOffice:lingver:1.3.0"

File diff suppressed because it is too large Load Diff

View File

@ -93,6 +93,7 @@ import io.github.wulkanowy.data.db.migrations.Migration35
import io.github.wulkanowy.data.db.migrations.Migration36
import io.github.wulkanowy.data.db.migrations.Migration37
import io.github.wulkanowy.data.db.migrations.Migration38
import io.github.wulkanowy.data.db.migrations.Migration39
import io.github.wulkanowy.data.db.migrations.Migration4
import io.github.wulkanowy.data.db.migrations.Migration5
import io.github.wulkanowy.data.db.migrations.Migration6
@ -141,7 +142,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 38
const val VERSION_SCHEMA = 39
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(),
@ -181,6 +182,7 @@ abstract class AppDatabase : RoomDatabase() {
Migration36(),
Migration37(),
Migration38(),
Migration39(),
)
fun newInstance(

View File

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

View File

@ -21,4 +21,7 @@ data class SchoolAnnouncement(
@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 Migration39 : Migration(38, 39) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE Conferences ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1")
database.execSQL("ALTER TABLE SchoolAnnouncements ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1")
}
}

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.ConferenceDao
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
@ -10,6 +11,8 @@ import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -25,19 +28,46 @@ class ConferenceRepository @Inject constructor(
private val cacheKey = "conference"
fun getConferences(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource(
fun getConferences(
student: Student,
semester: Semester,
forceRefresh: Boolean,
notify: Boolean = false
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) },
query = { conferenceDb.loadAll(semester.diaryId, student.studentId) },
shouldFetch = {
it.isEmpty() || forceRefresh
|| refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester))
},
query = {
conferenceDb.loadAll(
semester.diaryId,
student.studentId
)
},
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getConferences()
.mapToEntities(semester)
},
saveFetchResult = { old, new ->
val conferencesToSave = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
}
conferenceDb.deleteAll(old uniqueSubtract new)
conferenceDb.insertAll(new uniqueSubtract old)
conferenceDb.insertAll(conferencesToSave)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
}
)
fun getNotNotifiedConference(semester: Semester): Flow<List<Conference>> {
return conferenceDb.loadAll(
diaryId = semester.diaryId,
studentId = semester.studentId
).map {
it.filter { conference -> !conference.isNotified }
}
}
suspend fun updateConference(conference: List<Conference>) = conferenceDb.updateAll(conference)
}

View File

@ -1,6 +1,9 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk
@ -9,6 +12,8 @@ import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -24,20 +29,42 @@ class SchoolAnnouncementRepository @Inject constructor(
private val cacheKey = "school_announcement"
fun getSchoolAnnouncements(student: Student, forceRefresh: Boolean) = networkBoundResource(
fun getSchoolAnnouncements(
student: Student,
forceRefresh: Boolean,
notify: Boolean = false
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(
getRefreshKey(cacheKey, student)
)
it.isEmpty() || forceRefresh
|| refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student))
},
query = {
schoolAnnouncementDb.loadAll(
student.studentId)
},
fetch = {
sdk.init(student)
.getDirectorInformation()
.mapToEntities(student)
},
query = { schoolAnnouncementDb.loadAll(student.studentId) },
fetch = { sdk.init(student).getDirectorInformation().mapToEntities(student) },
saveFetchResult = { old, new ->
schoolAnnouncementDb.deleteAll(old uniqueSubtract new)
schoolAnnouncementDb.insertAll(new uniqueSubtract old)
val schoolAnnouncementsToSave = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
}
schoolAnnouncementDb.deleteAll(old uniqueSubtract new)
schoolAnnouncementDb.insertAll(schoolAnnouncementsToSave)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
}
)
fun getNotNotifiedSchoolAnnouncement(semester: Semester): Flow<List<SchoolAnnouncement>> {
return schoolAnnouncementDb.loadAll(
studentId = semester.studentId
).map {
it.filter { schoolAnnouncement -> !schoolAnnouncement.isNotified }
}
}
suspend fun updateSchoolAnnouncement(schoolAnnouncement: List<SchoolAnnouncement>) = schoolAnnouncementDb.updateAll(schoolAnnouncement)
}

View File

@ -15,16 +15,19 @@ 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.NewConferencesChannel
import io.github.wulkanowy.services.sync.channels.NewExamChannel
import io.github.wulkanowy.services.sync.channels.NewGradesChannel
import io.github.wulkanowy.services.sync.channels.NewHomeworkChannel
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.UpcomingLessonsChannel
import io.github.wulkanowy.services.sync.works.AttendanceSummaryWork
import io.github.wulkanowy.services.sync.works.AttendanceWork
import io.github.wulkanowy.services.sync.works.CompletedLessonWork
import io.github.wulkanowy.services.sync.works.ConferenceWork
import io.github.wulkanowy.services.sync.works.ExamWork
import io.github.wulkanowy.services.sync.works.GradeStatisticsWork
import io.github.wulkanowy.services.sync.works.GradeWork
@ -73,6 +76,10 @@ abstract class ServicesModule {
@IntoSet
abstract fun provideAttendanceWork(work: AttendanceWork): Work
@Binds
@IntoSet
abstract fun provideConferenceWork(work: ConferenceWork): Work
@Binds
@IntoSet
abstract fun provideExamWork(work: ExamWork): Work
@ -125,6 +132,10 @@ abstract class ServicesModule {
@IntoSet
abstract fun provideLuckyNumberChannel(channel: LuckyNumberChannel): Channel
@Binds
@IntoSet
abstract fun provideNewConferenceChannel(channel: NewConferencesChannel): Channel
@Binds
@IntoSet
abstract fun provideNewExamChannel(channel: NewExamChannel): Channel
@ -145,6 +156,10 @@ abstract class ServicesModule {
@IntoSet
abstract fun provideNewNotesChannel(channel: NewNotesChannel): Channel
@Binds
@IntoSet
abstract fun provideNewSchoolAnnouncementChannel(channel: NewSchoolAnnouncementsChannel): Channel
@Binds
@IntoSet
abstract fun providePushChannel(channel: PushChannel): Channel

View File

@ -0,0 +1,32 @@
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 NewConferencesChannel @Inject constructor(
private val notificationManager: NotificationManagerCompat,
@ApplicationContext private val context: Context
) : Channel {
companion object {
const val CHANNEL_ID = "new_conferences_channel"
}
override fun create() {
notificationManager.createNotificationChannel(
NotificationChannel(CHANNEL_ID, context.getString(R.string.channel_new_conference), NotificationManager.IMPORTANCE_HIGH)
.apply {
enableLights(true)
enableVibration(true)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
})
}
}

View File

@ -0,0 +1,32 @@
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 NewSchoolAnnouncementsChannel @Inject constructor(
private val notificationManager: NotificationManagerCompat,
@ApplicationContext private val context: Context
) : Channel {
companion object {
const val CHANNEL_ID = "new_schoolAnnouncements_channel"
}
override fun create() {
notificationManager.createNotificationChannel(
NotificationChannel(CHANNEL_ID, context.getString(R.string.channel_new_school_announcement), NotificationManager.IMPORTANCE_HIGH)
.apply {
enableLights(true)
enableVibration(true)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
})
}
}

View File

@ -0,0 +1,79 @@
package io.github.wulkanowy.services.sync.works
import android.app.PendingIntent
import android.content.Context
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.Conference
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.ConferenceRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.services.sync.channels.NewConferencesChannel
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.getCompatBitmap
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.waitForResult
import kotlinx.coroutines.flow.first
import javax.inject.Inject
import kotlin.random.Random
class ConferenceWork @Inject constructor(
@ApplicationContext private val context: Context,
private val conferenceRepository: ConferenceRepository,
private val notificationManager: NotificationManagerCompat,
private val preferencesRepository: PreferencesRepository
) : Work {
override suspend fun doWork(student: Student, semester: Semester) {
conferenceRepository.getConferences(
student = student,
semester = semester,
forceRefresh = true,
notify = preferencesRepository.isNotificationsEnable
).waitForResult()
conferenceRepository.getNotNotifiedConference(semester).first().let {
if (it.isNotEmpty()) notify(it)
conferenceRepository.updateConference(it.onEach { conference -> conference.isNotified = true })
}
}
private fun notify(conference: List<Conference>) {
notificationManager.notify(
Random.nextInt(Int.MAX_VALUE),
NotificationCompat.Builder(context, NewConferencesChannel.CHANNEL_ID)
.setContentTitle(context.resources.getQuantityString(R.plurals.conference_notify_new_item_title, conference.size, conference.size))
.setContentText(context.resources.getQuantityString(R.plurals.conference_notify_new_items, conference.size, conference.size))
.setSmallIcon(R.drawable.ic_stat_all)
.setLargeIcon(context.getCompatBitmap(R.drawable.ic_more_conferences, R.color.colorPrimary))
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setContentIntent(
PendingIntent.getActivity(
context, MainView.Section.CONFERENCE.id,
MainActivity.getStartIntent(context, MainView.Section.CONFERENCE, true),
PendingIntent.FLAG_UPDATE_CURRENT
)
)
.setStyle(NotificationCompat.InboxStyle().run {
setSummaryText(
context.resources.getQuantityString(
R.plurals.conference_number_item,
conference.size,
conference.size
)
)
conference.forEach { addLine("${it.title}: ${it.subject}") }
this
})
.build()
)
}
}

View File

@ -1,16 +1,83 @@
package io.github.wulkanowy.services.sync.works
import android.app.PendingIntent
import android.content.Context
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.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.SchoolAnnouncementRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.getCompatBitmap
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.waitForResult
import kotlinx.coroutines.flow.first
import javax.inject.Inject
import kotlin.random.Random
class SchoolAnnouncementWork @Inject constructor(
@ApplicationContext private val context: Context,
private val schoolAnnouncementRepository: SchoolAnnouncementRepository,
private val notificationManager: NotificationManagerCompat,
private val preferencesRepository: PreferencesRepository
) : Work {
override suspend fun doWork(student: Student, semester: Semester) {
schoolAnnouncementRepository.getSchoolAnnouncements(student, true).waitForResult()
schoolAnnouncementRepository.getSchoolAnnouncements(
student,
true,
notify = preferencesRepository.isNotificationsEnable
).waitForResult()
schoolAnnouncementRepository.getNotNotifiedSchoolAnnouncement(semester).first().let {
if (it.isNotEmpty()) notify(it)
schoolAnnouncementRepository.updateSchoolAnnouncement(it.onEach { schoolAnnouncement -> schoolAnnouncement.isNotified = true })
}
}
private fun notify(schoolAnnouncement: List<SchoolAnnouncement>) {
notificationManager.notify(
Random.nextInt(Int.MAX_VALUE),
NotificationCompat.Builder(context, NewSchoolAnnouncementsChannel.CHANNEL_ID)
.setContentTitle(context.resources.getQuantityString(
R.plurals.school_announcement_notify_new_item_title,
schoolAnnouncement.size,
schoolAnnouncement.size
))
.setContentText(context.resources.getQuantityString(R.plurals.school_announcement_notify_new_items, schoolAnnouncement.size, schoolAnnouncement.size))
.setSmallIcon(R.drawable.ic_stat_all)
.setLargeIcon(context.getCompatBitmap(R.drawable.ic_all_about, R.color.colorPrimary))
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setContentIntent(
PendingIntent.getActivity(
context, MainView.Section.SCHOOL_ANNOUNCEMENT.id,
MainActivity.getStartIntent(context, MainView.Section.SCHOOL_ANNOUNCEMENT, true),
PendingIntent.FLAG_UPDATE_CURRENT
)
)
.setStyle(NotificationCompat.InboxStyle().run {
setSummaryText(
context.resources.getQuantityString(
R.plurals.school_announcement_number_item,
schoolAnnouncement.size,
schoolAnnouncement.size
)
)
schoolAnnouncement.forEach { addLine("${it.subject}: ${it.content}") }
this
})
.build()
)
}
}

View File

@ -70,6 +70,8 @@ interface MainView : BaseView {
ABOUT(10),
SCHOOL(11),
ACCOUNT(12),
STUDENT_INFO(13)
STUDENT_INFO(13),
CONFERENCE(14),
SCHOOL_ANNOUNCEMENT(15)
}
}

View File

@ -4,6 +4,6 @@
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:fillColor="#FFF"
android:pathData="M9,13.75c-2.34,0 -7,1.17 -7,3.5L2,19h14v-1.75c0,-2.33 -4.66,-3.5 -7,-3.5zM4.34,17c0.84,-0.58 2.87,-1.25 4.66,-1.25s3.82,0.67 4.66,1.25L4.34,17zM9,12c1.93,0 3.5,-1.57 3.5,-3.5S10.93,5 9,5 5.5,6.57 5.5,8.5 7.07,12 9,12zM9,7c0.83,0 1.5,0.67 1.5,1.5S9.83,10 9,10s-1.5,-0.67 -1.5,-1.5S8.17,7 9,7zM16.04,13.81c1.16,0.84 1.96,1.96 1.96,3.44L18,19h4v-1.75c0,-2.02 -3.5,-3.17 -5.96,-3.44zM15,12c1.93,0 3.5,-1.57 3.5,-3.5S16.93,5 15,5c-0.54,0 -1.04,0.13 -1.5,0.35 0.63,0.89 1,1.98 1,3.15s-0.37,2.26 -1,3.15c0.46,0.22 0.96,0.35 1.5,0.35z" />
</vector>

View File

@ -353,11 +353,35 @@
<!--Conference-->
<string name="conferences_title">Conferences</string>
<string name="conference_no_items">No info about conferences</string>
<plurals name="conference_number_item">
<item quantity="one">%d conference</item>
<item quantity="other">%d conferences</item>
</plurals>
<plurals name="conference_notify_new_item_title">
<item quantity="one">New conference</item>
<item quantity="other">New conferences</item>
</plurals>
<plurals name="conference_notify_new_items">
<item quantity="one">You have %1$d new conference</item>
<item quantity="other">You have %1$d new conferences</item>
</plurals>
<!--Director information-->
<string name="school_announcement_title">School announcements</string>
<string name="school_announcement_no_items">No school announcements</string>
<plurals name="school_announcement_number_item">
<item quantity="one">%d school announcement</item>
<item quantity="other">%d school announcements</item>
</plurals>
<plurals name="school_announcement_notify_new_item_title">
<item quantity="one">New school announcement</item>
<item quantity="other">New school announcements</item>
</plurals>
<plurals name="school_announcement_notify_new_items">
<item quantity="one">You have %1$d new school announcement</item>
<item quantity="other">You have %1$d new school announcements</item>
</plurals>
<!--Account-->
@ -545,10 +569,12 @@
<!--Notification Channels-->
<string name="channel_new_grades">New grades</string>
<string name="channel_new_homework">New homework</string>
<string name="channel_new_conference">New conferences</string>
<string name="channel_new_exam">New exams</string>
<string name="channel_lucky_number">Lucky number</string>
<string name="channel_new_message">New messages</string>
<string name="channel_new_notes">New notes</string>
<string name="channel_new_school_announcement">New school announcements</string>
<string name="channel_push">Push notifications</string>
<string name="channel_upcoming_lessons">Upcoming lessons</string>
<string name="channel_debug">Debug</string>