Fix crash when viewing source which contains an emoji.

Import source of jsonviewer as a module of this project.
This commit is contained in:
Benoit Marty 2022-01-18 12:04:06 +01:00
parent f5b16b834c
commit bdd30e3b8f
25 changed files with 1072 additions and 2 deletions

1
changelog.d/4796.bugfix Normal file
View file

@ -0,0 +1 @@
Fix crash when viewing source which contains an emoji

View file

@ -71,6 +71,7 @@ ext.libs = [
'espressoIntents' : "androidx.test.espresso:espresso-intents:$espresso" 'espressoIntents' : "androidx.test.espresso:espresso-intents:$espresso"
], ],
google : [ google : [
// TODO There is 1.6.0?
'material' : "com.google.android.material:material:1.4.0" 'material' : "com.google.android.material:material:1.4.0"
], ],
dagger : [ dagger : [

View file

@ -4,7 +4,6 @@ ext.groups = [
], ],
group: [ group: [
'com.github.Armen101', 'com.github.Armen101',
'com.github.BillCarsonFr',
'com.github.chrisbanes', 'com.github.chrisbanes',
'com.github.hyuwah', 'com.github.hyuwah',
'com.github.jetradarmobile', 'com.github.jetradarmobile',
@ -154,6 +153,7 @@ ext.groups = [
'org.jetbrains.intellij.deps', 'org.jetbrains.intellij.deps',
'org.jetbrains.kotlin', 'org.jetbrains.kotlin',
'org.jetbrains.kotlinx', 'org.jetbrains.kotlinx',
'org.json',
'org.jsoup', 'org.jsoup',
'org.junit', 'org.junit',
'org.junit.jupiter', 'org.junit.jupiter',

1
library/jsonviewer/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,64 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.jakewharton.butterknife'
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
}
}
android {
compileSdk versions.compileSdk
defaultConfig {
minSdk versions.minSdk
targetSdk versions.targetSdk
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility versions.sourceCompat
targetCompatibility versions.targetCompat
}
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
implementation libs.androidx.appCompat
implementation libs.androidx.core
implementation libs.airbnb.epoxy
kapt libs.airbnb.epoxyProcessor
implementation libs.airbnb.mavericks
// Span utils
implementation 'me.gujun.android:span:1.7'
implementation libs.google.material
implementation libs.jetbrains.coroutinesCore
implementation libs.jetbrains.coroutinesAndroid
testImplementation 'org.json:json:20190722'
testImplementation libs.tests.junit
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.espressoCore
}

View file

@ -0,0 +1 @@
<manifest package="org.billcarsonfr.jsonviewer" />

View file

@ -0,0 +1,77 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.billcarsonfr.jsonviewer
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.fragment.app.DialogFragment
import com.airbnb.mvrx.Mavericks
class JSonViewerDialog : DialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_dialog_jv, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val args: JSonViewerFragmentArgs = arguments?.getParcelable(Mavericks.KEY_ARG) ?: return
if (savedInstanceState == null) {
childFragmentManager.beginTransaction()
.replace(
R.id.fragmentContainer, JSonViewerFragment.newInstance(
args.jsonString,
args.defaultOpenDepth,
true,
args.styleProvider
)
)
.commitNow()
}
}
override fun onResume() {
super.onResume()
// Get existing layout params for the window
val params = dialog?.window?.attributes
// Assign window properties to fill the parent
params?.width = WindowManager.LayoutParams.MATCH_PARENT
params?.height = WindowManager.LayoutParams.MATCH_PARENT
dialog?.window?.attributes = params
}
companion object {
fun newInstance(
jsonString: String,
initialOpenDepth: Int = -1,
styleProvider: JSonViewerStyleProvider? = null
): JSonViewerDialog {
val args = Bundle()
val parcelableArgs =
JSonViewerFragmentArgs(jsonString, initialOpenDepth, false, styleProvider)
args.putParcelable(Mavericks.KEY_ARG, parcelableArgs)
return JSonViewerDialog().apply { arguments = args }
}
}
}

