Fix crash when viewing source which contains an emoji.
Import source of jsonviewer as a module of this project.
This commit is contained in:
parent
f5b16b834c
commit
bdd30e3b8f
|
@ -0,0 +1 @@
|
|||
Fix crash when viewing source which contains an emoji
|
|
@ -71,6 +71,7 @@ ext.libs = [
|
|||
'espressoIntents' : "androidx.test.espresso:espresso-intents:$espresso"
|
||||
],
|
||||
google : [
|
||||
// TODO There is 1.6.0?
|
||||
'material' : "com.google.android.material:material:1.4.0"
|
||||
],
|
||||
dagger : [
|
||||
|
|
|
@ -4,7 +4,6 @@ ext.groups = [
|
|||
],
|
||||
group: [
|
||||
'com.github.Armen101',
|
||||
'com.github.BillCarsonFr',
|
||||
'com.github.chrisbanes',
|
||||
'com.github.hyuwah',
|
||||
'com.github.jetradarmobile',
|
||||
|
@ -154,6 +153,7 @@ ext.groups = [
|
|||
'org.jetbrains.intellij.deps',
|
||||
'org.jetbrains.kotlin',
|
||||
'org.jetbrains.kotlinx',
|
||||
'org.json',
|
||||
'org.jsoup',
|
||||
'org.junit',
|
||||
'org.junit.jupiter',
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<manifest package="org.billcarsonfr.jsonviewer" />
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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" />
|
|
@ -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>
|
|
@ -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" />
|
|
@ -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=""Title": "example glossary"" />
|
||||
|
||||
</LinearLayout>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="copy_value">Copy Value</string>
|
||||
</resources>
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
|
@ -5,4 +5,5 @@ include ':diff-match-patch'
|
|||
include ':attachment-viewer'
|
||||
include ':multipicker'
|
||||
include ':library:ui-styles'
|
||||
include ':library:jsonviewer'
|
||||
include ':matrix-sdk-android-flow'
|
||||
|
|
|
@ -326,6 +326,7 @@ dependencies {
|
|||
implementation project(":diff-match-patch")
|
||||
implementation project(":multipicker")
|
||||
implementation project(":attachment-viewer")
|
||||
implementation project(":library:jsonviewer")
|
||||
implementation project(":library:ui-styles")
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
|
||||
|
@ -458,7 +459,6 @@ dependencies {
|
|||
gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
|
||||
|
||||
implementation "androidx.emoji2:emoji2:1.0.1"
|
||||
implementation('com.github.BillCarsonFr:JsonViewer:0.7')
|
||||
|
||||
// WebRTC
|
||||
// org.webrtc:google-webrtc is for development purposes only
|
||||
|
|
Loading…
Reference in New Issue