Commit b4580445 authored by MozLando's avatar MozLando
Browse files

Merge #7608



7608: Uplift Resources.getSpanned r=Amejia481 a=NotWoods

Uplift Resources.getSpannable from Fenix. The AC variant returns the immutable SpannedString instead of the mutable SpannableString.



Co-authored-by: default avatarTiger Oakes <toakes@mozilla.com>
parents 127702f0 d82844f7
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.support.ktx.android.content.res
import android.content.res.Resources
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.text.SpannableStringBuilder
import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
import android.text.SpannedString
import androidx.annotation.StringRes
import java.util.Formatter
import java.util.Locale
/**
* Returns the primary locale according to the user's preference.
*/
val Resources.locale: Locale
get() = if (SDK_INT >= Build.VERSION_CODES.N) {
configuration.locales[0]
} else {
@Suppress("Deprecation")
configuration.locale
}
/**
* Returns the character sequence associated with a given resource [id],
* substituting format arguments with additional styling spans.
*
* Credit to Michael Spitsin https://medium.com/@programmerr47/working-with-spans-in-android-ca4ab1327bc4
*
* @param id The desired resource identifier, corresponding to a string resource.
* @param spanParts The format arguments that will be used for substitution.
* The first element of each pair is the text to insert, similar to [String.format].
* The second element of each pair is a span that will be used to style the inserted string.
*/
@Suppress("SpreadOperator")
fun Resources.getSpanned(
@StringRes id: Int,
vararg spanParts: Pair<Any, Any>
): SpannedString {
val builder = SpannableStringBuilder()
val formatArgs = spanParts.map { (text) -> text }.toTypedArray()
val formatter = Formatter(SpannableAppendable(builder, spanParts), locale)
formatter.format(getString(id), *formatArgs)
return SpannedString(builder)
}
/**
* [Appendable] implementation that wraps [SpannableStringBuilder]
* and inserts spans from the span parts array.
*/
private class SpannableAppendable(
private val builder: SpannableStringBuilder,
spanParts: Array<out Pair<Any, Any>>
) : Appendable {
/**
* Map of values from span parts, with keys converted to char sequences.
*/
private val spansMap = spanParts
.toMap()
.mapKeys { (key) -> key.let { it as? CharSequence ?: it.toString() } }
override fun append(csq: CharSequence?) = apply { appendSmart(csq) }
override fun append(csq: CharSequence?, start: Int, end: Int) = apply {
if (csq != null) {
if (start in 0 until end && end <= csq.length) {
append(csq.subSequence(start, end))
} else {
throw IndexOutOfBoundsException("start " + start + ", end " + end + ", s.length() " + csq.length)
}
}
}
override fun append(c: Char) = apply { builder.append(c.toString()) }
/**
* Tries to find [csq] in the [spansMap] and use the corresponding span value.
* If [csq] is not found, the map is searched manually by converting values to strings.
* If no match is found afterwards, [csq] is appended with no corresponding span.
*/
private fun appendSmart(csq: CharSequence?) {
if (csq != null) {
if (csq in spansMap) {
val span = spansMap.getValue(csq)
builder.append(csq, span, SPAN_EXCLUSIVE_EXCLUSIVE)
} else {
val possibleMatchDict = spansMap.filter { (text) -> text.toString() == csq }
if (possibleMatchDict.isNotEmpty()) {
val spanDictEntry = possibleMatchDict.entries.first()
builder.append(spanDictEntry.key, spanDictEntry.value, SPAN_EXCLUSIVE_EXCLUSIVE)
} else {
builder.append(csq)
}
}
}
}
}
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.support.ktx.android.content.res
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Typeface.BOLD
import android.graphics.Typeface.ITALIC
import android.os.Build
import android.os.LocaleList
import android.text.Html
import android.text.style.StyleSpan
import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.support.test.mock
import mozilla.components.support.test.whenever
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.spy
import org.robolectric.annotation.Config
import java.util.Locale
@RunWith(AndroidJUnit4::class)
class ResourcesTest {
private lateinit var resources: Resources
private lateinit var configuration: Configuration
@Before
fun setup() {
resources = mock()
configuration = spy(Configuration())
whenever(resources.configuration).thenReturn(configuration)
}
@Config(sdk = [Build.VERSION_CODES.N])
@Test
fun `locale returns first item in locales list`() {
whenever(configuration.locales).thenReturn(LocaleList(Locale.CANADA, Locale.ENGLISH))
assertEquals(Locale.CANADA, resources.locale)
}
@Suppress("Deprecation")
@Config(sdk = [Build.VERSION_CODES.M])
@Test
fun `locale returns locale from configuration`() {
configuration.locale = Locale.FRENCH
assertEquals(Locale.FRENCH, resources.locale)
}
@Config(sdk = [Build.VERSION_CODES.N])
@Test
fun `getSpanned formats corresponding string`() {
val id = 100
whenever(configuration.locales).thenReturn(LocaleList(Locale.ROOT))
whenever(resources.getString(id)).thenReturn("Allow %1\$s to open %2\$s")
assertEquals(
"<p dir=\"ltr\">Allow <b>App</b> to open <i>Website</i></p>\n",
Html.toHtml(
resources.getSpanned(
id,
"App" to StyleSpan(BOLD),
"Website" to StyleSpan(ITALIC)
),
Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE
)
)
}
}
......@@ -29,6 +29,10 @@ permalink: /changelog/
* **concept-engine**
* Adds `profiler` property with `isProfilerActive`, `getProfilerTime` and `addMarker` Firefox Profiler APIs. These will allow to add profiler markers.
* **support-ktx**
* Adds `Resources.getSpanned` to format strings using style spans.
* Adds `Resources.locale` to get the corresponding locale on all SDK versions.
# 48.0.0
* [Commits](https://github.com/mozilla-mobile/android-components/compare/v47.0.0...v48.0.0)
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment