1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2024-09-20 02:29:08 -05:00

Add new notifications (#1243)

This commit is contained in:
Tomasz F 2021-04-04 16:15:07 +02:00 committed by GitHub
parent 6cb4ea4b0f
commit 7bc5219d81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 2504 additions and 21 deletions

File diff suppressed because it is too large Load Diff

View File

@ -46,5 +46,9 @@
{ {
"displayName": "Kamil Studziński", "displayName": "Kamil Studziński",
"githubUsername": "studzinskik" "githubUsername": "studzinskik"
},
{
"displayName": "Tomasz F.",
"githubUsername": "Pengwius"
} }
] ]

View File

@ -86,6 +86,7 @@ import io.github.wulkanowy.data.db.migrations.Migration32
import io.github.wulkanowy.data.db.migrations.Migration33 import io.github.wulkanowy.data.db.migrations.Migration33
import io.github.wulkanowy.data.db.migrations.Migration34 import io.github.wulkanowy.data.db.migrations.Migration34
import io.github.wulkanowy.data.db.migrations.Migration35 import io.github.wulkanowy.data.db.migrations.Migration35
import io.github.wulkanowy.data.db.migrations.Migration36
import io.github.wulkanowy.data.db.migrations.Migration4 import io.github.wulkanowy.data.db.migrations.Migration4
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
@ -132,7 +133,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val VERSION_SCHEMA = 35 const val VERSION_SCHEMA = 36
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(), Migration2(),
@ -168,7 +169,8 @@ abstract class AppDatabase : RoomDatabase() {
Migration32(), Migration32(),
Migration33(), Migration33(),
Migration34(), Migration34(),
Migration35(appInfo) Migration35(appInfo),
Migration36()
) )
fun newInstance( fun newInstance(

View File

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

View File

@ -37,4 +37,7 @@ data class Homework(
@ColumnInfo(name = "is_done") @ColumnInfo(name = "is_done")
var isDone: Boolean = false var isDone: Boolean = false
@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 Migration36 : Migration(35, 36) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE Exams ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1")
database.execSQL("ALTER TABLE Homework ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1")
}
}

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.data.repositories package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.ExamDao import io.github.wulkanowy.data.db.dao.ExamDao
import io.github.wulkanowy.data.db.entities.Exam
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.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
@ -12,6 +13,8 @@ import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.startExamsDay import io.github.wulkanowy.utils.startExamsDay
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
@ -28,20 +31,54 @@ class ExamRepository @Inject constructor(
private val cacheKey = "exam" private val cacheKey = "exam"
fun getExams(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( fun getExams(
student: Student,
semester: Semester,
start: LocalDate,
end: LocalDate,
forceRefresh: Boolean,
notify: Boolean = false
) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, shouldFetch = {
query = { examDb.loadAll(semester.diaryId, semester.studentId, start.startExamsDay, start.endExamsDay) }, it.isEmpty() || forceRefresh
|| refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end))
},
query = {
examDb.loadAll(
diaryId = semester.diaryId,
studentId = semester.studentId,
from = start.startExamsDay,
end = start.endExamsDay
)
},
fetch = { fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getExams(start.startExamsDay, start.endExamsDay, semester.semesterId) .getExams(start.startExamsDay, start.endExamsDay, semester.semesterId)
.mapToEntities(semester) .mapToEntities(semester)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
val examsToSave = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
}
examDb.deleteAll(old uniqueSubtract new) examDb.deleteAll(old uniqueSubtract new)
examDb.insertAll(new uniqueSubtract old) examDb.insertAll(examsToSave)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
}, },
filterResult = { it.filter { item -> item.date in start..end } } filterResult = { it.filter { item -> item.date in start..end } }
) )
fun getNotNotifiedExam(semester: Semester, start: LocalDate): Flow<List<Exam>> {
return examDb.loadAll(
diaryId = semester.diaryId,
studentId = semester.studentId,
from = start.startExamsDay,
end = start.endExamsDay
).map {
it.filter { exam -> !exam.isNotified }
}
}
suspend fun updateExam(exam: List<Exam>) = examDb.updateAll(exam)
} }

