Add lucky numbers (#216)

This commit is contained in:
Kacper Ziubryniewicz
2019-01-25 20:54:27 +01:00
committed by Rafał Borcz
parent d3c13b8fc3
commit 4da812af39
29 changed files with 658 additions and 15 deletions

View File

@ -113,4 +113,8 @@ internal class RepositoryModule {
@Singleton
@Provides
fun provideSubjectDao(database: AppDatabase) = database.subjectDao
@Singleton
@Provides
fun provideLuckyNumberDao(database: AppDatabase) = database.luckyNumberDao
}

View File

@ -6,6 +6,8 @@ import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.RoomDatabase.JournalMode.TRUNCATE
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import io.github.wulkanowy.data.db.dao.AttendanceDao
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
import io.github.wulkanowy.data.db.dao.ExamDao
@ -13,6 +15,7 @@ import io.github.wulkanowy.data.db.dao.GradeDao
import io.github.wulkanowy.data.db.dao.GradeSummaryDao
import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.dao.HomeworkDao
import io.github.wulkanowy.data.db.dao.LuckyNumberDao
import io.github.wulkanowy.data.db.dao.NoteDao
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
@ -25,11 +28,13 @@ import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Subject
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.migrations.Migration2
import javax.inject.Singleton
@Singleton
@ -46,9 +51,10 @@ import javax.inject.Singleton
Message::class,
Note::class,
Homework::class,
Subject::class
Subject::class,
LuckyNumber::class
],
version = 1,
version = 2,
exportSchema = false
)
@TypeConverters(Converters::class)
@ -58,6 +64,9 @@ abstract class AppDatabase : RoomDatabase() {
fun newInstance(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database")
.setJournalMode(TRUNCATE)
.addMigrations(
Migration2()
)
.build()
}
}
@ -85,4 +94,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract val homeworkDao: HomeworkDao
abstract val subjectDao: SubjectDao
abstract val luckyNumberDao: LuckyNumberDao
}

View File

@ -0,0 +1,29 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.reactivex.Maybe
import org.threeten.bp.LocalDate
import javax.inject.Singleton
@Singleton
@Dao
interface LuckyNumberDao {
@Insert
fun insert(luckyNumber: LuckyNumber)
@Update
fun update(luckyNumber: LuckyNumber)
@Delete
fun delete(luckyNumber: LuckyNumber)
@Query("SELECT * FROM LuckyNumbers WHERE student_id = :studentId AND date = :date")
fun loadFromDate(studentId: Int, date: LocalDate): Maybe<LuckyNumber>
}

View File

@ -0,0 +1,28 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import org.threeten.bp.LocalDate
import java.io.Serializable
@Entity(tableName = "LuckyNumbers")
data class LuckyNumber (
@ColumnInfo(name = "student_id")
var studentId: Int,
var date: LocalDate,
@ColumnInfo(name = "lucky_number")
var luckyNumber: Int
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
@ColumnInfo(name = "is_notified")
var isNotified: Boolean = true
}

View File

@ -0,0 +1,16 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration2 : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE LuckyNumbers (" +
"id INTEGER NOT NULL PRIMARY KEY, " +
"is_notified INTEGER NOT NULL, " +
"student_id INTEGER NOT NULL, " +
"date INTEGER NOT NULL, " +
"lucky_number INTEGER NOT NULL)")
}
}

View File

@ -0,0 +1,52 @@
package io.github.wulkanowy.data.repositories
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.repositories.local.LuckyNumberLocal
import io.github.wulkanowy.data.repositories.remote.LuckyNumberRemote
import io.reactivex.Completable
import io.reactivex.Maybe
import org.threeten.bp.LocalDate
import java.net.UnknownHostException
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LuckyNumberRepository @Inject constructor(
private val settings: InternetObservingSettings,
private val local: LuckyNumberLocal,
private val remote: LuckyNumberRemote
) {
fun getLuckyNumber(semester: Semester, forceRefresh: Boolean = false, notify: Boolean = false): Maybe<LuckyNumber> {
return local.getLuckyNumber(semester, LocalDate.now()).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMapMaybe {
if (it) remote.getLuckyNumber(semester)
else Maybe.error(UnknownHostException())
}.flatMap { new ->
local.getLuckyNumber(semester, LocalDate.now())
.doOnSuccess { old ->
if (new != old) {
local.deleteLuckyNumber(old)
local.saveLuckyNumber(new.apply {
if (notify) isNotified = false
})
}
}
.doOnComplete {
local.saveLuckyNumber(new.apply {
if (notify) isNotified = false
})
}
}.flatMap({ local.getLuckyNumber(semester, LocalDate.now()) }, { Maybe.error(it) },
{ local.getLuckyNumber(semester, LocalDate.now()) })
)
}
fun updateLuckyNumber(luckyNumber: LuckyNumber): Completable {
return local.updateLuckyNumber(luckyNumber)
}
}

