forked from github/szkolny
[Lab] Add JSON editor.
This commit is contained in:
parent
481af64137
commit
e34e4d6906
@ -26,6 +26,7 @@ import pl.szczodrzynski.edziennik.ui.modules.debug.models.LabJsonObject
|
||||
import pl.szczodrzynski.edziennik.ui.modules.debug.viewholder.JsonArrayViewHolder
|
||||
import pl.szczodrzynski.edziennik.ui.modules.debug.viewholder.JsonElementViewHolder
|
||||
import pl.szczodrzynski.edziennik.ui.modules.debug.viewholder.JsonObjectViewHolder
|
||||
import pl.szczodrzynski.edziennik.ui.modules.debug.viewholder.JsonSubObjectViewHolder
|
||||
import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel
|
||||
import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
@ -35,14 +36,20 @@ class LabJsonAdapter(
|
||||
var onJsonElementClick: ((item: LabJsonElement) -> Unit)? = null
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), CoroutineScope {
|
||||
companion object {
|
||||
private const val TAG = "AttendanceAdapter"
|
||||
private const val TAG = "LabJsonAdapter"
|
||||
private const val ITEM_TYPE_OBJECT = 0
|
||||
private const val ITEM_TYPE_ARRAY = 1
|
||||
private const val ITEM_TYPE_ELEMENT = 2
|
||||
private const val ITEM_TYPE_SUB_OBJECT = 1
|
||||
private const val ITEM_TYPE_ARRAY = 2
|
||||
private const val ITEM_TYPE_ELEMENT = 3
|
||||
const val STATE_CLOSED = 0
|
||||
const val STATE_OPENED = 1
|
||||
|
||||
fun expand(item: Any, level: Int): MutableList<Any> {
|
||||
val path = when (item) {
|
||||
is LabJsonObject -> item.key + ":"
|
||||
is LabJsonArray -> item.key + ":"
|
||||
else -> ""
|
||||
}
|
||||
val json = when (item) {
|
||||
is LabJsonObject -> item.jsonObject
|
||||
is LabJsonArray -> item.jsonArray
|
||||
@ -53,17 +60,16 @@ class LabJsonAdapter(
|
||||
}
|
||||
|
||||
return when (json) {
|
||||
is JsonObject -> json.entrySet().mapNotNull { wrap(it.key, it.value, level) }
|
||||
is JsonArray -> json.mapIndexedNotNull { index, jsonElement -> wrap(index.toString(), jsonElement, level) }
|
||||
else -> listOf(LabJsonElement("?", json, level))
|
||||
is JsonObject -> json.entrySet().mapNotNull { wrap(path + it.key, it.value, level) }
|
||||
is JsonArray -> json.mapIndexedNotNull { index, jsonElement -> wrap(path + index.toString(), jsonElement, level) }
|
||||
else -> listOf(LabJsonElement("$path?", json, level))
|
||||
}.toMutableList()
|
||||
}
|
||||
fun wrap(key: String, item: JsonElement, level: Int = 0): Any? {
|
||||
fun wrap(key: String, item: JsonElement, level: Int = 0): Any {
|
||||
return when (item) {
|
||||
is JsonObject -> LabJsonObject(key, item, level + 1)
|
||||
is JsonArray -> LabJsonArray(key, item, level + 1)
|
||||
is JsonElement -> LabJsonElement(key, item, level + 1)
|
||||
else -> null
|
||||
else -> LabJsonElement(key, item, level + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -80,6 +86,7 @@ class LabJsonAdapter(
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return when (viewType) {
|
||||
ITEM_TYPE_OBJECT -> JsonObjectViewHolder(inflater, parent)
|
||||
ITEM_TYPE_SUB_OBJECT -> JsonSubObjectViewHolder(inflater, parent)
|
||||
ITEM_TYPE_ARRAY -> JsonArrayViewHolder(inflater, parent)
|
||||
ITEM_TYPE_ELEMENT -> JsonElementViewHolder(inflater, parent)
|
||||
else -> throw IllegalArgumentException("Incorrect viewType")
|
||||
@ -87,8 +94,10 @@ class LabJsonAdapter(
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (items[position]) {
|
||||
is LabJsonObject -> ITEM_TYPE_OBJECT
|
||||
return when (val item = items[position]) {
|
||||
is LabJsonObject ->
|
||||
if (item.level == 1) ITEM_TYPE_OBJECT
|
||||
else ITEM_TYPE_SUB_OBJECT
|
||||
is LabJsonArray -> ITEM_TYPE_ARRAY
|
||||
is LabJsonElement -> ITEM_TYPE_ELEMENT
|
||||
else -> throw IllegalArgumentException("Incorrect viewType")
|
||||
@ -118,7 +127,7 @@ class LabJsonAdapter(
|
||||
View.ROTATION,
|
||||
if (model.state == STATE_CLOSED) 0f else 180f,
|
||||
if (model.state == STATE_CLOSED) 180f else 0f
|
||||
).setDuration(200).start();
|
||||
).setDuration(200).start()
|
||||
}
|
||||
|
||||
// hide the preview, show summary
|
||||
@ -143,7 +152,9 @@ class LabJsonAdapter(
|
||||
var end: Int = items.size
|
||||
for (i in start until items.size) {
|
||||
val model1 = items[i]
|
||||
val level = (model1 as? ExpandableItemModel<*>)?.level ?: 3
|
||||
val level = (model1 as? ExpandableItemModel<*>)?.level
|
||||
?: (model1 as? LabJsonElement)?.level
|
||||
?: model.level
|
||||
if (level <= model.level) {
|
||||
end = i
|
||||
break
|
||||
@ -170,6 +181,7 @@ class LabJsonAdapter(
|
||||
|
||||
val viewType = when (holder) {
|
||||
is JsonObjectViewHolder -> ITEM_TYPE_OBJECT
|
||||
is JsonSubObjectViewHolder -> ITEM_TYPE_SUB_OBJECT
|
||||
is JsonArrayViewHolder -> ITEM_TYPE_ARRAY
|
||||
is JsonElementViewHolder -> ITEM_TYPE_ELEMENT
|
||||
else -> throw IllegalArgumentException("Incorrect viewType")
|
||||
@ -180,6 +192,7 @@ class LabJsonAdapter(
|
||||
|
||||
when {
|
||||
holder is JsonObjectViewHolder && item is LabJsonObject -> holder.onBind(activity, app, item, position, this)
|
||||
holder is JsonSubObjectViewHolder && item is LabJsonObject -> holder.onBind(activity, app, item, position, this)
|
||||
holder is JsonArrayViewHolder && item is LabJsonArray -> holder.onBind(activity, app, item, position, this)
|
||||
holder is JsonElementViewHolder && item is LabJsonElement -> holder.onBind(activity, app, item, position, this)
|
||||
}
|
||||
|
@ -10,16 +10,14 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.google.gson.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import pl.szczodrzynski.edziennik.App
|
||||
import pl.szczodrzynski.edziennik.MainActivity
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
|
||||
import pl.szczodrzynski.edziennik.*
|
||||
import pl.szczodrzynski.edziennik.data.api.models.ApiError
|
||||
import pl.szczodrzynski.edziennik.databinding.TemplateListPageFragmentBinding
|
||||
import pl.szczodrzynski.edziennik.startCoroutineTimer
|
||||
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
|
||||
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
@ -38,6 +36,10 @@ class LabProfileFragment : LazyFragment(), CoroutineScope {
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
// local/private variables go here
|
||||
private lateinit var adapter: LabJsonAdapter
|
||||
private val loginStore by lazy {
|
||||
app.db.loginStoreDao().getByIdNow(app.profile.loginStoreId)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
activity = (getActivity() as MainActivity?) ?: return null
|
||||
@ -48,22 +50,96 @@ class LabProfileFragment : LazyFragment(), CoroutineScope {
|
||||
}
|
||||
|
||||
override fun onPageCreated(): Boolean { startCoroutineTimer(100L) {
|
||||
val adapter = LabJsonAdapter(activity)
|
||||
val json = JsonObject().also { json ->
|
||||
json.add("app.profile", app.profile.studentData)
|
||||
json.add("app.config", JsonParser().parse(app.gson.toJson(app.config.values)))
|
||||
EdziennikTask.profile?.let {
|
||||
json.add("API.profile", it.studentData)
|
||||
} ?: {
|
||||
json.addProperty("API.profile", "null")
|
||||
}()
|
||||
EdziennikTask.loginStore?.let {
|
||||
json.add("API.loginStore", it.data)
|
||||
} ?: {
|
||||
json.addProperty("API.loginStore", "null")
|
||||
}()
|
||||
}
|
||||
adapter.items = LabJsonAdapter.expand(json, 0)
|
||||
adapter = LabJsonAdapter(activity, onJsonElementClick = { item ->
|
||||
try {
|
||||
var parent: Any = Unit
|
||||
var obj: Any = Unit
|
||||
var objName: String = ""
|
||||
item.key.split(":").forEach { el ->
|
||||
parent = obj
|
||||
obj = when (el) {
|
||||
"App.profile" -> app.profile
|
||||
"App.profile.studentData" -> app.profile.studentData
|
||||
"App.profile.loginStore" -> loginStore?.data ?: JsonObject()
|
||||
"App.config" -> app.config.values
|
||||
else -> when (obj) {
|
||||
is JsonObject -> (obj as JsonObject).get(el)
|
||||
is JsonArray -> (obj as JsonArray).get(el.toInt())
|
||||
is HashMap<*, *> -> (obj as HashMap<String, String?>)[el].toString()
|
||||
else -> {
|
||||
val field = obj::class.java.getDeclaredField(el)
|
||||
field.isAccessible = true
|
||||
field.get(obj) ?: return@forEach
|
||||
}
|
||||
}
|
||||
}
|
||||
objName = el
|
||||
}
|
||||
|
||||
val objVal = obj
|
||||
val value = when (objVal) {
|
||||
is JsonPrimitive -> when {
|
||||
objVal.isString -> objVal.asString
|
||||
objVal.isNumber -> objVal.asNumber.toString()
|
||||
objVal.isBoolean -> objVal.asBoolean.toString()
|
||||
else -> objVal.asString
|
||||
}
|
||||
else -> objVal.toString()
|
||||
}
|
||||
|
||||
MaterialDialog.Builder(activity)
|
||||
.input("value", value, false) { _, input ->
|
||||
val input = input.toString()
|
||||
when (parent) {
|
||||
is JsonObject -> {
|
||||
val v = objVal as JsonPrimitive
|
||||
when {
|
||||
v.isString -> (parent as JsonObject)[objName] = input
|
||||
v.isNumber -> (parent as JsonObject)[objName] = input.toLong()
|
||||
v.isBoolean -> (parent as JsonObject)[objName] = input.toBoolean()
|
||||
}
|
||||
}
|
||||
is JsonArray -> {
|
||||
|
||||
}
|
||||
is HashMap<*, *> -> app.config.set(objName, input)
|
||||
else -> {
|
||||
val field = parent::class.java.getDeclaredField(objName)
|
||||
field.isAccessible = true
|
||||
val newVal = when (objVal) {
|
||||
is Int -> input.toInt()
|
||||
is Boolean -> input.toBoolean()
|
||||
is Float -> input.toFloat()
|
||||
is Char -> input.toCharArray()[0]
|
||||
is String -> input
|
||||
is Long -> input.toLong()
|
||||
is Double -> input.toDouble()
|
||||
else -> input
|
||||
}
|
||||
field.set(parent, newVal)
|
||||
}
|
||||
}
|
||||
|
||||
when (item.key.substringBefore(":")) {
|
||||
"App.profile" -> app.profileSave()
|
||||
"App.profile.studentData" -> app.profileSave()
|
||||
"App.profile.loginStore" -> app.db.loginStoreDao().add(loginStore)
|
||||
}
|
||||
|
||||
showJson()
|
||||
|
||||
}
|
||||
.title(item.key)
|
||||
.positiveText(R.string.ok)
|
||||
.negativeText(R.string.cancel)
|
||||
.show()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
activity.error(ApiError.fromThrowable(TAG, e))
|
||||
}
|
||||
})
|
||||
|
||||
showJson()
|
||||
|
||||
b.list.adapter = adapter
|
||||
b.list.apply {
|
||||
@ -79,4 +155,15 @@ class LabProfileFragment : LazyFragment(), CoroutineScope {
|
||||
b.noData.isVisible = false
|
||||
|
||||
}; return true }
|
||||
|
||||
private fun showJson() {
|
||||
val json = JsonObject().also { json ->
|
||||
json.add("App.profile", app.gson.toJsonTree(app.profile))
|
||||
json.add("App.profile.studentData", app.profile.studentData)
|
||||
json.add("App.profile.loginStore", loginStore?.data ?: JsonObject())
|
||||
json.add("App.config", JsonParser().parse(app.gson.toJson(app.config.values)))
|
||||
}
|
||||
adapter.items = LabJsonAdapter.expand(json, 0)
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import androidx.core.view.isInvisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import pl.szczodrzynski.edziennik.App
|
||||
import pl.szczodrzynski.edziennik.databinding.LabItemObjectBinding
|
||||
import pl.szczodrzynski.edziennik.dp
|
||||
import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter
|
||||
import pl.szczodrzynski.edziennik.ui.modules.debug.LabJsonAdapter
|
||||
import pl.szczodrzynski.edziennik.ui.modules.debug.models.LabJsonArray
|
||||
@ -32,6 +33,10 @@ class JsonArrayViewHolder(
|
||||
override fun onBind(activity: AppCompatActivity, app: App, item: LabJsonArray, position: Int, adapter: LabJsonAdapter) {
|
||||
val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme)
|
||||
|
||||
b.root.setPadding(item.level * 8.dp + 8.dp, 8.dp, 8.dp, 8.dp)
|
||||
|
||||
b.type.text = "Array"
|
||||
|
||||
b.dropdownIcon.rotation = when (item.state) {
|
||||
AttendanceAdapter.STATE_CLOSED -> 0f
|
||||
else -> 180f
|
||||
@ -39,7 +44,7 @@ class JsonArrayViewHolder(
|
||||
b.previewContainer.isInvisible = item.state != AttendanceAdapter.STATE_CLOSED
|
||||
b.summaryContainer.isInvisible = item.state == AttendanceAdapter.STATE_CLOSED
|
||||
|
||||
b.key.text = item.key
|
||||
b.key.text = item.key.substringAfterLast(":")
|
||||
b.previewContainer.text = item.jsonArray.toString().take(200)
|
||||
b.summaryContainer.text = item.jsonArray.size().toString() + " elements"
|
||||
}
|
||||
|
@ -32,6 +32,8 @@ class JsonElementViewHolder(
|
||||
override fun onBind(activity: AppCompatActivity, app: App, item: LabJsonElement, position: Int, adapter: LabJsonAdapter) {
|
||||
val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme)
|
||||
|
||||
b.root.setPadding(item.level * 8.dp + 8.dp, 8.dp, 8.dp, 8.dp)
|
||||
|
||||
b.type.text = when (item.jsonElement) {
|
||||
is JsonPrimitive -> when {
|
||||
item.jsonElement.isNumber -> "Number"
|
||||
@ -45,7 +47,9 @@ class JsonElementViewHolder(
|
||||
|
||||
val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity)
|
||||
b.key.text = listOf(
|
||||
item.key.asColoredSpannable(colorSecondary),
|
||||
item.key
|
||||
.substringAfterLast(":")
|
||||
.asColoredSpannable(colorSecondary),
|
||||
": ",
|
||||
item.jsonElement.toString().asItalicSpannable()
|
||||
).concat("")
|
||||
|
@ -13,6 +13,7 @@ import androidx.core.view.isInvisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import pl.szczodrzynski.edziennik.App
|
||||
import pl.szczodrzynski.edziennik.databinding.LabItemObjectBinding
|
||||
import pl.szczodrzynski.edziennik.dp
|
||||
import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter
|
||||
import pl.szczodrzynski.edziennik.ui.modules.debug.LabJsonAdapter
|
||||
import pl.szczodrzynski.edziennik.ui.modules.debug.models.LabJsonObject
|
||||
@ -32,6 +33,10 @@ class JsonObjectViewHolder(
|
||||
override fun onBind(activity: AppCompatActivity, app: App, item: LabJsonObject, position: Int, adapter: LabJsonAdapter) {
|
||||
val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme)
|
||||
|
||||
b.root.setPadding(item.level * 8.dp + 8.dp, 8.dp, 8.dp, 8.dp)
|
||||
|
||||
b.type.text = "Object"
|
||||
|
||||
b.dropdownIcon.rotation = when (item.state) {
|
||||
AttendanceAdapter.STATE_CLOSED -> 0f
|
||||
else -> 180f
|
||||
@ -39,7 +44,7 @@ class JsonObjectViewHolder(
|
||||
b.previewContainer.isInvisible = item.state != AttendanceAdapter.STATE_CLOSED
|
||||
b.summaryContainer.isInvisible = item.state == AttendanceAdapter.STATE_CLOSED
|
||||
|
||||
b.key.text = item.key
|
||||
b.key.text = item.key.substringAfterLast(":")
|
||||
b.previewContainer.text = item.jsonObject.toString().take(200)
|
||||
b.summaryContainer.text = item.jsonObject.size().toString() + " elements"
|
||||
}
|
||||
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-2-26.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.modules.debug.viewholder
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import pl.szczodrzynski.edziennik.App
|
||||
import pl.szczodrzynski.edziennik.databinding.LabItemSubObjectBinding
|
||||
import pl.szczodrzynski.edziennik.dp
|
||||
import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter
|
||||
import pl.szczodrzynski.edziennik.ui.modules.debug.LabJsonAdapter
|
||||
import pl.szczodrzynski.edziennik.ui.modules.debug.models.LabJsonObject
|
||||
import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder
|
||||
import pl.szczodrzynski.edziennik.utils.Themes
|
||||
|
||||
class JsonSubObjectViewHolder(
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup,
|
||||
val b: LabItemSubObjectBinding = LabItemSubObjectBinding.inflate(inflater, parent, false)
|
||||
) : RecyclerView.ViewHolder(b.root), BindableViewHolder<LabJsonObject, LabJsonAdapter> {
|
||||
companion object {
|
||||
private const val TAG = "JsonSubObjectViewHolder"
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(activity: AppCompatActivity, app: App, item: LabJsonObject, position: Int, adapter: LabJsonAdapter) {
|
||||
val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme)
|
||||
|
||||
b.root.setPadding(item.level * 8.dp + 8.dp, 8.dp, 8.dp, 8.dp)
|
||||
|
||||
b.type.text = "Object"
|
||||
|
||||
b.dropdownIcon.rotation = when (item.state) {
|
||||
AttendanceAdapter.STATE_CLOSED -> 0f
|
||||
else -> 180f
|
||||
}
|
||||
|
||||
b.key.text = item.key.substringAfterLast(":")
|
||||
}
|
||||
}
|
@ -30,11 +30,12 @@
|
||||
tools:text="lessonRanges" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:fontFamily="monospace"
|
||||
android:text="JsonObject"
|
||||
android:text="Object"
|
||||
android:textAppearance="@style/NavView.TextView.Helper" />
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsImageView
|
||||
|
48
app/src/main/res/layout/lab_item_sub_object.xml
Normal file
48
app/src/main/res/layout/lab_item_sub_object.xml
Normal file
@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (c) Kuba Szczodrzyński 2021-2-26.
|
||||
-->
|
||||
|
||||
<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="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:baselineAligned="false"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/key"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="monospace"
|
||||
android:maxLines="2"
|
||||
tools:text="lessonRanges" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:fontFamily="monospace"
|
||||
android:text="Object"
|
||||
android:textAppearance="@style/NavView.TextView.Helper" />
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsImageView
|
||||
android:id="@+id/dropdownIcon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:scaleType="centerInside"
|
||||
app:iiv_color="?android:textColorSecondary"
|
||||
app:iiv_icon="cmd-chevron-down"
|
||||
app:iiv_size="18dp"
|
||||
tools:src="@android:drawable/ic_menu_more" />
|
||||
</LinearLayout>
|
||||
</layout>
|
Loading…
Reference in New Issue
Block a user