View file

@ -0,0 +1,261 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.billcarsonfr.jsonviewer
import android.content.Context
import android.view.View
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Success
import me.gujun.android.span.Span
import me.gujun.android.span.span
internal class JSonViewerEpoxyController(private val context: Context) :
TypedEpoxyController<JSonViewerState>() {
private var styleProvider: JSonViewerStyleProvider = JSonViewerStyleProvider.default(context)
fun setStyle(styleProvider: JSonViewerStyleProvider?) {
this.styleProvider = styleProvider ?: JSonViewerStyleProvider.default(context)
}
override fun buildModels(data: JSonViewerState?) {
val async = data?.root ?: return
when (async) {
is Fail -> {
valueItem {
id("fail")
text(async.error.localizedMessage?.toSafeCharSequence())
}
}
is Success -> {
val model = data.root.invoke()
model?.let {
buildRec(it, 0, "")
}
}
}
}
private fun buildRec(
model: JSonViewerModel,
depth: Int,
idBase: String
) {
val host = this
val id = "$idBase/${model.key ?: model.index}_${model.isExpanded}}"
when (model) {
is JSonViewerObject -> {
if (model.isExpanded) {
open(id, model.key, model.index, depth, true, model)
model.keys.forEach {
buildRec(it.value, depth + 1, id)
}
close(id, depth, true)
} else {
valueItem {
id(id + "_sum")
depth(depth)
text(
span {
if (model.key != null) {
span("\"${model.key}\"") {
textColor = host.styleProvider.keyColor
}
span(" : ") {
textColor = host.styleProvider.baseColor
}
}
if (model.index != null) {
span("${model.index}") {
textColor = host.styleProvider.secondaryColor
}
span(" : ") {
textColor = host.styleProvider.baseColor
}
}
span {
+"{+${model.keys.size}}"
textColor = host.styleProvider.baseColor
}
}.toSafeCharSequence()
)
itemClickListener(View.OnClickListener { host.itemClicked(model) })
}
}
}
is JSonViewerArray -> {
if (model.isExpanded) {
open(id, model.key, model.index, depth, false, model)
model.items.forEach {
buildRec(it, depth + 1, id)
}
close(id, depth, false)
} else {
valueItem {
id(id + "_sum")
depth(depth)
text(
span {
if (model.key != null) {
span("\"${model.key}\"") {
textColor = host.styleProvider.keyColor
}
span(" : ") {
textColor = host.styleProvider.baseColor
}
}
if (model.index != null) {
span("${model.index}") {
textColor = host.styleProvider.secondaryColor
}
span(" : ") {
textColor = host.styleProvider.baseColor
}
}
span {
+"[+${model.items.size}]"
textColor = host.styleProvider.baseColor
}
}.toSafeCharSequence()
)
itemClickListener(View.OnClickListener { host.itemClicked(model) })
}
}
}
is JSonViewerLeaf -> {
valueItem {
id(id)
depth(depth)
text(
span {
if (model.key != null) {
span("\"${model.key}\"") {
textColor = host.styleProvider.keyColor
}
span(" : ") {
textColor = host.styleProvider.baseColor
}
}
if (model.index != null) {
span("${model.index}") {
textColor = host.styleProvider.secondaryColor
}
span(" : ") {
textColor = host.styleProvider.baseColor
}
}
append(host.valueToSpan(model))
}.toSafeCharSequence()
)
copyValue(model.stringRes)
}
}
}
}
private fun valueToSpan(leaf: JSonViewerLeaf): Span {
val host = this
return when (leaf.type) {
JSONType.STRING -> {
span("\"${leaf.stringRes}\"") {
textColor = host.styleProvider.stringColor
}
}
JSONType.NUMBER -> {
span(leaf.stringRes) {
textColor = host.styleProvider.numberColor
}
}
JSONType.BOOLEAN -> {
span(leaf.stringRes) {
textColor = host.styleProvider.booleanColor
}
}
JSONType.NULL -> {
span("null") {
textColor = host.styleProvider.booleanColor
}
}
}
}
private fun open(
id: String,
key: String?,
index: Int?,
depth: Int,
isObject: Boolean = true,
composed: JSonViewerModel
) {
val host = this
valueItem {
id("${id}_Open")
depth(depth)
text(
span {
if (key != null) {
span("\"$key\"") {
textColor = host.styleProvider.keyColor
}
span(" : ") {
textColor = host.styleProvider.baseColor
}
}
if (index != null) {
span("$index") {
textColor = host.styleProvider.secondaryColor
}
span(" : ") {
textColor = host.styleProvider.baseColor
}
}
span("- ") {
textColor = host.styleProvider.secondaryColor
}
span("{".takeIf { isObject } ?: "[") {
textColor = host.styleProvider.baseColor
}
}.toSafeCharSequence()
)
itemClickListener(View.OnClickListener { host.itemClicked(composed) })
}
}
private fun itemClicked(model: JSonViewerModel) {
model.isExpanded = !model.isExpanded
setData(currentData)
}
private fun close(id: String, depth: Int, isObject: Boolean = true) {
val host = this
valueItem {
id("${id}_Close")
depth(depth)
text(
span {
text = "}".takeIf { isObject } ?: "]"
textColor = host.styleProvider.baseColor
}.toSafeCharSequence()
)
}
}
}

