Add underline to urls in AnnotatedStringResource
To match other urls in Settings.
Also simplify the footer content for TogglePermissionAppList.
Fix: 294008469
Test: unit test
Change-Id: I540bea8c85ea8aa6434819aad485bfa7570886cb
diff --git a/packages/SettingsLib/Spa/gallery/res/values/strings.xml b/packages/SettingsLib/Spa/gallery/res/values/strings.xml
index 0d1a1fe..ec60f8c 100644
--- a/packages/SettingsLib/Spa/gallery/res/values/strings.xml
+++ b/packages/SettingsLib/Spa/gallery/res/values/strings.xml
@@ -24,4 +24,6 @@
<string name="single_line_summary_preference_title" translatable="false">Preference (singleLineSummary = true)</string>
<!-- Summary for single line summary preference. [DO NOT TRANSLATE] -->
<string name="single_line_summary_preference_summary" translatable="false">A very long summary to show case a preference which only shows a single line summary.</string>
+ <!-- Footer text with two links. [DO NOT TRANSLATE] -->
+ <string name="footer_with_two_links" translatable="false">Annotated string with <a href="https://www.android.com/">link 1</a> and <a href="https://source.android.com/">link 2</a>.</string>
</resources>
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPageProvider.kt
similarity index 92%
rename from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt
rename to packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPageProvider.kt
index 9c7e0ce..50c0eb7 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPageProvider.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,9 +28,11 @@
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.gallery.R
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.android.settingslib.spa.widget.ui.AnnotatedText
import com.android.settingslib.spa.widget.ui.Footer
private const val TITLE = "Sample Footer"
@@ -78,6 +80,9 @@
entry.UiLayout()
}
Footer(footerText = "Footer text always at the end of page.")
+ Footer {
+ AnnotatedText(R.string.footer_with_two_links)
+ }
}
}
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/AnnotatedStringResource.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/AnnotatedStringResource.kt
index 9ddd0c6..88ba4b0 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/AnnotatedStringResource.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/AnnotatedStringResource.kt
@@ -16,105 +16,93 @@
package com.android.settingslib.spa.framework.util
-import android.content.res.Resources
import android.graphics.Typeface
import android.text.Spanned
import android.text.style.StyleSpan
import android.text.style.URLSpan
import androidx.annotation.StringRes
+import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.Density
+import androidx.compose.ui.text.style.TextDecoration
-const val URLSPAN_TAG = "URLSPAN_TAG"
+const val URL_SPAN_TAG = "URL_SPAN_TAG"
@Composable
-fun annotatedStringResource(@StringRes id: Int, urlSpanColor: Color): AnnotatedString {
- LocalConfiguration.current
+fun annotatedStringResource(@StringRes id: Int): AnnotatedString {
val resources = LocalContext.current.resources
- val density = LocalDensity.current
+ val urlSpanColor = MaterialTheme.colorScheme.primary
return remember(id) {
val text = resources.getText(id)
- spannableStringToAnnotatedString(text, density, urlSpanColor)
+ spannableStringToAnnotatedString(text, urlSpanColor)
}
}
-private fun spannableStringToAnnotatedString(text: CharSequence, density: Density, urlSpanColor: Color): AnnotatedString {
- return if (text is Spanned) {
- with(density) {
- buildAnnotatedString {
- append((text.toString()))
- text.getSpans(0, text.length, Any::class.java).forEach {
- val start = text.getSpanStart(it)
- val end = text.getSpanEnd(it)
- when (it) {
- is StyleSpan ->
- when (it.style) {
- Typeface.NORMAL -> addStyle(
- SpanStyle(
- fontWeight = FontWeight.Normal,
- fontStyle = FontStyle.Normal
- ),
- start,
- end
- )
- Typeface.BOLD -> addStyle(
- SpanStyle(
- fontWeight = FontWeight.Bold,
- fontStyle = FontStyle.Normal
- ),
- start,
- end
- )
- Typeface.ITALIC -> addStyle(
- SpanStyle(
- fontWeight = FontWeight.Normal,
- fontStyle = FontStyle.Italic
- ),
- start,
- end
- )
- Typeface.BOLD_ITALIC -> addStyle(
- SpanStyle(
- fontWeight = FontWeight.Bold,
- fontStyle = FontStyle.Italic
- ),
- start,
- end
- )
- }
- is URLSpan -> {
- addStyle(
- SpanStyle(
- color = urlSpanColor,
- ),
- start,
- end
- )
- if (!it.url.isNullOrEmpty()) {
- addStringAnnotation(
- URLSPAN_TAG,
- it.url,
- start,
- end
- )
- }
- }
- else -> addStyle(SpanStyle(), start, end)
- }
+private fun spannableStringToAnnotatedString(text: CharSequence, urlSpanColor: Color) =
+ if (text is Spanned) {
+ buildAnnotatedString {
+ append((text.toString()))
+ for (span in text.getSpans(0, text.length, Any::class.java)) {
+ val start = text.getSpanStart(span)
+ val end = text.getSpanEnd(span)
+ when (span) {
+ is StyleSpan -> addStyleSpan(span, start, end)
+ is URLSpan -> addUrlSpan(span, urlSpanColor, start, end)
+ else -> addStyle(SpanStyle(), start, end)
}
}
}
} else {
AnnotatedString(text.toString())
}
+
+private fun AnnotatedString.Builder.addStyleSpan(styleSpan: StyleSpan, start: Int, end: Int) {
+ when (styleSpan.style) {
+ Typeface.NORMAL -> addStyle(
+ SpanStyle(fontWeight = FontWeight.Normal, fontStyle = FontStyle.Normal),
+ start,
+ end,
+ )
+
+ Typeface.BOLD -> addStyle(
+ SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Normal),
+ start,
+ end,
+ )
+
+ Typeface.ITALIC -> addStyle(
+ SpanStyle(fontWeight = FontWeight.Normal, fontStyle = FontStyle.Italic),
+ start,
+ end,
+ )
+
+ Typeface.BOLD_ITALIC -> addStyle(
+ SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic),
+ start,
+ end,
+ )
+ }
+}
+
+private fun AnnotatedString.Builder.addUrlSpan(
+ urlSpan: URLSpan,
+ urlSpanColor: Color,
+ start: Int,
+ end: Int,
+) {
+ addStyle(
+ SpanStyle(color = urlSpanColor, textDecoration = TextDecoration.Underline),
+ start,
+ end,
+ )
+ if (!urlSpan.url.isNullOrEmpty()) {
+ addStringAnnotation(URL_SPAN_TAG, urlSpan.url, start, end)
+ }
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/AnnotatedText.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/AnnotatedText.kt
new file mode 100644
index 0000000..82ac7e3
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/AnnotatedText.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 com.android.settingslib.spa.widget.ui
+
+import androidx.annotation.StringRes
+import androidx.compose.foundation.text.ClickableText
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalUriHandler
+import com.android.settingslib.spa.framework.util.URL_SPAN_TAG
+import com.android.settingslib.spa.framework.util.annotatedStringResource
+
+@Composable
+fun AnnotatedText(@StringRes id: Int) {
+ val uriHandler = LocalUriHandler.current
+ val annotatedString = annotatedStringResource(id)
+ ClickableText(
+ text = annotatedString,
+ style = MaterialTheme.typography.bodyMedium.copy(
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ ),
+ ) { offset ->
+ // Gets the url at the clicked position.
+ annotatedString.getStringAnnotations(URL_SPAN_TAG, offset, offset)
+ .firstOrNull()
+ ?.let { uriHandler.openUri(it.item) }
+ }
+}
diff --git a/packages/SettingsLib/Spa/tests/res/values/strings.xml b/packages/SettingsLib/Spa/tests/res/values/strings.xml
index cbfea06..fb8f878 100644
--- a/packages/SettingsLib/Spa/tests/res/values/strings.xml
+++ b/packages/SettingsLib/Spa/tests/res/values/strings.xml
@@ -26,5 +26,7 @@
other {There are # songs found in {place}.}
}</string>
- <string name="test_annotated_string_resource">Annotated string with <b>bold</b> and <a href="https://www.google.com/">link</a>.</string>
+ <string name="test_annotated_string_resource">Annotated string with <b>bold</b> and <a href="https://www.android.com/">link</a>.</string>
+
+ <string name="test_link"><a href="https://www.android.com/">link</a></string>
</resources>
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/AnnotatedStringResourceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/AnnotatedStringResourceTest.kt
index b65be42..9928355 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/AnnotatedStringResourceTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/AnnotatedStringResourceTest.kt
@@ -16,14 +16,14 @@
package com.android.settingslib.spa.framework.util
-import androidx.compose.ui.graphics.Color
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.text.style.TextDecoration
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.settingslib.spa.framework.util.URLSPAN_TAG
-import com.android.settingslib.spa.framework.util.annotatedStringResource
import com.android.settingslib.spa.test.R
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
@@ -38,24 +38,34 @@
@Test
fun testAnnotatedStringResource() {
composeTestRule.setContent {
- val annotatedString = annotatedStringResource(R.string.test_annotated_string_resource, Color.Blue)
+ val annotatedString =
+ annotatedStringResource(R.string.test_annotated_string_resource)
val annotations = annotatedString.getStringAnnotations(0, annotatedString.length)
- assertThat(annotations).hasSize(1)
- assertThat(annotations[0].start).isEqualTo(31)
- assertThat(annotations[0].end).isEqualTo(35)
- assertThat(annotations[0].tag).isEqualTo(URLSPAN_TAG)
- assertThat(annotations[0].item).isEqualTo("https://www.google.com/")
+ assertThat(annotations).containsExactly(
+ AnnotatedString.Range(
+ item = "https://www.android.com/",
+ start = 31,
+ end = 35,
+ tag = URL_SPAN_TAG,
+ )
+ )
- assertThat(annotatedString.spanStyles).hasSize(2)
- assertThat(annotatedString.spanStyles[0].start).isEqualTo(22)
- assertThat(annotatedString.spanStyles[0].end).isEqualTo(26)
- assertThat(annotatedString.spanStyles[0].item).isEqualTo(
- SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Normal))
-
- assertThat(annotatedString.spanStyles[1].start).isEqualTo(31)
- assertThat(annotatedString.spanStyles[1].end).isEqualTo(35)
- assertThat(annotatedString.spanStyles[1].item).isEqualTo(SpanStyle(color = Color.Blue))
+ assertThat(annotatedString.spanStyles).containsExactly(
+ AnnotatedString.Range(
+ item = SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Normal),
+ start = 22,
+ end = 26,
+ ),
+ AnnotatedString.Range(
+ item = SpanStyle(
+ color = MaterialTheme.colorScheme.primary,
+ textDecoration = TextDecoration.Underline,
+ ),
+ start = 31,
+ end = 35,
+ ),
+ )
}
}
}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/AnnotatedTextTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/AnnotatedTextTest.kt
new file mode 100644
index 0000000..2c218e3
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/AnnotatedTextTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 com.android.settingslib.spa.framework.util
+
+import android.content.Context
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.platform.UriHandler
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.test.R
+import com.android.settingslib.spa.widget.ui.AnnotatedText
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@RunWith(AndroidJUnit4::class)
+class AnnotatedTextTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @get:Rule
+ val mockito: MockitoRule = MockitoJUnit.rule()
+
+ @Mock
+ private lateinit var uriHandler: UriHandler
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Test
+ fun text_isDisplayed() {
+ composeTestRule.setContent {
+ AnnotatedText(R.string.test_annotated_string_resource)
+ }
+
+ composeTestRule.onNodeWithText(context.getString(R.string.test_annotated_string_resource))
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun onUriClick_openUri() {
+ composeTestRule.setContent {
+ CompositionLocalProvider(LocalUriHandler provides uriHandler) {
+ AnnotatedText(R.string.test_link)
+ }
+ }
+
+ composeTestRule.onNodeWithText(context.getString(R.string.test_link)).performClick()
+
+ verify(uriHandler).openUri("https://www.android.com/")
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfoPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfoPage.kt
index 21c9e34..945f2e2 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfoPage.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfoPage.kt
@@ -28,8 +28,7 @@
title: String,
packageName: String,
userId: Int,
- footerText: String,
- footerContent: (@Composable () -> Unit)?,
+ footerContent: @Composable () -> Unit,
packageManagers: IPackageManagers,
content: @Composable PackageInfo.() -> Unit,
) {
@@ -41,10 +40,6 @@
packageInfo.content()
- if (footerContent != null) {
- Footer(footerContent)
- } else {
- Footer(footerText)
- }
+ Footer(footerContent)
}
}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
index 7c689c6..7f82be4 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
@@ -38,6 +38,7 @@
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
+import com.android.settingslib.spa.widget.ui.AnnotatedText
import com.android.settingslib.spaprivileged.model.app.AppRecord
import com.android.settingslib.spaprivileged.model.app.IPackageManagers
import com.android.settingslib.spaprivileged.model.app.PackageManagers
@@ -140,8 +141,7 @@
title = stringResource(pageTitleResId),
packageName = packageName,
userId = userId,
- footerText = stringResource(footerResId),
- footerContent = footerContent(),
+ footerContent = { AnnotatedText(footerResId) },
packageManagers = packageManagers,
) {
val model = createSwitchModel(applicationInfo)
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt
index f4b3204..1ab6230 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt
@@ -20,7 +20,6 @@
import android.content.pm.ApplicationInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
-import androidx.compose.ui.text.AnnotatedString
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.compose.rememberContext
@@ -37,10 +36,7 @@
val footerResId: Int
val switchRestrictionKeys: List<String>
get() = emptyList()
- @Composable
- fun footerContent(): (@Composable () -> Unit)? {
- return null
- }
+
/**
* Loads the extra info for the App List, and generates the [AppRecord] List.
*