Merge branch 'release/1.3.11' into main

This commit is contained in:
ganfra 2021-12-17 18:53:26 +01:00
commit cab28ffe7e
20 changed files with 117 additions and 60 deletions

View file

@ -1,3 +1,20 @@
Changes in Element v1.3.11 (2021-12-17)
=======================================
Bugfixes 🐛
----------
- Fixing proximity sensor still being active after a call ([#2467](https://github.com/vector-im/element-android/issues/2467))
- Fix name and shield are truncated in the room detail screen ([#4700](https://github.com/vector-im/element-android/issues/4700))
- Call banner: center text vertically ([#4710](https://github.com/vector-im/element-android/issues/4710))
- Fixes unable to render messages by allowing them to render whilst the emoji library is initialising ([#4733](https://github.com/vector-im/element-android/issues/4733))
- Fix app crash uppon long press on a reply event ([#4742](https://github.com/vector-im/element-android/issues/4742))
- Fixes crash when launching rooms which contain emojis in the emote content on android 12+ ([#4743](https://github.com/vector-im/element-android/issues/4743))
Other changes
-------------
- Avoids leaking the activity windows when loading dialogs are displaying ([#4713](https://github.com/vector-im/element-android/issues/4713))
Changes in Element v1.3.10 (2021-12-14) Changes in Element v1.3.10 (2021-12-14)
======================================= =======================================

View file

@ -0,0 +1,2 @@
Main changes in this version: Bug fixes!
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.11

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=b75392c5625a88bccd58a574552a5a323edca82dab5942d2d41097f809c6bcce distributionSha256Sum=dd54e87b4d7aa8ff3c6afb0f7805aa121d4b70bca55b8c9b1b896eb103184582
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View file

@ -31,7 +31,7 @@ android {
// that the app's state is completely cleared between tests. // that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true' testInstrumentationRunnerArguments clearPackageData: 'true'
buildConfigField "String", "SDK_VERSION", "\"1.3.10\"" buildConfigField "String", "SDK_VERSION", "\"1.3.11\""
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
resValue "string", "git_sdk_revision", "\"${gitRevision()}\"" resValue "string", "git_sdk_revision", "\"${gitRevision()}\""

View file

@ -17,7 +17,7 @@ PARAM_KEYSTORE_PATH=$1
PARAM_APK=$2 PARAM_APK=$2
# Other params # Other params
BUILD_TOOLS_VERSION="31.0.0-rc5" BUILD_TOOLS_VERSION="31.0.0"
MIN_SDK_VERSION=21 MIN_SDK_VERSION=21
echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..." echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..."

View file

@ -23,7 +23,7 @@ PARAM_KS_PASS=$3
PARAM_KEY_PASS=$4 PARAM_KEY_PASS=$4
# Other params # Other params
BUILD_TOOLS_VERSION="31.0.0-rc5" BUILD_TOOLS_VERSION="31.0.0"
MIN_SDK_VERSION=21 MIN_SDK_VERSION=21
echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..." echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..."

View file

@ -15,7 +15,7 @@ kapt {
// Note: 2 digits max for each value // Note: 2 digits max for each value
ext.versionMajor = 1 ext.versionMajor = 1
ext.versionMinor = 3 ext.versionMinor = 3
ext.versionPatch = 10 ext.versionPatch = 11
static def getGitTimestamp() { static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct' def cmd = 'git show -s --format=%ct'

View file

@ -24,6 +24,7 @@ import android.text.Spanned
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.text.style.StrikethroughSpan import android.text.style.StrikethroughSpan
import android.text.style.UnderlineSpan import android.text.style.UnderlineSpan
import androidx.emoji2.text.EmojiCompat
import im.vector.app.InstrumentedTest import im.vector.app.InstrumentedTest
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeTrue import org.amshove.kluent.shouldBeTrue
@ -32,12 +33,18 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.JUnit4 import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters import org.junit.runners.MethodSorters
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@RunWith(JUnit4::class) @RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
class SpanUtilsTest : InstrumentedTest { class SpanUtilsTest : InstrumentedTest {
private val spanUtils = SpanUtils() private val spanUtils = SpanUtils {
val emojiCompat = EmojiCompat.get()
emojiCompat.waitForInit()
emojiCompat.process(it) ?: it
}
private fun SpanUtils.canUseTextFuture(message: CharSequence): Boolean { private fun SpanUtils.canUseTextFuture(message: CharSequence): Boolean {
return getBindingOptions(message).canUseTextFuture return getBindingOptions(message).canUseTextFuture
@ -122,4 +129,17 @@ class SpanUtilsTest : InstrumentedTest {
} }
private fun trueIfAlwaysAllowed() = Build.VERSION.SDK_INT < Build.VERSION_CODES.P private fun trueIfAlwaysAllowed() = Build.VERSION.SDK_INT < Build.VERSION_CODES.P
private fun EmojiCompat.waitForInit() {
val latch = CountDownLatch(1)
registerInitCallback(object : EmojiCompat.InitCallback() {
override fun onInitialized() = latch.countDown()
override fun onFailed(throwable: Throwable?) {
latch.countDown()
throw RuntimeException(throwable)
}
})
EmojiCompat.init(context())
latch.await(30, TimeUnit.SECONDS)
}
} }

View file

@ -23,8 +23,12 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
fun interface EmojiSpanify {
fun spanify(sequence: CharSequence): CharSequence
}
@Singleton @Singleton
class EmojiCompatWrapper @Inject constructor(private val context: Context) { class EmojiCompatWrapper @Inject constructor(private val context: Context) : EmojiSpanify {
private var initialized = false private var initialized = false
@ -49,7 +53,7 @@ class EmojiCompatWrapper @Inject constructor(private val context: Context) {
}) })
} }
fun safeEmojiSpanify(sequence: CharSequence): CharSequence { override fun spanify(sequence: CharSequence): CharSequence {
if (initialized) { if (initialized) {
try { try {
return EmojiCompat.get().process(sequence) ?: sequence return EmojiCompat.get().process(sequence) ?: sequence

View file

@ -26,6 +26,8 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import im.vector.app.EmojiCompatWrapper
import im.vector.app.EmojiSpanify
import im.vector.app.core.dispatchers.CoroutineDispatchers import im.vector.app.core.dispatchers.CoroutineDispatchers
import im.vector.app.core.error.DefaultErrorFormatter import im.vector.app.core.error.DefaultErrorFormatter
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
@ -76,6 +78,9 @@ abstract class VectorBindModule {
@Binds @Binds
abstract fun bindDefaultClock(clock: DefaultClock): Clock abstract fun bindDefaultClock(clock: DefaultClock): Clock
@Binds
abstract fun bindEmojiSpanify(emojiCompatWrapper: EmojiCompatWrapper): EmojiSpanify
} }
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)

View file

@ -151,6 +151,7 @@ abstract class VectorBaseFragment<VB : ViewBinding> : Fragment(), MavericksView
override fun onDestroyView() { override fun onDestroyView() {
Timber.i("onDestroyView Fragment ${javaClass.simpleName}") Timber.i("onDestroyView Fragment ${javaClass.simpleName}")
_binding = null _binding = null
dismissLoadingDialog()
super.onDestroyView() super.onDestroyView()
} }

View file

@ -93,7 +93,9 @@ class CallProximityManager @Inject constructor(
if (wakeLock == null) { if (wakeLock == null) {
wakeLock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, generateWakeLockTag()) wakeLock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, generateWakeLockTag())
} }
wakeLock?.acquire(WAKE_LOCK_TIMEOUT_MILLIS) wakeLock
?.takeIf { !it.isHeld }
?.acquire(WAKE_LOCK_TIMEOUT_MILLIS)
} }
private fun onProximityFar() { private fun onProximityFar() {

View file

@ -613,7 +613,7 @@ class MessageItemFactory @Inject constructor(
val formattedBody = SpannableStringBuilder() val formattedBody = SpannableStringBuilder()
formattedBody.append("* ${informationData.memberName} ") formattedBody.append("* ${informationData.memberName} ")
formattedBody.append(messageContent.getHtmlBody()) formattedBody.append(messageContent.getHtmlBody())
val bindingOptions = spanUtils.getBindingOptions(formattedBody)
val message = formattedBody.linkify(callback) val message = formattedBody.linkify(callback)
return MessageTextItem_() return MessageTextItem_()
@ -625,6 +625,7 @@ class MessageItemFactory @Inject constructor(
message(message) message(message)
} }
} }
.bindingOptions(bindingOptions)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.previewUrlRetriever(callback?.getPreviewUrlRetriever()) .previewUrlRetriever(callback?.getPreviewUrlRetriever())
.imageContentRenderer(imageContentRenderer) .imageContentRenderer(imageContentRenderer)

View file

@ -17,7 +17,7 @@
package im.vector.app.features.home.room.detail.timeline.format package im.vector.app.features.home.room.detail.timeline.format
import dagger.Lazy import dagger.Lazy
import im.vector.app.EmojiCompatWrapper import im.vector.app.EmojiSpanify
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
@ -39,7 +39,7 @@ import javax.inject.Inject
class DisplayableEventFormatter @Inject constructor( class DisplayableEventFormatter @Inject constructor(
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val emojiCompatWrapper: EmojiCompatWrapper, private val emojiSpanify: EmojiSpanify,
private val noticeEventFormatter: NoticeEventFormatter, private val noticeEventFormatter: NoticeEventFormatter,
private val htmlRenderer: Lazy<EventHtmlRenderer> private val htmlRenderer: Lazy<EventHtmlRenderer>
) { ) {
@ -100,7 +100,7 @@ class DisplayableEventFormatter @Inject constructor(
} }
EventType.REACTION -> { EventType.REACTION -> {
timelineEvent.root.getClearContent().toModel<ReactionContent>()?.relatesTo?.let { timelineEvent.root.getClearContent().toModel<ReactionContent>()?.relatesTo?.let {
val emojiSpanned = emojiCompatWrapper.safeEmojiSpanify(stringProvider.getString(R.string.sent_a_reaction, it.key)) val emojiSpanned = emojiSpanify.spanify(stringProvider.getString(R.string.sent_a_reaction, it.key))
simpleFormat(senderName, emojiSpanned, appendAuthor) simpleFormat(senderName, emojiSpanned, appendAuthor)
} ?: span { } } ?: span { }
} }

View file

@ -20,7 +20,7 @@ import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import im.vector.app.EmojiCompatWrapper import im.vector.app.EmojiSpanify
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericFooterItem
@ -32,8 +32,8 @@ import javax.inject.Inject
*/ */
class ViewReactionsEpoxyController @Inject constructor( class ViewReactionsEpoxyController @Inject constructor(
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val emojiCompatWrapper: EmojiCompatWrapper) : private val emojiSpanify: EmojiSpanify) :
TypedEpoxyController<DisplayReactionsViewState>() { TypedEpoxyController<DisplayReactionsViewState>() {
var listener: Listener? = null var listener: Listener? = null
@ -56,7 +56,7 @@ class ViewReactionsEpoxyController @Inject constructor(
reactionInfoSimpleItem { reactionInfoSimpleItem {
id(reactionInfo.eventId) id(reactionInfo.eventId)
timeStamp(reactionInfo.timestamp) timeStamp(reactionInfo.timestamp)
reactionKey(host.emojiCompatWrapper.safeEmojiSpanify(reactionInfo.reactionKey)) reactionKey(host.emojiSpanify.spanify(reactionInfo.reactionKey))
authorDisplayName(reactionInfo.authorName ?: reactionInfo.authorId) authorDisplayName(reactionInfo.authorName ?: reactionInfo.authorId)
userClicked { host.listener?.didSelectUser(reactionInfo.authorId) } userClicked { host.listener?.didSelectUser(reactionInfo.authorId) }
} }

View file

@ -16,6 +16,7 @@
package im.vector.app.features.home.room.detail.timeline.tools package im.vector.app.features.home.room.detail.timeline.tools
import android.text.SpannableStringBuilder
import android.text.style.ClickableSpan import android.text.style.ClickableSpan
import android.view.MotionEvent import android.view.MotionEvent
import android.widget.TextView import android.widget.TextView
@ -44,7 +45,8 @@ fun CharSequence.findPillsAndProcess(scope: CoroutineScope, processBlock: (PillI
fun CharSequence.linkify(callback: TimelineEventController.UrlClickCallback?): CharSequence { fun CharSequence.linkify(callback: TimelineEventController.UrlClickCallback?): CharSequence {
val text = this.toString() val text = this.toString()
val spannable = toSpannable() // SpannableStringBuilder is used to avoid Epoxy throwing ImmutableModelException
val spannable = SpannableStringBuilder(this)
MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback { MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback {
override fun onUrlClicked(url: String) { override fun onUrlClicked(url: String) {
callback?.onUrlClicked(url, text) callback?.onUrlClicked(url, text)

View file

@ -21,13 +21,15 @@ import android.text.Spanned
import android.text.style.MetricAffectingSpan import android.text.style.MetricAffectingSpan
import android.text.style.StrikethroughSpan import android.text.style.StrikethroughSpan
import android.text.style.UnderlineSpan import android.text.style.UnderlineSpan
import androidx.emoji2.text.EmojiCompat import im.vector.app.EmojiSpanify
import im.vector.app.features.home.room.detail.timeline.item.BindingOptions import im.vector.app.features.home.room.detail.timeline.item.BindingOptions
import javax.inject.Inject import javax.inject.Inject
class SpanUtils @Inject constructor() { class SpanUtils @Inject constructor(
private val emojiSpanify: EmojiSpanify
) {
fun getBindingOptions(charSequence: CharSequence): BindingOptions { fun getBindingOptions(charSequence: CharSequence): BindingOptions {
val emojiCharSequence = EmojiCompat.get().process(charSequence) val emojiCharSequence = emojiSpanify.spanify(charSequence)
if (emojiCharSequence !is Spanned) { if (emojiCharSequence !is Spanned) {
return BindingOptions() return BindingOptions()

View file

@ -24,7 +24,7 @@ import android.widget.LinearLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.withStyledAttributes import androidx.core.content.withStyledAttributes
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.EmojiCompatWrapper import im.vector.app.EmojiSpanify
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
import im.vector.app.core.utils.TextUtils import im.vector.app.core.utils.TextUtils
@ -39,9 +39,9 @@ import javax.inject.Inject
class ReactionButton @JvmOverloads constructor(context: Context, class ReactionButton @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : defStyleAttr: Int = 0) :
LinearLayout(context, attrs, defStyleAttr), View.OnClickListener, View.OnLongClickListener { LinearLayout(context, attrs, defStyleAttr), View.OnClickListener, View.OnLongClickListener {
@Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper @Inject lateinit var emojiSpanify: EmojiSpanify
private val views: ReactionButtonBinding private val views: ReactionButtonBinding
@ -57,7 +57,7 @@ class ReactionButton @JvmOverloads constructor(context: Context,
set(value) { set(value) {
field = value field = value
// maybe cache this for performances? // maybe cache this for performances?
val emojiSpanned = emojiCompatWrapper.safeEmojiSpanify(value) val emojiSpanned = emojiSpanify.spanify(value)
views.reactionText.text = emojiSpanned views.reactionText.text = emojiSpanned
} }

View file

@ -1,25 +1,20 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" <merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorPrimary"
android:foreground="?attr/selectableItemBackground" android:foreground="?attr/selectableItemBackground"
tools:background="?colorPrimary"
tools:parentTag="android.widget.FrameLayout"> tools:parentTag="android.widget.FrameLayout">
<TextView <TextView
android:id="@+id/currentCallsInfo" android:id="@+id/currentCallsInfo"
style="@style/Widget.Vector.TextView.Body" style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:drawablePadding="10dp" android:layout_gravity="center"
android:gravity="center" android:gravity="center"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingTop="12dp"
android:paddingEnd="16dp" android:paddingEnd="16dp"
android:paddingBottom="12dp" android:textColor="?colorOnPrimary"
android:text="@string/call_only_active" tools:text="@string/call_only_active" />
android:textColor="?colorOnPrimary" />
</merge> </merge>

View file

@ -12,8 +12,8 @@
android:id="@+id/memberProfileInfoContainer" android:id="@+id/memberProfileInfoContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="16dp" android:layout_marginStart="16dp"
android:paddingEnd="16dp" android:layout_marginEnd="16dp"
app:layout_constraintBottom_toTopOf="@id/memberProfileNameView" app:layout_constraintBottom_toTopOf="@id/memberProfileNameView"
app:layout_constraintTop_toTopOf="@id/memberProfileNameView"> app:layout_constraintTop_toTopOf="@id/memberProfileNameView">
@ -23,7 +23,7 @@
android:layout_height="128dp" android:layout_height="128dp"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:contentDescription="@string/avatar" android:contentDescription="@string/avatar"
app:layout_constraintBottom_toTopOf="@id/memberProfileNameView" app:layout_constraintBottom_toTopOf="@id/memberProfileLinearLayout"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@ -45,30 +45,36 @@
tools:src="@drawable/ic_presence_offline" tools:src="@drawable/ic_presence_offline"
tools:visibility="visible" /> tools:visibility="visible" />
<im.vector.app.core.ui.views.ShieldImageView <LinearLayout
android:id="@+id/memberProfileDecorationImageView" android:id="@+id/memberProfileLinearLayout"
android:layout_width="30dp" android:layout_width="0dp"
android:layout_height="30dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="@id/memberProfileNameView"
app:layout_constraintEnd_toStartOf="@id/memberProfileNameView"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/memberProfileNameView" />
<TextView
android:id="@+id/memberProfileNameView"
style="@style/Widget.Vector.TextView.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:textStyle="bold" android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@id/memberProfileIdView" app:layout_constraintBottom_toTopOf="@id/memberProfileIdView"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/memberProfileDecorationImageView" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/memberProfileAvatarView" app:layout_constraintTop_toBottomOf="@id/memberProfileAvatarView">
tools:text="@sample/users.json/data/displayName" />
<im.vector.app.core.ui.views.ShieldImageView
android:id="@+id/memberProfileDecorationImageView"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginStart="8dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/memberProfileNameView"
style="@style/Widget.Vector.TextView.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:gravity="center"
android:textStyle="bold"
tools:text="@sample/users.json/data/displayName" />
</LinearLayout>
<TextView <TextView
android:id="@+id/memberProfileIdView" android:id="@+id/memberProfileIdView"
@ -82,7 +88,7 @@
app:layout_constraintBottom_toTopOf="@id/memberProfilePowerLevelView" app:layout_constraintBottom_toTopOf="@id/memberProfilePowerLevelView"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/memberProfileNameView" app:layout_constraintTop_toBottomOf="@id/memberProfileLinearLayout"
tools:text="@sample/users.json/data/id" /> tools:text="@sample/users.json/data/id" />
<TextView <TextView