[API/Vulcan] Migrate to new MessageBox API. (#134)

* Implement fetching vulcan messages from new api

* Bump kotlin version and fix timetable card lessons size display

* Revert formatting changes

* Revert disabling message composing

* Revert MultiDex changes and dependency upgrades

* Add missing MultiDex dependency, update Google Services

* Separate MessageBoxes sync, revert API data behavior changes

* Revert migrating MessageRecipient to Kotlin

* Use loginId from MessageBox address book

* Revert using compatible HTML mode in Vulcan

* Implement message meta and read status changing

* Always set attachment lists

* Fix setting tutor role description

* Implement sending messages

* Replace millis constant with WEEK

* Revert timetable changes

* Remove unused DataVulcan properties

* Ensure UUID-format recipient IDs

Co-authored-by: Kuba Szczodrzyński <kuba@szczodrzynski.pl>
This commit is contained in:
Antoni Czaplicki 2022-09-17 12:31:35 +02:00 committed by GitHub
parent 54a61c6254
commit bb44fa066c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 297 additions and 102 deletions

View File

@ -142,6 +142,7 @@ dependencies {
// Language cores // Language cores
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "androidx.multidex:multidex:2.0.1"
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5" coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5"
// Android Jetpack // Android Jetpack

View File

@ -92,7 +92,7 @@ val MOBIDZIENNIK_USER_AGENT = SYSTEM_USER_AGENT
const val VULCAN_HEBE_USER_AGENT = "Dart/2.10 (dart:io)" const val VULCAN_HEBE_USER_AGENT = "Dart/2.10 (dart:io)"
const val VULCAN_HEBE_APP_NAME = "DzienniczekPlus 2.0" const val VULCAN_HEBE_APP_NAME = "DzienniczekPlus 2.0"
const val VULCAN_HEBE_APP_VERSION = "21.02.09 (G)" const val VULCAN_HEBE_APP_VERSION = "22.09.02 (G)"
private const val VULCAN_API_DEVICE_NAME_PREFIX = "Szkolny.eu " private const val VULCAN_API_DEVICE_NAME_PREFIX = "Szkolny.eu "
private const val VULCAN_API_DEVICE_NAME_SUFFIX = " - nie usuwać" private const val VULCAN_API_DEVICE_NAME_SUFFIX = " - nie usuwać"
val VULCAN_API_DEVICE_NAME by lazy { val VULCAN_API_DEVICE_NAME by lazy {
@ -116,9 +116,11 @@ const val VULCAN_HEBE_ENDPOINT_GRADE_SUMMARY = "api/mobile/grade/summary"
const val VULCAN_HEBE_ENDPOINT_HOMEWORK = "api/mobile/homework" const val VULCAN_HEBE_ENDPOINT_HOMEWORK = "api/mobile/homework"
const val VULCAN_HEBE_ENDPOINT_NOTICES = "api/mobile/note" const val VULCAN_HEBE_ENDPOINT_NOTICES = "api/mobile/note"
const val VULCAN_HEBE_ENDPOINT_ATTENDANCE = "api/mobile/lesson" const val VULCAN_HEBE_ENDPOINT_ATTENDANCE = "api/mobile/lesson"
const val VULCAN_HEBE_ENDPOINT_MESSAGES = "api/mobile/message" const val VULCAN_HEBE_ENDPOINT_MESSAGEBOX = "api/mobile/messagebox"
const val VULCAN_HEBE_ENDPOINT_MESSAGES_STATUS = "api/mobile/message/status" const val VULCAN_HEBE_ENDPOINT_MESSAGEBOX_ADDRESSBOOK = "api/mobile/messagebox/addressbook"
const val VULCAN_HEBE_ENDPOINT_MESSAGES_SEND = "api/mobile/message" const val VULCAN_HEBE_ENDPOINT_MESSAGEBOX_MESSAGES = "api/mobile/messagebox/message"
const val VULCAN_HEBE_ENDPOINT_MESSAGEBOX_STATUS = "api/mobile/messagebox/message/status"
const val VULCAN_HEBE_ENDPOINT_MESSAGEBOX_SEND = "api/mobile/messagebox/message"
const val VULCAN_HEBE_ENDPOINT_LUCKY_NUMBER = "api/mobile/school/lucky" const val VULCAN_HEBE_ENDPOINT_LUCKY_NUMBER = "api/mobile/school/lucky"
const val EDUDZIENNIK_USER_AGENT = "Szkolny.eu/${BuildConfig.VERSION_NAME}" const val EDUDZIENNIK_USER_AGENT = "Szkolny.eu/${BuildConfig.VERSION_NAME}"

View File

@ -20,6 +20,10 @@ object Regexes {
"""<br\s?/?>""".toRegex() """<br\s?/?>""".toRegex()
} }
val MESSAGE_META by lazy {
"""^\[META:([A-z0-9-&=]+)]""".toRegex()
}
val MOBIDZIENNIK_GRADES_SUBJECT_NAME by lazy { val MOBIDZIENNIK_GRADES_SUBJECT_NAME by lazy {

View File

@ -222,15 +222,15 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
get() { mHebeContext = mHebeContext ?: profile?.getStudentData("hebeContext", null); return mHebeContext } get() { mHebeContext = mHebeContext ?: profile?.getStudentData("hebeContext", null); return mHebeContext }
set(value) { profile?.putStudentData("hebeContext", value) ?: return; mHebeContext = value } set(value) { profile?.putStudentData("hebeContext", value) ?: return; mHebeContext = value }
private var mSenderAddressHash: String? = null private var mMessageBoxKey: String? = null
var senderAddressHash: String? var messageBoxKey: String?
get() { mSenderAddressHash = mSenderAddressHash ?: profile?.getStudentData("senderAddressHash", null); return mSenderAddressHash } get() { mMessageBoxKey = mMessageBoxKey ?: profile?.getStudentData("messageBoxKey", null); return mMessageBoxKey }
set(value) { profile?.putStudentData("senderAddressHash", value) ?: return; mSenderAddressHash = value } set(value) { profile?.putStudentData("messageBoxKey", value) ?: return; mMessageBoxKey = value }
private var mSenderAddressName: String? = null private var mMessageBoxName: String? = null
var senderAddressName: String? var messageBoxName: String?
get() { mSenderAddressName = mSenderAddressName ?: profile?.getStudentData("senderAddressName", null); return mSenderAddressName } get() { mMessageBoxName = mMessageBoxName ?: profile?.getStudentData("messageBoxName", null); return mMessageBoxName }
set(value) { profile?.putStudentData("senderAddressName", value) ?: return; mSenderAddressName = value } set(value) { profile?.putStudentData("messageBoxName", value) ?: return; mMessageBoxName = value }
val apiUrl: String? val apiUrl: String?
get() { get() {

View File

@ -12,6 +12,7 @@ const val ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS = 2010
const val ENDPOINT_VULCAN_HEBE_MAIN = 3000 const val ENDPOINT_VULCAN_HEBE_MAIN = 3000
const val ENDPOINT_VULCAN_HEBE_PUSH_CONFIG = 3005 const val ENDPOINT_VULCAN_HEBE_PUSH_CONFIG = 3005
const val ENDPOINT_VULCAN_HEBE_ADDRESSBOOK = 3010 const val ENDPOINT_VULCAN_HEBE_ADDRESSBOOK = 3010
const val ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2 = 3011
const val ENDPOINT_VULCAN_HEBE_TIMETABLE = 3020 const val ENDPOINT_VULCAN_HEBE_TIMETABLE = 3020
const val ENDPOINT_VULCAN_HEBE_EXAMS = 3030 const val ENDPOINT_VULCAN_HEBE_EXAMS = 3030
const val ENDPOINT_VULCAN_HEBE_GRADES = 3040 const val ENDPOINT_VULCAN_HEBE_GRADES = 3040
@ -19,10 +20,11 @@ const val ENDPOINT_VULCAN_HEBE_GRADE_SUMMARY = 3050
const val ENDPOINT_VULCAN_HEBE_HOMEWORK = 3060 const val ENDPOINT_VULCAN_HEBE_HOMEWORK = 3060
const val ENDPOINT_VULCAN_HEBE_NOTICES = 3070 const val ENDPOINT_VULCAN_HEBE_NOTICES = 3070
const val ENDPOINT_VULCAN_HEBE_ATTENDANCE = 3080 const val ENDPOINT_VULCAN_HEBE_ATTENDANCE = 3080
const val ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX = 3090
const val ENDPOINT_VULCAN_HEBE_MESSAGES_SENT = 3100
const val ENDPOINT_VULCAN_HEBE_TEACHERS = 3110 const val ENDPOINT_VULCAN_HEBE_TEACHERS = 3110
const val ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER = 3200 const val ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER = 3200
const val ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES = 3500
const val ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX = 3510
const val ENDPOINT_VULCAN_HEBE_MESSAGES_SENT = 3520
val VulcanFeatures = listOf( val VulcanFeatures = listOf(
// timetable // timetable
@ -85,6 +87,8 @@ val VulcanFeatures = listOf(
Feature(LOGIN_TYPE_VULCAN, FEATURE_ALWAYS_NEEDED, listOf( Feature(LOGIN_TYPE_VULCAN, FEATURE_ALWAYS_NEEDED, listOf(
ENDPOINT_VULCAN_HEBE_MAIN to LOGIN_METHOD_VULCAN_HEBE, ENDPOINT_VULCAN_HEBE_MAIN to LOGIN_METHOD_VULCAN_HEBE,
ENDPOINT_VULCAN_HEBE_ADDRESSBOOK to LOGIN_METHOD_VULCAN_HEBE, ENDPOINT_VULCAN_HEBE_ADDRESSBOOK to LOGIN_METHOD_VULCAN_HEBE,
ENDPOINT_VULCAN_HEBE_TEACHERS to LOGIN_METHOD_VULCAN_HEBE ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2 to LOGIN_METHOD_VULCAN_HEBE,
ENDPOINT_VULCAN_HEBE_TEACHERS to LOGIN_METHOD_VULCAN_HEBE,
ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES to LOGIN_METHOD_VULCAN_HEBE,
), listOf(LOGIN_METHOD_VULCAN_HEBE)) ), listOf(LOGIN_METHOD_VULCAN_HEBE))
) )

View File

@ -21,10 +21,12 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) {
ENDPOINT_VULCAN_HEBE_MAIN, ENDPOINT_VULCAN_HEBE_MAIN,
ENDPOINT_VULCAN_HEBE_PUSH_CONFIG, ENDPOINT_VULCAN_HEBE_PUSH_CONFIG,
ENDPOINT_VULCAN_HEBE_ADDRESSBOOK, ENDPOINT_VULCAN_HEBE_ADDRESSBOOK,
ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2,
ENDPOINT_VULCAN_HEBE_TIMETABLE, ENDPOINT_VULCAN_HEBE_TIMETABLE,
ENDPOINT_VULCAN_HEBE_EXAMS, ENDPOINT_VULCAN_HEBE_EXAMS,
ENDPOINT_VULCAN_HEBE_HOMEWORK, ENDPOINT_VULCAN_HEBE_HOMEWORK,
ENDPOINT_VULCAN_HEBE_NOTICES, ENDPOINT_VULCAN_HEBE_NOTICES,
ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES,
ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX, ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX,
ENDPOINT_VULCAN_HEBE_MESSAGES_SENT, ENDPOINT_VULCAN_HEBE_MESSAGES_SENT,
ENDPOINT_VULCAN_HEBE_TEACHERS, ENDPOINT_VULCAN_HEBE_TEACHERS,
@ -107,6 +109,10 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) {
data.startProgress(R.string.edziennik_progress_endpoint_addressbook) data.startProgress(R.string.edziennik_progress_endpoint_addressbook)
VulcanHebeAddressbook(data, lastSync, onSuccess) VulcanHebeAddressbook(data, lastSync, onSuccess)
} }
ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2 -> {
data.startProgress(R.string.edziennik_progress_endpoint_addressbook)
VulcanHebeAddressbook2(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_TEACHERS -> { ENDPOINT_VULCAN_HEBE_TEACHERS -> {
data.startProgress(R.string.edziennik_progress_endpoint_teachers) data.startProgress(R.string.edziennik_progress_endpoint_teachers)
VulcanHebeTeachers(data, lastSync, onSuccess) VulcanHebeTeachers(data, lastSync, onSuccess)
@ -139,6 +145,10 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) {
data.startProgress(R.string.edziennik_progress_endpoint_attendance) data.startProgress(R.string.edziennik_progress_endpoint_attendance)
VulcanHebeAttendance(data, lastSync, onSuccess) VulcanHebeAttendance(data, lastSync, onSuccess)
} }
ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES -> {
data.startProgress(R.string.edziennik_progress_endpoint_messages)
VulcanHebeMessageBoxes(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX -> { ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX -> {
data.startProgress(R.string.edziennik_progress_endpoint_messages_inbox) data.startProgress(R.string.edziennik_progress_endpoint_messages_inbox)
VulcanHebeMessages(data, lastSync, onSuccess).getMessages(Message.TYPE_RECEIVED) VulcanHebeMessages(data, lastSync, onSuccess).getMessages(Message.TYPE_RECEIVED)

View File

@ -14,7 +14,6 @@ import im.wangchao.mhttp.Response
import im.wangchao.mhttp.body.MediaTypeUtils import im.wangchao.mhttp.body.MediaTypeUtils
import im.wangchao.mhttp.callback.JsonCallbackHandler import im.wangchao.mhttp.callback.JsonCallbackHandler
import io.github.wulkanowy.signer.hebe.getSignatureHeaders import io.github.wulkanowy.signer.hebe.getSignatureHeaders
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.HebeFilterType import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.HebeFilterType
@ -55,6 +54,15 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
return date.getLong("Timestamp") ?: return default return date.getLong("Timestamp") ?: return default
} }
fun buildDateTime(): JsonObject {
return JsonObject(
"Timestamp" to System.currentTimeMillis(),
"Date" to Date.getToday().stringY_m_d,
"DateDisplay" to Date.getToday().stringDmy,
"Time" to Time.getNow().stringHMS,
)
}
fun getDate(json: JsonObject?, key: String): Date? { fun getDate(json: JsonObject?, key: String): Date? {
val date = json.getJsonObject(key) val date = json.getJsonObject(key)
return date.getString("Date")?.let { Date.fromY_m_d(it) } return date.getString("Date")?.let { Date.fromY_m_d(it) }
@ -74,6 +82,22 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
return teacherId return teacherId
} }
fun getTeacherRecipient(json: JsonObject): Teacher? {
val globalKey = json.getString("GlobalKey") ?: return null
if (globalKey == data.messageBoxKey)
return null
var name = json.getString("Name") ?: return null
val group = json.getString("Group", "P")
val loginId = "${globalKey};${group};${name}"
val pattern = " - $group - (${data.schoolShort})"
if (name.endsWith(pattern))
name = name.substringBefore(pattern)
val teacher = data.getTeacherByFirstLast(name, loginId)
if (teacher.type == 0)
teacher.type = Teacher.TYPE_OTHER
return teacher
}
fun getSubjectId(json: JsonObject?, key: String): Long? { fun getSubjectId(json: JsonObject?, key: String): Long? {
val subject = json.getJsonObject(key) val subject = json.getJsonObject(key)
val subjectId = subject.getLong("Id") ?: return null val subjectId = subject.getLong("Id") ?: return null
@ -89,7 +113,7 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
} }
fun getTeamId(json: JsonObject?, key: String): Long? { fun getTeamId(json: JsonObject?, key: String): Long? {
val team = json.getJsonObject(key) val team = json.getJsonObject(key) ?: return null
val teamId = team.getLong("Id") val teamId = team.getLong("Id")
var teamName = team.getString("Shortcut") var teamName = team.getString("Shortcut")
?: team.getString("Name") ?: team.getString("Name")
@ -104,7 +128,7 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
} }
fun getClassId(json: JsonObject?, key: String): Long? { fun getClassId(json: JsonObject?, key: String): Long? {
val team = json.getJsonObject(key) val team = json.getJsonObject(key) ?: return null
val teamId = team.getLong("Id") val teamId = team.getLong("Id")
val teamName = data.profile?.studentClassName val teamName = data.profile?.studentClassName
?: team.getString("Name") ?: team.getString("Name")
@ -148,7 +172,7 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
fun isCurrentYear(dateTime: Long): Boolean { fun isCurrentYear(dateTime: Long): Boolean {
return profile?.let { profile -> return profile?.let { profile ->
return@let dateTime >= profile.dateSemester1Start.inMillis return@let dateTime >= profile.dateSemester1Start.inMillis - WEEK * MS
} ?: false } ?: false
} }
@ -355,6 +379,7 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
dateTo: Date? = null, dateTo: Date? = null,
lastSync: Long? = null, lastSync: Long? = null,
folder: Int? = null, folder: Int? = null,
messageBox: String? = null,
params: Map<String, String> = mapOf(), params: Map<String, String> = mapOf(),
includeFilterType: Boolean = true, includeFilterType: Boolean = true,
onSuccess: (data: List<JsonObject>, response: Response?) -> Unit onSuccess: (data: List<JsonObject>, response: Response?) -> Unit
@ -378,6 +403,9 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
query["periodId"] = data.studentSemesterId.toString() query["periodId"] = data.studentSemesterId.toString()
query["pupilId"] = data.studentId.toString() query["pupilId"] = data.studentId.toString()
} }
HebeFilterType.BY_MESSAGEBOX -> {
query["box"] = messageBox ?: data.messageBoxKey ?: ""
}
} }
if (dateFrom != null) if (dateFrom != null)

View File

@ -5,6 +5,7 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
enum class HebeFilterType(val endpoint: String) { enum class HebeFilterType(val endpoint: String) {
BY_MESSAGEBOX("byBox"),
BY_PUPIL("byPupil"), BY_PUPIL("byPupil"),
BY_PERSON("byPerson"), BY_PERSON("byPerson"),
BY_PERIOD("byPeriod") BY_PERIOD("byPeriod")

View File

@ -5,6 +5,7 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import androidx.core.util.set import androidx.core.util.set
import androidx.room.OnConflictStrategy
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_ADDRESSBOOK import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_ADDRESSBOOK
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
@ -44,7 +45,6 @@ class VulcanHebeAddressbook(
) { list, _ -> ) { list, _ ->
list.forEach { person -> list.forEach { person ->
val id = person.getString("Id") ?: return@forEach val id = person.getString("Id") ?: return@forEach
val loginId = person.getString("LoginId") ?: return@forEach
val idType = id.split("-") val idType = id.split("-")
.getOrNull(0) .getOrNull(0)
@ -69,7 +69,7 @@ class VulcanHebeAddressbook(
idLong, idLong,
name, name,
surname, surname,
loginId null
).also { ).also {
data.teacherList[idLong] = it data.teacherList[idLong] = it
} }
@ -108,13 +108,14 @@ class VulcanHebeAddressbook(
} }
teacher.setTeacherType(personType) teacher.setTeacherType(personType)
teacher.typeDescription = roleText if (roleText != null)
teacher.typeDescription = roleText
} }
if (teacher.type == 0) if (teacher.type == 0)
teacher.setTeacherType(typeBase) teacher.setTeacherType(typeBase)
} }
data.teacherOnConflictStrategy = OnConflictStrategy.REPLACE
data.setSyncNext(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK, 2 * DAY) data.setSyncNext(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK, 2 * DAY)
onSuccess(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK) onSuccess(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK)
} }

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-21.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import androidx.room.OnConflictStrategy
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGEBOX_ADDRESSBOOK
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_OTHER
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_PARENT
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_STUDENT
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_TEACHER
import pl.szczodrzynski.edziennik.ext.DAY
import pl.szczodrzynski.edziennik.ext.getString
class VulcanHebeAddressbook2(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeAddressbook2"
}
init {
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_MESSAGEBOX_ADDRESSBOOK,
HebeFilterType.BY_MESSAGEBOX,
messageBox = data.messageBoxKey,
lastSync = lastSync,
includeFilterType = false
) { list, _ ->
list.forEach { person ->
val teacher = getTeacherRecipient(person) ?: return@forEach
val group = person.getString("Group", "P")
if (teacher.type == TYPE_OTHER) {
teacher.type = when (group) {
"P" -> TYPE_TEACHER // Pracownik
"O" -> TYPE_PARENT // Opiekun
"U" -> TYPE_STUDENT // Uczeń
else -> TYPE_OTHER
}
}
}
data.teacherOnConflictStrategy = OnConflictStrategy.REPLACE
data.setSyncNext(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2, 2 * DAY)
onSuccess(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2)
}
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-9-16.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGEBOX
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.ext.DAY
import pl.szczodrzynski.edziennik.ext.getString
class VulcanHebeMessageBoxes(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeMessageBoxes"
}
init {
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_MESSAGEBOX,
lastSync = lastSync
) { list, _ ->
for (messageBox in list) {
val name = messageBox.getString("Name") ?: continue
val studentName = profile?.studentNameLong ?: continue
if (!name.startsWith(studentName))
continue
data.messageBoxKey = messageBox.getString("GlobalKey")
data.messageBoxName = name
break
}
data.setSyncNext(ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES, 7 * DAY)
onSuccess(ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES)
}
}
}

View File

@ -4,22 +4,21 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import androidx.core.util.set
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGEBOX_MESSAGES
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_MESSAGES_SENT import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_MESSAGES_SENT
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.* import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_DELETED import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_DELETED
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT
import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.navlib.crc16
class VulcanHebeMessages( class VulcanHebeMessages(
override val data: DataVulcan, override val data: DataVulcan,
@ -27,29 +26,7 @@ class VulcanHebeMessages(
val onSuccess: (endpointId: Int) -> Unit val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) { ) : VulcanHebe(data, lastSync) {
companion object { companion object {
const val TAG = "VulcanHebeMessagesInbox" const val TAG = "VulcanHebeMessages"
}
private fun getPersonId(json: JsonObject): Long {
val senderLoginId = json.getInt("LoginId") ?: return -1
/*if (senderLoginId == data.studentLoginId)
return -1*/
val senderName = json.getString("Address") ?: return -1
val senderNameSplit = senderName.splitName()
val senderLoginIdStr = senderLoginId.toString()
val teacher = data.teacherList.singleOrNull { it.loginId == senderLoginIdStr }
?: Teacher(
profileId,
-1 * crc16(senderName).toLong(),
senderNameSplit?.second ?: "",
senderNameSplit?.first ?: "",
senderLoginIdStr
).also {
it.setTeacherType(Teacher.TYPE_OTHER)
data.teacherList[it.id] = it
}
return teacher.id
} }
fun getMessages(messageType: Int) { fun getMessages(messageType: Int) {
@ -64,17 +41,28 @@ class VulcanHebeMessages(
TYPE_SENT -> ENDPOINT_VULCAN_HEBE_MESSAGES_SENT TYPE_SENT -> ENDPOINT_VULCAN_HEBE_MESSAGES_SENT
else -> ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX else -> ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX
} }
val messageBox = data.messageBoxKey
if (messageBox == null) {
onSuccess(endpointId)
return
}
apiGetList( apiGetList(
TAG, TAG,
VULCAN_HEBE_ENDPOINT_MESSAGES, VULCAN_HEBE_ENDPOINT_MESSAGEBOX_MESSAGES,
HebeFilterType.BY_PERSON, HebeFilterType.BY_MESSAGEBOX,
messageBox = data.messageBoxKey,
folder = folder, folder = folder,
lastSync = lastSync lastSync = lastSync
) { list, _ -> ) { list, _ ->
list.forEach { message -> list.forEach { message ->
val id = message.getLong("Id") ?: return@forEach val uuid = message.getString("Id") ?: return@forEach
val id = Utils.crc32(uuid.toByteArray())
val globalKey = message.getString("GlobalKey", "")
val threadKey = message.getString("ThreadKey", "")
val subject = message.getString("Subject") ?: return@forEach val subject = message.getString("Subject") ?: return@forEach
val body = message.getString("Content") ?: return@forEach var body = message.getString("Content") ?: return@forEach
val sender = message.getJsonObject("Sender") ?: return@forEach val sender = message.getJsonObject("Sender") ?: return@forEach
@ -83,13 +71,27 @@ class VulcanHebeMessages(
if (!isCurrentYear(sentDate)) return@forEach if (!isCurrentYear(sentDate)) return@forEach
val senderId = if (messageType == TYPE_RECEIVED)
getTeacherRecipient(sender)?.id
else
null
val meta = mutableMapOf(
"uuid" to uuid,
"globalKey" to globalKey,
"threadKey" to threadKey,
)
val metaString = meta.map { "${it.key}=${it.value}" }.join("&")
body = "[META:${metaString}]" + body
body = body.replace("\n", "<br>")
val messageObject = Message( val messageObject = Message(
profileId = profileId, profileId = profileId,
id = id, id = id,
type = messageType, type = messageType,
subject = subject, subject = subject,
body = body.replace("\n", "<br>"), body = body,
senderId = if (messageType == TYPE_RECEIVED) getPersonId(sender) else null, senderId = senderId,
addedDate = sentDate addedDate = sentDate
) )
@ -101,9 +103,14 @@ class VulcanHebeMessages(
else -1 else -1
for (receiver in receivers) { for (receiver in receivers) {
val recipientId = if (messageType == TYPE_SENT)
getTeacherRecipient(receiver)?.id ?: -1
else
-1
val messageRecipientObject = MessageRecipient( val messageRecipientObject = MessageRecipient(
profileId, profileId,
if (messageType == TYPE_SENT) getPersonId(receiver) else -1, recipientId,
-1, -1,
receiverReadDate, receiverReadDate,
id id
@ -115,6 +122,9 @@ class VulcanHebeMessages(
?.asJsonObjectList() ?.asJsonObjectList()
?: return@forEach ?: return@forEach
messageObject.attachmentIds = mutableListOf()
messageObject.attachmentNames = mutableListOf()
messageObject.attachmentSizes = mutableListOf()
for (attachment in attachments) { for (attachment in attachments) {
val fileName = attachment.getString("Name") ?: continue val fileName = attachment.getString("Name") ?: continue
val url = attachment.getString("Link") ?: continue val url = attachment.getString("Link") ?: continue

View File

@ -5,7 +5,7 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES_STATUS import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGEBOX_STATUS
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent
@ -23,13 +23,19 @@ class VulcanHebeMessagesChangeStatus(
const val TAG = "VulcanHebeMessagesChangeStatus" const val TAG = "VulcanHebeMessagesChangeStatus"
} }
init { init { let {
val messageKey = messageObject.body?.let { data.parseMessageMeta(it) }?.get("globalKey") ?: run {
EventBus.getDefault().postSticky(MessageGetEvent(messageObject))
onSuccess()
return@let
}
apiPost( apiPost(
TAG, TAG,
VULCAN_HEBE_ENDPOINT_MESSAGES_STATUS, VULCAN_HEBE_ENDPOINT_MESSAGEBOX_STATUS,
payload = JsonObject( payload = JsonObject(
"MessageId" to messageObject.id, "BoxKey" to data.messageBoxKey,
"LoginId" to data.studentLoginId, "MessageKey" to messageKey,
"Status" to 1 "Status" to 1
) )
) { _: Boolean, _ -> ) { _: Boolean, _ ->
@ -61,5 +67,5 @@ class VulcanHebeMessagesChangeStatus(
EventBus.getDefault().postSticky(MessageGetEvent(messageObject)) EventBus.getDefault().postSticky(MessageGetEvent(messageObject))
onSuccess() onSuccess()
} }
} }}
} }

View File

@ -4,17 +4,20 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import com.google.gson.JsonArray
import com.google.gson.JsonObject import com.google.gson.JsonObject
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.ERROR_MESSAGE_NOT_SENT
import pl.szczodrzynski.edziennik.data.api.ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY import pl.szczodrzynski.edziennik.data.api.ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES_SEND import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGEBOX_SEND
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent
import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.ext.*
import java.util.UUID
class VulcanHebeSendMessage( class VulcanHebeSendMessage(
override val data: DataVulcan, override val data: DataVulcan,
@ -28,9 +31,9 @@ class VulcanHebeSendMessage(
} }
init { init {
if (data.senderAddressName == null || data.senderAddressHash == null) { if (data.messageBoxKey == null || data.messageBoxName == null) {
VulcanHebeMain(data).getStudents(data.profile, null) { VulcanHebeMessageBoxes(data, 0) {
if (data.senderAddressName == null || data.senderAddressHash == null) { if (data.messageBoxKey == null || data.messageBoxName == null) {
data.error(TAG, ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY) data.error(TAG, ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY)
} }
else { else {
@ -44,47 +47,64 @@ class VulcanHebeSendMessage(
} }
private fun sendMessage() { private fun sendMessage() {
val uuid = UUID.randomUUID().toString()
val globalKey = UUID.randomUUID().toString()
val partition = "${data.symbol}-${data.schoolSymbol}"
val recipientsArray = JsonArray() val recipientsArray = JsonArray()
recipients.forEach { teacher -> recipients.forEach { teacher ->
val loginId = teacher.loginId?.split(";", limit = 3) ?: return@forEach
val key = loginId.getOrNull(0) ?: teacher.loginId
val group = loginId.getOrNull(1)
val name = loginId.getOrNull(2)
if (key?.toIntOrNull() != null) {
// raise error for old-format (non-UUID) login IDs
data.error(TAG, ERROR_MESSAGE_NOT_SENT)
return
}
recipientsArray += JsonObject( recipientsArray += JsonObject(
"Address" to teacher.fullNameLastFirst, "Id" to "${data.messageBoxKey}-${key}",
"LoginId" to (teacher.loginId?.toIntOrNull() ?: return@forEach), "Partition" to partition,
"Initials" to teacher.initialsLastFirst, "Owner" to data.messageBoxKey,
"AddressHash" to teacher.fullNameLastFirst.sha1Hex() "GlobalKey" to key,
"Name" to name,
"Group" to group,
"Initials" to "",
"HasRead" to 0,
) )
} }
val senderName = (profile?.accountName ?: profile?.studentNameLong)
?.swapFirstLastName() ?: ""
val sender = JsonObject( val sender = JsonObject(
"Address" to data.senderAddressName, "Id" to "0",
"LoginId" to data.studentLoginId.toString(), "Partition" to partition,
"Initials" to senderName.getNameInitials(), "Owner" to data.messageBoxKey,
"AddressHash" to data.senderAddressHash "GlobalKey" to data.messageBoxKey,
"Name" to data.messageBoxName,
"Group" to "",
"Initials" to "",
"HasRead" to 0,
) )
apiPost( apiPost(
TAG, TAG,
VULCAN_HEBE_ENDPOINT_MESSAGES_SEND, VULCAN_HEBE_ENDPOINT_MESSAGEBOX_SEND,
payload = JsonObject( payload = JsonObject(
"Status" to 1, "Id" to uuid,
"Sender" to sender, "GlobalKey" to globalKey,
"DateSent" to null, "Partition" to partition,
"DateRead" to null, "ThreadKey" to globalKey, // TODO correct threadKey for reply messages
"Content" to text,
"Receiver" to recipientsArray,
"Id" to 0,
"Subject" to subject, "Subject" to subject,
"Attachments" to null, "Content" to text,
"Self" to null "Status" to 1,
"Owner" to data.messageBoxKey,
"DateSent" to buildDateTime(),
"DateRead" to null,
"Sender" to sender,
"Receiver" to recipientsArray,
"Attachments" to JsonArray(),
) )
) { json: JsonObject, _ -> ) { _: JsonObject, _ ->
val messageId = json.getLong("Id") // TODO handle errors
if (messageId == null) {
// TODO error
return@apiPost
}
VulcanHebeMessages(data, null) { VulcanHebeMessages(data, null) {
val message = data.messageList.firstOrNull { it.isSent && it.subject == subject } val message = data.messageList.firstOrNull { it.isSent && it.subject == subject }

View File

@ -45,6 +45,7 @@ class VulcanHebeTeachers(
when (subjectName) { when (subjectName) {
"Pedagog" -> teacher.setTeacherType(Teacher.TYPE_PEDAGOGUE) "Pedagog" -> teacher.setTeacherType(Teacher.TYPE_PEDAGOGUE)
"Dyrektor" -> teacher.setTeacherType(Teacher.TYPE_PRINCIPAL)
else -> { else -> {
val subjectId = data.getSubject(null, subjectName).id val subjectId = data.getSubject(null, subjectName).id
if (!teacher.subjects.contains(subjectId)) if (!teacher.subjects.contains(subjectId))

View File

@ -11,6 +11,7 @@ import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.BuildConfig import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE
import pl.szczodrzynski.edziennik.data.api.Regexes.MESSAGE_META
import pl.szczodrzynski.edziennik.data.api.interfaces.EndpointCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EndpointCallback
import pl.szczodrzynski.edziennik.data.db.AppDb import pl.szczodrzynski.edziennik.data.db.AppDb
import pl.szczodrzynski.edziennik.data.db.entity.* import pl.szczodrzynski.edziennik.data.db.entity.*
@ -489,11 +490,19 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
teacherList[id] = this teacherList[id] = this
} }
return obj.also { return obj.also {
if (loginId != null && it.loginId != null) if (loginId != null)
it.loginId = loginId it.loginId = loginId
if (firstName.length > 1) if (firstName.length > 1)
it.name = firstName it.name = firstName
it.surname = lastName it.surname = lastName
} }
} }
fun parseMessageMeta(body: String): Map<String, String>? {
val match = MESSAGE_META.find(body) ?: return null
return match[1].split("&").associateBy(
{ it.substringBefore("=") },
{ it.substringAfter("=") },
)
}
} }

View File

@ -77,7 +77,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
private lateinit var stylingConfig: StylingConfig private lateinit var stylingConfig: StylingConfig
private lateinit var uiConfig: UIConfig private lateinit var uiConfig: UIConfig
private val enableTextStyling private val enableTextStyling
get() = app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN && app.profile.loginStoreType != LoginStore.LOGIN_TYPE_LIBRUS get() = app.profile.loginStoreType != LoginStore.LOGIN_TYPE_LIBRUS
private var changedRecipients = false private var changedRecipients = false
private var changedSubject = false private var changedSubject = false
private var changedBody = false private var changedBody = false

View File

@ -15,6 +15,7 @@ import android.text.style.*
import androidx.appcompat.widget.AppCompatEditText import androidx.appcompat.widget.AppCompatEditText
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import pl.szczodrzynski.edziennik.data.api.Regexes.MESSAGE_META
import pl.szczodrzynski.edziennik.ext.dp import pl.szczodrzynski.edziennik.ext.dp
import pl.szczodrzynski.edziennik.ext.getWordBounds import pl.szczodrzynski.edziennik.ext.getWordBounds
import pl.szczodrzynski.edziennik.ext.resolveAttr import pl.szczodrzynski.edziennik.ext.resolveAttr
@ -45,7 +46,7 @@ object BetterHtml {
.toRegex(RegexOption.IGNORE_CASE) .toRegex(RegexOption.IGNORE_CASE)
var text = html var text = html
.replace("\\[META:[A-z0-9]+;[0-9-]+]".toRegex(), "") .replace(MESSAGE_META, "")
.replace("background-color: ?$hexPattern;".toRegex(), "") .replace("background-color: ?$hexPattern;".toRegex(), "")
// treat paragraphs as if they had no margin // treat paragraphs as if they had no margin
.replace("<p", "<span") .replace("<p", "<span")

View File

@ -859,7 +859,7 @@
<string name="settings_about_licenses_text">Open-source licenses</string> <string name="settings_about_licenses_text">Open-source licenses</string>
<string name="settings_about_privacy_policy_text">Privacy policy</string> <string name="settings_about_privacy_policy_text">Privacy policy</string>
<string name="settings_card_register_title">E-register</string> <string name="settings_card_register_title">E-register</string>
<string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nSeptember 2018 - 2022</string> <string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nSeptember 2018 2022</string>
<string name="settings_about_update_subtext">Click to check for updates</string> <string name="settings_about_update_subtext">Click to check for updates</string>
<string name="settings_about_update_text">Update</string> <string name="settings_about_update_text">Update</string>
<string name="settings_about_version_text">Version</string> <string name="settings_about_version_text">Version</string>

View File

@ -23,8 +23,8 @@ buildscript {
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.2.2' classpath 'com.android.tools.build:gradle:7.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.gms:google-services:4.3.13' classpath 'com.google.gms:google-services:4.3.14'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.1' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.2'
} }
} }