From cc89a8fc776fefe91e812cbd7302b8bdef04ad9f Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 17 Jan 2022 19:16:32 +0100 Subject: [PATCH 01/13] Timeline : fix 4959 --- changelog.d/4959.bugfix | 1 + .../detail/timeline/TimelineEventController.kt | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 changelog.d/4959.bugfix diff --git a/changelog.d/4959.bugfix b/changelog.d/4959.bugfix new file mode 100644 index 0000000000..6ffa3937f9 --- /dev/null +++ b/changelog.d/4959.bugfix @@ -0,0 +1 @@ +Prevent crash in Timeline and add more logs. \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 241ccb7428..bf0c5ee062 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -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 timber.log.Timber import javax.inject.Inject +import kotlin.math.min import kotlin.system.measureTimeMillis 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?) { synchronized(modelCache) { assertUpdateCallbacksAllowed() + Timber.v("listUpdateCallback.onChanged(position: $position, count: $count). " + + "\ncurrentSnapshot has size of ${currentSnapshot.size} items") (position until position + count).forEach { // Invalidate cache modelCache[it] = null @@ -192,10 +195,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // 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. 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 smaller than currentSnapshot.size. + val prevList = currentSnapshot.subList(0, min(position, currentSnapshot.size)) + val prevDisplayableEventIndex = prevList.indexOfLast { 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 } requestModelBuild() @@ -205,6 +210,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec override fun onMoved(fromPosition: Int, toPosition: Int) { synchronized(modelCache) { assertUpdateCallbacksAllowed() + Timber.v("listUpdateCallback.onMoved(fromPosition: $fromPosition, toPosition: $toPosition). " + + "\ncurrentSnapshot has size of ${currentSnapshot.size} items") val model = modelCache.removeAt(fromPosition) modelCache.add(toPosition, model) requestModelBuild() @@ -214,6 +221,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec override fun onInserted(position: Int, count: Int) { synchronized(modelCache) { assertUpdateCallbacksAllowed() + Timber.v("listUpdateCallback.onInserted(position: $position, count: $count). " + + "\ncurrentSnapshot has size of ${currentSnapshot.size} items") repeat(count) { modelCache.add(position, null) } @@ -224,6 +233,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec override fun onRemoved(position: Int, count: Int) { synchronized(modelCache) { assertUpdateCallbacksAllowed() + Timber.v("listUpdateCallback.onRemoved(position: $position, count: $count). " + + "\ncurrentSnapshot has size of ${currentSnapshot.size} items") repeat(count) { modelCache.removeAt(position) } @@ -306,6 +317,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec inSubmitList = true val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot) currentSnapshot = newSnapshot + Timber.v("Submit a new snapshot of ${currentSnapshot.size} items.") val diffResult = DiffUtil.calculateDiff(diffCallback) diffResult.dispatchUpdatesTo(listUpdateCallback) requestDelayedModelBuild(0) From a08304788897d8b1641975765557da1c955e24e9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jan 2022 09:12:39 +0100 Subject: [PATCH 02/13] Fix typo --- .../home/room/detail/timeline/TimelineEventController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index bf0c5ee062..4a9a03789f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -195,7 +195,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // 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. val invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId - // In some cases onChanged will be called before onRemoved and onInserted so position will be smaller than currentSnapshot.size. + // 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) From 13881a589a1b30686c5ec5f12fbb276e88876411 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 17 Jan 2022 11:48:58 +0100 Subject: [PATCH 03/13] Remove temporary change on the "NewApi" lint error. --- vector/lint.xml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/vector/lint.xml b/vector/lint.xml index 818349da24..f02090489c 100644 --- a/vector/lint.xml +++ b/vector/lint.xml @@ -40,6 +40,7 @@ + @@ -82,10 +83,6 @@ - - - - From 961f821ab9e270442bc948b1322b1a258d75d5f0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jan 2022 09:19:52 +0100 Subject: [PATCH 04/13] Create and use `removeIfCompat` (#4961) --- .../utils/compat/MutableCollectionCompat.kt | 27 +++++++++++++++++++ .../analytics/DecryptionFailureTracker.kt | 5 ++-- .../reactions/EmojiRecyclerAdapter.kt | 10 ++----- 3 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/utils/compat/MutableCollectionCompat.kt diff --git a/vector/src/main/java/im/vector/app/core/utils/compat/MutableCollectionCompat.kt b/vector/src/main/java/im/vector/app/core/utils/compat/MutableCollectionCompat.kt new file mode 100644 index 0000000000..e131b5f328 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/compat/MutableCollectionCompat.kt @@ -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 MutableCollection.removeIfCompat(predicate: (E) -> Boolean) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + removeIf(predicate) + } else { + removeAll(filter(predicate).toSet()) + } +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt index cd98356445..ec11702054 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt @@ -19,6 +19,7 @@ package im.vector.app.features.analytics import im.vector.app.core.flow.tickerFlow import im.vector.app.core.time.Clock import im.vector.app.features.analytics.plan.Error +import im.vector.app.core.utils.compat.removeIfCompat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -89,7 +90,7 @@ class DecryptionFailureTracker @Inject constructor( fun onTimeLineDisposed(roomId: String) { scope.launch(Dispatchers.Default) { 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) { synchronized(failures) { - failures.removeIf { it.failedEventId == eventId } + failures.removeIfCompat { it.failedEventId == eventId } } } diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt index d64ee0f705..6abbbe29af 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt @@ -32,6 +32,7 @@ import androidx.transition.AutoTransition import androidx.transition.TransitionManager import im.vector.app.R import im.vector.app.features.reactions.data.EmojiData +import im.vector.app.core.utils.compat.removeIfCompat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -215,14 +216,7 @@ class EmojiRecyclerAdapter @Inject constructor() : override fun onViewRecycled(holder: ViewHolder) { if (holder is EmojiViewHolder) { holder.data = null - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - toUpdateWhenNotBusy.removeIf { it.second == holder } - } else { - val index = toUpdateWhenNotBusy.indexOfFirst { it.second == holder } - if (index != -1) { - toUpdateWhenNotBusy.removeAt(index) - } - } + toUpdateWhenNotBusy.removeIfCompat { it.second == holder } } super.onViewRecycled(holder) } From f5b16b834cb50c0715032c84ade27b0613a5f3a9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 17 Jan 2022 12:50:44 +0100 Subject: [PATCH 05/13] Changelog --- changelog.d/4962.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/4962.bugfix diff --git a/changelog.d/4962.bugfix b/changelog.d/4962.bugfix new file mode 100644 index 0000000000..489f91cfc3 --- /dev/null +++ b/changelog.d/4962.bugfix @@ -0,0 +1 @@ +Fix crash on API <24 and make sure this error will not occur again. \ No newline at end of file From 114c60cfedc1d32c9042ed95d2f50c8c70ff2052 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 18 Jan 2022 10:29:26 +0000 Subject: [PATCH 06/13] fixing onboarding crash when signing in/up with sso - handles the sso flows by not forwarding to the signin/signup pages and instead using the previous onLoginFlowRetrieved when the selected server type is other --- .../features/onboarding/OnboardingViewModel.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 5d1e0fdade..4b3ce14002 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -826,13 +826,17 @@ class OnboardingViewModel @AssistedInject constructor( } withState { - when (it.onboardingFlow) { - OnboardingFlow.SignIn -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignIn)) - OnboardingFlow.SignUp -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignUp)) - OnboardingFlow.SignInSignUp, - null -> { - _viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved) + if (it.serverType == ServerType.MatrixOrg) { + when (it.onboardingFlow) { + OnboardingFlow.SignIn -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignIn)) + OnboardingFlow.SignUp -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignUp)) + OnboardingFlow.SignInSignUp, + null -> { + _viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved) + } } + } else { + _viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved) } } } From 54a5c3e52a71b0a4add78fa9ac8a04524f04002c Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 18 Jan 2022 10:43:39 +0000 Subject: [PATCH 07/13] adding changelog entry --- changelog.d/4969.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/4969.bugfix diff --git a/changelog.d/4969.bugfix b/changelog.d/4969.bugfix new file mode 100644 index 0000000000..6edfd498bf --- /dev/null +++ b/changelog.d/4969.bugfix @@ -0,0 +1 @@ +Fixes sign in/up crash when selecting ems and other server types which use SSO \ No newline at end of file From bdd30e3b8fbcea43ec35a08d28f7380a7b14b1f6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jan 2022 12:04:06 +0100 Subject: [PATCH 08/13] Fix crash when viewing source which contains an emoji. Import source of jsonviewer as a module of this project. --- changelog.d/4796.bugfix | 1 + dependencies.gradle | 1 + dependencies_groups.gradle | 2 +- library/jsonviewer/.gitignore | 1 + library/jsonviewer/build.gradle | 64 +++++ .../jsonviewer/src/main/AndroidManifest.xml | 1 + .../jsonviewer/JSonViewerDialog.kt | 77 ++++++ .../jsonviewer/JSonViewerEpoxyController.kt | 261 ++++++++++++++++++ .../jsonviewer/JSonViewerFragment.kt | 103 +++++++ .../jsonviewer/JSonViewerModel.kt | 124 +++++++++ .../jsonviewer/JSonViewerStyleProvider.kt | 46 +++ .../jsonviewer/JSonViewerViewModel.kt | 67 +++++ .../jsonviewer/SafeCharSequence.kt | 30 ++ .../java/org/billcarsonfr/jsonviewer/Utils.kt | 33 +++ .../org/billcarsonfr/jsonviewer/ValueItem.kt | 93 +++++++ .../main/res/layout/fragment_dialog_jv.xml | 5 + .../res/layout/fragment_jv_recycler_view.xml | 18 ++ .../layout/fragment_jv_recycler_view_wrap.xml | 10 + .../main/res/layout/item_jv_base_value.xml | 16 ++ .../src/main/res/menu/jv_menu_item.xml | 8 + .../jsonviewer/src/main/res/values/colors.xml | 11 + .../src/main/res/values/strings.xml | 3 + .../billcarsonfr/jsonviewer/ModelParseTest.kt | 96 +++++++ settings.gradle | 1 + vector/build.gradle | 2 +- 25 files changed, 1072 insertions(+), 2 deletions(-) create mode 100644 changelog.d/4796.bugfix create mode 100644 library/jsonviewer/.gitignore create mode 100644 library/jsonviewer/build.gradle create mode 100644 library/jsonviewer/src/main/AndroidManifest.xml create mode 100644 library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerDialog.kt create mode 100644 library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt create mode 100644 library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerFragment.kt create mode 100644 library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerModel.kt create mode 100644 library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerStyleProvider.kt create mode 100644 library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerViewModel.kt create mode 100644 library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/SafeCharSequence.kt create mode 100644 library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt create mode 100644 library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt create mode 100644 library/jsonviewer/src/main/res/layout/fragment_dialog_jv.xml create mode 100644 library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view.xml create mode 100644 library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view_wrap.xml create mode 100644 library/jsonviewer/src/main/res/layout/item_jv_base_value.xml create mode 100644 library/jsonviewer/src/main/res/menu/jv_menu_item.xml create mode 100644 library/jsonviewer/src/main/res/values/colors.xml create mode 100644 library/jsonviewer/src/main/res/values/strings.xml create mode 100644 library/jsonviewer/src/test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt 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 From 2a7719cdf60f2828e02d216e01e3de711d9d3173 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jan 2022 12:12:04 +0100 Subject: [PATCH 09/13] ktlint --- .../jsonviewer/JSonViewerEpoxyController.kt | 1 - .../org/billcarsonfr/jsonviewer/JSonViewerFragment.kt | 1 - .../org/billcarsonfr/jsonviewer/JSonViewerModel.kt | 2 -- .../jsonviewer/JSonViewerStyleProvider.kt | 1 - .../billcarsonfr/jsonviewer/JSonViewerViewModel.kt | 11 +++++++++-- .../java/org/billcarsonfr/jsonviewer/ValueItem.kt | 1 - .../org/billcarsonfr/jsonviewer/ModelParseTest.kt | 5 ----- .../features/analytics/DecryptionFailureTracker.kt | 2 +- .../app/features/reactions/EmojiRecyclerAdapter.kt | 2 +- 9 files changed, 11 insertions(+), 15 deletions(-) diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt index 1ff35f5005..9c48a137da 100644 --- a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt @@ -237,7 +237,6 @@ internal class JSonViewerEpoxyController(private val context: Context) : ) itemClickListener(View.OnClickListener { host.itemClicked(composed) }) } - } private fun itemClicked(model: JSonViewerModel) { diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerFragment.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerFragment.kt index a8aa2493d2..51e2797958 100644 --- a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerFragment.kt +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerFragment.kt @@ -38,7 +38,6 @@ internal data class JSonViewerFragmentArgs( val styleProvider: JSonViewerStyleProvider? ) : Parcelable - class JSonViewerFragment : Fragment(), MavericksView { private val viewModel: JSonViewerViewModel by fragmentViewModel() diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerModel.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerModel.kt index 3850044880..3d1f8dd3e2 100644 --- a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerModel.kt +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerModel.kt @@ -38,7 +38,6 @@ internal class JSonViewerObject(key: String?, index: Int?, jObject: JSONObject) override fun addChild(model: JSonViewerModel) { keys[model.key!!] = model } - } internal class JSonViewerArray(key: String?, index: Int?, jObject: JSONArray) : @@ -53,7 +52,6 @@ internal class JSonViewerArray(key: String?, index: Int?, jObject: JSONArray) : internal class JSonViewerLeaf(key: String?, index: Int?, val stringRes: String, val type: JSONType) : JSonViewerModel(key, index, stringRes) - internal enum class JSONType { STRING, NUMBER, diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerStyleProvider.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerStyleProvider.kt index 956e585f80..4fc04c91e4 100644 --- a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerStyleProvider.kt +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerStyleProvider.kt @@ -43,4 +43,3 @@ data class JSonViewerStyleProvider( ) } } - diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerViewModel.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerViewModel.kt index d978573a64..bc3f022cfa 100644 --- a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerViewModel.kt +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerViewModel.kt @@ -16,7 +16,15 @@ package org.billcarsonfr.jsonviewer -import com.airbnb.mvrx.* +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( @@ -61,7 +69,6 @@ internal class JSonViewerViewModel(initialState: JSonViewerState) : } catch (failure: Throwable) { JSonViewerState(Fail(failure)) } - } } } diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt index 5c003c97e9..9193a20ab2 100644 --- a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt @@ -75,7 +75,6 @@ internal abstract class ValueItem : EpoxyModelWithHolder() { v: View?, menuInfo: ContextMenu.ContextMenuInfo? ) { - if (copyValue != null) { val menuItem = menu?.add( Menu.NONE, R.id.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 index 4caf9ce958..b2d80911f1 100644 --- a/library/jsonviewer/src/test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt +++ b/library/jsonviewer/src/test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt @@ -69,28 +69,23 @@ class ModelParseTest { 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/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt index ec11702054..18fec37c62 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt @@ -18,8 +18,8 @@ package im.vector.app.features.analytics import im.vector.app.core.flow.tickerFlow import im.vector.app.core.time.Clock -import im.vector.app.features.analytics.plan.Error import im.vector.app.core.utils.compat.removeIfCompat +import im.vector.app.features.analytics.plan.Error import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt index 6abbbe29af..06d8a0bf88 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt @@ -31,8 +31,8 @@ import androidx.recyclerview.widget.RecyclerView import androidx.transition.AutoTransition import androidx.transition.TransitionManager import im.vector.app.R -import im.vector.app.features.reactions.data.EmojiData import im.vector.app.core.utils.compat.removeIfCompat +import im.vector.app.features.reactions.data.EmojiData import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch From f0d5260be8418d2d5c1bba2875cbdca1a47ae430 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jan 2022 12:34:14 +0100 Subject: [PATCH 10/13] Fix unit test --- .../test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/jsonviewer/src/test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt b/library/jsonviewer/src/test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt index b2d80911f1..350bcdf289 100644 --- a/library/jsonviewer/src/test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt +++ b/library/jsonviewer/src/test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt @@ -85,7 +85,8 @@ class ModelParseTest { val glossSeeAlso = glossDef.keys["GlossSeeAlso"] as JSonViewerArray Assert.assertEquals(2, glossSeeAlso.items.size) - Assert.assertEquals("0", glossSeeAlso.items.first().key) + Assert.assertEquals(0, glossSeeAlso.items.first().index) + Assert.assertNull(glossSeeAlso.items.first().key) Assert.assertEquals("GML", (glossSeeAlso.items.first() as JSonViewerLeaf).stringRes) } } From ea8465f5f3f96fab3ed75f6be541d3ab3e07f050 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jan 2022 14:06:02 +0100 Subject: [PATCH 11/13] Version 1.3.15 for hotfix release --- matrix-sdk-android/build.gradle | 2 +- vector/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index a1fb006e88..8b32b7dbc5 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -31,7 +31,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.3.14\"" + buildConfigField "String", "SDK_VERSION", "\"1.3.15\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" resValue "string", "git_sdk_revision", "\"${gitRevision()}\"" diff --git a/vector/build.gradle b/vector/build.gradle index fad04ffabe..695c205002 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -18,7 +18,7 @@ ext.versionMinor = 3 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 14 +ext.versionPatch = 15 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' From 68cd303a59332a19f25ff96adbc312e944de6a0d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jan 2022 14:11:01 +0100 Subject: [PATCH 12/13] towncrier --- CHANGES.md | 11 +++++++++++ changelog.d/4796.bugfix | 1 - changelog.d/4959.bugfix | 1 - changelog.d/4962.bugfix | 1 - changelog.d/4969.bugfix | 1 - 5 files changed, 11 insertions(+), 4 deletions(-) delete mode 100644 changelog.d/4796.bugfix delete mode 100644 changelog.d/4959.bugfix delete mode 100644 changelog.d/4962.bugfix delete mode 100644 changelog.d/4969.bugfix diff --git a/CHANGES.md b/CHANGES.md index e93d1bb089..cf885d5cd5 100644 --- a/CHANGES.md +++ b/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) ======================================= diff --git a/changelog.d/4796.bugfix b/changelog.d/4796.bugfix deleted file mode 100644 index df5b5a4eba..0000000000 --- a/changelog.d/4796.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix crash when viewing source which contains an emoji \ No newline at end of file diff --git a/changelog.d/4959.bugfix b/changelog.d/4959.bugfix deleted file mode 100644 index 6ffa3937f9..0000000000 --- a/changelog.d/4959.bugfix +++ /dev/null @@ -1 +0,0 @@ -Prevent crash in Timeline and add more logs. \ No newline at end of file diff --git a/changelog.d/4962.bugfix b/changelog.d/4962.bugfix deleted file mode 100644 index 489f91cfc3..0000000000 --- a/changelog.d/4962.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix crash on API <24 and make sure this error will not occur again. \ No newline at end of file diff --git a/changelog.d/4969.bugfix b/changelog.d/4969.bugfix deleted file mode 100644 index 6edfd498bf..0000000000 --- a/changelog.d/4969.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixes sign in/up crash when selecting ems and other server types which use SSO \ No newline at end of file From 5e96e87bdc2bd12f8dd4844f8dbd0d2454c5f175 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jan 2022 14:13:31 +0100 Subject: [PATCH 13/13] fastlane --- fastlane/metadata/android/en-US/changelogs/40103150.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/40103150.txt diff --git a/fastlane/metadata/android/en-US/changelogs/40103150.txt b/fastlane/metadata/android/en-US/changelogs/40103150.txt new file mode 100644 index 0000000000..2b5fbe76ca --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40103150.txt @@ -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 \ No newline at end of file