[Messages] Implement APIv2 in MessageFragment.

This commit is contained in:
Kuba Szczodrzyński 2019-11-12 14:11:35 +01:00
parent c6e2519dcc
commit c83abe57d5
27 changed files with 814 additions and 77 deletions

View File

@ -39,4 +39,13 @@
-keep class okhttp3.** { *; }
-keep class com.google.android.material.tabs.** {*;}
-keep class com.google.android.material.tabs.** {*;}
# ServiceLoader support
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
# Most of volatile fields are updated with AFU and should not be mangled
-keepclassmembernames class kotlinx.** {
volatile <fields>;
}

View File

@ -11,6 +11,7 @@ import android.text.style.ForegroundColorSpan
import android.text.style.StrikethroughSpan
import android.util.LongSparseArray
import android.util.SparseArray
import android.view.View
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.core.app.ActivityCompat
@ -408,4 +409,11 @@ fun JsonObject(vararg properties: Pair<String, Any>): JsonObject {
}
fun JsonArray?.isNullOrEmpty(): Boolean = (this?.size() ?: 0) == 0
fun JsonArray.isEmpty(): Boolean = this.size() == 0
fun JsonArray.isEmpty(): Boolean = this.size() == 0
@Suppress("UNCHECKED_CAST")
inline fun <T : View> T.onClick(crossinline onClickListener: (v: T) -> Unit) {
setOnClickListener { v: View ->
onClickListener(v as T)
}
}

View File

@ -57,7 +57,7 @@ import pl.szczodrzynski.edziennik.ui.modules.grades.editor.GradesEditorFragment
import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment
import pl.szczodrzynski.edziennik.ui.modules.homework.HomeworkFragment
import pl.szczodrzynski.edziennik.ui.modules.login.LoginActivity
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesDetailsFragment
import pl.szczodrzynski.edziennik.ui.modules.messages.MessageFragment
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment
import pl.szczodrzynski.edziennik.ui.modules.notifications.NotificationsFragment
import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment
@ -203,7 +203,7 @@ class MainActivity : AppCompatActivity() {
list += NavTarget(TARGET_GRADES_EDITOR, R.string.menu_grades_editor, GradesEditorFragment::class)
list += NavTarget(TARGET_HELP, R.string.menu_help, HelpFragment::class)
list += NavTarget(TARGET_FEEDBACK, R.string.menu_feedback, FeedbackFragment::class)
list += NavTarget(TARGET_MESSAGES_DETAILS, R.string.menu_message, MessagesDetailsFragment::class)
list += NavTarget(TARGET_MESSAGES_DETAILS, R.string.menu_message, MessageFragment::class)
list += NavTarget(DRAWER_ITEM_DEBUG, R.string.menu_debug, DebugFragment::class)
list

View File

@ -0,0 +1,9 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-12.
*/
package pl.szczodrzynski.edziennik.api.v2.events
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
data class MessageGetEvent(val message: MessageFull)

View File

@ -12,6 +12,7 @@ import pl.szczodrzynski.edziennik.api.v2.mobidziennik.Mobidziennik
import pl.szczodrzynski.edziennik.api.v2.template.Template
import pl.szczodrzynski.edziennik.api.v2.vulcan.Vulcan
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTask(profileId) {
companion object {
@ -21,7 +22,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
fun sync() = EdziennikTask(-1, SyncRequest())
fun syncProfile(profileId: Int, viewIds: List<Pair<Int, Int>>? = null, arguments: JsonObject? = null) = EdziennikTask(profileId, SyncProfileRequest(viewIds, arguments))
fun syncProfileList(profileList: List<Int>) = EdziennikTask(-1, SyncProfileListRequest(profileList))
fun messageGet(profileId: Int, messageId: Long) = EdziennikTask(profileId, MessageGetRequest(messageId))
fun messageGet(profileId: Int, message: MessageFull) = EdziennikTask(profileId, MessageGetRequest(message))
fun announcementsRead(profileId: Int) = EdziennikTask(profileId, AnnouncementsReadRequest())
}
@ -39,7 +40,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
// get the requested profile and login store
val profile = app.db.profileDao().getByIdNow(profileId)
this.profile = profile
if (profile == null || !profile.syncEnabled) {
if (profile == null) {
return
}
val loginStore = app.db.loginStoreDao().getByIdNow(profile.loginStoreId) ?: return
@ -69,7 +70,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
featureIds = request.viewIds?.flatMap { Features.getIdsByView(it.first, it.second) } ?: Features.getAllIds(),
viewId = request.viewIds?.get(0)?.first,
arguments = request.arguments)
is MessageGetRequest -> edziennikInterface?.getMessage(request.messageId)
is MessageGetRequest -> edziennikInterface?.getMessage(request.message)
is FirstLoginRequest -> edziennikInterface?.firstLogin()
is AnnouncementsReadRequest -> edziennikInterface?.markAllAnnouncementsAsRead()
}
@ -87,6 +88,6 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
class SyncRequest
data class SyncProfileRequest(val viewIds: List<Pair<Int, Int>>? = null, val arguments: JsonObject? = null)
data class SyncProfileListRequest(val profileList: List<Int>)
data class MessageGetRequest(val messageId: Long)
data class MessageGetRequest(val message: MessageFull)
class AnnouncementsReadRequest
}

