From 959168771b5a927ef97206625d1b3abc1c3f97ef Mon Sep 17 00:00:00 2001 From: Antoni Czaplicki <56671347+Antoni-Czaplicki@users.noreply.github.com> Date: Fri, 1 Oct 2021 18:40:44 +0200 Subject: [PATCH 01/50] [UI] Add secret features. (#73) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add easter egg * Remove big file * Reformat code * Remove unneded blank lines, optimize some code * Fix small issue * Update easter egg logic Co-authored-by: Kuba Szczodrzyński --- .../contributors/ContributorsActivity.kt | 67 ++++++++++++++++-- app/src/main/res/drawable/thanos_idle.png | Bin 0 -> 20714 bytes .../main/res/layout/contributors_activity.xml | 38 ++++++++-- 3 files changed, 93 insertions(+), 12 deletions(-) create mode 100644 app/src/main/res/drawable/thanos_idle.png diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/contributors/ContributorsActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/contributors/ContributorsActivity.kt index 676a5d58..cc498495 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/contributors/ContributorsActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/contributors/ContributorsActivity.kt @@ -1,23 +1,24 @@ package pl.szczodrzynski.edziennik.ui.modules.settings.contributors import android.os.Bundle +import android.os.Process +import android.view.KeyEvent import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isInvisible import androidx.core.view.isVisible -import androidx.viewpager.widget.ViewPager -import com.google.android.material.tabs.TabLayout +import com.google.android.material.dialog.MaterialAlertDialogBuilder 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.Bundle -import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.data.api.szkolny.response.ContributorsResponse import pl.szczodrzynski.edziennik.databinding.ContributorsActivityBinding import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.FragmentLazyPagerAdapter import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar import kotlin.coroutines.CoroutineContext +import kotlin.system.exitProcess class ContributorsActivity : AppCompatActivity(), CoroutineScope { companion object { @@ -35,6 +36,28 @@ class ContributorsActivity : AppCompatActivity(), CoroutineScope { // local/private variables go here private val errorSnackbar: ErrorSnackbar by lazy { ErrorSnackbar(this) } + private var konami = 0 + + override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { + when (keyCode) { + KeyEvent.KEYCODE_DPAD_UP -> konami in 0..1 + KeyEvent.KEYCODE_DPAD_DOWN -> konami in 2..3 + KeyEvent.KEYCODE_DPAD_LEFT -> konami in 4..6 step 2 + KeyEvent.KEYCODE_DPAD_RIGHT -> konami in 5..7 step 2 + KeyEvent.KEYCODE_B -> konami == 8 + KeyEvent.KEYCODE_A -> konami == 9 + else -> false + }.let { + if (!it) { + konami = 0 + return super.onKeyUp(keyCode, event) + } + konami++ + b.konami.isVisible = konami == 10 + return true + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) app = application as App @@ -45,6 +68,40 @@ class ContributorsActivity : AppCompatActivity(), CoroutineScope { b.tabLayout.isVisible = false b.viewPager.isVisible = false + b.szkolny.onLongClick { + if (b.konami.isVisible) { + b.glove.isVisible = true + b.szkolny.isInvisible = true + } + true + } + + b.glove.onClick { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.are_you_sure) + .setMessage(R.string.dev_mode_enable_warning) + .setPositiveButton(R.string.yes) { _, _ -> + app.config.devMode = true + App.devMode = true + MaterialAlertDialogBuilder(this) + .setTitle("Restart") + .setMessage("Wymagany restart aplikacji") + .setPositiveButton(R.string.ok) { _, _ -> + Process.killProcess(Process.myPid()) + Runtime.getRuntime().exit(0) + exitProcess(0) + } + .setCancelable(false) + .show() + } + .setNegativeButton(R.string.no) { _, _ -> + app.config.devMode = false + App.devMode = false + this.finish() + } + .show() + } + launch { contributors = contributors ?: SzkolnyApi(app).runCatching(errorSnackbar) { getContributors() diff --git a/app/src/main/res/drawable/thanos_idle.png b/app/src/main/res/drawable/thanos_idle.png new file mode 100644 index 0000000000000000000000000000000000000000..b94feb351c533c3f1c306b583c099302dfcf20bd GIT binary patch literal 20714 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/contributors_activity.xml b/app/src/main/res/layout/contributors_activity.xml index b05e9c37..a9d8a452 100644 --- a/app/src/main/res/layout/contributors_activity.xml +++ b/app/src/main/res/layout/contributors_activity.xml @@ -20,6 +20,14 @@ android:layout_height="match_parent" android:fitsSystemWindows="true"> + + - + android:layout_gravity="center" + android:layout_marginTop="16dp"> + + + + + Date: Fri, 1 Oct 2021 19:46:11 +0200 Subject: [PATCH 02/50] [Data] Refactor checking homework event type. --- .../edziennik/data/api/task/Notifications.kt | 10 +++--- .../edziennik/data/db/dao/MetadataDao.java | 32 +++++++++---------- .../edziennik/data/db/entity/Event.kt | 3 ++ .../data/firebase/SzkolnyAppFirebase.kt | 6 ++-- .../ui/dialogs/event/EventDetailsDialog.kt | 3 +- .../ui/modules/debug/LabPageFragment.kt | 3 +- .../timetable/WidgetTimetableProvider.kt | 3 +- 7 files changed, 30 insertions(+), 30 deletions(-) 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 28b5c9e9..43cc1455 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 @@ -70,7 +70,7 @@ class Notifications(val app: App, val notifications: MutableList, private fun eventNotifications() { for (event in app.db.eventDao().getNotNotifiedNow().filter { it.date >= today }) { - val text = if (event.type == Event.TYPE_HOMEWORK) + val text = if (event.isHomework) app.getString( if (event.subjectLongName.isNullOrEmpty()) R.string.notification_homework_no_subject_format @@ -98,7 +98,7 @@ class Notifications(val app: App, val notifications: MutableList, event.time?.stringHM ?: app.getString(R.string.event_all_day), event.topic.take(200) ) - val type = if (event.type == Event.TYPE_HOMEWORK) Notification.TYPE_NEW_HOMEWORK else Notification.TYPE_NEW_EVENT + val type = if (event.isHomework) Notification.TYPE_NEW_HOMEWORK else Notification.TYPE_NEW_EVENT notifications += Notification( id = Notification.buildId(event.profileId, type, event.id), title = app.getNotificationTitle(type), @@ -107,7 +107,7 @@ class Notifications(val app: App, val notifications: MutableList, type = type, profileId = event.profileId, profileName = profiles.singleOrNull { it.id == event.profileId }?.name, - viewId = if (event.type == Event.TYPE_HOMEWORK) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA, + viewId = if (event.isHomework) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA, addedDate = event.addedDate ).addExtra("eventId", event.id).addExtra("eventDate", event.date.value.toLong()) } @@ -132,7 +132,7 @@ class Notifications(val app: App, val notifications: MutableList, event.time?.stringHM ?: app.getString(R.string.event_all_day), event.topic.take(200) ) - val type = if (event.type == Event.TYPE_HOMEWORK) Notification.TYPE_NEW_HOMEWORK else Notification.TYPE_NEW_EVENT + val type = if (event.isHomework) Notification.TYPE_NEW_HOMEWORK else Notification.TYPE_NEW_EVENT notifications += Notification( id = Notification.buildId(event.profileId, type, event.id), title = app.getNotificationTitle(type), @@ -141,7 +141,7 @@ class Notifications(val app: App, val notifications: MutableList, type = type, profileId = event.profileId, profileName = profiles.singleOrNull { it.id == event.profileId }?.name, - viewId = if (event.type == Event.TYPE_HOMEWORK) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA, + viewId = if (event.isHomework) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA, addedDate = event.addedDate ).addExtra("eventId", event.id).addExtra("eventDate", event.date.value.toLong()) } 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 eac92f21..ab694702 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 @@ -4,6 +4,15 @@ package pl.szczodrzynski.edziennik.data.db.dao; +import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_ANNOUNCEMENT; +import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_ATTENDANCE; +import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_EVENT; +import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_GRADE; +import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_HOMEWORK; +import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_LESSON_CHANGE; +import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_MESSAGE; +import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_NOTICE; + import androidx.lifecycle.LiveData; import androidx.room.Dao; import androidx.room.Insert; @@ -23,15 +32,6 @@ import pl.szczodrzynski.edziennik.data.db.entity.Notice; import pl.szczodrzynski.edziennik.data.db.full.LessonFull; import pl.szczodrzynski.edziennik.utils.models.UnreadCounter; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_ANNOUNCEMENT; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_ATTENDANCE; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_EVENT; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_GRADE; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_HOMEWORK; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_LESSON_CHANGE; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_MESSAGE; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_NOTICE; - @Dao public abstract class MetadataDao { @Insert(onConflict = OnConflictStrategy.IGNORE) @@ -78,8 +78,8 @@ public abstract class MetadataDao { } } if (o instanceof Event) { - if (add(new Metadata(profileId, ((Event) o).getType() == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, ((Event) o).getId(), seen, false)) == -1) { - updateSeen(profileId, ((Event) o).getType() == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, ((Event) o).getId(), seen); + if (add(new Metadata(profileId, ((Event) o).isHomework() ? TYPE_HOMEWORK : TYPE_EVENT, ((Event) o).getId(), seen, false)) == -1) { + updateSeen(profileId, ((Event) o).isHomework() ? TYPE_HOMEWORK : TYPE_EVENT, ((Event) o).getId(), seen); } } if (o instanceof LessonFull) { @@ -117,8 +117,8 @@ public abstract class MetadataDao { } } if (o instanceof Event) { - if (add(new Metadata(profileId, ((Event) o).getType() == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, ((Event) o).getId(), false, notified)) == -1) { - updateNotified(profileId, ((Event) o).getType() == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, ((Event) o).getId(), notified); + if (add(new Metadata(profileId, ((Event) o).isHomework() ? TYPE_HOMEWORK : TYPE_EVENT, ((Event) o).getId(), false, notified)) == -1) { + updateNotified(profileId, ((Event) o).isHomework() ? TYPE_HOMEWORK : TYPE_EVENT, ((Event) o).getId(), notified); } } if (o instanceof LessonFull) { @@ -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.getType() == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, o.getId(), seen, notified)) == -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); + if (add(new Metadata(profileId, o.isHomework() ? TYPE_HOMEWORK : TYPE_EVENT, o.getId(), seen, notified)) == -1) { + updateSeen(profileId, o.isHomework() ? TYPE_HOMEWORK : TYPE_EVENT, o.getId(), seen); + updateNotified(profileId, o.isHomework() ? TYPE_HOMEWORK : TYPE_EVENT, o.getId(), notified); } } } 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 index 3e59cc17..c6666c3a 100644 --- 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 @@ -123,6 +123,9 @@ open class Event( it.timeInMillis += 45 * MINUTE * 1000 } + val isHomework + get() = type == TYPE_HOMEWORK + @Ignore fun withMetadata(metadata: Metadata) = EventFull(this, metadata) } 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 111fef4a..a0e70326 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 @@ -139,13 +139,13 @@ class SzkolnyAppFirebase(val app: App, val profiles: List, val message: val metadata = Metadata( event.profileId, - if (event.type == Event.TYPE_HOMEWORK) Metadata.TYPE_HOMEWORK else Metadata.TYPE_EVENT, + if (event.isHomework) Metadata.TYPE_HOMEWORK else Metadata.TYPE_EVENT, event.id, false, true ) - val type = if (event.type == Event.TYPE_HOMEWORK) Notification.TYPE_NEW_SHARED_HOMEWORK else Notification.TYPE_NEW_SHARED_EVENT + val type = if (event.isHomework) Notification.TYPE_NEW_SHARED_HOMEWORK else Notification.TYPE_NEW_SHARED_EVENT val notificationFilter = app.config.getFor(event.profileId).sync.notificationFilter if (!notificationFilter.contains(type)) { @@ -156,7 +156,7 @@ class SzkolnyAppFirebase(val app: App, val profiles: List, val message: type = type, profileId = profile.id, profileName = profile.name, - viewId = if (event.type == Event.TYPE_HOMEWORK) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA, + viewId = if (event.isHomework) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA, addedDate = event.addedDate ).addExtra("eventId", event.id).addExtra("eventDate", event.date.value.toLong()) notificationList += notification 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 c80f0c63..7653fb69 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 @@ -22,7 +22,6 @@ 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 @@ -227,7 +226,7 @@ class EventDetailsDialog( ) } - if (event.homeworkBody == null && !event.addedManually && event.type == Event.TYPE_HOMEWORK) { + if (!event.addedManually && event.isHomework && event.homeworkBody == null) { b.bodyTitle.isVisible = true b.bodyProgressBar.isVisible = true b.body.isVisible = false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabPageFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabPageFragment.kt index 19ae4697..19598af5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabPageFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabPageFragment.kt @@ -17,7 +17,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.config.Config -import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.databinding.LabFragmentBinding import pl.szczodrzynski.edziennik.ui.dialogs.profile.ProfileRemoveDialog import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment @@ -55,7 +54,7 @@ class LabPageFragment : LazyFragment(), CoroutineScope { 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) + val ids = events.sortedBy { it.date }.filter { it.isHomework }.takeLast(10) ids.forEach { app.db.metadataDao().setSeen(App.profileId, it, false) } 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 6d5badf5..7482a065 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,6 @@ 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.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 @@ -355,7 +354,7 @@ class WidgetTimetableProvider : AppWidgetProvider() { for (event in events) { if (event.time == null || event.time != lesson.displayStartTime) continue - model.eventColors.add(if (event.type == TYPE_HOMEWORK) ItemWidgetTimetableModel.EVENT_COLOR_HOMEWORK else event.eventColor) + model.eventColors.add(if (event.isHomework) ItemWidgetTimetableModel.EVENT_COLOR_HOMEWORK else event.eventColor) } models += model From 0b4421c7a7a9f6deef7cb4e6091829a82e4a0f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Fri, 1 Oct 2021 21:06:48 +0200 Subject: [PATCH 03/50] [API/Mobidziennik] Implement getting full event description. --- .../edziennik/data/api/Regexes.kt | 9 +++ .../edziennik/mobidziennik/Mobidziennik.kt | 11 +++- .../data/web/MobidziennikWebCalendar.kt | 1 + .../data/web/MobidziennikWebGetEvent.kt | 57 +++++++++++++++++++ .../edziennik/data/api/models/Data.kt | 6 +- .../szczodrzynski/edziennik/data/db/AppDb.kt | 5 +- .../edziennik/data/db/entity/Event.kt | 7 +++ .../edziennik/data/db/full/EventFull.kt | 1 + .../data/db/migration/Migration94.kt | 26 +++++++++ .../ui/dialogs/event/EventDetailsDialog.kt | 2 +- 10 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetEvent.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration94.kt 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 2bbf1894..597c02d7 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 @@ -16,6 +16,10 @@ object Regexes { """[^0-9]""".toRegex() } + val HTML_BR by lazy { + """""".toRegex() + } + val MOBIDZIENNIK_GRADES_SUBJECT_NAME by lazy { @@ -128,6 +132,11 @@ object Regexes { } + val MOBIDZIENNIK_EVENT_CONTENT by lazy { + """

(.+?) \(wpisał\(a\) (.+?) w dniu ([0-9-]{10})\).+?(.+?)""".toRegex(DOT_MATCHES_ALL) 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 0e002576..e19fabca 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 @@ -120,8 +120,15 @@ 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() + if (eventFull.isHomework) { + MobidziennikWebGetHomework(data, eventFull) { + completed() + } + } + else { + MobidziennikWebGetEvent(data, eventFull) { + completed() + } } } } 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 95c1f6df..53122198 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 @@ -81,6 +81,7 @@ class MobidziennikWebCalendar(override val data: DataMobidziennik, subjectId = -1, teamId = data.teamClass?.id ?: -1 ) + eventObject.isDownloaded = false data.eventList.add(eventObject) data.metadataList.add( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetEvent.kt new file mode 100644 index 00000000..27cacb5d --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetEvent.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-1. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web + +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.data.api.POST +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 MobidziennikWebGetEvent( + override val data: DataMobidziennik, + val event: EventFull, + val onSuccess: () -> Unit +) : MobidziennikWeb(data, null) { + companion object { + private const val TAG = "MobidziennikWebGetEvent" + } + + init { + val params = listOf( + "typ" to "kalendarz", + "uczen" to data.studentId, + "id" to event.id, + ) + + webGet(TAG, "/dziennik/ajaxkalendarzklasowy", method = POST, parameters = params) { text -> + Regexes.MOBIDZIENNIK_EVENT_CONTENT.find(text)?.let { + val topic = it[1] + val teacherName = it[2] + val teacher = data.getTeacherByLastFirst(teacherName) + val addedDate = Date.fromY_m_d(it[3]) + val body = it[4] + .replace("\n", "") + .replace(Regexes.HTML_BR, "\n") + + event.topic = topic + event.homeworkBody = body + event.teacherId = teacher.id + event.addedDate = addedDate.inMillis + event.isDownloaded = true + } + + 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/models/Data.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt index 2b7bdf1a..68d7a17e 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 @@ -437,11 +437,13 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt } fun getTeacherByLastFirst(nameLastFirst: String, loginId: String? = null): Teacher { + // comparing full name is safer than splitting and swapping + val teacher = teacherList.singleOrNull { it.fullNameLastFirst == nameLastFirst } val nameParts = nameLastFirst.split(" ") return if (nameParts.size == 1) - getTeacher(nameParts[0], "", loginId) + validateTeacher(teacher, nameParts[0], "", loginId) else - getTeacher(nameParts[1], nameParts[0], loginId) + validateTeacher(teacher, nameParts[1], nameParts[0], loginId) } fun getTeacherByFirstLast(nameFirstLast: String, loginId: String? = null): Teacher { 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 4547d852..32b4b703 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 = 93) +], version = 94) @TypeConverters( ConverterTime::class, ConverterDate::class, @@ -178,7 +178,8 @@ abstract class AppDb : RoomDatabase() { Migration90(), Migration91(), Migration92(), - Migration93() + Migration93(), + Migration94() ).allowMainThreadQueries().build() } } 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 index c6666c3a..4d7d4450 100644 --- 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 @@ -83,6 +83,13 @@ open class Event( @ColumnInfo(name = "eventIsDone") var isDone: Boolean = false + /** + * Whether the full contents of the event are already stored locally. + * There may be a need to download the full topic or body. + */ + @ColumnInfo(name = "eventIsDownloaded") + var isDownloaded: Boolean = true + /** * 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]. 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 index 01e85395..86a5ab92 100644 --- 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 @@ -26,6 +26,7 @@ class EventFull( sharedBy = it.sharedBy sharedByName = it.sharedByName blacklisted = it.blacklisted + isDownloaded = it.isDownloaded homeworkBody = it.homeworkBody attachmentIds = it.attachmentIds attachmentNames = it.attachmentNames diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration94.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration94.kt new file mode 100644 index 00000000..84df9c59 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration94.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-1. + */ + +package pl.szczodrzynski.edziennik.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import pl.szczodrzynski.edziennik.data.db.entity.Event +import pl.szczodrzynski.edziennik.data.db.entity.LoginStore + +class Migration94 : Migration(93, 94) { + override fun migrate(database: SupportSQLiteDatabase) { + // events - is downloaded flag + + // get all profiles using Mobidziennik + database.execSQL("CREATE TABLE _94_ids (id INTEGER NOT NULL);") + database.execSQL("INSERT INTO _94_ids SELECT profileId FROM profiles JOIN loginStores USING(loginStoreId) WHERE loginStores.loginStoreType = ${LoginStore.LOGIN_TYPE_MOBIDZIENNIK};") + + database.execSQL("ALTER TABLE events ADD COLUMN eventIsDownloaded INT NOT NULL DEFAULT 1;") + // set isDownloaded = 0 for information events in Mobidziennik + database.execSQL("UPDATE events SET eventIsDownloaded = 0 WHERE profileId IN (SELECT id FROM _94_ids) AND eventType = ${Event.TYPE_INFORMATION};") + + database.execSQL("DROP TABLE _94_ids;") + } +} 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 7653fb69..3c4be7bd 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 @@ -226,7 +226,7 @@ class EventDetailsDialog( ) } - if (!event.addedManually && event.isHomework && event.homeworkBody == null) { + if (!event.addedManually && (!event.isDownloaded || event.isHomework && event.homeworkBody == null)) { b.bodyTitle.isVisible = true b.bodyProgressBar.isVisible = true b.body.isVisible = false From 3cdca5eb3397a473bf331c789e9fdd752eae84b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sat, 2 Oct 2021 16:15:02 +0200 Subject: [PATCH 04/50] [API/Mobidziennik] Fix showing homework attachments. --- .../edziennik/data/api/Regexes.kt | 23 +++++--- .../data/web/EdudziennikWebGetHomework.kt | 1 + .../synergia/LibrusSynergiaGetHomework.kt | 1 + .../data/synergia/LibrusSynergiaHomework.kt | 1 + .../data/web/MobidziennikWebGetEvent.kt | 2 +- .../data/web/MobidziennikWebGetHomework.kt | 59 ++++++++++++------- .../data/web/MobidziennikWebGetMessage.kt | 20 +++---- .../data/web/MobidziennikWebHomework.kt | 7 ++- .../podlasie/data/api/PodlasieApiEvents.kt | 1 + .../podlasie/data/api/PodlasieApiHomework.kt | 1 + .../data/api/edziennik/vulcan/Vulcan.kt | 1 + 11 files changed, 72 insertions(+), 45 deletions(-) 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 597c02d7..b42de2c4 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 @@ -54,15 +54,16 @@ object Regexes { """events: (.+),$""".toRegex(RegexOption.MULTILINE) } + val MOBIDZIENNIK_WEB_ATTACHMENT by lazy { + """href="https://.+?\.mobidziennik.pl/.+?&(?:amp;)?zalacznik(_rozwiazania)?=([0-9]+)".+?>(.+?)(?: )?""".toRegex() + } + val MOBIDZIENNIK_MESSAGE_READ_DATE by lazy { """czas przeczytania:.+?,\s([0-9]+)\s(.+?)\s([0-9]{4}),\sgodzina\s([0-9:]+)""".toRegex(DOT_MATCHES_ALL) } val MOBIDZIENNIK_MESSAGE_SENT_READ_DATE by lazy { """.+?,\s([0-9]+)\s(.+?)\s([0-9]{4}),\sgodzina\s([0-9:]+)""".toRegex(DOT_MATCHES_ALL) } - val MOBIDZIENNIK_MESSAGE_ATTACHMENT by lazy { - """href="https://.+?\.mobidziennik.pl/.+?&(?:amp;)?zalacznik=([0-9]+)"(?:.+?(.+?)\s*\s*\((.+?),\s*(.+?)\)""".toRegex(DOT_MATCHES_ALL) } - val MOBIDZIENNIK_HOMEWORK_ROW by lazy { + val MOBIDZIENNIK_MOBILE_HOMEWORK_ROW by lazy { """class="rowRolling">(.+?\s*)""".toRegex(DOT_MATCHES_ALL) } - val MOBIDZIENNIK_HOMEWORK_ITEM by lazy { + val MOBIDZIENNIK_MOBILE_HOMEWORK_ITEM by lazy { """

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

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

""".toRegex(DOT_MATCHES_ALL) } - val MOBIDZIENNIK_HOMEWORK_ID by lazy { - """zadanieFormularz\(([0-9]+),""".toRegex(DOT_MATCHES_ALL) + val MOBIDZIENNIK_MOBILE_HOMEWORK_ID by lazy { + """name="id_zadania" value="([0-9]+)"""".toRegex(DOT_MATCHES_ALL) } - val MOBIDZIENNIK_HOMEWORK_ATTACHMENT by lazy { + val MOBIDZIENNIK_MOBILE_HOMEWORK_ATTACHMENT by lazy { """zalacznik(_zadania)?=([0-9]+)'.+?word-break">(.+?)""".toRegex(DOT_MATCHES_ALL) } + val MOBIDZIENNIK_WEB_HOMEWORK_ADDED_DATE by lazy { + """Wpisał\(a\):\s+\s+(.+?), (.+?), ([0-9]{1,2}) (.+?) ([0-9]{4}), godzina ([0-9:]+)""".toRegex() + } + val MOBIDZIENNIK_TIMETABLE_TOP by lazy { """

.+?
""".toRegex(DOT_MATCHES_ALL) 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 index 84171699..5213b7eb 100644 --- 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 @@ -29,6 +29,7 @@ class EdudziennikWebGetHomework( if (description != null) event.topic = Html.fromHtml(description).toString() event.homeworkBody = "" + event.isDownloaded = true event.attachmentNames = null data.eventList += 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 index 379bd42d..75903907 100644 --- 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 @@ -24,6 +24,7 @@ class LibrusSynergiaGetHomework(override val data: DataLibrus, event.topic = table[1].select("td")[1].text() event.homeworkBody = Html.fromHtml(table[5].select("td")[1].html()).toString() + event.isDownloaded = true event.attachmentIds = mutableListOf() event.attachmentNames = mutableListOf() 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 d33f6911..42064610 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 @@ -79,6 +79,7 @@ class LibrusSynergiaHomework(override val data: DataLibrus, teamId = data.teamClass?.id ?: -1, addedDate = addedDate.inMillis ) + eventObject.isDownloaded = false data.eventList.add(eventObject) data.metadataList.add(Metadata( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetEvent.kt index 27cacb5d..41e46e65 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetEvent.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetEvent.kt @@ -42,9 +42,9 @@ class MobidziennikWebGetEvent( event.topic = topic event.homeworkBody = body + event.isDownloaded = true event.teacherId = teacher.id event.addedDate = addedDate.inMillis - event.isDownloaded = true } data.eventList.add(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 index b0699b4b..5b8d348f 100644 --- 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 @@ -12,41 +12,58 @@ 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 +import pl.szczodrzynski.edziennik.utils.models.Time class MobidziennikWebGetHomework(override val data: DataMobidziennik, val event: EventFull, val onSuccess: () -> Unit ) : MobidziennikWeb(data, null) { companion object { - private const val TAG = "MobidziennikWebHomework" + private const val TAG = "MobidziennikWebGetHomework" } init { - val endpoint = if (event.date >= Date.getToday()) - "zadaniadomowe" - else - "zadaniadomowearchiwalne" - - webGet(TAG, "/mobile/$endpoint") { text -> + webGet(TAG, "/dziennik/wyslijzadanie/?id_zadania=${event.id}&uczen=${data.studentId}") { 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) + event.clearAttachments() + Regexes.MOBIDZIENNIK_WEB_ATTACHMENT.findAll(text).forEach { match -> + if (match[1].isNotEmpty()) return@forEach - - event.attachmentIds = mutableListOf() - event.attachmentNames = mutableListOf() - Regexes.MOBIDZIENNIK_HOMEWORK_ATTACHMENT.findAll(tableRow).forEach { - event.attachmentIds?.add(it[2].toLongOrNull() ?: return@forEach) - event.attachmentNames?.add(it[3]) - } - - event.homeworkBody = "" + val attachmentId = match[2].toLong() + val attachmentName = match[3] + event.addAttachment(attachmentId, attachmentName) } + Regexes.MOBIDZIENNIK_WEB_HOMEWORK_ADDED_DATE.find(text)?.let { + // (Kowalski Jan), (wtorek), (2) (stycznia) (2019), godzina (12:34:56) + val month = when (it[4]) { + "stycznia" -> 1 + "lutego" -> 2 + "marca" -> 3 + "kwietnia" -> 4 + "maja" -> 5 + "czerwca" -> 6 + "lipca" -> 7 + "sierpnia" -> 8 + "września" -> 9 + "października" -> 10 + "listopada" -> 11 + "grudnia" -> 12 + else -> 1 + } + val addedDate = Date( + it[5].toInt(), + month, + it[3].toInt() + ) + val time = Time.fromH_m_s(it[6]) + event.addedDate = addedDate.combineWith(time) + } + + event.homeworkBody = "" + event.isDownloaded = true + data.eventList.add(event) data.eventListReplace = true 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 7282cc2d..542e4488 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 @@ -118,18 +118,16 @@ class MobidziennikWebGetMessage(override val data: DataMobidziennik, this.body = body.html() clearAttachments() - content.select("ul li").map { it.select("a").first() }.forEach { - val attachmentName = it.ownText() - Regexes.MOBIDZIENNIK_MESSAGE_ATTACHMENT.find(it.outerHtml())?.let { match -> - val attachmentId = match[1].toLong() - var size = match[2].toFloatOrNull() ?: -1f - when (match[3]) { - "K" -> size *= 1024f - "M" -> size *= 1024f * 1024f - "G" -> size *= 1024f * 1024f * 1024f - } - message.addAttachment(attachmentId, attachmentName, size.toLong()) + Regexes.MOBIDZIENNIK_WEB_ATTACHMENT.findAll(text).forEach { match -> + val attachmentId = match[2].toLong() + val attachmentName = match[3] + var size = match[4].toFloatOrNull() ?: -1f + when (match[5]) { + "K" -> size *= 1024f + "M" -> size *= 1024f * 1024f + "G" -> size *= 1024f * 1024f * 1024f } + message.addAttachment(attachmentId, attachmentName, size.toLong()) } } 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 index 944b21ea..ddae85e6 100644 --- 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 @@ -33,14 +33,14 @@ class MobidziennikWebHomework(override val data: DataMobidziennik, webGet(TAG, "/mobile/$endpoint") { text -> MobidziennikLuckyNumberExtractor(data, text) - Regexes.MOBIDZIENNIK_HOMEWORK_ROW.findAll(text).forEach { homeworkMatch -> + Regexes.MOBIDZIENNIK_MOBILE_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 + val id = Regexes.MOBIDZIENNIK_MOBILE_HOMEWORK_ID.find(tableRow)?.get(1)?.toLongOrNull() ?: return@forEach if (event.id != id) return@forEach @@ -48,12 +48,13 @@ class MobidziennikWebHomework(override val data: DataMobidziennik, event.attachmentIds = mutableListOf() event.attachmentNames = mutableListOf() - Regexes.MOBIDZIENNIK_HOMEWORK_ATTACHMENT.findAll(tableRow).forEach { + Regexes.MOBIDZIENNIK_MOBILE_HOMEWORK_ATTACHMENT.findAll(tableRow).forEach { event.attachmentIds?.add(it[1].toLongOrNull() ?: return@forEach) event.attachmentNames?.add(it[2]) } event.homeworkBody = "" + event.isDownloaded = true } //data.eventList.add(eventObject) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiEvents.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiEvents.kt index 8a169ee7..6139b402 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiEvents.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiEvents.kt @@ -57,6 +57,7 @@ class PodlasieApiEvents(val data: DataPodlasie, val rows: List) { addedDate = addedDate ).apply { homeworkBody = description + isDownloaded = true } data.eventList.add(eventObject) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiHomework.kt index 6ab53328..1165cf79 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiHomework.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiHomework.kt @@ -37,6 +37,7 @@ class PodlasieApiHomework(val data: DataPodlasie, val rows: List) { addedDate = addedDate ).apply { homeworkBody = description + isDownloaded = true } eventObject.attachmentIds = mutableListOf() 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 d066aca0..64c87bf6 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 @@ -164,6 +164,7 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va override fun getEvent(eventFull: EventFull) { eventFull.homeworkBody = "" + eventFull.isDownloaded = true EventBus.getDefault().postSticky(EventGetEvent(eventFull)) completed() From 2d277e80cc23df5cbaef98a167dc1c5e2eec7c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sat, 2 Oct 2021 16:15:32 +0200 Subject: [PATCH 05/50] [UI] Restore showing message attachment size. --- .../edziennik/ui/modules/messages/MessageFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 e147dea6..3f0cd068 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 @@ -301,8 +301,8 @@ class MessageFragment : Fragment(), CoroutineScope { 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()) + if (message.attachmentSizes.isNotNullNorEmpty()) + it.putLongArray("attachmentSizes", message.attachmentSizes!!.toLongArray()) }, owner = message) } } From 91cfa7e9452d2e8bd9aa8371905d9d82bf17ac36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 3 Oct 2021 16:02:36 +0200 Subject: [PATCH 06/50] [API/Mobidziennik] Implement syncing extra lessons. (#83) --- .../data/api/MobidziennikApiTeams.kt | 1 - .../data/api/MobidziennikApiTimetable.kt | 2 +- .../data/web/MobidziennikWebTimetable.kt | 90 ++++++++++++------- .../data/api/models/DataRemoveModel.kt | 14 +-- .../szczodrzynski/edziennik/data/db/AppDb.kt | 5 +- .../edziennik/data/db/dao/TimetableDao.kt | 12 +-- .../edziennik/data/db/entity/Lesson.kt | 10 ++- .../data/db/migration/Migration95.kt | 15 ++++ 8 files changed, 95 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration95.kt 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 6440b04e..b32e9d29 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 @@ -35,7 +35,6 @@ class MobidziennikApiTeams(val data: DataMobidziennik, tableTeams: List? } if (tableRelations != null) { val allTeams = data.teamList.values() - data.teamList.clear() for (row in tableRelations) { if (row.isEmpty()) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTimetable.kt index aa07b1c3..023e817b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTimetable.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTimetable.kt @@ -24,7 +24,7 @@ class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List) { val dataStart = Date.getToday() val dataEnd = dataStart.clone().stepForward(0, 0, 7 + (6 - dataStart.weekDay)) - data.toRemove.add(DataRemoveModel.Timetable.between(dataStart.clone(), dataEnd)) + data.toRemove.add(DataRemoveModel.Timetable.between(dataStart.clone(), dataEnd, isExtra = false)) val dataDays = mutableListOf() while (dataStart <= dataEnd) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebTimetable.kt index 6ceb3fd9..224653dd 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebTimetable.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebTimetable.kt @@ -11,6 +11,7 @@ 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_TIMETABLE import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel.Timetable.Companion.between import pl.szczodrzynski.edziennik.data.db.entity.Lesson import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS @@ -81,22 +82,30 @@ class MobidziennikWebTimetable( } ?: currentWeekStart val syncFutureDate = startDate > nextWeekEnd - // TODO: 2021-09-09 make DataRemoveModel keep extra lessons - val syncExtraLessons = false && System.currentTimeMillis() - (lastSync ?: 0) > 2 * DAY * MS - if (!syncFutureDate && !syncExtraLessons) { + val syncPastDate = startDate < currentWeekStart + val syncExtraLessons = System.currentTimeMillis() - (lastSync ?: 0) > 2 * DAY * MS + // sync not needed - everything present in the "API" + if (!syncFutureDate && !syncPastDate && !syncExtraLessons) { onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE) } else { val types = when { - syncFutureDate -> mutableListOf("podstawowy")//, "pozalekcyjny") + syncFutureDate || syncPastDate -> mutableListOf("podstawowy", "pozalekcyjny") syncExtraLessons -> mutableListOf("pozalekcyjny") else -> mutableListOf() } + val syncingExtra = types.contains("pozalekcyjny") + syncTypes(types, startDate) { - // set as synced now only when not syncing future date - // (to avoid waiting 2 days for normal sync after future sync) - if (syncExtraLessons) + if (syncingExtra) { + val endDate = startDate.clone().stepForward(0, 0, 7) + data.toRemove.add(between(startDate, endDate, isExtra = true)) + } + + // set as synced now only when not syncing future/past date + // (to avoid waiting 2 days for normal sync after future/past sync) + if (!syncFutureDate && !syncPastDate) data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE, SYNC_ALWAYS) onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE) } @@ -113,7 +122,7 @@ class MobidziennikWebTimetable( MobidziennikLuckyNumberExtractor(data, html) readRangesH(html) readRangesV(html) - readLessons(html) + readLessons(html, isExtra = type == "pozalekcyjny") syncTypes(types, startDate, onSuccess) } } @@ -183,7 +192,7 @@ class MobidziennikWebTimetable( } @SuppressLint("LongLogTag", "LogNotTimber") - private fun readLessons(html: String) { + private fun readLessons(html: String, isExtra: Boolean) { val matches = Regexes.MOBIDZIENNIK_TIMETABLE_CELL.findAll(html) val noLessonDays = mutableListOf() @@ -215,51 +224,63 @@ class MobidziennikWebTimetable( var teamName: String? = null val items = (cleanup(match[3]) + cleanup(match[4])).toMutableList() + // comparing items size before and after the iteration var length = 0 while (items.isNotEmpty() && length != items.size) { length = items.size - val toRemove = mutableListOf() - items.forEachIndexed { i, item -> + var i = 0 + while (i < items.size) { + // just to remain safe - I have no idea how all of this works. + if (i < 0) + break + val item = items[i] when { - item.isEmpty() -> - toRemove.add(item) - item.contains(":") && item.contains(" - ") -> - toRemove.add(item) + // remove empty items + item.isEmpty() -> { + items.remove(item) + i-- + } + // remove HH:MM items - it's calculated from the block position + item.contains(":") && item.contains(" - ") -> { + items.remove(item) + i-- + } item.startsWith("%") -> { - subjectName = item.trim('%') - // I have no idea what's going on here - // ok now seriously.. the subject (long or short) item - // may NOT be 0th, as the HH:MM - HH:MM item may be before - // or even the typeName item. As these are always **before**, - // they are removed in previous iterations, so the first not removed - // item should be the long/short subjectName needing to be removed now. - toRemove.add(items[toRemove.size]) - // ...and this has to be added later - toRemove.add(item) + // the one wrapped in % is the short subject name + items.remove(item) + // remove the first remaining item + subjectName = items.removeAt(0) + // decrement the index counter + i -= 2 } - item.startsWith("&") -> { - typeName = item.trim('&') - toRemove.add(item) + item.startsWith("$") -> { + typeName = item.trim('$') + items.remove(item) + i-- } - typeName != null && (item.contains(typeName!!) || item.contains("
")) -> { - toRemove.add(item) + typeName != null && (item.contains(typeName) || item.contains("
")) -> { + items.remove(item) + i-- } item.contains("(") && item.contains(")") -> { classroomName = classroomRegex.find(item)?.get(1) items[i] = item.replace("($classroomName)", "").trim() } - classroomName != null && item.contains(classroomName!!) -> { + classroomName != null && item.contains(classroomName) -> { items[i] = item.replace("($classroomName)", "").trim() } - item.contains("class=\"wyjatek tooltip\"") -> - toRemove.add(item) + item.contains("class=\"wyjatek tooltip\"") -> { + items.remove(item) + i-- + } } + // finally advance to the next item + i++ } - items.removeAll(toRemove) } if (items.size == 2 && items[0].contains(" - ")) { @@ -311,6 +332,7 @@ class MobidziennikWebTimetable( } it.id = it.buildId() + it.isExtra = isExtra val seen = profile?.empty == false || lessonDate < Date.getToday() 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 49b57999..73096f28 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 @@ -11,19 +11,19 @@ import pl.szczodrzynski.edziennik.data.db.dao.TimetableDao import pl.szczodrzynski.edziennik.utils.models.Date open class DataRemoveModel { - data class Timetable(private val dateFrom: Date?, private val dateTo: Date?) : DataRemoveModel() { + data class Timetable(private val dateFrom: Date?, private val dateTo: Date?, private val isExtra: Boolean?) : DataRemoveModel() { companion object { - fun from(dateFrom: Date) = Timetable(dateFrom, null) - fun to(dateTo: Date) = Timetable(null, dateTo) - fun between(dateFrom: Date, dateTo: Date) = Timetable(dateFrom, dateTo) + fun from(dateFrom: Date, isExtra: Boolean? = null) = Timetable(dateFrom, null, isExtra) + fun to(dateTo: Date, isExtra: Boolean? = null) = Timetable(null, dateTo, isExtra) + fun between(dateFrom: Date, dateTo: Date, isExtra: Boolean? = null) = Timetable(dateFrom, dateTo, isExtra) } fun commit(profileId: Int, dao: TimetableDao) { if (dateFrom != null && dateTo != null) { - dao.dontKeepBetweenDates(profileId, dateFrom, dateTo) + dao.dontKeepBetweenDates(profileId, dateFrom, dateTo, isExtra ?: false) } else { - dateFrom?.let { dateFrom -> dao.dontKeepFromDate(profileId, dateFrom) } - dateTo?.let { dateTo -> dao.dontKeepToDate(profileId, dateTo) } + dateFrom?.let { dateFrom -> dao.dontKeepFromDate(profileId, dateFrom, isExtra ?: false) } + dateTo?.let { dateTo -> dao.dontKeepToDate(profileId, dateTo, isExtra ?: false) } } } } 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 32b4b703..868a5aef 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 = 94) +], version = 95) @TypeConverters( ConverterTime::class, ConverterDate::class, @@ -179,7 +179,8 @@ abstract class AppDb : RoomDatabase() { Migration91(), Migration92(), Migration93(), - Migration94() + Migration94(), + Migration95(), ).allowMainThreadQueries().build() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TimetableDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TimetableDao.kt index 8ce9ed9b..4201a51d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TimetableDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TimetableDao.kt @@ -107,12 +107,12 @@ abstract class TimetableDao : BaseDao { fun getByIdNow(profileId: Int, id: Long) = getOneNow("$QUERY WHERE timetable.profileId = $profileId AND timetable.id = $id") - @Query("UPDATE timetable SET keep = 0 WHERE profileId = :profileId AND type != -1 AND ((type != 3 AND date >= :dateFrom) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom))") - abstract fun dontKeepFromDate(profileId: Int, dateFrom: Date) + @Query("UPDATE timetable SET keep = 0 WHERE profileId = :profileId AND isExtra = :isExtra AND type != -1 AND ((type != 3 AND date >= :dateFrom) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom))") + abstract fun dontKeepFromDate(profileId: Int, dateFrom: Date, isExtra: Boolean) - @Query("UPDATE timetable SET keep = 0 WHERE profileId = :profileId AND type != -1 AND ((type != 3 AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate <= :dateTo))") - abstract fun dontKeepToDate(profileId: Int, dateTo: Date) + @Query("UPDATE timetable SET keep = 0 WHERE profileId = :profileId AND isExtra = :isExtra AND type != -1 AND ((type != 3 AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate <= :dateTo))") + abstract fun dontKeepToDate(profileId: Int, dateTo: Date, isExtra: Boolean) - @Query("UPDATE timetable SET keep = 0 WHERE profileId = :profileId AND type != -1 AND ((type != 3 AND date >= :dateFrom AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom AND oldDate <= :dateTo))") - abstract fun dontKeepBetweenDates(profileId: Int, dateFrom: Date, dateTo: Date) + @Query("UPDATE timetable SET keep = 0 WHERE profileId = :profileId AND isExtra = :isExtra AND type != -1 AND ((type != 3 AND date >= :dateFrom AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom AND oldDate <= :dateTo))") + abstract fun dontKeepBetweenDates(profileId: Int, dateFrom: Date, dateTo: Date, isExtra: Boolean) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Lesson.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Lesson.kt index 0c97ba24..5123a2f5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Lesson.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Lesson.kt @@ -48,6 +48,8 @@ open class Lesson( var oldTeamId: Long? = null var oldClassroom: String? = null + var isExtra: Boolean = false + val displayDate: Date? get() { if (type == TYPE_SHIFTED_SOURCE) @@ -121,11 +123,13 @@ open class Lesson( return true } - override fun hashCode(): Int { // intentionally ignoring ID and display* here + override fun hashCode(): Int { // intentionally ignoring ID, display* and isExtra here var result = profileId result = 31 * result + type result = 31 * result + (date?.hashCode() ?: 0) - result = 31 * result + (lessonNumber ?: 0) + // this creates problems in Mobidziennik with extra lessons + // ... and is not generally useful anyway + // result = 31 * result + (lessonNumber ?: 0) result = 31 * result + (startTime?.hashCode() ?: 0) result = 31 * result + (endTime?.hashCode() ?: 0) result = 31 * result + (subjectId?.hashCode() ?: 0) @@ -133,7 +137,7 @@ open class Lesson( result = 31 * result + (teamId?.hashCode() ?: 0) result = 31 * result + (classroom?.hashCode() ?: 0) result = 31 * result + (oldDate?.hashCode() ?: 0) - result = 31 * result + (oldLessonNumber ?: 0) + // result = 31 * result + (oldLessonNumber ?: 0) result = 31 * result + (oldStartTime?.hashCode() ?: 0) result = 31 * result + (oldEndTime?.hashCode() ?: 0) result = 31 * result + (oldSubjectId?.hashCode() ?: 0) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration95.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration95.kt new file mode 100644 index 00000000..0281de6b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration95.kt @@ -0,0 +1,15 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-1. + */ + +package pl.szczodrzynski.edziennik.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration95 : Migration(94, 95) { + override fun migrate(database: SupportSQLiteDatabase) { + // timetable - is extra flag + database.execSQL("ALTER TABLE timetable ADD COLUMN isExtra INT NOT NULL DEFAULT 0;") + } +} From 692555732d0ee789286951028eae6ce827dcaa43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Tue, 5 Oct 2021 20:28:29 +0200 Subject: [PATCH 07/50] [Messages/Compose] Add text styling support. (#85) * [UI/Messages] Add draft text styling support. * [UI/Messages] Improve mid-word span styling. Restore subscript and superscript styles. * [UI/Messages] Replace framework spans with custom classes on replying. * [Messages/Compose] Move UI-related code to separate classes. * [UI/Messages] Disable text style buttons when not in focus. * [Messages/Compose] Disable text styling on Vulcan. * [UI/Messages] Add hint toasts to text style toggles. * [UI/Messages] Add button to clear text styling. * [Messages/Compose] Fix XML formatting. --- .../pl/szczodrzynski/edziennik/Extensions.kt | 38 ++ .../compose/MessagesComposeChipCreator.kt | 141 +++++++ .../compose/MessagesComposeChipTokenizer.kt | 25 ++ .../compose/MessagesComposeFragment.kt | 376 +++++++++--------- .../edziennik/utils/TextInputKeyboardEdit.kt | 16 +- .../edziennik/utils/html/BetterHtml.kt | 146 ++++++- .../edziennik/utils/span/BoldSpan.kt | 10 + .../{html => span}/ImprovedBulletSpan.kt | 10 +- .../edziennik/utils/span/ItalicSpan.kt | 10 + .../edziennik/utils/span/SubscriptSizeSpan.kt | 32 ++ .../utils/span/SuperscriptSizeSpan.kt | 32 ++ .../res/layout/messages_compose_fragment.xml | 155 ++++++-- app/src/main/res/values/strings.xml | 7 + 13 files changed, 744 insertions(+), 254 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeChipCreator.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeChipTokenizer.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/utils/span/BoldSpan.kt rename app/src/main/java/pl/szczodrzynski/edziennik/utils/{html => span}/ImprovedBulletSpan.kt (90%) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/utils/span/ItalicSpan.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/utils/span/SubscriptSizeSpan.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/utils/span/SuperscriptSizeSpan.kt diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt index 42eed408..8c03ded0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt @@ -1308,3 +1308,41 @@ fun Profile.getSchoolYearConstrains(): CalendarConstraints { .setEnd(dateYearEnd.inMillisUtc) .build() } + +fun CharSequence.getWordBounds(position: Int, onlyInWord: Boolean = false): Pair? { + if (length == 0) + return null + + // only if cursor between letters + if (onlyInWord) { + if (position < 1) + return null + if (position == length) + return null + + val charBefore = this[position - 1] + if (!charBefore.isLetterOrDigit()) + return null + val charAfter = this[position] + if (!charAfter.isLetterOrDigit()) + return null + } + + var rangeStart = substring(0 until position).indexOfLast { !it.isLetterOrDigit() } + if (rangeStart == -1) // no whitespace, set to first index + rangeStart = 0 + else // cut the leading whitespace + rangeStart += 1 + + var rangeEnd = substring(position).indexOfFirst { !it.isLetterOrDigit() } + if (rangeEnd == -1) // no whitespace, set to last index + rangeEnd = length + else // append the substring offset + rangeEnd += position + + if (!onlyInWord && rangeStart == rangeEnd) + return null + return rangeStart to rangeEnd +} + +infix fun Int.hasSet(what: Int) = this and what == what diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeChipCreator.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeChipCreator.kt new file mode 100644 index 00000000..649fd9ed --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeChipCreator.kt @@ -0,0 +1,141 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-4. + */ + +package pl.szczodrzynski.edziennik.ui.modules.messages.compose + +import android.content.Context +import android.graphics.drawable.BitmapDrawable +import android.text.style.AbsoluteSizeSpan +import android.text.style.ForegroundColorSpan +import android.widget.Toast +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.hootsuite.nachos.ChipConfiguration +import com.hootsuite.nachos.NachoTextView +import com.hootsuite.nachos.chip.ChipInfo +import com.hootsuite.nachos.chip.ChipSpan +import com.hootsuite.nachos.chip.ChipSpanChipCreator +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils +import pl.szczodrzynski.edziennik.utils.Colors +import pl.szczodrzynski.edziennik.utils.Themes +import pl.szczodrzynski.navlib.elevateSurface + +class MessagesComposeChipCreator( + private val context: Context, + private val nacho: NachoTextView, + private val teacherList: List, +) : ChipSpanChipCreator() { + + override fun createChip(context: Context, text: CharSequence, data: Any?): ChipSpan? { + if (data == null || data !is Teacher) + return null + if (data.id !in -24L..0L) { + nacho.allChips.forEach { + if (it.data == data) { + Toast.makeText( + context, + R.string.messages_compose_recipient_exists, + Toast.LENGTH_SHORT + ).show() + return null + } + } + val chipSpan = ChipSpan( + context, + data.fullName, + BitmapDrawable(context.resources, data.image), + data + ) + chipSpan.setIconBackgroundColor(Colors.stringToMaterialColor(data.fullName)) + return chipSpan + } + + val type = (data.id * -1).toInt() + + val textColorPrimary = android.R.attr.textColorPrimary.resolveAttr(context) + val textColorSecondary = android.R.attr.textColorSecondary.resolveAttr(context) + + val sortByCategory = type in listOf( + Teacher.TYPE_PARENTS_COUNCIL, + Teacher.TYPE_EDUCATOR, + Teacher.TYPE_STUDENT + ) + val teachers = if (sortByCategory) + teacherList.sortedBy { it.typeDescription } + else + teacherList + + val category = mutableListOf() + val categoryNames = mutableListOf() + val categoryCheckedItems = mutableListOf() + teachers.forEach { teacher -> + if (!teacher.isType(type)) + return@forEach + + category += teacher + val name = teacher.fullName + val description = when (type) { + Teacher.TYPE_TEACHER -> null + Teacher.TYPE_PARENTS_COUNCIL -> teacher.typeDescription + Teacher.TYPE_SCHOOL_PARENTS_COUNCIL -> null + Teacher.TYPE_PEDAGOGUE -> null + Teacher.TYPE_LIBRARIAN -> null + Teacher.TYPE_SCHOOL_ADMIN -> null + Teacher.TYPE_SUPER_ADMIN -> null + Teacher.TYPE_SECRETARIAT -> null + Teacher.TYPE_PRINCIPAL -> null + Teacher.TYPE_EDUCATOR -> teacher.typeDescription + Teacher.TYPE_PARENT -> teacher.typeDescription + Teacher.TYPE_STUDENT -> teacher.typeDescription + Teacher.TYPE_SPECIALIST -> null + else -> teacher.typeDescription + } + categoryNames += listOfNotNull( + name.asSpannable( + ForegroundColorSpan(textColorPrimary) + ), + description?.asSpannable( + ForegroundColorSpan(textColorSecondary), + AbsoluteSizeSpan(14.dp) + ) + ).concat("\n") + + // check the teacher if already added as a recipient + categoryCheckedItems += nacho.allChips.firstOrNull { it.data == teacher } != null + } + + MaterialAlertDialogBuilder(context) + .setTitle("Dodaj odbiorców - " + Teacher.typeName(context, type)) + //.setMessage(getString(R.string.messages_compose_recipients_text_format, Teacher.typeName(activity, type))) + .setPositiveButton("OK", null) + .setNeutralButton("Anuluj", null) + .setMultiChoiceItems( + categoryNames.toTypedArray(), + categoryCheckedItems.toBooleanArray() + ) { _, which, isChecked -> + val teacher = category[which] + if (isChecked) { + val chipInfoList = mutableListOf() + teacher.image = + MessagesUtils.getProfileImage(48, 24, 16, 12, 1, teacher.fullName) + chipInfoList.add(ChipInfo(teacher.fullName, teacher)) + nacho.addTextWithChips(chipInfoList) + } else { + nacho.allChips.forEach { + if (it.data == teacher) + nacho.chipTokenizer?.deleteChipAndPadding(it, nacho.text) + } + } + } + .show() + return null + } + + override fun configureChip(chip: ChipSpan, chipConfiguration: ChipConfiguration) { + super.configureChip(chip, chipConfiguration) + chip.setBackgroundColor(elevateSurface(context, 8).toColorStateList()) + chip.setTextColor(Themes.getPrimaryTextColor(context)) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeChipTokenizer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeChipTokenizer.kt new file mode 100644 index 00000000..560b0a06 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeChipTokenizer.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-4. + */ + +package pl.szczodrzynski.edziennik.ui.modules.messages.compose + +import android.content.Context +import com.hootsuite.nachos.NachoTextView +import com.hootsuite.nachos.chip.ChipSpan +import com.hootsuite.nachos.tokenizer.SpanChipTokenizer +import pl.szczodrzynski.edziennik.data.db.entity.Teacher + +class MessagesComposeChipTokenizer( + context: Context, + nacho: NachoTextView, + teacherList: List, +) : SpanChipTokenizer( + context, + MessagesComposeChipCreator( + context = context, + nacho = nacho, + teacherList = teacherList + ), + ChipSpan::class.java +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt index 33744a98..25599bb5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt @@ -5,30 +5,21 @@ package pl.szczodrzynski.edziennik.ui.modules.messages.compose import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Typeface -import android.graphics.drawable.BitmapDrawable import android.os.Bundle -import android.text.Spannable -import android.text.SpannableString -import android.text.SpannableStringBuilder -import android.text.style.AbsoluteSizeSpan -import android.text.style.ForegroundColorSpan -import android.text.style.StyleSpan +import android.text.* +import android.text.Spanned.* +import android.text.style.* import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.AutoCompleteTextView -import android.widget.Toast import androidx.core.text.HtmlCompat +import androidx.core.view.isVisible import androidx.core.widget.addTextChangedListener import androidx.fragment.app.Fragment +import com.google.android.material.button.MaterialButtonToggleGroup import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.hootsuite.nachos.ChipConfiguration import com.hootsuite.nachos.chip.ChipInfo -import com.hootsuite.nachos.chip.ChipSpan -import com.hootsuite.nachos.chip.ChipSpanChipCreator -import com.hootsuite.nachos.tokenizer.SpanChipTokenizer import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import kotlinx.coroutines.* import org.greenrobot.eventbus.EventBus @@ -47,13 +38,15 @@ import pl.szczodrzynski.edziennik.databinding.MessagesComposeFragmentBinding import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog 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 -import pl.szczodrzynski.edziennik.utils.Themes.getPrimaryTextColor +import pl.szczodrzynski.edziennik.utils.html.BetterHtml import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time +import pl.szczodrzynski.edziennik.utils.span.BoldSpan +import pl.szczodrzynski.edziennik.utils.span.ItalicSpan +import pl.szczodrzynski.edziennik.utils.span.SubscriptSizeSpan +import pl.szczodrzynski.edziennik.utils.span.SuperscriptSizeSpan import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem -import pl.szczodrzynski.navlib.elevateSurface import kotlin.coroutines.CoroutineContext import kotlin.text.replace @@ -76,11 +69,16 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { private var teachers = mutableListOf() + private var watchFormatChecked = true + private var watchSelectionChanged = true + private val enableTextStyling + get() = app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN + 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) + requireContext().theme.applyStyle(Themes.appTheme, true) // activity, context and profile is valid b = MessagesComposeFragmentBinding.inflate(inflater) return b.root @@ -103,40 +101,12 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { // do your job } - /*b.fontStyleBold.icon = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_format_bold) - .sizeDp(24) - .colorAttr(activity, R.attr.colorOnPrimary) - b.fontStyleItalic.icon = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_format_italic) - .sizeDp(24) - .colorAttr(activity, R.attr.colorOnPrimary) - b.fontStyleUnderline.icon = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_format_underline) - .sizeDp(24) - .colorAttr(activity, R.attr.colorOnPrimary) - - b.fontStyle.addOnButtonCheckedListener { _, checkedId, isChecked -> - val span: Any = when (checkedId) { - R.id.fontStyleBold -> StyleSpan(Typeface.BOLD) - R.id.fontStyleItalic -> StyleSpan(Typeface.ITALIC) - R.id.fontStyleUnderline -> UnderlineSpan() - else -> StyleSpan(Typeface.NORMAL) + if (App.devMode) { + b.textHtml.isVisible = true + b.text.addTextChangedListener { + b.textHtml.text = getHtmlText() } - - if (isChecked) { - val flags = if (b.text.selectionStart == b.text.selectionEnd) - SpannableString.SPAN_INCLUSIVE_INCLUSIVE - else - SpannableString.SPAN_EXCLUSIVE_INCLUSIVE - b.text.text?.setSpan(span, b.text.selectionStart, b.text.selectionEnd, flags) - } - else { - b.text.text?.getSpans(b.text.selectionStart, b.text.selectionEnd, span.javaClass)?.forEach { - if (it is StyleSpan && span is StyleSpan && it.style == span.style) - b.text.text?.removeSpan(it) - else if (it.javaClass == span.javaClass) - b.text.text?.removeSpan(it) - } - } - }*/ + } activity.bottomSheet.prependItem( BottomSheetPrimaryItem(true) @@ -156,6 +126,50 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { } } + private fun getHtmlText(): String { + val text = b.text.text ?: return "" + + // apparently setting the spans to a different Spannable calls the original EditText's + // onSelectionChanged with selectionStart=-1, which in effect unchecks the format toggles + watchSelectionChanged = false + var textHtml = if (enableTextStyling) { + val spanned = SpannableString(text) + // remove zero-length spans, as they seem to affect + // the whole line when converted to HTML + spanned.getSpans(0, spanned.length, Any::class.java).forEach { + val spanStart = spanned.getSpanStart(it) + val spanEnd = spanned.getSpanEnd(it) + if (spanStart == spanEnd && it::class.java in BetterHtml.customSpanClasses) + spanned.removeSpan(it) + } + HtmlCompat.toHtml(spanned, HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL) + .replace("\n", "") + .replace(" dir=\"ltr\"", "") + .replace("", "") + .replace("", "") + .replace("", "") + .replace("", "") + .replace("", "") + .replace("p style=\"margin-top:0; margin-bottom:0;\"", "p") + } else { + text.toString() + } + watchSelectionChanged = true + + if (app.profile.loginStoreType == LoginStore.LOGIN_TYPE_MOBIDZIENNIK) { + textHtml = textHtml + .replace("
", "

 

") + .replace("", "") + .replace("", "") + .replace("", "") + .replace("", "") + .replace("", "") + .replace("", "") + } + + return textHtml + } + private fun getRecipientList() { if (System.currentTimeMillis() - app.profile.lastReceiversSync > 1 * DAY * 1000 && app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN) { activity.snackbar("Pobieranie listy odbiorców...") @@ -171,6 +185,80 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { } } + @Suppress("UNUSED_PARAMETER") + private fun onFormatChecked( + group: MaterialButtonToggleGroup, + checkedId: Int, + isChecked: Boolean + ) { + if (!watchFormatChecked) + return + val span = when (checkedId) { + R.id.fontStyleBold -> BoldSpan() + R.id.fontStyleItalic -> ItalicSpan() + R.id.fontStyleUnderline -> UnderlineSpan() + R.id.fontStyleStrike -> StrikethroughSpan() + R.id.fontStyleSubscript -> SubscriptSizeSpan(10, dip = true) + R.id.fontStyleSuperscript -> SuperscriptSizeSpan(10, dip = true) + else -> return + } + + // see comments in getHtmlText() + watchSelectionChanged = false + if (isChecked) + BetterHtml.applyFormat(span = span, editText = b.text) + else + BetterHtml.removeFormat(span = span, editText = b.text) + watchSelectionChanged = true + + if (App.devMode) + b.textHtml.text = getHtmlText() + } + + @Suppress("UNUSED_PARAMETER") + private fun onFormatClear(view: View) { + // shortened version on onFormatChecked(), removing all spans + watchSelectionChanged = false + BetterHtml.removeFormat(span = null, editText = b.text) + watchSelectionChanged = true + if (App.devMode) + b.textHtml.text = getHtmlText() + // force update of text style toggle states + onSelectionChanged(b.text.selectionStart, b.text.selectionEnd) + } + + private fun onSelectionChanged(selectionStart: Int, selectionEnd: Int) { + if (!watchSelectionChanged) + return + val spanned = b.text.text ?: return + val spans = spanned.getSpans(selectionStart, selectionEnd, Any::class.java).mapNotNull { + if (it::class.java !in BetterHtml.customSpanClasses) + return@mapNotNull null + val spanStart = spanned.getSpanStart(it) + val spanEnd = spanned.getSpanEnd(it) + // remove 0-length spans after navigating out of them + if (spanStart == spanEnd) + spanned.removeSpan(it) + else if (spanned.getSpanFlags(it) hasSet SPAN_EXCLUSIVE_EXCLUSIVE) + spanned.setSpan(it, spanStart, spanEnd, SPAN_EXCLUSIVE_INCLUSIVE) + + // names are helpful here + val isNotAfterWord = selectionEnd <= spanEnd + val isSelectionInWord = selectionStart != selectionEnd && selectionStart >= spanStart + val isCursorInWord = selectionStart == selectionEnd && selectionStart > spanStart + val isChecked = (isCursorInWord || isSelectionInWord) && isNotAfterWord + if (isChecked) it::class.java else null + } + watchFormatChecked = false + b.fontStyleBold.isChecked = BoldSpan::class.java in spans + b.fontStyleItalic.isChecked = ItalicSpan::class.java in spans + b.fontStyleUnderline.isChecked = UnderlineSpan::class.java in spans + b.fontStyleStrike.isChecked = StrikethroughSpan::class.java in spans + b.fontStyleSubscript.isChecked = SubscriptSizeSpan::class.java in spans + b.fontStyleSuperscript.isChecked = SuperscriptSizeSpan::class.java in spans + watchFormatChecked = true + } + private fun createView() { b.recipientsLayout.setBoxCornerRadii(0f, 0f, 0f, 0f) b.subjectLayout.setBoxCornerRadii(0f, 0f, 0f, 0f) @@ -203,107 +291,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { else -> -1 } - b.recipients.chipTokenizer = SpanChipTokenizer(activity, object : ChipSpanChipCreator() { - override fun createChip(context: Context, text: CharSequence, data: Any?): ChipSpan? { - if (data == null || data !is Teacher) - return null - if (data.id !in -24L..0L) { - b.recipients.allChips.forEach { - if (it.data == data) { - Toast.makeText(activity, R.string.messages_compose_recipient_exists, Toast.LENGTH_SHORT).show() - return null - } - } - val chipSpan = ChipSpan(context, data.fullName, BitmapDrawable(context.resources, data.image), data) - chipSpan.setIconBackgroundColor(Colors.stringToMaterialColor(data.fullName)) - return chipSpan - } - - val type = (data.id * -1).toInt() - - val textColorPrimary = android.R.attr.textColorPrimary.resolveAttr(activity) - val textColorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity) - - val sortByCategory = type in listOf( - Teacher.TYPE_PARENTS_COUNCIL, - Teacher.TYPE_EDUCATOR, - Teacher.TYPE_STUDENT - ) - val teachers = if (sortByCategory) - teachers.sortedBy { it.typeDescription } - else - teachers - - val category = mutableListOf() - val categoryNames = mutableListOf() - val categoryCheckedItems = mutableListOf() - teachers.forEach { teacher -> - if (!teacher.isType(type)) - return@forEach - - category += teacher - val name = teacher.fullName - val description = when (type) { - Teacher.TYPE_TEACHER -> null - Teacher.TYPE_PARENTS_COUNCIL -> teacher.typeDescription - Teacher.TYPE_SCHOOL_PARENTS_COUNCIL -> null - Teacher.TYPE_PEDAGOGUE -> null - Teacher.TYPE_LIBRARIAN -> null - Teacher.TYPE_SCHOOL_ADMIN -> null - Teacher.TYPE_SUPER_ADMIN -> null - Teacher.TYPE_SECRETARIAT -> null - Teacher.TYPE_PRINCIPAL -> null - Teacher.TYPE_EDUCATOR -> teacher.typeDescription - Teacher.TYPE_PARENT -> teacher.typeDescription - Teacher.TYPE_STUDENT -> teacher.typeDescription - Teacher.TYPE_SPECIALIST -> null - else -> teacher.typeDescription - } - categoryNames += listOfNotNull( - name.asSpannable( - ForegroundColorSpan(textColorPrimary) - ), - description?.asSpannable( - ForegroundColorSpan(textColorSecondary), - AbsoluteSizeSpan(14.dp) - ) - ).concat("\n") - - // check the teacher if already added as a recipient - categoryCheckedItems += b.recipients.allChips.firstOrNull { it.data == teacher } != null - } - - MaterialAlertDialogBuilder(activity) - .setTitle("Dodaj odbiorców - "+ Teacher.typeName(activity, type)) - //.setMessage(getString(R.string.messages_compose_recipients_text_format, Teacher.typeName(activity, type))) - .setPositiveButton("OK", null) - .setNeutralButton("Anuluj", null) - .setMultiChoiceItems(categoryNames.toTypedArray(), categoryCheckedItems.toBooleanArray()) { _, which, isChecked -> - val teacher = category[which] - if (isChecked) { - val chipInfoList = mutableListOf() - teacher.image = getProfileImage(48, 24, 16, 12, 1, teacher.fullName) - chipInfoList.add(ChipInfo(teacher.fullName, teacher)) - b.recipients.addTextWithChips(chipInfoList) - } - else { - b.recipients.allChips.forEach { - if (it.data == teacher) - b.recipients.chipTokenizer?.deleteChipAndPadding(it, b.recipients.text) - } - } - } - .show() - return null - } - - override fun configureChip(chip: ChipSpan, chipConfiguration: ChipConfiguration) { - super.configureChip(chip, chipConfiguration) - chip.setBackgroundColor(elevateSurface(activity, 8).toColorStateList()) - chip.setTextColor(getPrimaryTextColor(activity)) - } - }, ChipSpan::class.java) - + b.recipients.chipTokenizer = MessagesComposeChipTokenizer(activity, b.recipients, teachers) b.recipients.setIllegalCharacterIdentifier { c -> c.toString().matches("[\\n;:_ ]".toRegex()) } @@ -330,6 +318,55 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { b.subjectLayout.isEnabled = false b.textLayout.isEnabled = false + b.fontStyleLayout.isVisible = enableTextStyling + b.fontStyleBold.isEnabled = false + b.fontStyleItalic.isEnabled = false + b.fontStyleUnderline.isEnabled = false + b.fontStyleStrike.isEnabled = false + b.fontStyleSubscript.isEnabled = false + b.fontStyleSuperscript.isEnabled = false + b.fontStyleClear.isEnabled = false + b.text.setOnFocusChangeListener { _, hasFocus -> + b.fontStyleBold.isEnabled = hasFocus + b.fontStyleItalic.isEnabled = hasFocus + b.fontStyleUnderline.isEnabled = hasFocus + b.fontStyleStrike.isEnabled = hasFocus + b.fontStyleSubscript.isEnabled = hasFocus + b.fontStyleSuperscript.isEnabled = hasFocus + b.fontStyleClear.isEnabled = hasFocus + } + + b.fontStyleBold.text = CommunityMaterial.Icon2.cmd_format_bold.character.toString() + b.fontStyleItalic.text = CommunityMaterial.Icon2.cmd_format_italic.character.toString() + b.fontStyleUnderline.text = CommunityMaterial.Icon2.cmd_format_underline.character.toString() + b.fontStyleStrike.text = CommunityMaterial.Icon2.cmd_format_strikethrough.character.toString() + b.fontStyleSubscript.text = CommunityMaterial.Icon2.cmd_format_subscript.character.toString() + b.fontStyleSuperscript.text = CommunityMaterial.Icon2.cmd_format_superscript.character.toString() + b.fontStyleBold.attachToastHint(R.string.hint_style_bold) + b.fontStyleItalic.attachToastHint(R.string.hint_style_italic) + b.fontStyleUnderline.attachToastHint(R.string.hint_style_underline) + b.fontStyleStrike.attachToastHint(R.string.hint_style_strike) + b.fontStyleSubscript.attachToastHint(R.string.hint_style_subscript) + b.fontStyleSuperscript.attachToastHint(R.string.hint_style_superscript) + + /*b.fontStyleBold.shapeAppearanceModel = b.fontStyleBold.shapeAppearanceModel + .toBuilder() + .setBottomLeftCornerSize(0f) + .build() + b.fontStyleSuperscript.shapeAppearanceModel = b.fontStyleBold.shapeAppearanceModel + .toBuilder() + .setBottomRightCornerSize(0f) + .build() + b.fontStyleClear.shapeAppearanceModel = b.fontStyleClear.shapeAppearanceModel + .toBuilder() + .setTopLeftCornerSize(0f) + .setTopRightCornerSize(0f) + .build()*/ + + b.fontStyle.addOnButtonCheckedListener(this::onFormatChecked) + b.fontStyleClear.setOnClickListener(this::onFormatClear) + b.text.setSelectionChangedListener(this::onSelectionChanged) + activity.navView.bottomBar.apply { fabEnable = true fabExtendedText = getString(R.string.messages_compose_send) @@ -382,11 +419,11 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { val dateString = getString(R.string.messages_date_time_format, Date.fromMillis(msg.addedDate).formattedStringShort, Time.fromMillis(msg.addedDate).stringHM) // add original message info span.appendText("W dniu ") - span.appendSpan(dateString, StyleSpan(Typeface.ITALIC), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + span.appendSpan(dateString, ItalicSpan(), SPAN_EXCLUSIVE_EXCLUSIVE) span.appendText(", ") - span.appendSpan(msg.senderName.fixName(), StyleSpan(Typeface.ITALIC), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + span.appendSpan(msg.senderName.fixName(), ItalicSpan(), SPAN_EXCLUSIVE_EXCLUSIVE) span.appendText(" napisał(a):") - span.setSpan(StyleSpan(Typeface.BOLD), 0, span.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + span.setSpan(BoldSpan(), 0, span.length, SPAN_EXCLUSIVE_EXCLUSIVE) span.appendText("\n\n") if (arguments?.getString("type") == "reply") { @@ -422,6 +459,8 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { b.subject.setText(subject) b.text.apply { text = span.appendText(body) + if (!enableTextStyling) + setText(text?.toString()) setSelection(0) } b.root.scrollTo(0, 0) @@ -494,30 +533,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { if (b.textLayout.counterMaxLength != -1 && b.text.length() > b.textLayout.counterMaxLength) return - var textHtml = if (app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN) { - HtmlCompat.toHtml(SpannableString(text), HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL) - .replace("\n", "") - .replace(" dir=\"ltr\"", "") - } - else { - text.toString() - } - - textHtml = textHtml - .replace("
", "") - .replace("", "") - .replace("p style=\"margin-top:0; margin-bottom:0;\"", "p") - - if (app.profile.loginStoreType == LoginStore.LOGIN_TYPE_MOBIDZIENNIK) { - textHtml = textHtml - .replace("


", "

") - .replace("", "") - .replace("", "") - .replace("", "") - .replace("", "") - .replace("", "") - .replace("", "") - } + val textHtml = getHtmlText() activity.bottomSheet.hideKeyboard() @@ -525,7 +541,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { .setTitle(R.string.messages_compose_confirm_title) .setMessage(R.string.messages_compose_confirm_text) .setPositiveButton(R.string.send) { _, _ -> - EdziennikTask.messageSend(App.profileId, recipients, subject.trim(), textHtml.trim()).enqueue(activity) + EdziennikTask.messageSend(App.profileId, recipients, subject.trim(), textHtml).enqueue(activity) } .setNegativeButton(R.string.cancel, null) .show() 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 cbb4f5a0..0b5e1c67 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputKeyboardEdit.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputKeyboardEdit.kt @@ -18,6 +18,7 @@ class TextInputKeyboardEdit : AppCompatEditText { * Keyboard Listener */ internal var listener: KeyboardListener? = null + private var selectionListener: ((Int, Int) -> Unit)? = null constructor(context: Context) : super(context) @@ -27,14 +28,12 @@ class TextInputKeyboardEdit : AppCompatEditText { override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) { super.onFocusChanged(focused, direction, previouslyFocusedRect) - if (listener != null) - listener!!.onStateChanged(this, true) + listener?.onStateChanged(this, true) } override fun onKeyPreIme(keyCode: Int, @NonNull event: KeyEvent): Boolean { if (event.keyCode == KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) { - if (listener != null) - listener!!.onStateChanged(this, false) + listener?.onStateChanged(this, false) // Hide cursor isFocusable = false @@ -50,6 +49,15 @@ class TextInputKeyboardEdit : AppCompatEditText { this.listener = listener } + fun setSelectionChangedListener(listener: ((selectionStart: Int, selectionEnd: Int) -> Unit)?) { + this.selectionListener = listener + } + + override fun onSelectionChanged(selStart: Int, selEnd: Int) { + super.onSelectionChanged(selStart, selEnd) + selectionListener?.invoke(selStart, selEnd) + } + interface KeyboardListener { fun onStateChanged(keyboardEditText: TextInputKeyboardEdit, showing: Boolean) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/BetterHtml.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/BetterHtml.kt index 4119369e..90119e55 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/BetterHtml.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/BetterHtml.kt @@ -6,26 +6,41 @@ package pl.szczodrzynski.edziennik.utils.html import android.content.Context import android.graphics.Color +import android.graphics.Typeface +import android.text.Editable import android.text.SpannableStringBuilder import android.text.Spanned -import android.text.style.BulletSpan +import android.text.Spanned.* +import android.text.style.* +import androidx.appcompat.widget.AppCompatEditText import androidx.core.graphics.ColorUtils import androidx.core.text.HtmlCompat import pl.szczodrzynski.edziennik.dp +import pl.szczodrzynski.edziennik.getWordBounds import pl.szczodrzynski.edziennik.resolveAttr +import pl.szczodrzynski.edziennik.utils.span.* import pl.szczodrzynski.navlib.blendColors object BetterHtml { + val customSpanClasses = listOf( + BoldSpan::class.java, + ItalicSpan::class.java, + UnderlineSpan::class.java, + StrikethroughSpan::class.java, + SubscriptSizeSpan::class.java, + SuperscriptSizeSpan::class.java, + ) + @JvmStatic fun fromHtml(context: Context, html: String): Spanned { val hexPattern = "(#[a-fA-F0-9]{6})" val colorRegex = "(?:color=\"$hexPattern\")|(?:style=\"color: ?${hexPattern})" - .toRegex(RegexOption.IGNORE_CASE) + .toRegex(RegexOption.IGNORE_CASE) var text = html - .replace("\\[META:[A-z0-9]+;[0-9-]+]".toRegex(), "") - .replace("background-color: ?$hexPattern;".toRegex(), "") + .replace("\\[META:[A-z0-9]+;[0-9-]+]".toRegex(), "") + .replace("background-color: ?$hexPattern;".toRegex(), "") val colorBackground = android.R.attr.colorBackground.resolveAttr(context) val textColorPrimary = android.R.attr.textColorPrimary.resolveAttr(context) and 0xffffff @@ -39,7 +54,11 @@ object BetterHtml { var blendAmount = 1 var numIterations = 0 - while (numIterations < 100 && ColorUtils.calculateContrast(colorBackground, newColor) < 4.5f) { + while (numIterations < 100 && ColorUtils.calculateContrast( + colorBackground, + newColor + ) < 4.5f + ) { blendAmount += 2 newColor = blendColors(color, blendAmount shl 24 or textColorPrimary) numIterations++ @@ -59,26 +78,109 @@ object BetterHtml { @Suppress("DEPRECATION") val htmlSpannable = HtmlCompat.fromHtml( - text, - HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM or HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST or HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_DIV, - null, - LiTagHandler() + text, + HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM or HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST or HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_DIV, + null, + LiTagHandler() ) - val spannableBuilder = SpannableStringBuilder(htmlSpannable) - val bulletSpans = spannableBuilder.getSpans(0, spannableBuilder.length, BulletSpan::class.java) - bulletSpans.forEach { - val start = spannableBuilder.getSpanStart(it) - val end = spannableBuilder.getSpanEnd(it) - spannableBuilder.removeSpan(it) - spannableBuilder.setSpan( - ImprovedBulletSpan(bulletRadius = 3.dp, startWidth = 24.dp, gapWidth = 8.dp), - start, - end, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE - ) + val spanned = SpannableStringBuilder(htmlSpannable) + spanned.getSpans(0, spanned.length, Any::class.java).forEach { + val spanStart = spanned.getSpanStart(it) + val spanEnd = spanned.getSpanEnd(it) + val spanFlags = spanned.getSpanFlags(it) + + val newSpan: Any? = when (it) { + is BulletSpan -> ImprovedBulletSpan( + bulletRadius = 3.dp, + startWidth = 24.dp, + gapWidth = 8.dp + ) + is StyleSpan -> when (it.style) { + Typeface.BOLD -> BoldSpan() + Typeface.ITALIC -> ItalicSpan() + else -> null + } + is SubscriptSpan -> SubscriptSizeSpan(size = 10, dip = true) + is SuperscriptSpan -> SuperscriptSizeSpan(size = 10, dip = true) + else -> null + } + + if (newSpan != null) { + spanned.removeSpan(it) + spanned.setSpan(newSpan, spanStart, spanEnd, spanFlags) + } } - return spannableBuilder + return spanned + } + + fun applyFormat(span: Any, editText: AppCompatEditText) { + applyFormat(span, editText.text ?: return, editText.selectionStart, editText.selectionEnd) + } + + fun removeFormat(span: Any?, editText: AppCompatEditText) { + removeFormat(span, editText.text ?: return, editText.selectionStart, editText.selectionEnd) + } + + fun applyFormat(span: Any, spanned: Editable, selectionStart: Int, selectionEnd: Int) { + if (selectionStart == -1 || selectionEnd == -1) return + val cursorOnly = selectionStart == selectionEnd + + val wordBounds = spanned.getWordBounds(selectionStart, onlyInWord = true) + if (cursorOnly && wordBounds != null) { + // use the detected word bounds instead of cursor/selection + val (start, end) = wordBounds + spanned.setSpan(span, start, end, SPAN_EXCLUSIVE_INCLUSIVE) + } else { + val spanFlags = if (cursorOnly) + SPAN_INCLUSIVE_INCLUSIVE + else + SPAN_EXCLUSIVE_INCLUSIVE + spanned.setSpan(span, selectionStart, selectionEnd, spanFlags) + } + } + + fun removeFormat(span: Any?, spanned: Editable, selectionStart: Int, selectionEnd: Int) { + if (selectionStart == -1 || selectionEnd == -1) return + val cursorOnly = selectionStart == selectionEnd + + val spanClass = span?.javaClass ?: Any::class.java + spanned.getSpans(selectionStart, selectionEnd, spanClass).forEach { + if (span == null && it::class.java !in customSpanClasses) + return@forEach + val spanStart = spanned.getSpanStart(it) + val spanEnd = spanned.getSpanEnd(it) + val wordBounds = spanned.getWordBounds(selectionStart, onlyInWord = true) + + val (newSpanStart, newSpanEnd, newSpanFlags) = when { + !cursorOnly -> { + // cut the selected range out of the span + Triple(selectionStart, selectionEnd, SPAN_EXCLUSIVE_INCLUSIVE) + } + wordBounds == null -> { + // this allows to change spans mid-word - EXCLUSIVE so the style does + // not apply to characters typed later + // it's set back to INCLUSIVE when the cursor enters the word again + // (onSelectionChanged) + Triple(selectionStart, selectionEnd, SPAN_EXCLUSIVE_EXCLUSIVE) + } + else /* wordBounds != null */ -> { + // a word is selected, slice the span in two + Triple(wordBounds.first, wordBounds.second, SPAN_EXCLUSIVE_INCLUSIVE) + } + } + + // remove the existing span + spanned.removeSpan(it) + // "clone" the span so it can be applied twice, if needed + // (can't use 'span' from the parameters as it's nullable) + val itClone = it::class.java.newInstance() + // reapply the span wherever needed + if (spanStart < newSpanStart) + spanned.setSpan(it, spanStart, newSpanStart, newSpanFlags) + if (spanEnd > newSpanEnd) + spanned.setSpan(itClone, newSpanEnd, spanEnd, newSpanFlags) + } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/span/BoldSpan.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/span/BoldSpan.kt new file mode 100644 index 00000000..1dd7fb7a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/span/BoldSpan.kt @@ -0,0 +1,10 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-3. + */ + +package pl.szczodrzynski.edziennik.utils.span + +import android.graphics.Typeface +import android.text.style.StyleSpan + +class BoldSpan : StyleSpan(Typeface.BOLD) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/ImprovedBulletSpan.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/span/ImprovedBulletSpan.kt similarity index 90% rename from app/src/main/java/pl/szczodrzynski/edziennik/utils/html/ImprovedBulletSpan.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/utils/span/ImprovedBulletSpan.kt index 96dcbdfa..6bdfbe42 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/ImprovedBulletSpan.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/span/ImprovedBulletSpan.kt @@ -6,7 +6,7 @@ * https://github.com/davidbilik/bullet-span-sample/blob/master/app/src/main/java/cz/davidbilik/bulletsample/ImprovedBulletSpan.kt */ -package pl.szczodrzynski.edziennik.utils.html +package pl.szczodrzynski.edziennik.utils.span import android.graphics.Canvas import android.graphics.Paint @@ -20,10 +20,10 @@ import android.text.style.LeadingMarginSpan * Copy of [android.text.style.BulletSpan] from android SDK 28 with removed internal code */ class ImprovedBulletSpan( - val bulletRadius: Int = STANDARD_BULLET_RADIUS, - val startWidth: Int = STANDARD_GAP_WIDTH, - val gapWidth: Int = STANDARD_GAP_WIDTH, - val color: Int = STANDARD_COLOR + val bulletRadius: Int = STANDARD_BULLET_RADIUS, + val startWidth: Int = STANDARD_GAP_WIDTH, + val gapWidth: Int = STANDARD_GAP_WIDTH, + val color: Int = STANDARD_COLOR ) : LeadingMarginSpan { companion object { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/span/ItalicSpan.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/span/ItalicSpan.kt new file mode 100644 index 00000000..4ae7650d --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/span/ItalicSpan.kt @@ -0,0 +1,10 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-3. + */ + +package pl.szczodrzynski.edziennik.utils.span + +import android.graphics.Typeface +import android.text.style.StyleSpan + +class ItalicSpan : StyleSpan(Typeface.ITALIC) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/span/SubscriptSizeSpan.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/span/SubscriptSizeSpan.kt new file mode 100644 index 00000000..97fd9f2a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/span/SubscriptSizeSpan.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-3. + */ + +package pl.szczodrzynski.edziennik.utils.span + +import android.text.TextPaint +import android.text.style.SubscriptSpan + +class SubscriptSizeSpan( + private val size: Int, + private val dip: Boolean, +) : SubscriptSpan() { + + override fun updateDrawState(textPaint: TextPaint) { + super.updateDrawState(textPaint) + if (dip) { + textPaint.textSize = size * textPaint.density + } else { + textPaint.textSize = size.toFloat() + } + } + + override fun updateMeasureState(textPaint: TextPaint) { + super.updateMeasureState(textPaint) + if (dip) { + textPaint.textSize = size * textPaint.density + } else { + textPaint.textSize = size.toFloat() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/span/SuperscriptSizeSpan.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/span/SuperscriptSizeSpan.kt new file mode 100644 index 00000000..7b82ed51 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/span/SuperscriptSizeSpan.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-3. + */ + +package pl.szczodrzynski.edziennik.utils.span + +import android.text.TextPaint +import android.text.style.SuperscriptSpan + +class SuperscriptSizeSpan( + private val size: Int, + private val dip: Boolean, +) : SuperscriptSpan() { + + override fun updateDrawState(textPaint: TextPaint) { + super.updateDrawState(textPaint) + if (dip) { + textPaint.textSize = size * textPaint.density + } else { + textPaint.textSize = size.toFloat() + } + } + + override fun updateMeasureState(textPaint: TextPaint) { + super.updateMeasureState(textPaint) + if (dip) { + textPaint.textSize = size * textPaint.density + } else { + textPaint.textSize = size.toFloat() + } + } +} diff --git a/app/src/main/res/layout/messages_compose_fragment.xml b/app/src/main/res/layout/messages_compose_fragment.xml index 9363a92f..d9d24613 100644 --- a/app/src/main/res/layout/messages_compose_fragment.xml +++ b/app/src/main/res/layout/messages_compose_fragment.xml @@ -6,15 +6,21 @@ + + + + + + + - + android:orientation="vertical" + android:paddingBottom="40dp"> + app:endIconDrawable="@drawable/dropdown_arrow" + app:endIconMode="custom"> - - + tools:text="kachoomba" /> + + android:hint="@string/messages_compose_text_hint" + android:inputType="textMultiLine|textAutoCorrect|textLongMessage|textAutoComplete|textCapSentences" + android:minLines="3" + tools:text="Witam,\n\nchciałem przekazać bardzo ważną wiadomość.\nJest to cytat znanej osoby.\n\n"To jest tak, ale nie. Pamiętaj żeby oczywiście"\n\nCytat ma bardzo duże przesłanie i ogromny wpływ na dzisiejszą kulturę i rozwój współczesnej cywilizacji.\n\nJako zadanie domowe, należy wypisać 5 pierwszych liczb pierwszych. Uzasadnij decyzję, odwołując się do cytatu i 3 innych przykładów z literatury lub filmu.\n\nPozdrawiam,\nJa." /> - + - - - - --> + android:layout_marginHorizontal="8dp" + android:text="@string/messages_compose_style_clear" /> +