diff --git a/annotation/.gitignore b/annotation/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/annotation/.gitignore @@ -0,0 +1 @@ +/build diff --git a/annotation/build.gradle b/annotation/build.gradle new file mode 100644 index 00000000..ee7f6d51 --- /dev/null +++ b/annotation/build.gradle @@ -0,0 +1,29 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-28. + */ + +apply plugin: 'java-library' +apply plugin: 'kotlin' +apply plugin: 'kotlin-kapt' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} + +sourceCompatibility = "7" +targetCompatibility = "7" + +repositories { + mavenCentral() +} +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} +compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} diff --git a/annotation/src/main/java/pl/szczodrzynski/edziennik/annotation/SelectiveDao.kt b/annotation/src/main/java/pl/szczodrzynski/edziennik/annotation/SelectiveDao.kt new file mode 100644 index 00000000..d37e5b0a --- /dev/null +++ b/annotation/src/main/java/pl/szczodrzynski/edziennik/annotation/SelectiveDao.kt @@ -0,0 +1,14 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-28. + */ + +package pl.szczodrzynski.edziennik.annotation + +import kotlin.reflect.KClass + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +@MustBeDocumented +annotation class SelectiveDao( + val db: KClass<*> +) diff --git a/annotation/src/main/java/pl/szczodrzynski/edziennik/annotation/UpdateSelective.kt b/annotation/src/main/java/pl/szczodrzynski/edziennik/annotation/UpdateSelective.kt new file mode 100644 index 00000000..224fca1c --- /dev/null +++ b/annotation/src/main/java/pl/szczodrzynski/edziennik/annotation/UpdateSelective.kt @@ -0,0 +1,13 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-28. + */ + +package pl.szczodrzynski.edziennik.annotation + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.SOURCE) +@MustBeDocumented +annotation class UpdateSelective( + val primaryKeys: Array, + val skippedColumns: Array = [] +) diff --git a/app/build.gradle b/app/build.gradle index ca5c9fcd..06caeb83 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -193,6 +193,11 @@ dependencies { implementation "io.coil-kt:coil:0.9.2" implementation 'com.github.kuba2k2:NumberSlidingPicker:2921225f76' + + implementation project(":annotation") + kapt project(":codegen") + + implementation 'com.google.android:flexbox:2.0.1' } repositories { mavenCentral() diff --git a/app/src/main/assets/pl-changelog.html b/app/src/main/assets/pl-changelog.html index 011fb6ab..a7dbbd6c 100644 --- a/app/src/main/assets/pl-changelog.html +++ b/app/src/main/assets/pl-changelog.html @@ -1,4 +1,11 @@ -

Wersja 4.0-rc.1, 2020-03-26

+

Wersja 4.0-rc.5, 2020-04-05

+ +

Dzięki za korzystanie ze Szkolnego!
diff --git a/app/src/main/cpp/szkolny-signing.cpp b/app/src/main/cpp/szkolny-signing.cpp index 70297de3..20dd9492 100644 --- a/app/src/main/cpp/szkolny-signing.cpp +++ b/app/src/main/cpp/szkolny-signing.cpp @@ -9,7 +9,7 @@ /*secret password - removed for source code publication*/ static toys AES_IV[16] = { - 0x53, 0xfb, 0x18, 0x06, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; + 0x7b, 0x51, 0x86, 0xc5, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt index 59db1ab2..6bd6934a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt @@ -43,10 +43,7 @@ import pl.szczodrzynski.edziennik.sync.SyncWorker import pl.szczodrzynski.edziennik.sync.UpdateWorker import pl.szczodrzynski.edziennik.ui.modules.base.CrashActivity import pl.szczodrzynski.edziennik.utils.* -import pl.szczodrzynski.edziennik.utils.managers.GradesManager -import pl.szczodrzynski.edziennik.utils.managers.NotificationChannelsManager -import pl.szczodrzynski.edziennik.utils.managers.TimetableManager -import pl.szczodrzynski.edziennik.utils.managers.UserActionManager +import pl.szczodrzynski.edziennik.utils.managers.* import java.util.concurrent.TimeUnit import kotlin.coroutines.CoroutineContext @@ -67,6 +64,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { val userActionManager by lazy { UserActionManager(this) } val gradesManager by lazy { GradesManager(this) } val timetableManager by lazy { TimetableManager(this) } + val eventManager by lazy { EventManager(this) } val db get() = App.db @@ -168,7 +166,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { db.profileDao().firstId?.let { profileLoadById(it) } } - devMode = "f054761fbdb6a238" == deviceId || BuildConfig.DEBUG + devMode = BuildConfig.DEBUG Signing.getCert(this) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt index 0d7b1f56..0383be1e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt @@ -27,10 +27,7 @@ import android.util.Base64.NO_WRAP import android.util.Base64.encodeToString import android.view.View import android.view.WindowManager -import android.widget.CheckBox -import android.widget.CompoundButton -import android.widget.RadioButton -import android.widget.TextView +import android.widget.* import androidx.annotation.* import androidx.core.app.ActivityCompat import androidx.core.database.getIntOrNull @@ -40,7 +37,11 @@ import androidx.core.util.forEach import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.Observer +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import androidx.viewpager.widget.ViewPager import com.google.android.gms.security.ProviderInstaller +import com.google.android.material.button.MaterialButton import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject @@ -141,6 +142,10 @@ fun CharSequence?.isNotNullNorEmpty(): Boolean { return this != null && this.isNotEmpty() } +fun Collection?.isNotNullNorEmpty(): Boolean { + return this != null && this.isNotEmpty() +} + fun CharSequence?.isNotNullNorBlank(): Boolean { return this != null && this.isNotBlank() } @@ -160,6 +165,13 @@ fun Bundle?.getString(key: String, defaultValue: String): String { return this?.getString(key, defaultValue) ?: defaultValue } +fun Bundle?.getIntOrNull(key: String): Int? { + return this?.get(key) as? Int +} +fun Bundle?.get(key: String): T? { + return this?.get(key) as? T? +} + /** * ` The quick BROWN_fox Jumps OveR THE LAZy-DOG. ` * @@ -560,7 +572,7 @@ fun CharSequence?.asBoldSpannable(): Spannable { spannable.setSpan(StyleSpan(Typeface.BOLD), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) return spannable } -fun CharSequence.asSpannable(vararg spans: Any, substring: String? = null, ignoreCase: Boolean = false): Spannable { +fun CharSequence.asSpannable(vararg spans: Any, substring: String? = null, ignoreCase: Boolean = false, ignoreDiacritics: Boolean = false): Spannable { val spannable = SpannableString(this) if (substring == null) { spans.forEach { @@ -568,17 +580,44 @@ fun CharSequence.asSpannable(vararg spans: Any, substring: String? = null, ignor } } else if (substring.isNotEmpty()) { - var index = indexOf(substring, ignoreCase = ignoreCase) + val string = + if (ignoreDiacritics) + this.cleanDiacritics() + else this + + var index = string.indexOf(substring, ignoreCase = ignoreCase) + .takeIf { it != -1 } ?: indexOf(substring, ignoreCase = ignoreCase) while (index >= 0) { spans.forEach { spannable.setSpan(it, index, index + substring.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } - index = indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase) + index = string.indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase) + .takeIf { it != -1 } ?: indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase) } } return spannable } +fun CharSequence.cleanDiacritics(): String { + val nameClean = StringBuilder() + forEach { + val ch = when (it) { + 'ż' -> 'z' + 'ó' -> 'o' + 'ł' -> 'l' + 'ć' -> 'c' + 'ę' -> 'e' + 'ś' -> 's' + 'ą' -> 'a' + 'ź' -> 'z' + 'ń' -> 'n' + else -> it + } + nameClean.append(ch) + } + return nameClean.toString() +} + /** * Returns a new read-only list only of those given elements, that are not empty. * Applies for CharSequence and descendants. @@ -722,6 +761,13 @@ inline fun T.onClick(crossinline onClickListener: (v: T) -> Unit) { } } +@Suppress("UNCHECKED_CAST") +inline fun T.onLongClick(crossinline onLongClickListener: (v: T) -> Boolean) { + setOnLongClickListener { v: View -> + onLongClickListener(v as T) + } +} + @Suppress("UNCHECKED_CAST") inline fun T.onChange(crossinline onChangeListener: (v: T, isChecked: Boolean) -> Unit) { setOnCheckedChangeListener { buttonView, isChecked -> @@ -729,6 +775,19 @@ inline fun T.onChange(crossinline onChangeListener: (v: T, } } +@Suppress("UNCHECKED_CAST") +inline fun T.onChange(crossinline onChangeListener: (v: T, isChecked: Boolean) -> Unit) { + clearOnCheckedChangeListeners() + addOnCheckedChangeListener { buttonView, isChecked -> + onChangeListener(buttonView as T, isChecked) + } +} + +fun View.attachToastHint(stringRes: Int) = onLongClick { + Toast.makeText(it.context, stringRes, Toast.LENGTH_SHORT).show() + true +} + fun LiveData.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer) { observe(lifecycleOwner, object : Observer { override fun onChanged(t: T?) { @@ -784,7 +843,7 @@ fun View.findParentById(targetId: Int): View? { return null } -fun CoroutineScope.startCoroutineTimer(delayMillis: Long = 0, repeatMillis: Long = 0, action: () -> Unit) = launch { +fun CoroutineScope.startCoroutineTimer(delayMillis: Long = 0, repeatMillis: Long = 0, action: suspend CoroutineScope.() -> Unit) = launch { delay(delayMillis) if (repeatMillis > 0) { while (true) { @@ -1008,6 +1067,7 @@ fun Context.getNotificationTitle(type: Int): String { Notification.TYPE_FEEDBACK_MESSAGE -> R.string.notification_type_feedback_message Notification.TYPE_NEW_ANNOUNCEMENT -> R.string.notification_type_new_announcement Notification.TYPE_AUTO_ARCHIVING -> R.string.notification_type_auto_archiving + Notification.TYPE_TEACHER_ABSENCE -> R.string.notification_type_new_teacher_absence Notification.TYPE_GENERAL -> R.string.notification_type_general else -> R.string.notification_type_general }) @@ -1166,3 +1226,23 @@ fun TextView.getTextPosition(range: IntRange): Rect { return parentTextViewRect } + +inline fun ViewPager.addOnPageSelectedListener(crossinline block: (position: Int) -> Unit) = addOnPageChangeListener(object : ViewPager.OnPageChangeListener { + override fun onPageScrollStateChanged(state: Int) {} + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} + override fun onPageSelected(position: Int) { block(position) } +}) + +val SwipeRefreshLayout.onScrollListener: RecyclerView.OnScrollListener + get() = object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + if (recyclerView.canScrollVertically(-1)) + this@onScrollListener.isEnabled = false + if (!recyclerView.canScrollVertically(-1) && newState == RecyclerView.SCROLL_STATE_IDLE) + this@onScrollListener.isEnabled = true + } + } + +operator fun Iterable>.get(key: K): V? { + return firstOrNull { it.first == key }?.second +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt index 8f53b625..85b4f887 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt @@ -20,7 +20,6 @@ import androidx.appcompat.widget.PopupMenu import androidx.core.graphics.ColorUtils import androidx.lifecycle.Observer import androidx.navigation.NavOptions -import androidx.recyclerview.widget.RecyclerView import com.danimahardhika.cafebar.CafeBar import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.mikepenz.iconics.IconicsColor @@ -55,22 +54,22 @@ import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog import pl.szczodrzynski.edziennik.ui.modules.agenda.AgendaFragment import pl.szczodrzynski.edziennik.ui.modules.announcements.AnnouncementsFragment import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceFragment -import pl.szczodrzynski.edziennik.ui.modules.base.DebugFragment import pl.szczodrzynski.edziennik.ui.modules.base.MainSnackbar import pl.szczodrzynski.edziennik.ui.modules.behaviour.BehaviourFragment +import pl.szczodrzynski.edziennik.ui.modules.debug.DebugFragment +import pl.szczodrzynski.edziennik.ui.modules.debug.LabFragment import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackFragment import pl.szczodrzynski.edziennik.ui.modules.feedback.HelpFragment -import pl.szczodrzynski.edziennik.ui.modules.grades.GradesFragment +import pl.szczodrzynski.edziennik.ui.modules.grades.GradesListFragment import pl.szczodrzynski.edziennik.ui.modules.grades.editor.GradesEditorFragment import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment import pl.szczodrzynski.edziennik.ui.modules.homework.HomeworkFragment import pl.szczodrzynski.edziennik.ui.modules.login.LoginActivity import pl.szczodrzynski.edziennik.ui.modules.messages.MessageFragment -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesComposeFragment import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesListFragment -import pl.szczodrzynski.edziennik.ui.modules.notifications.NotificationsFragment +import pl.szczodrzynski.edziennik.ui.modules.messages.compose.MessagesComposeFragment +import pl.szczodrzynski.edziennik.ui.modules.notifications.NotificationsListFragment import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsNewFragment import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment @@ -129,6 +128,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { const val TARGET_MESSAGES_DETAILS = 503 const val TARGET_MESSAGES_COMPOSE = 504 const val TARGET_WEB_PUSH = 140 + const val TARGET_LAB = 1000 const val HOME_ID = DRAWER_ITEM_HOME @@ -153,7 +153,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { .withBadgeTypeId(TYPE_EVENT) .isInDrawer(true) - list += NavTarget(DRAWER_ITEM_GRADES, R.string.menu_grades, GradesFragment::class) + list += NavTarget(DRAWER_ITEM_GRADES, R.string.menu_grades, GradesListFragment::class) .withIcon(CommunityMaterial.Icon2.cmd_numeric_5_box_outline) .withBadgeTypeId(TYPE_GRADE) .isInDrawer(true) @@ -185,7 +185,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { // static drawer items - list += NavTarget(DRAWER_ITEM_NOTIFICATIONS, R.string.menu_notifications, NotificationsFragment::class) + list += NavTarget(DRAWER_ITEM_NOTIFICATIONS, R.string.menu_notifications, NotificationsListFragment::class) .withIcon(CommunityMaterial.Icon.cmd_bell_ring_outline) .isInDrawer(true) .isStatic(true) @@ -227,6 +227,13 @@ class MainActivity : AppCompatActivity(), CoroutineScope { list += NavTarget(TARGET_MESSAGES_COMPOSE, R.string.menu_message_compose, MessagesComposeFragment::class) list += NavTarget(TARGET_WEB_PUSH, R.string.menu_web_push, WebPushFragment::class) list += NavTarget(DRAWER_ITEM_DEBUG, R.string.menu_debug, DebugFragment::class) + if (App.devMode) { + list += NavTarget(TARGET_LAB, R.string.menu_lab, LabFragment::class) + .withIcon(CommunityMaterial.Icon.cmd_flask_outline) + .isInDrawer(true) + .isBelowSeparator(true) + .isStatic(true) + } list } @@ -509,7 +516,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { .withIcon(CommunityMaterial.Icon2.cmd_help_circle_outline) .withOnClickListener(View.OnClickListener { loadTarget(TARGET_FEEDBACK) }) ) - if (App.devMode) { + if (App.debugMode) { bottomSheet += BottomSheetPrimaryItem(false) .withTitle(R.string.menu_debug) .withIcon(CommunityMaterial.Icon.cmd_android_studio) @@ -880,9 +887,6 @@ class MainActivity : AppCompatActivity(), CoroutineScope { } app.profileLoad(id) { MessagesFragment.pageSelection = -1 - MessagesListFragment.tapPositions = intArrayOf(RecyclerView.NO_POSITION, RecyclerView.NO_POSITION) - MessagesListFragment.topPositions = intArrayOf(RecyclerView.NO_POSITION, RecyclerView.NO_POSITION) - MessagesListFragment.bottomPositions = intArrayOf(RecyclerView.NO_POSITION, RecyclerView.NO_POSITION) setDrawerItems() // the drawer profile is updated automatically when the drawer item is clicked @@ -907,9 +911,10 @@ class MainActivity : AppCompatActivity(), CoroutineScope { loadTarget(target, arguments) } } - private fun loadTarget(target: NavTarget, arguments: Bundle? = null) { - d("NavDebug", "loadTarget(target = $target, arguments = $arguments)") + private fun loadTarget(target: NavTarget, args: Bundle? = null) { + d("NavDebug", "loadTarget(target = $target, args = $args)") + val arguments = args ?: navBackStack.firstOrNull { it.first.id == target.id }?.second ?: Bundle() bottomSheet.close() bottomSheet.removeAllContextual() bottomSheet.toggleGroupEnabled = false @@ -957,6 +962,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { navBackStack.removeAt(navBackStack.lastIndex) } navTarget = target + navArguments = arguments return@let null }?.let { @@ -966,7 +972,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { R.anim.task_open_enter, R.anim.task_open_exit ) - navBackStack.add(navTarget to arguments) + navBackStack.add(navTarget to navArguments) navTarget = target navArguments = arguments } @@ -1068,6 +1074,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { .also { if (target.icon != null) it.withIcon(target.icon!!) } .also { if (target.title != null) it.withAppTitle(getString(target.title!!)) } .also { if (target.badgeTypeId != null) it.withBadgeStyle(drawer.badgeStyle)} + .withSelectedBackgroundAnimated(false) if (target.badgeTypeId != null) drawer.addUnreadCounterType(target.badgeTypeId!!, target.id) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/Config.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/Config.kt index bf4f743f..98240be5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/Config.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/Config.kt @@ -22,7 +22,7 @@ import kotlin.coroutines.CoroutineContext class Config(val db: AppDb) : CoroutineScope, AbstractConfig { companion object { - const val DATA_VERSION = 11 + const val DATA_VERSION = 12 } private val job = Job() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt index 6281225d..d956c1ec 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt @@ -18,7 +18,7 @@ import kotlin.coroutines.CoroutineContext class ProfileConfig(val db: AppDb, val profileId: Int, rawEntries: List) : CoroutineScope, AbstractConfig { companion object { - const val DATA_VERSION = 1 + const val DATA_VERSION = 2 } private val job = Job() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigMigration.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigMigration.kt index a30c996a..1095f511 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigMigration.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigMigration.kt @@ -64,11 +64,25 @@ class ConfigMigration(app: App, config: Config) { dataVersion = 2 } + if (dataVersion < 3) { + update = null + privacyPolicyAccepted = false + debugMode = false + devModePassword = null + appInstalledTime = 0L + appRateSnackbarTime = 0L + + dataVersion = 3 + } + if (dataVersion < 10) { ui.openDrawerOnBackPressed = false ui.snowfall = false ui.bottomSheetOpened = false sync.dontShowAppManagerDialog = false + sync.webPushEnabled = true + sync.lastAppSync = 0L + dataVersion = 10 } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ProfileConfigMigration.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ProfileConfigMigration.kt index 67861d25..17f071f2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ProfileConfigMigration.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ProfileConfigMigration.kt @@ -5,6 +5,7 @@ package pl.szczodrzynski.edziennik.config.utils import pl.szczodrzynski.edziennik.config.ProfileConfig +import pl.szczodrzynski.edziennik.data.db.entity.Notification import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.AGENDA_DEFAULT import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_ALL_GRADES @@ -14,11 +15,23 @@ class ProfileConfigMigration(config: ProfileConfig) { if (dataVersion < 1) { grades.colorMode = COLOR_MODE_WEIGHTED - grades.dontCountEnabled = false grades.yearAverageMode = YEAR_ALL_GRADES + grades.hideImproved = false + grades.averageWithoutWeight = true + grades.plusValue = null + grades.minusValue = null + grades.dontCountEnabled = false + grades.dontCountGrades = listOf() ui.agendaViewType = AGENDA_DEFAULT + // no migration for ui.homeCards dataVersion = 1 } + + if (dataVersion < 2) { + sync.notificationFilter = sync.notificationFilter + Notification.TYPE_TEACHER_ABSENCE + + dataVersion = 2 + } }} } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt index 2b0b63f2..ad9c4296 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt @@ -38,6 +38,9 @@ class ApiService : Service() { context.startService(Intent(context, ApiService::class.java)) EventBus.getDefault().postSticky(request) } + + var lastEventTime = System.currentTimeMillis() + var taskCancelTries = 0 } private val app by lazy { applicationContext as App } @@ -64,9 +67,6 @@ class ApiService : Service() { private val notification by lazy { EdziennikNotification(app) } - private var lastEventTime = System.currentTimeMillis() - private var taskCancelTries = 0 - /* ______ _ _ _ _ _____ _ _ _ _ | ____| | | (_) (_) | / ____| | | | | | | | |__ __| |_____ ___ _ __ _ __ _| | __ | | __ _| | | |__ __ _ ___| | __ diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt index 4fba7d9b..004954bc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt @@ -56,6 +56,8 @@ const val LIBRUS_SYNERGIA_TOKEN_LOGIN_URL = "https://synergia.librus.pl/loguj/to const val LIBRUS_MESSAGES_URL = "https://wiadomosci.librus.pl/module" const val LIBRUS_SANDBOX_URL = "https://sandbox.librus.pl/index.php?action=" +const val LIBRUS_SYNERGIA_HOMEWORK_ATTACHMENT_URL = "https://synergia.librus.pl/homework/downloadFile" + const val IDZIENNIK_USER_AGENT = SYNERGIA_USER_AGENT const val IDZIENNIK_WEB_URL = "https://iuczniowie.progman.pl/idziennik" const val IDZIENNIK_WEB_LOGIN = "login.aspx" @@ -74,6 +76,8 @@ const val IDZIENNIK_WEB_GET_MESSAGE = "mod_komunikator/WS_wiadomosci.asmx/Pobier const val IDZIENNIK_WEB_GET_RECIPIENT_LIST = "mod_komunikator/WS_wiadomosci.asmx/pobierzListeOdbiorcowPanelRodzic" const val IDZIENNIK_WEB_SEND_MESSAGE = "mod_komunikator/WS_wiadomosci.asmx/WyslijWiadomosc" const val IDZIENNIK_WEB_GET_ATTACHMENT = "mod_komunikator/Download.ashx" +const val IDZIENNIK_WEB_GET_HOMEWORK = "mod_panelRodzica/pracaDomowa/WS_pracaDomowa.asmx/pobierzJednaPraceDomowa" +const val IDZIENNIK_WEB_GET_HOMEWORK_ATTACHMENT = "mod_panelRodzica/pracaDomowa.aspx" val IDZIENNIK_API_USER_AGENT = SYSTEM_USER_AGENT const val IDZIENNIK_API_URL = "https://iuczniowie.progman.pl/idziennik/api" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EdziennikNotification.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EdziennikNotification.kt index 8582fc03..605b644c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EdziennikNotification.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EdziennikNotification.kt @@ -8,11 +8,12 @@ import android.app.Notification import android.app.NotificationManager import android.app.PendingIntent import android.content.Context -import android.content.Intent import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.PRIORITY_MIN import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.Bundle import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.receivers.SzkolnyReceiver import kotlin.math.roundToInt @@ -35,16 +36,18 @@ class EdziennikNotification(val app: App) { var serviceClosed = false private fun cancelPendingIntent(taskId: Int): PendingIntent { - val intent = Intent("pl.szczodrzynski.edziennik.SZKOLNY_MAIN") - intent.putExtra("task", "TaskCancelRequest") - intent.putExtra("taskId", taskId) - return PendingIntent.getBroadcast(app, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT) as PendingIntent + val intent = SzkolnyReceiver.getIntent(app, Bundle( + "task" to "TaskCancelRequest", + "taskId" to taskId + )) + return PendingIntent.getBroadcast(app, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) as PendingIntent } private val closePendingIntent: PendingIntent get() { - val intent = Intent("pl.szczodrzynski.edziennik.SZKOLNY_MAIN") - intent.putExtra("task", "ServiceCloseRequest") - return PendingIntent.getBroadcast(app, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT) as PendingIntent + val intent = SzkolnyReceiver.getIntent(app, Bundle( + "task" to "ServiceCloseRequest" + )) + return PendingIntent.getBroadcast(app, 0, intent, 0) as PendingIntent } private fun errorCountText(): String? { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt index 1d86abf6..989a279c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt @@ -126,6 +126,7 @@ const val ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED = 184 const val ERROR_LIBRUS_API_DEVICE_REGISTERED = 185 const val ERROR_LIBRUS_MESSAGES_NOT_FOUND = 186 const val ERROR_LOGIN_LIBRUS_API_INVALID_REQUEST = 187 +const val ERROR_LIBRUS_MESSAGES_ATTACHMENT_NOT_FOUND = 188 const val ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_LOGIN = 201 const val ERROR_LOGIN_MOBIDZIENNIK_WEB_OLD_PASSWORD = 202 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Features.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Features.kt index 0e75753b..e38b37b5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Features.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Features.kt @@ -13,8 +13,8 @@ import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_HOME import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_HOMEWORK import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_TIMETABLE -import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_RECEIVED -import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_SENT +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT internal const val FEATURE_TIMETABLE = 1 internal const val FEATURE_AGENDA = 2 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt index e2955190..66b764cd 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt @@ -70,13 +70,13 @@ val librusLoginMethods = listOf( LoginMethod(LOGIN_TYPE_LIBRUS, LOGIN_METHOD_LIBRUS_SYNERGIA, LibrusLoginSynergia::class.java) .withIsPossible { _, loginStore -> !loginStore.hasLoginData("fakeLogin") } .withRequiredLoginMethod { profile, _ -> - if (profile?.hasStudentData("accountPassword") == false) LOGIN_METHOD_LIBRUS_API else LOGIN_METHOD_NOT_NEEDED + if (profile?.hasStudentData("accountPassword") == false || true) LOGIN_METHOD_LIBRUS_API else LOGIN_METHOD_NOT_NEEDED }, LoginMethod(LOGIN_TYPE_LIBRUS, LOGIN_METHOD_LIBRUS_MESSAGES, LibrusLoginMessages::class.java) .withIsPossible { _, loginStore -> !loginStore.hasLoginData("fakeLogin") } .withRequiredLoginMethod { profile, _ -> - if (profile?.hasStudentData("accountPassword") == false) LOGIN_METHOD_LIBRUS_SYNERGIA else LOGIN_METHOD_NOT_NEEDED + if (profile?.hasStudentData("accountPassword") == false || true) LOGIN_METHOD_LIBRUS_SYNERGIA else LOGIN_METHOD_NOT_NEEDED } ) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt index 633370f5..8ad2a505 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt @@ -84,6 +84,22 @@ object Regexes { """(.+?) - (.*?).+?.+?\((.+?), .+?(.+?)\)""".toRegex(DOT_MATCHES_ALL) } + val MOBIDZIENNIK_HOMEWORK_ROW by lazy { + """class="rowRolling">(.+?\s*)""".toRegex(DOT_MATCHES_ALL) + } + val MOBIDZIENNIK_HOMEWORK_ITEM by lazy { + """

(.+?):\s*(.+?)\s*

""".toRegex(DOT_MATCHES_ALL) + } + val MOBIDZIENNIK_HOMEWORK_BODY by lazy { + """Treść:(.+?)

""".toRegex(DOT_MATCHES_ALL) + } + val MOBIDZIENNIK_HOMEWORK_ID by lazy { + """zadanieFormularz\(([0-9]+),""".toRegex(DOT_MATCHES_ALL) + } + val MOBIDZIENNIK_HOMEWORK_ATTACHMENT by lazy { + """zalacznik=([0-9]+)'.+?word-break">(.+?)""".toRegex(DOT_MATCHES_ALL) + } + val IDZIENNIK_LOGIN_HIDDEN_FIELDS by lazy { @@ -155,6 +171,9 @@ object Regexes { val EDUDZIENNIK_ANNOUNCEMENT_DESCRIPTION by lazy { """

.*?

(.*?)

""".toRegex(DOT_MATCHES_ALL) } + val EDUDZIENNIK_HOMEWORK_DESCRIPTION by lazy { + """
(.*?)
""".toRegex(DOT_MATCHES_ALL) + } val EDUDZIENNIK_SUBJECT_ID by lazy { """/Courses/([\w-_]+?)/""".toRegex() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt index db8186c9..13de2f17 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt @@ -19,9 +19,9 @@ import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.api.task.IApiTask import pl.szczodrzynski.edziennik.data.db.entity.LoginStore -import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull +import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.MessageFull open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTask(profileId) { @@ -36,8 +36,9 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa fun messageSend(profileId: Int, recipients: List, subject: String, text: String) = EdziennikTask(profileId, MessageSendRequest(recipients, subject, text)) fun announcementsRead(profileId: Int) = EdziennikTask(profileId, AnnouncementsReadRequest()) fun announcementGet(profileId: Int, announcement: AnnouncementFull) = EdziennikTask(profileId, AnnouncementGetRequest(announcement)) - fun attachmentGet(profileId: Int, message: Message, attachmentId: Long, attachmentName: String) = EdziennikTask(profileId, AttachmentGetRequest(message, attachmentId, attachmentName)) + fun attachmentGet(profileId: Int, owner: Any, attachmentId: Long, attachmentName: String) = EdziennikTask(profileId, AttachmentGetRequest(owner, attachmentId, attachmentName)) fun recipientListGet(profileId: Int) = EdziennikTask(profileId, RecipientListGetRequest()) + fun eventGet(profileId: Int, event: EventFull) = EdziennikTask(profileId, EventGetRequest(event)) } private lateinit var loginStore: LoginStore @@ -92,8 +93,9 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa is FirstLoginRequest -> edziennikInterface?.firstLogin() is AnnouncementsReadRequest -> edziennikInterface?.markAllAnnouncementsAsRead() is AnnouncementGetRequest -> edziennikInterface?.getAnnouncement(request.announcement) - is AttachmentGetRequest -> edziennikInterface?.getAttachment(request.message, request.attachmentId, request.attachmentName) + is AttachmentGetRequest -> edziennikInterface?.getAttachment(request.owner, request.attachmentId, request.attachmentName) is RecipientListGetRequest -> edziennikInterface?.getRecipientList() + is EventGetRequest -> edziennikInterface?.getEvent(request.event) } } @@ -113,6 +115,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa data class MessageSendRequest(val recipients: List, val subject: String, val text: String) class AnnouncementsReadRequest data class AnnouncementGetRequest(val announcement: AnnouncementFull) - data class AttachmentGetRequest(val message: Message, val attachmentId: Long, val attachmentName: String) + data class AttachmentGetRequest(val owner: Any, val attachmentId: Long, val attachmentName: String) class RecipientListGetRequest + data class EventGetRequest(val event: EventFull) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/Edudziennik.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/Edudziennik.kt index 7bc50511..17bf771f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/Edudziennik.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/Edudziennik.kt @@ -9,6 +9,7 @@ import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikData import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web.EdudziennikWebGetAnnouncement +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web.EdudziennikWebGetHomework import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.firstlogin.EdudziennikFirstLogin import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login.EdudziennikLogin import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login.EdudziennikLoginWeb @@ -16,10 +17,10 @@ import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.db.entity.LoginStore -import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull +import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.utils.Utils.d @@ -94,13 +95,22 @@ class Edudziennik(val app: App, val profile: Profile?, val loginStore: LoginStor } } - override fun getAttachment(message: Message, attachmentId: Long, attachmentName: String) {} + override fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) {} override fun getRecipientList() {} + override fun getEvent(eventFull: EventFull) { + EdudziennikLoginWeb(data) { + EdudziennikWebGetHomework(data, eventFull) { + completed() + } + } + } + override fun firstLogin() { EdudziennikFirstLogin(data) { completed() } } override fun cancel() { d(TAG, "Cancelled") data.cancel() + callback.onCompleted() } private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebEvents.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebEvents.kt index cf846fce..52833b9a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebEvents.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebEvents.kt @@ -39,17 +39,16 @@ class EdudziennikWebEvents(override val data: DataEdudziennik, ?: return@forEach val eventObject = Event( - profileId, - id, - date, - null, - title, - -1, - Event.TYPE_CLASS_EVENT, - false, - -1, - -1, - data.teamClass?.id ?: -1 + profileId = profileId, + id = id, + date = date, + time = null, + topic = title, + color = null, + type = Event.TYPE_CLASS_EVENT, + teacherId = -1, + subjectId = -1, + teamId = data.teamClass?.id ?: -1 ) data.eventList.add(eventObject) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebExams.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebExams.kt index 8eb7d04e..8e7836fd 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebExams.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebExams.kt @@ -56,17 +56,16 @@ class EdudziennikWebExams(override val data: DataEdudziennik, val eventType = data.getEventType(eventTypeId, eventTypeName) val eventObject = Event( - profileId, - id, - date, - startTime, - topic, - -1, - eventType.id, - false, - -1, - subject.id, - data.teamClass?.id ?: -1 + profileId = profileId, + id = id, + date = date, + time = startTime, + topic = topic, + color = null, + type = eventType.id, + teacherId = -1, + subjectId = subject.id, + teamId = data.teamClass?.id ?: -1 ) data.eventList.add(eventObject) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGetHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGetHomework.kt new file mode 100644 index 00000000..84171699 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGetHomework.kt @@ -0,0 +1,45 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web + +import android.text.Html +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.data.api.Regexes +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb +import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent +import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.isNotNullNorEmpty + +class EdudziennikWebGetHomework( + override val data: DataEdudziennik, + val event: EventFull, + val onSuccess: () -> Unit +) : EdudziennikWeb(data, null) { + companion object { + const val TAG = "EdudziennikWebGetHomework" + } + + init { + if (event.attachmentNames.isNotNullNorEmpty()) { + val id = event.attachmentNames!![0] + + webGet(TAG, "Homework/$id") { text -> + val description = Regexes.EDUDZIENNIK_HOMEWORK_DESCRIPTION.find(text)?.get(1)?.trim() + + if (description != null) event.topic = Html.fromHtml(description).toString() + + event.homeworkBody = "" + event.attachmentNames = null + + data.eventList += event + data.eventListReplace = true + + EventBus.getDefault().postSticky(EventGetEvent(event)) + onSuccess() + } + } else { + EventBus.getDefault().postSticky(EventGetEvent(event)) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebHomework.kt index 9d9a336a..acabcfa6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebHomework.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebHomework.kt @@ -33,8 +33,8 @@ class EdudziennikWebHomework(override val data: DataEdudziennik, if (doc.getElementsByClass("message").text().trim() != "Brak prac domowych") { doc.getElementsByTag("tr").forEach { homeworkElement -> val dateElement = homeworkElement.getElementsByClass("date").first().child(0) - val id = EDUDZIENNIK_HOMEWORK_ID.find(dateElement.attr("href"))?.get(1)?.crc32() - ?: return@forEach + val idStr = EDUDZIENNIK_HOMEWORK_ID.find(dateElement.attr("href"))?.get(1) ?: return@forEach + val id = idStr.crc32() val date = Date.fromY_m_d(dateElement.text()) val subjectElement = homeworkElement.child(1).child(0) @@ -49,22 +49,23 @@ class EdudziennikWebHomework(override val data: DataEdudziennik, val teacherName = homeworkElement.child(2).text() val teacher = data.getTeacherByFirstLast(teacherName) - val topic = homeworkElement.child(4).text() + val topic = homeworkElement.child(4).text()?.trim() val eventObject = Event( - profileId, - id, - date, - startTime, - topic, - -1, - Event.TYPE_HOMEWORK, - false, - teacher.id, - subject.id, - data.teamClass?.id ?: -1 + profileId = profileId, + id = id, + date = date, + time = startTime, + topic = topic ?: "", + color = null, + type = Event.TYPE_HOMEWORK, + teacherId = teacher.id, + subjectId = subject.id, + teamId = data.teamClass?.id ?: -1 ) + eventObject.attachmentNames = mutableListOf(idStr) + data.eventList.add(eventObject) data.metadataList.add(Metadata( profileId, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/Idziennik.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/Idziennik.kt index be0d0d1c..e1fa6f00 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/Idziennik.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/Idziennik.kt @@ -8,20 +8,15 @@ import com.google.gson.JsonObject import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikData -import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web.IdziennikWebGetAttachment -import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web.IdziennikWebGetMessage -import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web.IdziennikWebGetRecipientList -import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web.IdziennikWebSendMessage +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web.* import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.firstlogin.IdziennikFirstLogin import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.login.IdziennikLogin import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.data.db.entity.LoginStore -import pl.szczodrzynski.edziennik.data.db.entity.Message -import pl.szczodrzynski.edziennik.data.db.entity.Profile -import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.data.db.entity.* import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull +import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.utils.Utils.d @@ -103,10 +98,17 @@ class Idziennik(val app: App, val profile: Profile?, val loginStore: LoginStore, override fun markAllAnnouncementsAsRead() {} override fun getAnnouncement(announcement: AnnouncementFull) {} - override fun getAttachment(message: Message, attachmentId: Long, attachmentName: String) { + override fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) { login(LOGIN_METHOD_IDZIENNIK_WEB) { - IdziennikWebGetAttachment(data, message, attachmentId, attachmentName) { - completed() + if (owner is Message) { + IdziennikWebGetAttachment(data, owner, attachmentId, attachmentName) { + completed() + } + } + else if (owner is Event) { + IdziennikWebGetHomeworkAttachment(data, owner, attachmentId, attachmentName) { + completed() + } } } } @@ -119,10 +121,19 @@ class Idziennik(val app: App, val profile: Profile?, val loginStore: LoginStore, } } + override fun getEvent(eventFull: EventFull) { + login(LOGIN_METHOD_IDZIENNIK_WEB) { + IdziennikWebGetHomework(data, eventFull) { + completed() + } + } + } + override fun firstLogin() { IdziennikFirstLogin(data) { completed() } } override fun cancel() { d(TAG, "Cancelled") data.cancel() + callback.onCompleted() } private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/IdziennikWeb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/IdziennikWeb.kt index 907e1f50..b871cd37 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/IdziennikWeb.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/IdziennikWeb.kt @@ -229,6 +229,7 @@ open class IdziennikWeb(open val data: DataIdziennik, open val lastSync: Long?) .apply { parameters.forEach { (k, v) -> addParameter(k, v) } } + .contentType("application/x-www-form-urlencoded") .post() .callback(callback) .build() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/api/IdziennikApiMessagesInbox.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/api/IdziennikApiMessagesInbox.kt index c8d756fe..58e7660c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/api/IdziennikApiMessagesInbox.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/api/IdziennikApiMessagesInbox.kt @@ -11,8 +11,8 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.ENDPOINT_IDZIENNIK_API_MESSAGES_INBOX import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikApi import pl.szczodrzynski.edziennik.data.db.entity.* -import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_DELETED -import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_RECEIVED +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_DELETED +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED import pl.szczodrzynski.edziennik.getBoolean import pl.szczodrzynski.edziennik.getString import pl.szczodrzynski.edziennik.utils.Utils.crc32 @@ -33,11 +33,11 @@ class IdziennikApiMessagesInbox(override val data: DataIdziennik, return@apiGet } - json.asJsonObjectList()?.forEach { jMessage -> - val subject = jMessage.getString("tytul") - if (subject?.contains("(") == true && subject.startsWith("iDziennik - ")) + json.asJsonObjectList().forEach { jMessage -> + val subject = jMessage.getString("tytul") ?: "" + if (subject.contains("(") && subject.startsWith("iDziennik - ")) return@forEach - if (subject?.startsWith("Uwaga dla ucznia (klasa:") == true) + if (subject.startsWith("Uwaga dla ucznia (klasa:")) return@forEach val messageIdStr = jMessage.getString("id") @@ -64,13 +64,12 @@ class IdziennikApiMessagesInbox(override val data: DataIdziennik, rTeacher.setTeacherType(Teacher.TYPE_OTHER) val message = Message( - profileId, - messageId, - subject, - body, - if (jMessage.getBoolean("rekordUsuniety") == true) TYPE_DELETED else TYPE_RECEIVED, - rTeacher.id, - -1 + profileId = profileId, + id = messageId, + type = if (jMessage.getBoolean("rekordUsuniety") == true) TYPE_DELETED else TYPE_RECEIVED, + subject = subject, + body = body, + senderId = rTeacher.id ) val messageRecipient = MessageRecipient( @@ -81,7 +80,7 @@ class IdziennikApiMessagesInbox(override val data: DataIdziennik, /*messageId*/ messageId ) - data.messageIgnoreList.add(message) + data.messageList.add(message) data.messageRecipientList.add(messageRecipient) data.setSeenMetadataList.add(Metadata( profileId, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/api/IdziennikApiMessagesSent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/api/IdziennikApiMessagesSent.kt index b0b17689..286c9896 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/api/IdziennikApiMessagesSent.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/api/IdziennikApiMessagesSent.kt @@ -13,7 +13,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.ENDPOINT_IDZIENNIK_API_MESSAGES_SENT import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikApi import pl.szczodrzynski.edziennik.data.db.entity.Message -import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_SENT +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.utils.Utils.crc32 @@ -46,13 +46,12 @@ class IdziennikApiMessagesSent(override val data: DataIdziennik, val sentDate = Date.fromIso(jMessage.get("dataWyslania").asString) val message = Message( - profileId, - messageId, - subject, - body, - TYPE_SENT, - -1, - -1 + profileId = profileId, + id = messageId, + type = TYPE_SENT, + subject = subject, + body = body, + senderId = null ) for (recipientEl in jMessage.getAsJsonArray("odbiorcy")) { @@ -76,7 +75,7 @@ class IdziennikApiMessagesSent(override val data: DataIdziennik, data.messageRecipientIgnoreList.add(messageRecipient) } - data.messageIgnoreList.add(message) + data.messageList.add(message) data.metadataList.add(Metadata(profileId, Metadata.TYPE_MESSAGE, message.id, true, true, sentDate)) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebExams.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebExams.kt index 42202f4e..82e63e79 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebExams.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebExams.kt @@ -66,7 +66,7 @@ class IdziennikWebExams(override val data: DataIdziennik, val subjectId = data.getSubject(subjectName, null, subjectName).id val teacherName = exam.getString("wpisal") ?: return@forEach val teacherId = data.getTeacherByLastFirst(teacherName).id - val topic = exam.getString("zakres") ?: "" + val topic = exam.getString("zakres")?.trim() ?: "" val lessonList = data.db.timetableDao().getForDateNow(profileId, examDate) val startTime = lessonList.firstOrNull { it.subjectId == subjectId }?.startTime @@ -80,17 +80,16 @@ class IdziennikWebExams(override val data: DataIdziennik, } val eventObject = Event( - profileId, - id, - examDate, - startTime, - topic, - -1, - eventType, - false, - teacherId, - subjectId, - data.teamClass?.id ?: -1 + profileId = profileId, + id = id, + date = examDate, + time = startTime, + topic = topic, + color = null, + type = eventType, + teacherId = teacherId, + subjectId = subjectId, + teamId = data.teamClass?.id ?: -1 ) data.eventList.add(eventObject) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetAttachment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetAttachment.kt index 562f527c..b7a462f6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetAttachment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetAttachment.kt @@ -15,7 +15,7 @@ import pl.szczodrzynski.edziennik.utils.Utils import java.io.File class IdziennikWebGetAttachment(override val data: DataIdziennik, - val message: Message, + val owner: Any, val attachmentId: Long, val attachmentName: String, val onSuccess: () -> Unit @@ -25,6 +25,8 @@ class IdziennikWebGetAttachment(override val data: DataIdziennik, } init { + val message = owner as Message + val messageId = "\\[META:([A-z0-9]+);([0-9-]+)]".toRegex().find(message.body ?: "")?.get(2) ?: -1 val targetFile = File(Utils.getStorageDir(), attachmentName) @@ -34,29 +36,29 @@ class IdziennikWebGetAttachment(override val data: DataIdziennik, ), { file -> val event = AttachmentGetEvent( profileId, - message.id, + owner, attachmentId, AttachmentGetEvent.TYPE_FINISHED, file.absolutePath ) - val attachmentDataFile = File(Utils.getStorageDir(), ".${profileId}_${event.messageId}_${event.attachmentId}") + val attachmentDataFile = File(Utils.getStorageDir(), ".${profileId}_${event.ownerId}_${event.attachmentId}") Utils.writeStringToFile(attachmentDataFile, event.fileName) - EventBus.getDefault().post(event) + EventBus.getDefault().postSticky(event) onSuccess() }) { written, _ -> val event = AttachmentGetEvent( profileId, - message.id, + owner, attachmentId, AttachmentGetEvent.TYPE_PROGRESS, bytesWritten = written ) - EventBus.getDefault().post(event) + EventBus.getDefault().postSticky(event) } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetHomework.kt new file mode 100644 index 00000000..12d804db --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetHomework.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-1. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web + +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.data.api.ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA +import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_WEB_GET_HOMEWORK +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikWeb +import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.getBoolean +import pl.szczodrzynski.edziennik.getJsonObject +import pl.szczodrzynski.edziennik.getString + +class IdziennikWebGetHomework(override val data: DataIdziennik, + val event: EventFull, + val onSuccess: () -> Unit +) : IdziennikWeb(data, null) { + companion object { + private const val TAG = "IdziennikWebGetHomework" + } + + init { + webApiGet(TAG, IDZIENNIK_WEB_GET_HOMEWORK, mapOf( + "idP" to data.registerId, + "idPD" to event.id + )) { result -> + val json = result.getJsonObject("d") ?: run { + data.error(ApiError(TAG, ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA) + .withApiResponse(result)) + return@webApiGet + } + + val homework = json.getJsonObject("praca") ?: return@webApiGet + + if (homework.getBoolean("zalacznik", false)) { + event.attachmentIds = mutableListOf(event.id) + event.attachmentNames = mutableListOf("Załącznik do zadania") + } + else { + event.attachmentIds = mutableListOf() + event.attachmentNames = mutableListOf() + } + event.homeworkBody = homework.getString("tresc") + + data.eventList.add(event) + data.eventListReplace = true + + EventBus.getDefault().postSticky(EventGetEvent(event)) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetHomeworkAttachment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetHomeworkAttachment.kt new file mode 100644 index 00000000..f4a60f98 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetHomeworkAttachment.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-1. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web + +import com.google.gson.JsonObject +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_WEB_GET_HOMEWORK_ATTACHMENT +import pl.szczodrzynski.edziennik.data.api.Regexes +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikWeb +import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent +import pl.szczodrzynski.edziennik.data.db.entity.Event +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.set +import pl.szczodrzynski.edziennik.utils.Utils +import java.io.File + +class IdziennikWebGetHomeworkAttachment(override val data: DataIdziennik, + val owner: Any, + val attachmentId: Long, + val attachmentName: String, + val onSuccess: () -> Unit +) : IdziennikWeb(data, null) { + companion object { + const val TAG = "IdziennikWebGetHomeworkAttachment" + } + + init { + val homework = owner as Event + + /*val request = Request.Builder() + .url("") + .build() + data.app.http.newCall(request).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + data.error(ApiError(TAG, ERROR_REQUEST_FAILURE) + .withThrowable(e)) + } + + override fun onResponse(call: Call, response: Response) { + val filename = response.header("content-disposition")?.substringAfter("\"")?.substringBeforeLast("\"") + + val file: File = File(Utils.getStorageDir(), filename) + val sink = file.sink().buffer() + response.body()?.source()?.let { + sink.writeAll(it) + } + sink.close() + } + })*/ + + webGet(TAG, IDZIENNIK_WEB_GET_HOMEWORK_ATTACHMENT) { text -> + val hiddenFields = JsonObject() + Regexes.IDZIENNIK_LOGIN_HIDDEN_FIELDS.findAll(text).forEach { + hiddenFields[it[1]] = it[2] + } + + webGetFile(TAG, IDZIENNIK_WEB_GET_HOMEWORK_ATTACHMENT, Utils.getStorageDir(), mapOf( + "__VIEWSTATE" to hiddenFields.getString("__VIEWSTATE", ""), + "__VIEWSTATEGENERATOR" to hiddenFields.getString("__VIEWSTATEGENERATOR", ""), + "__EVENTVALIDATION" to hiddenFields.getString("__EVENTVALIDATION", ""), + "__EVENTTARGET" to "ctl00\$cphContent\$bt_pobraniePliku", + "ctl00\$dxComboUczniowie" to data.registerId, + "ctl00\$cphContent\$idPracyDomowej" to attachmentId + ), { file -> + val event = AttachmentGetEvent( + profileId, + owner, + attachmentId, + AttachmentGetEvent.TYPE_FINISHED, + file.absolutePath + ) + + val attachmentDataFile = File(Utils.getStorageDir(), ".${profileId}_${event.ownerId}_${event.attachmentId}") + Utils.writeStringToFile(attachmentDataFile, event.fileName) + + homework.attachmentNames = mutableListOf(file.name) + data.eventList.add(homework) + data.eventListReplace = true + + EventBus.getDefault().postSticky(event) + onSuccess() + + }) { written, _ -> + val event = AttachmentGetEvent( + profileId, + owner, + attachmentId, + AttachmentGetEvent.TYPE_PROGRESS, + bytesWritten = written + ) + + EventBus.getDefault().postSticky(event) + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetMessage.kt index dfe9fba3..e709122d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetMessage.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetMessage.kt @@ -10,8 +10,8 @@ import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_WEB_GET_MESSAGE import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikWeb import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent -import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_RECEIVED -import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_SENT +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.data.db.full.MessageRecipientFull @@ -50,7 +50,11 @@ class IdziennikWebGetMessage(override val data: DataIdziennik, message.recipients?.clear() when (message.type) { TYPE_RECEIVED -> { - val recipientObject = MessageRecipientFull(profileId, -1, message.id) + val recipientObject = MessageRecipientFull( + profileId = profileId, + id = -1, + messageId = message.id + ) val readDateString = it.getString("DataOdczytania") recipientObject.readDate = if (readDateString.isNullOrBlank()) System.currentTimeMillis() @@ -67,7 +71,11 @@ class IdziennikWebGetMessage(override val data: DataIdziennik, val recipientName = recipient.getString("NazwaOdbiorcy") ?: return@forEach val teacher = data.getTeacherByLastFirst(recipientName) - val recipientObject = MessageRecipientFull(profileId, teacher.id, message.id) + val recipientObject = MessageRecipientFull( + profileId = profileId, + id = teacher.id, + messageId = message.id + ) recipientObject.readDate = recipient.getLong("Status") ?: return@forEach recipientObject.fullName = teacher.fullName @@ -91,9 +99,10 @@ class IdziennikWebGetMessage(override val data: DataIdziennik, )) } - EventBus.getDefault().postSticky(MessageGetEvent(message)) - data.messageList.add(message) + data.messageListReplace = true + + EventBus.getDefault().postSticky(MessageGetEvent(message)) onSuccess() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebHomework.kt index 345e7da4..2289e13d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebHomework.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebHomework.kt @@ -52,13 +52,14 @@ class IdziennikWebHomework(override val data: DataIdziennik, json.getJsonArray("ListK")?.asJsonObjectList()?.forEach { homework -> val id = homework.getLong("_recordId") ?: return@forEach val eventDate = Date.fromY_m_d(homework.getString("dataO") ?: return@forEach) + val addedDate = Date.fromY_m_d(homework.getString("dataZ") ?: return@forEach) val subjectName = homework.getString("przed") ?: return@forEach val subjectId = data.getSubject(subjectName, null, subjectName).id val teacherName = homework.getString("usr") ?: return@forEach val teacherId = data.getTeacherByLastFirst(teacherName).id val lessonList = data.db.timetableDao().getForDateNow(profileId, eventDate) val startTime = lessonList.firstOrNull { it.subjectId == subjectId }?.displayStartTime - val topic = homework.getString("tytul") ?: "" + val topic = homework.getString("tytul")?.trim() ?: "" val seen = when (profile?.empty) { true -> true @@ -67,17 +68,16 @@ class IdziennikWebHomework(override val data: DataIdziennik, val eventObject = Event( - profileId, - id, - eventDate, - startTime, - topic, - -1, - Event.TYPE_HOMEWORK, - false, - teacherId, - subjectId, - data.teamClass?.id ?: -1 + profileId = profileId, + id = id, + date = eventDate, + time = startTime, + topic = topic, + color = null, + type = Event.TYPE_HOMEWORK, + teacherId = teacherId, + subjectId = subjectId, + teamId = data.teamClass?.id ?: -1 ) data.eventList.add(eventObject) @@ -87,7 +87,7 @@ class IdziennikWebHomework(override val data: DataIdziennik, eventObject.id, seen, seen, - System.currentTimeMillis() + addedDate.inMillis )) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebProposedGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebProposedGrades.kt index fe5a0156..c2639e51 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebProposedGrades.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebProposedGrades.kt @@ -13,6 +13,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikWeb import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel import pl.szczodrzynski.edziennik.data.db.entity.Grade +import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_DESCRIPTIVE import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_SEMESTER1_PROPOSED import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_YEAR_PROPOSED import pl.szczodrzynski.edziennik.data.db.entity.Metadata @@ -20,7 +21,6 @@ import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS import pl.szczodrzynski.edziennik.getJsonArray import pl.szczodrzynski.edziennik.getJsonObject import pl.szczodrzynski.edziennik.getString -import pl.szczodrzynski.edziennik.utils.Utils.getWordGradeValue class IdziennikWebProposedGrades(override val data: DataIdziennik, override val lastSync: Long?, @@ -39,36 +39,64 @@ class IdziennikWebProposedGrades(override val data: DataIdziennik, .withApiResponse(result)) return@webApiGet } + val manager = data.app.gradesManager json.getJsonArray("Przedmioty")?.asJsonObjectList()?.forEach { subject -> val subjectName = subject.getString("Przedmiot") ?: return@forEach val subjectObject = data.getSubject(subjectName, null, subjectName) val semester1Proposed = subject.getString("OcenaSem1") ?: "" - val semester1Value = getWordGradeValue(semester1Proposed) + val semester1Value = manager.getGradeValue(semester1Proposed) val semester1Id = subjectObject.id * (-100) - 1 + val semester1Type = + if (semester1Value == 0f) TYPE_DESCRIPTIVE + else TYPE_SEMESTER1_PROPOSED + val semester1Name = when { + semester1Value == 0f -> " " + semester1Value % 1.0f == 0f -> semester1Value.toInt().toString() + else -> semester1Value.toString() + } + val semester1Color = + if (semester1Value == 0f) 0xff536dfe.toInt() + else -1 val semester2Proposed = subject.getString("OcenaSem2") ?: "" - val semester2Value = getWordGradeValue(semester2Proposed) + val semester2Value = manager.getGradeValue(semester2Proposed) val semester2Id = subjectObject.id * (-100) - 2 + val semester2Type = + if (semester2Value == 0f) TYPE_DESCRIPTIVE + else TYPE_YEAR_PROPOSED + val semester2Name = when { + semester2Value == 0f -> " " + semester2Value % 1.0f == 0f -> semester2Value.toInt().toString() + else -> semester2Value.toString() + } + val semester2Color = + if (semester2Value == 0f) 0xffff4081.toInt() + else -1 if (semester1Proposed != "") { val gradeObject = Grade( profileId = profileId, id = semester1Id, - name = semester1Value.toString(), - type = TYPE_SEMESTER1_PROPOSED, - value = semester1Value.toFloat(), + name = semester1Name, + type = semester1Type, + value = semester1Value, weight = 0f, - color = -1, - category = null, - description = null, + color = semester1Color, + category = if (semester1Value == 0f) "Ocena opisowa semestralna" else null, + description = if (semester1Value == 0f) semester1Proposed else null, comment = null, semester = 1, teacherId = -1, subjectId = subjectObject.id ) + val addedDate = if (data.profile.empty) + data.profile.dateSemester1Start.inMillis + else + System.currentTimeMillis() + data.gradeList.add(gradeObject) data.metadataList.add(Metadata( profileId, @@ -76,7 +104,7 @@ class IdziennikWebProposedGrades(override val data: DataIdziennik, gradeObject.id, profile.empty, profile.empty, - System.currentTimeMillis() + addedDate )) } @@ -84,13 +112,13 @@ class IdziennikWebProposedGrades(override val data: DataIdziennik, val gradeObject = Grade( profileId = profileId, id = semester2Id, - name = semester2Value.toString(), - type = TYPE_YEAR_PROPOSED, - value = semester2Value.toFloat(), + name = semester2Name, + type = semester2Type, + value = semester2Value, weight = 0f, - color = -1, - category = null, - description = null, + color = semester2Color, + category = if (semester2Value == 0f) "Ocena opisowa końcoworoczna" else null, + description = if (semester2Value == 0f) semester2Proposed else null, comment = null, semester = 2, teacherId = -1, @@ -98,7 +126,7 @@ class IdziennikWebProposedGrades(override val data: DataIdziennik, ) val addedDate = if (data.profile.empty) - data.profile.dateSemester1Start.inMillis + data.profile.dateSemester2Start.inMillis else System.currentTimeMillis() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebSendMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebSendMessage.kt index 9a997a32..973bc74f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebSendMessage.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebSendMessage.kt @@ -57,7 +57,7 @@ class IdziennikWebSendMessage(override val data: DataIdziennik, } IdziennikApiMessagesSent(data, null) { - val message = data.messageIgnoreList.firstOrNull { it.type == Message.TYPE_SENT && it.subject == subject } + val message = data.messageList.firstOrNull { it.type == Message.TYPE_SENT && it.subject == subject } val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == message?.id } val event = MessageSentEvent(data.profileId, message, metadata?.addedDate) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/Librus.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/Librus.kt index 0a1ed593..a79a66f8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/Librus.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/Librus.kt @@ -13,6 +13,8 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.Librus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.LibrusMessagesGetMessage import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.LibrusMessagesGetRecipientList import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.LibrusMessagesSendMessage +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia.LibrusSynergiaGetHomework +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia.LibrusSynergiaHomeworkGetAttachment import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia.LibrusSynergiaMarkAllAnnouncementsAsRead import pl.szczodrzynski.edziennik.data.api.edziennik.librus.firstlogin.LibrusFirstLogin import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLogin @@ -24,6 +26,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull +import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.utils.Utils.d @@ -118,11 +121,23 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va } } - override fun getAttachment(message: Message, attachmentId: Long, attachmentName: String) { - login(LOGIN_METHOD_LIBRUS_MESSAGES) { - LibrusMessagesGetAttachment(data, message, attachmentId, attachmentName) { - completed() + override fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) { + when (owner) { + is Message -> { + login(LOGIN_METHOD_LIBRUS_MESSAGES) { + LibrusMessagesGetAttachment(data, owner, attachmentId, attachmentName) { + completed() + } + } } + is EventFull -> { + login(LOGIN_METHOD_LIBRUS_SYNERGIA) { + LibrusSynergiaHomeworkGetAttachment(data, owner, attachmentId, attachmentName) { + completed() + } + } + } + else -> completed() } } @@ -134,10 +149,19 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va } } + override fun getEvent(eventFull: EventFull) { + login(LOGIN_METHOD_LIBRUS_SYNERGIA) { + LibrusSynergiaGetHomework(data, eventFull) { + completed() + } + } + } + override fun firstLogin() { LibrusFirstLogin(data) { completed() } } override fun cancel() { d(TAG, "Cancelled") data.cancel() + callback.onCompleted() } private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusMessages.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusMessages.kt index bccaa1f9..dc8d8c47 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusMessages.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusMessages.kt @@ -252,10 +252,11 @@ open class LibrusMessages(open val data: DataLibrus, open val lastSync: Long?) { .enqueue() } - fun sandboxGetFile(tag: String, action: String, targetFile: File, onSuccess: (file: File) -> Unit, + fun sandboxGetFile(tag: String, url: String, targetFile: File, onSuccess: (file: File) -> Unit, + method: Int = GET, onProgress: (written: Long, total: Long) -> Unit) { - d(tag, "Request: Librus/Messages - $LIBRUS_SANDBOX_URL$action") + d(tag, "Request: Librus/Messages - $url") val callback = object : FileCallbackHandler(targetFile) { override fun onSuccess(file: File?, response: Response?) { @@ -291,9 +292,14 @@ open class LibrusMessages(open val data: DataLibrus, open val lastSync: Long?) { } Request.builder() - .url("$LIBRUS_SANDBOX_URL$action") + .url(url) .userAgent(SYNERGIA_USER_AGENT) - .post() + .also { + when (method) { + POST -> it.post() + else -> it.get() + } + } .callback(callback) .build() .enqueue() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusSynergia.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusSynergia.kt index e201ca8f..96f28a65 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusSynergia.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusSynergia.kt @@ -35,7 +35,7 @@ open class LibrusSynergia(open val data: DataLibrus, open val lastSync: Long?) { return } - if (!text.contains("jesteś zalogowany")) { + if (!text.contains("jesteś zalogowany") && !text.contains("Podgląd zadania")) { when { text.contains("stop.png") -> ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED text.contains("Przerwa techniczna") -> ERROR_LIBRUS_SYNERGIA_MAINTENANCE @@ -48,7 +48,6 @@ open class LibrusSynergia(open val data: DataLibrus, open val lastSync: Long?) { } } - try { onSuccess(text) } catch (e: Exception) { @@ -90,4 +89,42 @@ open class LibrusSynergia(open val data: DataLibrus, open val lastSync: Long?) { .build() .enqueue() } + + fun redirectUrlGet(tag: String, url: String, onSuccess: (url: String) -> Unit) { + val callback = object : TextCallbackHandler() { + override fun onSuccess(text: String?, response: Response) { + val redirectUrl = response.headers().get("Location") + + if (redirectUrl != null) { + try { + onSuccess(redirectUrl) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_LIBRUS_SYNERGIA_REQUEST) + .withResponse(response) + .withThrowable(e) + .withApiResponse(text)) + } + } else { + data.error(ApiError(tag, ERROR_LIBRUS_SYNERGIA_OTHER) + .withResponse(response) + .withApiResponse(text)) + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(tag, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + Request.builder() + .url(url) + .userAgent(LIBRUS_USER_AGENT) + .withClient(data.app.httpLazy) + .get() + .callback(callback) + .build() + .enqueue() + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiEvents.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiEvents.kt index a04c2863..52f52844 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiEvents.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiEvents.kt @@ -35,7 +35,7 @@ class LibrusApiEvents(override val data: DataLibrus, events?.forEach { event -> val id = event.getLong("Id") ?: return@forEach val eventDate = Date.fromY_m_d(event.getString("Date")) - val topic = event.getString("Content") ?: "" + val topic = event.getString("Content")?.trim() ?: "" val type = event.getJsonObject("Category")?.getLong("Id") ?: -1 val teacherId = event.getJsonObject("CreatedBy")?.getLong("Id") ?: -1 val subjectId = event.getJsonObject("Subject")?.getLong("Id") ?: -1 @@ -47,17 +47,16 @@ class LibrusApiEvents(override val data: DataLibrus, val addedDate = Date.fromIso(event.getString("AddDate")) val eventObject = Event( - profileId, - id, - eventDate, - startTime, - topic, - -1, - type, - false, - teacherId, - subjectId, - teamId + profileId = profileId, + id = id, + date = eventDate, + time = startTime, + topic = topic, + color = null, + type = type, + teacherId = teacherId, + subjectId = subjectId, + teamId = teamId ) data.eventList.add(eventObject) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiHomework.kt index 16d66c6f..d47fbfdf 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiHomework.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiHomework.kt @@ -34,17 +34,16 @@ class LibrusApiHomework(override val data: DataLibrus, val addedDate = Date.fromY_m_d(homework.getString("Date")) val eventObject = Event( - profileId, - id, - eventDate, - null, - topic, - -1, - -1, - false, - teacherId, - -1, - -1 + profileId = profileId, + id = id, + date = eventDate, + time = null, + topic = topic, + color = null, + type = -1, + teacherId = teacherId, + subjectId = -1, + teamId = -1 ) data.eventList.add(eventObject) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPtMeetings.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPtMeetings.kt index 3a58d8e1..de696a99 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPtMeetings.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPtMeetings.kt @@ -39,17 +39,16 @@ class LibrusApiPtMeetings(override val data: DataLibrus, } val eventObject = Event( - profileId, - id, - eventDate, - startTime, - topic, - -1, - Event.TYPE_PT_MEETING, - false, - teacherId, - -1, - data.teamClass?.id ?: -1 + profileId = profileId, + id = id, + date = eventDate, + time = startTime, + topic = topic, + color = null, + type = Event.TYPE_PT_MEETING, + teacherId = teacherId, + subjectId = -1, + teamId = data.teamClass?.id ?: -1 ) data.eventList.add(eventObject) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTeacherFreeDays.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTeacherFreeDays.kt index 04c44efb..3d4db1aa 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTeacherFreeDays.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTeacherFreeDays.kt @@ -59,7 +59,7 @@ class LibrusApiTeacherFreeDays(override val data: DataLibrus, profileId, Metadata.TYPE_TEACHER_ABSENCE, id, - profile?.empty ?: false, + true, profile?.empty ?: false, System.currentTimeMillis() )) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetAttachment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetAttachment.kt index f7234891..b54e704b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetAttachment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetAttachment.kt @@ -4,22 +4,12 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages -import kotlinx.coroutines.* -import org.greenrobot.eventbus.EventBus -import pl.szczodrzynski.edziennik.data.api.ERROR_FILE_DOWNLOAD -import pl.szczodrzynski.edziennik.data.api.EXCEPTION_LIBRUS_MESSAGES_REQUEST -import pl.szczodrzynski.edziennik.data.api.Regexes +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusMessages -import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent -import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent.Companion.TYPE_FINISHED -import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent.Companion.TYPE_PROGRESS -import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.db.entity.Message -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.getString -import pl.szczodrzynski.edziennik.utils.Utils -import java.io.File import kotlin.coroutines.CoroutineContext class LibrusMessagesGetAttachment(override val data: DataLibrus, @@ -37,8 +27,6 @@ class LibrusMessagesGetAttachment(override val data: DataLibrus, override val coroutineContext: CoroutineContext get() = job + Dispatchers.Default - private var getAttachmentCheckKeyTries = 0 - init { messagesGet(TAG, "GetFileDownloadLink", parameters = mapOf( "fileId" to attachmentId, @@ -46,81 +34,8 @@ class LibrusMessagesGetAttachment(override val data: DataLibrus, "archive" to 0 )) { doc -> val downloadLink = doc.select("response GetFileDownloadLink downloadLink").text() - val keyMatcher = Regexes.LIBRUS_ATTACHMENT_KEY.find(downloadLink) - if (keyMatcher != null) { - getAttachmentCheckKeyTries = 0 - - val attachmentKey = keyMatcher[1] - getAttachmentCheckKey(attachmentKey) { - downloadAttachment(attachmentKey) - } - } else { - data.error(ApiError(TAG, ERROR_FILE_DOWNLOAD) - .withApiResponse(doc.toString())) - } - } - } - - private fun getAttachmentCheckKey(attachmentKey: String, callback: () -> Unit) { - sandboxGet(TAG, "CSCheckKey", - parameters = mapOf("singleUseKey" to attachmentKey)) { json -> - - when (json.getString("status")) { - "not_downloaded_yet" -> { - if (getAttachmentCheckKeyTries++ > 5) { - data.error(ApiError(TAG, ERROR_FILE_DOWNLOAD) - .withApiResponse(json)) - return@sandboxGet - } - launch { - delay(2000) - getAttachmentCheckKey(attachmentKey, callback) - } - } - - "ready" -> { - launch { callback() } - } - - else -> { - data.error(ApiError(TAG, EXCEPTION_LIBRUS_MESSAGES_REQUEST) - .withApiResponse(json)) - } - } - } - } - - private fun downloadAttachment(attachmentKey: String) { - val targetFile = File(Utils.getStorageDir(), attachmentName) - - sandboxGetFile(TAG, "CSDownload&singleUseKey=$attachmentKey", targetFile, { file -> - - val event = AttachmentGetEvent( - profileId, - message.id, - attachmentId, - TYPE_FINISHED, - file.absolutePath - ) - - val attachmentDataFile = File(Utils.getStorageDir(), ".${profileId}_${event.messageId}_${event.attachmentId}") - Utils.writeStringToFile(attachmentDataFile, event.fileName) - - EventBus.getDefault().post(event) - - onSuccess() - - }) { written, _ -> - val event = AttachmentGetEvent( - profileId, - message.id, - attachmentId, - TYPE_PROGRESS, - bytesWritten = written - ) - - EventBus.getDefault().post(event) + LibrusSandboxDownloadAttachment(data, downloadLink, message, attachmentId, attachmentName, onSuccess) } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetList.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetList.kt index f4aa2b3a..35148cef 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetList.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetList.kt @@ -12,7 +12,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_MESS import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_MESSAGES_SENT import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusMessages import pl.szczodrzynski.edziennik.data.db.entity.* -import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_RECEIVED +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED import pl.szczodrzynski.edziennik.fixName import pl.szczodrzynski.edziennik.singleOrNull import pl.szczodrzynski.edziennik.utils.Utils @@ -78,7 +78,7 @@ class LibrusMessagesGetList(override val data: DataLibrus, val senderId = when (type) { TYPE_RECEIVED -> recipientId - else -> -1 + else -> null } val receiverId = when (type) { @@ -92,13 +92,12 @@ class LibrusMessagesGetList(override val data: DataLibrus, } val messageObject = Message( - profileId, - id, - subject, - null, - type, - senderId, - -1 + profileId = profileId, + id = id, + type = type, + subject = subject, + body = null, + senderId = senderId ) val messageRecipientObject = MessageRecipient( @@ -109,7 +108,12 @@ class LibrusMessagesGetList(override val data: DataLibrus, id ) - data.messageIgnoreList.add(messageObject) + element.select("isAnyFileAttached")?.text()?.let { + if (it == "1") + messageObject.hasAttachments = true + } + + data.messageList.add(messageObject) data.messageRecipientList.add(messageRecipientObject) data.setSeenMetadataList.add(Metadata( profileId, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetMessage.kt index 824fdc5e..bb4c620b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetMessage.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetMessage.kt @@ -9,8 +9,8 @@ import org.greenrobot.eventbus.EventBus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusMessages import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent -import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_RECEIVED -import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_SENT +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.full.MessageFull @@ -102,11 +102,10 @@ class LibrusMessagesGetMessage(override val data: DataLibrus, } val messageRecipientObject = MessageRecipientFull( - profileId, - -1, - -1, - readDate, - messageObject.id + profileId = profileId, + id = -1, + messageId = messageObject.id, + readDate = readDate ) messageRecipientObject.fullName = profile.accountName ?: profile.studentNameLong ?: "" @@ -132,11 +131,10 @@ class LibrusMessagesGetMessage(override val data: DataLibrus, } val messageRecipientObject = MessageRecipientFull( - profileId, - receiverId, - -1, - readDate, - messageObject.id + profileId = profileId, + id = receiverId, + messageId = messageObject.id, + readDate = readDate ) messageRecipientObject.fullName = "$receiverFirstName $receiverLastName" @@ -159,7 +157,9 @@ class LibrusMessagesGetMessage(override val data: DataLibrus, messageObject.recipients = messageRecipientList data.messageRecipientList.addAll(messageRecipientList) + data.messageList.add(messageObject) + data.messageListReplace = true EventBus.getDefault().postSticky(MessageGetEvent(messageObject)) onSuccess() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesSendMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesSendMessage.kt index 02215dea..9364f2b8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesSendMessage.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesSendMessage.kt @@ -48,7 +48,7 @@ class LibrusMessagesSendMessage(override val data: DataLibrus, } LibrusMessagesGetList(data, type = Message.TYPE_SENT, lastSync = null) { - val message = data.messageIgnoreList.firstOrNull { it.type == Message.TYPE_SENT && it.id == id } + val message = data.messageList.firstOrNull { it.type == Message.TYPE_SENT && it.id == id } val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == message?.id } val event = MessageSentEvent(data.profileId, message, metadata?.addedDate) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusSandboxDownloadAttachment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusSandboxDownloadAttachment.kt new file mode 100644 index 00000000..c6131bb2 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusSandboxDownloadAttachment.kt @@ -0,0 +1,117 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages + +import kotlinx.coroutines.* +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusMessages +import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.utils.Utils +import java.io.File +import kotlin.coroutines.CoroutineContext + +class LibrusSandboxDownloadAttachment(override val data: DataLibrus, + downloadLink: String, + val owner: Any, + val attachmentId: Long, + val attachmentName: String, + val onSuccess: () -> Unit +) : LibrusMessages(data, null), CoroutineScope { + companion object { + const val TAG = "LibrusSandboxDownloadAttachment" + } + + private var job = Job() + + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Default + + private var getAttachmentCheckKeyTries = 0 + + init { + val keyMatcher = Regexes.LIBRUS_ATTACHMENT_KEY.find(downloadLink) + + when { + downloadLink.contains("CSDownloadFailed") -> { + data.error(ApiError(TAG, ERROR_LIBRUS_MESSAGES_ATTACHMENT_NOT_FOUND)) + onSuccess() + } + keyMatcher != null -> { + getAttachmentCheckKeyTries = 0 + + val attachmentKey = keyMatcher[1] + getAttachmentCheckKey(attachmentKey) { + downloadAttachment("${LIBRUS_SANDBOX_URL}CSDownload&singleUseKey=$attachmentKey", method = POST) + } + } + else -> { + downloadAttachment("$downloadLink/get", method = GET) + } + } + } + + private fun getAttachmentCheckKey(attachmentKey: String, callback: () -> Unit) { + sandboxGet(LibrusMessagesGetAttachment.TAG, "CSCheckKey", + parameters = mapOf("singleUseKey" to attachmentKey)) { json -> + + when (json.getString("status")) { + "not_downloaded_yet" -> { + if (getAttachmentCheckKeyTries++ > 5) { + data.error(ApiError(LibrusMessagesGetAttachment.TAG, ERROR_FILE_DOWNLOAD) + .withApiResponse(json)) + return@sandboxGet + } + launch { + delay(2000) + getAttachmentCheckKey(attachmentKey, callback) + } + } + + "ready" -> { + launch { callback() } + } + + else -> { + data.error(ApiError(LibrusMessagesGetAttachment.TAG, EXCEPTION_LIBRUS_MESSAGES_REQUEST) + .withApiResponse(json)) + } + } + } + } + + private fun downloadAttachment(url: String, method: Int = GET) { + val targetFile = File(Utils.getStorageDir(), attachmentName) + + sandboxGetFile(LibrusMessagesGetAttachment.TAG, url, targetFile, { file -> + + val event = AttachmentGetEvent( + profileId, + owner, + attachmentId, + AttachmentGetEvent.TYPE_FINISHED, + file.absolutePath + ) + + val attachmentDataFile = File(Utils.getStorageDir(), ".${profileId}_${event.ownerId}_${event.attachmentId}") + Utils.writeStringToFile(attachmentDataFile, event.fileName) + + EventBus.getDefault().postSticky(event) + + onSuccess() + + }) { written, _ -> + val event = AttachmentGetEvent( + profileId, + owner, + attachmentId, + AttachmentGetEvent.TYPE_PROGRESS, + bytesWritten = written + ) + + EventBus.getDefault().postSticky(event) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaGetHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaGetHomework.kt new file mode 100644 index 00000000..379bd42d --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaGetHomework.kt @@ -0,0 +1,48 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia + +import android.text.Html +import org.greenrobot.eventbus.EventBus +import org.jsoup.Jsoup +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusSynergia +import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent +import pl.szczodrzynski.edziennik.data.db.full.EventFull + +class LibrusSynergiaGetHomework(override val data: DataLibrus, + val event: EventFull, + val onSuccess: () -> Unit +) : LibrusSynergia(data, null) { + companion object { + const val TAG = "LibrusSynergiaGetHomework" + } + + init { + synergiaGet(TAG, "moje_zadania/podglad/${event.id}") { text -> + val doc = Jsoup.parse(text) + + val table = doc.select("table.decorated tbody > tr") + + event.topic = table[1].select("td")[1].text() + event.homeworkBody = Html.fromHtml(table[5].select("td")[1].html()).toString() + + event.attachmentIds = mutableListOf() + event.attachmentNames = mutableListOf() + + if (table.size > 6) { + table[6].select("a").forEach { a -> + val attachmentId = a.attr("href").split('/') + .last().toLongOrNull() ?: return@forEach + val filename = a.text() + event.attachmentIds?.add(attachmentId) + event.attachmentNames?.add(filename) + } + } + + data.eventList.add(event) + data.eventListReplace = true + + EventBus.getDefault().postSticky(EventGetEvent(event)) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaHomework.kt index 87cd88cf..48f88ef4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaHomework.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaHomework.kt @@ -42,8 +42,6 @@ class LibrusSynergiaHomework(override val data: DataLibrus, doc.select("table.myHomeworkTable > tbody").firstOrNull()?.also { homeworkTable -> val homeworkElements = homeworkTable.children() - val graphElements = doc.select("table[border].center td[align=left] tbody").first().children() - homeworkElements.forEachIndexed { i, el -> val elements = el.children() @@ -63,43 +61,22 @@ class LibrusSynergiaHomework(override val data: DataLibrus, val lessons = data.db.timetableDao().getForDateNow(profileId, eventDate) val startTime = lessons.firstOrNull { it.subjectId == subjectId }?.startTime - /*val moreInfo = graphElements[2 * i + 1].select("td[title]") - .attr("title").trim()*/ - - var description = "" - - graphElements.forEach { graphEl -> - graphEl.select("td[title]")?.also { - val title = it.attr("title") - val r = "Temat: (.*?)Data udostępnienia: (.*?)Termin wykonania: (.*?)Treść: (.*)" - .toRegex(RegexOption.DOT_MATCHES_ALL).find(title) ?: return@forEach - val gTopic = r[1].trim() - val gAddedDate = Date.fromY_m_d(r[2].trim()) - val gEventDate = Date.fromY_m_d(r[3].trim()) - if (gTopic == topic && gAddedDate == addedDate && gEventDate == eventDate) { - description = r[4].replace("".toRegex(), "\n").trim() - return@forEach - } - } - } - val seen = when (profile.empty) { true -> true else -> eventDate < Date.getToday() } val eventObject = Event( - profileId, - id, - eventDate, - startTime, - "$topic\n$description", - -1, - Event.TYPE_HOMEWORK, - false, - teacherId, - subjectId, - data.teamClass?.id ?: -1 + profileId = profileId, + id = id, + date = eventDate, + time = startTime, + topic = topic, + color = null, + type = Event.TYPE_HOMEWORK, + teacherId = teacherId, + subjectId = subjectId, + teamId = data.teamClass?.id ?: -1 ) data.eventList.add(eventObject) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaHomeworkGetAttachment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaHomeworkGetAttachment.kt new file mode 100644 index 00000000..a6d4b94b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaHomeworkGetAttachment.kt @@ -0,0 +1,25 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia + +import pl.szczodrzynski.edziennik.data.api.LIBRUS_SYNERGIA_HOMEWORK_ATTACHMENT_URL +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusSynergia +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.LibrusSandboxDownloadAttachment +import pl.szczodrzynski.edziennik.data.db.full.EventFull + +class LibrusSynergiaHomeworkGetAttachment( + override val data: DataLibrus, + val event: EventFull, + val attachmentId: Long, + val attachmentName: String, + val onSuccess: () -> Unit +) : LibrusSynergia(data, null) { + companion object { + const val TAG = "LibrusSynergiaHomeworkGetAttachment" + } + + init { + redirectUrlGet(TAG, "$LIBRUS_SYNERGIA_HOMEWORK_ATTACHMENT_URL/$attachmentId") { url -> + LibrusSandboxDownloadAttachment(data, url, event, attachmentId, attachmentName, onSuccess) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/Mobidziennik.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/Mobidziennik.kt index 5e08f6c4..ae5c6072 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/Mobidziennik.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/Mobidziennik.kt @@ -8,20 +8,17 @@ import com.google.gson.JsonObject import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikData -import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.MobidziennikWebGetAttachment -import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.MobidziennikWebGetMessage -import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.MobidziennikWebGetRecipientList -import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.MobidziennikWebSendMessage +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.* import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.firstlogin.MobidziennikFirstLogin import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.MobidziennikLogin import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.db.entity.LoginStore -import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull +import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.utils.Utils.d @@ -105,9 +102,9 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto override fun markAllAnnouncementsAsRead() {} override fun getAnnouncement(announcement: AnnouncementFull) {} - override fun getAttachment(message: Message, attachmentId: Long, attachmentName: String) { + override fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) { login(LOGIN_METHOD_MOBIDZIENNIK_WEB) { - MobidziennikWebGetAttachment(data, message, attachmentId, attachmentName) { + MobidziennikWebGetAttachment(data, owner, attachmentId, attachmentName) { completed() } } @@ -121,10 +118,19 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto } } + override fun getEvent(eventFull: EventFull) { + login(LOGIN_METHOD_MOBIDZIENNIK_WEB) { + MobidziennikWebGetHomework(data, eventFull) { + completed() + } + } + } + override fun firstLogin() { MobidziennikFirstLogin(data) { completed() } } override fun cancel() { d(TAG, "Cancelled") data.cancel() + callback.onCompleted() } private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/MobidziennikFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/MobidziennikFeatures.kt index d056fc4b..7396ac52 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/MobidziennikFeatures.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/MobidziennikFeatures.kt @@ -17,6 +17,7 @@ const val ENDPOINT_MOBIDZIENNIK_WEB_NOTICES = 2040 const val ENDPOINT_MOBIDZIENNIK_WEB_ATTENDANCE = 2050 const val ENDPOINT_MOBIDZIENNIK_WEB_MANUALS = 2100 const val ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL = 2200 +const val ENDPOINT_MOBIDZIENNIK_WEB_HOMEWORK = 2300 // not used as an endpoint const val ENDPOINT_MOBIDZIENNIK_API2_MAIN = 3000 val MobidziennikFeatures = listOf( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiEvents.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiEvents.kt index f5cb912b..02fbb67e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiEvents.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiEvents.kt @@ -30,7 +30,7 @@ class MobidziennikApiEvents(val data: DataMobidziennik, rows: List) { val teacherId = cols[1].toLong() val subjectId = cols[3].toLong() var type = Event.TYPE_DEFAULT - var topic = cols[5] + var topic = cols[5].trim() Regexes.MOBIDZIENNIK_EVENT_TYPE.find(topic)?.let { val typeText = it.groupValues[1] when (typeText) { @@ -51,17 +51,16 @@ class MobidziennikApiEvents(val data: DataMobidziennik, rows: List) { val eventObject = Event( - data.profileId, - id, - eventDate, - startTime, - topic, - -1, - type, - false, - teacherId, - subjectId, - teamId) + profileId = data.profileId, + id = id, + date = eventDate, + time = startTime, + topic = topic, + color = null, + type = type, + teacherId = teacherId, + subjectId = subjectId, + teamId = teamId) data.eventList.add(eventObject) data.metadataList.add( @@ -76,6 +75,8 @@ class MobidziennikApiEvents(val data: DataMobidziennik, rows: List) { } } - data.toRemove.add(DataRemoveModel.Events.futureExceptType(Event.TYPE_HOMEWORK)) + data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_DEFAULT)) + data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_EXAM)) + data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_SHORT_QUIZ)) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiHomework.kt index 4c5b4e03..4623a26d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiHomework.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiHomework.kt @@ -4,6 +4,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api +import android.text.Html import androidx.core.util.contains import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel @@ -25,22 +26,21 @@ class MobidziennikApiHomework(val data: DataMobidziennik, rows: List) { val id = cols[0].toLong() val teacherId = cols[7].toLong() val subjectId = cols[6].toLong() - val topic = cols[1] + val topic = Html.fromHtml(cols[1])?.toString()?.trim() ?: "" val eventDate = Date.fromYmd(cols[2]) val startTime = Time.fromYmdHm(cols[3]) val eventObject = Event( - data.profileId, - id, - eventDate, - startTime, - topic, - -1, - Event.TYPE_HOMEWORK, - false, - teacherId, - subjectId, - teamId) + profileId = data.profileId, + id = id, + date = eventDate, + time = startTime, + topic = topic, + color = null, + type = Event.TYPE_HOMEWORK, + teacherId = teacherId, + subjectId = subjectId, + teamId = teamId) data.eventList.add(eventObject) data.metadataList.add( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTeams.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTeams.kt index 8fd5677b..6440b04e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTeams.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTeams.kt @@ -44,7 +44,7 @@ class MobidziennikApiTeams(val data: DataMobidziennik, tableTeams: List? val studentId = cols[1].toInt() val teamId = cols[2].toLong() - val studentNumber = cols[4].toInt() + val studentNumber = cols[4].toIntOrNull() ?: -1 if (studentId != data.studentId) continue diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebCalendar.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebCalendar.kt index 9664a68b..c1749d76 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebCalendar.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebCalendar.kt @@ -61,26 +61,25 @@ class MobidziennikWebCalendar(override val data: DataMobidziennik, val title = event.getString("title") val comment = event.getString("comment") - var topic = title + var topic = title ?: "" if (title != comment) { topic += "\n" + comment } if (id == -1L) { - id = crc16(topic?.toByteArray()).toLong() + id = crc16(topic.toByteArray()).toLong() } val eventObject = Event( - profileId, - id, - eventDate, null, - topic, - -1, - eventType, - false, - -1, - -1, - data.teamClass?.id ?: -1 + profileId = profileId, + id = id, + date = eventDate, time = null, + topic = topic, + color = null, + type = eventType, + teacherId = -1, + subjectId = -1, + teamId = data.teamClass?.id ?: -1 ) data.eventList.add(eventObject) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetAttachment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetAttachment.kt index 80803441..1bb660c1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetAttachment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetAttachment.kt @@ -8,12 +8,14 @@ import org.greenrobot.eventbus.EventBus import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent +import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.utils.Utils +import pl.szczodrzynski.edziennik.utils.models.Date import java.io.File class MobidziennikWebGetAttachment(override val data: DataMobidziennik, - val message: Message, + val owner: Any, val attachmentId: Long, val attachmentName: String, val onSuccess: () -> Unit @@ -25,25 +27,40 @@ class MobidziennikWebGetAttachment(override val data: DataMobidziennik, init { val targetFile = File(Utils.getStorageDir(), attachmentName) - val typeUrl = if (message.type == Message.TYPE_SENT) - "wiadwyslana" - else - "wiadodebrana" + val typeUrl = when (owner) { + is Message -> if (owner.type == Message.TYPE_SENT) + "dziennik/wiadwyslana?id=" + else + "dziennik/wiadodebrana?id=" - webGetFile(TAG, "/dziennik/$typeUrl/?id=${message.id}&zalacznik=$attachmentId", targetFile, { file -> + is Event -> if (owner.date >= Date.getToday()) + "mobile/zadaniadomowe?id_zadania=" + else + "mobile/zadaniadomowearchiwalne?id_zadania=" + + else -> "" + } + + val ownerId = when (owner) { + is Message -> owner.id + is Event -> owner.id + else -> -1 + } + + webGetFile(TAG, "/$typeUrl${ownerId}&zalacznik=$attachmentId", targetFile, { file -> val event = AttachmentGetEvent( profileId, - message.id, + owner, attachmentId, AttachmentGetEvent.TYPE_FINISHED, file.absolutePath ) - val attachmentDataFile = File(Utils.getStorageDir(), ".${profileId}_${event.messageId}_${event.attachmentId}") + val attachmentDataFile = File(Utils.getStorageDir(), ".${profileId}_${event.ownerId}_${event.attachmentId}") Utils.writeStringToFile(attachmentDataFile, event.fileName) - EventBus.getDefault().post(event) + EventBus.getDefault().postSticky(event) onSuccess() @@ -51,13 +68,13 @@ class MobidziennikWebGetAttachment(override val data: DataMobidziennik, // TODO make use of bytesTotal val event = AttachmentGetEvent( profileId, - message.id, + owner, attachmentId, AttachmentGetEvent.TYPE_PROGRESS, bytesWritten = written ) - EventBus.getDefault().post(event) + EventBus.getDefault().postSticky(event) } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetHomework.kt new file mode 100644 index 00000000..ef4e8e5d --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetHomework.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-31. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web + +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.data.api.Regexes +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb +import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent +import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.utils.models.Date + +class MobidziennikWebGetHomework(override val data: DataMobidziennik, + val event: EventFull, + val onSuccess: () -> Unit +) : MobidziennikWeb(data, null) { + companion object { + private const val TAG = "MobidziennikWebHomework" + } + + init { + val endpoint = if (event.date >= Date.getToday()) + "zadaniadomowe" + else + "zadaniadomowearchiwalne" + + webGet(TAG, "/mobile/$endpoint") { text -> + MobidziennikLuckyNumberExtractor(data, text) + + Regexes.MOBIDZIENNIK_HOMEWORK_ROW.findAll(text).forEach { homeworkMatch -> + val tableRow = homeworkMatch[1].ifBlank { return@forEach } + + val id = Regexes.MOBIDZIENNIK_HOMEWORK_ID.find(tableRow)?.get(1)?.toLongOrNull() ?: return@forEach + if (event.id != id) + return@forEach + + event.attachmentIds = mutableListOf() + event.attachmentNames = mutableListOf() + Regexes.MOBIDZIENNIK_HOMEWORK_ATTACHMENT.findAll(tableRow).onEach { + event.attachmentIds?.add(it[1].toLongOrNull() ?: return@onEach) + event.attachmentNames?.add(it[2]) + } + + event.homeworkBody = "" + } + + data.eventList.add(event) + data.eventListReplace = true + + EventBus.getDefault().postSticky(EventGetEvent(event)) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetMessage.kt index 5b462e57..1c44a219 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetMessage.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetMessage.kt @@ -11,7 +11,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidzienn import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent import pl.szczodrzynski.edziennik.data.db.entity.Message -import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_RECEIVED +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.data.db.full.MessageRecipientFull @@ -61,19 +61,17 @@ class MobidziennikWebGetMessage(override val data: DataMobidziennik, } val recipient = MessageRecipientFull( - profileId, - -1, - -1, - readDate, - message.id + profileId = profileId, + id = -1, + messageId = message.id, + readDate = readDate ) recipient.fullName = profile?.accountName ?: profile?.studentNameLong ?: "" messageRecipientList.add(recipient) } else { - message.senderId = -1 - message.senderReplyId = -1 + message.senderId = null content.select("table.spis tr:has(td)")?.forEach { recipientEl -> val senderEl = recipientEl.select("td:eq(0)").first() @@ -100,11 +98,10 @@ class MobidziennikWebGetMessage(override val data: DataMobidziennik, } val recipient = MessageRecipientFull( - profileId, - receiverId, - -1, - readDate, - message.id + profileId = profileId, + id = receiverId, + messageId = message.id, + readDate = readDate ) recipient.fullName = teacher?.fullName ?: "?" @@ -149,7 +146,9 @@ class MobidziennikWebGetMessage(override val data: DataMobidziennik, message.recipients = messageRecipientList data.messageRecipientList.addAll(messageRecipientList) + data.messageList.add(message) + data.messageListReplace = true EventBus.getDefault().postSticky(MessageGetEvent(message)) onSuccess() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebHomework.kt new file mode 100644 index 00000000..944b21ea --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebHomework.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-31. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web + +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.data.api.Regexes +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBIDZIENNIK_WEB_HOMEWORK +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb +import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent +import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.get + +class MobidziennikWebHomework(override val data: DataMobidziennik, + override val lastSync: Long?, + val type: Int = TYPE_CURRENT, + val event: EventFull, + val onSuccess: (endpointId: Int) -> Unit +) : MobidziennikWeb(data, lastSync) { + companion object { + private const val TAG = "MobidziennikWebHomework" + const val TYPE_CURRENT = 0 + const val TYPE_PAST = 1 + } + + init { + val endpoint = when (type) { + TYPE_PAST -> "zadaniadomowearchiwalne" + else -> "zadaniadomowe" + } + webGet(TAG, "/mobile/$endpoint") { text -> + MobidziennikLuckyNumberExtractor(data, text) + + Regexes.MOBIDZIENNIK_HOMEWORK_ROW.findAll(text).forEach { homeworkMatch -> + val tableRow = homeworkMatch[1].ifBlank { return@forEach } + + /*val items = Regexes.MOBIDZIENNIK_HOMEWORK_ITEM.findAll(tableRow).map { match -> + match[1] to match[2].fixWhiteSpaces() + }.toList()*/ + + val id = Regexes.MOBIDZIENNIK_HOMEWORK_ID.find(tableRow)?.get(1)?.toLongOrNull() ?: return@forEach + if (event.id != id) + return@forEach + + //val homeworkBody = Regexes.MOBIDZIENNIK_HOMEWORK_BODY.find(tableRow)?.get(1) ?: "" + + event.attachmentIds = mutableListOf() + event.attachmentNames = mutableListOf() + Regexes.MOBIDZIENNIK_HOMEWORK_ATTACHMENT.findAll(tableRow).forEach { + event.attachmentIds?.add(it[1].toLongOrNull() ?: return@forEach) + event.attachmentNames?.add(it[2]) + } + + event.homeworkBody = "" + } + + //data.eventList.add(eventObject) + //data.metadataList.add( + // Metadata( + // profileId, + // Metadata.TYPE_EVENT, + // eventObject.id, + // profile?.empty ?: false, + // profile?.empty ?: false, + // System.currentTimeMillis() /* no addedDate here though */ + // )) + + // not used as an endpoint + //data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_HOMEWORK, SYNC_ALWAYS) + data.eventList.add(event) + data.eventListReplace = true + + EventBus.getDefault().postSticky(EventGetEvent(event)) + onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_HOMEWORK) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesAll.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesAll.kt index 61c9222e..c0373c54 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesAll.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesAll.kt @@ -10,8 +10,8 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidzienn import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb import pl.szczodrzynski.edziennik.data.db.entity.Message -import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_RECEIVED -import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_SENT +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.fixName @@ -54,12 +54,12 @@ class MobidziennikWebMessagesAll(override val data: DataMobidziennik, type = TYPE_SENT val senderEl = item.select("td:eq(3) div").first() - var senderId: Long = -1 + var senderId: Long? = null if (type == TYPE_RECEIVED) { // search sender teacher val senderName = senderEl.text().fixName() - senderId = data.teacherList.singleOrNull { it.fullNameLastFirst == senderName }?.id ?: -1 + senderId = data.teacherList.singleOrNull { it.fullNameLastFirst == senderName }?.id data.messageRecipientList.add(MessageRecipient(profileId, -1, id)) } else { // TYPE_SENT, so multiple recipients possible @@ -72,16 +72,15 @@ class MobidziennikWebMessagesAll(override val data: DataMobidziennik, } val message = Message( - profileId, - id, - subject, - null, - type, - senderId, - -1 + profileId = profileId, + id = id, + type = type, + subject = subject, + body = null, + senderId = senderId ) - data.messageIgnoreList.add(message) + data.messageList.add(message) data.metadataList.add(Metadata(profileId, Metadata.TYPE_MESSAGE, message.id, true, true, addedDate)) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesInbox.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesInbox.kt index ee5cbd24..385ea71e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesInbox.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesInbox.kt @@ -52,25 +52,24 @@ class MobidziennikWebMessagesInbox(override val data: DataMobidziennik, val senderEl = item.select("td:eq(2)").first() val senderName = senderEl.ownText().fixName() - val senderId = data.teacherList.singleOrNull { it.fullNameLastFirst == senderName }?.id ?: -1 + val senderId = data.teacherList.singleOrNull { it.fullNameLastFirst == senderName }?.id data.messageRecipientIgnoreList.add(MessageRecipient(profileId, -1, id)) val isRead = item.select("td:eq(3) span").first().hasClass("wiadomosc_przeczytana") val message = Message( - profileId, - id, - subject, - null, - Message.TYPE_RECEIVED, - senderId, - -1 + profileId = profileId, + id = id, + type = Message.TYPE_RECEIVED, + subject = subject, + body = null, + senderId = senderId ) if (hasAttachments) - message.setHasAttachments() + message.hasAttachments = true - data.messageIgnoreList.add(message) + data.messageList.add(message) data.setSeenMetadataList.add( Metadata( profileId, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesSent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesSent.kt index 8e7123c3..918816c8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesSent.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesSent.kt @@ -73,19 +73,18 @@ class MobidziennikWebMessagesSent(override val data: DataMobidziennik, val addedDate = Date.fromIsoHm(addedDateEl.text()) val message = Message( - profileId, - id, - subject, - null, - Message.TYPE_SENT, - -1, - -1 + profileId = profileId, + id = id, + type = Message.TYPE_SENT, + subject = subject, + body = null, + senderId = null ) if (hasAttachments) - message.setHasAttachments() + message.hasAttachments = true - data.messageIgnoreList.add(message) + data.messageList.add(message) data.setSeenMetadataList.add( Metadata( profileId, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebSendMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebSendMessage.kt index 074fb83e..d0cee4dc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebSendMessage.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebSendMessage.kt @@ -43,7 +43,7 @@ class MobidziennikWebSendMessage(override val data: DataMobidziennik, // TODO create MobidziennikWebMessagesSent and replace this MobidziennikWebMessagesAll(data, null) { - val message = data.messageIgnoreList.firstOrNull { it.type == Message.TYPE_SENT && it.subject == subject } + val message = data.messageList.firstOrNull { it.type == Message.TYPE_SENT && it.subject == subject } val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == message?.id } val event = MessageSentEvent(data.profileId, message, metadata?.addedDate) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/Template.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/Template.kt index 9048bbcf..5843f927 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/Template.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/Template.kt @@ -16,10 +16,10 @@ import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.api.prepare import pl.szczodrzynski.edziennik.data.api.templateLoginMethods import pl.szczodrzynski.edziennik.data.db.entity.LoginStore -import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull +import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.utils.Utils.d @@ -79,7 +79,7 @@ class Template(val app: App, val profile: Profile?, val loginStore: LoginStore, } - override fun getAttachment(message: Message, attachmentId: Long, attachmentName: String) { + override fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) { } @@ -87,6 +87,10 @@ class Template(val app: App, val profile: Profile?, val loginStore: LoginStore, } + override fun getEvent(eventFull: EventFull) { + + } + override fun firstLogin() { TemplateFirstLogin(data) { completed() @@ -96,6 +100,7 @@ class Template(val app: App, val profile: Profile?, val loginStore: LoginStore, override fun cancel() { d(TAG, "Cancelled") data.cancel() + callback.onCompleted() } private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/Vulcan.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/Vulcan.kt index 4e6def85..511b6591 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/Vulcan.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/Vulcan.kt @@ -19,10 +19,10 @@ import pl.szczodrzynski.edziennik.data.api.prepare import pl.szczodrzynski.edziennik.data.api.prepareFor import pl.szczodrzynski.edziennik.data.api.vulcanLoginMethods import pl.szczodrzynski.edziennik.data.db.entity.LoginStore -import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull +import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.utils.Utils.d @@ -101,26 +101,17 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va } } - override fun markAllAnnouncementsAsRead() { - - } - - override fun getAnnouncement(announcement: AnnouncementFull) { - - } - - override fun getAttachment(message: Message, attachmentId: Long, attachmentName: String) { - - } - - override fun getRecipientList() { - - } + override fun markAllAnnouncementsAsRead() {} + override fun getAnnouncement(announcement: AnnouncementFull) {} + override fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) {} + override fun getRecipientList() {} + override fun getEvent(eventFull: EventFull) {} override fun firstLogin() { VulcanFirstLogin(data) { completed() } } override fun cancel() { d(TAG, "Cancelled") data.cancel() + callback.onCompleted() } private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiEvents.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiEvents.kt index c55dac5d..4ba7c460 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiEvents.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiEvents.kt @@ -57,7 +57,7 @@ class VulcanApiEvents(override val data: DataVulcan, val eventDate = Date.fromY_m_d(event.getString("DataTekst") ?: return@forEach) val subjectId = event.getLong("IdPrzedmiot") ?: -1 val teacherId = event.getLong("IdPracownik") ?: -1 - val topic = event.getString("Opis") ?: "" + val topic = event.getString("Opis")?.trim() ?: "" val lessonList = data.db.timetableDao().getForDateNow(profileId, eventDate) val startTime = lessonList.firstOrNull { it.subjectId == subjectId }?.startTime @@ -72,17 +72,16 @@ class VulcanApiEvents(override val data: DataVulcan, val teamId = event.getLong("IdOddzial") ?: data.teamClass?.id ?: -1 val eventObject = Event( - profileId, - id, - eventDate, - startTime, - topic, - -1, - type, - false, - teacherId, - subjectId, - teamId + profileId = profileId, + id = id, + date = eventDate, + time = startTime, + topic = topic, + color = null, + type = type, + teacherId = teacherId, + subjectId = subjectId, + teamId = teamId ) data.eventList.add(eventObject) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesChangeStatus.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesChangeStatus.kt index 3a72c963..5e226849 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesChangeStatus.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesChangeStatus.kt @@ -9,7 +9,7 @@ import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_MESSAGES_CHANGE_S import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent -import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_SENT +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.full.MessageFull diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesInbox.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesInbox.kt index e78b157e..388a97ad 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesInbox.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesInbox.kt @@ -10,7 +10,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_API_MESSAGES_INBOX import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi import pl.szczodrzynski.edziennik.data.db.entity.* -import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_RECEIVED +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.models.Date import kotlin.text.replace @@ -44,8 +44,7 @@ class VulcanApiMessagesInbox(override val data: DataVulcan, val body = message.getString("Tresc") ?: "" val senderLoginId = message.getString("NadawcaId") ?: return@forEach - val senderId = data.teacherList - .singleOrNull { it.loginId == senderLoginId }?.id ?: { + val senderId = data.teacherList.singleOrNull { it.loginId == senderLoginId }?.id ?: { val senderName = message.getString("Nadawca") ?: "" @@ -60,7 +59,7 @@ class VulcanApiMessagesInbox(override val data: DataVulcan, data.teacherList.put(teacherObject.id, teacherObject) teacherObject.id } - }.invoke() ?: -1 + }.invoke() val sentDate = message.getLong("DataWyslaniaUnixEpoch")?.let { it * 1000 } ?: -1 @@ -68,13 +67,12 @@ class VulcanApiMessagesInbox(override val data: DataVulcan, ?: -1 val messageObject = Message( - profileId, - id, - subject, - body.replace("\n", "
"), - TYPE_RECEIVED, - senderId, - -1 + profileId = profileId, + id = id, + type = TYPE_RECEIVED, + subject = subject, + body = body.replace("\n", "
"), + senderId = senderId ) val messageRecipientObject = MessageRecipient( @@ -85,7 +83,7 @@ class VulcanApiMessagesInbox(override val data: DataVulcan, id ) - data.messageIgnoreList.add(messageObject) + data.messageList.add(messageObject) data.messageRecipientList.add(messageRecipientObject) data.setSeenMetadataList.add(Metadata( profileId, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesSent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesSent.kt index 7437aab4..d190c00f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesSent.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesSent.kt @@ -11,7 +11,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_API_MESSAGES_SENT import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi import pl.szczodrzynski.edziennik.data.db.entity.Message -import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_SENT +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Teacher @@ -92,16 +92,15 @@ class VulcanApiMessagesSent(override val data: DataVulcan, } val messageObject = Message( - profileId, - id, - subject, - body.replace("\n", "
"), - TYPE_SENT, - -1, - -1 + profileId = profileId, + id = id, + type = TYPE_SENT, + subject = subject, + body = body.replace("\n", "
"), + senderId = null ) - data.messageIgnoreList.add(messageObject) + data.messageList.add(messageObject) data.setSeenMetadataList.add(Metadata( profileId, Metadata.TYPE_MESSAGE, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiSendMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiSendMessage.kt index c7f7b29e..b88ccdb6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiSendMessage.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiSendMessage.kt @@ -52,7 +52,7 @@ class VulcanApiSendMessage(override val data: DataVulcan, } VulcanApiMessagesSent(data, null) { - val message = data.messageIgnoreList.firstOrNull { it.type == Message.TYPE_SENT && it.subject == subject } + val message = data.messageList.firstOrNull { it.type == Message.TYPE_SENT && it.subject == subject } val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == messageId } val event = MessageSentEvent(data.profileId, message, metadata?.addedDate) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/AttachmentGetEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/AttachmentGetEvent.kt index cbc85b7d..52415c56 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/AttachmentGetEvent.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/AttachmentGetEvent.kt @@ -4,11 +4,21 @@ package pl.szczodrzynski.edziennik.data.api.events -data class AttachmentGetEvent(val profileId: Int, val messageId: Long, val attachmentId: Long, +import pl.szczodrzynski.edziennik.data.db.entity.Event +import pl.szczodrzynski.edziennik.data.db.entity.Message + +data class AttachmentGetEvent(val profileId: Int, val owner: Any, val attachmentId: Long, var eventType: Int = TYPE_PROGRESS, val fileName: String? = null, val bytesWritten: Long = 0) { companion object { const val TYPE_PROGRESS = 0 const val TYPE_FINISHED = 1 } + + val ownerId + get() = when (owner) { + is Message -> owner.id + is Event -> owner.id + else -> -1 + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/EventGetEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/EventGetEvent.kt new file mode 100644 index 00000000..00cf1727 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/EventGetEvent.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-31. + */ + +package pl.szczodrzynski.edziennik.data.api.events + +import pl.szczodrzynski.edziennik.data.db.full.EventFull + +data class EventGetEvent(val event: EventFull) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikInterface.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikInterface.kt index e19a19ee..153bbe18 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikInterface.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikInterface.kt @@ -5,9 +5,9 @@ package pl.szczodrzynski.edziennik.data.api.interfaces import com.google.gson.JsonObject -import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull +import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.MessageFull interface EdziennikInterface { @@ -16,8 +16,9 @@ interface EdziennikInterface { fun sendMessage(recipients: List, subject: String, text: String) fun markAllAnnouncementsAsRead() fun getAnnouncement(announcement: AnnouncementFull) - fun getAttachment(message: Message, attachmentId: Long, attachmentName: String) + fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) fun getRecipientList() + fun getEvent(eventFull: EventFull) fun firstLogin() fun cancel() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt index b5086539..5e998425 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt @@ -86,6 +86,8 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt val gradeCategories = LongSparseArray() var teacherOnConflictStrategy = OnConflictStrategy.IGNORE + var eventListReplace = false + var messageListReplace = false val classrooms = LongSparseArray() val attendanceTypes = LongSparseArray() @@ -125,7 +127,6 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt val teacherAbsenceList = mutableListOf() val messageList = mutableListOf() - val messageIgnoreList = mutableListOf() val messageRecipientList = mutableListOf() val messageRecipientIgnoreList = mutableListOf() @@ -181,7 +182,6 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt luckyNumberList.clear() teacherAbsenceList.clear() messageList.clear() - messageIgnoreList.clear() messageRecipientList.clear() messageRecipientIgnoreList.clear() metadataList.clear() @@ -284,7 +284,10 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt db.gradeDao().addAll(gradeList) } if (eventList.isNotEmpty()) { - db.eventDao().addAll(eventList) + if (eventListReplace) + db.eventDao().replaceAll(eventList) + else + db.eventDao().upsertAll(eventList, removeNotKept = true) } if (noticeList.isNotEmpty()) { db.noticeDao().clear(profile.id) @@ -301,10 +304,12 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt if (teacherAbsenceList.isNotEmpty()) db.teacherAbsenceDao().addAll(teacherAbsenceList) - if (messageList.isNotEmpty()) - db.messageDao().addAll(messageList) - if (messageIgnoreList.isNotEmpty()) - db.messageDao().addAllIgnore(messageIgnoreList) + if (messageList.isNotEmpty()) { + if (messageListReplace) + db.messageDao().replaceAll(messageList) + else + db.messageDao().upsertAll(messageList, removeNotKept = false) // TODO dataRemoveModel for messages + } if (messageRecipientList.isNotEmpty()) db.messageRecipientDao().addAll(messageRecipientList) if (messageRecipientIgnoreList.isNotEmpty()) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/DataRemoveModel.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/DataRemoveModel.kt index ccccc00e..f247ee19 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/DataRemoveModel.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/DataRemoveModel.kt @@ -56,9 +56,9 @@ open class DataRemoveModel { } fun commit(profileId: Int, dao: EventDao) { - type?.let { dao.removeFutureWithType(profileId, Date.getToday(), it) } - exceptType?.let { dao.removeFutureExceptType(profileId, Date.getToday(), it) } - exceptTypes?.let { dao.removeFutureExceptTypes(profileId, Date.getToday(), it) } + type?.let { dao.dontKeepFutureWithType(profileId, Date.getToday(), it) } + exceptType?.let { dao.dontKeepFutureExceptType(profileId, Date.getToday(), it) } + exceptTypes?.let { dao.dontKeepFutureExceptTypes(profileId, Date.getToday(), it) } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt index eb70ab9a..bdd14f33 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt @@ -196,6 +196,11 @@ class SzkolnyApi(val app: App) : CoroutineScope { // skip blacklisted events if (event.id in blacklistedIds) return@forEach + + // force nullable non-negative colors + if (event.color == -1) + event.color = null + // create the event for every matching team and profile teams.filter { it.code == event.teamCode }.onEach { team -> val profile = profiles.firstOrNull { it.id == team.profileId } ?: return@onEach diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt index 446ae7ab..47f633f6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt @@ -46,6 +46,6 @@ object Signing { /*fun provideKey(param1: String, param2: Long): ByteArray {*/ fun pleaseStopRightNow(param1: String, param2: Long): ByteArray { - return "$param1.MTIzNDU2Nzg5MD86J6EdtN===.$param2".sha256() + return "$param1.MTIzNDU2Nzg5MD43hCWBBS===.$param2".sha256() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/request/EventShareRequest.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/request/EventShareRequest.kt index 624b8b0f..1977d1b2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/request/EventShareRequest.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/request/EventShareRequest.kt @@ -12,7 +12,8 @@ data class EventShareRequest ( val action: String = "event", - val sharedByName: String, + /* If null, the server shows an error */ + val sharedByName: String?, val shareTeamCode: String? = null, val unshareTeamCode: String? = null, val requesterName: String? = null, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/AppSync.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/AppSync.kt index 56d5132a..63d4ec88 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/AppSync.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/AppSync.kt @@ -34,7 +34,7 @@ class AppSync(val app: App, val notifications: MutableList, val pr if (events.isNotEmpty()) { val today = Date.getToday() app.db.metadataDao().addAllIgnore(events.map { event -> - val isPast = event.eventDate < today + val isPast = event.date < today Metadata( event.profileId, Metadata.TYPE_EVENT, @@ -44,7 +44,7 @@ class AppSync(val app: App, val notifications: MutableList, val pr event.addedDate ) }) - return app.db.eventDao().addAll(events).size + return app.db.eventDao().upsertAll(events).size } return 0; } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/Notifications.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/Notifications.kt index 7c049f5a..39b68499 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/Notifications.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/Notifications.kt @@ -34,6 +34,7 @@ class Notifications(val app: App, val notifications: MutableList, announcementNotifications() messageNotifications() luckyNumberNotifications() + teacherAbsenceNotifications() } private fun timetableNotifications() { @@ -58,7 +59,7 @@ class Notifications(val app: App, val notifications: MutableList, } private fun eventNotifications() { - for (event in app.db.eventDao().notNotifiedNow.filter { it.eventDate >= today }) { + for (event in app.db.eventDao().getNotNotifiedNow().filter { it.date >= today }) { val text = if (event.type == Event.TYPE_HOMEWORK) app.getString( if (event.subjectLongName.isNullOrEmpty()) @@ -66,7 +67,7 @@ class Notifications(val app: App, val notifications: MutableList, else R.string.notification_homework_format, event.subjectLongName, - event.eventDate.formattedString + event.date.formattedString ) else app.getString( @@ -74,8 +75,8 @@ class Notifications(val app: App, val notifications: MutableList, R.string.notification_event_no_subject_format else R.string.notification_event_format, - event.typeName, - event.eventDate.formattedString, + event.typeName ?: "wydarzenie", + event.date.formattedString, event.subjectLongName ) val type = if (event.type == Event.TYPE_HOMEWORK) Notification.TYPE_NEW_HOMEWORK else Notification.TYPE_NEW_EVENT @@ -88,17 +89,17 @@ class Notifications(val app: App, val notifications: MutableList, profileName = profiles.singleOrNull { it.id == event.profileId }?.name, viewId = if (event.type == Event.TYPE_HOMEWORK) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA, addedDate = event.addedDate - ).addExtra("eventId", event.id).addExtra("eventDate", event.eventDate.value.toLong()) + ).addExtra("eventId", event.id).addExtra("eventDate", event.date.value.toLong()) } } fun sharedEventNotifications() { - for (event in app.db.eventDao().notNotifiedNow.filter { it.eventDate >= today && it.sharedBy != null }) { + for (event in app.db.eventDao().getNotNotifiedNow().filter { it.date >= today && it.sharedBy != null }) { val text = app.getString( R.string.notification_shared_event_format, event.sharedByName, event.typeName ?: "wydarzenie", - event.eventDate.formattedString, + event.date.formattedString, event.topic ) val type = if (event.type == Event.TYPE_HOMEWORK) Notification.TYPE_NEW_HOMEWORK else Notification.TYPE_NEW_EVENT @@ -111,7 +112,7 @@ class Notifications(val app: App, val notifications: MutableList, profileName = profiles.singleOrNull { it.id == event.profileId }?.name, viewId = if (event.type == Event.TYPE_HOMEWORK) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA, addedDate = event.addedDate - ).addExtra("eventId", event.id).addExtra("eventDate", event.eventDate.value.toLong()) + ).addExtra("eventId", event.id).addExtra("eventDate", event.date.value.toLong()) } } @@ -226,10 +227,10 @@ class Notifications(val app: App, val notifications: MutableList, } private fun messageNotifications() { - for (message in app.db.messageDao().receivedNotNotifiedNow) { + for (message in app.db.messageDao().getNotNotifiedNow()) { val text = app.getString( R.string.notification_message_format, - message.senderFullName, + message.senderName, message.subject ) notifications += Notification( @@ -274,4 +275,23 @@ class Notifications(val app: App, val notifications: MutableList, ) } } + + private fun teacherAbsenceNotifications() { + for (teacherAbsence in app.db.teacherAbsenceDao().getNotNotifiedNow()) { + val message = app.getString( + R.string.notification_teacher_absence_new_format, + teacherAbsence.teacherFullName + ) + notifications += Notification( + id = Notification.buildId(teacherAbsence.profileId, Notification.TYPE_TEACHER_ABSENCE, teacherAbsence.id), + title = app.getNotificationTitle(Notification.TYPE_TEACHER_ABSENCE), + text = message, + type = Notification.TYPE_TEACHER_ABSENCE, + profileId = teacherAbsence.profileId, + profileName = profiles.singleOrNull { it.id == teacherAbsence.profileId }?.name, + viewId = MainActivity.DRAWER_ITEM_AGENDA, + addedDate = teacherAbsence.addedDate + ).addExtra("eventDate", teacherAbsence.dateFrom.value.toLong()) + } + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt index 5b01d476..b57cc4d4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt @@ -43,7 +43,7 @@ import pl.szczodrzynski.edziennik.data.db.migration.* LibrusLesson::class, TimetableManual::class, Metadata::class -], version = 79) +], version = 85) @TypeConverters( ConverterTime::class, ConverterDate::class, @@ -164,7 +164,13 @@ abstract class AppDb : RoomDatabase() { Migration76(), Migration77(), Migration78(), - Migration79() + Migration79(), + Migration80(), + Migration81(), + Migration82(), + Migration83(), + Migration84(), + Migration85() ).allowMainThreadQueries().build() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/BaseDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/BaseDao.kt new file mode 100644 index 00000000..8c13dca9 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/BaseDao.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-28. + */ + +package pl.szczodrzynski.edziennik.data.db.dao + +import androidx.lifecycle.LiveData +import androidx.room.* +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery +import pl.szczodrzynski.edziennik.data.db.entity.Keepable + +@Dao +interface BaseDao { + @RawQuery + fun getRaw(query: SupportSQLiteQuery): LiveData> + fun getRaw(query: String) = getRaw(SimpleSQLiteQuery(query)) + @RawQuery + fun getRawNow(query: SupportSQLiteQuery): List + fun getRawNow(query: String) = getRawNow(SimpleSQLiteQuery(query)) + @RawQuery + fun getOneNow(query: SupportSQLiteQuery): F? + fun getOneNow(query: String) = getOneNow(SimpleSQLiteQuery(query)) + + @Query("DELETE FROM events WHERE keep = 0") + fun removeNotKept() + + /** + * INSERT an [item] into the database, + * ignoring any conflicts. + */ + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun add(item: T): Long + /** + * INSERT [items] into the database, + * ignoring any conflicts. + */ + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun addAll(items: List): LongArray + + /** + * REPLACE an [item] in the database, + * removing any conflicting rows. + */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun replace(item: T) + /** + * REPLACE [items] in the database, + * removing any conflicting rows. + */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun replaceAll(items: List) + + /** + * Selective UPDATE an [item] in the database. + * Do nothing if a matching item does not exist. + */ + fun update(item: T): Long + /** + * Selective UPDATE [items] in the database. + * Do nothing for those items which do not exist. + */ + fun updateAll(items: List): LongArray + + /** + * Remove all items from the database, + * that match the given [profileId]. + */ + fun clear(profileId: Int) + + /** + * INSERT an [item] into the database, + * doing a selective [update] on conflicts. + * @return the newly inserted item's ID or -1L if the item was updated instead + */ + @Transaction + fun upsert(item: T): Long { + val id = add(item) + if (id == -1L) update(item) + return id + } + /** + * INSERT [items] into the database, + * doing a selective [update] on conflicts. + * @return a [LongArray] of IDs of newly inserted items or -1L if the item existed before + */ + @Transaction + fun upsertAll(items: List, removeNotKept: Boolean = false): LongArray { + val insertResult = addAll(items) + val updateList = mutableListOf() + + insertResult.forEachIndexed { index, result -> + if (result == -1L) updateList.add(items[index]) + } + + if (updateList.isNotEmpty()) updateAll(items) + if (removeNotKept) removeNotKept() + return insertResult + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventDao.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventDao.java deleted file mode 100644 index 083b499a..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventDao.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2020-1-6 - */ - -package pl.szczodrzynski.edziennik.data.db.dao; - -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.lifecycle.LiveData; -import androidx.room.Dao; -import androidx.room.Insert; -import androidx.room.OnConflictStrategy; -import androidx.room.Query; -import androidx.room.RawQuery; -import androidx.room.Transaction; -import androidx.sqlite.db.SimpleSQLiteQuery; -import androidx.sqlite.db.SupportSQLiteQuery; - -import java.util.List; - -import pl.szczodrzynski.edziennik.data.db.entity.Event; -import pl.szczodrzynski.edziennik.data.db.full.EventFull; -import pl.szczodrzynski.edziennik.utils.models.Date; -import pl.szczodrzynski.edziennik.utils.models.Time; - -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_EVENT; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_HOMEWORK; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_LESSON_CHANGE; - -@Dao -public abstract class EventDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - public abstract long add(Event event); - - @Insert(onConflict = OnConflictStrategy.REPLACE) - public abstract long[] addAll(List eventList); - - @Query("DELETE FROM events WHERE profileId = :profileId") - public abstract void clear(int profileId); - - @Query("DELETE FROM events WHERE profileId = :profileId AND eventId = :id") - public abstract void remove(int profileId, long id); - @Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = :thingType AND thingId = :thingId") - public abstract void removeMetadata(int profileId, int thingType, long thingId); - @Transaction - public void remove(int profileId, long type, long id) { - remove(profileId, id); - removeMetadata(profileId, type == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, id); - } - @Transaction - public void remove(Event event) { - remove(event.profileId, event.type, event.id); - } - @Transaction - public void remove(int profileId, Event event) { - remove(profileId, event.type, event.id); - } - @Query("DELETE FROM events WHERE teamId = :teamId AND eventId = :id") - public abstract void removeByTeamId(long teamId, long id); - - @RawQuery(observedEntities = {Event.class}) - abstract LiveData> getAll(SupportSQLiteQuery query); - public LiveData> getAll(int profileId, String filter, String limit) { - String query = "SELECT \n" + - "*, \n" + - "teachers.teacherName || ' ' || teachers.teacherSurname AS teacherFullName,\n" + - "eventTypes.eventTypeName AS typeName,\n" + - "eventTypes.eventTypeColor AS typeColor\n" + - "FROM events\n" + - "LEFT JOIN subjects USING(profileId, subjectId)\n" + - "LEFT JOIN teachers USING(profileId, teacherId)\n" + - "LEFT JOIN teams USING(profileId, teamId)\n" + - "LEFT JOIN eventTypes USING(profileId, eventType)\n" + - "LEFT JOIN metadata ON eventId = thingId AND (thingType = " + TYPE_EVENT + " OR thingType = " + TYPE_HOMEWORK + ") AND metadata.profileId = "+profileId+"\n" + - "WHERE events.profileId = "+profileId+" AND events.eventBlacklisted = 0 AND "+filter+"\n" + - "GROUP BY eventId\n" + - "ORDER BY eventDate, eventStartTime ASC "+limit; - Log.d("DB", query); - return getAll(new SimpleSQLiteQuery(query)); - } - public LiveData> getAll(int profileId) { - return getAll(profileId, "1", ""); - } - public List getAllNow(int profileId) { - return getAllNow(profileId, "1"); - } - public LiveData> getAllWhere(int profileId, String filter) { - return getAll(profileId, filter, ""); - } - public LiveData> getAllByType(int profileId, long type, String filter) { - return getAll(profileId, "eventType = "+type+" AND "+filter, ""); - } - public LiveData> getAllByDate(int profileId, @NonNull Date date) { - return getAll(profileId, "eventDate = '"+date.getStringY_m_d()+"'", ""); - } - public List getAllByDateNow(int profileId, @NonNull Date date) { - return getAllNow(profileId, "eventDate = '"+date.getStringY_m_d()+"'"); - } - public LiveData> getAllByDateTime(int profileId, @NonNull Date date, Time time) { - if (time == null) - return getAllByDate(profileId, date); - return getAll(profileId, "eventDate = '"+date.getStringY_m_d()+"' AND eventStartTime = '"+time.getStringValue()+"'", ""); - } - public LiveData> getAllNearest(int profileId, @NonNull Date today, int limit) { - return getAll(profileId, "eventDate >= '"+today.getStringY_m_d()+"'", "LIMIT "+limit); - } - - @RawQuery - abstract List getAllNow(SupportSQLiteQuery query); - public List getAllNow(int profileId, String filter) { - return getAllNow(new SimpleSQLiteQuery("SELECT \n" + - "*, \n" + - "teachers.teacherName || ' ' || teachers.teacherSurname AS teacherFullName,\n" + - "eventTypes.eventTypeName AS typeName,\n" + - "eventTypes.eventTypeColor AS typeColor\n" + - "FROM events \n" + - "LEFT JOIN subjects USING(profileId, subjectId)\n" + - "LEFT JOIN teachers USING(profileId, teacherId)\n" + - "LEFT JOIN teams USING(profileId, teamId)\n" + - "LEFT JOIN eventTypes USING(profileId, eventType)\n" + - "LEFT JOIN metadata ON eventId = thingId AND (thingType = " + TYPE_EVENT + " OR thingType = " + TYPE_HOMEWORK + ") AND metadata.profileId = "+profileId+"\n" + - "WHERE events.profileId = "+profileId+" AND events.eventBlacklisted = 0 AND "+filter+"\n" + - "GROUP BY eventId\n" + - "ORDER BY eventStartTime, addedDate ASC")); - } - public List getNotNotifiedNow(int profileId) { - return getAllNow(profileId, "notified = 0"); - } - - @Query("SELECT eventId FROM events WHERE profileId = :profileId AND eventBlacklisted = 1") - public abstract List getBlacklistedIds(int profileId); - @Query("SELECT eventId FROM events WHERE eventBlacklisted = 1") - public abstract List getBlacklistedIds(); - - @Query("SELECT " + - "*, " + - "eventTypes.eventTypeName AS typeName, " + - "eventTypes.eventTypeColor AS typeColor " + - "FROM events " + - "LEFT JOIN subjects USING(profileId, subjectId) " + - "LEFT JOIN eventTypes USING(profileId, eventType) " + - "LEFT JOIN metadata ON eventId = thingId AND (thingType = " + TYPE_EVENT + " OR thingType = " + TYPE_HOMEWORK + ") AND metadata.profileId = events.profileId " + - "WHERE events.eventBlacklisted = 0 AND notified = 0 " + - "GROUP BY eventId " + - "ORDER BY addedDate ASC") - public abstract List getNotNotifiedNow(); - - public EventFull getByIdNow(int profileId, long eventId) { - List eventList = getAllNow(profileId, "eventId = "+eventId); - return eventList.size() == 0 ? null : eventList.get(0); - } - - @Query("UPDATE events SET eventAddedManually = 1 WHERE profileId = :profileId AND eventDate < :date") - public abstract void convertOlderToManual(int profileId, Date date); - - @Query("DELETE FROM events WHERE profileId = :profileId AND eventAddedManually = 0") - public abstract void removeNotManual(int profileId); - - @RawQuery - abstract long removeFuture(SupportSQLiteQuery query); - @Transaction - public void removeFuture(int profileId, Date todayDate, String filter) { - removeFuture(new SimpleSQLiteQuery("DELETE FROM events WHERE profileId = " + profileId - + " AND eventAddedManually = 0 AND eventDate >= '" + todayDate.getStringY_m_d() + "'" + - " AND " + filter)); - } - - @Query("DELETE FROM events WHERE profileId = :profileId AND eventAddedManually = 0 AND eventDate >= :todayDate AND eventType = :type") - public abstract void removeFutureWithType(int profileId, Date todayDate, long type); - - @Query("DELETE FROM events WHERE profileId = :profileId AND eventAddedManually = 0 AND eventDate >= :todayDate AND eventType != :exceptType") - public abstract void removeFutureExceptType(int profileId, Date todayDate, long exceptType); - - @Transaction - public void removeFutureExceptTypes(int profileId, Date todayDate, List exceptTypes) { - removeFuture(profileId, todayDate, "eventType NOT IN " + exceptTypes.toString().replace('[', '(').replace(']', ')')); - } - - @Query("UPDATE metadata SET seen = :seen WHERE profileId = :profileId AND (thingType = "+TYPE_EVENT+" OR thingType = "+TYPE_LESSON_CHANGE+" OR thingType = "+TYPE_HOMEWORK+") AND thingId IN (SELECT eventId FROM events WHERE profileId = :profileId AND eventDate = :date)") - public abstract void setSeenByDate(int profileId, Date date, boolean seen); - - @Query("UPDATE events SET eventBlacklisted = :blacklisted WHERE profileId = :profileId AND eventId = :eventId") - public abstract void setBlacklisted(int profileId, long eventId, boolean blacklisted); -} - diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventDao.kt new file mode 100644 index 00000000..b2017cc5 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventDao.kt @@ -0,0 +1,151 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2020-1-6 + */ +package pl.szczodrzynski.edziennik.data.db.dao + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Query +import androidx.room.RawQuery +import androidx.room.Transaction +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.annotation.SelectiveDao +import pl.szczodrzynski.edziennik.annotation.UpdateSelective +import pl.szczodrzynski.edziennik.data.db.AppDb +import pl.szczodrzynski.edziennik.data.db.entity.Event +import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time + +@Dao +@SelectiveDao(db = AppDb::class) +abstract class EventDao : BaseDao { + companion object { + private const val QUERY = """ + SELECT + *, + teachers.teacherName ||" "|| teachers.teacherSurname AS teacherName, + eventTypes.eventTypeName AS typeName, + eventTypes.eventTypeColor AS typeColor + FROM events + LEFT JOIN subjects USING(profileId, subjectId) + LEFT JOIN teachers USING(profileId, teacherId) + LEFT JOIN teams USING(profileId, teamId) + LEFT JOIN eventTypes USING(profileId, eventType) + LEFT JOIN metadata ON eventId = thingId AND (thingType = ${Metadata.TYPE_EVENT} OR thingType = ${Metadata.TYPE_HOMEWORK}) AND metadata.profileId = events.profileId + """ + + private const val ORDER_BY = """GROUP BY eventId ORDER BY eventDate, eventTime, addedDate ASC""" + private const val NOT_BLACKLISTED = """events.eventBlacklisted = 0""" + private const val NOT_DONE = """events.eventIsDone = 0""" + } + + private val selective by lazy { EventDaoSelective(App.db) } + + @RawQuery(observedEntities = [Event::class]) + abstract override fun getRaw(query: SupportSQLiteQuery): LiveData> + + // SELECTIVE UPDATE + @UpdateSelective(primaryKeys = ["profileId", "eventId"], skippedColumns = ["eventIsDone", "eventBlacklisted", "homeworkBody", "attachmentIds", "attachmentNames"]) + override fun update(item: Event) = selective.update(item) + override fun updateAll(items: List) = selective.updateAll(items) + + // CLEAR + @Query("DELETE FROM events WHERE profileId = :profileId") + abstract override fun clear(profileId: Int) + + // GET ALL - LIVE DATA + fun getAll(profileId: Int) = + getRaw("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId $ORDER_BY") + fun getAllByType(profileId: Int, type: Long, filter: String = "1") = + getRaw("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId AND eventType = $type AND $filter $ORDER_BY") + fun getAllByDate(profileId: Int, date: Date) = + getRaw("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId AND eventDate = '${date.stringY_m_d}' $ORDER_BY") + fun getAllByDateTime(profileId: Int, date: Date, time: Time) = + getRaw("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId AND eventDate = '${date.stringY_m_d}' AND eventTime = '${time.stringValue}' $ORDER_BY") + fun getNearestNotDone(profileId: Int, today: Date, limit: Int) = + getRaw("$QUERY WHERE $NOT_BLACKLISTED AND $NOT_DONE AND events.profileId = $profileId AND eventDate >= '${today.stringY_m_d}' $ORDER_BY LIMIT $limit") + + // GET ALL - NOW + fun getAllNow(profileId: Int) = + getRawNow("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId $ORDER_BY") + fun getNotNotifiedNow() = + getRawNow("$QUERY WHERE $NOT_BLACKLISTED AND notified = 0 $ORDER_BY") + fun getNotNotifiedNow(profileId: Int) = + getRawNow("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId AND notified = 0 $ORDER_BY") + fun getAllByDateNow(profileId: Int, date: Date) = + getRawNow("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId AND eventDate = '${date.stringY_m_d}' $ORDER_BY") + + // GET ONE - NOW + fun getByIdNow(profileId: Int, id: Long) = + getOneNow("$QUERY WHERE events.profileId = $profileId AND eventId = $id") + + + @Query("SELECT eventId FROM events WHERE profileId = :profileId AND eventBlacklisted = 1") + abstract fun getBlacklistedIds(profileId: Int): List + + @get:Query("SELECT eventId FROM events WHERE eventBlacklisted = 1") + abstract val blacklistedIds: List + + /*@Query("UPDATE events SET eventAddedManually = 1 WHERE profileId = :profileId AND eventDate < :date") + abstract fun convertOlderToManual(profileId: Int, date: Date?) + + @Query("DELETE FROM events WHERE teamId = :teamId AND eventId = :id") + abstract fun removeByTeamId(teamId: Long, id: Long) + + @Query("DELETE FROM events WHERE profileId = :profileId AND eventAddedManually = 0") + abstract fun removeNotManual(profileId: Int)*/ + + @RawQuery + abstract fun dontKeepFuture(query: SupportSQLiteQuery?): Long + + @Transaction + open fun dontKeepFuture(profileId: Int, todayDate: Date, filter: String) { + dontKeepFuture(SimpleSQLiteQuery("UPDATE events SET keep = 0 WHERE profileId = " + profileId + + " AND eventAddedManually = 0 AND eventDate >= '" + todayDate.stringY_m_d + "'" + + " AND " + filter)) + } + + @Query("UPDATE events SET keep = 0 WHERE profileId = :profileId AND eventAddedManually = 0 AND eventDate >= :todayDate AND eventType = :type") + abstract fun dontKeepFutureWithType(profileId: Int, todayDate: Date, type: Long) + + @Query("UPDATE events SET keep = 0 WHERE profileId = :profileId AND eventAddedManually = 0 AND eventDate >= :todayDate AND eventType != :exceptType") + abstract fun dontKeepFutureExceptType(profileId: Int, todayDate: Date, exceptType: Long) + + @Transaction + open fun dontKeepFutureExceptTypes(profileId: Int, todayDate: Date, exceptTypes: List) { + dontKeepFuture(profileId, todayDate, "eventType NOT IN " + exceptTypes.toString().replace('[', '(').replace(']', ')')) + } + + @Query("UPDATE metadata SET seen = :seen WHERE profileId = :profileId AND (thingType = " + Metadata.TYPE_EVENT + " OR thingType = " + Metadata.TYPE_LESSON_CHANGE + " OR thingType = " + Metadata.TYPE_HOMEWORK + ") AND thingId IN (SELECT eventId FROM events WHERE profileId = :profileId AND eventDate = :date)") + abstract fun setSeenByDate(profileId: Int, date: Date, seen: Boolean) + + @Query("UPDATE events SET eventBlacklisted = :blacklisted WHERE profileId = :profileId AND eventId = :eventId") + abstract fun setBlacklisted(profileId: Int, eventId: Long, blacklisted: Boolean) + + @Query("DELETE FROM events WHERE profileId = :profileId AND eventId = :id") + abstract fun remove(profileId: Int, id: Long) + + @Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = :thingType AND thingId = :thingId") + abstract fun removeMetadata(profileId: Int, thingType: Int, thingId: Long) + + @Transaction + open fun remove(profileId: Int, type: Long, id: Long) { + remove(profileId, id) + removeMetadata(profileId, if (type == Event.TYPE_HOMEWORK) Metadata.TYPE_HOMEWORK else Metadata.TYPE_EVENT, id) + } + + @Transaction + open fun remove(event: Event) { + remove(event.profileId, event.type, event.id) + } + + @Transaction + open fun remove(profileId: Int, event: Event) { + remove(profileId, event.type, event.id) + } + +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.java deleted file mode 100644 index 25366699..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2020-1-6 - */ - -package pl.szczodrzynski.edziennik.data.db.dao; - -import androidx.lifecycle.LiveData; -import androidx.room.Dao; -import androidx.room.Insert; -import androidx.room.OnConflictStrategy; -import androidx.room.Query; - -import java.util.List; - -import pl.szczodrzynski.edziennik.data.db.entity.EventType; - -@Dao -public interface EventTypeDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - void add(EventType gradeCategory); - - @Insert(onConflict = OnConflictStrategy.REPLACE) - void addAll(List gradeCategoryList); - - @Query("DELETE FROM eventTypes WHERE profileId = :profileId") - void clear(int profileId); - - @Query("SELECT * FROM eventTypes WHERE profileId = :profileId AND eventType = :typeId") - EventType getByIdNow(int profileId, long typeId); - - @Query("SELECT * FROM eventTypes WHERE profileId = :profileId") - LiveData> getAll(int profileId); - - @Query("SELECT * FROM eventTypes WHERE profileId = :profileId") - List getAllNow(int profileId); - - @Query("SELECT * FROM eventTypes") - List getAllNow(); -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.kt new file mode 100644 index 00000000..28be6d65 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2020-1-6 + */ +package pl.szczodrzynski.edziennik.data.db.dao + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_CLASS_EVENT +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_DEFAULT +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_ESSAY +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_EXAM +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_EXCURSION +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_HOMEWORK +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_INFORMATION +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_PROJECT +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_PT_MEETING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_READING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_SHORT_QUIZ +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_CLASS_EVENT +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_DEFAULT +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_ESSAY +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_EXAM +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_EXCURSION +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_HOMEWORK +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_INFORMATION +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_PROJECT +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_PT_MEETING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_READING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_SHORT_QUIZ +import pl.szczodrzynski.edziennik.data.db.entity.EventType + +@Dao +abstract class EventTypeDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun add(eventType: EventType) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun addAll(eventTypeList: List) + + @Query("DELETE FROM eventTypes WHERE profileId = :profileId") + abstract fun clear(profileId: Int) + + @Query("SELECT * FROM eventTypes WHERE profileId = :profileId AND eventType = :typeId") + abstract fun getByIdNow(profileId: Int, typeId: Long): EventType? + + @Query("SELECT * FROM eventTypes WHERE profileId = :profileId") + abstract fun getAll(profileId: Int): LiveData> + + @Query("SELECT * FROM eventTypes WHERE profileId = :profileId") + abstract fun getAllNow(profileId: Int): List + + @get:Query("SELECT * FROM eventTypes") + abstract val allNow: List + + fun addDefaultTypes(context: Context, profileId: Int): List { + val typeList = listOf( + EventType(profileId, TYPE_HOMEWORK, context.getString(R.string.event_type_homework), COLOR_HOMEWORK), + EventType(profileId, TYPE_DEFAULT, context.getString(R.string.event_other), COLOR_DEFAULT), + EventType(profileId, TYPE_EXAM, context.getString(R.string.event_exam), COLOR_EXAM), + EventType(profileId, TYPE_SHORT_QUIZ, context.getString(R.string.event_short_quiz), COLOR_SHORT_QUIZ), + EventType(profileId, TYPE_ESSAY, context.getString(R.string.event_essay), COLOR_ESSAY), + EventType(profileId, TYPE_PROJECT, context.getString(R.string.event_project), COLOR_PROJECT), + EventType(profileId, TYPE_PT_MEETING, context.getString(R.string.event_pt_meeting), COLOR_PT_MEETING), + EventType(profileId, TYPE_EXCURSION, context.getString(R.string.event_excursion), COLOR_EXCURSION), + EventType(profileId, TYPE_READING, context.getString(R.string.event_reading), COLOR_READING), + EventType(profileId, TYPE_CLASS_EVENT, context.getString(R.string.event_class_event), COLOR_CLASS_EVENT), + EventType(profileId, TYPE_INFORMATION, context.getString(R.string.event_information), COLOR_INFORMATION) + ) + addAll(typeList) + return typeList + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageDao.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageDao.java deleted file mode 100644 index bc3a46a5..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageDao.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2020-1-6 - */ - -package pl.szczodrzynski.edziennik.data.db.dao; - -import androidx.annotation.Nullable; -import androidx.lifecycle.LiveData; -import androidx.room.Dao; -import androidx.room.Insert; -import androidx.room.OnConflictStrategy; -import androidx.room.Query; -import androidx.room.RawQuery; -import androidx.sqlite.db.SimpleSQLiteQuery; -import androidx.sqlite.db.SupportSQLiteQuery; - -import java.util.List; - -import pl.szczodrzynski.edziennik.data.db.entity.Message; -import pl.szczodrzynski.edziennik.data.db.entity.Metadata; -import pl.szczodrzynski.edziennik.data.db.full.MessageFull; - -import static pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_DELETED; -import static pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_RECEIVED; -import static pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_SENT; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_MESSAGE; - -@Dao -public abstract class MessageDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - public abstract long add(Message message); - - @Insert(onConflict = OnConflictStrategy.REPLACE) - public abstract void addAll(List messageList); - - @Insert(onConflict = OnConflictStrategy.IGNORE) - public abstract void addAllIgnore(List messageList); - - @Query("DELETE FROM messages WHERE profileId = :profileId") - public abstract void clear(int profileId); - - @RawQuery(observedEntities = {Message.class}) - abstract LiveData> getAll(SupportSQLiteQuery query); - @RawQuery(observedEntities = {Message.class, Metadata.class}) - abstract List getNow(SupportSQLiteQuery query); - @RawQuery(observedEntities = {Message.class, Metadata.class}) - abstract MessageFull getOneNow(SupportSQLiteQuery query); - - public LiveData> getWithMetadataAndSenderName(int profileId, int messageType, String filter) { - return getAll(new SimpleSQLiteQuery("SELECT \n" + - "*, \n" + - "teachers.teacherName || ' ' || teachers.teacherSurname AS senderFullName\n" + - "FROM messages \n" + - "LEFT JOIN teachers ON teachers.profileId = "+profileId+" AND teacherId = senderId\n" + - "LEFT JOIN metadata ON messageId = thingId AND thingType = "+TYPE_MESSAGE+" AND metadata.profileId = "+profileId+"\n" + - "WHERE messages.profileId = "+profileId+" AND messageType = "+messageType+" AND "+filter+"\n" + - "ORDER BY addedDate DESC")); - } - - public LiveData> getWithMetadata(int profileId, int messageType, String filter) { - return getAll(new SimpleSQLiteQuery("SELECT \n" + - "* \n" + - "FROM messages \n" + - "LEFT JOIN metadata ON messageId = thingId AND thingType = "+TYPE_MESSAGE+" AND metadata.profileId = "+profileId+"\n" + - "WHERE messages.profileId = "+profileId+" AND messageType = "+messageType+" AND "+filter+"\n" + - "ORDER BY addedDate DESC")); - } - - @Nullable - public MessageFull getById(int profileId, long messageId) { - return getOneNow(new SimpleSQLiteQuery("SELECT \n" + - "*, \n" + - "teachers.teacherName || ' ' || teachers.teacherSurname AS senderFullName\n" + - "FROM messages \n" + - "LEFT JOIN teachers ON teachers.profileId = "+profileId+" AND teacherId = senderId\n" + - "LEFT JOIN metadata ON messageId = thingId AND thingType = "+TYPE_MESSAGE+" AND metadata.profileId = "+profileId+"\n" + - "WHERE messages.profileId = "+profileId+" AND messageId = "+messageId+"\n" + - "ORDER BY addedDate DESC")); - } - - public LiveData> getReceived(int profileId) { - return getWithMetadataAndSenderName(profileId, TYPE_RECEIVED, "1"); - } - public LiveData> getDeleted(int profileId) { - return getWithMetadataAndSenderName(profileId, TYPE_DELETED, "1"); - } - public LiveData> getSent(int profileId) { - return getWithMetadata(profileId, TYPE_SENT, "1"); - } - - public List getReceivedNow(int profileId, String filter) { - return getNow(new SimpleSQLiteQuery("SELECT \n" + - "*, \n" + - "teachers.teacherName || ' ' || teachers.teacherSurname AS senderFullName\n" + - "FROM messages \n" + - "LEFT JOIN teachers ON teachers.profileId = "+profileId+" AND teacherId = senderId\n" + - "LEFT JOIN metadata ON messageId = thingId AND thingType = "+TYPE_MESSAGE+" AND metadata.profileId = "+profileId+"\n" + - "WHERE messages.profileId = "+profileId+" AND messageType = 0 AND "+filter+"\n" + - "ORDER BY addedDate DESC")); - } - public List getReceivedNotNotifiedNow(int profileId) { - return getReceivedNow(profileId, "notified = 0"); - } - - @Query("SELECT " + - "*, " + - "teachers.teacherName || ' ' || teachers.teacherSurname AS senderFullName " + - "FROM messages " + - "LEFT JOIN teachers ON teachers.profileId = messages.profileId AND teacherId = senderId " + - "LEFT JOIN metadata ON messageId = thingId AND thingType = "+TYPE_MESSAGE+" AND metadata.profileId = messages.profileId " + - "WHERE messageType = 0 AND notified = 0 " + - "ORDER BY addedDate DESC") - public abstract List getReceivedNotNotifiedNow(); -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageDao.kt new file mode 100644 index 00000000..606f29a7 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageDao.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2020-1-6 + */ +package pl.szczodrzynski.edziennik.data.db.dao + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Query +import androidx.room.RawQuery +import androidx.sqlite.db.SupportSQLiteQuery +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.annotation.SelectiveDao +import pl.szczodrzynski.edziennik.annotation.UpdateSelective +import pl.szczodrzynski.edziennik.data.db.AppDb +import pl.szczodrzynski.edziennik.data.db.entity.Message +import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.full.MessageFull + +@Dao +@SelectiveDao(db = AppDb::class) +abstract class MessageDao : BaseDao { + companion object { + private const val QUERY = """ + SELECT + *, + teachers.teacherName ||" "|| teachers.teacherSurname AS senderName + FROM messages + LEFT JOIN teachers ON teachers.profileId = messages.profileId AND teacherId = senderId + LEFT JOIN metadata ON messageId = thingId AND thingType = ${Metadata.TYPE_MESSAGE} AND metadata.profileId = messages.profileId + """ + + private const val ORDER_BY = """ORDER BY messageIsPinned, addedDate DESC""" + } + + private val selective by lazy { MessageDaoSelective(App.db) } + + @RawQuery(observedEntities = [Message::class]) + abstract override fun getRaw(query: SupportSQLiteQuery): LiveData> + + @UpdateSelective(primaryKeys = ["profileId", "messageId"], skippedColumns = ["messageType", "messageBody", "messageIsPinned", "attachmentIds", "attachmentNames", "attachmentSizes"]) + override fun update(item: Message) = selective.update(item) + override fun updateAll(items: List) = selective.updateAll(items) + + // CLEAR + @Query("DELETE FROM messages WHERE profileId = :profileId") + abstract override fun clear(profileId: Int) + + // GET ALL - LIVE DATA + fun getAll(profileId: Int) = + getRaw("$QUERY WHERE messages.profileId = $profileId $ORDER_BY") + fun getAllByType(profileId: Int, type: Int) = + getRaw("$QUERY WHERE messages.profileId = $profileId AND messageType = $type $ORDER_BY") + fun getReceived(profileId: Int) = getAllByType(profileId, Message.TYPE_RECEIVED) + fun getSent(profileId: Int) = getAllByType(profileId, Message.TYPE_SENT) + fun getDeleted(profileId: Int) = getAllByType(profileId, Message.TYPE_DELETED) + fun getDraft(profileId: Int) = getAllByType(profileId, Message.TYPE_DRAFT) + + // GET ALL - NOW + fun getAllNow(profileId: Int) = + getRawNow("$QUERY WHERE messages.profileId = $profileId $ORDER_BY") + fun getNotNotifiedNow() = + getRawNow("$QUERY WHERE notified = 0 AND messageType = ${Message.TYPE_RECEIVED} $ORDER_BY") + + // GET ONE - NOW + fun getByIdNow(profileId: Int, id: Long) = + getOneNow("$QUERY WHERE messages.profileId = $profileId AND messageId = $id") +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MetadataDao.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MetadataDao.java index 845941de..7a074bca 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MetadataDao.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MetadataDao.java @@ -78,8 +78,8 @@ public abstract class MetadataDao { } } if (o instanceof Event) { - if (add(new Metadata(profileId, ((Event) o).type == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, ((Event) o).id, seen, false, 0)) == -1) { - updateSeen(profileId, ((Event) o).type == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, ((Event) o).id, seen); + if (add(new Metadata(profileId, ((Event) o).getType() == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, ((Event) o).getId(), seen, false, 0)) == -1) { + updateSeen(profileId, ((Event) o).getType() == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, ((Event) o).getId(), seen); } } if (o instanceof LessonFull) { @@ -93,8 +93,8 @@ public abstract class MetadataDao { } } if (o instanceof Message) { - if (add(new Metadata(profileId, TYPE_MESSAGE, ((Message) o).id, seen, false, 0)) == -1) { - updateSeen(profileId, TYPE_MESSAGE, ((Message) o).id, seen); + if (add(new Metadata(profileId, TYPE_MESSAGE, ((Message) o).getId(), seen, false, 0)) == -1) { + updateSeen(profileId, TYPE_MESSAGE, ((Message) o).getId(), seen); } } } @@ -117,8 +117,8 @@ public abstract class MetadataDao { } } if (o instanceof Event) { - if (add(new Metadata(profileId, ((Event) o).type == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, ((Event) o).id, false, notified, 0)) == -1) { - updateNotified(profileId, ((Event) o).type == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, ((Event) o).id, notified); + if (add(new Metadata(profileId, ((Event) o).getType() == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, ((Event) o).getId(), false, notified, 0)) == -1) { + updateNotified(profileId, ((Event) o).getType() == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, ((Event) o).getId(), notified); } } if (o instanceof LessonFull) { @@ -132,8 +132,8 @@ public abstract class MetadataDao { } } if (o instanceof Message) { - if (add(new Metadata(profileId, TYPE_MESSAGE, ((Message) o).id, false, notified, 0)) == -1) { - updateNotified(profileId, TYPE_MESSAGE, ((Message) o).id, notified); + if (add(new Metadata(profileId, TYPE_MESSAGE, ((Message) o).getId(), false, notified, 0)) == -1) { + updateNotified(profileId, TYPE_MESSAGE, ((Message) o).getId(), notified); } } } @@ -141,9 +141,9 @@ public abstract class MetadataDao { @Transaction public void setBoth(int profileId, Event o, boolean seen, boolean notified, long addedDate) { if (o != null) { - if (add(new Metadata(profileId, o.type == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, o.id, seen, notified, addedDate)) == -1) { - updateSeen(profileId, o.type == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, o.id, seen); - updateNotified(profileId, o.type == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, o.id, notified); + if (add(new Metadata(profileId, o.getType() == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, o.getId(), seen, notified, addedDate)) == -1) { + updateSeen(profileId, o.getType() == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, o.getId(), seen); + updateNotified(profileId, o.getType() == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, o.getId(), notified); } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TeacherAbsenceDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TeacherAbsenceDao.kt index 3da88080..1d4881bb 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TeacherAbsenceDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TeacherAbsenceDao.kt @@ -49,6 +49,17 @@ interface TeacherAbsenceDao { "AND :date BETWEEN teacherAbsenceDateFrom AND teacherAbsenceDateTo") fun getAllByDateNow(profileId: Int, date: Date): List + @Query(""" + SELECT *, + teachers.teacherName || ' ' || teachers.teacherSurname as teacherFullName + FROM teacherAbsence + LEFT JOIN teachers USING (profileId, teacherId) + LEFT JOIN metadata ON teacherAbsenceId = thingId AND metadata.thingType = ${Metadata.TYPE_TEACHER_ABSENCE} + AND teachers.profileId = metadata.profileId WHERE metadata.notified = 0 + ORDER BY addedDate DESC + """) + fun getNotNotifiedNow(): List + @Query("DELETE FROM teacherAbsence WHERE profileId = :profileId") fun clear(profileId: Int) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TeacherDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TeacherDao.kt index d886cd2d..27b9b869 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TeacherDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TeacherDao.kt @@ -1,10 +1,8 @@ package pl.szczodrzynski.edziennik.data.db.dao import androidx.lifecycle.LiveData -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query +import androidx.room.* +import androidx.sqlite.db.SupportSQLiteQuery import pl.szczodrzynski.edziennik.data.db.entity.Teacher @Dao @@ -18,6 +16,9 @@ interface TeacherDao { @Insert(onConflict = OnConflictStrategy.IGNORE) fun addAllIgnore(teacherList: List) + @RawQuery + fun query(query: SupportSQLiteQuery): Int + @Query("DELETE FROM teachers WHERE profileId = :profileId") fun clear(profileId: Int) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.java deleted file mode 100644 index bed95bdf..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2020-1-6 - */ - -package pl.szczodrzynski.edziennik.data.db.entity; - -import androidx.annotation.Nullable; -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.Ignore; -import androidx.room.Index; - -import java.util.Calendar; - -import pl.szczodrzynski.edziennik.data.db.full.EventFull; -import pl.szczodrzynski.edziennik.utils.models.Date; -import pl.szczodrzynski.edziennik.utils.models.Time; - -@Entity(tableName = "events", - primaryKeys = {"profileId", "eventId"}, - indices = {@Index(value = {"profileId", "eventDate", "eventStartTime"}), @Index(value = {"profileId", "eventType"})}) -public class Event { - public int profileId; - - @ColumnInfo(name = "eventId") - public long id; - - @ColumnInfo(name = "eventDate") - public Date eventDate; - @ColumnInfo(name = "eventStartTime") - @Nullable - public Time startTime; // null for allDay - @ColumnInfo(name = "eventTopic") - public String topic; - @ColumnInfo(name = "eventColor") - public int color = -1; - public static final long TYPE_UNDEFINED = -2; - public static final long TYPE_HOMEWORK = -1; - public static final long TYPE_DEFAULT = 0; - public static final long TYPE_EXAM = 1; - public static final long TYPE_SHORT_QUIZ = 2; - public static final long TYPE_ESSAY = 3; - public static final long TYPE_PROJECT = 4; - public static final long TYPE_PT_MEETING = 5; - public static final long TYPE_EXCURSION = 6; - public static final long TYPE_READING = 7; - public static final long TYPE_CLASS_EVENT = 8; - public static final long TYPE_INFORMATION = 9; - public static final long TYPE_TEACHER_ABSENCE = 10; - public static final int COLOR_HOMEWORK = 0xff795548; - public static final int COLOR_DEFAULT = 0xffffc107; - public static final int COLOR_EXAM = 0xfff44336; - public static final int COLOR_SHORT_QUIZ = 0xff76ff03; - public static final int COLOR_ESSAY = 0xFF4050B5; - public static final int COLOR_PROJECT = 0xFF673AB7; - public static final int COLOR_PT_MEETING = 0xff90caf9; - public static final int COLOR_EXCURSION = 0xFF4CAF50; - public static final int COLOR_READING = 0xFFFFEB3B; - public static final int COLOR_CLASS_EVENT = 0xff388e3c; - public static final int COLOR_INFORMATION = 0xff039be5; - public static final int COLOR_TEACHER_ABSENCE = 0xff039be5; - @ColumnInfo(name = "eventType") - public long type = TYPE_DEFAULT; - @ColumnInfo(name = "eventAddedManually") - public boolean addedManually; - @ColumnInfo(name = "eventSharedBy") - public String sharedBy = null; - @ColumnInfo(name = "eventSharedByName") - public String sharedByName = null; - @ColumnInfo(name = "eventBlacklisted") - public boolean blacklisted = false; - - - public long teacherId; - public long subjectId; - public long teamId; - - @Ignore - public Event() {} - - public Event(int profileId, long id, Date eventDate, @Nullable Time startTime, String topic, int color, long type, boolean addedManually, long teacherId, long subjectId, long teamId) - { - this.profileId = profileId; - this.id = id; - this.eventDate = eventDate; - this.startTime = startTime; - this.topic = topic; - this.color = color; - this.type = type; - this.addedManually = addedManually; - this.teacherId = teacherId; - this.subjectId = subjectId; - this.teamId = teamId; - } - - @Ignore - public EventFull withMetadata(Metadata metadata) { - return new EventFull(this, metadata); - } - - @Ignore - public Calendar getStartTimeCalendar() { - Calendar c = Calendar.getInstance(); - c.set( - eventDate.year, - eventDate.month - 1, - eventDate.day, - (startTime == null) ? 0 : startTime.hour, - (startTime == null) ? 0 : startTime.minute, - (startTime == null) ? 0 : startTime.second - ); - return c; - } - - @Ignore - public Calendar getEndTimeCalendar() { - Calendar c = Calendar.getInstance(); - c.setTimeInMillis(getStartTimeCalendar().getTimeInMillis() + (45 * 60 * 1000)); - return c; - } - - @Override - public Event clone() { - Event event = new Event( - profileId, - id, - eventDate.clone(), - startTime == null ? null : startTime.clone(), - topic, - color, - type, - addedManually, - subjectId, - teacherId, - teamId - ); - event.sharedBy = sharedBy; - event.sharedByName = sharedByName; - event.blacklisted = blacklisted; - return event; - } - - @Override - public String toString() { - return "Event{" + - "profileId=" + profileId + - ", id=" + id + - ", eventDate=" + eventDate + - ", startTime=" + startTime + - ", topic='" + topic + '\'' + - ", color=" + color + - ", type=" + type + - ", addedManually=" + addedManually + - ", sharedBy='" + sharedBy + '\'' + - ", sharedByName='" + sharedByName + '\'' + - ", teacherId=" + teacherId + - ", subjectId=" + subjectId + - ", teamId=" + teamId + - '}'; - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.kt new file mode 100644 index 00000000..b811ff61 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2020-1-6 + */ +package pl.szczodrzynski.edziennik.data.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Ignore +import androidx.room.Index +import com.google.gson.annotations.SerializedName +import pl.szczodrzynski.edziennik.MINUTE +import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time +import java.util.* + +@Entity(tableName = "events", + primaryKeys = ["profileId", "eventId"], + indices = [ + Index(value = ["profileId", "eventDate", "eventTime"]), + Index(value = ["profileId", "eventType"]) + ]) +open class Event( + /* This needs to be mutable: see SzkolnyApi.getEvents() */ + var profileId: Int, + @ColumnInfo(name = "eventId") + var id: Long, + @ColumnInfo(name = "eventDate") + @SerializedName("eventDate") + var date: Date, + @ColumnInfo(name = "eventTime") + @SerializedName("startTime") + var time: Time?, + + @ColumnInfo(name = "eventTopic") + var topic: String, + @ColumnInfo(name = "eventColor") + var color: Int?, + @ColumnInfo(name = "eventType") + var type: Long, + + var teacherId: Long, + var subjectId: Long, + var teamId: Long +) : Keepable() { + companion object { + const val TYPE_UNDEFINED = -2L + const val TYPE_HOMEWORK = -1L + const val TYPE_DEFAULT = 0L + const val TYPE_EXAM = 1L + const val TYPE_SHORT_QUIZ = 2L + const val TYPE_ESSAY = 3L + const val TYPE_PROJECT = 4L + const val TYPE_PT_MEETING = 5L + const val TYPE_EXCURSION = 6L + const val TYPE_READING = 7L + const val TYPE_CLASS_EVENT = 8L + const val TYPE_INFORMATION = 9L + const val TYPE_TEACHER_ABSENCE = 10L + const val COLOR_HOMEWORK = 0xff795548.toInt() + const val COLOR_DEFAULT = 0xffffc107.toInt() + const val COLOR_EXAM = 0xfff44336.toInt() + const val COLOR_SHORT_QUIZ = 0xff76ff03.toInt() + const val COLOR_ESSAY = 0xFF4050B5.toInt() + const val COLOR_PROJECT = 0xFF673AB7.toInt() + const val COLOR_PT_MEETING = 0xff90caf9.toInt() + const val COLOR_EXCURSION = 0xFF4CAF50.toInt() + const val COLOR_READING = 0xFFFFEB3B.toInt() + const val COLOR_CLASS_EVENT = 0xff388e3c.toInt() + const val COLOR_INFORMATION = 0xff039be5.toInt() + const val COLOR_TEACHER_ABSENCE = 0xff039be5.toInt() + } + + @ColumnInfo(name = "eventAddedManually") + var addedManually: Boolean = false + @ColumnInfo(name = "eventSharedBy") + var sharedBy: String? = null + @ColumnInfo(name = "eventSharedByName") + var sharedByName: String? = null + @ColumnInfo(name = "eventBlacklisted") + var blacklisted: Boolean = false + @ColumnInfo(name = "eventIsDone") + var isDone: Boolean = false + + /** + * Body/text of the event, if this is a [TYPE_HOMEWORK]. + * May be null if the body is not downloaded yet, or the type is not [TYPE_HOMEWORK]. + * May be empty or blank if the homework has no specific body attached, + * or the topic contains the body already. + */ + var homeworkBody: String? = null + var attachmentIds: MutableList? = null + var attachmentNames: MutableList? = null + + @Ignore + var showAsUnseen: Boolean? = null + + val startTimeCalendar: Calendar + get() = Calendar.getInstance().also { it.set( + date.year, + date.month - 1, + date.day, + time?.hour ?: 0, + time?.minute ?: 0, + time?.second ?: 0 + ) } + + val endTimeCalendar: Calendar + get() = startTimeCalendar.also { + it.timeInMillis += 45 * MINUTE * 1000 + } + + @Ignore + fun withMetadata(metadata: Metadata) = EventFull(this, metadata) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Keepable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Keepable.kt new file mode 100644 index 00000000..eb0da986 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Keepable.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-30. + */ + +package pl.szczodrzynski.edziennik.data.db.entity + +abstract class Keepable { + var keep: Boolean = true +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Message.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Message.java deleted file mode 100644 index f1b6453b..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Message.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2020-1-6 - */ - -package pl.szczodrzynski.edziennik.data.db.entity; - -import androidx.annotation.Nullable; -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.Ignore; -import androidx.room.Index; - -import java.util.ArrayList; -import java.util.List; - -@Entity(tableName = "messages", - primaryKeys = {"profileId", "messageId"}, - indices = {@Index(value = {"profileId"})}) -public class Message { - public int profileId; - - @ColumnInfo(name = "messageId") - public long id; - - @ColumnInfo(name = "messageSubject") - public String subject; - @Nullable - @ColumnInfo(name = "messageBody") - public String body = null; - - public static final int TYPE_RECEIVED = 0; - public static final int TYPE_SENT = 1; - public static final int TYPE_DELETED = 2; - public static final int TYPE_DRAFT = 3; - @ColumnInfo(name = "messageType") - public int type = TYPE_RECEIVED; - - public long senderId = -1; // -1 for sent messages - public long senderReplyId = -1; - public boolean overrideHasAttachments = false; // if the attachments are not yet downloaded but we already know there are some - public List attachmentIds = null; - public List attachmentNames = null; - public List attachmentSizes = null; - - @Ignore - public Message() {} - - public Message(int profileId, long id, String subject, @Nullable String body, int type, long senderId, long senderReplyId) { - this.profileId = profileId; - this.id = id; - this.subject = subject; - this.body = body; - this.type = type; - this.senderId = senderId; - this.senderReplyId = senderReplyId; - } - - /** - * Add an attachment - * @param id attachment ID - * @param name file name incl. extension - * @param size file size or -1 if unknown - * @return a Message to which the attachment has been added - */ - public Message addAttachment(long id, String name, long size) { - if (attachmentIds == null) - attachmentIds = new ArrayList<>(); - if (attachmentNames == null) - attachmentNames = new ArrayList<>(); - if (attachmentSizes == null) - attachmentSizes = new ArrayList<>(); - attachmentIds.add(id); - attachmentNames.add(name); - attachmentSizes.add(size); - return this; - } - - public void clearAttachments() { - attachmentIds = null; - attachmentNames = null; - attachmentSizes = null; - } - - public Message setHasAttachments() { - overrideHasAttachments = true; - return this; - } - - public boolean hasAttachments() { - return overrideHasAttachments || (attachmentIds != null && attachmentIds.size() > 0); - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Message.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Message.kt new file mode 100644 index 00000000..1cf9b3ba --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Message.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2020-1-6 + */ +package pl.szczodrzynski.edziennik.data.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Ignore +import androidx.room.Index +import pl.szczodrzynski.edziennik.isNotNullNorEmpty + +@Entity(tableName = "messages", + primaryKeys = ["profileId", "messageId"], + indices = [ + Index(value = ["profileId", "messageType"]) + ]) +open class Message( + val profileId: Int, + @ColumnInfo(name = "messageId") + val id: Long, + @ColumnInfo(name = "messageType") + var type: Int, + + @ColumnInfo(name = "messageSubject") + var subject: String, + @ColumnInfo(name = "messageBody") + var body: String?, + + /** + * Keep in mind that this being null does NOT + * necessarily mean the message is sent. + */ + var senderId: Long? +) : Keepable() { + companion object { + const val TYPE_RECEIVED = 0 + const val TYPE_SENT = 1 + const val TYPE_DELETED = 2 + const val TYPE_DRAFT = 3 + } + + @ColumnInfo(name = "messageIsPinned") + var isPinned: Boolean = false + + var hasAttachments = false // if the attachments are not yet downloaded but we already know there are some + get() = field || attachmentIds.isNotNullNorEmpty() + var attachmentIds: MutableList? = null + var attachmentNames: MutableList? = null + var attachmentSizes: MutableList? = null + + @Ignore + var showAsUnseen: Boolean? = null + + /** + * Add an attachment + * @param id attachment ID + * @param name file name incl. extension + * @param size file size or -1 if unknown + * @return a Message to which the attachment has been added + */ + fun addAttachment(id: Long, name: String, size: Long): Message { + if (attachmentIds == null) attachmentIds = mutableListOf() + if (attachmentNames == null) attachmentNames = mutableListOf() + if (attachmentSizes == null) attachmentSizes = mutableListOf() + attachmentIds?.add(id) + attachmentNames?.add(name) + attachmentSizes?.add(size) + return this + } + + fun clearAttachments() { + attachmentIds = null + attachmentNames = null + attachmentSizes = null + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Notification.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Notification.kt index 242d5999..1e8b8e0e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Notification.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Notification.kt @@ -52,6 +52,7 @@ data class Notification( const val TYPE_NEW_ANNOUNCEMENT = 15 const val TYPE_FEEDBACK_MESSAGE = 16 const val TYPE_AUTO_ARCHIVING = 17 + const val TYPE_TEACHER_ABSENCE = 19 fun buildId(profileId: Int, type: Int, itemId: Long): Long { return 1000000000000 + profileId*10000000000 + type*100000000 + itemId; diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/EventFull.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/EventFull.java deleted file mode 100644 index beaf794e..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/EventFull.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2020-1-6 - */ - -package pl.szczodrzynski.edziennik.data.db.full; - -import pl.szczodrzynski.edziennik.data.db.entity.Event; -import pl.szczodrzynski.edziennik.data.db.entity.Metadata; - -public class EventFull extends Event { - public String typeName = ""; - public int typeColor = -1; - - public String teacherFullName = ""; - - public String subjectLongName = ""; - public String subjectShortName = ""; - - public String teamName = ""; - public String teamCode = null; - - // metadata - public boolean seen; - public boolean notified; - public long addedDate; - - public EventFull() {} - - public EventFull(Event event) { - super( - event.profileId, - event.id, - event.eventDate.clone(), - event.startTime == null ? null : event.startTime.clone(), - event.topic, - event.color, - event.type, - event.addedManually, - event.teacherId, - event.subjectId, - event.teamId - ); - - this.sharedBy = event.sharedBy; - this.sharedByName = event.sharedByName; - this.blacklisted = event.blacklisted; - } - public EventFull(EventFull event) { - super( - event.profileId, - event.id, - event.eventDate.clone(), - event.startTime == null ? null : event.startTime.clone(), - event.topic, - event.color, - event.type, - event.addedManually, - event.teacherId, - event.subjectId, - event.teamId - ); - - this.sharedBy = event.sharedBy; - this.sharedByName = event.sharedByName; - this.blacklisted = event.blacklisted; - this.typeName = event.typeName; - this.typeColor = event.typeColor; - this.teacherFullName = event.teacherFullName; - this.subjectLongName = event.subjectLongName; - this.subjectShortName = event.subjectShortName; - this.teamName = event.teamName; - this.teamCode = event.teamCode; - this.seen = event.seen; - this.notified = event.notified; - this.addedDate = event.addedDate; - } - - public EventFull(Event event, Metadata metadata) { - this(event); - - this.seen = metadata.seen; - this.notified = metadata.notified; - this.addedDate = metadata.addedDate; - } - - public int getColor() { - return color == -1 ? typeColor : color; - } - - @Override - public String toString() { - return "EventFull{" + - "profileId=" + profileId + - ", id=" + id + - ", eventDate=" + eventDate + - ", startTime=" + startTime + - ", topic='" + topic + '\'' + - ", color=" + color + - ", type=" + type + - ", addedManually=" + addedManually + - ", sharedBy='" + sharedBy + '\'' + - ", sharedByName='" + sharedByName + '\'' + - ", blacklisted=" + blacklisted + - ", teacherId=" + teacherId + - ", subjectId=" + subjectId + - ", teamId=" + teamId + - ", typeName='" + typeName + '\'' + - ", teacherFullName='" + teacherFullName + '\'' + - ", subjectLongName='" + subjectLongName + '\'' + - ", subjectShortName='" + subjectShortName + '\'' + - ", teamName='" + teamName + '\'' + - ", seen=" + seen + - ", notified=" + notified + - ", addedDate=" + addedDate + - '}'; - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/EventFull.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/EventFull.kt new file mode 100644 index 00000000..a3dcc2cb --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/EventFull.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2020-1-6 + */ +package pl.szczodrzynski.edziennik.data.db.full + +import pl.szczodrzynski.edziennik.data.db.entity.Event +import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time + +class EventFull( + profileId: Int, id: Long, date: Date, time: Time?, + topic: String, color: Int?, type: Long, + teacherId: Long, subjectId: Long, teamId: Long +) : Event( + profileId, id, date, time, + topic, color, type, + teacherId, subjectId, teamId +) { + constructor(event: Event, metadata: Metadata? = null) : this( + event.profileId, event.id, event.date, event.time, + event.topic, event.color, event.type, + event.teacherId, event.subjectId, event.teamId) { + event.let { + addedManually = it.addedManually + sharedBy = it.sharedBy + sharedByName = it.sharedByName + blacklisted = it.blacklisted + homeworkBody = it.homeworkBody + attachmentIds = it.attachmentIds + attachmentNames = it.attachmentNames + } + metadata?.let { + seen = it.seen + notified = it.notified + addedDate = it.addedDate + } + } + + var typeName: String? = null + var typeColor: Int? = null + + var teacherName: String? = null + var subjectLongName: String? = null + var subjectShortName: String? = null + var teamName: String? = null + var teamCode: String? = null + // metadata + var seen = false + var notified = false + var addedDate: Long = 0 + + val eventColor + get() = color ?: typeColor ?: 0xff2196f3.toInt() +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.java deleted file mode 100644 index be7ba692..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2020-1-6 - */ - -package pl.szczodrzynski.edziennik.data.db.full; - -import androidx.annotation.Nullable; -import androidx.room.Ignore; - -import java.util.ArrayList; -import java.util.List; - -import pl.szczodrzynski.edziennik.data.db.entity.Message; - -public class MessageFull extends Message { - public String senderFullName = null; - @Ignore - @Nullable - public List recipients = null; - - public MessageFull addRecipient(MessageRecipientFull recipient) { - if (recipients == null) - recipients = new ArrayList<>(); - recipients.add(recipient); - return this; - } - - // metadata - public boolean seen; - public boolean notified; - public long addedDate; -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt new file mode 100644 index 00000000..5b873b7a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2020-1-6 + */ +package pl.szczodrzynski.edziennik.data.db.full + +import androidx.room.Ignore +import androidx.room.Relation +import pl.szczodrzynski.edziennik.data.db.entity.Message +import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient + +class MessageFull( + profileId: Int, id: Long, type: Int, + subject: String, body: String?, senderId: Long? +) : Message( + profileId, id, type, + subject, body, senderId +) { + var senderName: String? = null + @Relation(parentColumn = "messageId", entityColumn = "messageId", entity = MessageRecipient::class) + var recipients: MutableList? = null + + fun addRecipient(recipient: MessageRecipientFull): MessageFull { + if (recipients == null) recipients = mutableListOf() + recipients?.add(recipient) + return this + } + + @Ignore + var filterWeight = 0 + @Ignore + var searchHighlightText: String? = null + + // metadata + var seen = false + var notified = false + var addedDate: Long = 0 +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageRecipientFull.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageRecipientFull.kt index 7f72245e..2126f872 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageRecipientFull.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageRecipientFull.kt @@ -2,17 +2,13 @@ package pl.szczodrzynski.edziennik.data.db.full import androidx.room.Ignore import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient -import pl.szczodrzynski.edziennik.fixName - -class MessageRecipientFull : MessageRecipient { - var fullName: String? = "" - get() { - return field?.fixName() ?: "" - } +class MessageRecipientFull( + profileId: Int, + id: Long, + messageId: Long, + readDate: Long = -1L +) : MessageRecipient(profileId, id, -1, readDate, messageId) { @Ignore - constructor(profileId: Int, id: Long, replyId: Long, readDate: Long, messageId: Long) : super(profileId, id, replyId, readDate, messageId) {} - @Ignore - constructor(profileId: Int, id: Long, messageId: Long) : super(profileId, id, messageId) {} - constructor() : super() {} + var fullName: String? = null } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration80.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration80.kt new file mode 100644 index 00000000..86394792 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration80.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-27. + */ + +package pl.szczodrzynski.edziennik.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration80 : Migration(79, 80) { + override fun migrate(database: SupportSQLiteDatabase) { + // The Homework Update + database.execSQL("ALTER TABLE events RENAME TO _events;") + database.execSQL("""CREATE TABLE events ( + profileId INTEGER NOT NULL, + eventId INTEGER NOT NULL, + eventDate TEXT NOT NULL, + eventTime TEXT, + eventTopic TEXT NOT NULL, + eventColor INTEGER, + eventType INTEGER NOT NULL, + teacherId INTEGER NOT NULL, + subjectId INTEGER NOT NULL, + teamId INTEGER NOT NULL, + eventAddedManually INTEGER NOT NULL DEFAULT 0, + eventSharedBy TEXT DEFAULT NULL, + eventSharedByName TEXT DEFAULT NULL, + eventBlacklisted INTEGER NOT NULL DEFAULT 0, + homeworkBody TEXT DEFAULT NULL, + attachmentIds TEXT DEFAULT NULL, + attachmentNames TEXT DEFAULT NULL, + PRIMARY KEY(profileId, eventId) + )""") + database.execSQL("DROP INDEX IF EXISTS index_events_profileId_eventDate_eventStartTime") + database.execSQL("DROP INDEX IF EXISTS index_events_profileId_eventType") + database.execSQL("CREATE INDEX index_events_profileId_eventDate_eventTime ON events (profileId, eventDate, eventTime)") + database.execSQL("CREATE INDEX index_events_profileId_eventType ON events (profileId, eventType)") + database.execSQL(""" + INSERT INTO events (profileId, eventId, eventDate, eventTime, eventTopic, eventColor, eventType, teacherId, subjectId, teamId, eventAddedManually, eventSharedBy, eventSharedByName, eventBlacklisted) + SELECT profileId, eventId, eventDate, eventStartTime, eventTopic, + CASE eventColor WHEN -1 THEN NULL ELSE eventColor END, + eventType, teacherId, subjectId, teamId, + eventAddedManually, eventSharedBy, eventSharedByName, eventBlacklisted + FROM _events + """) + database.execSQL("DROP TABLE _events") + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration81.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration81.kt new file mode 100644 index 00000000..bf547244 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration81.kt @@ -0,0 +1,11 @@ +package pl.szczodrzynski.edziennik.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import pl.szczodrzynski.edziennik.data.db.entity.Metadata + +class Migration81 : Migration(80, 81) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("UPDATE metadata SET seen = 1, notified = 1 WHERE thingType = ${Metadata.TYPE_TEACHER_ABSENCE}") + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration82.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration82.kt new file mode 100644 index 00000000..b0e1c769 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration82.kt @@ -0,0 +1,10 @@ +package pl.szczodrzynski.edziennik.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration82 : Migration(81, 82) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE events ADD COLUMN eventIsDone INTEGER DEFAULT 0 NOT NULL") + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration83.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration83.kt new file mode 100644 index 00000000..340d5768 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration83.kt @@ -0,0 +1,14 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-30. + */ + +package pl.szczodrzynski.edziennik.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration83 : Migration(82, 83) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE events ADD COLUMN keep INTEGER NOT NULL DEFAULT 1") + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration84.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration84.kt new file mode 100644 index 00000000..67ae8dbc --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration84.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-4. + */ + +package pl.szczodrzynski.edziennik.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration84 : Migration(83, 84) { + override fun migrate(database: SupportSQLiteDatabase) { + // The Message Update + database.execSQL("ALTER TABLE messages RENAME TO _messages;") + database.execSQL("""CREATE TABLE messages ( + profileId INTEGER NOT NULL, + messageId INTEGER NOT NULL, + messageType INTEGER NOT NULL, + messageSubject TEXT NOT NULL, + messageBody TEXT, + senderId INTEGER, + messageIsPinned INTEGER NOT NULL DEFAULT 0, + hasAttachments INTEGER NOT NULL DEFAULT 0, + attachmentIds TEXT DEFAULT NULL, + attachmentNames TEXT DEFAULT NULL, + attachmentSizes TEXT DEFAULT NULL, + keep INTEGER NOT NULL DEFAULT 1, + PRIMARY KEY(profileId, messageId) + )""") + database.execSQL("DROP INDEX IF EXISTS index_messages_profileId") + database.execSQL("CREATE INDEX index_messages_profileId_messageType ON messages (profileId, messageType)") + database.execSQL(""" + INSERT INTO messages (profileId, messageId, messageType, messageSubject, messageBody, senderId, hasAttachments, attachmentIds, attachmentNames, attachmentSizes) + SELECT profileId, messageId, messageType, messageSubject, messageBody, + CASE senderId WHEN -1 THEN NULL ELSE senderId END, + overrideHasAttachments, attachmentIds, attachmentNames, attachmentSizes + FROM _messages + """) + database.execSQL("DROP TABLE _messages") + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration85.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration85.kt new file mode 100644 index 00000000..6f1c79c0 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration85.kt @@ -0,0 +1,12 @@ +package pl.szczodrzynski.edziennik.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_EDUDZIENNIK +import pl.szczodrzynski.edziennik.data.db.entity.Event + +class Migration85 : Migration(84, 85) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("DELETE FROM events WHERE eventAddedManually = 0 AND eventType = ${Event.TYPE_HOMEWORK} AND profileId IN (SELECT profileId FROM (SELECT profileId FROM profiles WHERE loginStoreType = $LOGIN_TYPE_EDUDZIENNIK) x)") + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt index 78d03873..c394c7cc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt @@ -105,18 +105,19 @@ class SzkolnyAppFirebase(val app: App, val profiles: List, val message: if (profile.registration != Profile.REGISTRATION_ENABLED) return@forEach val event = Event( - team.profileId, - json.getLong("id") ?: return, - json.getInt("eventDate")?.let { Date.fromValue(it) } ?: return, - json.getInt("startTime")?.let { Time.fromValue(it) }, - json.getString("topic") ?: "", - json.getInt("color") ?: -1, - json.getLong("type") ?: 0, - true, - json.getLong("teacherId") ?: -1, - json.getLong("subjectId") ?: -1, - team.id + profileId = team.profileId, + id = json.getLong("id") ?: return, + date = json.getInt("eventDate")?.let { Date.fromValue(it) } ?: return, + time = json.getInt("startTime")?.let { Time.fromValue(it) }, + topic = json.getString("topic") ?: "", + color = json.getInt("color"), + type = json.getLong("type") ?: 0, + teacherId = json.getLong("teacherId") ?: -1, + subjectId = json.getLong("subjectId") ?: -1, + teamId = team.id ) + if (event.color == -1) + event.color = null event.sharedBy = json.getString("sharedBy") event.sharedByName = json.getString("sharedByName") @@ -144,14 +145,14 @@ class SzkolnyAppFirebase(val app: App, val profiles: List, val message: profileName = profile.name, viewId = if (event.type == Event.TYPE_HOMEWORK) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA, addedDate = metadata.addedDate - ).addExtra("eventId", event.id).addExtra("eventDate", event.eventDate.value.toLong()) + ).addExtra("eventId", event.id).addExtra("eventDate", event.date.value.toLong()) notificationList += notification } events += event metadataList += metadata } - app.db.eventDao().addAll(events) + app.db.eventDao().upsertAll(events) app.db.metadataDao().addAllReplace(metadataList) if (notificationList.isNotEmpty()) { app.db.notificationDao().addAll(notificationList) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyMobidziennikFirebase.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyMobidziennikFirebase.kt index f102fc2a..b097dbed 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyMobidziennikFirebase.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyMobidziennikFirebase.kt @@ -12,7 +12,7 @@ import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_MOBIDZIENNIK import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.task.IApiTask -import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_RECEIVED +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.getLong import pl.szczodrzynski.edziennik.getString diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/receivers/SzkolnyReceiver.kt b/app/src/main/java/pl/szczodrzynski/edziennik/receivers/SzkolnyReceiver.kt index f456bae3..f7819c43 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/receivers/SzkolnyReceiver.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/receivers/SzkolnyReceiver.kt @@ -7,6 +7,7 @@ package pl.szczodrzynski.edziennik.receivers import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.os.Bundle import pl.szczodrzynski.edziennik.data.api.ApiService import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.events.requests.ServiceCloseRequest @@ -15,6 +16,11 @@ import pl.szczodrzynski.edziennik.data.api.events.requests.TaskCancelRequest class SzkolnyReceiver : BroadcastReceiver() { companion object { const val ACTION = "pl.szczodrzynski.edziennik.SZKOLNY_MAIN" + fun getIntent(context: Context, extras: Bundle): Intent { + val intent = Intent(context, SzkolnyReceiver::class.java) + intent.putExtras(extras) + return intent + } } override fun onReceive(context: Context?, intent: Intent?) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/TemplateAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/TemplateAdapter.kt deleted file mode 100644 index 8366862e..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/TemplateAdapter.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2019-12-19. - */ - -package pl.szczodrzynski.edziennik.ui - -import android.content.Context -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.databinding.TemplateListItemBinding -import pl.szczodrzynski.edziennik.onClick - -class TemplateAdapter( - val context: Context, - val onItemClick: ((item: TemplateItem) -> Unit)? = null, - val onItemButtonClick: ((item: TemplateItem) -> Unit)? = null -) : RecyclerView.Adapter() { - - private val app by lazy { context.applicationContext as App } - - var items = listOf() - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val inflater = LayoutInflater.from(parent.context) - val view = TemplateListItemBinding.inflate(inflater, parent, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val item = items[position] - val b = holder.b - - onItemClick?.let { listener -> - b.root.onClick { listener(item) } - } - - /*b.someButton.visibility = if (buttonVisible) View.VISIBLE else View.GONE - onItemButtonClick?.let { listener -> - b.someButton.onClick { listener(item) } - }*/ - } - - override fun getItemCount() = items.size - - class ViewHolder(val b: TemplateListItemBinding) : RecyclerView.ViewHolder(b.root) - - data class TemplateItem(val text: String) -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/day/DayDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/day/DayDialog.kt index b46925f0..03cc4879 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/day/DayDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/day/DayDialog.kt @@ -146,6 +146,12 @@ class DayDialog( adapter = EventListAdapter( activity, + showWeekDay = false, + showDate = false, + showType = true, + showTime = true, + showSubject = true, + markAsSeen = true, onItemClick = { EventDetailsDialog( activity, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt index e4d1c144..83df5e77 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt @@ -12,10 +12,17 @@ import android.view.View import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.* +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask +import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi +import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.databinding.DialogEventDetailsBinding import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment @@ -54,6 +61,7 @@ class EventDetailsDialog( if (activity.isFinishing) return@run onShowListener?.invoke(TAG) + EventBus.getDefault().register(this) app = activity.applicationContext as App b = DialogEventDetailsBinding.inflate(activity.layoutInflater) dialog = MaterialAlertDialogBuilder(activity) @@ -67,6 +75,7 @@ class EventDetailsDialog( } .setOnDismissListener { onDismissListener?.invoke(TAG) + EventBus.getDefault().unregister(this@EventDetailsDialog) progressDialog?.dismiss() } .show() @@ -87,11 +96,11 @@ class EventDetailsDialog( val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity) try { - b.monthName = app.resources.getStringArray(R.array.months_day_of_array)[event.eventDate.month - 1] + b.monthName = app.resources.getStringArray(R.array.months_day_of_array)[event.date.month - 1] } catch (_: Exception) {} - b.typeColor.background?.setTintColor(event.getColor()) + b.typeColor.background?.setTintColor(event.eventColor) b.details = mutableListOf( event.subjectLongName, @@ -102,16 +111,45 @@ class EventDetailsDialog( when (event.sharedBy) { null -> when { event.addedManually -> R.string.event_details_added_by_self_format - event.teacherFullName == null -> R.string.event_details_added_by_unknown_format + event.teacherName == null -> R.string.event_details_added_by_unknown_format else -> R.string.event_details_added_by_format } "self" -> R.string.event_details_shared_by_self_format else -> R.string.event_details_shared_by_format }, Date.fromMillis(event.addedDate).formattedString, - event.sharedByName ?: event.teacherFullName ?: "" + event.sharedByName ?: event.teacherName ?: "" ) + + // MARK AS DONE + b.checkDoneButton.isChecked = event.isDone + b.checkDoneButton.onChange { _, isChecked -> + if (isChecked && !event.isDone) { + b.checkDoneButton.isChecked = false + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.event_mark_as_done_title) + .setMessage(R.string.event_mark_as_done_text) + .setPositiveButton(R.string.ok) { _, _ -> + event.isDone = isChecked + launch(Dispatchers.Default) { + app.db.eventDao().replace(event) + } + b.checkDoneButton.isChecked = true + } + .setNegativeButton(R.string.cancel, null) + .show() + } + else if (!isChecked && event.isDone) { + event.isDone = isChecked + launch(Dispatchers.Default) { + app.db.eventDao().replace(event) + } + } + } + b.checkDoneButton.attachToastHint(R.string.hint_mark_as_done) + + // EDIT EVENT b.editButton.visibility = if (event.addedManually) View.VISIBLE else View.GONE b.editButton.setOnClickListener { EventManualDialog( @@ -122,10 +160,18 @@ class EventDetailsDialog( onDismissListener = onDismissListener ) } + b.editButton.attachToastHint(R.string.hint_edit_event) - b.goToTimetableButton.setOnClickListener { + // SAVE IN CALENDAR + b.saveInCalendarButton.onClick { + openInCalendar() + } + b.saveInCalendarButton.attachToastHint(R.string.hint_save_in_calendar) + + // GO TO TIMETABLE + b.goToTimetableButton.onClick { dialog.dismiss() - val dateStr = event.eventDate?.stringY_m_d ?: return@setOnClickListener + val dateStr = event.date.stringY_m_d val intent = if (activity is MainActivity && activity.navTargetId == MainActivity.DRAWER_ITEM_TIMETABLE) @@ -144,27 +190,61 @@ class EventDetailsDialog( else activity.startActivity(intent) } - b.saveInCalendarButton.setOnClickListener { - openInCalendar() - } + b.goToTimetableButton.attachToastHint(R.string.hint_go_to_timetable) - b.goToTimetableButton.setOnLongClickListener { - Toast.makeText(activity, R.string.hint_go_to_timetable, Toast.LENGTH_SHORT).show() - true - } - b.saveInCalendarButton.setOnLongClickListener { - Toast.makeText(activity, R.string.hint_save_in_calendar, Toast.LENGTH_SHORT).show() - true - } - b.editButton.setOnLongClickListener { - Toast.makeText(activity, R.string.hint_edit_event, Toast.LENGTH_SHORT).show() - true + // RE-DOWNLOAD + b.downloadButton.isVisible = App.debugMode + b.downloadButton.onClick { + EdziennikTask.eventGet(event.profileId, event).enqueue(activity) } + b.downloadButton.attachToastHint(R.string.hint_download_again) + b.topic.text = event.topic BetterLink.attach(b.topic) { dialog.dismiss() } + + if (event.homeworkBody == null && !event.addedManually && event.type == Event.TYPE_HOMEWORK) { + b.bodyTitle.isVisible = true + b.bodyProgressBar.isVisible = true + b.body.isVisible = false + EdziennikTask.eventGet(event.profileId, event).enqueue(activity) + } + else if (event.homeworkBody.isNullOrBlank()) { + b.bodyTitle.isVisible = false + b.bodyProgressBar.isVisible = false + b.body.isVisible = false + } + else { + b.bodyTitle.isVisible = true + b.bodyProgressBar.isVisible = false + b.body.isVisible = true + b.body.text = event.homeworkBody + BetterLink.attach(b.body) { + dialog.dismiss() + } + } + + if (event.attachmentIds.isNullOrEmpty() || event.attachmentNames.isNullOrEmpty()) { + b.attachmentsTitle.isVisible = false + b.attachmentsFragment.isVisible = false + } + else { + b.attachmentsTitle.isVisible = true + b.attachmentsFragment.isVisible = true + b.attachmentsFragment.init(Bundle().also { + it.putInt("profileId", event.profileId) + it.putLongArray("attachmentIds", event.attachmentIds!!.toLongArray()) + it.putStringArray("attachmentNames", event.attachmentNames!!.toTypedArray()) + }, owner = event) + } + } + + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) + fun onEventGetEvent(event: EventGetEvent) { + EventBus.getDefault().removeStickyEvent(event) + update() } private fun showRemovingProgressDialog() { @@ -245,7 +325,7 @@ class EventDetailsDialog( } private fun openInCalendar() { launch { - val title = (event.typeName ?: "") + + val title = event.typeName ?: "" + (if (event.typeName.isNotNullNorBlank() && event.subjectLongName.isNotNullNorBlank()) " - " else " ") + (event.subjectLongName ?: "") @@ -254,12 +334,12 @@ class EventDetailsDialog( putExtra(Events.TITLE, title) putExtra(Events.DESCRIPTION, event.topic) - if (event.startTime == null) { + if (event.time == null) { putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, true) - putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, event.eventDate.inMillis) - putExtra(CalendarContract.EXTRA_EVENT_END_TIME, event.eventDate.inMillis) + putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, event.date.inMillis) + putExtra(CalendarContract.EXTRA_EVENT_END_TIME, event.date.inMillis) } else { - val startTime = event.eventDate.combineWith(event.startTime) + val startTime = event.date.combineWith(event.time) val endTime = startTime + 45 * 60 * 1000 /* 45 min */ putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, false) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt index c70e97cf..34cc6a2d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt @@ -6,26 +6,38 @@ package pl.szczodrzynski.edziennik.ui.dialogs.event import android.content.Context import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.Toast +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.databinding.EventListItemBinding import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Week +import kotlin.coroutines.CoroutineContext class EventListAdapter( val context: Context, val simpleMode: Boolean = false, - val showDate: Boolean = false, val showWeekDay: Boolean = false, + val showDate: Boolean = false, + val showType: Boolean = true, + val showTime: Boolean = true, + val showSubject: Boolean = true, + val markAsSeen: Boolean = true, val onItemClick: ((event: EventFull) -> Unit)? = null, val onEventEditClick: ((event: EventFull) -> Unit)? = null -) : RecyclerView.Adapter() { +) : RecyclerView.Adapter(), CoroutineScope { - private val app by lazy { context.applicationContext as App } + private val app = context.applicationContext as App + private val manager = app.eventManager + + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main var items = listOf() @@ -38,9 +50,17 @@ class EventListAdapter( override fun onBindViewHolder(holder: ViewHolder, position: Int) { val event = items[position] val b = holder.b + val manager = app.eventManager b.root.onClick { onItemClick?.invoke(event) + if (!event.seen) { + manager.markAsSeen(event) + } + if (event.showAsUnseen == true) { + event.showAsUnseen = false + notifyItemChanged(event) + } } val bullet = " • " @@ -48,71 +68,57 @@ class EventListAdapter( b.simpleMode = simpleMode b.topic.text = event.topic + b.topic.maxLines = if (simpleMode) 2 else 3 b.details.text = mutableListOf( - if (showWeekDay) Week.getFullDayName(event.eventDate.weekDay) else null, - if (showDate) event.eventDate.getRelativeString(context, 7) ?: event.eventDate.formattedStringShort else null, - event.typeName, - if (simpleMode) null else event.startTime?.stringHM ?: app.getString(R.string.event_all_day), - if (simpleMode) null else event.subjectLongName + if (showWeekDay) Week.getFullDayName(event.date.weekDay) else null, + if (showDate) event.date.getRelativeString(context, 7) ?: event.date.formattedStringShort else null, + if (showType) event.typeName else null, + if (showTime) event.time?.stringHM ?: app.getString(R.string.event_all_day) else null, + if (showSubject) event.subjectLongName else null ).concat(bullet) b.addedBy.setText( when (event.sharedBy) { null -> when { event.addedManually -> R.string.event_list_added_by_self_format - event.teacherFullName == null -> R.string.event_list_added_by_unknown_format + event.teacherName == null -> R.string.event_list_added_by_unknown_format else -> R.string.event_list_added_by_format } "self" -> R.string.event_list_shared_by_self_format else -> R.string.event_list_shared_by_format }, Date.fromMillis(event.addedDate).formattedString, - event.sharedByName ?: event.teacherFullName ?: "", + event.sharedByName ?: event.teacherName ?: "", event.teamName?.let { bullet+it } ?: "" ) - b.typeColor.background?.setTintColor(event.getColor()) + b.typeColor.background?.setTintColor(event.eventColor) + b.typeColor.isVisible = showType - b.editButton.visibility = if (event.addedManually && !simpleMode) View.VISIBLE else View.GONE + b.editButton.isVisible = !simpleMode && event.addedManually && !event.isDone b.editButton.onClick { onEventEditClick?.invoke(event) } + b.editButton.attachToastHint(R.string.hint_edit_event) - b.editButton.setOnLongClickListener { - Toast.makeText(context, R.string.hint_edit_event, Toast.LENGTH_SHORT).show() - true + b.isDone.isVisible = event.isDone + + if (event.showAsUnseen == null) + event.showAsUnseen = !event.seen + + b.unread.isVisible = event.showAsUnseen == true + if (markAsSeen && !event.seen) { + manager.markAsSeen(event) } + } - /*with(holder) { - b.eventListItemRoot.background.colorFilter = when (event.type) { - Event.TYPE_HOMEWORK -> PorterDuffColorFilter(0xffffffff.toInt(), PorterDuff.Mode.CLEAR) - else -> PorterDuffColorFilter(event.color, PorterDuff.Mode.MULTIPLY) - } - - b.eventListItemStartTime.text = if (event.startTime == null) app.getString(R.string.event_all_day) else event.startTime?.stringHM - b.eventListItemTeamName.text = bs(event.teamName) - b.eventListItemTeacherName.text = app.getString(R.string.concat_2_strings, bs(null, event.teacherFullName, "\n"), bs(event.subjectLongName)) - b.eventListItemAddedDate.text = Date.fromMillis(event.addedDate).formattedStringShort - b.eventListItemType.text = event.typeName - b.eventListItemTopic.text = event.topic - b.eventListItemHomework.visibility = if (event.type == Event.TYPE_HOMEWORK) View.VISIBLE else View.GONE - b.eventListItemSharedBy.text = app.getString(R.string.event_shared_by_format, if (event.sharedBy == "self") app.getString(R.string.event_shared_by_self) else event.sharedByName) - b.eventListItemSharedBy.visibility = if (event.sharedByName.isNullOrBlank()) View.GONE else View.VISIBLE - - b.eventListItemEdit.visibility = if (event.addedManually) View.VISIBLE else View.GONE - b.eventListItemEdit.setOnClickListener { - parentDialog.dismiss() - - EventManualDialog( - context as MainActivity, - event.profileId, - editingEvent = event, - onShowListener = parentDialog.onShowListener, - onDismissListener = parentDialog.onDismissListener - ) - } - }*/ + private fun notifyItemChanged(model: Any) { + startCoroutineTimer(1000L, 0L) { + val index = items.indexOf(model) + if (index != -1) + notifyItemChanged(index) + } } override fun getItemCount() = items.size diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt index 7af42647..d6d175d9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt @@ -216,7 +216,7 @@ class EventManualDialog( progressDialog?.dismiss() launch { b.timeDropdown.loadItems() - b.timeDropdown.selectDefault(editingEvent?.startTime) + b.timeDropdown.selectDefault(editingEvent?.time) b.timeDropdown.selectDefault(defaultLesson?.displayStartTime ?: defaultTime) } } @@ -251,7 +251,7 @@ class EventManualDialog( nextLessonTeamId = it.displayTeamId } loadItems() - selectDefault(editingEvent?.eventDate) + selectDefault(editingEvent?.date) selectDefault(defaultLesson?.displayDate ?: defaultDate) onDateSelected = { date, lesson -> b.timeDropdown.deselect() @@ -276,8 +276,8 @@ class EventManualDialog( displayMode = DISPLAY_LESSONS if (!loadItems()) syncTimetable(lessonsDate ?: Date.getToday()) - selectDefault(editingEvent?.startTime) - if (editingEvent != null && editingEvent.startTime == null) + selectDefault(editingEvent?.time) + if (editingEvent != null && editingEvent.time == null) select(0L) selectDefault(defaultLesson?.displayStartTime ?: defaultTime) onLessonSelected = { lesson -> @@ -319,7 +319,12 @@ class EventManualDialog( val deferred = async(Dispatchers.Default) { // get the event type list - val eventTypes = app.db.eventTypeDao().getAllNow(profileId) + var eventTypes = app.db.eventTypeDao().getAllNow(profileId) + + if (eventTypes.none { it.id in -1L..10L }) { + eventTypes = app.db.eventTypeDao().addDefaultTypes(activity, profileId) + } + b.typeDropdown.clear() b.typeDropdown += eventTypes.map { TextInputDropDown.Item(it.id, it.name, tag = it) } } @@ -338,10 +343,10 @@ class EventManualDialog( // copy IDs from event being edited editingEvent?.let { b.topic.setText(it.topic) - b.typeDropdown.select(it.type.toLong())?.let { item -> + b.typeDropdown.select(it.type)?.let { item -> customColor = (item.tag as EventType).color } - if (it.color != -1) + if (it.color != null && it.color != -1) customColor = it.color } @@ -464,22 +469,25 @@ class EventManualDialog( (timeSelected as? Pair<*, *>)?.first as? Time if (isError) return + date ?: return + topic ?: return val id = System.currentTimeMillis() val eventObject = Event( - profileId, - editingEvent?.id ?: id, - date, - startTime, - topic, - customColor ?: -1, - type ?: Event.TYPE_DEFAULT, - true, - teacherId ?: -1, - subjectId ?: -1, - teamId ?: -1 - ) + profileId = profileId, + id = editingEvent?.id ?: id, + date = date, + time = startTime, + topic = topic, + color = customColor, + type = type ?: Event.TYPE_DEFAULT, + teacherId = teacherId ?: -1, + subjectId = subjectId ?: -1, + teamId = teamId ?: -1 + ).also { + it.addedManually = true + } val metadataObject = Metadata( profileId, @@ -579,7 +587,7 @@ class EventManualDialog( private fun finishAdding(eventObject: Event, metadataObject: Metadata) { launch { withContext(Dispatchers.Default) { - app.db.eventDao().add(eventObject) + app.db.eventDao().upsert(eventObject) app.db.metadataDao().add(metadataObject) } } @@ -590,6 +598,7 @@ class EventManualDialog( activity.reloadTarget() } private fun finishRemoving() { + editingEvent ?: return launch { withContext(Dispatchers.Default) { app.db.eventDao().remove(editingEvent) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/NotificationFilterDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/NotificationFilterDialog.kt index eb8aecd3..ad31c02d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/NotificationFilterDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/NotificationFilterDialog.kt @@ -37,7 +37,8 @@ class NotificationFilterDialog( Notification.TYPE_NEW_ANNOUNCEMENT to R.string.notification_type_new_announcement, Notification.TYPE_NEW_SHARED_EVENT to R.string.notification_type_new_shared_event, Notification.TYPE_NEW_SHARED_HOMEWORK to R.string.notification_type_new_shared_homework, - Notification.TYPE_REMOVED_SHARED_EVENT to R.string.notification_type_removed_shared_event + Notification.TYPE_REMOVED_SHARED_EVENT to R.string.notification_type_removed_shared_event, + Notification.TYPE_TEACHER_ABSENCE to R.string.notification_type_new_teacher_absence ) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt index b384cb46..6dc6edb6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt @@ -78,7 +78,7 @@ class LessonDetailsDialog( ) } - if (App.devMode) + if (App.debugMode) b.lessonId.visibility = View.VISIBLE update() @@ -170,6 +170,12 @@ class LessonDetailsDialog( adapter = EventListAdapter( activity, + showWeekDay = false, + showDate = false, + showType = true, + showTime = true, + showSubject = true, + markAsSeen = true, onItemClick = { EventDetailsDialog( activity, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt index bddd805a..9ee93be9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt @@ -222,22 +222,22 @@ class AgendaFragment : Fragment(), CoroutineScope { events.forEach { event -> eventList.add(BaseCalendarEvent( - "${event.typeName} - ${event.topic}", + "${event.typeName ?: "wydarzenie"} - ${event.topic}", "", - (if (event.startTime == null) getString(R.string.agenda_event_all_day) else event.startTime!!.stringHM) + + (if (event.time == null) getString(R.string.agenda_event_all_day) else event.time!!.stringHM) + (event.subjectLongName?.let { ", $it" } ?: "") + - (event.teacherFullName?.let { ", $it" } ?: "") + + (event.teacherName?.let { ", $it" } ?: "") + (event.teamName?.let { ", $it" } ?: ""), - event.getColor(), - Colors.legibleTextColor(event.getColor()), + event.eventColor, + Colors.legibleTextColor(event.eventColor), event.startTimeCalendar, event.endTimeCalendar, - event.startTime == null, + event.time == null, event.id, !event.seen )) - if (!event.seen) unreadEventDates.add(event.eventDate.value) + if (!event.seen) unreadEventDates.add(event.date.value) } b.agendaDefaultView.init(eventList, minDate, maxDate, Locale.getDefault(), object : CalendarPickerController { @@ -281,11 +281,11 @@ class AgendaFragment : Fragment(), CoroutineScope { val eventIcon = IconicsDrawable(activity) .icon(CommunityMaterial.Icon.cmd_checkbox_blank_circle) .size(IconicsSize.dp(10)) - .color(IconicsColor.colorInt(event.getColor())) + .color(IconicsColor.colorInt(event.eventColor)) dayList.add(EventDay(event.startTimeCalendar, eventIcon)) - if (!event.seen) unreadEventDates.add(event.eventDate.value) + if (!event.seen) unreadEventDates.add(event.date.value) } b.agendaCalendarView.setEvents(dayList) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/PagerFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/PagerFragment.kt deleted file mode 100644 index 9a0a7b9c..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/PagerFragment.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2020-2-22. - */ - -package pl.szczodrzynski.edziennik.ui.modules.base - -import androidx.fragment.app.Fragment - -abstract class PagerFragment : Fragment() { - private var isPageCreated = false - - /** - * Called when the page is first shown, or if previous - * [onPageCreated] returned false - * - * @return true if the view is set up - * @return false if the setup failed. The method may be then called - * again, when page becomes visible. - */ - abstract fun onPageCreated(): Boolean - - override fun onResume() { - if (!isPageCreated) { - isPageCreated = onPageCreated() - } - super.onResume() - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/FragmentLazyPagerAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/FragmentLazyPagerAdapter.kt new file mode 100644 index 00000000..d6205596 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/FragmentLazyPagerAdapter.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-30. + */ + +package pl.szczodrzynski.edziennik.ui.modules.base.lazypager + +import androidx.fragment.app.FragmentManager +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout + +class FragmentLazyPagerAdapter( + fragmentManager: FragmentManager, + swipeRefreshLayout: SwipeRefreshLayout, + val fragments: List> +) : LazyPagerAdapter(fragmentManager, swipeRefreshLayout) { + override fun getPage(position: Int) = fragments[position].first + override fun getPageTitle(position: Int) = fragments[position].second + override fun getCount() = fragments.size +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/LazyFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/LazyFragment.kt new file mode 100644 index 00000000..b5f26172 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/LazyFragment.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-29. + */ + +package pl.szczodrzynski.edziennik.ui.modules.base.lazypager + +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.RecyclerView + +abstract class LazyFragment : Fragment() { + private var isPageCreated = false + internal var position = -1 + internal var swipeRefreshLayoutCallback: ((position: Int, isEnabled: Boolean) -> Unit)? = null + internal var onPageDestroy: ((position: Int, outState: Bundle?) -> Unit?)? = null + + /** + * Called when the page is first shown, or if previous + * [onPageCreated] returned false + * + * @return true if the view is set up + * @return false if the setup failed. The method may be then called + * again, when page becomes visible. + */ + abstract fun onPageCreated(): Boolean + + fun enableSwipeToRefresh() = swipeRefreshLayoutCallback?.invoke(position, true) + fun disableSwipeToRefresh() = swipeRefreshLayoutCallback?.invoke(position, false) + fun setSwipeToRefresh(enabled: Boolean) = swipeRefreshLayoutCallback?.invoke(position, enabled) + + val onScrollListener: RecyclerView.OnScrollListener + get() = object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + if (recyclerView.canScrollVertically(-1)) + disableSwipeToRefresh() + if (!recyclerView.canScrollVertically(-1) && newState == RecyclerView.SCROLL_STATE_IDLE) + enableSwipeToRefresh() + } + } + + internal fun createPage() { + if (!isPageCreated && isAdded) { + isPageCreated = onPageCreated() + } + } + + override fun onDestroyView() { + isPageCreated = false + super.onDestroyView() + } + + override fun onResume() { + createPage() + super.onResume() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/LazyPagerAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/LazyPagerAdapter.kt new file mode 100644 index 00000000..b70de59e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/LazyPagerAdapter.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-29. + */ + +package pl.szczodrzynski.edziennik.ui.modules.base.lazypager + +import android.util.SparseBooleanArray +import androidx.core.util.set +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentStatePagerAdapter +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout + +abstract class LazyPagerAdapter(fragmentManager: FragmentManager, val swipeRefreshLayout: SwipeRefreshLayout? = null) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + internal val enabledList = SparseBooleanArray() + private val refreshLayoutCallback: (position: Int, isEnabled: Boolean) -> Unit = { position, isEnabled -> + swipeRefreshLayout?.isEnabled = isEnabled + if (position > -1) + enabledList[position] = isEnabled + } + final override fun getItem(position: Int): LazyFragment { + return getPage(position).also { + it.position = position + it.swipeRefreshLayoutCallback = refreshLayoutCallback + } + } + abstract fun getPage(position: Int): LazyFragment + abstract override fun getPageTitle(position: Int): CharSequence +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/LazyViewPager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/LazyViewPager.kt new file mode 100644 index 00000000..45337990 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/LazyViewPager.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-29. + */ + +package pl.szczodrzynski.edziennik.ui.modules.base.lazypager + +import android.content.Context +import android.util.AttributeSet +import androidx.viewpager.widget.ViewPager + +class LazyViewPager @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : ViewPager(context, attrs) { + + private var pageSelection = -1 + private var scrollState = 0 + + init { + addOnPageChangeListener(object : OnPageChangeListener { + override fun onPageScrollStateChanged(state: Int) { + scrollState = state + (adapter as? LazyPagerAdapter)?.let { + it.swipeRefreshLayout?.isEnabled = state == SCROLL_STATE_IDLE && it.enabledList[pageSelection, true] + } + } + + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { + + } + + override fun onPageSelected(position: Int) { + pageSelection = position + (adapter as? LazyPagerAdapter)?.let { + it.swipeRefreshLayout?.isEnabled = scrollState == SCROLL_STATE_IDLE && it.enabledList[pageSelection, true] + val fragment = adapter?.instantiateItem(this@LazyViewPager, position) + val lazyFragment = fragment as? LazyFragment + lazyFragment?.createPage() + } + } + }) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/DebugFragment.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/DebugFragment.java similarity index 98% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/DebugFragment.java rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/DebugFragment.java index 57a4e00f..63ef7f51 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/DebugFragment.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/DebugFragment.java @@ -1,4 +1,8 @@ -package pl.szczodrzynski.edziennik.ui.modules.base; +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-3. + */ + +package pl.szczodrzynski.edziennik.ui.modules.debug; import android.os.Bundle; import android.util.Log; diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabFragment.kt new file mode 100644 index 00000000..35f16c20 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabFragment.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-3. + */ + +package pl.szczodrzynski.edziennik.ui.modules.debug + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.sqlite.db.SimpleSQLiteQuery +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.data.db.entity.Event +import pl.szczodrzynski.edziennik.databinding.LabFragmentBinding +import pl.szczodrzynski.edziennik.onClick +import kotlin.coroutines.CoroutineContext + +class LabFragment : Fragment(), CoroutineScope { + companion object { + private const val TAG = "LabFragment" + } + + private lateinit var app: App + private lateinit var activity: MainActivity + private lateinit var b: LabFragmentBinding + + private val job: Job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + // local/private variables go here + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + activity = (getActivity() as MainActivity?) ?: return null + context ?: return null + app = activity.application as App + b = LabFragmentBinding.inflate(inflater) + return b.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + if (!isAdded) return + + b.last10unseen.onClick { + launch(Dispatchers.Default) { + val events = app.db.eventDao().getAllNow(App.profileId) + val ids = events.sortedBy { it.date }.filter { it.type == Event.TYPE_HOMEWORK }.takeLast(10) + ids.forEach { + app.db.metadataDao().setSeen(App.profileId, it, false) + } + } + } + + b.rodo.onClick { + app.db.teacherDao().query(SimpleSQLiteQuery("UPDATE teachers SET teacherSurname = \"\" WHERE profileId = ${App.profileId}")) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorDetailsDialog.kt index 32ec46b9..36a64a3e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorDetailsDialog.kt @@ -45,7 +45,7 @@ class ErrorDetailsDialog( listOf( it.getStringReason(activity).asBoldSpannable().asColoredSpannable(R.attr.colorOnBackground.resolveAttr(activity)), activity.getString(R.string.error_unknown_format, it.errorCode, it.tag), - if (App.devMode) + if (App.debugMode) it.throwable?.stackTraceString ?: it.throwable?.localizedMessage else it.throwable?.localizedMessage diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradeView.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradeView.kt index 334ff238..b80ad2ac 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradeView.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradeView.kt @@ -49,28 +49,29 @@ class GradeView : AppCompatTextView { val gradeColor = manager.getGradeColor(grade) - text = if (periodGradesTextual) - when (grade.type) { + text = when { + periodGradesTextual -> when (grade.type) { TYPE_SEMESTER1_PROPOSED, TYPE_SEMESTER2_PROPOSED -> context.getString( - R.string.grade_semester_proposed_format, - gradeName + R.string.grade_semester_proposed_format, + gradeName ) TYPE_SEMESTER1_FINAL, TYPE_SEMESTER2_FINAL -> context.getString( - R.string.grade_semester_final_format, - gradeName + R.string.grade_semester_final_format, + gradeName ) TYPE_YEAR_PROPOSED -> context.getString( - R.string.grade_year_proposed_format, - gradeName + R.string.grade_year_proposed_format, + gradeName ) TYPE_YEAR_FINAL -> context.getString( - R.string.grade_year_final_format, - gradeName + R.string.grade_year_final_format, + gradeName ) else -> gradeName } - else - gradeName + gradeName.isBlank() -> " " + else -> gradeName + } setTextColor(when (grade.type) { TYPE_SEMESTER1_PROPOSED, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesAdapter.kt index 4b1c4f1e..46c75d8d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesAdapter.kt @@ -99,10 +99,31 @@ class GradesAdapter( } } if (model is GradesSubject) { + // hide the preview, show summary val preview = view?.findViewById(R.id.previewContainer) val summary = view?.findViewById(R.id.yearSummary) preview?.visibility = if (model.state == STATE_CLOSED) View.INVISIBLE else View.VISIBLE summary?.visibility = if (model.state == STATE_CLOSED) View.VISIBLE else View.INVISIBLE + + // expanding a subject - mark proposed & final grade as seen + var unseenChanged = false + if (model.proposedGrade?.seen == false) { + manager.markAsSeen(model.proposedGrade!!) + unseenChanged = true + } + if (model.finalGrade?.seen == false) { + manager.markAsSeen(model.finalGrade!!) + unseenChanged = true + } + // remove the override flag + model.hasUnseen = false + + if (unseenChanged) { + // check if the unseen status has changed + if (!model.hasUnseen) { + notifyItemChanged(model) + } + } } if (model.state == STATE_CLOSED) { @@ -166,7 +187,7 @@ class GradesAdapter( override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val item = items[position] - if (holder !is BindableViewHolder<*>) + if (holder !is BindableViewHolder<*, *>) return val viewType = when (holder) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesListFragment.kt similarity index 81% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesListFragment.kt index ba125d9c..901ad0cf 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesListFragment.kt @@ -10,11 +10,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial.Icon2 import kotlinx.coroutines.* @@ -23,7 +22,7 @@ import pl.szczodrzynski.edziennik.MainActivity.Companion.TARGET_GRADES_EDITOR import pl.szczodrzynski.edziennik.data.db.entity.Grade import pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_GRADE import pl.szczodrzynski.edziennik.data.db.full.GradeFull -import pl.szczodrzynski.edziennik.databinding.GradesFragmentBinding +import pl.szczodrzynski.edziennik.databinding.GradesListFragmentBinding import pl.szczodrzynski.edziennik.ui.dialogs.grade.GradeDetailsDialog import pl.szczodrzynski.edziennik.ui.dialogs.settings.GradesConfigDialog import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesAverages @@ -36,24 +35,20 @@ import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem import kotlin.coroutines.CoroutineContext import kotlin.math.max - -class GradesFragment : Fragment(), CoroutineScope { +class GradesListFragment : Fragment(), CoroutineScope { companion object { private const val TAG = "GradesFragment" } private lateinit var app: App private lateinit var activity: MainActivity - private lateinit var b: GradesFragmentBinding + private lateinit var b: GradesListFragmentBinding private val job: Job = Job() override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main // local/private variables go here - private val adapter by lazy { - GradesAdapter(activity) - } private val manager by lazy { app.gradesManager } private val dontCountEnabled by lazy { manager.dontCountEnabled } private val dontCountGrades by lazy { manager.dontCountGrades } @@ -63,51 +58,49 @@ class GradesFragment : Fragment(), CoroutineScope { activity = (getActivity() as MainActivity?) ?: return null context ?: return null app = activity.application as App - b = GradesFragmentBinding.inflate(inflater) + b = GradesListFragmentBinding.inflate(inflater) b.refreshLayout.setParent(activity.swipeRefreshLayout) return b.root } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - if (!isAdded) - return + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { startCoroutineTimer(100L) { + if (!isAdded) return@startCoroutineTimer expandSubjectId = arguments?.getLong("gradesSubjectId") ?: 0L - app.db.gradeDao() - .getAllOrderBy(App.profileId, app.gradesManager.getOrderByString()) - .observe(this, Observer { grades -> - if (b.gradesRecyclerView.adapter == null) { - b.gradesRecyclerView.adapter = adapter - b.gradesRecyclerView.apply { - setHasFixedSize(true) - layoutManager = LinearLayoutManager(context) - //addItemDecoration(SimpleDividerItemDecoration(context)) - addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { - if (recyclerView.canScrollVertically(-1)) { - b.refreshLayout.isEnabled = false - } - if (!recyclerView.canScrollVertically(-1) && newState == SCROLL_STATE_IDLE) { - b.refreshLayout.isEnabled = true - } - } - }) - } - } + val adapter = GradesAdapter(activity) + var firstRun = true - launch(Dispatchers.Default) { - processGrades(grades) - } + app.db.gradeDao().getAllOrderBy(App.profileId, app.gradesManager.getOrderByString()).observe(this@GradesListFragment, Observer { items -> this@GradesListFragment.launch { + if (!isAdded) return@launch - if (grades != null && grades.isNotEmpty()) { - b.gradesRecyclerView.visibility = View.VISIBLE - b.gradesNoData.visibility = View.GONE - } else { - b.gradesRecyclerView.visibility = View.GONE - b.gradesNoData.visibility = View.VISIBLE - } - }) + // load & configure the adapter + adapter.items = withContext(Dispatchers.Default) { processGrades(items) } + if (items.isNotNullNorEmpty() && b.list.adapter == null) { + b.list.adapter = adapter + b.list.apply { + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + addOnScrollListener(b.refreshLayout.onScrollListener) + } + } + adapter.notifyDataSetChanged() + + if (firstRun) { + expandSubject(adapter) + firstRun = false + } + + // show/hide relevant views + b.progressBar.isVisible = false + if (items.isNullOrEmpty()) { + b.list.isVisible = false + b.noData.isVisible = true + } else { + b.list.isVisible = true + b.noData.isVisible = false + } + }}) adapter.onGradeClick = { GradeDetailsDialog(activity, it) @@ -153,10 +146,30 @@ class GradesFragment : Fragment(), CoroutineScope { }) ) activity.gainAttention() + }} + + private fun expandSubject(adapter: GradesAdapter) { + var expandSubjectModel: GradesSubject? = null + if (expandSubjectId != 0L) { + expandSubjectModel = adapter.items.firstOrNull { it is GradesSubject && it.subjectId == expandSubjectId } as? GradesSubject + adapter.expandModel( + model = expandSubjectModel, + view = null, + notifyAdapter = false + ) + } + + startCoroutineTimer(500L) { + if (expandSubjectModel != null) { + b.list.smoothScrollToPosition( + adapter.items.indexOf(expandSubjectModel) + expandSubjectModel.semesters.size + (expandSubjectModel.semesters.firstOrNull()?.grades?.size ?: 0) + ) + } + } } @Suppress("SuspendFunctionOnCoroutineScope") - private suspend fun processGrades(grades: List) { + private fun processGrades(grades: List): MutableList { val items = mutableListOf() var subjectId = -1L @@ -191,7 +204,10 @@ class GradesFragment : Fragment(), CoroutineScope { grade.showAsUnseen = !grade.seen if (!grade.seen) { - semester.hasUnseen = true + if (grade.type == Grade.TYPE_YEAR_PROPOSED || grade.type == Grade.TYPE_YEAR_FINAL) + subject.hasUnseen = true // set an override flag + else + semester.hasUnseen = true } when (grade.type) { @@ -284,30 +300,7 @@ class GradesFragment : Fragment(), CoroutineScope { GradesManager.ORDER_BY_DATE_ASC -> items.sortBy { it.lastAddedDate } } - adapter.items = items.toMutableList() - adapter.items.add(stats) - - var expandSubjectModel: GradesSubject? = null - if (expandSubjectId != 0L) { - expandSubjectModel = items.firstOrNull { it.subjectId == expandSubjectId } - adapter.expandModel( - model = expandSubjectModel, - view = null, - notifyAdapter = false - ) - } - - withContext(Dispatchers.Main) { - adapter.notifyDataSetChanged() - } - - startCoroutineTimer(500L, 0L) { - if (expandSubjectModel != null) { - b.gradesRecyclerView.smoothScrollToPosition( - items.indexOf(expandSubjectModel) + expandSubjectModel.semesters.size + (expandSubjectModel.semesters.firstOrNull()?.grades?.size ?: 0) - ) - } - } + return (items + stats).toMutableList() } private fun countGrade(grade: Grade, averages: GradesAverages) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/GradesSubject.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/GradesSubject.kt index 89b07e6f..a61abc0d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/GradesSubject.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/GradesSubject.kt @@ -16,8 +16,8 @@ data class GradesSubject( var lastAddedDate = 0L var semester: Int = 1 - val hasUnseen - get() = semesters.any { it.hasUnseen } + var hasUnseen: Boolean = false + get() = field || semesters.any { it.hasUnseen } val averages = GradesAverages() var proposedGrade: GradeFull? = null diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/BindableViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/BindableViewHolder.kt index 876d9055..6dd83384 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/BindableViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/BindableViewHolder.kt @@ -6,8 +6,7 @@ package pl.szczodrzynski.edziennik.ui.modules.grades.viewholder import androidx.appcompat.app.AppCompatActivity import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter -interface BindableViewHolder { - fun onBind(activity: AppCompatActivity, app: App, item: T, position: Int, adapter: GradesAdapter) +interface BindableViewHolder { + fun onBind(activity: AppCompatActivity, app: App, item: T, position: Int, adapter: A) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/EmptyViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/EmptyViewHolder.kt index ea28847e..ec0eb8ba 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/EmptyViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/EmptyViewHolder.kt @@ -17,7 +17,7 @@ class EmptyViewHolder( inflater: LayoutInflater, parent: ViewGroup, val b: GradesItemEmptyBinding = GradesItemEmptyBinding.inflate(inflater, parent, false) -) : RecyclerView.ViewHolder(b.root), BindableViewHolder { +) : RecyclerView.ViewHolder(b.root), BindableViewHolder { companion object { private const val TAG = "EmptyViewHolder" } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/GradeViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/GradeViewHolder.kt index 9927eabe..e71b7819 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/GradeViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/GradeViewHolder.kt @@ -21,7 +21,7 @@ class GradeViewHolder( inflater: LayoutInflater, parent: ViewGroup, val b: GradesItemGradeBinding = GradesItemGradeBinding.inflate(inflater, parent, false) -) : RecyclerView.ViewHolder(b.root), BindableViewHolder { +) : RecyclerView.ViewHolder(b.root), BindableViewHolder { companion object { private const val TAG = "GradeViewHolder" } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SemesterViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SemesterViewHolder.kt index d10d671e..5686a37f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SemesterViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SemesterViewHolder.kt @@ -21,7 +21,7 @@ class SemesterViewHolder( inflater: LayoutInflater, parent: ViewGroup, val b: GradesItemSemesterBinding = GradesItemSemesterBinding.inflate(inflater, parent, false) -) : RecyclerView.ViewHolder(b.root), BindableViewHolder { +) : RecyclerView.ViewHolder(b.root), BindableViewHolder { companion object { private const val TAG = "SemesterViewHolder" } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/StatsViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/StatsViewHolder.kt index 3e6a6be2..de1b071f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/StatsViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/StatsViewHolder.kt @@ -24,7 +24,7 @@ class StatsViewHolder( inflater: LayoutInflater, parent: ViewGroup, val b: GradesItemStatsBinding = GradesItemStatsBinding.inflate(inflater, parent, false) -) : RecyclerView.ViewHolder(b.root), BindableViewHolder { +) : RecyclerView.ViewHolder(b.root), BindableViewHolder { companion object { private const val TAG = "StatsViewHolder" } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SubjectViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SubjectViewHolder.kt index e20a5177..0dce6297 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SubjectViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SubjectViewHolder.kt @@ -28,7 +28,7 @@ class SubjectViewHolder( inflater: LayoutInflater, parent: ViewGroup, val b: GradesItemSubjectBinding = GradesItemSubjectBinding.inflate(inflater, parent, false) -) : RecyclerView.ViewHolder(b.root), BindableViewHolder { +) : RecyclerView.ViewHolder(b.root), BindableViewHolder { companion object { private const val TAG = "SubjectViewHolder" } @@ -96,6 +96,7 @@ class SubjectViewHolder( ellipsize = TextUtils.TruncateAt.END }) + // add the topmost semester's grades to preview container (collapsed) firstSemester.proposedGrade?.let { b.previewContainer.addView(GradeView( contextWrapper, @@ -111,6 +112,23 @@ class SubjectViewHolder( )) } + // add the yearly grades to summary container (expanded) + item.proposedGrade?.let { + b.yearContainer.addView(GradeView( + contextWrapper, + it, + manager + )) + } + item.finalGrade?.let { + b.yearContainer.addView(GradeView( + contextWrapper, + it, + manager + )) + } + + // if showing semester 2, add yearly grades to preview container (collapsed) if (firstSemester.number == item.semester) { b.previewContainer.addView(TextView(contextWrapper).apply { text = manager.getAverageString(app, item.averages, nameSemester = true) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeEventsCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeEventsCard.kt index e6b2b23b..9a831c0f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeEventsCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeEventsCard.kt @@ -60,8 +60,12 @@ class HomeEventsCard( adapter = EventListAdapter( activity, simpleMode = true, - showDate = true, showWeekDay = true, + showDate = true, + showType = true, + showTime = false, + showSubject = false, + markAsSeen = false, onItemClick = { EventDetailsDialog( activity, @@ -77,7 +81,7 @@ class HomeEventsCard( } ) - app.db.eventDao().getAllNearest(profile.id, Date.getToday(), 4).observe(activity, Observer { events -> + app.db.eventDao().getNearestNotDone(profile.id, Date.getToday(), 4).observe(activity, Observer { events -> adapter.items = events if (b.eventsView.adapter == null) { b.eventsView.adapter = adapter diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkAdapter.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkAdapter.java index c8b42fbb..0a568464 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkAdapter.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkAdapter.java @@ -50,17 +50,17 @@ public class HomeworkAdapter extends RecyclerView.Adapter { App.db.metadataDao().setSeen(App.Companion.getProfileId(), homework, true); }); @@ -69,11 +69,11 @@ public class HomeworkAdapter extends RecyclerView.Adapter { new EventManualDialog( (MainActivity) context, - homework.profileId, + homework.getProfileId(), null, null, null, @@ -83,11 +83,11 @@ public class HomeworkAdapter extends RecyclerView.Adapter - adapter.addFragment(HomeworkListFragment().also { fragment -> - fragment.arguments = Bundle().also { args -> - args.putInt("homeworkDate", HomeworkDate.CURRENT) - } - }, getString(R.string.homework_tab_current)) + val pagerAdapter = FragmentLazyPagerAdapter( + fragmentManager ?: return, + b.refreshLayout, + listOf( + HomeworkListFragment().apply { + arguments = Bundle("homeworkDate" to HomeworkDate.CURRENT) + } to getString(R.string.homework_tab_current), - adapter.addFragment(HomeworkListFragment().also { fragment -> - fragment.arguments = Bundle().also { args -> - args.putInt("homeworkDate", HomeworkDate.PAST) - } - }, getString(R.string.homework_tab_past)) + HomeworkListFragment().apply { + arguments = Bundle("homeworkDate" to HomeworkDate.PAST) + } to getString(R.string.homework_tab_past) + ) + ) + b.viewPager.apply { + offscreenPageLimit = 1 + adapter = pagerAdapter + currentItem = pageSelection + addOnPageSelectedListener { + pageSelection = it + } + b.tabLayout.setupWithViewPager(this) } - b.viewPager.currentItem = pageSelection - b.viewPager.clearOnPageChangeListeners() - b.viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { - override fun onPageScrollStateChanged(state: Int) { - b.refreshLayout.isEnabled = state == ViewPager.SCROLL_STATE_IDLE - } - override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} - override fun onPageSelected(position: Int) { - pageSelection = position - } - }) - - b.tabLayout.setupWithViewPager(b.viewPager) - activity.navView.apply { bottomBar.apply { fabEnable = true diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkListFragment.kt index 950e6172..1852c340 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkListFragment.kt @@ -4,67 +4,107 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.Fragment +import androidx.core.view.isVisible import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.MainActivity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.Event -import pl.szczodrzynski.edziennik.databinding.HomeworkListBinding -import pl.szczodrzynski.edziennik.getInt +import pl.szczodrzynski.edziennik.databinding.HomeworkListFragmentBinding +import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog +import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter +import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog +import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment +import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import pl.szczodrzynski.edziennik.utils.models.Date +import kotlin.coroutines.CoroutineContext -class HomeworkListFragment : Fragment() { +class HomeworkListFragment : LazyFragment(), CoroutineScope { + companion object { + private const val TAG = "HomeworkListFragment" + } private lateinit var app: App private lateinit var activity: MainActivity - private lateinit var b: HomeworkListBinding + private lateinit var b: HomeworkListFragmentBinding - private var homeworkDate = HomeworkDate.CURRENT + private val job: Job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + // local/private variables go here override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { activity = (getActivity() as MainActivity?) ?: return null context ?: return null app = activity.application as App - b = HomeworkListBinding.inflate(inflater) + b = HomeworkListFragmentBinding.inflate(inflater) return b.root } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - // TODO check if app, activity, b can be null - if (!isAdded) - return - - if (arguments != null) { - homeworkDate = arguments.getInt("homeworkDate", HomeworkDate.CURRENT) - } - - val layoutManager = LinearLayoutManager(context) - layoutManager.reverseLayout = homeworkDate == HomeworkDate.PAST - layoutManager.stackFromEnd = homeworkDate == HomeworkDate.PAST - - b.homeworkView.setHasFixedSize(true) - b.homeworkView.layoutManager = layoutManager + override fun onPageCreated(): Boolean { startCoroutineTimer(100L) { + val homeworkDate = arguments.getInt("homeworkDate", HomeworkDate.CURRENT) + val today = Date.getToday() val filter = when(homeworkDate) { - HomeworkDate.CURRENT -> "eventDate >= '" + Date.getToday().stringY_m_d + "'" - else -> "eventDate < '" + Date.getToday().stringY_m_d + "'" + HomeworkDate.CURRENT -> "eventDate >= '${today.stringY_m_d}' AND eventIsDone = 0" + else -> "(eventDate < '${today.stringY_m_d}' OR eventIsDone = 1)" } - app.db.eventDao() - .getAllByType(App.profileId, Event.TYPE_HOMEWORK, filter) - .observe(this, Observer { homeworkList -> - if (!isAdded) return@Observer + val adapter = EventListAdapter( + activity, + showWeekDay = true, + showDate = true, + showType = false, + showTime = true, + showSubject = true, + markAsSeen = true, + onItemClick = { + EventDetailsDialog( + activity, + it + ) + }, + onEventEditClick = { + EventManualDialog( + activity, + it.profileId, + editingEvent = it + ) + } + ) - if (homeworkList != null && homeworkList.size > 0) { - val adapter = HomeworkAdapter(context, homeworkList) - b.homeworkView.adapter = adapter - b.homeworkView.visibility = View.VISIBLE - b.homeworkNoData.visibility = View.GONE - } else { - b.homeworkView.visibility = View.GONE - b.homeworkNoData.visibility = View.VISIBLE + app.db.eventDao().getAllByType(App.profileId, Event.TYPE_HOMEWORK, filter).observe(this@HomeworkListFragment, Observer { items -> + if (!isAdded) return@Observer + + // load & configure the adapter + adapter.items = items + if (items.isNotNullNorEmpty() && b.list.adapter == null) { + b.list.adapter = adapter + b.list.apply { + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context).apply { + reverseLayout = homeworkDate == HomeworkDate.PAST + stackFromEnd = homeworkDate == HomeworkDate.PAST } - }) - } + addItemDecoration(SimpleDividerItemDecoration(context)) + addOnScrollListener(onScrollListener) + } + } + adapter.notifyDataSetChanged() + setSwipeToRefresh(items.isNullOrEmpty()) + + // show/hide relevant views + b.progressBar.isVisible = false + if (items.isNullOrEmpty()) { + b.list.isVisible = false + b.noData.isVisible = true + } else { + b.list.isVisible = true + b.noData.isVisible = false + } + }) + }; return true } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSyncFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSyncFragment.kt index 572008ab..789bcb07 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSyncFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSyncFragment.kt @@ -19,8 +19,6 @@ import pl.szczodrzynski.edziennik.data.api.events.ApiTaskAllFinishedEvent import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent import pl.szczodrzynski.edziennik.data.api.events.ApiTaskProgressEvent import pl.szczodrzynski.edziennik.data.api.events.ApiTaskStartedEvent -import pl.szczodrzynski.edziennik.data.db.entity.Event.* -import pl.szczodrzynski.edziennik.data.db.entity.EventType import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.REGISTRATION_DISABLED import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.REGISTRATION_ENABLED import pl.szczodrzynski.edziennik.databinding.FragmentLoginSyncBinding @@ -62,20 +60,7 @@ class LoginSyncFragment : Fragment(), CoroutineScope { else REGISTRATION_DISABLED - val typeList = listOf( - EventType(it.id, TYPE_HOMEWORK, getString(R.string.event_type_homework), COLOR_HOMEWORK), - EventType(it.id, TYPE_DEFAULT, getString(R.string.event_other), COLOR_DEFAULT), - EventType(it.id, TYPE_EXAM, getString(R.string.event_exam), COLOR_EXAM), - EventType(it.id, TYPE_SHORT_QUIZ, getString(R.string.event_short_quiz), COLOR_SHORT_QUIZ), - EventType(it.id, TYPE_ESSAY, getString(R.string.event_essay), COLOR_SHORT_QUIZ), - EventType(it.id, TYPE_PROJECT, getString(R.string.event_project), COLOR_PROJECT), - EventType(it.id, TYPE_PT_MEETING, getString(R.string.event_pt_meeting), COLOR_PT_MEETING), - EventType(it.id, TYPE_EXCURSION, getString(R.string.event_excursion), COLOR_EXCURSION), - EventType(it.id, TYPE_READING, getString(R.string.event_reading), COLOR_READING), - EventType(it.id, TYPE_CLASS_EVENT, getString(R.string.event_class_event), COLOR_CLASS_EVENT), - EventType(it.id, TYPE_INFORMATION, getString(R.string.event_information), COLOR_INFORMATION) - ) - app.db.eventTypeDao().addAll(typeList) + app.db.eventTypeDao().addDefaultTypes(activity, it.id) } app.db.profileDao().addAll(profiles) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt index ef6f6518..1a94d4a4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt @@ -5,24 +5,16 @@ package pl.szczodrzynski.edziennik.ui.modules.messages import android.os.Bundle -import android.os.Environment import android.text.Html -import android.text.TextUtils -import android.view.Gravity.CENTER_VERTICAL -import android.view.Gravity.END import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.ProgressBar +import android.widget.Toast +import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import com.google.android.material.chip.Chip -import com.mikepenz.iconics.IconicsColor +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.mikepenz.iconics.IconicsDrawable -import com.mikepenz.iconics.IconicsSize -import com.mikepenz.iconics.typeface.IIcon import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial -import com.mikepenz.iconics.typeface.library.szkolny.font.SzkolnyFont import com.mikepenz.iconics.utils.sizeDp import kotlinx.coroutines.* import org.greenrobot.eventbus.EventBus @@ -30,26 +22,19 @@ import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask -import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent -import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent.Companion.TYPE_FINISHED -import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent.Companion.TYPE_PROGRESS import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.LoginStore.Companion.LOGIN_TYPE_IDZIENNIK -import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_RECEIVED -import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_SENT +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_DELETED +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED +import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.databinding.MessageFragmentBinding import pl.szczodrzynski.edziennik.utils.Anim import pl.szczodrzynski.edziennik.utils.BetterLink -import pl.szczodrzynski.edziennik.utils.Themes -import pl.szczodrzynski.edziennik.utils.Utils -import pl.szczodrzynski.edziennik.utils.Utils.getStringFromFile -import pl.szczodrzynski.edziennik.utils.Utils.readableFileSize import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.navlib.colorAttr -import java.io.File import kotlin.coroutines.CoroutineContext import kotlin.math.min @@ -62,27 +47,22 @@ class MessageFragment : Fragment(), CoroutineScope { private lateinit var activity: MainActivity private lateinit var b: MessageFragmentBinding - private lateinit var job: Job + private val job: Job = Job() override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main private lateinit var message: MessageFull - private var attachmentList = mutableListOf() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { activity = (getActivity() as MainActivity?) ?: return null context ?: return null app = activity.application as App - context!!.theme.applyStyle(Themes.appTheme, true) b = MessageFragmentBinding.inflate(inflater) - job = Job() return b.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - // TODO check if app, activity, b can be null - if (app.profile == null || !isAdded) - return + if (!isAdded) return b.closeButton.setImageDrawable( IconicsDrawable(activity, CommunityMaterial.Icon2.cmd_window_close) @@ -111,6 +91,23 @@ class MessageFragment : Fragment(), CoroutineScope { "type" to "forward" )) } + b.deleteButton.onClick { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.messages_delete_confirmation) + .setMessage(R.string.messages_delete_confirmation_text) + .setPositiveButton(R.string.ok) { _, _ -> + launch { + message.type = TYPE_DELETED + withContext(Dispatchers.Default) { + app.db.messageDao().replace(message) + } + Toast.makeText(activity, "Wiadomość przeniesiona do usuniętych", Toast.LENGTH_SHORT).show() + activity.navigateUp() + } + } + .setNegativeButton(R.string.cancel, null) + .show() + } launch { @@ -126,14 +123,26 @@ class MessageFragment : Fragment(), CoroutineScope { val msg = if (messageString != null) app.gson.fromJson(messageString, MessageFull::class.java)?.also { - //it.body = null - it.addedDate = arguments?.getLong("sentDate") ?: System.currentTimeMillis() + if (arguments?.getLong("sentDate") ?: 0L > 0L) + it.addedDate = arguments?.getLong("sentDate") ?: 0L } else - app.db.messageDao().getById(App.profileId, messageId) + app.db.messageDao().getByIdNow(App.profileId, messageId) + + // load recipients in sent messages + val teachers by lazy { app.db.teacherDao().getAllNow(App.profileId) } + msg?.recipients?.forEach { recipient -> + if (recipient.fullName == null) { + recipient.fullName = teachers.firstOrNull { it.id == recipient.id }?.fullName ?: "" + } + } + + if (msg?.type == TYPE_SENT && msg.senderName == null) { + msg.senderName = app.profile.accountName ?: app.profile.studentNameLong + } msg?.also { - it.recipients = app.db.messageRecipientDao().getAllByMessageId(it.profileId, it.id) + //it.recipients = app.db.messageRecipientDao().getAllByMessageId(it.profileId, it.id) if (it.body != null && !it.seen) { app.db.metadataDao().setSeen(it.profileId, it, true) } @@ -199,7 +208,7 @@ class MessageFragment : Fragment(), CoroutineScope { } private fun showMessage() { - b.body.text = MessagesUtils.htmlToSpannable(activity, message.body ?: "") + b.body.text = MessagesUtils.htmlToSpannable(activity, message.body.toString()) b.date.text = getString(R.string.messages_date_time_format, Date.fromMillis(message.addedDate).formattedStringShort, Time.fromMillis(message.addedDate).stringHM) val messageInfo = MessagesUtils.getMessageInfo(app, message, 40, 20, 14, 10) @@ -208,8 +217,9 @@ class MessageFragment : Fragment(), CoroutineScope { b.subject.text = message.subject - b.replyButton.visibility = if (message.type == TYPE_RECEIVED) View.VISIBLE else View.INVISIBLE - if (message.type == TYPE_RECEIVED) { + b.replyButton.isVisible = message.type == TYPE_RECEIVED || message.type == TYPE_DELETED + b.deleteButton.isVisible = message.type == TYPE_RECEIVED + if (message.type == TYPE_RECEIVED || message.type == TYPE_DELETED) { activity.navView.apply { bottomBar.apply { fabEnable = true @@ -261,154 +271,20 @@ class MessageFragment : Fragment(), CoroutineScope { } private fun showAttachments() { - if (message.attachmentIds != null) { - val insertPoint = b.attachments - insertPoint.removeAllViews() - - val chipLayoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) - chipLayoutParams.setMargins(0, Utils.dpToPx(8), 0, Utils.dpToPx(8)) - - val progressLayoutParams = FrameLayout.LayoutParams(Utils.dpToPx(18), Utils.dpToPx(18)) - progressLayoutParams.setMargins(Utils.dpToPx(8), 0, Utils.dpToPx(8), 0) - progressLayoutParams.gravity = END or CENTER_VERTICAL - - // CREATE VIEWS AND AN OBJECT FOR EVERY ATTACHMENT - - message.attachmentNames.forEachIndexed { index, name -> - val messageId = message.id - val id = message.attachmentIds[index] - val size = message.attachmentSizes[index] - // create the parent - val attachmentLayout = FrameLayout(b.root.context) - attachmentLayout.setPadding(Utils.dpToPx(16), 0, Utils.dpToPx(16), 0) - - val attachmentChip = Chip(attachmentLayout.context) - //attachmentChip.setChipBackgroundColorResource(ThemeUtils.getChipColorRes()); - attachmentChip.layoutParams = chipLayoutParams - attachmentChip.height = Utils.dpToPx(40) - - // show the file size or not - if (size == -1L) - attachmentChip.text = getString(R.string.messages_attachment_no_size_format, name) - else - attachmentChip.text = getString(R.string.messages_attachment_format, name, readableFileSize(size)) - attachmentChip.ellipsize = TextUtils.TruncateAt.MIDDLE - - // create an icon for the attachment - val icon: IIcon = when (Utils.getExtensionFromFileName(name)) { - "doc", "docx", "odt", "rtf" -> SzkolnyFont.Icon.szf_file_word_outline - "xls", "xlsx", "ods" -> SzkolnyFont.Icon.szf_file_excel_outline - "ppt", "pptx", "odp" -> SzkolnyFont.Icon.szf_file_powerpoint_outline - "pdf" -> SzkolnyFont.Icon.szf_file_pdf_outline - "mp3", "wav", "aac" -> SzkolnyFont.Icon.szf_file_music_outline - "mp4", "avi", "3gp", "mkv", "flv" -> SzkolnyFont.Icon.szf_file_video_outline - "jpg", "jpeg", "png", "bmp", "gif" -> SzkolnyFont.Icon.szf_file_image_outline - "zip", "rar", "tar", "7z" -> SzkolnyFont.Icon.szf_zip_box_outline - "html", "cpp", "c", "h", "css", "java", "py" -> SzkolnyFont.Icon.szf_file_code_outline - else -> CommunityMaterial.Icon.cmd_file_document_outline - } - attachmentChip.chipIcon = IconicsDrawable(activity).color(IconicsColor.colorRes(R.color.colorPrimary)).icon(icon).size(IconicsSize.dp(26)) - attachmentChip.closeIcon = IconicsDrawable(activity).icon(CommunityMaterial.Icon.cmd_check).size(IconicsSize.dp(18)).color(IconicsColor.colorInt(Utils.getAttr(activity, android.R.attr.textColorPrimary))) - attachmentChip.isCloseIconVisible = false - // set the object's index in the attachmentList as the tag - attachmentChip.tag = index - attachmentChip.setOnClickListener { v -> - if (v.tag is Int) { - downloadAttachment(v.tag as Int) - } - } - attachmentLayout.addView(attachmentChip) - - val attachmentProgress = ProgressBar(attachmentLayout.context) - attachmentProgress.layoutParams = progressLayoutParams - attachmentProgress.visibility = View.GONE - attachmentLayout.addView(attachmentProgress) - - insertPoint.addView(attachmentLayout) - // create an object and add to the list - val a = Attachment(App.profileId, messageId, id, name, size, attachmentLayout, attachmentChip, attachmentProgress) - attachmentList.add(a) - // check if the file is already downloaded. Show the check icon if necessary and set `downloaded` to true. - checkAttachment(a) - - } - } else { - // no attachments found - b.attachmentsTitle.visibility = View.GONE + if (message.attachmentIds.isNullOrEmpty() || message.attachmentNames.isNullOrEmpty()) { + b.attachmentsTitle.isVisible = false + b.attachmentsFragment.isVisible = false } - } - - private fun downloadAttachment(index: Int) { - val attachment = attachmentList[index] - - if (attachment.downloaded != null) { - Utils.openFile(activity, File(attachment.downloaded)) - return - } - - attachment.chip.isEnabled = false - attachment.chip.setTextColor(Themes.getSecondaryTextColor(activity)) - attachment.progressBar.visibility = View.VISIBLE - - EdziennikTask.attachmentGet( - App.profileId, - message, - attachment.attachmentId, - attachment.attachmentName - ).enqueue(activity) - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onAttachmentGetEvent(event: AttachmentGetEvent) { - attachmentList.firstOrNull { it.profileId == event.profileId - && it.messageId == event.messageId - && it.attachmentId == event.attachmentId }?.let { attachment -> - - when (event.eventType) { - TYPE_FINISHED -> { - // save the downloaded file name - attachment.downloaded = event.fileName - - // set the correct name (and size) - if (attachment.attachmentSize == -1L) - attachment.chip.text = getString(R.string.messages_attachment_no_size_format, attachment.attachmentName) - else - attachment.chip.text = getString(R.string.messages_attachment_format, attachment.attachmentName, readableFileSize(attachment.attachmentSize)) - - // hide the progress bar and show a tick icon - attachment.progressBar.visibility = View.GONE - attachment.chip.isEnabled = true - attachment.chip.setTextColor(Themes.getPrimaryTextColor(activity)) - attachment.chip.isCloseIconVisible = true - - // open the file - Utils.openFile(activity, File(attachment.downloaded)) - } - - TYPE_PROGRESS -> { - attachment.chip.text = getString(R.string.messages_attachment_downloading_format, attachment.attachmentName, event.bytesWritten.toFloat() / 1000000) - } - } - } - } - - private fun checkAttachment(attachment: Attachment) { - val storageDir = Environment.getExternalStoragePublicDirectory("Szkolny.eu") - storageDir.mkdirs() - - val attachmentDataFile = File(storageDir, "." + attachment.profileId + "_" + attachment.messageId + "_" + attachment.attachmentId) - if (attachmentDataFile.exists()) { - try { - val attachmentFileName = getStringFromFile(attachmentDataFile) - val attachmentFile = File(attachmentFileName) - if (attachmentFile.exists()) { - attachment.downloaded = attachmentFileName - attachment.chip.isCloseIconVisible = true - } - } catch (e: Exception) { - e.printStackTrace() - //app.apiEdziennik.guiReportException(activity, 355, e) - } + else { + b.attachmentsTitle.isVisible = true + b.attachmentsFragment.isVisible = true + b.attachmentsFragment.init(Bundle().also { + it.putInt("profileId", message.profileId) + it.putLongArray("attachmentIds", message.attachmentIds!!.toLongArray()) + it.putStringArray("attachmentNames", message.attachmentNames!!.toTypedArray()) + //if (message.attachmentSizes.isNotNullNorEmpty()) + // it.putLongArray("attachmentSizes", message.attachmentSizes!!.toLongArray()) + }, owner = message) } } @@ -424,20 +300,4 @@ class MessageFragment : Fragment(), CoroutineScope { super.onDestroy() job.cancel() } - - private class Attachment( - var profileId: Int, - var messageId: Long, - var attachmentId: Long, - var attachmentName: String, - var attachmentSize: Long, - var parent: FrameLayout, - var chip: Chip, - var progressBar: ProgressBar - ) { - /** - * An absolute path of the downloaded file. `null` if not downloaded yet. - */ - internal var downloaded: String? = null - } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt index 8cdbc005..f8ccb491 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt @@ -1,74 +1,240 @@ package pl.szczodrzynski.edziennik.ui.modules.messages import android.graphics.Typeface +import android.text.Editable +import android.text.TextWatcher import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.AdapterView.OnItemClickListener -import androidx.core.view.ViewCompat -import androidx.databinding.DataBindingUtil +import android.widget.Filter +import android.widget.Filterable +import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.Adapter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.cleanDiacritics import pl.szczodrzynski.edziennik.data.db.entity.Message +import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.full.MessageFull -import pl.szczodrzynski.edziennik.databinding.MessagesItemBinding -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesAdapter.ViewHolder -import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder +import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch +import pl.szczodrzynski.edziennik.ui.modules.messages.viewholder.MessageViewHolder +import pl.szczodrzynski.edziennik.ui.modules.messages.viewholder.SearchViewHolder +import java.util.* +import kotlin.coroutines.CoroutineContext +import kotlin.math.min -class MessagesAdapter(private val app: App, private val onItemClickListener: OnItemClickListener) : Adapter() { - var messageList: List = ArrayList() - fun setData(messageList: List) { - this.messageList = messageList - notifyDataSetChanged() +class MessagesAdapter( + val activity: AppCompatActivity, + val teachers: List, + val onItemClick: ((item: MessageFull) -> Unit)? = null +) : RecyclerView.Adapter(), CoroutineScope, Filterable { + companion object { + private const val TAG = "MessagesAdapter" + private const val ITEM_TYPE_MESSAGE = 0 + private const val ITEM_TYPE_SEARCH = 1 } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val inflater = LayoutInflater.from(parent.context) - return ViewHolder(DataBindingUtil.inflate(inflater, R.layout.messages_item, parent, false)) - } + private val app = activity.applicationContext as App + // optional: place the manager here - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val b = holder.b - val message = messageList[position] - b.root.setOnClickListener { v: View? -> onItemClickListener.onItemClick(null, v, position, position.toLong()) } + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main - ViewCompat.setTransitionName(b.root, message.id.toString()) - - b.messageSubject.text = message.subject - b.messageDate.text = Date.fromMillis(message.addedDate).formattedStringShort - b.messageAttachmentImage.visibility = if (message.hasAttachments()) View.VISIBLE else View.GONE - - val text = message.body?.substring(0, message.body!!.length.coerceAtMost(200)) ?: "" - b.messageBody.text = MessagesUtils.htmlToSpannable(b.root.context, text) - - if (message.type == Message.TYPE_SENT || message.type == Message.TYPE_DRAFT || message.seen) { - b.messageSender.setTextAppearance(b.messageSender.context, R.style.NavView_TextView_Small) - b.messageSender.typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) - b.messageSender.textSize = 16f - b.messageSubject.setTextAppearance(b.messageSubject.context, R.style.NavView_TextView_Small) - b.messageSubject.typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) - b.messageDate.setTextAppearance(b.messageDate.context, R.style.NavView_TextView_Small) - b.messageDate.typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) - } else { - b.messageSender.setTextAppearance(b.messageSender.context, R.style.NavView_TextView_Normal) - b.messageSender.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) - b.messageSender.textSize = 16f - b.messageSubject.setTextAppearance(b.messageSubject.context, R.style.NavView_TextView_Normal) - b.messageSubject.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) - b.messageDate.setTextAppearance(b.messageDate.context, R.style.NavView_TextView_Normal) - b.messageDate.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) + var items = mutableListOf() + var allItems = mutableListOf() + val typefaceNormal: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) } + val typefaceBold: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.BOLD) } + private val comparator by lazy { Comparator { o1: Any, o2: Any -> + if (o1 !is MessageFull || o2 !is MessageFull) + return@Comparator 0 + when { + // standard sorting + o1.filterWeight > o2.filterWeight -> return@Comparator 1 + o1.filterWeight < o2.filterWeight -> return@Comparator -1 + else -> when { + // reversed sorting + o1.addedDate > o2.addedDate -> return@Comparator -1 + o1.addedDate < o2.addedDate -> return@Comparator 1 + else -> return@Comparator 0 + } + } + }} + + val textWatcher by lazy { + object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + getFilter().filter(s.toString()) + } + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + /*items.getOrNull(0)?.let { + if (it is MessagesSearch) { + it.searchText = s?.toString() ?: "" + } + }*/ + } } - val messageInfo = MessagesUtils.getMessageInfo(app, message, 48, 24, 18, 12) - b.messageProfileBackground.setImageBitmap(messageInfo.profileImage) - b.messageSender.text = messageInfo.profileName } - override fun getItemCount(): Int { - return messageList.size + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + ITEM_TYPE_MESSAGE -> MessageViewHolder(inflater, parent) + ITEM_TYPE_SEARCH -> SearchViewHolder(inflater, parent) + else -> throw IllegalArgumentException("Incorrect viewType") + } } - inner class ViewHolder(var b: MessagesItemBinding) : RecyclerView.ViewHolder(b.root) + override fun getItemViewType(position: Int): Int { + return when (items[position]) { + is MessageFull -> ITEM_TYPE_MESSAGE + is MessagesSearch -> ITEM_TYPE_SEARCH + else -> throw IllegalArgumentException("Incorrect viewType") + } + } + @Suppress("DEPRECATION") + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = items[position] + if (holder !is BindableViewHolder<*, *>) + return + + when { + holder is MessageViewHolder && item is MessageFull -> holder.onBind(activity, app, item, position, this) + holder is SearchViewHolder && item is MessagesSearch -> holder.onBind(activity, app, item, position, this) + } + } + + override fun getItemCount() = items.size + override fun getFilter() = filter + private var prevCount = -1 + private val filter by lazy { object : Filter() { + override fun performFiltering(prefix: CharSequence?): FilterResults { + val results = FilterResults() + + if (prevCount == -1) + prevCount = allItems.size + + if (prefix.isNullOrEmpty()) { + allItems.forEach { + if (it is MessageFull) + it.searchHighlightText = null + } + results.values = allItems.toList() + results.count = allItems.size + return results + } + + val items = mutableListOf() + val prefixString = prefix.toString() + + allItems.forEach { + if (it !is MessageFull) { + items.add(it) + return@forEach + } + it.filterWeight = 100 + it.searchHighlightText = null + + var weight: Int + if (it.type == Message.TYPE_SENT) { + it.recipients?.forEach { recipient -> + weight = getMatchWeight(recipient.fullName, prefixString) + if (weight != 100) { + if (weight == 3) + weight = 31 + it.filterWeight = min(it.filterWeight, 10 + weight) + } + } + } + else { + weight = getMatchWeight(it.senderName, prefixString) + if (weight != 100) { + if (weight == 3) + weight = 31 + it.filterWeight = min(it.filterWeight, 10 + weight) + } + } + + + weight = getMatchWeight(it.subject, prefixString) + if (weight != 100) { + if (weight == 3) + weight = 22 + it.filterWeight = min(it.filterWeight, 20 + weight) + } + + if (it.filterWeight != 100) { + it.searchHighlightText = prefixString + items.add(it) + } + } + + Collections.sort(items, comparator) + results.values = items + results.count = items.size + return results + } + + override fun publishResults(constraint: CharSequence?, results: FilterResults) { + results.values?.let { items = it as MutableList } + // do not re-bind the search box + val count = results.count - 1 + + // this tries to update every item except the search field + when { + count > prevCount -> { + notifyItemRangeInserted(prevCount + 1, count - prevCount) + notifyItemRangeChanged(1, prevCount) + } + count < prevCount -> { + notifyItemRangeRemoved(prevCount + 1, prevCount - count) + notifyItemRangeChanged(1, count) + } + else -> { + notifyItemRangeChanged(1, count) + } + } + + /*if (prevCount != count) { + items.getOrNull(0)?.let { + if (it is MessagesSearch) { + it.count = count + notifyItemChanged(0) + } + } + }*/ + + prevCount = count + } + }} + + private fun getMatchWeight(name: CharSequence?, prefix: String): Int { + if (name == null) + return 100 + + val nameClean = name.cleanDiacritics() + + // First match against the whole, non-split value + if (nameClean.startsWith(prefix, ignoreCase = true) || name.startsWith(prefix, ignoreCase = true)) { + return 1 + } else { + // check if prefix matches any of the words + val words = nameClean.split(" ").toTypedArray() + name.split(" ").toTypedArray() + for (word in words) { + if (word.startsWith(prefix, ignoreCase = true)) { + return 2 + } + } + } + // finally check if the prefix matches any part of the name + if (nameClean.contains(prefix, ignoreCase = true) || name.contains(prefix, ignoreCase = true)) { + return 3 + } + + return 100 + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt index abea430a..043cce7d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt @@ -5,87 +5,94 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentPagerAdapter -import androidx.viewpager.widget.ViewPager import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.MainActivity -import pl.szczodrzynski.edziennik.R +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.Message -import pl.szczodrzynski.edziennik.databinding.FragmentMessagesBinding -import pl.szczodrzynski.edziennik.utils.Themes -import java.util.* +import pl.szczodrzynski.edziennik.databinding.MessagesFragmentBinding +import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.FragmentLazyPagerAdapter +import kotlin.coroutines.CoroutineContext -class MessagesFragment : Fragment() { +class MessagesFragment : Fragment(), CoroutineScope { companion object { + private const val TAG = "MessagesFragment" var pageSelection = 0 } private lateinit var app: App private lateinit var activity: MainActivity - private lateinit var b: FragmentMessagesBinding + private lateinit var b: MessagesFragmentBinding + + private val job: Job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + // local/private variables go here override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { activity = (getActivity() as MainActivity?) ?: return null - if (context == null) - return null + context ?: return null app = activity.application as App - context!!.theme.applyStyle(Themes.appTheme, true) - if (app.profile == null) - return inflater.inflate(R.layout.fragment_loading, container, false) - // activity, context and profile is valid - b = FragmentMessagesBinding.inflate(inflater) + b = MessagesFragmentBinding.inflate(inflater) b.refreshLayout.setParent(activity.swipeRefreshLayout) return b.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - // TODO check if app, activity, b can be null - if (app.profile == null || !isAdded) - return + if (!isAdded) return val messageId = arguments?.getLong("messageId", -1L) ?: -1L if (messageId != -1L) { val args = Bundle() args.putLong("messageId", messageId) - arguments!!.remove("messageId") + arguments?.remove("messageId") activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, args) return } - b.viewPager.adapter = Adapter(childFragmentManager).also { adapter -> + val args = arguments - adapter.addFragment(MessagesListFragment().also { fragment -> - fragment.arguments = Bundle().also { args -> - args.putInt("messageType", Message.TYPE_RECEIVED) - } - }, getString(R.string.menu_messages_inbox)) + val pagerAdapter = FragmentLazyPagerAdapter( + fragmentManager ?: return, + b.refreshLayout, + listOf( + MessagesListFragment().apply { + onPageDestroy = this@MessagesFragment.onPageDestroy + arguments = Bundle("messageType" to Message.TYPE_RECEIVED) + args?.getBundle("page0")?.let { + arguments?.putAll(it) + } + } to getString(R.string.messages_tab_received), - adapter.addFragment(MessagesListFragment().also { fragment -> - fragment.arguments = Bundle().also { args -> - args.putInt("messageType", Message.TYPE_SENT) - } - }, getString(R.string.menu_messages_sent)) + MessagesListFragment().apply { + onPageDestroy = this@MessagesFragment.onPageDestroy + arguments = Bundle("messageType" to Message.TYPE_SENT) + args?.getBundle("page1")?.let { + arguments?.putAll(it) + } + } to getString(R.string.messages_tab_sent), + MessagesListFragment().apply { + onPageDestroy = this@MessagesFragment.onPageDestroy + arguments = Bundle("messageType" to Message.TYPE_DELETED) + args?.getBundle("page2")?.let { + arguments?.putAll(it) + } + } to getString(R.string.messages_tab_deleted) + ) + ) + b.viewPager.apply { + offscreenPageLimit = 1 + adapter = pagerAdapter + currentItem = pageSelection + addOnPageSelectedListener { + pageSelection = it + } + b.tabLayout.setupWithViewPager(this) } - b.viewPager.currentItem = pageSelection - b.viewPager.clearOnPageChangeListeners() - b.viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { - override fun onPageScrollStateChanged(state: Int) { - if (b.refreshLayout != null) { - b.refreshLayout.isEnabled = state == ViewPager.SCROLL_STATE_IDLE - } - } - override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} - override fun onPageSelected(position: Int) { - pageSelection = position - } - }) - - b.tabLayout.setupWithViewPager(b.viewPager) - activity.navView.apply { bottomBar.apply { fabEnable = true @@ -99,52 +106,16 @@ class MessagesFragment : Fragment() { } activity.gainAttentionFAB() - - /*if (app.profile.loginStoreType == LOGIN_TYPE_LIBRUS && app.profile.getStudentData("accountPassword", null) == null) { - MaterialDialog.Builder(activity) - .title("Wiadomości w systemie Synergia") - .content("Moduł Wiadomości w aplikacji Szkolny.eu jest przeglądarką zasobów szkolnego konta Synergia. Z tego powodu, musisz wpisać swoje hasło do tego konta, aby móc korzystać z tej funkcji.") - .positiveText(R.string.ok) - .onPositive { dialog, which -> - MaterialDialog.Builder(activity) - .title("Zaloguj się") - .content(Html.fromHtml("Podaj hasło do konta Synergia z loginem " + app.profile.getStudentData("accountLogin", "???") + "")) - .inputType(InputType.TYPE_TEXT_VARIATION_PASSWORD) - .input(null, null) { dialog1, input -> - app.profile.putStudentData("accountPassword", input.toString()) - app.profileSaveFullAsync(app.profile) - EdziennikTask.syncProfile(App.profileId, listOf( - DRAWER_ITEM_MESSAGES to Message.TYPE_RECEIVED, - DRAWER_ITEM_MESSAGES to Message.TYPE_SENT - )).enqueue(context!!) - } - .positiveText(R.string.ok) - .negativeText(R.string.cancel) - .show() - } - .show() - }*/ } - internal class Adapter(manager: FragmentManager) : FragmentPagerAdapter(manager) { - private val mFragmentList = ArrayList() - private val mFragmentTitleList = ArrayList() + private val onPageDestroy = { position: Int, outState: Bundle? -> + arguments?.putBundle("page$position", outState) + } - override fun getItem(position: Int): Fragment { - return mFragmentList[position] - } - - override fun getCount(): Int { - return mFragmentList.size - } - - fun addFragment(fragment: Fragment, title: String) { - mFragmentList.add(fragment) - mFragmentTitleList.add(title) - } - - override fun getPageTitle(position: Int): CharSequence? { - return mFragmentTitleList[position] + override fun onDestroy() { + super.onDestroy() + (b.viewPager.adapter as? FragmentLazyPagerAdapter)?.fragments?.forEach { + it.first.onDestroy() } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt new file mode 100644 index 00000000..2e4a30b8 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt @@ -0,0 +1,128 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-4. + */ + +package pl.szczodrzynski.edziennik.ui.modules.messages + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView.NO_POSITION +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.withContext +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.db.entity.Message +import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.databinding.MessagesListFragmentBinding +import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment +import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch +import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration +import kotlin.coroutines.CoroutineContext + +class MessagesListFragment : LazyFragment(), CoroutineScope { + companion object { + private const val TAG = "MessagesListFragment" + } + + private lateinit var app: App + private lateinit var activity: MainActivity + private lateinit var b: MessagesListFragmentBinding + + private val job: Job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + // local/private variables go here + var teachers = listOf() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + activity = (getActivity() as MainActivity?) ?: return null + context ?: return null + app = activity.application as App + b = MessagesListFragmentBinding.inflate(inflater) + return b.root + } + + override fun onPageCreated(): Boolean { startCoroutineTimer(100L) { + val messageType = arguments.getInt("messageType", Message.TYPE_RECEIVED) + var topPosition = arguments.getInt("topPosition", NO_POSITION) + var bottomPosition = arguments.getInt("bottomPosition", NO_POSITION) + + teachers = withContext(Dispatchers.Default) { + app.db.teacherDao().getAllNow(App.profileId) + } + + val adapter = MessagesAdapter(activity, teachers) { + activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, Bundle( + "messageId" to it.id + )) + } + + app.db.messageDao().getAllByType(App.profileId, messageType).observe(this@MessagesListFragment, Observer { items -> + if (!isAdded) return@Observer + + items.forEach { message -> + message.recipients?.removeAll { it.profileId != message.profileId } + message.recipients?.forEach { recipient -> + if (recipient.fullName == null) { + recipient.fullName = teachers.firstOrNull { it.id == recipient.id }?.fullName ?: "" + } + } + } + + // load & configure the adapter + adapter.items = items.toMutableList() + adapter.items.add(0, MessagesSearch().also { + it.count = items.size + }) + adapter.allItems = adapter.items.toMutableList() + if (items.isNotNullNorEmpty() && b.list.adapter == null) { + b.list.adapter = adapter + b.list.apply { + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + addItemDecoration(SimpleDividerItemDecoration(context)) + if (messageType in Message.TYPE_RECEIVED..Message.TYPE_SENT) + addOnScrollListener(onScrollListener) + } + } + adapter.notifyDataSetChanged() + setSwipeToRefresh(messageType in Message.TYPE_RECEIVED..Message.TYPE_SENT && items.isNullOrEmpty()) + + (b.list.layoutManager as? LinearLayoutManager)?.let { layoutManager -> + if (topPosition != NO_POSITION && topPosition > layoutManager.findLastCompletelyVisibleItemPosition()) { + b.list.scrollToPosition(topPosition) + } else if (bottomPosition != NO_POSITION && bottomPosition < layoutManager.findFirstVisibleItemPosition()) { + b.list.scrollToPosition(bottomPosition) + } + topPosition = NO_POSITION + bottomPosition = NO_POSITION + } + + // show/hide relevant views + b.progressBar.isVisible = false + if (items.isNullOrEmpty()) { + b.list.isVisible = false + b.noData.isVisible = true + } else { + b.list.isVisible = true + b.noData.isVisible = false + } + }) + }; return true } + + override fun onDestroy() { + super.onDestroy() + if (!isAdded) return + onPageDestroy?.invoke(position, Bundle( + "topPosition" to (b.list.layoutManager as? LinearLayoutManager)?.findFirstVisibleItemPosition(), + "bottomPosition" to (b.list.layoutManager as? LinearLayoutManager)?.findLastCompletelyVisibleItemPosition() + )) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragmentOld.java similarity index 87% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.java rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragmentOld.java index bfbf749d..c9574de7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragmentOld.java @@ -10,11 +10,10 @@ import android.view.ViewGroup; import android.view.animation.Interpolator; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.databinding.DataBindingUtil; -import androidx.fragment.app.Fragment; import androidx.interpolator.view.animation.FastOutSlowInInterpolator; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; @@ -26,13 +25,15 @@ import pl.szczodrzynski.edziennik.data.db.entity.Message; import pl.szczodrzynski.edziennik.data.db.full.MessageFull; import pl.szczodrzynski.edziennik.data.db.full.MessageRecipientFull; import pl.szczodrzynski.edziennik.databinding.MessagesListBinding; +import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment; import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration; import pl.szczodrzynski.edziennik.utils.Themes; import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; +import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE; import static pl.szczodrzynski.edziennik.utils.Utils.d; -public class MessagesListFragment extends Fragment { +public class MessagesListFragmentOld extends LazyFragment { private App app = null; private MainActivity activity = null; @@ -65,9 +66,9 @@ public class MessagesListFragment extends Fragment { } @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + public boolean onPageCreated() { if (app == null || activity == null || b == null || !isAdded()) - return; + return false; long messageId = -1; if (getArguments() != null) { @@ -78,7 +79,7 @@ public class MessagesListFragment extends Fragment { args.putLong("messageId", messageId); getArguments().remove("messageId"); activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, args); - return; + return false; } if (getArguments() != null) { @@ -89,19 +90,19 @@ public class MessagesListFragment extends Fragment { activity.syncCurrentFeature(messageType, b.refreshLayout); });*/ - messagesAdapter = new MessagesAdapter(app, ((parent, view1, position, id) -> { + /*messagesAdapter = new MessagesAdapter(app, ((parent, view1, position, id) -> { // TODO ANIMATION tapPositions[messageType] = position; topPositions[messageType] = ((LinearLayoutManager) b.emailList.getLayoutManager()).findFirstCompletelyVisibleItemPosition(); bottomPositions[messageType] = ((LinearLayoutManager) b.emailList.getLayoutManager()).findLastCompletelyVisibleItemPosition(); - /*view1.getGlobalVisibleRect(viewRect); + *//*view1.getGlobalVisibleRect(viewRect); ((Transition) MessagesListFragment.this.getExitTransition()).setEpicenterCallback(new Transition.EpicenterCallback() { @Override public Rect onGetEpicenter(@NonNull Transition transition) { return viewRect; } - });*/ + });*//* Bundle args = new Bundle(); args.putLong("messageId", messagesAdapter.getMessageList().get(position).id); @@ -109,7 +110,7 @@ public class MessagesListFragment extends Fragment { // KOD W WERSJI 2.7 // TODO ANIMATION - /*TransitionSet sharedElementTransition = new TransitionSet() + *//*TransitionSet sharedElementTransition = new TransitionSet() .addTransition(new Fade()) .addTransition(new ChangeBounds()) .addTransition(new ChangeTransform()) @@ -122,10 +123,10 @@ public class MessagesListFragment extends Fragment { args.putLong("messageId", messagesAdapter.messageList.get(position).id); fragment.setArguments(args); fragment.setSharedElementEnterTransition(sharedElementTransition); - fragment.setSharedElementReturnTransition(sharedElementTransition);*/ + fragment.setSharedElementReturnTransition(sharedElementTransition);*//* // JAKIS STARSZY KOD - /*Intent intent = new Intent(activity, MessagesDetailsActivity.class); + *//*Intent intent = new Intent(activity, MessagesDetailsActivity.class); intent.putExtra("item_id", 1); intent.putExtra("transition_name", ViewCompat.getTransitionName(view1)); @@ -142,17 +143,17 @@ public class MessagesListFragment extends Fragment { setExitTransition(sharedElementTransition); setSharedElementEnterTransition(sharedElementTransition); setSharedElementReturnTransition(sharedElementTransition); - startActivity(intent, options.toBundle());*/ + startActivity(intent, options.toBundle());*//* - /*activity.getSupportFragmentManager() + *//*activity.getSupportFragmentManager() .beginTransaction() .setReorderingAllowed(true) .replace(R.id.fragment_container, fragment) .addToBackStack(null) .addSharedElement(view1, getString(R.string.transition_name)) - .commit();*/ + .commit();*//* - })); + }));*/ //tapPosition = savedInstanceState != null ? savedInstanceState.getInt(TAP_POSITION, tapPosition) : tapPosition; @@ -161,11 +162,22 @@ public class MessagesListFragment extends Fragment { // TODO ANIMATION //postponeEnterTransition(); - viewParent = (ViewGroup) view.getParent(); + viewParent = (ViewGroup) getView().getParent(); - b.emailList.setLayoutManager(new LinearLayoutManager(view.getContext())); - b.emailList.addItemDecoration(new SimpleDividerItemDecoration(view.getContext())); + b.emailList.setLayoutManager(new LinearLayoutManager(getView().getContext())); + b.emailList.addItemDecoration(new SimpleDividerItemDecoration(getView().getContext())); b.emailList.setAdapter(messagesAdapter); + b.emailList.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { + if (b.emailList.canScrollVertically(-1)) { + setSwipeToRefresh(false); + } + if (!b.emailList.canScrollVertically(-1) && newState == SCROLL_STATE_IDLE) { + setSwipeToRefresh(true); + } + } + }); if (messageType == Message.TYPE_RECEIVED) { App.db.messageDao().getReceived(App.Companion.getProfileId()).observe(this, messageFulls -> { @@ -183,7 +195,7 @@ public class MessagesListFragment extends Fragment { List messageRecipients = App.db.messageRecipientDao().getAll(App.Companion.getProfileId()); List messageIds = new ArrayList<>(); for (MessageFull messageFull: messageFulls) { - messageIds.add(messageFull.id); + messageIds.add(messageFull.getId()); } for (MessageRecipientFull messageRecipientFull: messageRecipients) { if (messageRecipientFull.id == -1) @@ -215,13 +227,13 @@ public class MessagesListFragment extends Fragment { }); } - + return true; } private void createMessageList(List messageFulls) { b.progressBar.setVisibility(View.GONE); b.emailList.setVisibility(View.VISIBLE); - messagesAdapter.setData(messageFulls); + //messagesAdapter.setData(messageFulls); LinearLayoutManager layoutManager = (LinearLayoutManager) b.emailList.getLayoutManager(); if (tapPositions[messageType] != NO_POSITION && layoutManager != null) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesUtils.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesUtils.kt index 8a5549bd..07a0769a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesUtils.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesUtils.kt @@ -124,7 +124,7 @@ object MessagesUtils { var profileImage: Bitmap? = null var profileName: String? = null if (message.type == Message.TYPE_RECEIVED || message.type == Message.TYPE_DELETED) { - profileName = message.senderFullName?.fixName() + profileName = message.senderName?.fixName() profileImage = getProfileImage(diameterDp, textSizeBigDp, textSizeMediumDp, textSizeSmallDp, 1, profileName) } else if (message.type == Message.TYPE_SENT || message.type == Message.TYPE_DRAFT && message.recipients != null) { when (val count = message.recipients?.size ?: 0) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/SlideExplode.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/SlideExplode.java deleted file mode 100644 index 6161b6dd..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/SlideExplode.java +++ /dev/null @@ -1,75 +0,0 @@ -package pl.szczodrzynski.edziennik.ui.modules.messages; - -import android.animation.Animator; -import android.animation.ObjectAnimator; -import android.graphics.Rect; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.transition.TransitionValues; -import androidx.transition.Visibility; - -public class SlideExplode extends Visibility { - private static final String KEY_SCREEN_BOUNDS = "screenBounds"; - - private int[] mTempLoc = new int[2]; - - private void captureValues(TransitionValues transitionValues) { - View view = transitionValues.view; - view.getLocationOnScreen(mTempLoc); - int left = mTempLoc[0]; - int top = mTempLoc[1]; - int right = left + view.getWidth(); - int bottom = top + view.getHeight(); - transitionValues.values.put(KEY_SCREEN_BOUNDS, new Rect(left, top, right, bottom)); - } - - @Override - public void captureStartValues(@NonNull TransitionValues transitionValues) { - super.captureStartValues(transitionValues); - captureValues(transitionValues); - } - - @Override - public void captureEndValues(@NonNull TransitionValues transitionValues) { - super.captureEndValues(transitionValues); - captureValues(transitionValues); - } - - @Override - public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) { - if (endValues == null) - return null; - - Rect bounds = (Rect) endValues.values.get(KEY_SCREEN_BOUNDS); - float endY = view.getTranslationY(); - float startY = endY + calculateDistance(sceneRoot, bounds); - return ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, startY, endY); - } - - @Override - public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) { - if (startValues == null) - return null; - - Rect bounds = (Rect) startValues.values.get(KEY_SCREEN_BOUNDS); - float startY = view.getTranslationY(); - float endY = startY + calculateDistance(sceneRoot, bounds); - return ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, startY, endY); - } - - private int calculateDistance(View sceneRoot, Rect viewBounds) { - sceneRoot.getLocationOnScreen(mTempLoc); - int sceneRootY = mTempLoc[1]; - if (getEpicenter() == null) { - return -sceneRoot.getHeight(); - } - else if (viewBounds.top <= getEpicenter().top) { - return sceneRootY - getEpicenter().top; - } - else { - return sceneRootY + sceneRoot.getHeight() - getEpicenter().bottom; - } - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/Transitions.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/Transitions.java deleted file mode 100644 index 4e6f38a2..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/Transitions.java +++ /dev/null @@ -1,18 +0,0 @@ -package pl.szczodrzynski.edziennik.ui.modules.messages; - -import android.animation.TimeInterpolator; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.transition.TransitionSet; - -public class Transitions extends TransitionSet { - @NonNull - @Override - public TransitionSet setInterpolator(@Nullable TimeInterpolator interpolator) { - for (int i = 0; i < getTransitionCount(); i++) { - getTransitionAt(i).setInterpolator(interpolator); - } - return this; - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesComposeFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt similarity index 95% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesComposeFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt index 7326a653..96898871 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesComposeFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt @@ -1,8 +1,8 @@ /* - * Copyright (c) Kuba Szczodrzyński 2019-12-22. + * Copyright (c) Kuba Szczodrzyński 2020-4-4. */ -package pl.szczodrzynski.edziennik.ui.modules.messages +package pl.szczodrzynski.edziennik.ui.modules.messages.compose import android.content.Context import android.graphics.Typeface @@ -43,6 +43,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.databinding.MessagesComposeFragmentBinding +import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage import pl.szczodrzynski.edziennik.utils.Colors import pl.szczodrzynski.edziennik.utils.Themes @@ -361,7 +362,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { span.appendText("W dniu ") span.appendSpan(dateString, StyleSpan(Typeface.ITALIC), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) span.appendText(", ") - span.appendSpan(msg.senderFullName.fixName(), StyleSpan(Typeface.ITALIC), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + span.appendSpan(msg.senderName.fixName(), StyleSpan(Typeface.ITALIC), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) span.appendText(" napisał(a):") span.setSpan(StyleSpan(Typeface.BOLD), 0, span.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) span.appendText("\n\n") @@ -380,7 +381,8 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { span.replace(0, 0, "\n\n") subject = "Fwd: ${msg.subject}" } - body = MessagesUtils.htmlToSpannable(activity,msg.body ?: "Nie udało się wczytać oryginalnej wiadomości.")//Html.fromHtml(msg.body?.replace("".toRegex(), "\n") ?: "Nie udało się wczytać oryginalnej wiadomości.") + body = MessagesUtils.htmlToSpannable(activity, msg.body + ?: "Nie udało się wczytać oryginalnej wiadomości.")//Html.fromHtml(msg.body?.replace("".toRegex(), "\n") ?: "Nie udało się wczytać oryginalnej wiadomości.") } b.recipients.addTextWithChips(chipList) @@ -433,6 +435,15 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { return } + // do magic + // apparently this removes an underline + // span from the text where the caret is + b.subject.requestFocus() + b.subject.clearFocus() + activity.navView.bottomSheet.hideKeyboard() + b.text.clearFocus() + b.text.setSelection(0) + if (b.subjectLayout.counterMaxLength != -1 && b.subject.length() > b.subjectLayout.counterMaxLength) return if (b.textLayout.counterMaxLength != -1 && b.text.length() > b.textLayout.counterMaxLength) @@ -494,7 +505,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { activity.snackbar(app.getString(R.string.messages_sent_success), app.getString(R.string.ok)) activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, Bundle( - "messageId" to (event.message.id ?: -1), + "messageId" to event.message.id, "message" to app.gson.toJson(event.message), "sentDate" to event.sentDate )) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesComposeInfo.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeInfo.kt similarity index 79% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesComposeInfo.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeInfo.kt index fcb509f8..30b89472 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesComposeInfo.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeInfo.kt @@ -1,4 +1,8 @@ -package pl.szczodrzynski.edziennik.ui.modules.messages +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-4. + */ + +package pl.szczodrzynski.edziennik.ui.modules.messages.compose class MessagesComposeInfo( /** @@ -18,4 +22,4 @@ class MessagesComposeInfo( * -1 means unlimited length. */ var maxBodyLength: Int -) \ No newline at end of file +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesComposeSuggestionAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeSuggestionAdapter.kt similarity index 73% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesComposeSuggestionAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeSuggestionAdapter.kt index 935d2d34..83a5ba39 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesComposeSuggestionAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeSuggestionAdapter.kt @@ -1,4 +1,4 @@ -package pl.szczodrzynski.edziennik.ui.modules.messages +package pl.szczodrzynski.edziennik.ui.modules.messages.compose import android.content.Context import android.graphics.Typeface.BOLD @@ -13,6 +13,7 @@ import android.widget.ImageView import android.widget.TextView import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.asSpannable +import pl.szczodrzynski.edziennik.cleanDiacritics import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.resolveAttr import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage @@ -85,36 +86,15 @@ class MessagesComposeSuggestionAdapter( val list = mutableListOf() originalList.forEach { teacher -> - val teacherFullName = teacher.fullName - teacher.recipientWeight = 0 + teacher.recipientWeight = getMatchWeight(teacher.fullName, prefixString) - // First match against the whole, non-split value - var found = false - if (teacherFullName.startsWith(prefixString, ignoreCase = true)) { - teacher.recipientWeight = 1 - found = true - } else { - // check if prefix matches any of the words - val words = teacherFullName.split(" ").toTypedArray() - for (word in words) { - if (word.startsWith(prefixString, ignoreCase = true)) { - teacher.recipientWeight = 2 - found = true - break - } - } - } - // finally check if the prefix matches any part of the name - if (!found && teacherFullName.contains(prefixString, ignoreCase = true)) { - teacher.recipientWeight = 3 - } - - if (teacher.recipientWeight != 0) { - teacher.recipientDisplayName = teacherFullName.asSpannable( + if (teacher.recipientWeight != 100) { + teacher.recipientDisplayName = teacher.fullName.asSpannable( StyleSpan(BOLD), BackgroundColorSpan(R.attr.colorControlHighlight.resolveAttr(context)), substring = prefixString, - ignoreCase = true + ignoreCase = true, + ignoreDiacritics = true ) list += teacher } @@ -137,4 +117,29 @@ class MessagesComposeSuggestionAdapter( } } + private fun getMatchWeight(name: CharSequence?, prefix: String): Int { + if (name == null) + return 100 + + val nameClean = name.cleanDiacritics() + + // First match against the whole, non-split value + if (nameClean.startsWith(prefix, ignoreCase = true) || name.startsWith(prefix, ignoreCase = true)) { + return 1 + } else { + // check if prefix matches any of the words + val words = nameClean.split(" ").toTypedArray() + name.split(" ").toTypedArray() + for (word in words) { + if (word.startsWith(prefix, ignoreCase = true)) { + return 2 + } + } + } + // finally check if the prefix matches any part of the name + if (nameClean.contains(prefix, ignoreCase = true) || name.contains(prefix, ignoreCase = true)) { + return 3 + } + + return 100 + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt new file mode 100644 index 00000000..ad206c62 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt @@ -0,0 +1,11 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-5. + */ + +package pl.szczodrzynski.edziennik.ui.modules.messages.models + +class MessagesSearch { + var isFocused = false + var searchText = "" + var count = 0 +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt new file mode 100644 index 00000000..62789a0c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-5. + */ + +package pl.szczodrzynski.edziennik.ui.modules.messages.viewholder + +import android.graphics.Typeface +import android.text.style.BackgroundColorSpan +import android.text.style.StyleSpan +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.db.entity.Message +import pl.szczodrzynski.edziennik.data.db.full.MessageFull +import pl.szczodrzynski.edziennik.databinding.MessagesListItemBinding +import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder +import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesAdapter +import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils +import pl.szczodrzynski.edziennik.utils.models.Date + +class MessageViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + val b: MessagesListItemBinding = MessagesListItemBinding.inflate(inflater, parent, false) +) : RecyclerView.ViewHolder(b.root), BindableViewHolder { + companion object { + private const val TAG = "MessageViewHolder" + } + + override fun onBind(activity: AppCompatActivity, app: App, item: MessageFull, position: Int, adapter: MessagesAdapter) { + val manager = app.gradesManager + + b.messageSubject.text = item.subject + b.messageDate.text = Date.fromMillis(item.addedDate).formattedStringShort + b.messageAttachmentImage.isVisible = item.hasAttachments + + val text = item.body?.take(200) ?: "" + b.messageBody.text = MessagesUtils.htmlToSpannable(activity, text) + + val isRead = item.type == Message.TYPE_SENT || item.type == Message.TYPE_DRAFT || item.seen + val typeface = if (isRead) adapter.typefaceNormal else adapter.typefaceBold + val style = if (isRead) R.style.NavView_TextView_Small else R.style.NavView_TextView_Normal + // set text styles + b.messageSender.setTextAppearance(activity, style) + b.messageSender.typeface = typeface + b.messageSubject.setTextAppearance(activity, style) + b.messageSubject.typeface = typeface + b.messageDate.setTextAppearance(activity, style) + b.messageDate.typeface = typeface + + val messageInfo = MessagesUtils.getMessageInfo(app, item, 48, 24, 18, 12) + b.messageProfileBackground.setImageBitmap(messageInfo.profileImage) + b.messageSender.text = messageInfo.profileName + + item.searchHighlightText?.let { highlight -> + val colorHighlight = R.attr.colorControlHighlight.resolveAttr(activity) + + b.messageSubject.text = b.messageSubject.text.asSpannable( + StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight), + substring = highlight, ignoreCase = true, ignoreDiacritics = true) + b.messageSender.text = b.messageSender.text.asSpannable( + StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight), + substring = highlight, ignoreCase = true, ignoreDiacritics = true) + } + + adapter.onItemClick?.let { listener -> + b.root.onClick { listener(item) } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt new file mode 100644 index 00000000..5adb1984 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-5. + */ + +package pl.szczodrzynski.edziennik.ui.modules.messages.viewholder + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.RecyclerView +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.databinding.MessagesListItemSearchBinding +import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder +import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesAdapter +import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch + +class SearchViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + val b: MessagesListItemSearchBinding = MessagesListItemSearchBinding.inflate(inflater, parent, false) +) : RecyclerView.ViewHolder(b.root), BindableViewHolder { + companion object { + private const val TAG = "SearchViewHolder" + } + + override fun onBind(activity: AppCompatActivity, app: App, item: MessagesSearch, position: Int, adapter: MessagesAdapter) { + b.searchEdit.removeTextChangedListener(adapter.textWatcher) + b.searchEdit.addTextChangedListener(adapter.textWatcher) + + /*b.searchEdit.setOnKeyboardListener(object : TextInputKeyboardEdit.KeyboardListener { + override fun onStateChanged(keyboardEditText: TextInputKeyboardEdit, showing: Boolean) { + item.isFocused = showing + } + })*/ + + /*if (b.searchEdit.text.toString() != item.searchText) { + b.searchEdit.setText(item.searchText) + b.searchEdit.setSelection(item.searchText.length) + }*/ + + //b.searchLayout.helperText = app.getString(R.string.messages_search_results, item.count) + + /*if (item.isFocused && !b.searchEdit.isFocused) + b.searchEdit.requestFocus()*/ + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsAdapter.kt index 473b9855..8df33a8c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsAdapter.kt @@ -1,71 +1,62 @@ package pl.szczodrzynski.edziennik.ui.modules.notifications -import android.app.Activity -import android.content.Context -import android.content.Intent import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.TextView -import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.Notification -import pl.szczodrzynski.edziennik.utils.Utils.d +import pl.szczodrzynski.edziennik.databinding.NotificationsListItemBinding import pl.szczodrzynski.edziennik.utils.models.Date +import kotlin.coroutines.CoroutineContext class NotificationsAdapter( - private val context: Context -) : RecyclerView.Adapter() { + private val activity: AppCompatActivity, + val onItemClick: ((item: Notification) -> Unit)? = null +) : RecyclerView.Adapter(), CoroutineScope { companion object { private const val TAG = "NotificationsAdapter" } + private val app = activity.applicationContext as App + // optional: place the manager here + + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + var items = listOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val inflater = LayoutInflater.from(context) - val view = inflater.inflate(R.layout.row_notifications_item, parent, false) + val inflater = LayoutInflater.from(activity) + val view = NotificationsListItemBinding.inflate(inflater, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val app = context.applicationContext as App + val item = items[position] + val b = holder.b - val notification = items[position] + val date = Date.fromMillis(item.addedDate).formattedString + val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity) - val date = Date.fromMillis(notification.addedDate).formattedString - val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(context) - - holder.title.text = notification.text - holder.profileDate.text = listOf( - notification.profileName ?: "", + b.title.text = item.text + b.profileDate.text = listOf( + item.profileName ?: "", " • ", - date.asColoredSpannable(colorSecondary) - ).concat() - holder.type.text = context.getNotificationTitle(notification.type) + date + ).concat().asColoredSpannable(colorSecondary) + b.type.text = activity.getNotificationTitle(item.type) - holder.root.onClick { - val intent = Intent("android.intent.action.MAIN") - notification.fillIntent(intent) - - d(TAG, "notification with item " + notification.viewId + " extras " + if (intent.extras == null) "null" else intent.extras!!.toString()) - - //Log.d(TAG, "Got date "+intent.getLongExtra("timetableDate", 0)); - - if (notification.profileId != null && notification.profileId != -1 && notification.profileId != app.profile.id && context is Activity) { - Toast.makeText(app, app.getString(R.string.toast_changing_profile), Toast.LENGTH_LONG).show() - } - app.sendBroadcast(intent) + onItemClick?.let { listener -> + b.root.onClick { listener(item) } } } override fun getItemCount() = items.size - class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var root = itemView - var title: TextView = itemView.findViewById(R.id.title) - var profileDate: TextView = itemView.findViewById(R.id.profileDate) - var type: TextView = itemView.findViewById(R.id.type) - } + class ViewHolder(val b: NotificationsListItemBinding) : RecyclerView.ViewHolder(b.root) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsFragment.kt deleted file mode 100644 index 74d01bd1..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsFragment.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2019-11-22. - */ - -package pl.szczodrzynski.edziennik.ui.modules.notifications - -import android.os.AsyncTask -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.fragment.app.Fragment -import androidx.lifecycle.Observer -import androidx.recyclerview.widget.LinearLayoutManager -import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.MainActivity -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.databinding.FragmentNotificationsBinding -import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration -import pl.szczodrzynski.edziennik.utils.Themes -import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem - - -class NotificationsFragment : Fragment() { - private lateinit var app: App - private lateinit var activity: MainActivity - private lateinit var b: FragmentNotificationsBinding - - private val adapter by lazy { - NotificationsAdapter(activity) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - activity = (getActivity() as MainActivity?) ?: return null - context ?: return null - app = activity.application as App - context!!.theme.applyStyle(Themes.appTheme, true) - if (app.profile == null) - return inflater.inflate(R.layout.fragment_loading, container, false) - // activity, context and profile is valid - b = FragmentNotificationsBinding.inflate(inflater) - return b.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - // TODO check if app, activity, b can be null - if (app.profile == null || !isAdded) - return - - activity.bottomSheet.prependItems( - BottomSheetPrimaryItem(true) - .withTitle(R.string.menu_remove_notifications) - .withIcon(CommunityMaterial.Icon.cmd_delete_sweep_outline) - .withOnClickListener(View.OnClickListener { - activity.bottomSheet.close() - AsyncTask.execute { app.db.notificationDao().clearAll() } - Toast.makeText(activity, R.string.menu_remove_notifications_success, Toast.LENGTH_SHORT).show() - })) - - app.db.notificationDao() - .getAll() - .observe(this, Observer { notifications -> - if (app.profile == null || !isAdded) return@Observer - - adapter.items = notifications - if (b.notificationsView.adapter == null) { - b.notificationsView.adapter = adapter - b.notificationsView.apply { - setHasFixedSize(true) - layoutManager = LinearLayoutManager(context) - addItemDecoration(SimpleDividerItemDecoration(context)) - } - } - adapter.notifyDataSetChanged() - - if (notifications != null && notifications.isNotEmpty()) { - b.notificationsView.visibility = View.VISIBLE - b.notificationsNoData.visibility = View.GONE - } else { - b.notificationsView.visibility = View.GONE - b.notificationsNoData.visibility = View.VISIBLE - } - }) - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsListFragment.kt new file mode 100644 index 00000000..7cd9703b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsListFragment.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-22. + */ + +package pl.szczodrzynski.edziennik.ui.modules.notifications + +import android.app.Activity +import android.content.Intent +import android.os.AsyncTask +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.databinding.NotificationsListFragmentBinding +import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration +import pl.szczodrzynski.edziennik.utils.Utils +import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem +import kotlin.coroutines.CoroutineContext + +class NotificationsListFragment : Fragment(), CoroutineScope { + companion object { + private const val TAG = "NotificationsListFragment" + } + + private lateinit var app: App + private lateinit var activity: MainActivity + private lateinit var b: NotificationsListFragmentBinding + + private val job: Job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + activity = (getActivity() as MainActivity?) ?: return null + context ?: return null + app = activity.application as App + b = NotificationsListFragmentBinding.inflate(inflater) + return b.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { startCoroutineTimer(100L) { + if (!isAdded) return@startCoroutineTimer + + activity.bottomSheet.prependItems( + BottomSheetPrimaryItem(true) + .withTitle(R.string.menu_remove_notifications) + .withIcon(CommunityMaterial.Icon.cmd_delete_sweep_outline) + .withOnClickListener(View.OnClickListener { + activity.bottomSheet.close() + AsyncTask.execute { app.db.notificationDao().clearAll() } + Toast.makeText(activity, R.string.menu_remove_notifications_success, Toast.LENGTH_SHORT).show() + })) + + val adapter = NotificationsAdapter(activity) { notification -> + val intent = Intent("android.intent.action.MAIN") + notification.fillIntent(intent) + + Utils.d(TAG, "notification with item " + notification.viewId + " extras " + if (intent.extras == null) "null" else intent.extras!!.toString()) + if (notification.profileId != null && notification.profileId != -1 && notification.profileId != app.profile.id && context is Activity) { + Toast.makeText(app, app.getString(R.string.toast_changing_profile), Toast.LENGTH_LONG).show() + } + app.sendBroadcast(intent) + } + + app.db.notificationDao().getAll().observe(this@NotificationsListFragment, Observer { items -> + if (!isAdded) return@Observer + + // load & configure the adapter + adapter.items = items + if (items.isNotNullNorEmpty() && b.list.adapter == null) { + b.list.adapter = adapter + b.list.apply { + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + addItemDecoration(SimpleDividerItemDecoration(context)) + } + } + adapter.notifyDataSetChanged() + + // show/hide relevant views + b.progressBar.isVisible = false + if (items.isNullOrEmpty()) { + b.list.isVisible = false + b.noData.isVisible = true + } else { + b.list.isVisible = true + b.noData.isVisible = false + } + }) + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsNewFragment.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsNewFragment.java index 7aaa7f3a..cc72c46c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsNewFragment.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsNewFragment.java @@ -1217,7 +1217,7 @@ public class SettingsNewFragment extends MaterialAboutFragment { }) .build());*/ - if (App.Companion.getDevMode()) { + if (App.Companion.getDebugMode()) { items.add(new MaterialAboutActionItem.Builder() .text(R.string.settings_about_crash_text) .subText(R.string.settings_about_crash_subtext) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateAdapter.kt new file mode 100644 index 00000000..fe78396f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateAdapter.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-30. + */ + +package pl.szczodrzynski.edziennik.ui.modules.template + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.db.entity.Notification +import pl.szczodrzynski.edziennik.databinding.TemplateListItemBinding +import pl.szczodrzynski.edziennik.utils.models.Date +import kotlin.coroutines.CoroutineContext + +class TemplateAdapter( + val activity: AppCompatActivity, + val onItemClick: ((item: Notification) -> Unit)? = null +) : RecyclerView.Adapter(), CoroutineScope { + companion object { + private const val TAG = "TemplateAdapter" + } + + private val app = activity.applicationContext as App + // optional: place the manager here + + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + var items = listOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(parent.context) + val view = TemplateListItemBinding.inflate(inflater, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = items[position] + val b = holder.b + + val date = Date.fromMillis(item.addedDate).formattedString + val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity) + + b.title.text = item.text + b.profileDate.text = listOf( + item.profileName ?: "", + " • ", + date + ).concat().asColoredSpannable(colorSecondary) + b.type.text = activity.getNotificationTitle(item.type) + + onItemClick?.let { listener -> + b.root.onClick { listener(item) } + } + } + + override fun getItemCount() = items.size + + class ViewHolder(val b: TemplateListItemBinding) : RecyclerView.ViewHolder(b.root) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/TemplateDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateDialog.kt similarity index 95% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/TemplateDialog.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateDialog.kt index a5e2dbeb..e5d68ebe 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/TemplateDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateDialog.kt @@ -1,8 +1,8 @@ /* - * Copyright (c) Kuba Szczodrzyński 2020-1-8. + * Copyright (c) Kuba Szczodrzyński 2020-3-30. */ -package pl.szczodrzynski.edziennik.ui.dialogs +package pl.szczodrzynski.edziennik.ui.modules.template import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateFragment.kt new file mode 100644 index 00000000..8adee96a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateFragment.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-30. + */ + +package pl.szczodrzynski.edziennik.ui.modules.template + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.databinding.TemplateFragmentBinding +import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.FragmentLazyPagerAdapter +import pl.szczodrzynski.edziennik.ui.modules.homework.HomeworkDate +import pl.szczodrzynski.edziennik.ui.modules.homework.HomeworkListFragment +import kotlin.coroutines.CoroutineContext + +class TemplateFragment : Fragment(), CoroutineScope { + companion object { + private const val TAG = "TemplateFragment" + var pageSelection = 0 + } + + private lateinit var app: App + private lateinit var activity: MainActivity + private lateinit var b: TemplateFragmentBinding + + private val job: Job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + // local/private variables go here + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + activity = (getActivity() as MainActivity?) ?: return null + context ?: return null + app = activity.application as App + b = TemplateFragmentBinding.inflate(inflater) + b.refreshLayout.setParent(activity.swipeRefreshLayout) + return b.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + if (!isAdded) return + + val pagerAdapter = FragmentLazyPagerAdapter( + fragmentManager ?: return, + b.refreshLayout, + listOf( + HomeworkListFragment().apply { + arguments = Bundle("homeworkDate" to HomeworkDate.CURRENT) + } to getString(R.string.homework_tab_current), + + HomeworkListFragment().apply { + arguments = Bundle("homeworkDate" to HomeworkDate.PAST) + } to getString(R.string.homework_tab_past), + + TemplatePageFragment() to "Pager 0", + TemplatePageFragment() to "Pager 1", + TemplatePageFragment() to "Pager 2", + TemplatePageFragment() to "Pager 3", + TemplateListPageFragment() to "Pager 4", + TemplateListPageFragment() to "Pager 5", + TemplateListPageFragment() to "Pager 6", + TemplateListPageFragment() to "Pager 7" + ) + ) + b.viewPager.apply { + offscreenPageLimit = 1 + adapter = pagerAdapter + currentItem = pageSelection + addOnPageSelectedListener { + pageSelection = it + } + b.tabLayout.setupWithViewPager(this) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateListFragment.kt new file mode 100644 index 00000000..c044c4cf --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateListFragment.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-30. + */ + +package pl.szczodrzynski.edziennik.ui.modules.template + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.databinding.TemplateListFragmentBinding +import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration +import kotlin.coroutines.CoroutineContext + +class TemplateListFragment : Fragment(), CoroutineScope { + companion object { + private const val TAG = "TemplateListFragment" + } + + private lateinit var app: App + private lateinit var activity: MainActivity + private lateinit var b: TemplateListFragmentBinding + + private val job: Job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + // local/private variables go here + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + activity = (getActivity() as MainActivity?) ?: return null + context ?: return null + app = activity.application as App + b = TemplateListFragmentBinding.inflate(inflater) + b.refreshLayout.setParent(activity.swipeRefreshLayout) + return b.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { startCoroutineTimer(100L) { + if (!isAdded) return@startCoroutineTimer + + val adapter = TemplateAdapter(activity) + + app.db.notificationDao().getAll().observe(this@TemplateListFragment, Observer { items -> + if (!isAdded) return@Observer + + // load & configure the adapter + adapter.items = items + if (items.isNotNullNorEmpty() && b.list.adapter == null) { + b.list.adapter = adapter + b.list.apply { + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + addItemDecoration(SimpleDividerItemDecoration(context)) + addOnScrollListener(b.refreshLayout.onScrollListener) + } + } + adapter.notifyDataSetChanged() + b.refreshLayout.isEnabled = false // TODO + + // show/hide relevant views + b.progressBar.isVisible = false + if (items.isNullOrEmpty()) { + b.list.isVisible = false + b.noData.isVisible = true + } else { + b.list.isVisible = true + b.noData.isVisible = false + } + }) + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateListPageFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateListPageFragment.kt new file mode 100644 index 00000000..e2395668 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateListPageFragment.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-30. + */ + +package pl.szczodrzynski.edziennik.ui.modules.template + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.databinding.TemplateListPageFragmentBinding +import pl.szczodrzynski.edziennik.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.startCoroutineTimer +import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment +import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration +import kotlin.coroutines.CoroutineContext + +class TemplateListPageFragment : LazyFragment(), CoroutineScope { + companion object { + private const val TAG = "TemplateListPagerFragment" + } + + private lateinit var app: App + private lateinit var activity: MainActivity + private lateinit var b: TemplateListPageFragmentBinding + + private val job: Job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + // local/private variables go here + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + activity = (getActivity() as MainActivity?) ?: return null + context ?: return null + app = activity.application as App + b = TemplateListPageFragmentBinding.inflate(inflater) + return b.root + } + + override fun onPageCreated(): Boolean { startCoroutineTimer(100L) { + val adapter = TemplateAdapter(activity) + + app.db.notificationDao().getAll().observe(this@TemplateListPageFragment, Observer { items -> + if (!isAdded) return@Observer + + // load & configure the adapter + adapter.items = items + if (items.isNotNullNorEmpty() && b.list.adapter == null) { + b.list.adapter = adapter + b.list.apply { + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + addItemDecoration(SimpleDividerItemDecoration(context)) + addOnScrollListener(onScrollListener) + } + } + adapter.notifyDataSetChanged() + setSwipeToRefresh(items.isNullOrEmpty()) + + // show/hide relevant views + b.progressBar.isVisible = false + if (items.isNullOrEmpty()) { + b.list.isVisible = false + b.noData.isVisible = true + } else { + b.list.isVisible = true + b.noData.isVisible = false + } + }) + }; return true } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/TemplateFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplatePageFragment.kt similarity index 56% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/TemplateFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplatePageFragment.kt index 3151dcb4..f2404ea4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/TemplateFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplatePageFragment.kt @@ -1,26 +1,30 @@ -package pl.szczodrzynski.edziennik.ui.modules.base +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-30. + */ + +package pl.szczodrzynski.edziennik.ui.modules.template import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.Fragment import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.MainActivity -import pl.szczodrzynski.edziennik.databinding.FragmentTemplateBinding +import pl.szczodrzynski.edziennik.databinding.TemplatePageFragmentBinding +import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment import kotlin.coroutines.CoroutineContext -class TemplateFragment : Fragment(), CoroutineScope { +class TemplatePageFragment : LazyFragment(), CoroutineScope { companion object { - private const val TAG = "TemplateFragment" + private const val TAG = "TemplatePagerFragment" } private lateinit var app: App private lateinit var activity: MainActivity - private lateinit var b: FragmentTemplateBinding + private lateinit var b: TemplatePageFragmentBinding private val job: Job = Job() override val coroutineContext: CoroutineContext @@ -32,15 +36,16 @@ class TemplateFragment : Fragment(), CoroutineScope { activity = (getActivity() as MainActivity?) ?: return null context ?: return null app = activity.application as App - b = FragmentTemplateBinding.inflate(inflater) - b.refreshLayout.setParent(activity.swipeRefreshLayout) + b = TemplatePageFragmentBinding.inflate(inflater) return b.root } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - if (!isAdded) - return - + override fun onPageCreated(): Boolean { + b.text.text = "Fragment $position" + b.button.addOnCheckedChangeListener { button, isChecked -> + setSwipeToRefresh(isChecked) + } + return true } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplatePagerAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplatePagerAdapter.kt new file mode 100644 index 00000000..eaa8ba1d --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplatePagerAdapter.kt @@ -0,0 +1,15 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-30. + */ + +package pl.szczodrzynski.edziennik.ui.modules.template + +import androidx.fragment.app.FragmentManager +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyPagerAdapter + +class TemplatePagerAdapter(fragmentManager: FragmentManager, swipeRefreshLayout: SwipeRefreshLayout) : LazyPagerAdapter(fragmentManager, swipeRefreshLayout) { + override fun getPage(position: Int) = TemplatePageFragment() + override fun getPageTitle(position: Int) = "Page $position" + override fun getCount() = 10 +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt index da2450e4..848d0801 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt @@ -27,7 +27,7 @@ import pl.szczodrzynski.edziennik.data.db.full.LessonFull import pl.szczodrzynski.edziennik.databinding.TimetableLessonBinding import pl.szczodrzynski.edziennik.databinding.TimetableNoTimetableBinding import pl.szczodrzynski.edziennik.ui.dialogs.timetable.LessonDetailsDialog -import pl.szczodrzynski.edziennik.ui.modules.base.PagerFragment +import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment.Companion.DEFAULT_END_HOUR import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment.Companion.DEFAULT_START_HOUR import pl.szczodrzynski.edziennik.utils.ListenerScrollView @@ -36,7 +36,7 @@ import java.util.* import kotlin.coroutines.CoroutineContext import kotlin.math.min -class TimetableDayFragment : PagerFragment(), CoroutineScope { +class TimetableDayFragment : LazyFragment(), CoroutineScope { companion object { private const val TAG = "TimetableDayFragment" } @@ -104,9 +104,6 @@ class TimetableDayFragment : PagerFragment(), CoroutineScope { } override fun onPageCreated(): Boolean { - if (!isAdded) - return false - // observe lesson database app.db.timetableDao().getForDate(App.profileId, date).observe(this, Observer { lessons -> launch { @@ -216,23 +213,23 @@ class TimetableDayFragment : PagerFragment(), CoroutineScope { LessonDetailsDialog(activity, it.tag as LessonFull) } - val eventList = events.filter { it.startTime != null && it.startTime == lesson.displayStartTime }.take(3) + val eventList = events.filter { it.time != null && it.time == lesson.displayStartTime }.take(3) eventList.getOrNull(0).let { lb.event1.visibility = if (it == null) View.GONE else View.VISIBLE lb.event1.background = it?.let { - R.drawable.bg_circle.resolveDrawable(activity).setTintColor(it.getColor()) + R.drawable.bg_circle.resolveDrawable(activity).setTintColor(it.eventColor) } } eventList.getOrNull(1).let { lb.event2.visibility = if (it == null) View.GONE else View.VISIBLE lb.event2.background = it?.let { - R.drawable.bg_circle.resolveDrawable(activity).setTintColor(it.getColor()) + R.drawable.bg_circle.resolveDrawable(activity).setTintColor(it.eventColor) } } eventList.getOrNull(2).let { lb.event3.visibility = if (it == null) View.GONE else View.VISIBLE lb.event3.background = it?.let { - R.drawable.bg_circle.resolveDrawable(activity).setTintColor(it.getColor()) + R.drawable.bg_circle.resolveDrawable(activity).setTintColor(it.eventColor) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetablePagerAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetablePagerAdapter.kt index 31851498..289860ac 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetablePagerAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetablePagerAdapter.kt @@ -5,9 +5,9 @@ package pl.szczodrzynski.edziennik.ui.modules.timetable import android.os.Bundle -import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentStatePagerAdapter +import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment +import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyPagerAdapter import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Week @@ -16,7 +16,7 @@ class TimetablePagerAdapter( private val items: List, private val startHour: Int, private val endHour: Int -) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { +) : LazyPagerAdapter(fragmentManager, null) { companion object { private const val TAG = "TimetablePagerAdapter" } @@ -25,7 +25,7 @@ class TimetablePagerAdapter( private val weekStart by lazy { today.weekStart } private val weekEnd by lazy { weekStart.clone().stepForward(0, 0, 6) } - override fun getItem(position: Int): Fragment { + override fun getPage(position: Int): LazyFragment { return TimetableDayFragment().apply { arguments = Bundle().apply { putInt("date", items[position].value) @@ -39,7 +39,7 @@ class TimetablePagerAdapter( return items.size } - override fun getPageTitle(position: Int): CharSequence? { + override fun getPageTitle(position: Int): CharSequence { val date = items[position] val pageTitle = StringBuilder(Week.getFullDayName(date.weekDay)) if (date > weekEnd || date < weekStart) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/AttachmentAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/AttachmentAdapter.kt new file mode 100644 index 00000000..cc638693 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/AttachmentAdapter.kt @@ -0,0 +1,127 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-1. + */ + +package pl.szczodrzynski.edziennik.ui.modules.views + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.chip.Chip +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import com.mikepenz.iconics.typeface.library.szkolny.font.SzkolnyFont +import com.mikepenz.iconics.utils.paddingDp +import com.mikepenz.iconics.utils.sizeDp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.Event +import pl.szczodrzynski.edziennik.data.db.entity.Message +import pl.szczodrzynski.edziennik.databinding.AttachmentListItemBinding +import pl.szczodrzynski.edziennik.onClick +import pl.szczodrzynski.edziennik.onLongClick +import pl.szczodrzynski.edziennik.utils.Utils +import pl.szczodrzynski.navlib.colorAttr +import kotlin.coroutines.CoroutineContext + +class AttachmentAdapter( + val context: Context, + val onAttachmentClick: (item: Item) -> Unit, + val onAttachmentLongClick: ((view: Chip, item: Item) -> Unit)? = null +) : RecyclerView.Adapter(), CoroutineScope { + companion object { + private const val TAG = "AttachmentAdapter" + } + + private val app = context.applicationContext as App + // optional: place the manager here + + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + var items = listOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(parent.context) + val view = AttachmentListItemBinding.inflate(inflater, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = items[position] + val b = holder.b + + // create an icon for the attachment + val icon: IIcon = when (Utils.getExtensionFromFileName(item.name)) { + "doc", "docx", "odt", "rtf" -> SzkolnyFont.Icon.szf_file_word_outline + "xls", "xlsx", "ods" -> SzkolnyFont.Icon.szf_file_excel_outline + "ppt", "pptx", "odp" -> SzkolnyFont.Icon.szf_file_powerpoint_outline + "pdf" -> SzkolnyFont.Icon.szf_file_pdf_outline + "mp3", "wav", "aac" -> SzkolnyFont.Icon.szf_file_music_outline + "mp4", "avi", "3gp", "mkv", "flv" -> SzkolnyFont.Icon.szf_file_video_outline + "jpg", "jpeg", "png", "bmp", "gif" -> SzkolnyFont.Icon.szf_file_image_outline + "zip", "rar", "tar", "7z" -> SzkolnyFont.Icon.szf_zip_box_outline + "html", "cpp", "c", "h", "css", "java", "py" -> SzkolnyFont.Icon.szf_file_code_outline + else -> CommunityMaterial.Icon.cmd_file_document_outline + } + + b.chip.text = if (item.isDownloading) { + app.getString(R.string.messages_attachment_downloading_format, item.name, item.downloadProgress) + } + else { + item.size?.let { + app.getString(R.string.messages_attachment_format, item.name, Utils.readableFileSize(it)) + } ?: item.name + } + + b.chip.chipIcon = IconicsDrawable(context) + .icon(icon) + .colorAttr(context, R.attr.colorOnSurface) + .sizeDp(24) + .paddingDp(2) + b.chip.closeIcon = IconicsDrawable(context) + .icon(CommunityMaterial.Icon.cmd_check) + .colorAttr(context, R.attr.colorOnSurface) + .sizeDp(18) + + b.chip.isCloseIconVisible = item.isDownloaded && !item.isDownloading + // prevent progress bar flickering + if (b.progressBar.isVisible != item.isDownloading) + b.progressBar.isVisible = item.isDownloading + + b.chip.onClick { onAttachmentClick(item) } + onAttachmentLongClick?.let { listener -> + b.chip.onLongClick { listener(it, item); true } + } + } + + override fun getItemCount() = items.size + + data class Item( + val profileId: Int, + val owner: Any, + val id: Long, + var name: String, + var size: Long? + ) { + val ownerId + get() = when (owner) { + is Message -> owner.id + is Event -> owner.id + else -> -1 + } + var isDownloaded = false + var isDownloading = false + var downloadProgress: Float = 0f + var downloadedName: String? = null + } + + class ViewHolder(val b: AttachmentListItemBinding) : RecyclerView.ViewHolder(b.root) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/AttachmentsView.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/AttachmentsView.kt new file mode 100644 index 00000000..22c5cfa6 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/AttachmentsView.kt @@ -0,0 +1,165 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-1. + */ + +package pl.szczodrzynski.edziennik.ui.modules.views + +import android.content.Context +import android.os.Bundle +import android.os.Environment +import android.util.AttributeSet +import androidx.appcompat.widget.PopupMenu +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask +import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration +import pl.szczodrzynski.edziennik.utils.Utils +import java.io.File + +class AttachmentsView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : RecyclerView(context, attrs, defStyleAttr) { + companion object { + private const val TAG = "AttachmentsFragment" + const val TYPE_MESSAGE = 0 + const val TYPE_EVENT = 1 + } + + private val storageDir by lazy { + val storageDir = Environment.getExternalStoragePublicDirectory("Szkolny.eu") + storageDir.mkdirs() + storageDir + } + + fun init(arguments: Bundle, owner: Any) { + val list = this as? RecyclerView ?: return + + val profileId = arguments.get("profileId") ?: return + val attachmentIds = arguments.getLongArray("attachmentIds") ?: return + val attachmentNames = arguments.getStringArray("attachmentNames") ?: return + val attachmentSizes = arguments.getLongArray("attachmentSizes") + + val adapter = AttachmentAdapter(context, onAttachmentClick = { item -> + downloadAttachment(item) + }, onAttachmentLongClick = { chip, item -> + val popupMenu = PopupMenu(chip.context, chip) + popupMenu.menu.add(0, 1, 0, R.string.messages_attachment_download_again) + popupMenu.setOnMenuItemClickListener { + downloadAttachment(item, forceDownload = true) + true + } + popupMenu.show() + }) + + attachmentIds.forEachIndexed { index, id -> + val name = attachmentNames[index] ?: return@forEachIndexed + val size = attachmentSizes?.getOrNull(index) + + val item = AttachmentAdapter.Item(profileId, owner, id, name, size) + adapter.items += item + checkAttachment(item = item) + } + + // load & configure the adapter + if (adapter.items.isNotNullNorEmpty() && list.adapter == null) { + list.adapter = adapter + list.apply { + setHasFixedSize(false) + layoutManager = LinearLayoutManager(context) + addItemDecoration(SimpleDividerItemDecoration(context)) + } + } + } + + private fun checkAttachment(item: AttachmentAdapter.Item): Boolean { + val attachmentDataFile = File(storageDir, "." + item.profileId + "_" + item.ownerId + "_" + item.id) + item.isDownloaded = if (attachmentDataFile.exists()) { + try { + val attachmentFileName = Utils.getStringFromFile(attachmentDataFile) + val attachmentFile = File(attachmentFileName) + attachmentFile.exists() + } catch (e: Exception) { + e.printStackTrace() + false + } + } else false + return item.isDownloaded + } + + private fun downloadAttachment(attachment: AttachmentAdapter.Item, forceDownload: Boolean = false) { + if (!forceDownload && attachment.isDownloaded) { + Utils.openFile(context, File(Utils.getStorageDir(), attachment.name)) + return + } + + attachment.isDownloading = true + (adapter as? AttachmentAdapter)?.let { + it.notifyItemChanged(it.items.indexOf(attachment)) + } + + EdziennikTask.attachmentGet( + attachment.profileId, + attachment.owner, + attachment.id, + attachment.name + ).enqueue(context) + } + + private val lastUpdate: Long = 0 + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) + fun onAttachmentGetEvent(event: AttachmentGetEvent) { + EventBus.getDefault().removeStickyEvent(event) + val attachment = (adapter as? AttachmentAdapter)?.items?.firstOrNull { + it.profileId == event.profileId + && it.owner == event.owner + && it.id == event.attachmentId + } ?: return + + + when (event.eventType) { + AttachmentGetEvent.TYPE_FINISHED -> { + // save the downloaded file name + attachment.downloadedName = event.fileName + attachment.isDownloading = false + attachment.isDownloaded = true + + // update file name for iDziennik which + // does not provide the name before downloading + if (!attachment.name.contains(".")) + attachment.name = File(attachment.downloadedName).name + + // open the file + Utils.openFile(context, File(Utils.getStorageDir(), attachment.name)) + } + + AttachmentGetEvent.TYPE_PROGRESS -> { + attachment.downloadProgress = event.bytesWritten.toFloat() / 1000000f + } + } + + if (event.eventType != AttachmentGetEvent.TYPE_PROGRESS || System.currentTimeMillis() - lastUpdate > 100L) { + (adapter as? AttachmentAdapter)?.let { + it.notifyItemChanged(it.items.indexOf(attachment)) + } + } + } + + override fun onAttachedToWindow() { + EventBus.getDefault().register(this) + super.onAttachedToWindow() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + EventBus.getDefault().unregister(this) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsProvider.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsProvider.kt index 77d2a902..aec64aef 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsProvider.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsProvider.kt @@ -17,10 +17,7 @@ import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.MainActivity -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.getJsonObject +import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.receivers.SzkolnyReceiver import pl.szczodrzynski.edziennik.ui.widgets.WidgetConfig @@ -43,8 +40,9 @@ class WidgetNotificationsProvider : AppWidgetProvider() { RemoteViews(app.packageName, if (config.darkTheme) R.layout.widget_notifications_dark else R.layout.widget_notifications) } - val syncIntent = Intent(SzkolnyReceiver.ACTION) - syncIntent.putExtra("task", "SyncRequest") + val syncIntent = SzkolnyReceiver.getIntent(context, Bundle( + "task" to "SyncRequest" + )) val syncPendingIntent = PendingIntent.getBroadcast(context, 0, syncIntent, 0) views.setOnClickPendingIntent(R.id.widgetNotificationsSync, syncPendingIntent) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableProvider.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableProvider.kt index 7b68a10d..bc284ba2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableProvider.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableProvider.kt @@ -27,7 +27,7 @@ import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask -import pl.szczodrzynski.edziennik.data.db.entity.Event.TYPE_HOMEWORK +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_HOMEWORK import pl.szczodrzynski.edziennik.data.db.entity.Lesson import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_NO_LESSONS import pl.szczodrzynski.edziennik.ui.widgets.LessonDialogActivity @@ -289,7 +289,7 @@ class WidgetTimetableProvider : AppWidgetProvider() { } // get all events for the current date - val events = app.db.eventDao().getAllByDateNow(profile.id, timetableDate)?.filterNotNull() ?: emptyList() + val events = app.db.eventDao().getAllByDateNow(profile.id, timetableDate) lessons.forEachIndexed { pos, lesson -> if (lesson.type == TYPE_NO_LESSONS) @@ -345,9 +345,9 @@ class WidgetTimetableProvider : AppWidgetProvider() { // add every event on this lesson for (event in events) { - if (event.startTime == null || event.startTime != lesson.displayStartTime) + if (event.time == null || event.time != lesson.displayStartTime) continue - model.eventColors.add(if (event.type == TYPE_HOMEWORK) ItemWidgetTimetableModel.EVENT_COLOR_HOMEWORK else event.getColor()) + model.eventColors.add(if (event.type == TYPE_HOMEWORK) ItemWidgetTimetableModel.EVENT_COLOR_HOMEWORK else event.eventColor) } models += model diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputKeyboardEdit.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputKeyboardEdit.kt index 477e0d27..cbb4f5a0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputKeyboardEdit.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputKeyboardEdit.kt @@ -10,9 +10,9 @@ import android.util.AttributeSet import android.view.KeyEvent import android.view.KeyEvent.KEYCODE_BACK import androidx.annotation.NonNull -import com.google.android.material.textfield.TextInputEditText +import androidx.appcompat.widget.AppCompatEditText -class TextInputKeyboardEdit : TextInputEditText { +class TextInputKeyboardEdit : AppCompatEditText { /** * Keyboard Listener @@ -53,4 +53,4 @@ class TextInputKeyboardEdit : TextInputEditText { interface KeyboardListener { fun onStateChanged(keyboardEditText: TextInputKeyboardEdit, showing: Boolean) } -} \ No newline at end of file +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt new file mode 100644 index 00000000..ca444ca5 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-3. + */ + +package pl.szczodrzynski.edziennik.utils.managers + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.startCoroutineTimer +import kotlin.coroutines.CoroutineContext + +class EventManager(val app: App) : CoroutineScope { + + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Default + + /* _ _ _____ _____ _ __ _ + | | | |_ _| / ____| (_)/ _(_) + | | | | | | | (___ _ __ ___ ___ _| |_ _ ___ + | | | | | | \___ \| '_ \ / _ \/ __| | _| |/ __| + | |__| |_| |_ ____) | |_) | __/ (__| | | | | (__ + \____/|_____| |_____/| .__/ \___|\___|_|_| |_|\___| + | | + |*/ + fun markAsSeen(event: EventFull) { + event.seen = true + startCoroutineTimer(500L, 0L) { + app.db.metadataDao().setSeen(event.profileId, event, true) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/GradesManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/GradesManager.kt index 27fdd18b..8a09df3d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/GradesManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/GradesManager.kt @@ -82,10 +82,19 @@ class GradesManager(val app: App) : CoroutineScope { else -> null } + /** + * Returns the "rounded" grade value as an integer. + * The decimal value is rounded to ceil if >= 0.75. + */ fun getRoundedGrade(value: Float): Int { return floor(value.toDouble()).toInt() + if (value % 1.0f >= 0.75) 1 else 0 } + /** + * Get a grade value, either saved in the [grade] + * or calculated including the [plusValue] and + * [minusValue]. + */ fun getGradeValue(grade: Grade): Float { if (plusValue == null && minusValue == null) return grade.value @@ -102,6 +111,10 @@ class GradesManager(val app: App) : CoroutineScope { return grade.value } + /** + * Returns a weight if the grade should be counted + * to the average, 0f otherwise. + */ fun getGradeWeight(dontCountEnabled: Boolean, dontCountGrades: List, grade: Grade): Float { if (!dontCountEnabled) return grade.weight @@ -161,6 +174,48 @@ class GradesManager(val app: App) : CoroutineScope { return color or 0xff000000.toInt() } + /** + * Calculate the grade's value using + * the specified [name]. + */ + fun getGradeValue(name: String): Float { + return when (name.toLowerCase()) { + "1-" -> 0.75f + "1" -> 1.00f + "1+" -> 1.50f + "2-" -> 1.75f + "2" -> 2.00f + "2+" -> 2.50f + "3-" -> 2.75f + "3" -> 3.00f + "3+" -> 3.50f + "4-" -> 3.75f + "4" -> 4.00f + "4+" -> 4.50f + "5-" -> 4.75f + "5" -> 5.00f + "5+" -> 5.50f + "6-" -> 5.75f + "6" -> 6.00f + "6+" -> 6.50f + "niedostateczny", "f" -> 1f + "dopuszczający", "e" -> 2f + "dostateczny", "d" -> 3f + "dobry", "c" -> 4f + "bardzo dobry", "b" -> 5f + "celujący", "a" -> 6f + else -> 0f + } + } + + /* _ _ _____ _____ _ __ _ + | | | |_ _| / ____| (_)/ _(_) + | | | | | | | (___ _ __ ___ ___ _| |_ _ ___ + | | | | | | \___ \| '_ \ / _ \/ __| | _| |/ __| + | |__| |_| |_ ____) | |_) | __/ (__| | | | | (__ + \____/|_____| |_____/| .__/ \___|\___|_|_| |_|\___| + | | + |*/ fun markAsSeen(grade: GradeFull) { grade.seen = true startCoroutineTimer(500L, 0L) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java b/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java index 5f889bfb..c1d32538 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java @@ -137,7 +137,7 @@ public class Date implements Comparable { } public static int diffDays(Date d1, Date d2) { - return (int) ((d1.getInMillis() - d2.getInMillis()) / (24 * 60 * 60 * 1000)); + return Math.round((d1.getInMillis() - d2.getInMillis()) / (24 * 60 * 60 * 1000f)); } public static boolean isToday(Date date) { @@ -254,6 +254,7 @@ public class Date implements Comparable { /** * @return 2019-06-02 */ + @NonNull public String getStringY_m_d() { return year + (month < 10 ? "-0" : "-") + month + (day < 10 ? "-0" : "-") + day; } diff --git a/app/src/main/res/drawable/ic_no_homework.xml b/app/src/main/res/drawable/ic_no_homework.xml new file mode 100644 index 00000000..d8ba8c38 --- /dev/null +++ b/app/src/main/res/drawable/ic_no_homework.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_no_messages.xml b/app/src/main/res/drawable/ic_no_messages.xml new file mode 100644 index 00000000..dfbf4678 --- /dev/null +++ b/app/src/main/res/drawable/ic_no_messages.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/app/src/main/res/layout/attachment_list_item.xml b/app/src/main/res/layout/attachment_list_item.xml new file mode 100644 index 00000000..4e8fcab0 --- /dev/null +++ b/app/src/main/res/layout/attachment_list_item.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_event_details.xml b/app/src/main/res/layout/dialog_event_details.xml index f23e9093..11f57902 100644 --- a/app/src/main/res/layout/dialog_event_details.xml +++ b/app/src/main/res/layout/dialog_event_details.xml @@ -4,10 +4,14 @@ --> + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto"> + @@ -81,14 +85,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="@style/NavView.TextView.Helper" - android:text="@{event.startTime == null ? @string/event_all_day : event.startTime.stringHM}" + android:text="@{event.time == null ? @string/event_all_day : event.time.stringHM}" tools:text="14:50"/> @@ -107,13 +111,13 @@ android:layout_height="wrap_content" android:textAppearance="@style/NavView.TextView.Helper" android:text="@string/dialog_event_details_teacher" - android:visibility="@{event.teacherFullName != null ? View.VISIBLE : View.GONE}"/> + android:visibility="@{event.teacherName != null ? View.VISIBLE : View.GONE}"/> + + + + + + + + + + - + app:flexWrap="wrap" + app:justifyContent="flex_end"> + + + android:textSize="20sp" /> + android:textSize="20sp" /> - + android:textSize="20sp" /> + + + diff --git a/app/src/main/res/layout/event_list_item.xml b/app/src/main/res/layout/event_list_item.xml index f43a7637..295ffdc2 100644 --- a/app/src/main/res/layout/event_list_item.xml +++ b/app/src/main/res/layout/event_list_item.xml @@ -4,7 +4,8 @@ --> + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> @@ -35,13 +36,23 @@ + + + tools:visibility="gone" /> + + diff --git a/app/src/main/res/layout/fragment_debug.xml b/app/src/main/res/layout/fragment_debug.xml index 6dd2413c..2fecffa5 100644 --- a/app/src/main/res/layout/fragment_debug.xml +++ b/app/src/main/res/layout/fragment_debug.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.modules.base.DebugFragment"> + tools:context=".ui.modules.debug.DebugFragment"> - - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_timetable_v2.xml b/app/src/main/res/layout/fragment_timetable_v2.xml index c14b26e5..953638cf 100644 --- a/app/src/main/res/layout/fragment_timetable_v2.xml +++ b/app/src/main/res/layout/fragment_timetable_v2.xml @@ -28,7 +28,7 @@ android:id="@+id/tabLayout" android:layout_width="match_parent" android:layout_height="48dp" - android:background="@color/colorSurface_6dp" + android:background="@color/colorSurface_1dp" app:rtl_tabIndicatorColor="?colorPrimary" app:rtl_tabMaxWidth="300dp" app:rtl_tabMinWidth="90dp" @@ -40,7 +40,7 @@ app:rtl_tabTextAppearance="@style/rtl_RecyclerTabLayout.Tab" /> - - \ No newline at end of file + diff --git a/app/src/main/res/layout/grades_item_subject.xml b/app/src/main/res/layout/grades_item_subject.xml index 8d4f47f3..440857e8 100644 --- a/app/src/main/res/layout/grades_item_subject.xml +++ b/app/src/main/res/layout/grades_item_subject.xml @@ -65,39 +65,52 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="8dp" - android:layout_marginBottom="8dp" - android:layout_marginLeft="8dp"> + android:layout_marginLeft="8dp" + android:layout_marginBottom="8dp"> + tools:visibility="visible"> - + android:baselineAligned="false" + android:orientation="horizontal" + tools:visibility="visible"> + + + + diff --git a/app/src/main/res/layout/grades_fragment.xml b/app/src/main/res/layout/grades_list_fragment.xml similarity index 67% rename from app/src/main/res/layout/grades_fragment.xml rename to app/src/main/res/layout/grades_list_fragment.xml index a9702e8c..faf8264d 100644 --- a/app/src/main/res/layout/grades_fragment.xml +++ b/app/src/main/res/layout/grades_list_fragment.xml @@ -16,8 +16,14 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + + android:visibility="gone" + app:drawableTopCompat="@drawable/ic_no_grades" + tools:visibility="visible" /> + android:visibility="gone" + tools:listitem="@layout/grades_item_subject" + tools:visibility="visible" /> diff --git a/app/src/main/res/layout/homework_fragment.xml b/app/src/main/res/layout/homework_fragment.xml new file mode 100644 index 00000000..9a015c15 --- /dev/null +++ b/app/src/main/res/layout/homework_fragment.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/homework_list.xml b/app/src/main/res/layout/homework_list.xml deleted file mode 100644 index bfcc4960..00000000 --- a/app/src/main/res/layout/homework_list.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/homework_list_fragment.xml b/app/src/main/res/layout/homework_list_fragment.xml new file mode 100644 index 00000000..d53b9d17 --- /dev/null +++ b/app/src/main/res/layout/homework_list_fragment.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/lab_fragment.xml b/app/src/main/res/layout/lab_fragment.xml new file mode 100644 index 00000000..d6988c69 --- /dev/null +++ b/app/src/main/res/layout/lab_fragment.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + +