Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import android.text.TextUtils
import android.text.TextWatcher
import android.text.method.KeyListener
import android.text.method.QwertyKeyListener
import android.text.style.CharacterStyle
import android.text.style.ParagraphStyle
import android.util.TypedValue
import android.view.ActionMode
import android.view.DragEvent
Expand Down Expand Up @@ -1242,6 +1244,10 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
} catch (e: IndexOutOfBoundsException) {
logSoftException(TAG, e)
}
// Drop editor spans like PasswordTransformationMethod.Visible, which hold on to this view
stripSpansOfKind(sb, Any::class.java) { span ->
span !is CharacterStyle && span !is ParagraphStyle && span !is ReactSpan
}
}

// If we don't have text, make sure we have *something* to measure.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.views.textinput

import android.provider.Settings
import android.text.Spannable
import android.text.method.PasswordTransformationMethod
import android.util.DisplayMetrics
import androidx.core.content.res.ResourcesCompat.ID_NULL
import com.facebook.react.bridge.ReactTestHelper.createTestReactApplicationContext
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
import com.facebook.react.uimanager.DisplayMetricsHolder
import com.facebook.react.uimanager.StateWrapper
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.views.text.TextLayoutManager
import com.facebook.testutils.shadows.ShadowSoLoader
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(shadows = [ShadowSoLoader::class])
class ReactEditTextCachedSpannableTest {

private lateinit var view: ReactEditText

@Before
fun setup() {
ReactNativeFeatureFlagsForTests.setUp()
val context = createTestReactApplicationContext(RuntimeEnvironment.getApplication())
val themedContext = ThemedReactContext(context, context.baseContext, null, ID_NULL)
DisplayMetricsHolder.setScreenDisplayMetrics(DisplayMetrics())
view = ReactTextInputManager().createViewInstance(themedContext)
view.id = 42
view.stateWrapper = mock<StateWrapper>()
}

@Test
fun testCachedSpannableDoesNotKeepPasswordSpans() {
Settings.System.putInt(
RuntimeEnvironment.getApplication().contentResolver,
Settings.System.TEXT_SHOW_PASSWORD,
1,
)
view.transformationMethod = PasswordTransformationMethod.getInstance()

view.text?.append("a")
view.text?.append("b")

assertThat(spanOwners(checkNotNull(view.text)))
.contains(PasswordTransformationMethod::class.java)
assertThat(spanOwners(getCachedSpannable(view.id)))
.doesNotContain(PasswordTransformationMethod::class.java)
}

private fun spanOwners(text: Spannable): List<Class<*>?> =
text.getSpans(0, text.length, Any::class.java).map { it.javaClass.enclosingClass }

@Suppress("UNCHECKED_CAST")
private fun getCachedSpannable(tag: Int): Spannable {
val cache =
TextLayoutManager::class
.java
.getDeclaredField("tagToSpannableCache")
.apply { isAccessible = true }
.get(null) as Map<Int, Spannable>
return checkNotNull(cache[tag])
}
}
Loading