View file

@ -0,0 +1,103 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.billcarsonfr.jsonviewer
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.epoxy.EpoxyRecyclerView
import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.MavericksView
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import kotlinx.parcelize.Parcelize
@Parcelize
internal data class JSonViewerFragmentArgs(
val jsonString: String,
val defaultOpenDepth: Int,
val wrap: Boolean,
val styleProvider: JSonViewerStyleProvider?
) : Parcelable
class JSonViewerFragment : Fragment(), MavericksView {
private val viewModel: JSonViewerViewModel by fragmentViewModel()
private val epoxyController by lazy {
JSonViewerEpoxyController(requireContext())
}
private lateinit var recyclerView: EpoxyRecyclerView
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val args: JSonViewerFragmentArgs? = arguments?.getParcelable(Mavericks.KEY_ARG)
val inflate =
if (args?.wrap == true) {
inflater.inflate(R.layout.fragment_jv_recycler_view_wrap, container, false)
} else {
inflater.inflate(R.layout.fragment_jv_recycler_view, container, false)
}
recyclerView = inflate.findViewById(R.id.jvRecyclerView)
recyclerView.layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
recyclerView.setController(epoxyController)
epoxyController.setStyle(args?.styleProvider)
registerForContextMenu(recyclerView)
return inflate
}
fun showJson(jsonString: String, initialOpenDepth: Int) {
viewModel.setJsonSource(jsonString, initialOpenDepth)
}
override fun invalidate() = withState(viewModel) { state ->
epoxyController.setData(state)
}
companion object {
fun newInstance(
jsonString: String,
initialOpenDepth: Int = -1,
wrap: Boolean = false,
styleProvider: JSonViewerStyleProvider? = null
): JSonViewerFragment {
return JSonViewerFragment().apply {
arguments = Bundle().apply {
putParcelable(
Mavericks.KEY_ARG,
JSonViewerFragmentArgs(
jsonString,
initialOpenDepth,
wrap,
styleProvider
)
)
}
}
}
}
}

View file

