diff --git a/.gitignore b/.gitignore index 3eb9aa654..11fb79e5c 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ Thumbs.db ./app/key.p12 ./app/upload-key.jks *.log +.idea/assetWizardSettings.xml diff --git a/app/build.gradle b/app/build.gradle index 59f90fdbd..4e27ed4bc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -78,7 +78,7 @@ ext.androidx_version = "1.0.0" dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation('com.github.wulkanowy:api:e829b094de') { exclude module: "threetenbp" } + implementation('com.github.wulkanowy:api:96be34a') { exclude module: "threetenbp" } implementation "androidx.legacy:legacy-support-v4:$androidx_version" implementation "androidx.appcompat:appcompat:$androidx_version" diff --git a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt index df14b4f53..3a5a87421 100644 --- a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt @@ -67,4 +67,8 @@ internal class RepositoryModule { @Singleton @Provides fun provideTimetableDao(database: AppDatabase) = database.timetableDao() + + @Singleton + @Provides + fun provideNoteDao(database: AppDatabase) = database.noteDao() } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt index 2f5920a5c..f690a6947 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt @@ -5,23 +5,38 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters -import io.github.wulkanowy.data.db.dao.* -import io.github.wulkanowy.data.db.entities.* +import io.github.wulkanowy.data.db.dao.AttendanceDao +import io.github.wulkanowy.data.db.dao.ExamDao +import io.github.wulkanowy.data.db.dao.GradeDao +import io.github.wulkanowy.data.db.dao.GradeSummaryDao +import io.github.wulkanowy.data.db.dao.NoteDao +import io.github.wulkanowy.data.db.dao.SemesterDao +import io.github.wulkanowy.data.db.dao.StudentDao +import io.github.wulkanowy.data.db.dao.TimetableDao +import io.github.wulkanowy.data.db.entities.Attendance +import io.github.wulkanowy.data.db.entities.Exam +import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeSummary +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.Timetable import javax.inject.Singleton @Singleton @Database( - entities = [ - Student::class, - Semester::class, - Exam::class, - Timetable::class, - Attendance::class, - Grade::class, - GradeSummary::class - ], - version = 1, - exportSchema = false + entities = [ + Student::class, + Semester::class, + Exam::class, + Timetable::class, + Attendance::class, + Grade::class, + GradeSummary::class, + Note::class + ], + version = 1, + exportSchema = false ) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { @@ -29,7 +44,7 @@ abstract class AppDatabase : RoomDatabase() { companion object { fun newInstance(context: Context): AppDatabase { return Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database") - .build() + .build() } } @@ -46,4 +61,6 @@ abstract class AppDatabase : RoomDatabase() { abstract fun gradeDao(): GradeDao abstract fun gradeSummaryDao(): GradeSummaryDao + + abstract fun noteDao(): NoteDao } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/NoteDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/NoteDao.kt new file mode 100644 index 000000000..efba6e46c --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/NoteDao.kt @@ -0,0 +1,31 @@ +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.Note +import io.reactivex.Maybe + +@Dao +interface NoteDao { + + @Insert + fun insertAll(notes: List) + + @Update + fun update(note: Note) + + @Update + fun updateAll(notes: List) + + @Delete + fun deleteAll(notes: List) + + @Query("SELECT * FROM Notes WHERE semester_id = :semesterId AND student_id = :studentId") + fun getNotes(semesterId: Int, studentId: Int): Maybe> + + @Query("SELECT * FROM Notes WHERE is_read = 0 AND semester_id = :semesterId AND student_id = :studentId") + fun getNewNotes(semesterId: Int, studentId: Int): Maybe> +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Note.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Note.kt new file mode 100644 index 000000000..1f61f0870 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Note.kt @@ -0,0 +1,35 @@ +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 = "Notes") +data class Note( + + @ColumnInfo(name = "semester_id") + var semesterId: Int, + + @ColumnInfo(name = "student_id") + var studentId: Int, + + var date: LocalDate, + + var teacher: String, + + var category: String, + + var content: String +) : Serializable { + + @PrimaryKey(autoGenerate = true) + var id: Long = 0 + + @ColumnInfo(name = "is_read") + var isRead: Boolean = false + + @ColumnInfo(name = "is_notified") + var isNotified: Boolean = true +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt new file mode 100644 index 000000000..2836894f2 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt @@ -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.Note +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.repositories.local.NoteLocal +import io.github.wulkanowy.data.repositories.remote.NoteRemote +import io.reactivex.Completable +import io.reactivex.Single +import java.net.UnknownHostException +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NoteRepository @Inject constructor( + private val settings: InternetObservingSettings, + private val local: NoteLocal, + private val remote: NoteRemote +) { + + fun getNotes(semester: Semester, forceRefresh: Boolean = false, notify: Boolean = false): Single> { + return local.getNotes(semester).filter { !forceRefresh } + .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings) + .flatMap { + if (it) remote.getNotes(semester) + else Single.error(UnknownHostException()) + }.flatMap { new -> + local.getNotes(semester).toSingle(emptyList()) + .doOnSuccess { old -> + local.deleteNotes(old - new) + local.saveNotes((new - old) + .onEach { + if (notify) it.isNotified = false + }) + } + }.flatMap { local.getNotes(semester).toSingle(emptyList()) } + ) + } + + fun getNewNotes(semester: Semester): Single> { + return local.getNewNotes(semester).toSingle(emptyList()) + } + + fun updateNote(note: Note): Completable { + return local.updateNote(note) + } + + fun updateNotes(notes: List): Completable { + return local.updateNotes(notes) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/local/NoteLocal.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/local/NoteLocal.kt new file mode 100644 index 000000000..c8778125e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/local/NoteLocal.kt @@ -0,0 +1,37 @@ +package io.github.wulkanowy.data.repositories.local + +import io.github.wulkanowy.data.db.dao.NoteDao +import io.github.wulkanowy.data.db.entities.Note +import io.github.wulkanowy.data.db.entities.Semester +import io.reactivex.Completable +import io.reactivex.Maybe +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NoteLocal @Inject constructor(private val noteDb: NoteDao) { + + fun getNotes(semester: Semester): Maybe> { + return noteDb.getNotes(semester.semesterId, semester.studentId).filter { !it.isEmpty() } + } + + fun getNewNotes(semester: Semester): Maybe> { + return noteDb.getNewNotes(semester.semesterId, semester.studentId) + } + + fun saveNotes(notes: List) { + noteDb.insertAll(notes) + } + + fun updateNote(note: Note): Completable { + return Completable.fromCallable { noteDb.update(note) } + } + + fun updateNotes(notes: List): Completable { + return Completable.fromCallable { noteDb.updateAll(notes) } + } + + fun deleteNotes(notes: List) { + noteDb.deleteAll(notes) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/remote/NoteRemote.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/remote/NoteRemote.kt new file mode 100644 index 000000000..288966ee0 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/remote/NoteRemote.kt @@ -0,0 +1,34 @@ +package io.github.wulkanowy.data.repositories.remote + +import io.github.wulkanowy.api.Api +import io.github.wulkanowy.data.db.entities.Note +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.utils.toLocalDate +import io.reactivex.Single +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NoteRemote @Inject constructor(private val api: Api) { + + fun getNotes(semester: Semester): Single> { + return Single.just(api.run { + if (diaryId != semester.diaryId) { + diaryId = semester.diaryId + notifyDataChanged() + } + }).flatMap { api.getNotes() } + .map { notes -> + notes.map { + Note( + semesterId = semester.semesterId, + studentId = semester.studentId, + date = it.date.toLocalDate(), + teacher = it.teacher, + category = it.category, + content = it.content + ) + } + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/job/SyncWorker.kt b/app/src/main/java/io/github/wulkanowy/services/job/SyncWorker.kt index 4e91f5f01..f205c2865 100644 --- a/app/src/main/java/io/github/wulkanowy/services/job/SyncWorker.kt +++ b/app/src/main/java/io/github/wulkanowy/services/job/SyncWorker.kt @@ -7,10 +7,12 @@ import io.github.wulkanowy.data.repositories.AttendanceRepository 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.NoteRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SessionRepository import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.services.notification.GradeNotification +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 @@ -40,6 +42,9 @@ class SyncWorker : SimpleJobService() { @Inject lateinit var timetable: TimetableRepository + @Inject + lateinit var note: NoteRepository + @Inject lateinit var prefRepository: PreferencesRepository @@ -73,7 +78,8 @@ class SyncWorker : SimpleJobService() { gradesSummary.getGradesSummary(it, true), attendance.getAttendance(it, start, end, true), exam.getExams(it, start, end, true), - timetable.getTimetable(it, start, end, true) + timetable.getTimetable(it, start, end, true), + note.getNotes(it, true, true) ) ) } @@ -90,7 +96,12 @@ class SyncWorker : SimpleJobService() { } private fun sendNotifications() { - disposable.add(session.getSemesters(true) + sendGradeNotifications() + sendNoteNotification() + } + + private fun sendGradeNotifications() { + disposable.add(session.getSemesters() .map { it.single { semester -> semester.current } } .flatMap { gradesDetails.getNewGrades(it) } .map { it.filter { grade -> !grade.isNotified } } @@ -103,6 +114,20 @@ class SyncWorker : SimpleJobService() { }) { Timber.e("Notifications sending failed") }) } + private fun sendNoteNotification() { + disposable.add(session.getSemesters() + .map { it.single { semester -> semester.current } } + .flatMap { note.getNewNotes(it) } + .map { it.filter { note -> !note.isNotified } } + .subscribe({ + if (it.isNotEmpty()) { + Timber.d("Found ${it.size} unread notes") + NoteNotification(applicationContext).sendNotification(it) + note.updateNotes(it.map { note -> note.apply { isNotified = true } }).subscribe() + } + }) { Timber.e("Notifications sending failed") }) + } + override fun onDestroy() { super.onDestroy() disposable.clear() diff --git a/app/src/main/java/io/github/wulkanowy/services/notification/NoteNotification.kt b/app/src/main/java/io/github/wulkanowy/services/notification/NoteNotification.kt new file mode 100644 index 000000000..33c2fdc71 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/notification/NoteNotification.kt @@ -0,0 +1,58 @@ +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.Note +import io.github.wulkanowy.ui.modules.main.MainActivity +import timber.log.Timber + +class NoteNotification(context: Context) : BaseNotification(context) { + + private val channelId = "Note_Notify" + + @TargetApi(26) + override fun createChannel(channelId: String) { + notificationManager.createNotificationChannel(NotificationChannel( + channelId, context.getString(R.string.notify_note_channel), NotificationManager.IMPORTANCE_HIGH + ).apply { + enableLights(true) + enableVibration(true) + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + }) + } + + fun sendNotification(items: List) { + notify(notificationBuilder(channelId) + .setContentTitle(context.resources.getQuantityString(R.plurals.note_new_items, items.size, items.size)) + .setContentText(context.resources.getQuantityString(R.plurals.notify_note_new_items, items.size, items.size)) + .setSmallIcon(R.drawable.ic_stat_notify_note) + .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 + ) + ) + .setStyle(NotificationCompat.InboxStyle().run { + setSummaryText(context.resources.getQuantityString(R.plurals.note_number_item, items.size, items.size)) + items.forEach { + addLine("${it.teacher}: ${it.category}") + } + this + }) + .build() + ) + + Timber.d("Notification sent") + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt index 704b9e7ba..32b7bf303 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt @@ -18,7 +18,6 @@ import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.more.MoreFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.utils.getThemeAttrColor -import io.github.wulkanowy.utils.logLogin import io.github.wulkanowy.utils.safelyPopFragment import io.github.wulkanowy.utils.setOnViewChangeListener import kotlinx.android.synthetic.main.activity_main.* diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainModule.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainModule.kt index 278c3f0b1..4546857fa 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainModule.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainModule.kt @@ -14,6 +14,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.more.MoreFragment +import io.github.wulkanowy.ui.modules.note.NoteFragment import io.github.wulkanowy.ui.modules.settings.SettingsFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment @@ -57,4 +58,7 @@ abstract class MainModule { @ContributesAndroidInjector abstract fun bindSettingsFragment(): SettingsFragment + + @ContributesAndroidInjector + abstract fun bindNoteFragment(): NoteFragment } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt index 03dce3f25..be1850d5d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt @@ -25,6 +25,7 @@ class MainPresenter @Inject constructor( when (initMenuIndex) { 1 -> logLogin("Grades") 3 -> logLogin("Timetable") + 4 -> logLogin("More") } serviceHelper.startFullSyncService() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt index 8bb9b0e33..5340f97e6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt @@ -14,6 +14,7 @@ import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.about.AboutFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.modules.note.NoteFragment import io.github.wulkanowy.ui.modules.settings.SettingsFragment import io.github.wulkanowy.utils.setOnItemClickListener import kotlinx.android.synthetic.main.fragment_more.* @@ -34,11 +35,18 @@ class MoreFragment : BaseFragment(), MoreView, MainView.TitledView, MainView.Mai override val titleStringId: Int get() = R.string.more_title + override val noteRes: Pair? + get() { + return context?.run { + getString(R.string.note_title) to ContextCompat.getDrawable(this, R.drawable.ic_menu_main_note_24dp) + } + } + override val settingsRes: Pair? get() { return context?.run { getString(R.string.settings_title) to - ContextCompat.getDrawable(this, R.drawable.ic_more_settings_24dp) + ContextCompat.getDrawable(this, R.drawable.ic_more_settings_24dp) } } @@ -46,7 +54,7 @@ class MoreFragment : BaseFragment(), MoreView, MainView.TitledView, MainView.Mai get() { return context?.run { getString(R.string.about_title) to - ContextCompat.getDrawable(this, R.drawable.ic_more_about_24dp) + ContextCompat.getDrawable(this, R.drawable.ic_more_about_24dp) } } @@ -76,6 +84,10 @@ class MoreFragment : BaseFragment(), MoreView, MainView.TitledView, MainView.Mai moreAdapter.updateDataSet(data) } + override fun openNoteView() { + (activity as? MainActivity)?.pushView(NoteFragment.newInstance()) + } + override fun openSettingsView() { (activity as? MainActivity)?.pushView(SettingsFragment.newInstance()) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt index 8da65f64f..aac981f8a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt @@ -5,8 +5,7 @@ import io.github.wulkanowy.data.ErrorHandler import io.github.wulkanowy.ui.base.BasePresenter import javax.inject.Inject -class MorePresenter @Inject constructor(errorHandler: ErrorHandler) - : BasePresenter(errorHandler) { +class MorePresenter @Inject constructor(errorHandler: ErrorHandler) : BasePresenter(errorHandler) { override fun onAttachView(view: MoreView) { super.onAttachView(view) @@ -18,6 +17,7 @@ class MorePresenter @Inject constructor(errorHandler: ErrorHandler) if (item is MoreItem) { view?.run { when (item.title) { + noteRes?.first -> openNoteView() settingsRes?.first -> openSettingsView() aboutRes?.first -> openAboutView() } @@ -32,8 +32,10 @@ class MorePresenter @Inject constructor(errorHandler: ErrorHandler) private fun loadData() { view?.run { updateData(listOfNotNull( - settingsRes?.let { MoreItem(it.first, it.second) }, - aboutRes?.let { MoreItem(it.first, it.second) })) + noteRes?.let { MoreItem(it.first, it.second) }, + settingsRes?.let { MoreItem(it.first, it.second) }, + aboutRes?.let { MoreItem(it.first, it.second) }) + ) } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt index 273c6b035..fe28b4769 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt @@ -5,6 +5,8 @@ import io.github.wulkanowy.ui.base.BaseView interface MoreView : BaseView { + val noteRes: Pair? + val settingsRes: Pair? val aboutRes: Pair? @@ -18,4 +20,5 @@ interface MoreView : BaseView { fun openAboutView() fun popView() + fun openNoteView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteDialog.kt new file mode 100644 index 000000000..492aeab26 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteDialog.kt @@ -0,0 +1,48 @@ +package io.github.wulkanowy.ui.modules.note + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Note +import io.github.wulkanowy.utils.toFormattedString +import kotlinx.android.synthetic.main.dialog_note.* + +class NoteDialog : DialogFragment() { + + private lateinit var note: Note + + companion object { + private const val ARGUMENT_KEY = "Item" + + fun newInstance(exam: Note): NoteDialog { + return NoteDialog().apply { + arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_TITLE, 0) + arguments?.run { + note = getSerializable(ARGUMENT_KEY) as Note + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.dialog_note, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + noteDialogDate.text = note.date.toFormattedString() + noteDialogCategory.text = note.category + noteDialogTeacher.text = note.teacher + noteDialogContent.text = note.content + noteDialogClose.setOnClickListener { dismiss() } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteFragment.kt new file mode 100644 index 000000000..fc382ee60 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteFragment.kt @@ -0,0 +1,95 @@ +package io.github.wulkanowy.ui.modules.note + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Note +import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.utils.setOnItemClickListener +import kotlinx.android.synthetic.main.fragment_note.* +import javax.inject.Inject + +class NoteFragment : BaseFragment(), NoteView, MainView.TitledView { + + @Inject + lateinit var presenter: NotePresenter + + @Inject + lateinit var noteAdapter: FlexibleAdapter> + + companion object { + fun newInstance() = NoteFragment() + } + + override val titleStringId: Int + get() = R.string.note_title + + override val isViewEmpty: Boolean + get() = noteAdapter.isEmpty + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_note, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + presenter.onAttachView(this) + } + + override fun initView() { + noteAdapter.run { + setOnItemClickListener { presenter.onNoteItemSelected(getItem(it)) } + } + + noteRecycler.run { + layoutManager = SmoothScrollLinearLayoutManager(context) + adapter = noteAdapter + } + noteSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } + } + + override fun showNoteDialog(note: Note) { + NoteDialog.newInstance(note).show(fragmentManager, note.toString()) + } + + override fun updateData(data: List) { + noteAdapter.updateDataSet(data, true) + } + + override fun updateItem(item: AbstractFlexibleItem<*>) { + noteAdapter.updateItem(item) + } + + override fun clearData() { + noteAdapter.clear() + } + + override fun showEmpty(show: Boolean) { + noteEmpty.visibility = if (show) VISIBLE else GONE + } + + override fun showProgress(show: Boolean) { + noteProgress.visibility = if (show) VISIBLE else GONE + } + + override fun showContent(show: Boolean) { + noteRecycler.visibility = if (show) VISIBLE else GONE + } + + override fun hideRefresh() { + noteSwipe.isRefreshing = false + } + + override fun onDestroyView() { + presenter.onDetachView() + super.onDestroyView() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteItem.kt new file mode 100644 index 000000000..97635a2c8 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteItem.kt @@ -0,0 +1,61 @@ +package io.github.wulkanowy.ui.modules.note + +import android.graphics.Typeface.BOLD +import android.graphics.Typeface.NORMAL +import android.view.View +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.viewholders.FlexibleViewHolder +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Note +import io.github.wulkanowy.utils.toFormattedString +import kotlinx.android.extensions.LayoutContainer +import kotlinx.android.synthetic.main.item_note.* + +class NoteItem(val note: Note) : AbstractFlexibleItem() { + + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): NoteItem.ViewHolder { + return NoteItem.ViewHolder(view, adapter) + } + + override fun getLayoutRes(): Int = R.layout.item_note + + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: NoteItem.ViewHolder, position: Int, payloads: MutableList? + ) { + holder.apply { + noteItemDate.apply { + text = note.date.toFormattedString() + setTypeface(null, if (note.isRead) NORMAL else BOLD) + } + noteItemType.apply { + text = note.category + setTypeface(null, if (note.isRead) NORMAL else BOLD) + } + noteItemTeacher.text = note.teacher + noteItemContent.text = note.content + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as NoteItem + + if (note != other.note) return false + return true + } + + override fun hashCode(): Int { + return note.hashCode() + } + + class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter), LayoutContainer { + + override val containerView: View + get() = contentView + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt new file mode 100644 index 000000000..d2c5b4f6d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt @@ -0,0 +1,80 @@ +package io.github.wulkanowy.ui.modules.note + +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import io.github.wulkanowy.data.ErrorHandler +import io.github.wulkanowy.data.db.entities.Note +import io.github.wulkanowy.data.repositories.NoteRepository +import io.github.wulkanowy.data.repositories.SessionRepository +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.utils.SchedulersProvider +import io.github.wulkanowy.utils.logEvent +import timber.log.Timber +import javax.inject.Inject + +class NotePresenter @Inject constructor( + private val errorHandler: ErrorHandler, + private val schedulers: SchedulersProvider, + private val sessionRepository: SessionRepository, + private val noteRepository: NoteRepository +) : BasePresenter(errorHandler) { + + override fun onAttachView(view: NoteView) { + super.onAttachView(view) + view.initView() + loadData() + } + + fun onSwipeRefresh() { + loadData(true) + } + + private fun loadData(forceRefresh: Boolean = false) { + disposable.add(sessionRepository.getSemesters() + .map { it.single { semester -> semester.current } } + .flatMap { noteRepository.getNotes(it, forceRefresh) } + .map { items -> items.map { NoteItem(it) } } + .map { items -> items.sortedByDescending { it.note.date } } + .subscribeOn(schedulers.backgroundThread) + .observeOn(schedulers.mainThread) + .doFinally { + view?.run { + hideRefresh() + showProgress(false) + } + }.subscribe({ + view?.apply { + updateData(it) + showEmpty(it.isEmpty()) + showContent(it.isNotEmpty()) + } + logEvent("Note load", mapOf("items" to it.size, "forceRefresh" to forceRefresh)) + }, { + view?.run { showEmpty(isViewEmpty) } + errorHandler.proceed(it) + }) + ) + } + + fun onNoteItemSelected(item: AbstractFlexibleItem<*>?) { + if (item is NoteItem) { + view?.run { + showNoteDialog(item.note) + if (!item.note.isRead) { + item.note.isRead = true + updateItem(item) + updateNote(item.note) + } + } + } + } + + private fun updateNote(note: Note) { + disposable.add(noteRepository.updateNote(note) + .subscribeOn(schedulers.backgroundThread) + .observeOn(schedulers.mainThread) + .subscribe({ + Timber.d("Note ${note.id} updated") + }) { error -> errorHandler.proceed(error) } + ) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteView.kt new file mode 100644 index 000000000..63b615fe6 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteView.kt @@ -0,0 +1,28 @@ +package io.github.wulkanowy.ui.modules.note + +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import io.github.wulkanowy.data.db.entities.Note +import io.github.wulkanowy.ui.base.BaseView + +interface NoteView : BaseView { + + val isViewEmpty: Boolean + + fun initView() + + fun updateData(data: List) + + fun updateItem(item: AbstractFlexibleItem<*>) + + fun clearData() + + fun showEmpty(show: Boolean) + + fun showProgress(show: Boolean) + + fun showContent(show: Boolean) + + fun hideRefresh() + + fun showNoteDialog(note: Note) +} diff --git a/app/src/main/res/drawable-hdpi/ic_stat_notify_note.png b/app/src/main/res/drawable-hdpi/ic_stat_notify_note.png new file mode 100644 index 000000000..b49e4ad2c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_stat_notify_note.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_stat_notify_note.png b/app/src/main/res/drawable-mdpi/ic_stat_notify_note.png new file mode 100644 index 000000000..3498f71c3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_stat_notify_note.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_notify_note.png b/app/src/main/res/drawable-xhdpi/ic_stat_notify_note.png new file mode 100644 index 000000000..5aa30e5f7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_stat_notify_note.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_stat_notify_note.png b/app/src/main/res/drawable-xxhdpi/ic_stat_notify_note.png new file mode 100644 index 000000000..db3ec517b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_stat_notify_note.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_stat_notify_note.png b/app/src/main/res/drawable-xxxhdpi/ic_stat_notify_note.png new file mode 100644 index 000000000..06a9299a9 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_stat_notify_note.png differ diff --git a/app/src/main/res/drawable/ic_menu_main_note_24dp.xml b/app/src/main/res/drawable/ic_menu_main_note_24dp.xml new file mode 100644 index 000000000..000ac40a3 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_main_note_24dp.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_menu_main_timetable_24dp.xml b/app/src/main/res/drawable/ic_menu_main_timetable_24dp.xml index 213c7ea72..b291063ab 100644 --- a/app/src/main/res/drawable/ic_menu_main_timetable_24dp.xml +++ b/app/src/main/res/drawable/ic_menu_main_timetable_24dp.xml @@ -1,8 +1,8 @@ + android:viewportWidth="24" + android:viewportHeight="24"> + + + + + + + + + + + + + + + + + + + + + + +