View File

@ -13,6 +13,7 @@ import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
@ -29,18 +30,39 @@ class HomeworkRepository @Inject constructor(
private val cacheKey = "homework" private val cacheKey = "homework"
fun getHomework(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( fun getHomework(
student: Student,
semester: Semester,
start: LocalDate,
end: LocalDate,
forceRefresh: Boolean,
notify: Boolean = false
) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, shouldFetch = {
query = { homeworkDb.loadAll(semester.semesterId, semester.studentId, start.monday, end.sunday) }, it.isEmpty() || forceRefresh ||
refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end))
},
query = {
homeworkDb.loadAll(
semesterId = semester.semesterId,
studentId = semester.studentId,
from = start.monday,
end = end.sunday
)
},
fetch = { fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getHomework(start.monday, end.sunday) .getHomework(start.monday, end.sunday)
.mapToEntities(semester) .mapToEntities(semester)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
val homeWorkToSave = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
}
homeworkDb.deleteAll(old uniqueSubtract new) homeworkDb.deleteAll(old uniqueSubtract new)
homeworkDb.insertAll(new uniqueSubtract old) homeworkDb.insertAll(homeWorkToSave)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
} }
@ -51,4 +73,15 @@ class HomeworkRepository @Inject constructor(
isDone = !isDone isDone = !isDone
})) }))
} }
fun getNotNotifiedHomework(
semester: Semester,
start: LocalDate,
end: LocalDate
) = homeworkDb.loadAll(semester.semesterId, semester.studentId, start.monday, end.sunday)
.map {
it.filter { homework -> !homework.isNotified }
}
suspend fun updateHomework(homework: List<Homework>) = homeworkDb.updateAll(homework)
} }

View File