@ -0,0 +1,124 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.billcarsonfr.jsonviewer
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
internal open class JSonViewerModel(var key: String?, var index: Int?, val jObject: Any) {
var depth = 0
var isExpanded = false
}
internal interface Composed {
fun addChild(model: JSonViewerModel)
}
internal class JSonViewerObject(key: String?, index: Int?, jObject: JSONObject) :
JSonViewerModel(key, index, jObject),
Composed {
var keys = LinkedHashMap<String, JSonViewerModel>()
override fun addChild(model: JSonViewerModel) {
keys[model.key!!] = model
}
}
internal class JSonViewerArray(key: String?, index: Int?, jObject: JSONArray) :
JSonViewerModel(key, index, jObject), Composed {
var items = ArrayList<JSonViewerModel>()
override fun addChild(model: JSonViewerModel) {
items.add(model)
}
}
internal class JSonViewerLeaf(key: String?, index: Int?, val stringRes: String, val type: JSONType) :
JSonViewerModel(key, index, stringRes)
internal enum class JSONType {
STRING,
NUMBER,
BOOLEAN,
NULL
}
internal object ModelParser {
@Throws(JSONException::class)
fun fromJsonString(jsonString: String, initialOpenDepth: Int = -1): JSonViewerObject {
val jobj = JSONObject(jsonString.trim())
val root = JSonViewerObject(null, null, jobj).apply { isExpanded = true }
jobj.keys().forEach {
eval(root, it, null, jobj.get(it), 1, initialOpenDepth)
}
return root
}
private fun eval(parent: Composed, key: String?, index: Int?, obj: Any, depth: Int, initialOpenDepth: Int) {
when (obj) {
is JSONObject -> {
val objectComposed = JSonViewerObject(key, index, obj)
.apply { isExpanded = initialOpenDepth == -1 || depth <= initialOpenDepth }
objectComposed.depth = depth
obj.keys().forEach {
eval(objectComposed, it, null, obj.get(it), depth + 1, initialOpenDepth)
}
parent.addChild(objectComposed)
}
is JSONArray -> {
val objectComposed = JSonViewerArray(key, index, obj)
.apply { isExpanded = initialOpenDepth == -1 || depth <= initialOpenDepth }
objectComposed.depth = depth
for (i in 0 until obj.length()) {
eval(objectComposed, null, i, obj[i], depth + 1, initialOpenDepth)
}
parent.addChild(objectComposed)
}
is String -> {
JSonViewerLeaf(key, index, obj, JSONType.STRING).let {
it.depth = depth
parent.addChild(it)
}
}
is Number -> {
JSonViewerLeaf(key, index, obj.toString(), JSONType.NUMBER).let {
it.depth = depth
parent.addChild(it)
}
}
is Boolean -> {
JSonViewerLeaf(key, index, obj.toString(), JSONType.BOOLEAN).let {
it.depth = depth
parent.addChild(it)
}
}
else -> {
if (obj == JSONObject.NULL) {
JSonViewerLeaf(key, index, "null", JSONType.NULL).let {
it.depth = depth
parent.addChild(it)
}
}
}
}
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.billcarsonfr.jsonviewer
import android.content.Context
import android.os.Parcelable
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import kotlinx.parcelize.Parcelize
@Parcelize
data class JSonViewerStyleProvider(
@ColorInt val keyColor: Int,
@ColorInt val stringColor: Int,
@ColorInt val booleanColor: Int,
@ColorInt val numberColor: Int,
@ColorInt val baseColor: Int,
@ColorInt val secondaryColor: Int
) : Parcelable {
companion object {
fun default(context: Context) = JSonViewerStyleProvider(
keyColor = ContextCompat.getColor(context, R.color.key_color),
stringColor = ContextCompat.getColor(context, R.color.string_color),
booleanColor = ContextCompat.getColor(context, R.color.bool_color),
numberColor = ContextCompat.getColor(context, R.color.number_color),
baseColor = ContextCompat.getColor(context, R.color.base_color),
secondaryColor = ContextCompat.getColor(context, R.color.secondary_color)
)
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.billcarsonfr.jsonviewer
import com.airbnb.mvrx.*
import kotlinx.coroutines.launch
internal data class JSonViewerState(
val root: Async<JSonViewerObject> = Uninitialized
) : MavericksState
internal class JSonViewerViewModel(initialState: JSonViewerState) :
MavericksViewModel<JSonViewerState>(initialState) {
fun setJsonSource(json: String, initialOpenDepth: Int) {
setState {
copy(root = Loading())
}
viewModelScope.launch {
try {
ModelParser.fromJsonString(json, initialOpenDepth).let {
setState {
copy(
root = Success(it)
)
}
}
} catch (error: Throwable) {
setState {
copy(
root = Fail(error)
)
}
}
}
}
companion object : MavericksViewModelFactory<JSonViewerViewModel, JSonViewerState> {
@JvmStatic
override fun initialState(viewModelContext: ViewModelContext): JSonViewerState? {
val arg: JSonViewerFragmentArgs = viewModelContext.args()
return try {
JSonViewerState(
Success(ModelParser.fromJsonString(arg.jsonString, arg.defaultOpenDepth))
)
} catch (failure: Throwable) {
JSonViewerState(Fail(failure))
}
}
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.billcarsonfr.jsonviewer
/**
* Wrapper for a CharSequence, which support mutation of the CharSequence, which can happen during rendering
* TODO Mutualize
*/
internal class SafeCharSequence(val charSequence: CharSequence) {
private val hash = charSequence.toString().hashCode()
override fun hashCode() = hash
override fun equals(other: Any?) = other is SafeCharSequence && other.hash == hash
}
internal fun CharSequence.toSafeCharSequence() = SafeCharSequence(this)

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.billcarsonfr.jsonviewer
import android.content.Context
import android.util.TypedValue
/**
* TODO Mutualize
*/
internal object Utils {
fun dpToPx(dp: Int, context: Context): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp.toFloat(),
context.resources.displayMetrics
).toInt()
}
}

View file

@ -0,0 +1,93 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.billcarsonfr.jsonviewer
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.view.ContextMenu
import android.view.Menu
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyHolder
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
@EpoxyModelClass(layout = R2.layout.item_jv_base_value)
internal abstract class ValueItem : EpoxyModelWithHolder<ValueItem.Holder>() {
@EpoxyAttribute
var text: SafeCharSequence? = null
@EpoxyAttribute
var depth: Int = 0
@EpoxyAttribute
var copyValue: String? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var itemClickListener: View.OnClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.textView.text = text?.charSequence
holder.baseView.setPadding(Utils.dpToPx(16 * depth, holder.baseView.context), 0, 0, 0)
itemClickListener?.let { holder.baseView.setOnClickListener(it) }
holder.copyValue = copyValue
}
override fun unbind(holder: Holder) {
super.unbind(holder)
holder.baseView.setOnClickListener(null)
holder.copyValue = null
}
class Holder : EpoxyHolder(), View.OnCreateContextMenuListener {
lateinit var textView: TextView
lateinit var baseView: LinearLayout
var copyValue: String? = null
override fun bindView(itemView: View) {
baseView = itemView.findViewById(R.id.jvBaseLayout)
textView = itemView.findViewById(R.id.jvValueText)
itemView.setOnCreateContextMenuListener(this)
}
override fun onCreateContextMenu(
menu: ContextMenu?,
v: View?,
menuInfo: ContextMenu.ContextMenuInfo?
) {
if (copyValue != null) {
val menuItem = menu?.add(
Menu.NONE, R.id.copy_value,
Menu.NONE, R.string.copy_value
)
val clipService =
v?.context?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
menuItem?.setOnMenuItemClickListener {
clipService?.setPrimaryClip(ClipData.newPlainText("", copyValue))
true
}
}
}
}
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:orientation="vertical">
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/jvRecyclerView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:fadeScrollbars="false"
android:scrollbars="vertical"
tools:itemCount="5"
tools:listitem="@layout/item_jv_base_value" />
</HorizontalScrollView>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<com.airbnb.epoxy.EpoxyRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/jvRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fadeScrollbars="false"
android:scrollbars="vertical"
tools:itemCount="5"
tools:listitem="@layout/item_jv_base_value" />

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/jvBaseLayout"
android:background="?attr/selectableItemBackground"
tools:paddingLeft="16dp">
<TextView
android:id="@+id/jvValueText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="&quot;Title&quot;: &quot;example glossary&quot;" />
</LinearLayout>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/copy_value"
android:title="@string/copy_value" />
</menu>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="key_color">#FF006700</color>
<color name="string_color">#FF040091</color>
<color name="bool_color">#FF980000</color>
<color name="number_color">#FF1700FF</color>
<color name="base_color">#FF000000</color>
<color name="secondary_color">#FFAAAAAA</color>
</resources>

View file

@ -0,0 +1,3 @@
<resources>
<string name="copy_value">Copy Value</string>
</resources>

View file

@ -0,0 +1,96 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.billcarsonfr.jsonviewer
import org.junit.Assert
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ModelParseTest {
@Test
fun parsing_isCorrect() {
val string = """
{
"glossary": {
"title": "example glossary",
"GlossDiv": {
"title": "S",
"GlossList": {
"GlossEntry": {
"ID": "SGML",
"SortAs": "SGML",
"GlossTerm": "Standard Generalized Markup Language",
"Acronym": "SGML",
"Abbrev": "ISO 8879:1986",
"GlossDef": {
"para": "A meta-markup language, used to create markup languages such as DocBook.",
"GlossSeeAlso": ["GML", "XML"]
},
"GlossSee": "markup"
}
}
}
}
}
""".trim()
val model = ModelParser.fromJsonString(string)
Assert.assertEquals(0, model.depth)
Assert.assertEquals(1, model.keys.size)
Assert.assertTrue(model.keys.containsKey("glossary"))
Assert.assertTrue(model.keys["glossary"] is JSonViewerObject)
val glossary = model.keys["glossary"] as JSonViewerObject
Assert.assertEquals(2, glossary.keys.size)
Assert.assertTrue(glossary.keys.containsKey("title"))
Assert.assertTrue(glossary.keys.containsKey("GlossDiv"))
Assert.assertTrue(glossary.keys["title"] is JSonViewerLeaf)
(glossary.keys["title"] as JSonViewerLeaf).let {
Assert.assertEquals(JSONType.STRING, it.type)
}
Assert.assertTrue(glossary.keys["GlossDiv"] is JSonViewerObject)
val glossDiv = glossary.keys["GlossDiv"] as JSonViewerObject
Assert.assertTrue(glossDiv.keys["GlossList"] is JSonViewerObject)
val glossList = glossDiv.keys["GlossList"] as JSonViewerObject
Assert.assertTrue(glossList.keys["GlossEntry"] is JSonViewerObject)
val glossEntry = glossList.keys["GlossEntry"] as JSonViewerObject
Assert.assertTrue(glossEntry.keys["GlossDef"] is JSonViewerObject)
val glossDef = glossEntry.keys["GlossDef"] as JSonViewerObject
Assert.assertTrue(glossDef.keys["GlossSeeAlso"] is JSonViewerArray)
val glossSeeAlso = glossDef.keys["GlossSeeAlso"] as JSonViewerArray
Assert.assertEquals(2, glossSeeAlso.items.size)
Assert.assertEquals("0", glossSeeAlso.items.first().key)
Assert.assertEquals("GML", (glossSeeAlso.items.first() as JSonViewerLeaf).stringRes)
}
}

View file

@ -5,4 +5,5 @@ include ':diff-match-patch'
include ':attachment-viewer' include ':attachment-viewer'
include ':multipicker' include ':multipicker'
include ':library:ui-styles' include ':library:ui-styles'
include ':library:jsonviewer'
include ':matrix-sdk-android-flow' include ':matrix-sdk-android-flow'

View file

@ -326,6 +326,7 @@ dependencies {
implementation project(":diff-match-patch") implementation project(":diff-match-patch")
implementation project(":multipicker") implementation project(":multipicker")
implementation project(":attachment-viewer") implementation project(":attachment-viewer")
implementation project(":library:jsonviewer")
implementation project(":library:ui-styles") implementation project(":library:ui-styles")
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
@ -458,7 +459,6 @@ dependencies {
gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
implementation "androidx.emoji2:emoji2:1.0.1" implementation "androidx.emoji2:emoji2:1.0.1"
implementation('com.github.BillCarsonFr:JsonViewer:0.7')
// WebRTC // WebRTC
// org.webrtc:google-webrtc is for development purposes only // org.webrtc:google-webrtc is for development purposes only