View File

@ -0,0 +1,30 @@
package io.github.wulkanowy.data.repositories.local
import io.github.wulkanowy.data.db.dao.LuckyNumberDao
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Semester
import io.reactivex.Completable
import io.reactivex.Maybe
import org.threeten.bp.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LuckyNumberLocal @Inject constructor(private val luckyNumberDb: LuckyNumberDao) {
fun getLuckyNumber(semester: Semester, date: LocalDate): Maybe<LuckyNumber> {
return luckyNumberDb.loadFromDate(semester.studentId, date)
}
fun saveLuckyNumber(luckyNumber: LuckyNumber) {
luckyNumberDb.insert(luckyNumber)
}
fun updateLuckyNumber(luckyNumber: LuckyNumber): Completable {
return Completable.fromCallable { luckyNumberDb.update(luckyNumber) }
}
fun deleteLuckyNumber(luckyNumber: LuckyNumber) {
luckyNumberDb.delete(luckyNumber)
}
}

View File

@ -0,0 +1,26 @@
package io.github.wulkanowy.data.repositories.remote
import io.github.wulkanowy.api.Api
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Semester
import io.reactivex.Maybe
import io.reactivex.Single
import org.threeten.bp.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LuckyNumberRemote @Inject constructor(private val api: Api) {
fun getLuckyNumber(semester: Semester): Maybe<LuckyNumber> {
return Single.just(api.apply { diaryId = semester.diaryId })
.flatMapMaybe { it.getLuckyNumber() }
.map {
LuckyNumber(
studentId = semester.studentId,
date = LocalDate.now(),
luckyNumber = it
)
}
}
}

View File

