mirror of
https://github.com/element-hq/element-android
synced 2024-09-06 17:05:20 +00:00
Merge branch hotfix/1.3.15
This commit is contained in:
commit
194a584e4c
11
CHANGES.md
11
CHANGES.md
|
@ -1,3 +1,14 @@
|
||||||
|
Changes in Element v1.3.15 (2022-01-18)
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- Fix crash when viewing source which contains an emoji ([#4796](https://github.com/vector-im/element-android/issues/4796))
|
||||||
|
- Prevent crash in Timeline and add more logs. ([#4959](https://github.com/vector-im/element-android/issues/4959))
|
||||||
|
- Fix crash on API <24 and make sure this error will not occur again. ([#4962](https://github.com/vector-im/element-android/issues/4962))
|
||||||
|
- Fixes sign in/up crash when selecting ems and other server types which use SSO ([#4969](https://github.com/vector-im/element-android/issues/4969))
|
||||||
|
|
||||||
|
|
||||||
Changes in Element v1.3.14 (2022-01-12)
|
Changes in Element v1.3.14 (2022-01-12)
|
||||||
=======================================
|
=======================================
|
||||||
|
|
||||||
|
|
|
@ -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 : [
|
||||||
|
|
|
@ -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',
|
||||||
|
|
2
fastlane/metadata/android/en-US/changelogs/40103150.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40103150.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: First change in onboarding screens, including Analytics opt-in. Support for Events with Math added in the labs.
|
||||||
|
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.15
|
1
library/jsonviewer/.gitignore
vendored
Normal file
1
library/jsonviewer/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
64
library/jsonviewer/build.gradle
Normal file
64
library/jsonviewer/build.gradle
Normal 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
|
||||||
|
}
|
1
library/jsonviewer/src/main/AndroidManifest.xml
Normal file
1
library/jsonviewer/src/main/AndroidManifest.xml
Normal file
|
@ -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,260 @@
|
||||||
|
/*
|
||||||
|
* 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,102 @@
|
||||||
|
/*
|
||||||
|
* 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,122 @@
|
||||||
|
/*
|
||||||
|
* 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,45 @@
|
||||||
|
/*
|
||||||
|
* 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,74 @@
|
||||||
|
/*
|
||||||
|
* 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.Async
|
||||||
|
import com.airbnb.mvrx.Fail
|
||||||
|
import com.airbnb.mvrx.Loading
|
||||||
|
import com.airbnb.mvrx.MavericksState
|
||||||
|
import com.airbnb.mvrx.MavericksViewModel
|
||||||
|
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||||
|
import com.airbnb.mvrx.Success
|
||||||
|
import com.airbnb.mvrx.Uninitialized
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
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,92 @@
|
||||||
|
/*
|
||||||
|
* 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>
|
8
library/jsonviewer/src/main/res/menu/jv_menu_item.xml
Normal file
8
library/jsonviewer/src/main/res/menu/jv_menu_item.xml
Normal 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>
|
11
library/jsonviewer/src/main/res/values/colors.xml
Normal file
11
library/jsonviewer/src/main/res/values/colors.xml
Normal 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>
|
3
library/jsonviewer/src/main/res/values/strings.xml
Normal file
3
library/jsonviewer/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<resources>
|
||||||
|
<string name="copy_value">Copy Value</string>
|
||||||
|
</resources>
|
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* 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().index)
|
||||||
|
Assert.assertNull(glossSeeAlso.items.first().key)
|
||||||
|
Assert.assertEquals("GML", (glossSeeAlso.items.first() as JSonViewerLeaf).stringRes)
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,4 +5,5 @@ include ':attachment-viewer'
|
||||||
include ':multipicker'
|
include ':multipicker'
|
||||||
include ':library:core-utils'
|
include ':library:core-utils'
|
||||||
include ':library:ui-styles'
|
include ':library:ui-styles'
|
||||||
|
include ':library:jsonviewer'
|
||||||
include ':matrix-sdk-android-flow'
|
include ':matrix-sdk-android-flow'
|
||||||
|
|
|
@ -332,6 +332,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 project(":library:core-utils")
|
implementation project(":library:core-utils")
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
|
@ -465,7 +466,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
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
<issue id="RtlSymmetry" severity="error" />
|
<issue id="RtlSymmetry" severity="error" />
|
||||||
|
|
||||||
<!-- Code -->
|
<!-- Code -->
|
||||||
|
<issue id="NewApi" severity="error" />
|
||||||
<issue id="SetTextI18n" severity="error" />
|
<issue id="SetTextI18n" severity="error" />
|
||||||
<issue id="ViewConstructor" severity="error" />
|
<issue id="ViewConstructor" severity="error" />
|
||||||
<issue id="UseValueOf" severity="error" />
|
<issue id="UseValueOf" severity="error" />
|
||||||
|
@ -82,10 +83,6 @@
|
||||||
<ignore path="**/generated/resolved/**/resolved.xml" />
|
<ignore path="**/generated/resolved/**/resolved.xml" />
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<!-- Bug in lint agp 4.1 incorrectly thinks kotlin forEach is using java 8 API's. -->
|
|
||||||
<!-- FIXME this workaround should be removed in a near future -->
|
|
||||||
<issue id="NewApi" severity="warning" />
|
|
||||||
|
|
||||||
<!-- DI -->
|
<!-- DI -->
|
||||||
<issue id="JvmStaticProvidesInObjectDetector" severity="error" />
|
<issue id="JvmStaticProvidesInObjectDetector" severity="error" />
|
||||||
</lint>
|
</lint>
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* 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 im.vector.app.core.utils.compat
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
|
fun <E> MutableCollection<E>.removeIfCompat(predicate: (E) -> Boolean) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
removeIf(predicate)
|
||||||
|
} else {
|
||||||
|
removeAll(filter(predicate).toSet())
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@
|
||||||
package im.vector.app.features.analytics
|
package im.vector.app.features.analytics
|
||||||
|
|
||||||
import im.vector.app.core.time.Clock
|
import im.vector.app.core.time.Clock
|
||||||
|
import im.vector.app.core.utils.compat.removeIfCompat
|
||||||
import im.vector.app.features.analytics.plan.Error
|
import im.vector.app.features.analytics.plan.Error
|
||||||
import im.vector.lib.core.utils.flow.tickerFlow
|
import im.vector.lib.core.utils.flow.tickerFlow
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -89,7 +90,7 @@ class DecryptionFailureTracker @Inject constructor(
|
||||||
fun onTimeLineDisposed(roomId: String) {
|
fun onTimeLineDisposed(roomId: String) {
|
||||||
scope.launch(Dispatchers.Default) {
|
scope.launch(Dispatchers.Default) {
|
||||||
synchronized(failures) {
|
synchronized(failures) {
|
||||||
failures.removeIf { it.roomId == roomId }
|
failures.removeIfCompat { it.roomId == roomId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,7 +106,7 @@ class DecryptionFailureTracker @Inject constructor(
|
||||||
|
|
||||||
private fun removeFailureForEventId(eventId: String) {
|
private fun removeFailureForEventId(eventId: String) {
|
||||||
synchronized(failures) {
|
synchronized(failures) {
|
||||||
failures.removeIf { it.failedEventId == eventId }
|
failures.removeIfCompat { it.failedEventId == eventId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,7 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.min
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
|
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
|
||||||
|
@ -185,6 +186,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||||
synchronized(modelCache) {
|
synchronized(modelCache) {
|
||||||
assertUpdateCallbacksAllowed()
|
assertUpdateCallbacksAllowed()
|
||||||
|
Timber.v("listUpdateCallback.onChanged(position: $position, count: $count). " +
|
||||||
|
"\ncurrentSnapshot has size of ${currentSnapshot.size} items")
|
||||||
(position until position + count).forEach {
|
(position until position + count).forEach {
|
||||||
// Invalidate cache
|
// Invalidate cache
|
||||||
modelCache[it] = null
|
modelCache[it] = null
|
||||||
|
@ -192,10 +195,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||||
// Also invalidate the first previous displayable event if
|
// Also invalidate the first previous displayable event if
|
||||||
// it's sent by the same user so we are sure we have up to date information.
|
// it's sent by the same user so we are sure we have up to date information.
|
||||||
val invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId
|
val invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId
|
||||||
val prevDisplayableEventIndex = currentSnapshot.subList(0, position).indexOfLast {
|
// In some cases onChanged will be called before onRemoved and onInserted so position will be bigger than currentSnapshot.size.
|
||||||
|
val prevList = currentSnapshot.subList(0, min(position, currentSnapshot.size))
|
||||||
|
val prevDisplayableEventIndex = prevList.indexOfLast {
|
||||||
timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId)
|
timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId)
|
||||||
}
|
}
|
||||||
if (prevDisplayableEventIndex != -1 && currentSnapshot[prevDisplayableEventIndex].senderInfo.userId == invalidatedSenderId) {
|
if (prevDisplayableEventIndex != -1 && currentSnapshot.getOrNull(prevDisplayableEventIndex)?.senderInfo?.userId == invalidatedSenderId) {
|
||||||
modelCache[prevDisplayableEventIndex] = null
|
modelCache[prevDisplayableEventIndex] = null
|
||||||
}
|
}
|
||||||
requestModelBuild()
|
requestModelBuild()
|
||||||
|
@ -205,6 +210,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||||
synchronized(modelCache) {
|
synchronized(modelCache) {
|
||||||
assertUpdateCallbacksAllowed()
|
assertUpdateCallbacksAllowed()
|
||||||
|
Timber.v("listUpdateCallback.onMoved(fromPosition: $fromPosition, toPosition: $toPosition). " +
|
||||||
|
"\ncurrentSnapshot has size of ${currentSnapshot.size} items")
|
||||||
val model = modelCache.removeAt(fromPosition)
|
val model = modelCache.removeAt(fromPosition)
|
||||||
modelCache.add(toPosition, model)
|
modelCache.add(toPosition, model)
|
||||||
requestModelBuild()
|
requestModelBuild()
|
||||||
|
@ -214,6 +221,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||||
override fun onInserted(position: Int, count: Int) {
|
override fun onInserted(position: Int, count: Int) {
|
||||||
synchronized(modelCache) {
|
synchronized(modelCache) {
|
||||||
assertUpdateCallbacksAllowed()
|
assertUpdateCallbacksAllowed()
|
||||||
|
Timber.v("listUpdateCallback.onInserted(position: $position, count: $count). " +
|
||||||
|
"\ncurrentSnapshot has size of ${currentSnapshot.size} items")
|
||||||
repeat(count) {
|
repeat(count) {
|
||||||
modelCache.add(position, null)
|
modelCache.add(position, null)
|
||||||
}
|
}
|
||||||
|
@ -224,6 +233,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||||
override fun onRemoved(position: Int, count: Int) {
|
override fun onRemoved(position: Int, count: Int) {
|
||||||
synchronized(modelCache) {
|
synchronized(modelCache) {
|
||||||
assertUpdateCallbacksAllowed()
|
assertUpdateCallbacksAllowed()
|
||||||
|
Timber.v("listUpdateCallback.onRemoved(position: $position, count: $count). " +
|
||||||
|
"\ncurrentSnapshot has size of ${currentSnapshot.size} items")
|
||||||
repeat(count) {
|
repeat(count) {
|
||||||
modelCache.removeAt(position)
|
modelCache.removeAt(position)
|
||||||
}
|
}
|
||||||
|
@ -306,6 +317,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||||
inSubmitList = true
|
inSubmitList = true
|
||||||
val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot)
|
val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot)
|
||||||
currentSnapshot = newSnapshot
|
currentSnapshot = newSnapshot
|
||||||
|
Timber.v("Submit a new snapshot of ${currentSnapshot.size} items.")
|
||||||
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
||||||
diffResult.dispatchUpdatesTo(listUpdateCallback)
|
diffResult.dispatchUpdatesTo(listUpdateCallback)
|
||||||
requestDelayedModelBuild(0)
|
requestDelayedModelBuild(0)
|
||||||
|
|
|
@ -852,6 +852,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
withState {
|
withState {
|
||||||
|
if (it.serverType == ServerType.MatrixOrg) {
|
||||||
when (it.onboardingFlow) {
|
when (it.onboardingFlow) {
|
||||||
OnboardingFlow.SignIn -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignIn))
|
OnboardingFlow.SignIn -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignIn))
|
||||||
OnboardingFlow.SignUp -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignUp))
|
OnboardingFlow.SignUp -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignUp))
|
||||||
|
@ -860,6 +861,9 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
_viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved)
|
_viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
_viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.transition.AutoTransition
|
import androidx.transition.AutoTransition
|
||||||
import androidx.transition.TransitionManager
|
import androidx.transition.TransitionManager
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.utils.compat.removeIfCompat
|
||||||
import im.vector.app.features.reactions.data.EmojiData
|
import im.vector.app.features.reactions.data.EmojiData
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -215,14 +216,7 @@ class EmojiRecyclerAdapter @Inject constructor() :
|
||||||
override fun onViewRecycled(holder: ViewHolder) {
|
override fun onViewRecycled(holder: ViewHolder) {
|
||||||
if (holder is EmojiViewHolder) {
|
if (holder is EmojiViewHolder) {
|
||||||
holder.data = null
|
holder.data = null
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
toUpdateWhenNotBusy.removeIfCompat { it.second == holder }
|
||||||
toUpdateWhenNotBusy.removeIf { it.second == holder }
|
|
||||||
} else {
|
|
||||||
val index = toUpdateWhenNotBusy.indexOfFirst { it.second == holder }
|
|
||||||
if (index != -1) {
|
|
||||||
toUpdateWhenNotBusy.removeAt(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
super.onViewRecycled(holder)
|
super.onViewRecycled(holder)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue