diff options
10 files changed, 221 insertions, 107 deletions
diff --git a/packages/SettingsLib/Spa/gallery/res/values/strings.xml b/packages/SettingsLib/Spa/gallery/res/values/strings.xml index 0d1a1fe312e2..ec60f8c55331 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 index 9c7e0ce67479..50c0eb70bb03 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.common.createSettingsPage 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 @@ object FooterPageProvider : SettingsPageProvider { 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 9ddd0c6d2c4c..88ba4b07d30a 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 000000000000..82ac7e3bc9b2 --- /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 cbfea060d157..fb8f878230d5 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 b65be42f50fe..9928355ca089 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 @@ class AnnotatedStringResourceTest { @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(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(annotations).containsExactly( + AnnotatedString.Range( + item = "https://www.android.com/", + start = 31, + end = 35, + tag = URL_SPAN_TAG, + ) + ) - 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 000000000000..2c218e3050e0 --- /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 21c9e349ef93..945f2e252a22 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 @@ fun AppInfoPage( title: String, packageName: String, userId: Int, - footerText: String, - footerContent: (@Composable () -> Unit)?, + footerContent: @Composable () -> Unit, packageManagers: IPackageManagers, content: @Composable PackageInfo.() -> Unit, ) { @@ -41,10 +40,6 @@ fun AppInfoPage( 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 7c689c62427e..7f82be4df196 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.framework.compose.navigator 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 @@ internal fun TogglePermissionAppListModel<out AppRecord>.TogglePermissionAppInfo 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 f4b32043b1b1..1ab623076f0a 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.Context 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 @@ interface TogglePermissionAppListModel<T : AppRecord> { 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. * |