@ -8,6 +8,7 @@ import io.github.wulkanowy.data.repositories.ExamRepository
import io.github.wulkanowy.data.repositories.GradeRepository
import io.github.wulkanowy.data.repositories.GradeSummaryRepository
import io.github.wulkanowy.data.repositories.HomeworkRepository
import io.github.wulkanowy.data.repositories.LuckyNumberRepository
import io.github.wulkanowy.data.repositories.MessagesRepository
import io.github.wulkanowy.data.repositories.MessagesRepository.MessageFolder.RECEIVED
import io.github.wulkanowy.data.repositories.NoteRepository
@ -16,12 +17,13 @@ import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.services.notification.GradeNotification
import io.github.wulkanowy.services.notification.LuckyNumberNotification
import io.github.wulkanowy.services.notification.MessageNotification
import io.github.wulkanowy.services.notification.NoteNotification
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.monday
import io.reactivex.Single
import io.reactivex.Completable
import io.reactivex.disposables.CompositeDisposable
import org.threeten.bp.LocalDate
import timber.log.Timber
@ -59,6 +61,9 @@ class SyncWorker : SimpleJobService() {
@Inject
lateinit var homework: HomeworkRepository
@Inject
lateinit var luckyNumber: LuckyNumberRepository
@Inject
lateinit var prefRepository: PreferencesRepository
@ -88,18 +93,19 @@ class SyncWorker : SimpleJobService() {
disposable.add(student.getCurrentStudent()
.flatMap { semester.getCurrentSemester(it, true).map { semester -> semester to it } }
.flatMapPublisher {
Single.merge(
.flatMapCompletable {
Completable.merge(
listOf(
gradesDetails.getGrades(it.first, true, notify),
gradesSummary.getGradesSummary(it.first, true),
attendance.getAttendance(it.first, start, end, true),
exam.getExams(it.first, start, end, true),
timetable.getTimetable(it.first, start, end, true),
message.getMessages(it.second, RECEIVED, true, notify),
note.getNotes(it.first, true, notify),
homework.getHomework(it.first, LocalDate.now(), true),
homework.getHomework(it.first, LocalDate.now().plusDays(1), true)
gradesDetails.getGrades(it.first, true, notify).ignoreElement(),
gradesSummary.getGradesSummary(it.first, true).ignoreElement(),
attendance.getAttendance(it.first, start, end, true).ignoreElement(),
exam.getExams(it.first, start, end, true).ignoreElement(),
timetable.getTimetable(it.first, start, end, true).ignoreElement(),
message.getMessages(it.second, RECEIVED, true, notify).ignoreElement(),
note.getNotes(it.first, true, notify).ignoreElement(),
homework.getHomework(it.first, LocalDate.now(), true).ignoreElement(),
homework.getHomework(it.first, LocalDate.now().plusDays(1), true).ignoreElement(),
luckyNumber.getLuckyNumber(it.first, true, notify).ignoreElement()
)
)
}
@ -119,6 +125,7 @@ class SyncWorker : SimpleJobService() {
sendGradeNotifications()
sendMessageNotification()
sendNoteNotification()
sendLuckyNumberNotification()
}
private fun sendGradeNotifications() {
@ -170,6 +177,19 @@ class SyncWorker : SimpleJobService() {
)
}
private fun sendLuckyNumberNotification() {
disposable.add(student.getCurrentStudent()
.flatMap { semester.getCurrentSemester(it) }
.flatMapMaybe { luckyNumber.getLuckyNumber(it) }
.filter { !it.isNotified }
.doOnSuccess {
LuckyNumberNotification(applicationContext).sendNotification(it)
}
.map { it.apply { isNotified = true } }
.flatMapCompletable { luckyNumber.updateLuckyNumber(it) }
.subscribe({}, { Timber.e("Lucky number notification sending failed") }))
}
override fun onDestroy() {
super.onDestroy()
disposable.clear()

View File

@ -0,0 +1,48 @@
package io.github.wulkanowy.services.notification
import android.annotation.TargetApi
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.ui.modules.main.MainActivity
class LuckyNumberNotification(context: Context) : BaseNotification(context) {
private val channelId = "Lucky_Number_Notify"
@TargetApi(26)
override fun createChannel(channelId: String) {
notificationManager.createNotificationChannel(NotificationChannel(
channelId, context.getString(R.string.notify_lucky_number_channel), NotificationManager.IMPORTANCE_HIGH
).apply {
enableLights(true)
enableVibration(true)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
})
}
fun sendNotification(luckyNumber: LuckyNumber) {
notify(notificationBuilder(channelId)
.setContentTitle(context.getString(R.string.notify_lucky_number_new_item_title))
.setContentText(context.getString(R.string.notify_lucky_number_new_item, luckyNumber.luckyNumber))
.setSmallIcon(R.drawable.ic_stat_notify_lucky_number)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColor(ContextCompat.getColor(context, R.color.colorPrimary))
.setContentIntent(
PendingIntent.getActivity(context, 0,
MainActivity.getStartIntent(context).putExtra(MainActivity.EXTRA_START_MENU_INDEX, 4),
PendingIntent.FLAG_UPDATE_CURRENT
)
)
.build()
)
}
}

View File

@ -0,0 +1,64 @@
package io.github.wulkanowy.ui.modules.luckynumber
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.ui.base.session.BaseSessionFragment
import io.github.wulkanowy.ui.modules.main.MainView
import kotlinx.android.synthetic.main.fragment_lucky_number.*
import javax.inject.Inject
class LuckyNumberFragment : BaseSessionFragment(), LuckyNumberView, MainView.TitledView {
@Inject
lateinit var presenter: LuckyNumberPresenter
companion object {
fun newInstance() = LuckyNumberFragment()
}
override val titleStringId: Int
get() = R.string.lucky_number_title
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_lucky_number, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.onAttachView(this)
}
override fun initView() {
showContent(false)
showProgress(true)
luckyNumberSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
}
override fun updateData(data: LuckyNumber) {
luckyNumberText.text = data.luckyNumber.toString()
}
override fun hideRefresh() {
luckyNumberSwipe.isRefreshing = false
}
override fun showEmpty(show: Boolean) {
luckyNumberEmpty.visibility = if (show) View.VISIBLE else View.GONE
}
override fun showProgress(show: Boolean) {
luckyNumberProgress.visibility = if (show) View.VISIBLE else View.GONE
}
override fun showContent(show: Boolean) {
luckyNumberContent.visibility = if (show) View.VISIBLE else View.GONE
}
override fun isViewEmpty(): Boolean {
return luckyNumberText.text.isBlank()
}
}

View File

@ -0,0 +1,65 @@
package io.github.wulkanowy.ui.modules.luckynumber
import io.github.wulkanowy.data.repositories.LuckyNumberRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.session.SessionErrorHandler
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.reactivex.MaybeSource
import javax.inject.Inject
class LuckyNumberPresenter @Inject constructor(
private val errorHandler: SessionErrorHandler,
private val schedulers: SchedulersProvider,
private val luckyNumberRepository: LuckyNumberRepository,
private val studentRepository: StudentRepository,
private val semesterRepository: SemesterRepository,
private val analytics: FirebaseAnalyticsHelper
) : BasePresenter<LuckyNumberView>(errorHandler) {
override fun onAttachView(view: LuckyNumberView) {
super.onAttachView(view)
view.initView()
loadData()
}
private fun loadData(forceRefresh: Boolean = false) {
disposable.apply {
clear()
add(studentRepository.getCurrentStudent()
.flatMap { semesterRepository.getCurrentSemester(it) }
.flatMapMaybe { luckyNumberRepository.getLuckyNumber(it, forceRefresh) }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally {
view?.run {
hideRefresh()
showProgress(false)
}
}
.subscribe({
view?.apply {
updateData(it)
showContent(true)
showEmpty(false)
}
analytics.logEvent("load_lucky_number", mapOf("force_refresh" to forceRefresh))
}, {
view?.run { showEmpty(isViewEmpty()) }
errorHandler.dispatch(it)
}, {
view?.run {
showContent(false)
showEmpty(true)
}
})
)
}
}
fun onSwipeRefresh() {
loadData(true)
}
}

View File

@ -0,0 +1,21 @@
package io.github.wulkanowy.ui.modules.luckynumber
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.ui.base.session.BaseSessionView
interface LuckyNumberView : BaseSessionView {
fun initView()
fun updateData(data: LuckyNumber)
fun hideRefresh()
fun showEmpty(show: Boolean)
fun showProgress(show: Boolean)
fun showContent(show: Boolean)
fun isViewEmpty(): Boolean
}

View File

@ -16,6 +16,7 @@ import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.grade.GradeModule
import io.github.wulkanowy.ui.modules.homework.HomeworkFragment
import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.message.MessageModule
import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment
@ -86,6 +87,10 @@ abstract class MainModule {
@ContributesAndroidInjector
abstract fun bindHomeworkFragment(): HomeworkFragment
@PerFragment
@ContributesAndroidInjector
abstract fun bindLuckyNumberFragment(): LuckyNumberFragment
@PerFragment
@ContributesAndroidInjector
abstract fun bindsAccountDialog(): AccountDialog

View File

@ -13,6 +13,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.about.AboutFragment
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
@ -60,6 +61,14 @@ class MoreFragment : BaseFragment(), MoreView, MainView.TitledView, MainView.Mai
}
}
override val luckyNumberRes: Pair<String, Drawable?>?
get() {
return context?.run {
getString(R.string.lucky_number_title) to
ContextCompat.getDrawable(this, R.drawable.ic_more_lucky_number_24dp)
}
}
override val settingsRes: Pair<String, Drawable?>?
get() {
return context?.run {
@ -114,6 +123,10 @@ class MoreFragment : BaseFragment(), MoreView, MainView.TitledView, MainView.Mai
(activity as? MainActivity)?.pushView(NoteFragment.newInstance())
}
override fun openLuckyNumberView() {
(activity as? MainActivity)?.pushView(LuckyNumberFragment.newInstance())
}
override fun openSettingsView() {
(activity as? MainActivity)?.pushView(SettingsFragment.newInstance())
}

View File

@ -23,6 +23,7 @@ class MorePresenter @Inject constructor(errorHandler: ErrorHandler) : BasePresen
messagesRes?.first -> openMessagesView()
homeworkRes?.first -> openHomeworkView()
noteRes?.first -> openNoteView()
luckyNumberRes?.first -> openLuckyNumberView()
settingsRes?.first -> openSettingsView()
aboutRes?.first -> openAboutView()
}
@ -42,6 +43,7 @@ class MorePresenter @Inject constructor(errorHandler: ErrorHandler) : BasePresen
messagesRes?.let { MoreItem(it.first, it.second) },
homeworkRes?.let { MoreItem(it.first, it.second) },
noteRes?.let { MoreItem(it.first, it.second) },
luckyNumberRes?.let { MoreItem(it.first, it.second) },
settingsRes?.let { MoreItem(it.first, it.second) },
aboutRes?.let { MoreItem(it.first, it.second) })
)

View File

@ -11,6 +11,8 @@ interface MoreView : BaseView {
val noteRes: Pair<String, Drawable?>?
val luckyNumberRes: Pair<String, Drawable?>?
val settingsRes: Pair<String, Drawable?>?
val aboutRes: Pair<String, Drawable?>?
@ -30,4 +32,6 @@ interface MoreView : BaseView {
fun openHomeworkView()
fun openNoteView()
fun openLuckyNumberView()
}