1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.permissioncontroller.permission.ui.wear.elements
18 
19 import android.text.Spanned
20 import android.text.style.ClickableSpan
21 import android.view.View
22 import androidx.compose.foundation.text.BasicText
23 import androidx.compose.runtime.Composable
24 import androidx.compose.ui.Modifier
25 import androidx.compose.ui.graphics.Color
26 import androidx.compose.ui.platform.LocalContext
27 import androidx.compose.ui.text.AnnotatedString
28 import androidx.compose.ui.text.LinkAnnotation
29 import androidx.compose.ui.text.LinkInteractionListener
30 import androidx.compose.ui.text.SpanStyle
31 import androidx.compose.ui.text.TextStyle
32 import androidx.compose.ui.text.buildAnnotatedString
33 import androidx.compose.ui.text.style.TextDecoration
34 import androidx.wear.compose.material.MaterialTheme
35 
36 const val CLICKABLE_SPAN_TAG = "CLICKABLE_SPAN_TAG"
37 
38 @Composable
AnnotatedTextnull39 fun AnnotatedText(text: CharSequence, style: TextStyle, modifier: Modifier = Modifier) {
40     val onClickCallbacks = mutableMapOf<String, (View) -> Unit>()
41     val context = LocalContext.current
42     val listener = LinkInteractionListener {
43         if (it is LinkAnnotation.Clickable) {
44             onClickCallbacks.get(it.tag)?.invoke(View(context))
45         }
46     }
47 
48     val annotatedString =
49         spannableStringToAnnotatedString(text, onClickCallbacks, listener = listener)
50     BasicText(text = annotatedString, style = style, modifier = modifier)
51 }
52 
53 @Composable
spannableStringToAnnotatedStringnull54 private fun spannableStringToAnnotatedString(
55     text: CharSequence,
56     onClickCallbacks: MutableMap<String, (View) -> Unit>,
57     spanColor: Color = MaterialTheme.colors.primary,
58     listener: LinkInteractionListener
59 ) =
60     if (text is Spanned) {
61         buildAnnotatedString {
62             append((text.toString()))
63             for (span in text.getSpans(0, text.length, Any::class.java)) {
64                 val start = text.getSpanStart(span)
65                 val end = text.getSpanEnd(span)
66                 when (span) {
67                     is ClickableSpan ->
68                         addClickableSpan(span, spanColor, start, end, onClickCallbacks, listener)
69                     else -> addStyle(SpanStyle(), start, end)
70                 }
71             }
72         }
73     } else {
74         AnnotatedString(text.toString())
75     }
76 
AnnotatedStringnull77 private fun AnnotatedString.Builder.addClickableSpan(
78     span: ClickableSpan,
79     spanColor: Color,
80     start: Int,
81     end: Int,
82     onClickCallbacks: MutableMap<String, (View) -> Unit>,
83     listener: LinkInteractionListener
84 ) {
85     val key = "${CLICKABLE_SPAN_TAG}:$start:$end"
86     onClickCallbacks[key] = span::onClick
87     addLink(LinkAnnotation.Clickable(key, linkInteractionListener = listener), start, end)
88     addStyle(
89         SpanStyle(color = spanColor, textDecoration = TextDecoration.Underline),
90         start,
91         end,
92     )
93 }
94