diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 4d84d11d..0816fb00 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -32,8 +32,9 @@ -keepnames class pl.szczodrzynski.edziennik.ui.widgets.luckynumber.WidgetLuckyNumberProvider -keepnames class androidx.appcompat.view.menu.MenuBuilder { setHeaderTitleInt(java.lang.CharSequence); } --keepclassmembernames class androidx.appcompat.view.menu.StandardMenuPopup { private *; } -keepnames class androidx.appcompat.view.menu.MenuPopupHelper { showPopup(int, int, boolean, boolean); } +-keepclassmembernames class androidx.appcompat.view.menu.StandardMenuPopup { private *; } +-keepclassmembernames class androidx.appcompat.view.menu.MenuItemImpl { private *; } -keepclassmembernames class com.mikepenz.materialdrawer.widget.MiniDrawerSliderView { private *; } 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 b41da04b..cdd2e50a 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 @@ -199,10 +199,14 @@ class EventDetailsDialog( } b.downloadButton.attachToastHint(R.string.hint_download_again) + BetterLink.attach(b.topic, onActionSelected = dialog::dismiss) - b.topic.text = event.topic - BetterLink.attach(b.topic) { - dialog.dismiss() + event.teacherName?.let { name -> + BetterLink.attach( + b.teacherName, + teachers = mapOf(event.teacherId to name), + onActionSelected = dialog::dismiss + ) } if (event.homeworkBody == null && !event.addedManually && event.type == Event.TYPE_HOMEWORK) { @@ -220,10 +224,7 @@ class EventDetailsDialog( b.bodyTitle.isVisible = true b.bodyProgressBar.isVisible = false b.body.isVisible = true - b.body.text = event.homeworkBody - BetterLink.attach(b.body) { - dialog.dismiss() - } + BetterLink.attach(b.body, onActionSelected = dialog::dismiss) } if (event.attachmentIds.isNullOrEmpty() || event.attachmentNames.isNullOrEmpty()) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/grade/GradeDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/grade/GradeDetailsDialog.kt index 5b9579ed..a42f9e2f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/grade/GradeDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/grade/GradeDetailsDialog.kt @@ -14,6 +14,7 @@ import pl.szczodrzynski.edziennik.databinding.DialogGradeDetailsBinding import pl.szczodrzynski.edziennik.onClick import pl.szczodrzynski.edziennik.setTintColor import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter +import pl.szczodrzynski.edziennik.utils.BetterLink import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import kotlin.coroutines.CoroutineContext @@ -68,6 +69,14 @@ class GradeDetailsDialog( GradesConfigDialog(activity, reloadOnDismiss = true) } + grade.teacherName?.let { name -> + BetterLink.attach( + b.teacherName, + teachers = mapOf(grade.teacherId to name), + onActionSelected = dialog::dismiss + ) + } + launch { val historyList = withContext(Dispatchers.Default) { app.db.gradeDao().getByParentIdNow(App.profileId, grade.id) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt index 5896def0..f444f38e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt @@ -25,6 +25,7 @@ import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment +import pl.szczodrzynski.edziennik.utils.BetterLink import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Week @@ -216,5 +217,19 @@ class LessonDetailsDialog( b.eventsNoData.visibility = View.VISIBLE } }) + + lesson.displayTeacherName?.let { name -> + lesson.displayTeacherId ?: return@let + BetterLink.attach( + b.teacherNameView, + teachers = mapOf(lesson.displayTeacherId!! to name), + onActionSelected = dialog::dismiss + ) + BetterLink.attach( + b.oldTeacherNameView, + teachers = mapOf(lesson.displayTeacherId!! to name), + onActionSelected = dialog::dismiss + ) + } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceDetailsDialog.kt index 9e6c14d6..5ee6bdc3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceDetailsDialog.kt @@ -16,6 +16,7 @@ import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull import pl.szczodrzynski.edziennik.databinding.AttendanceDetailsDialogBinding import pl.szczodrzynski.edziennik.setTintColor +import pl.szczodrzynski.edziennik.utils.BetterLink import kotlin.coroutines.CoroutineContext class AttendanceDetailsDialog( @@ -60,5 +61,13 @@ class AttendanceDetailsDialog( b.attendanceName.background.setTintColor(attendanceColor) b.attendanceIsCounted.setText(if (attendance.isCounted) R.string.yes else R.string.no) + + attendance.teacherName?.let { name -> + BetterLink.attach( + b.teacherName, + teachers = mapOf(attendance.teacherId to name), + onActionSelected = dialog::dismiss + ) + } }} } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/behaviour/NoticesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/behaviour/NoticesAdapter.kt index a3288da2..61ac01f1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/behaviour/NoticesAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/behaviour/NoticesAdapter.kt @@ -21,6 +21,7 @@ import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_MOBIDZIENNIK import pl.szczodrzynski.edziennik.data.db.entity.Notice import pl.szczodrzynski.edziennik.data.db.full.NoticeFull +import pl.szczodrzynski.edziennik.utils.BetterLink import pl.szczodrzynski.edziennik.utils.Utils.bs import pl.szczodrzynski.edziennik.utils.models.Date @@ -83,6 +84,14 @@ class NoticesAdapter//getting the context and product list with constructor } else { holder.noticesItemReason.background = null } + + BetterLink.attach(holder.noticesItemReason) + + notice.teacherName?.let { name -> + BetterLink.attach(holder.noticesItemTeacherName, teachers = mapOf( + notice.teacherId to name + )) + } } override fun getItemCount(): Int { 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 942876f2..051d86bd 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 @@ -345,6 +345,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { b.recipients.setAdapter(adapter) handleReplyMessage() + handleMailToIntent() }} private fun handleReplyMessage() { launch { @@ -402,6 +403,22 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { } }} + private fun handleMailToIntent() { + val teacherId = arguments?.getLong("messageRecipientId") + if (teacherId == 0L) + return + + val chipList = mutableListOf() + teachers.firstOrNull { it.id == teacherId }?.let { teacher -> + teacher.image = getProfileImage(48, 24, 16, 12, 1, teacher.fullName) + chipList += ChipInfo(teacher.fullName, teacher) + } + b.recipients.addTextWithChips(chipList) + + val subject = arguments?.getString("messageSubject") + b.subject.setText(subject ?: return) + } + private fun sendMessage() { b.recipientsLayout.error = null b.subjectLayout.error = null diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/BetterLink.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/BetterLink.kt index 53aa0429..6a5306bf 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/BetterLink.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/BetterLink.kt @@ -7,6 +7,8 @@ package pl.szczodrzynski.edziennik.utils import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent import android.graphics.Color import android.text.Spannable import android.text.SpannableString @@ -19,141 +21,268 @@ import android.view.ViewGroup import android.widget.TextView import androidx.appcompat.view.menu.MenuBuilder import androidx.appcompat.view.menu.MenuPopupHelper -import pl.szczodrzynski.edziennik.Intent -import pl.szczodrzynski.edziennik.copyToClipboard +import androidx.core.widget.addTextChangedListener +import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.Regexes -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.getTextPosition import pl.szczodrzynski.edziennik.utils.models.Date +@SuppressLint("RestrictedApi") object BetterLink { - @SuppressLint("RestrictedApi") - fun attach(textView: TextView, onActionSelected: (() -> Unit)? = null) { - textView.autoLinkMask = Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES - BetterLinkMovementMethod.linkify(textView.autoLinkMask, textView).setOnLinkClickListener { v, span: BetterLinkMovementMethod.ClickableSpanWithText -> - val url = span.text() - val c = v.context + /** + * Used in conjunction with the item's ID to execute the + * [attach]'s onActionSelected listener when the item is + * clicked. + */ + private const val FLAG_ACTION = 0x8000 - val s = v.text as Spanned - val start = s.getSpanStart(span.span()) - val end = s.getSpanEnd(span.span()) + private fun MenuBuilder.setTitle(title: CharSequence): MenuBuilder { + this::class.java.getDeclaredMethod("setHeaderTitleInt", CharSequence::class.java).let { + it.isAccessible = true + it.invoke(this, title) + } + return this + } - val parent = v.rootView.findViewById(android.R.id.content) - val parentLocation = intArrayOf(0, 0) - parent.getLocationOnScreen(parentLocation) - - val rect = textView.getTextPosition(start..end) - - val view = View(c) - view.layoutParams = ViewGroup.LayoutParams(rect.width(), rect.height()) - view.setBackgroundColor(Color.TRANSPARENT) - - parent.addView(view) - - view.x = rect.left.toFloat() - parentLocation[0] - view.y = rect.top.toFloat() - parentLocation[1] - - val menu = MenuBuilder(c) - val helper = MenuPopupHelper(c, menu, view) - val popup = helper.popup - - var menuTitle = url.substringAfter(":") - var date: Date? = null - - var urlItem: MenuItem? = null - var createEventItem: MenuItem? = null - //var goToTimetableItem: MenuItem? = null // TODO 2020-03-19: implement this - var mailItem: MenuItem? = null - var copyItem: MenuItem? = null - - when { - url.startsWith("mailto:") -> { - mailItem = menu.add(1, 20, 2, "Napisz e-mail") + private fun MenuItem.addListener(listener: (item: MenuItem) -> Boolean): MenuItem { + this::class.java.getDeclaredField("mClickListener").let { + it.isAccessible = true + val oldListener = it.get(this) as? MenuItem.OnMenuItemClickListener + it.set(this, object : MenuItem.OnMenuItemClickListener { + override fun onMenuItemClick(item: MenuItem): Boolean { + oldListener?.onMenuItemClick(item) + return listener(item) } - url.startsWith("dateYmd:") -> { - createEventItem = menu.add(1, 10, 2, "Utwórz wydarzenie") - //goToTimetableItem = menu.add(1, 11, 3, "Idź do planu lekcji") - date = parseDateYmd(menuTitle) - } - url.startsWith("dateDmy:") -> { - createEventItem = menu.add(1, 10, 2, "Utwórz wydarzenie") - //goToTimetableItem = menu.add(1, 11, 3, "Idź do planu lekcji") - date = parseDateDmy(menuTitle) - } - url.startsWith("dateAbs:") -> { - createEventItem = menu.add(1, 10, 2, "Utwórz wydarzenie") - //goToTimetableItem = menu.add(1, 11, 3, "Idź do planu lekcji") - date = parseDateAbs(menuTitle) - } - url.startsWith("dateRel:") -> { - createEventItem = menu.add(1, 10, 2, "Utwórz wydarzenie") - //goToTimetableItem = menu.add(1, 11, 3, "Idź do planu lekcji") - date = parseDateRel(menuTitle) - } - else -> { - urlItem = menu.add(1, 1, 2, "Otwórz w przeglądarce") - menuTitle = url - } - } - copyItem = menu.add(1, 1000, 1000, "Kopiuj tekst") + }) + } + return this + } - helper.setOnDismissListener { parent.removeView(view) } + private fun createUrlItems(menu: MenuBuilder, context: Context, url: String) { + menu.setTitle(url) + menu.add( + 1, + 2, + 2, + "Otwórz w przeglądarce" + ).setOnMenuItemClickListener { + Utils.openUrl(context, url) + true + } + } - urlItem?.setOnMenuItemClickListener { Utils.openUrl(c, url); true } - mailItem?.setOnMenuItemClickListener { Utils.openUrl(c, url); true } - copyItem?.setOnMenuItemClickListener { menuTitle.copyToClipboard(c); true } - createEventItem?.setOnMenuItemClickListener { - onActionSelected?.invoke() - val intent = Intent( - android.content.Intent.ACTION_MAIN, - "action" to "createManualEvent", - "eventDate" to date?.stringY_m_d - ) - c.sendBroadcast(intent) - true - } + private fun createMailtoItems(menu: MenuBuilder, context: Context, url: String) { + menu.add( + 1, + 3, + 3, + "Napisz e-mail" + ).setOnMenuItemClickListener { + Utils.openUrl(context, url) + true + } + } - menu::class.java.getDeclaredMethod("setHeaderTitleInt", CharSequence::class.java).let { - it.isAccessible = true - it.invoke(menu, menuTitle) - } - popup::class.java.getDeclaredField("mShowTitle").let { - it.isAccessible = true - it.set(popup, true) - } - helper::class.java.getDeclaredMethod("showPopup", Int::class.java, Int::class.java, Boolean::class.java, Boolean::class.java).let { - it.isAccessible = true - it.invoke(helper, 0, 0, false, true) - } + private fun createDateItems(menu: MenuBuilder, context: Context, date: Date?) { + date ?: return + menu.setTitle(date.formattedString) + menu.add( + 1, + 4 or FLAG_ACTION, + 4, + "Utwórz wydarzenie" + ).setOnMenuItemClickListener { + val intent = Intent( + Intent.ACTION_MAIN, + "action" to "createManualEvent", + "eventDate" to date.stringY_m_d + ) + context.sendBroadcast(intent) + true + } + } + + private fun createTeacherItems(menu: MenuBuilder, context: Context, teacherId: Long, fullName: String) { + menu.setTitle(fullName) + menu.add( + 1, + 5 or FLAG_ACTION, + 5, + "Napisz wiadomość" + ).setOnMenuItemClickListener { + val intent = Intent( + Intent.ACTION_MAIN, + "fragmentId" to MainActivity.TARGET_MESSAGES_COMPOSE, + "messageRecipientId" to teacherId + ) + context.sendBroadcast(intent) + true + } + } + + private fun onClickListener( + view: TextView, + span: BetterLinkMovementMethod.ClickableSpanWithText, + onActionSelected: (() -> Unit)? + ): Boolean { + val context = view.context + + val spanned = view.text as Spanned + val start = spanned.getSpanStart(span.span()) + val end = spanned.getSpanEnd(span.span()) + + val parent = view.rootView.findViewById(android.R.id.content) + val parentLocation = intArrayOf(0, 0) + parent.getLocationOnScreen(parentLocation) + + val rect = view.getTextPosition(start..end) + + val popupView = View(context) + popupView.layoutParams = ViewGroup.LayoutParams(rect.width(), rect.height()) + popupView.setBackgroundColor(Color.TRANSPARENT) + + parent.addView(popupView) + + popupView.x = rect.left.toFloat() - parentLocation[0] + popupView.y = rect.top.toFloat() - parentLocation[1] + + val menu = MenuBuilder(context) + val helper = MenuPopupHelper(context, menu, popupView) + val popup = helper.popup + + val spanUrl = span.text() + val spanParameter = spanUrl.substringAfter(":") + val spanText = spanned.substring(start, end) + + //goToTimetableItem = menu.add(1, 11, 3, "Idź do planu lekcji") + + // create appropriate items for spans + when { + spanUrl.startsWith("mailto:") -> createMailtoItems(menu, context, spanUrl) + spanUrl.startsWith("dateYmd:") -> createDateItems(menu, context, parseDateYmd(spanParameter)) + spanUrl.startsWith("dateDmy:") -> createDateItems(menu, context, parseDateDmy(spanParameter)) + spanUrl.startsWith("dateAbs:") -> createDateItems(menu, context, parseDateAbs(spanParameter)) + spanUrl.startsWith("dateRel:") -> createDateItems(menu, context, parseDateRel(spanParameter)) + spanUrl.startsWith("teacher:") -> createTeacherItems( + menu, + context, + teacherId = spanParameter.toLongOrNull() ?: -1, + fullName = spanText + ) + else -> createUrlItems(menu, context, spanUrl) + } + menu.add(1, 1000, 1000, "Kopiuj tekst").setOnMenuItemClickListener { + spanParameter.copyToClipboard(context) true } - val spanned = textView.text as? Spannable ?: { - SpannableString(textView.text) - }() + helper.setOnDismissListener { parent.removeView(popupView) } + + menu.visibleItems.forEach { item -> + if ((item.itemId and FLAG_ACTION) != FLAG_ACTION) + return@forEach + item.addListener { + onActionSelected?.invoke() + true + } + } + + popup::class.java.getDeclaredField("mShowTitle").let { + it.isAccessible = true + it.set(popup, true) + } + helper::class.java.getDeclaredMethod( + "showPopup", + Int::class.java, + Int::class.java, + Boolean::class.java, + Boolean::class.java + ).let { + it.isAccessible = true + it.invoke(helper, 0, 0, false, true) + } + return true + } + + fun attach( + textView: TextView, + teachers: Map? = null, + onActionSelected: (() -> Unit)? = null + ) { + textView.autoLinkMask = Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES + + BetterLinkMovementMethod + .linkify(textView.autoLinkMask, textView) + .setOnLinkClickListener { view, span -> + onClickListener(view, span, onActionSelected) + } + + textView.addTextChangedListener { + attachSpan(textView, teachers) + } + + attachSpan(textView, teachers) + } + + private fun attachSpan( + textView: TextView, + teachers: Map? = null + ) { + val spanned = textView.text as? Spannable ?: SpannableString(textView.text) + + teachers?.forEach { (id, fullName) -> + val index = textView.text.indexOf(fullName) + if (index == -1) + return@forEach + val span = URLSpan("teacher:$id") + spanned.setSpan( + span, + index, + index + fullName.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } Regexes.LINKIFY_DATE_YMD.findAll(textView.text).forEach { match -> val span = URLSpan("dateYmd:" + match.value) - spanned.setSpan(span, match.range.first, match.range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spanned.setSpan( + span, + match.range.first, + match.range.last + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) } Regexes.LINKIFY_DATE_DMY.findAll(textView.text).forEach { match -> val span = URLSpan("dateDmy:" + match.value) - spanned.setSpan(span, match.range.first, match.range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spanned.setSpan( + span, + match.range.first, + match.range.last + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) } Regexes.LINKIFY_DATE_ABSOLUTE.findAll(textView.text).forEach { match -> val span = URLSpan("dateAbs:" + match.value) - spanned.setSpan(span, match.range.first, match.range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spanned.setSpan( + span, + match.range.first, + match.range.last + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) } Regexes.LINKIFY_DATE_RELATIVE.findAll(textView.text).forEach { match -> val span = URLSpan("dateRel:" + match.value) - spanned.setSpan(span, match.range.first, match.range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spanned.setSpan( + span, + match.range.first, + match.range.last + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) } - //Linkify.addLinks(textView, LINKIFY_DATE_ABSOLUTE.toPattern(), "dateAbs:") - //Linkify.addLinks(textView, LINKIFY_DATE_RELATIVE.toPattern(), "dateRel:") } - private val monthNames = listOf("sty", "lut", "mar", "kwi", "maj", "cze", "lip", "sie", "wrz", "paź", "lis", "gru") + private val monthNames = + listOf("sty", "lut", "mar", "kwi", "maj", "cze", "lip", "sie", "wrz", "paź", "lis", "gru") private fun parseDateYmd(text: String): Date? { return Regexes.LINKIFY_DATE_YMD.find(text)?.let { @@ -163,6 +292,7 @@ object BetterLink { Date(year, month, day) } } + private fun parseDateDmy(text: String): Date? { return Regexes.LINKIFY_DATE_DMY.find(text)?.let { val day = it[1].toIntOrNull() ?: 1 @@ -194,7 +324,7 @@ object BetterLink { else -> 1 } - date.stepForward(0, 0, amount*unitInDays) + date.stepForward(0, 0, amount * unitInDays) } } } diff --git a/app/src/main/res/layout/attendance_details_dialog.xml b/app/src/main/res/layout/attendance_details_dialog.xml index 34a2dc0d..b18e1c89 100644 --- a/app/src/main/res/layout/attendance_details_dialog.xml +++ b/app/src/main/res/layout/attendance_details_dialog.xml @@ -94,6 +94,7 @@ android:textAppearance="@style/NavView.TextView.Helper" />