diff --git a/changelog.d/4796.bugfix b/changelog.d/4796.bugfix
new file mode 100644
index 0000000000..df5b5a4eba
--- /dev/null
+++ b/changelog.d/4796.bugfix
@@ -0,0 +1 @@
+Fix crash when viewing source which contains an emoji
\ No newline at end of file
diff --git a/dependencies.gradle b/dependencies.gradle
index 6cb5fac64c..3fb47ba711 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -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 : [
diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle
index 3853919bcb..fd36f5110c 100644
--- a/dependencies_groups.gradle
+++ b/dependencies_groups.gradle
@@ -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',
diff --git a/library/jsonviewer/.gitignore b/library/jsonviewer/.gitignore
new file mode 100644
index 0000000000..796b96d1c4
--- /dev/null
+++ b/library/jsonviewer/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/library/jsonviewer/build.gradle b/library/jsonviewer/build.gradle
new file mode 100644
index 0000000000..ee2be6fd25
--- /dev/null
+++ b/library/jsonviewer/build.gradle
@@ -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
+}
diff --git a/library/jsonviewer/src/main/AndroidManifest.xml b/library/jsonviewer/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..73322c2fdb
--- /dev/null
+++ b/library/jsonviewer/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerDialog.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerDialog.kt
new file mode 100644
index 0000000000..a8d9cac849
--- /dev/null
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerDialog.kt
@@ -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 }
+ }
+ }
+}
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt
new file mode 100644
index 0000000000..1ff35f5005
--- /dev/null
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt
@@ -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() {
+
+ 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()
+ )
+ }
+ }
+}
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerFragment.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerFragment.kt
new file mode 100644
index 0000000000..a8aa2493d2
--- /dev/null
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerFragment.kt
@@ -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
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerModel.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerModel.kt
new file mode 100644
index 0000000000..3850044880
--- /dev/null
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerModel.kt
@@ -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()
+
+ 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()
+
+ 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)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerStyleProvider.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerStyleProvider.kt
new file mode 100644
index 0000000000..956e585f80
--- /dev/null
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerStyleProvider.kt
@@ -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)
+ )
+ }
+}
+
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerViewModel.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerViewModel.kt
new file mode 100644
index 0000000000..d978573a64
--- /dev/null
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerViewModel.kt
@@ -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 = Uninitialized
+) : MavericksState
+
+internal class JSonViewerViewModel(initialState: JSonViewerState) :
+ MavericksViewModel(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 {
+
+ @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))
+ }
+
+ }
+ }
+}
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/SafeCharSequence.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/SafeCharSequence.kt
new file mode 100644
index 0000000000..79556f81d7
--- /dev/null
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/SafeCharSequence.kt
@@ -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)
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt
new file mode 100644
index 0000000000..6536a3401e
--- /dev/null
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt
@@ -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()
+ }
+}
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt
new file mode 100644
index 0000000000..5c003c97e9
--- /dev/null
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt
@@ -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() {
+
+ @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
+ }
+ }
+ }
+ }
+}
diff --git a/library/jsonviewer/src/main/res/layout/fragment_dialog_jv.xml b/library/jsonviewer/src/main/res/layout/fragment_dialog_jv.xml
new file mode 100644
index 0000000000..fb9e6d38c5
--- /dev/null
+++ b/library/jsonviewer/src/main/res/layout/fragment_dialog_jv.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view.xml b/library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view.xml
new file mode 100644
index 0000000000..20822191e6
--- /dev/null
+++ b/library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view_wrap.xml b/library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view_wrap.xml
new file mode 100644
index 0000000000..8b61b13111
--- /dev/null
+++ b/library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view_wrap.xml
@@ -0,0 +1,10 @@
+
+
diff --git a/library/jsonviewer/src/main/res/layout/item_jv_base_value.xml b/library/jsonviewer/src/main/res/layout/item_jv_base_value.xml
new file mode 100644
index 0000000000..b7dee1221b
--- /dev/null
+++ b/library/jsonviewer/src/main/res/layout/item_jv_base_value.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/library/jsonviewer/src/main/res/menu/jv_menu_item.xml b/library/jsonviewer/src/main/res/menu/jv_menu_item.xml
new file mode 100644
index 0000000000..4da69b5117
--- /dev/null
+++ b/library/jsonviewer/src/main/res/menu/jv_menu_item.xml
@@ -0,0 +1,8 @@
+
+
diff --git a/library/jsonviewer/src/main/res/values/colors.xml b/library/jsonviewer/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..7b92899918
--- /dev/null
+++ b/library/jsonviewer/src/main/res/values/colors.xml
@@ -0,0 +1,11 @@
+
+
+
+ #FF006700
+ #FF040091
+ #FF980000
+ #FF1700FF
+ #FF000000
+ #FFAAAAAA
+
+
diff --git a/library/jsonviewer/src/main/res/values/strings.xml b/library/jsonviewer/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..cc4b8726b4
--- /dev/null
+++ b/library/jsonviewer/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Copy Value
+
diff --git a/library/jsonviewer/src/test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt b/library/jsonviewer/src/test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt
new file mode 100644
index 0000000000..4caf9ce958
--- /dev/null
+++ b/library/jsonviewer/src/test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt
@@ -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)
+
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index e3b84b4733..7ba66c7cb1 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -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'
diff --git a/vector/build.gradle b/vector/build.gradle
index f136543a2e..fad04ffabe 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -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