@ -15,7 +15,9 @@ import dagger.multibindings.IntoSet
import io.github.wulkanowy.services.sync.channels.Channel import io.github.wulkanowy.services.sync.channels.Channel
import io.github.wulkanowy.services.sync.channels.DebugChannel import io.github.wulkanowy.services.sync.channels.DebugChannel
import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel
import io.github.wulkanowy.services.sync.channels.NewExamChannel
import io.github.wulkanowy.services.sync.channels.NewGradesChannel 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.NewMessagesChannel
import io.github.wulkanowy.services.sync.channels.NewNotesChannel import io.github.wulkanowy.services.sync.channels.NewNotesChannel
import io.github.wulkanowy.services.sync.channels.PushChannel import io.github.wulkanowy.services.sync.channels.PushChannel
@ -115,6 +117,14 @@ abstract class ServicesModule {
@IntoSet @IntoSet
abstract fun provideLuckyNumberChannel(channel: LuckyNumberChannel): Channel abstract fun provideLuckyNumberChannel(channel: LuckyNumberChannel): Channel
@Binds
@IntoSet
abstract fun provideNewExamChannel(channel: NewExamChannel): Channel
@Binds
@IntoSet
abstract fun provideNewHomeworkChannel(channel: NewHomeworkChannel): Channel
@Binds @Binds
@IntoSet @IntoSet
abstract fun provideNewGradesChannel(channel: NewGradesChannel): Channel abstract fun provideNewGradesChannel(channel: NewGradesChannel): 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 NewExamChannel @Inject constructor(
private val notificationManager: NotificationManagerCompat,
@ApplicationContext private val context: Context
) : Channel {
companion object {
const val CHANNEL_ID = "new_exam_channel"
}
override fun create() {
notificationManager.createNotificationChannel(
NotificationChannel(CHANNEL_ID, context.getString(R.string.channel_new_exam), 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 NewHomeworkChannel @Inject constructor(
private val notificationManager: NotificationManagerCompat,
@ApplicationContext private val context: Context
) : Channel {
companion object {
const val CHANNEL_ID = "new_homework_channel"
}
override fun create() {
notificationManager.createNotificationChannel(
NotificationChannel(CHANNEL_ID, context.getString(R.string.channel_new_homework), NotificationManager.IMPORTANCE_HIGH)
.apply {
enableLights(true)
enableVibration(true)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
})
}
}

View File

@ -1,15 +1,85 @@
package io.github.wulkanowy.services.sync.works 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.Exam
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.ExamRepository import io.github.wulkanowy.data.repositories.ExamRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.services.sync.channels.NewExamChannel
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.waitForResult import io.github.wulkanowy.utils.waitForResult
import kotlinx.coroutines.flow.first
import java.time.LocalDate.now import java.time.LocalDate.now
import javax.inject.Inject import javax.inject.Inject
import kotlin.random.Random
class ExamWork @Inject constructor(private val examRepository: ExamRepository) : Work { class ExamWork @Inject constructor(
@ApplicationContext private val context: Context,
private val examRepository: ExamRepository,
private val notificationManager: NotificationManagerCompat,
private val preferencesRepository: PreferencesRepository
) : Work {
override suspend fun doWork(student: Student, semester: Semester) { override suspend fun doWork(student: Student, semester: Semester) {
examRepository.getExams(student, semester, now(), now(), true).waitForResult() examRepository.getExams(
student = student,
semester = semester,
start = now(),
end = now(),
forceRefresh = true,
notify = preferencesRepository.isNotificationsEnable
).waitForResult()
examRepository.getNotNotifiedExam(semester, now()).first().let {
if (it.isNotEmpty()) notify(it)
examRepository.updateExam(it.onEach { exam -> exam.isNotified = true })
}
}
private fun notify(exam: List<Exam>) {
notificationManager.notify(
Random.nextInt(Int.MAX_VALUE),
NotificationCompat.Builder(context, NewExamChannel.CHANNEL_ID)
.setContentTitle(
context.resources.getQuantityString(
R.plurals.exam_notify_new_item_title,
exam.size,
exam.size
)
)
.setSmallIcon(R.drawable.ic_main_exam)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setContentIntent(
PendingIntent.getActivity(
context, MainView.Section.MESSAGE.id,
MainActivity.getStartIntent(context, MainView.Section.EXAM, true),
PendingIntent.FLAG_UPDATE_CURRENT
)
)
.setStyle(NotificationCompat.InboxStyle().run {
setSummaryText(
context.resources.getQuantityString(
R.plurals.exam_number_item,
exam.size,
exam.size
)
)
exam.forEach { addLine("${it.subject}: ${it.description}") }
this
})
.build()
)
} }
} }

View File

@ -1,17 +1,90 @@
package io.github.wulkanowy.services.sync.works 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.Homework
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.HomeworkRepository import io.github.wulkanowy.data.repositories.HomeworkRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.services.sync.channels.NewHomeworkChannel
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.waitForResult import io.github.wulkanowy.utils.waitForResult
import kotlinx.coroutines.flow.first
import java.time.LocalDate.now import java.time.LocalDate.now
import javax.inject.Inject import javax.inject.Inject
import kotlin.random.Random
class HomeworkWork @Inject constructor(private val homeworkRepository: HomeworkRepository) : Work { class HomeworkWork @Inject constructor(
@ApplicationContext private val context: Context,
private val homeworkRepository: HomeworkRepository,
private val notificationManager: NotificationManagerCompat,
private val preferencesRepository: PreferencesRepository
) : Work {
override suspend fun doWork(student: Student, semester: Semester) { override suspend fun doWork(student: Student, semester: Semester) {
homeworkRepository.getHomework(student, semester, now().monday, now().sunday, true).waitForResult() homeworkRepository.getHomework(
student = student,
semester = semester,
start = now().monday,
end = now().sunday,
forceRefresh = true,
notify = preferencesRepository.isNotificationsEnable
).waitForResult()
homeworkRepository.getNotNotifiedHomework(semester, now().monday, now().sunday).first()
.let {
if (it.isNotEmpty()) notify(it)
homeworkRepository.updateHomework(it.onEach { homework ->
homework.isNotified = true
})
}
}
private fun notify(homework: List<Homework>) {
notificationManager.notify(
Random.nextInt(Int.MAX_VALUE),
NotificationCompat.Builder(context, NewHomeworkChannel.CHANNEL_ID)
.setContentTitle(
context.resources.getQuantityString(
R.plurals.homework_notify_new_item_title,
homework.size,
homework.size
)
)
.setSmallIcon(R.drawable.ic_more_homework)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setContentIntent(
PendingIntent.getActivity(
context, MainView.Section.MESSAGE.id,
MainActivity.getStartIntent(context, MainView.Section.HOMEWORK, true),
PendingIntent.FLAG_UPDATE_CURRENT
)
)
.setStyle(NotificationCompat.InboxStyle().run {
setSummaryText(
context.resources.getQuantityString(
R.plurals.homework_number_item,
homework.size,
homework.size
)
)
homework.forEach { addLine("${it.subject}: ${it.content}") }
this
})
.build()
)
} }
} }

View File

@ -198,6 +198,14 @@
<string name="exam_no_items">No exams this week</string> <string name="exam_no_items">No exams this week</string>
<string name="exam_type">Type</string> <string name="exam_type">Type</string>
<string name="exam_entry_date">Entry date</string> <string name="exam_entry_date">Entry date</string>
<plurals name="exam_notify_new_item_title">
<item quantity="one">New exam</item>
<item quantity="other">New exams</item>
</plurals>
<plurals name="exam_number_item">
<item quantity="one">%d exam</item>
<item quantity="other">%d exams</item>
</plurals>
<!--Message--> <!--Message-->
@ -287,6 +295,14 @@
<string name="homework_mark_as_done">Mark as done</string> <string name="homework_mark_as_done">Mark as done</string>
<string name="homework_mark_as_undone">Mark as undone</string> <string name="homework_mark_as_undone">Mark as undone</string>
<string name="homework_attachments">Attachments</string> <string name="homework_attachments">Attachments</string>
<plurals name="homework_notify_new_item_title">
<item quantity="one">New homework</item>
<item quantity="other">New homework</item>
</plurals>
<plurals name="homework_number_item">
<item quantity="one">%d homework</item>
<item quantity="other">%d homework</item>
</plurals>
<!--Lucky number--> <!--Lucky number-->
@ -519,6 +535,8 @@
<!--Notification Channels--> <!--Notification Channels-->
<string name="channel_new_grades">New grades</string> <string name="channel_new_grades">New grades</string>
<string name="channel_new_homework">New homework</string>
<string name="channel_new_exam">New exams</string>
<string name="channel_lucky_number">Lucky number</string> <string name="channel_lucky_number">Lucky number</string>
<string name="channel_new_message">New messages</string> <string name="channel_new_message">New messages</string>
<string name="channel_new_notes">New notes</string> <string name="channel_new_notes">New notes</string>
@ -527,12 +545,6 @@
<string name="channel_debug">Debug</string> <string name="channel_debug">Debug</string>
<!--Drop kitkat alert dialog strings-->
<string name="drop_kitkat_title">End of support</string>
<string name="drop_kitkat_content">We are ending support for your device. No more new features will appear for it in Wulkanowy. However, we will be releasing critical patches until the end of 2021 so you have time to switch to a newer model</string>
<string name="drop_kitkat_again">Don\'t show again</string>
<!--Colors--> <!--Colors-->
<string name="all_black">Black</string> <string name="all_black">Black</string>
<string name="all_red">Red</string> <string name="all_red">Red</string>