View File

@ -16,6 +16,7 @@ import pl.szczodrzynski.edziennik.api.v2.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.edziennik.api.v2.prepare
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.utils.Utils.d
@ -61,7 +62,7 @@ class Idziennik(val app: App, val profile: Profile?, val loginStore: LoginStore,
}
}
override fun getMessage(messageId: Long) {
override fun getMessage(message: MessageFull) {
}

View File

@ -75,7 +75,7 @@ class IdziennikApiMessagesInbox(override val data: DataIdziennik,
/*messageId*/ messageId
)
data.messageList.add(message)
data.messageIgnoreList.add(message)
data.messageRecipientList.add(messageRecipient)
data.messageMetadataList.add(Metadata(
profileId,

View File

@ -74,7 +74,7 @@ class IdziennikApiMessagesSent(override val data: DataIdziennik,
data.messageRecipientIgnoreList.add(messageRecipient)
}
data.messageList.add(message)
data.messageIgnoreList.add(message)
data.metadataList.add(Metadata(profileId, Metadata.TYPE_MESSAGE, message.id, true, true, sentDate))
}

View File

@ -5,10 +5,11 @@
package pl.szczodrzynski.edziennik.api.v2.interfaces
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
interface EdziennikInterface {
fun sync(featureIds: List<Int>, viewId: Int? = null, arguments: JsonObject? = null)
fun getMessage(messageId: Long)
fun getMessage(message: MessageFull)
fun markAllAnnouncementsAsRead()
fun firstLogin()
fun cancel()

View File

@ -19,6 +19,7 @@ import pl.szczodrzynski.edziennik.api.v2.librus.login.LibrusLoginMessages
import pl.szczodrzynski.edziennik.api.v2.librus.login.LibrusLoginSynergia
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.utils.Utils.d
@ -80,11 +81,13 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va
}
}
override fun getMessage(messageId: Long) {
override fun getMessage(message: MessageFull) {
LibrusLoginApi(data) {
LibrusLoginMessages(data) {
LibrusMessagesGetMessage(data, messageId) {
completed()
LibrusLoginSynergia(data) {
LibrusLoginMessages(data) {
LibrusMessagesGetMessage(data, message) {
completed()
}
}
}
}

View File

@ -22,8 +22,8 @@ class LibrusApiUsers(override val data: DataLibrus,
users?.forEach { user ->
val id = user.getLong("Id") ?: return@forEach
val firstName = user.getString("FirstName")?.fixWhiteSpaces() ?: ""
val lastName = user.getString("LastName")?.fixWhiteSpaces() ?: ""
val firstName = user.getString("FirstName")?.fixName() ?: ""
val lastName = user.getString("LastName")?.fixName() ?: ""
data.teacherList.put(id, Teacher(profileId, id, firstName, lastName))
}

View File

@ -99,7 +99,7 @@ class LibrusMessagesGetList(override val data: DataLibrus, private val type: Int
id
)
data.messageList.add(messageObject)
data.messageIgnoreList.add(messageObject)
data.messageRecipientList.add(messageRecipientObject)
data.metadataList.add(Metadata(
profileId,

View File

@ -5,34 +5,43 @@
package pl.szczodrzynski.edziennik.api.v2.librus.data.messages
import android.util.Base64
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.api.v2.events.MessageGetEvent
import pl.szczodrzynski.edziennik.api.v2.librus.DataLibrus
import pl.szczodrzynski.edziennik.api.v2.librus.data.LibrusMessages
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipientFull
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.fixName
import pl.szczodrzynski.edziennik.singleOrNull
import pl.szczodrzynski.edziennik.utils.models.Date
import java.nio.charset.Charset
class LibrusMessagesGetMessage(override val data: DataLibrus, val messageId: Long, val onSuccess: () -> Unit) : LibrusMessages(data) {
class LibrusMessagesGetMessage(
override val data: DataLibrus,
private val messageObject: MessageFull,
val onSuccess: () -> Unit
) : LibrusMessages(data) {
companion object {
const val TAG = "LibrusMessagesGetMessage"
}
init {
init { data.profile?.also { profile ->
messagesGet(TAG, "GetMessage", parameters = mapOf(
"messageId" to messageId,
"messageId" to messageObject.id,
"archive" to 0
)) { doc ->
val message = doc.select("response GetMessage data").first()
val body = Base64.decode(message.select("Message").text(), Base64.DEFAULT)
.toString().apply {
replace("\n", "")
replace("<!\\[CDATA\\[", "")
replace("]]>", "")
}
.toString(Charset.defaultCharset())
.replace("\n", "<br>")
.replace("<!\\[CDATA\\[", "")
.replace("]]>", "")
val messageObject = data.db.messageDao().getById(profileId, messageId).apply {
messageObject.apply {
this.body = body
clearAttachments()
@ -43,16 +52,20 @@ class LibrusMessagesGetMessage(override val data: DataLibrus, val messageId: Lon
}
}
val messageRecipientList = mutableListOf<MessageRecipientFull>()
when (messageObject.type) {
TYPE_RECEIVED -> {
val senderLoginId = message.select("senderId").text()
data.teacherList.singleOrNull { it.id == messageObject.senderId }?.loginId = senderLoginId
val readDateText = message.select("readDate").text()
val readDate = when (readDateText.isNotEmpty()) {
true -> Date.fromIso(readDateText)
else -> 0
}
val messageRecipientObject = MessageRecipient(
val messageRecipientObject = MessageRecipientFull(
profileId,
-1,
-1,
@ -60,21 +73,21 @@ class LibrusMessagesGetMessage(override val data: DataLibrus, val messageId: Lon
messageObject.id
)
data.messageRecipientList.add(messageRecipientObject)
data.db.teacherDao().updateLoginId(profileId, messageObject.senderId, senderLoginId)
messageRecipientObject.fullName = profile.accountNameLong ?: profile.studentNameLong
messageRecipientList.add(messageRecipientObject)
}
TYPE_SENT -> {
val teachers = data.db.teacherDao().getAllNow(profileId)
message.select("receivers ArrayItem").forEach { receiver ->
val receiverFirstName = receiver.select("firstName").text()
val receiverLastName = receiver.select("lastName").text()
val receiverFirstName = receiver.select("firstName").text().fixName()
val receiverLastName = receiver.select("lastName").text().fixName()
val receiverLoginId = receiver.select("receiverId").text()
val receiverId = teachers.singleOrNull {
it.name.equals(receiverFirstName, true) && it.surname.equals(receiverLastName, true)
}?.id ?: -1
val teacher = data.teacherList.singleOrNull { it.name == receiverFirstName && it.surname == receiverLastName }
val receiverId = teacher?.id ?: -1
teacher?.loginId = receiverLoginId
val readDateText = message.select("readed").text()
val readDate = when (readDateText.isNotEmpty()) {
@ -82,24 +95,38 @@ class LibrusMessagesGetMessage(override val data: DataLibrus, val messageId: Lon
else -> 0
}
val messageRecipientObject = MessageRecipient(
val messageRecipientObject = MessageRecipientFull(
profileId,
-1,
receiverId,
-1,
readDate,
messageObject.id
)
data.messageRecipientList.add(messageRecipientObject)
data.db.teacherDao().updateLoginId(profileId, receiverId, receiverLoginId)
messageRecipientObject.fullName = "$receiverFirstName $receiverLastName"
messageRecipientList.add(messageRecipientObject)
}
}
}
if (!messageObject.seen) data.db.metadataDao().setSeen(profileId, messageObject, true)
if (!messageObject.seen) {
data.messageMetadataList.add(Metadata(
messageObject.profileId,
Metadata.TYPE_MESSAGE,
messageObject.id,
true,
true,
messageObject.addedDate
))
}
data.messageList.add(messageObject as Message)
messageObject.recipients = messageRecipientList
data.messageRecipientList.addAll(messageRecipientList)
data.messageList.add(messageObject)
EventBus.getDefault().postSticky(MessageGetEvent(messageObject))
onSuccess()
}
}
} ?: onSuccess()}
}

View File

@ -16,6 +16,7 @@ import pl.szczodrzynski.edziennik.api.v2.mobidziennikLoginMethods
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.edziennik.api.v2.prepare
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.utils.Utils.d
@ -61,7 +62,7 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto
}
}
override fun getMessage(messageId: Long) {
override fun getMessage(message: MessageFull) {
}

View File

@ -8,9 +8,7 @@ import org.jsoup.Jsoup
import pl.szczodrzynski.edziennik.DAY
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.DataMobidziennik
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_INBOX
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.data.MobidziennikWeb
import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT
@ -79,7 +77,7 @@ class MobidziennikWebMessagesAll(override val data: DataMobidziennik,
-1
)
data.messageList.add(message)
data.messageIgnoreList.add(message)
data.metadataList.add(Metadata(profileId, Metadata.TYPE_MESSAGE, message.id, true, true, addedDate))
}

View File

@ -67,7 +67,7 @@ class MobidziennikWebMessagesInbox(override val data: DataMobidziennik,
if (hasAttachments)
message.setHasAttachments()
data.messageList.add(message)
data.messageIgnoreList.add(message)
data.messageMetadataList.add(
Metadata(
profileId,

View File

@ -156,6 +156,7 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
val teacherAbsenceList = mutableListOf<TeacherAbsence>()
val messageList = mutableListOf<Message>()
val messageIgnoreList = mutableListOf<Message>()
val messageRecipientList = mutableListOf<MessageRecipient>()
val messageRecipientIgnoreList = mutableListOf<MessageRecipient>()
@ -205,7 +206,7 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
announcementList.clear()
luckyNumberList.clear()
teacherAbsenceList.clear()
messageList.clear()
messageIgnoreList.clear()
messageRecipientList.clear()
messageRecipientIgnoreList.clear()
metadataList.clear()
@ -285,6 +286,11 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
}
}
if (metadataList.isNotEmpty())
db.metadataDao().addAllIgnore(metadataList)
if (messageMetadataList.isNotEmpty())
db.metadataDao().setSeen(messageMetadataList)
// not extracted from DB - always new data
if (lessonList.isNotEmpty()) {
db.lessonDao().clear(profile.id)
@ -316,15 +322,13 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
db.teacherAbsenceDao().addAll(teacherAbsenceList)
if (messageList.isNotEmpty())
db.messageDao().addAllIgnore(messageList)
db.messageDao().addAll(messageList)
if (messageIgnoreList.isNotEmpty())
db.messageDao().addAllIgnore(messageIgnoreList)
if (messageRecipientList.isNotEmpty())
db.messageRecipientDao().addAll(messageRecipientList)
if (messageRecipientIgnoreList.isNotEmpty())
db.messageRecipientDao().addAllIgnore(messageRecipientIgnoreList)
if (metadataList.isNotEmpty())
db.metadataDao().addAllIgnore(metadataList)
if (messageMetadataList.isNotEmpty())
db.metadataDao().setSeen(messageMetadataList)
}
fun notifyAndSyncEvents(onSuccess: () -> Unit) {

View File

@ -16,6 +16,7 @@ import pl.szczodrzynski.edziennik.api.v2.template.firstlogin.TemplateFirstLogin
import pl.szczodrzynski.edziennik.api.v2.template.login.TemplateLogin
import pl.szczodrzynski.edziennik.api.v2.templateLoginMethods
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.utils.Utils.d
@ -61,7 +62,7 @@ class Template(val app: App, val profile: Profile?, val loginStore: LoginStore,
}
}
override fun getMessage(messageId: Long) {
override fun getMessage(message: MessageFull) {
}

View File

@ -16,6 +16,7 @@ import pl.szczodrzynski.edziennik.api.v2.vulcan.firstlogin.VulcanFirstLogin
import pl.szczodrzynski.edziennik.api.v2.vulcan.login.VulcanLogin
import pl.szczodrzynski.edziennik.api.v2.vulcanLoginMethods
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.utils.Utils.d
@ -61,7 +62,7 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
}
}
override fun getMessage(messageId: Long) {
override fun getMessage(message: MessageFull) {
}

View File

@ -68,7 +68,7 @@ class VulcanApiMessagesInbox(override val data: DataVulcan, val onSuccess: () ->
id
)
data.messageList.add(messageObject)
data.messageIgnoreList.add(messageObject)
data.messageRecipientList.add(messageRecipientObject)
data.metadataList.add(Metadata(
profileId,

View File

@ -80,7 +80,7 @@ class VulcanApiMessagesSent(override val data: DataVulcan, val onSuccess: () ->
data.messageRecipientList.add(messageRecipientObject)
}
data.messageList.add(messageObject)
data.messageIgnoreList.add(messageObject)
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_MESSAGE,

View File

@ -1,19 +1,19 @@
package pl.szczodrzynski.edziennik.data.db.modules.messages;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.Nullable;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.Index;
import java.util.ArrayList;
import java.util.List;
@Entity(tableName = "messages",
primaryKeys = {"profileId", "messageId"},
indices = {@Index(value = {"profileId"})})
public class Message {
int profileId;
public int profileId;
@ColumnInfo(name = "messageId")
public long id;

View File

@ -1,7 +1,6 @@
package pl.szczodrzynski.edziennik.data.db.modules.messages;
import java.util.List;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Insert;
@ -11,6 +10,8 @@ import androidx.room.RawQuery;
import androidx.sqlite.db.SimpleSQLiteQuery;
import androidx.sqlite.db.SupportSQLiteQuery;
import java.util.List;
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata;
import static pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_DELETED;
@ -23,6 +24,9 @@ public abstract class MessageDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract long add(Message message);
@Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract void addAll(List<Message> messageList);
@Insert(onConflict = OnConflictStrategy.IGNORE)
public abstract void addAllIgnore(List<Message> messageList);
@ -56,6 +60,7 @@ public abstract class MessageDao {
"ORDER BY addedDate DESC"));
}
@Nullable
public MessageFull getById(int profileId, long messageId) {
return getOneNow(new SimpleSQLiteQuery("SELECT \n" +
"*, \n" +

View File

@ -1,13 +1,15 @@
package pl.szczodrzynski.edziennik.data.db.modules.messages;
import androidx.annotation.Nullable;
import androidx.room.Ignore;
import java.util.ArrayList;
import java.util.List;
import androidx.room.Ignore;
public class MessageFull extends Message {
public String senderFullName = null;
@Ignore
@Nullable
public List<MessageRecipientFull> recipients = null;
public MessageFull addRecipient(MessageRecipientFull recipient) {

View File

@ -0,0 +1,319 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-12.
*/
package pl.szczodrzynski.edziennik.ui.modules.messages
import android.os.Bundle
import android.os.Environment
import android.text.Html
import android.text.TextUtils
import android.view.Gravity.CENTER_VERTICAL
import android.view.Gravity.END
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ProgressBar
import androidx.fragment.app.Fragment
import com.google.android.material.chip.Chip
import com.mikepenz.iconics.IconicsColor
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.IconicsSize
import com.mikepenz.iconics.typeface.IIcon
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.api.v2.events.MessageGetEvent
import pl.szczodrzynski.edziennik.api.v2.events.task.EdziennikTask
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.databinding.MessageFragmentBinding
import pl.szczodrzynski.edziennik.onClick
import pl.szczodrzynski.edziennik.utils.Anim
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.Utils.getStringFromFile
import pl.szczodrzynski.edziennik.utils.Utils.readableFileSize
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.navlib.colorAttr
import java.io.File
import kotlin.coroutines.CoroutineContext
import kotlin.math.min
class MessageFragment : Fragment(), CoroutineScope {
companion object {
private const val TAG = "MessageFragment"
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: MessageFragmentBinding
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private lateinit var message: MessageFull
private var attachmentList = mutableListOf<Attachment>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
context ?: return null
app = activity.application as App
context!!.theme.applyStyle(Themes.appTheme, true)
b = MessageFragmentBinding.inflate(inflater)
job = Job()
return b.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// TODO check if app, activity, b can be null
if (app.profile == null || !isAdded)
return
b.closeButton.setImageDrawable(
IconicsDrawable(activity, CommunityMaterial.Icon2.cmd_window_close)
.colorAttr(activity, android.R.attr.textColorSecondary)
.sizeDp(12)
)
b.closeButton.setOnClickListener { activity.navigateUp() }
val messageId = arguments?.getLong("messageId")
if (messageId == null) {
activity.navigateUp()
return
}
launch {
val deferred = async(Dispatchers.Default) {
val msg = app.db.messageDao().getById(App.profileId, messageId)?.also {
it.recipients = app.db.messageRecipientDao().getAllByMessageId(it.profileId, it.id)
if (it.body != null && !it.seen) {
app.db.metadataDao().setSeen(it.profileId, message, true)
}
}
msg
}
val msg = deferred.await() ?: run {
return@launch
}
message = msg
b.subject.text = message.subject
checkMessage()
}
// click to expand subject and sender
b.subject.onClick {
it.maxLines = if (it.maxLines == 30) 2 else 30
}
b.sender.onClick {
it.maxLines = if (it.maxLines == 30) 2 else 30
}
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onMessageGetEvent(event: MessageGetEvent) {
// TODO remove this: message = event.message
showMessage()
}
private fun checkMessage() {
if (message.body == null) {
EdziennikTask.messageGet(App.profileId, message).enqueue(activity)
return
}
var readByAll = true
message.recipients?.forEach { recipient ->
if (recipient.id == -1L)
recipient.fullName = app.profile.accountNameLong ?: app.profile.studentNameLong
if (message.type == TYPE_SENT && recipient.readDate < 1)
readByAll = false
}
// if a sent msg is not read by everyone, download it again to check the read status
if (!readByAll) {
EdziennikTask.messageGet(App.profileId, message).enqueue(activity)
return
}
showMessage()
}
private fun showMessage() {
b.body.text = Html.fromHtml(message.body?.replace("\\[META:[A-z0-9]+;[0-9-]+]".toRegex(), ""))
b.date.text = getString(R.string.messages_date_time_format, Date.fromMillis(message.addedDate).formattedStringShort, Time.fromMillis(message.addedDate).stringHM)
val messageInfo = MessagesUtils.getMessageInfo(app, message, 40, 20, 14, 10)
b.profileBackground.setImageBitmap(messageInfo.profileImage)
b.sender.text = messageInfo.profileName
b.subject.text = message.subject
val messageRecipients = StringBuilder("<ul>")
message.recipients?.forEach { recipient ->
when (recipient.readDate) {
-1L -> messageRecipients.append(getString(
R.string.messages_recipients_list_unknown_state_format,
recipient.fullName
))
0L -> messageRecipients.append(getString(
R.string.messages_recipients_list_unread_format,
recipient.fullName
))
1L -> messageRecipients.append(getString(
R.string.messages_recipients_list_read_unknown_date_format,
recipient.fullName
))
else -> messageRecipients.append(getString(
R.string.messages_recipients_list_read_format,
recipient.fullName,
Date.fromMillis(recipient.readDate).formattedString,
Time.fromMillis(recipient.readDate).stringHM
))
}
}
messageRecipients.append("</ul>")
b.recipients.text = Html.fromHtml(messageRecipients.toString())
showAttachments()
b.progress.visibility = View.GONE
Anim.fadeIn(b.content, 200, null)
MessagesFragment.pageSelection = min(message.type, 1)
}
private fun showAttachments() {
if (message.attachmentIds != null) {
val insertPoint = b.attachments
val chipLayoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
chipLayoutParams.setMargins(0, Utils.dpToPx(8), 0, Utils.dpToPx(8))
val progressLayoutParams = FrameLayout.LayoutParams(Utils.dpToPx(18), Utils.dpToPx(18))
progressLayoutParams.setMargins(Utils.dpToPx(8), 0, Utils.dpToPx(8), 0)
progressLayoutParams.gravity = END or CENTER_VERTICAL
// CREATE VIEWS AND AN OBJECT FOR EVERY ATTACHMENT
message.attachmentNames.forEachIndexed { index, name ->
val messageId = message.id
val id = message.attachmentIds[index]
val size = message.attachmentSizes[index]
// create the parent
val attachmentLayout = FrameLayout(b.root.context)
attachmentLayout.setPadding(Utils.dpToPx(16), 0, Utils.dpToPx(16), 0)
val attachmentChip = Chip(attachmentLayout.context)
//attachmentChip.setChipBackgroundColorResource(ThemeUtils.getChipColorRes());
attachmentChip.layoutParams = chipLayoutParams
attachmentChip.height = Utils.dpToPx(40)
// show the file size or not
if (size == -1L)
attachmentChip.text = getString(R.string.messages_attachment_no_size_format, name)
else
attachmentChip.text = getString(R.string.messages_attachment_format, name, readableFileSize(size))
attachmentChip.ellipsize = TextUtils.TruncateAt.MIDDLE
// create an icon for the attachment
var icon: IIcon = CommunityMaterial.Icon.cmd_file
when (Utils.getExtensionFromFileName(name)) {
"txt" -> icon = CommunityMaterial.Icon.cmd_file_document
"doc", "docx", "odt", "rtf" -> icon = CommunityMaterial.Icon.cmd_file_word
"xls", "xlsx", "ods" -> icon = CommunityMaterial.Icon.cmd_file_excel
"ppt", "pptx", "odp" -> icon = CommunityMaterial.Icon.cmd_file_powerpoint
"pdf" -> icon = CommunityMaterial.Icon.cmd_file_pdf
"mp3", "wav", "aac" -> icon = CommunityMaterial.Icon.cmd_file_music
"mp4", "avi", "3gp", "mkv", "flv" -> icon = CommunityMaterial.Icon.cmd_file_video
"jpg", "jpeg", "png", "bmp", "gif" -> icon = CommunityMaterial.Icon.cmd_file_image
"zip", "rar", "tar", "7z" -> icon = CommunityMaterial.Icon.cmd_file_lock
}
attachmentChip.chipIcon = IconicsDrawable(activity).color(IconicsColor.colorRes(R.color.colorPrimary)).icon(icon).size(IconicsSize.dp(26))
attachmentChip.closeIcon = IconicsDrawable(activity).icon(CommunityMaterial.Icon.cmd_check).size(IconicsSize.dp(18)).color(IconicsColor.colorInt(Utils.getAttr(activity, android.R.attr.textColorPrimary)))
attachmentChip.isCloseIconVisible = false
// set the object's index in the attachmentList as the tag
attachmentChip.tag = index
attachmentChip.setOnClickListener { v ->
if (v.tag is Int) {
// TODO downloadAttachment(v.tag as Int)
}
}
attachmentLayout.addView(attachmentChip)
val attachmentProgress = ProgressBar(attachmentLayout.context)
attachmentProgress.layoutParams = progressLayoutParams
attachmentProgress.visibility = View.GONE
attachmentLayout.addView(attachmentProgress)
insertPoint.addView(attachmentLayout)
// create an object and add to the list
val a = Attachment(App.profileId, messageId, id, name, size, attachmentLayout, attachmentChip, attachmentProgress)
attachmentList.add(a)
// check if the file is already downloaded. Show the check icon if necessary and set `downloaded` to true.
checkAttachment(a)
}
} else {
// no attachments found
b.attachmentsTitle.visibility = View.GONE
}
}
private fun checkAttachment(attachment: Attachment) {
val storageDir = Environment.getExternalStoragePublicDirectory("Szkolny.eu")
storageDir.mkdirs()
val attachmentDataFile = File(storageDir, "." + attachment.profileId + "_" + attachment.messageId + "_" + attachment.attachmentId)
if (attachmentDataFile.exists()) {
try {
val attachmentFileName = getStringFromFile(attachmentDataFile)
val attachmentFile = File(attachmentFileName)
if (attachmentFile.exists()) {
attachment.downloaded = attachmentFileName
attachment.chip.isCloseIconVisible = true
}
} catch (e: Exception) {
e.printStackTrace()
//app.apiEdziennik.guiReportException(activity, 355, e)
}
}
}
override fun onStart() {
EventBus.getDefault().register(this)
super.onStart()
}
override fun onStop() {
super.onStop()
EventBus.getDefault().unregister(this)
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
private class Attachment(
var profileId: Int,
var messageId: Long,
var attachmentId: Long,
var attachmentName: String,
var attachmentSize: Long,
var parent: FrameLayout,
var chip: Chip,
var progressBar: ProgressBar
) {
/**
* An absolute path of the downloaded file. `null` if not downloaded yet.
*/
internal var downloaded: String? = null
}
}

View File

@ -1,8 +1,6 @@
package pl.szczodrzynski.edziennik.ui.modules.messages
import android.os.Bundle
import android.text.Html
import android.text.InputType
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -10,18 +8,14 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import androidx.viewpager.widget.ViewPager
import com.afollestad.materialdialogs.MaterialDialog
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.api.v2.LOGIN_TYPE_LIBRUS
import pl.szczodrzynski.edziennik.api.v2.events.ApiTaskErrorEvent
import pl.szczodrzynski.edziennik.api.v2.events.ApiTaskFinishedEvent
import pl.szczodrzynski.edziennik.api.v2.events.task.EdziennikTask
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.databinding.FragmentMessagesBinding
import pl.szczodrzynski.edziennik.utils.Themes
@ -92,7 +86,7 @@ class MessagesFragment : Fragment() {
b.tabLayout.setupWithViewPager(b.viewPager)
if (app.profile.loginStoreType == LOGIN_TYPE_LIBRUS && app.profile.getStudentData("accountPassword", null) == null) {
/*if (app.profile.loginStoreType == LOGIN_TYPE_LIBRUS && app.profile.getStudentData("accountPassword", null) == null) {
MaterialDialog.Builder(activity)
.title("Wiadomości w systemie Synergia")
.content("Moduł Wiadomości w aplikacji Szkolny.eu jest przeglądarką zasobów szkolnego konta Synergia. Z tego powodu, musisz wpisać swoje hasło do tego konta, aby móc korzystać z tej funkcji.")
@ -115,7 +109,7 @@ class MessagesFragment : Fragment() {
.show()
}
.show()
}
}*/
}
@Subscribe(threadMode = ThreadMode.MAIN)

View File

@ -0,0 +1,353 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-12.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:orientation="horizontal"
android:background="@color/colorSurface_6dp">
<ImageButton
android:id="@+id/closeButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="2dp"
android:background="?android:attr/actionBarItemBackground"
app:srcCompat="@android:drawable/ic_delete" />
<TextView
android:id="@+id/subject"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:animateLayoutChanges="true"
android:background="?selectableItemBackground"
android:ellipsize="end"
android:maxLines="2"
android:padding="16dp"
android:textAppearance="@style/NavView.TextView.Title"
tools:ignore="HardcodedText"
tools:text="mobiDziennik - raport dzienny." />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@drawable/shadow_top" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:visibility="visible"
tools:visibility="gone"/>
<ScrollView
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="gone"
tools:visibility="visible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:orientation="horizontal">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/profileBackground"
android:layout_width="64dp"
android:layout_height="64dp"
android:padding="12dp"
app:srcCompat="@drawable/bg_circle" />
<TextView
android:id="@+id/profileName"
android:layout_width="64dp"
android:layout_height="64dp"
android:fontFamily="sans-serif"
android:gravity="center"
android:padding="12dp"
android:textColor="#ffffff"
android:textSize="20sp"
tools:text="JP"
tools:visibility="visible" />
<ImageView
android:id="@+id/profileImage"
android:layout_width="64dp"
android:layout_height="64dp"
android:padding="12dp"
android:visibility="gone"
tools:srcCompat="@tools:sample/avatars[0]" />
</FrameLayout>
<TextView
android:id="@+id/sender"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="?selectableItemBackground"
android:ellipsize="end"
android:maxLines="2"
android:paddingLeft="8dp"
android:paddingTop="12dp"
android:paddingRight="8dp"
android:textAppearance="@style/NavView.TextView.Subtitle"
tools:text="Allegro - wysyłamy duużo wiadomości!!! Masz nowe oferty! Możesz kupić nowego laptopa! Ale super! Ehh, to jest nadawca a nie temat więc nwm czemu to tutaj wpisałem" />
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:textAppearance="@style/NavView.TextView.Small"
tools:text="14:26" />
</LinearLayout>
<TextView
android:id="@+id/body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:autoLink="all"
android:minHeight="250dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:textIsSelectable="true"
tools:text="To jest treść wiadomości.\n\nZazwyczaj ma wiele linijek.\n\nTak" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?colorControlHighlight" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginRight="16dp"
android:text="Odbiorcy wiadomości:"
android:textAppearance="@style/NavView.TextView.Subtitle" />
<TextView
android:id="@+id/recipients"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
tools:text=" - Jan Kowalski, przeczytano: nie\n - Adam Dodatkowy, przeczytano: 20 marca, 17:35" />
<TextView
android:id="@+id/attachmentsTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginRight="16dp"
android:text="Załączniki:"
android:textAppearance="@style/NavView.TextView.Subtitle" />
<LinearLayout
android:id="@+id/attachments"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="8dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:paddingLeft="16dp"
android:paddingRight="16dp"
tools:visibility="visible">
<com.google.android.material.chip.Chip
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:text="Testowy plik.pdf"
android:visibility="visible"
app:chipBackgroundColor="@color/mtrl_chip_background_color"
app:chipIcon="@drawable/googleg_standard_color_18"
app:chipMinHeight="36dp"
app:chipSurfaceColor="@color/mtrl_chip_surface_color"
app:closeIcon="@drawable/ic_error_outline"
app:closeIconVisible="true" />
<ProgressBar
style="?android:attr/progressBarStyle"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_gravity="center_vertical|end"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:paddingLeft="16dp"
android:paddingRight="16dp"
tools:visibility="visible">
<com.google.android.material.chip.Chip
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:text="Wyniki sprawdzianu z matematyki.pdf"
android:visibility="visible"
app:chipIcon="@drawable/googleg_standard_color_18"
app:chipMinHeight="36dp" />
<ProgressBar
style="?android:attr/progressBarStyle"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_gravity="center_vertical|end"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp" />
</FrameLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp"
android:orientation="horizontal"
android:visibility="gone"
tools:visibility="visible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_weight="1"
android:background="@drawable/bg_rounded_ripple"
android:gravity="center"
android:orientation="vertical"
android:paddingLeft="4dp"
android:paddingTop="8dp"
android:paddingRight="4dp"
android:paddingBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Odpowiedz"
android:textAppearance="@style/NavView.TextView.Small" />
<com.mikepenz.iconics.view.IconicsImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:padding="4dp"
app:iiv_color="?android:textColorSecondary"
app:iiv_icon="cmd-reply"
tools:srcCompat="@android:drawable/ic_menu_revert" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_weight="1"
android:background="@drawable/bg_rounded_ripple"
android:gravity="center"
android:orientation="vertical"
android:paddingLeft="4dp"
android:paddingTop="8dp"
android:paddingRight="4dp"
android:paddingBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Przekaż dalej"
android:textAllCaps="false"
android:textAppearance="@style/NavView.TextView.Small" />
<com.mikepenz.iconics.view.IconicsImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:padding="4dp"
app:iiv_color="?android:textColorSecondary"
app:iiv_icon="cmd-share"
tools:srcCompat="@android:drawable/ic_media_ff" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_weight="1"
android:background="@drawable/bg_rounded_ripple"
android:gravity="center"
android:orientation="vertical"
android:paddingLeft="4dp"
android:paddingTop="8dp"
android:paddingRight="4dp"
android:paddingBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Usuń"
android:textAppearance="@style/NavView.TextView.Small" />
<com.mikepenz.iconics.view.IconicsImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:padding="4dp"
app:iiv_color="?android:textColorSecondary"
app:iiv_icon="cmd-delete"
tools:srcCompat="@android:drawable/ic_menu_delete" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>
</layout>