1 /*
<lambda>null2  * Copyright 2020 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 androidx.compose.ui.platform
18 
19 import android.content.ClipData
20 import android.content.ClipDescription
21 import android.content.Context
22 import android.os.Build
23 import android.os.Parcel
24 import android.text.Annotation
25 import android.text.SpannableString
26 import android.text.Spanned
27 import android.util.Base64
28 import androidx.annotation.RequiresApi
29 import androidx.compose.ui.geometry.Offset
30 import androidx.compose.ui.graphics.Color
31 import androidx.compose.ui.graphics.Shadow
32 import androidx.compose.ui.text.AnnotatedString
33 import androidx.compose.ui.text.SpanStyle
34 import androidx.compose.ui.text.font.FontFamily
35 import androidx.compose.ui.text.font.FontStyle
36 import androidx.compose.ui.text.font.FontSynthesis
37 import androidx.compose.ui.text.font.FontWeight
38 import androidx.compose.ui.text.intl.LocaleList
39 import androidx.compose.ui.text.style.BaselineShift
40 import androidx.compose.ui.text.style.TextDecoration
41 import androidx.compose.ui.text.style.TextGeometricTransform
42 import androidx.compose.ui.unit.ExperimentalUnitApi
43 import androidx.compose.ui.unit.TextUnit
44 import androidx.compose.ui.unit.TextUnitType
45 import androidx.compose.ui.util.fastForEach
46 
47 private const val PLAIN_TEXT_LABEL = "plain text"
48 
49 /** Android implementation for [ClipboardManager]. */
50 @Suppress("DEPRECATION")
51 internal class AndroidClipboardManager
52 internal constructor(private val clipboardManager: android.content.ClipboardManager) :
53     ClipboardManager {
54 
55     internal constructor(
56         context: Context
57     ) : this(
58         context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
59     )
60 
61     override fun setText(annotatedString: AnnotatedString) {
62         clipboardManager.setPrimaryClip(
63             ClipData.newPlainText(PLAIN_TEXT_LABEL, annotatedString.convertToCharSequence())
64         )
65     }
66 
67     override fun getText(): AnnotatedString? {
68         return clipboardManager.primaryClip?.let { primaryClip ->
69             if (primaryClip.itemCount > 0) {
70                 // note: text may be null, ensure this is null-safe
71                 primaryClip.getItemAt(0)?.text.convertToAnnotatedString()
72             } else {
73                 null
74             }
75         }
76     }
77 
78     override fun hasText() = clipboardManager.primaryClipDescription?.hasMimeType("text/*") ?: false
79 
80     override fun getClip(): ClipEntry? {
81         return clipboardManager.primaryClip?.let(::ClipEntry)
82     }
83 
84     override fun setClip(clipEntry: ClipEntry?) {
85         if (clipEntry == null) {
86             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
87                 Api28ClipboardManagerClipClear.clearPrimaryClip(clipboardManager)
88             } else {
89                 clipboardManager.setPrimaryClip(ClipData.newPlainText("", ""))
90             }
91         } else {
92             clipboardManager.setPrimaryClip(clipEntry.clipData)
93         }
94     }
95 
96     override val nativeClipboard: NativeClipboard
97         get() = clipboardManager
98 }
99 
100 /** Android specific class that contains the primary clip in [android.content.ClipboardManager]. */
101 // Defining this class not as a typealias but a wrapper gives us flexibility in the future to
102 // add more functionality in it.
103 actual class ClipEntry(val clipData: ClipData) {
104 
105     actual val clipMetadata: ClipMetadata
106         get() = clipData.description.toClipMetadata()
107 }
108 
toClipEntrynull109 fun ClipData.toClipEntry(): ClipEntry = ClipEntry(this)
110 
111 /**
112  * Android specific class that contains the metadata of primary clip in
113  * [android.content.ClipboardManager]
114  */
115 // Defining this class not as a typealias but a wrapper gives us flexibility in the future to
116 // add more functionality in it.
117 actual class ClipMetadata(val clipDescription: ClipDescription)
118 
119 fun ClipDescription.toClipMetadata(): ClipMetadata = ClipMetadata(this)
120 
121 actual typealias NativeClipboard = android.content.ClipboardManager
122 
123 @RequiresApi(28)
124 private object Api28ClipboardManagerClipClear {
125 
126     @JvmStatic
127     fun clearPrimaryClip(clipboardManager: android.content.ClipboardManager) {
128         clipboardManager.clearPrimaryClip()
129     }
130 }
131 
convertToAnnotatedStringnull132 internal fun CharSequence?.convertToAnnotatedString(): AnnotatedString? {
133     if (this == null) return null
134     if (this !is Spanned) {
135         return AnnotatedString(text = toString())
136     }
137     val annotations = getSpans(0, length, Annotation::class.java)
138     val spanStyleRanges = mutableListOf<AnnotatedString.Range<SpanStyle>>()
139     for (i in 0..annotations.lastIndex) {
140         val span = annotations[i]
141         if (span.key != "androidx.compose.text.SpanStyle") {
142             continue
143         }
144         val start = getSpanStart(span)
145         val end = getSpanEnd(span)
146         val decodeHelper = DecodeHelper(span.value)
147         val spanStyle = decodeHelper.decodeSpanStyle()
148         spanStyleRanges.add(AnnotatedString.Range(spanStyle, start, end))
149     }
150     return AnnotatedString(text = toString(), spanStyles = spanStyleRanges)
151 }
152 
convertToCharSequencenull153 internal fun AnnotatedString.convertToCharSequence(): CharSequence {
154     if (spanStyles.isEmpty()) {
155         return text
156     }
157     val spannableString = SpannableString(text)
158     // Normally a SpanStyle will take less than 100 bytes. However, fontFeatureSettings is a string
159     // and doesn't have a maximum length defined. Here we set tentatively set maxSize to be 1024.
160     val encodeHelper = EncodeHelper()
161     spanStyles.fastForEach { (spanStyle, start, end) ->
162         encodeHelper.apply {
163             reset()
164             encode(spanStyle)
165         }
166         spannableString.setSpan(
167             Annotation("androidx.compose.text.SpanStyle", encodeHelper.encodedString()),
168             start,
169             end,
170             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
171         )
172     }
173     return spannableString
174 }
175 
176 /**
177  * A helper class used to encode SpanStyles into bytes. Each field of SpanStyle is assigned with an
178  * ID. And if a field is not null or Unspecified, it will be encoded. Otherwise, it will simply be
179  * omitted to save space.
180  */
181 internal class EncodeHelper {
182     private var parcel = Parcel.obtain()
183 
resetnull184     fun reset() {
185         parcel.recycle()
186         parcel = Parcel.obtain()
187     }
188 
encodedStringnull189     fun encodedString(): String {
190         val bytes = parcel.marshall()
191         return Base64.encodeToString(bytes, Base64.DEFAULT)
192     }
193 
encodenull194     fun encode(spanStyle: SpanStyle) {
195         if (spanStyle.color != Color.Unspecified) {
196             encode(COLOR_ID)
197             encode(spanStyle.color)
198         }
199         if (spanStyle.fontSize != TextUnit.Unspecified) {
200             encode(FONT_SIZE_ID)
201             encode(spanStyle.fontSize)
202         }
203         spanStyle.fontWeight?.let {
204             encode(FONT_WEIGHT_ID)
205             encode(it)
206         }
207 
208         spanStyle.fontStyle?.let {
209             encode(FONT_STYLE_ID)
210             encode(it)
211         }
212 
213         spanStyle.fontSynthesis?.let {
214             encode(FONT_SYNTHESIS_ID)
215             encode(it)
216         }
217 
218         spanStyle.fontFeatureSettings?.let {
219             encode(FONT_FEATURE_SETTINGS_ID)
220             encode(it)
221         }
222 
223         if (spanStyle.letterSpacing != TextUnit.Unspecified) {
224             encode(LETTER_SPACING_ID)
225             encode(spanStyle.letterSpacing)
226         }
227 
228         spanStyle.baselineShift?.let {
229             encode(BASELINE_SHIFT_ID)
230             encode(it)
231         }
232 
233         spanStyle.textGeometricTransform?.let {
234             encode(TEXT_GEOMETRIC_TRANSFORM_ID)
235             encode(it)
236         }
237 
238         if (spanStyle.background != Color.Unspecified) {
239             encode(BACKGROUND_ID)
240             encode(spanStyle.background)
241         }
242 
243         spanStyle.textDecoration?.let {
244             encode(TEXT_DECORATION_ID)
245             encode(it)
246         }
247 
248         spanStyle.shadow?.let {
249             encode(SHADOW_ID)
250             encode(it)
251         }
252     }
253 
encodenull254     fun encode(color: Color) {
255         encode(color.value)
256     }
257 
encodenull258     fun encode(textUnit: TextUnit) {
259         val typeCode =
260             when (textUnit.type) {
261                 TextUnitType.Unspecified -> UNIT_TYPE_UNSPECIFIED
262                 TextUnitType.Sp -> UNIT_TYPE_SP
263                 TextUnitType.Em -> UNIT_TYPE_EM
264                 else -> UNIT_TYPE_UNSPECIFIED
265             }
266         encode(typeCode)
267         if (textUnit.type != TextUnitType.Unspecified) {
268             encode(textUnit.value)
269         }
270     }
271 
encodenull272     fun encode(fontWeight: FontWeight) {
273         encode(fontWeight.weight)
274     }
275 
encodenull276     fun encode(fontStyle: FontStyle) {
277         encode(
278             when (fontStyle) {
279                 FontStyle.Normal -> FONT_STYLE_NORMAL
280                 FontStyle.Italic -> FONT_STYLE_ITALIC
281                 else -> FONT_STYLE_NORMAL
282             }
283         )
284     }
285 
encodenull286     fun encode(fontSynthesis: FontSynthesis) {
287         val value =
288             when (fontSynthesis) {
289                 FontSynthesis.None -> FONT_SYNTHESIS_NONE
290                 FontSynthesis.All -> FONT_SYNTHESIS_ALL
291                 FontSynthesis.Weight -> FONT_SYNTHESIS_WEIGHT
292                 FontSynthesis.Style -> FONT_SYNTHESIS_STYLE
293                 else -> FONT_SYNTHESIS_NONE
294             }
295         encode(value)
296     }
297 
encodenull298     fun encode(baselineShift: BaselineShift) {
299         encode(baselineShift.multiplier)
300     }
301 
encodenull302     fun encode(textGeometricTransform: TextGeometricTransform) {
303         encode(textGeometricTransform.scaleX)
304         encode(textGeometricTransform.skewX)
305     }
306 
encodenull307     fun encode(textDecoration: TextDecoration) {
308         encode(textDecoration.mask)
309     }
310 
encodenull311     fun encode(shadow: Shadow) {
312         encode(shadow.color)
313         encode(shadow.offset.x)
314         encode(shadow.offset.y)
315         encode(shadow.blurRadius)
316     }
317 
encodenull318     fun encode(byte: Byte) {
319         parcel.writeByte(byte)
320     }
321 
encodenull322     fun encode(int: Int) {
323         parcel.writeInt(int)
324     }
325 
encodenull326     fun encode(float: Float) {
327         parcel.writeFloat(float)
328     }
329 
encodenull330     fun encode(uLong: ULong) {
331         parcel.writeLong(uLong.toLong())
332     }
333 
encodenull334     fun encode(string: String) {
335         parcel.writeString(string)
336     }
337 }
338 
339 /** The helper class to decode SpanStyle from a string encoded by [EncodeHelper]. */
340 internal class DecodeHelper(string: String) {
341     private val parcel = Parcel.obtain()
342 
343     init {
344         val bytes = Base64.decode(string, Base64.DEFAULT)
345         parcel.unmarshall(bytes, 0, bytes.size)
346         parcel.setDataPosition(0)
347     }
348 
349     /** Decode a SpanStyle from a string. */
decodeSpanStylenull350     fun decodeSpanStyle(): SpanStyle {
351         val mutableSpanStyle = MutableSpanStyle()
352         while (parcel.dataAvail() > BYTE_SIZE) {
353             when (decodeByte()) {
354                 COLOR_ID ->
355                     if (dataAvailable() >= COLOR_SIZE) {
356                         mutableSpanStyle.color = decodeColor()
357                     } else {
358                         break
359                     }
360                 FONT_SIZE_ID ->
361                     if (dataAvailable() >= TEXT_UNIT_SIZE) {
362                         mutableSpanStyle.fontSize = decodeTextUnit()
363                     } else {
364                         break
365                     }
366                 FONT_WEIGHT_ID ->
367                     if (dataAvailable() >= FONT_WEIGHT_SIZE) {
368                         mutableSpanStyle.fontWeight = decodeFontWeight()
369                     } else {
370                         break
371                     }
372                 FONT_STYLE_ID ->
373                     if (dataAvailable() >= FONT_STYLE_SIZE) {
374                         mutableSpanStyle.fontStyle = decodeFontStyle()
375                     } else {
376                         break
377                     }
378                 FONT_SYNTHESIS_ID ->
379                     if (dataAvailable() >= FONT_SYNTHESIS_SIZE) {
380                         mutableSpanStyle.fontSynthesis = decodeFontSynthesis()
381                     } else {
382                         break
383                     }
384                 FONT_FEATURE_SETTINGS_ID -> mutableSpanStyle.fontFeatureSettings = decodeString()
385                 LETTER_SPACING_ID ->
386                     if (dataAvailable() >= TEXT_UNIT_SIZE) {
387                         mutableSpanStyle.letterSpacing = decodeTextUnit()
388                     } else {
389                         break
390                     }
391                 BASELINE_SHIFT_ID ->
392                     if (dataAvailable() >= BASELINE_SHIFT_SIZE) {
393                         mutableSpanStyle.baselineShift = decodeBaselineShift()
394                     } else {
395                         break
396                     }
397                 TEXT_GEOMETRIC_TRANSFORM_ID ->
398                     if (dataAvailable() >= TEXT_GEOMETRIC_TRANSFORM_SIZE) {
399                         mutableSpanStyle.textGeometricTransform = decodeTextGeometricTransform()
400                     } else {
401                         break
402                     }
403                 BACKGROUND_ID ->
404                     if (dataAvailable() >= COLOR_SIZE) {
405                         mutableSpanStyle.background = decodeColor()
406                     } else {
407                         break
408                     }
409                 TEXT_DECORATION_ID ->
410                     if (dataAvailable() >= TEXT_DECORATION_SIZE) {
411                         mutableSpanStyle.textDecoration = decodeTextDecoration()
412                     } else {
413                         break
414                     }
415                 SHADOW_ID ->
416                     if (dataAvailable() >= SHADOW_SIZE) {
417                         mutableSpanStyle.shadow = decodeShadow()
418                     } else {
419                         break
420                     }
421             }
422         }
423 
424         return mutableSpanStyle.toSpanStyle()
425     }
426 
decodeColornull427     fun decodeColor(): Color {
428         return Color(decodeULong())
429     }
430 
431     @OptIn(ExperimentalUnitApi::class)
decodeTextUnitnull432     fun decodeTextUnit(): TextUnit {
433         val type =
434             when (decodeByte()) {
435                 UNIT_TYPE_SP -> TextUnitType.Sp
436                 UNIT_TYPE_EM -> TextUnitType.Em
437                 else -> TextUnitType.Unspecified
438             }
439         if (type == TextUnitType.Unspecified) {
440             return TextUnit.Unspecified
441         }
442         val value = decodeFloat()
443         return TextUnit(value, type)
444     }
445 
446     @OptIn(ExperimentalUnitApi::class)
decodeFontWeightnull447     fun decodeFontWeight(): FontWeight {
448         return FontWeight(decodeInt())
449     }
450 
decodeFontStylenull451     fun decodeFontStyle(): FontStyle {
452         return when (decodeByte()) {
453             FONT_STYLE_NORMAL -> FontStyle.Normal
454             FONT_STYLE_ITALIC -> FontStyle.Italic
455             else -> FontStyle.Normal
456         }
457     }
458 
decodeFontSynthesisnull459     fun decodeFontSynthesis(): FontSynthesis {
460         return when (decodeByte()) {
461             FONT_SYNTHESIS_NONE -> FontSynthesis.None
462             FONT_SYNTHESIS_ALL -> FontSynthesis.All
463             FONT_SYNTHESIS_STYLE -> FontSynthesis.Style
464             FONT_SYNTHESIS_WEIGHT -> FontSynthesis.Weight
465             else -> FontSynthesis.None
466         }
467     }
468 
decodeBaselineShiftnull469     private fun decodeBaselineShift(): BaselineShift {
470         return BaselineShift(decodeFloat())
471     }
472 
decodeTextGeometricTransformnull473     private fun decodeTextGeometricTransform(): TextGeometricTransform {
474         return TextGeometricTransform(scaleX = decodeFloat(), skewX = decodeFloat())
475     }
476 
decodeTextDecorationnull477     private fun decodeTextDecoration(): TextDecoration {
478         val mask = decodeInt()
479         val hasLineThrough = mask and TextDecoration.LineThrough.mask != 0
480         val hasUnderline = mask and TextDecoration.Underline.mask != 0
481         return if (hasLineThrough && hasUnderline) {
482             TextDecoration.combine(listOf(TextDecoration.LineThrough, TextDecoration.Underline))
483         } else if (hasLineThrough) {
484             TextDecoration.LineThrough
485         } else if (hasUnderline) {
486             TextDecoration.Underline
487         } else {
488             TextDecoration.None
489         }
490     }
491 
decodeShadownull492     private fun decodeShadow(): Shadow {
493         return Shadow(
494             color = decodeColor(),
495             offset = Offset(decodeFloat(), decodeFloat()),
496             blurRadius = decodeFloat()
497         )
498     }
499 
decodeBytenull500     private fun decodeByte(): Byte {
501         return parcel.readByte()
502     }
503 
decodeIntnull504     private fun decodeInt(): Int {
505         return parcel.readInt()
506     }
507 
decodeULongnull508     private fun decodeULong(): ULong {
509         return parcel.readLong().toULong()
510     }
511 
decodeFloatnull512     private fun decodeFloat(): Float {
513         return parcel.readFloat()
514     }
515 
decodeStringnull516     private fun decodeString(): String? {
517         return parcel.readString()
518     }
519 
dataAvailablenull520     private fun dataAvailable(): Int {
521         return parcel.dataAvail()
522     }
523 }
524 
525 private class MutableSpanStyle(
526     var color: Color = Color.Unspecified,
527     var fontSize: TextUnit = TextUnit.Unspecified,
528     var fontWeight: FontWeight? = null,
529     var fontStyle: FontStyle? = null,
530     var fontSynthesis: FontSynthesis? = null,
531     var fontFamily: FontFamily? = null,
532     var fontFeatureSettings: String? = null,
533     var letterSpacing: TextUnit = TextUnit.Unspecified,
534     var baselineShift: BaselineShift? = null,
535     var textGeometricTransform: TextGeometricTransform? = null,
536     var localeList: LocaleList? = null,
537     var background: Color = Color.Unspecified,
538     var textDecoration: TextDecoration? = null,
539     var shadow: Shadow? = null
540 ) {
toSpanStylenull541     fun toSpanStyle(): SpanStyle {
542         return SpanStyle(
543             color = color,
544             fontSize = fontSize,
545             fontWeight = fontWeight,
546             fontStyle = fontStyle,
547             fontSynthesis = fontSynthesis,
548             fontFamily = fontFamily,
549             fontFeatureSettings = fontFeatureSettings,
550             letterSpacing = letterSpacing,
551             baselineShift = baselineShift,
552             textGeometricTransform = textGeometricTransform,
553             localeList = localeList,
554             background = background,
555             textDecoration = textDecoration,
556             shadow = shadow
557         )
558     }
559 }
560 
561 private const val UNIT_TYPE_UNSPECIFIED: Byte = 0
562 private const val UNIT_TYPE_SP: Byte = 1
563 private const val UNIT_TYPE_EM: Byte = 2
564 
565 private const val FONT_STYLE_NORMAL: Byte = 0
566 private const val FONT_STYLE_ITALIC: Byte = 1
567 
568 private const val FONT_SYNTHESIS_NONE: Byte = 0
569 private const val FONT_SYNTHESIS_ALL: Byte = 1
570 private const val FONT_SYNTHESIS_WEIGHT: Byte = 2
571 private const val FONT_SYNTHESIS_STYLE: Byte = 3
572 
573 private const val COLOR_ID: Byte = 1
574 private const val FONT_SIZE_ID: Byte = 2
575 private const val FONT_WEIGHT_ID: Byte = 3
576 private const val FONT_STYLE_ID: Byte = 4
577 private const val FONT_SYNTHESIS_ID: Byte = 5
578 private const val FONT_FEATURE_SETTINGS_ID: Byte = 6
579 private const val LETTER_SPACING_ID: Byte = 7
580 private const val BASELINE_SHIFT_ID: Byte = 8
581 private const val TEXT_GEOMETRIC_TRANSFORM_ID: Byte = 9
582 private const val BACKGROUND_ID: Byte = 10
583 private const val TEXT_DECORATION_ID: Byte = 11
584 private const val SHADOW_ID: Byte = 12
585 
586 private const val BYTE_SIZE = 1
587 private const val INT_SIZE = 4
588 private const val FLOAT_SIZE = 4
589 private const val LONG_SIZE = 8
590 private const val COLOR_SIZE = LONG_SIZE
591 private const val TEXT_UNIT_SIZE = BYTE_SIZE + FLOAT_SIZE
592 private const val FONT_WEIGHT_SIZE = INT_SIZE
593 private const val FONT_STYLE_SIZE = BYTE_SIZE
594 private const val FONT_SYNTHESIS_SIZE = BYTE_SIZE
595 private const val BASELINE_SHIFT_SIZE = FLOAT_SIZE
596 private const val TEXT_GEOMETRIC_TRANSFORM_SIZE = FLOAT_SIZE * 2
597 private const val TEXT_DECORATION_SIZE = INT_SIZE
598 private const val SHADOW_SIZE = COLOR_SIZE + FLOAT_SIZE * 3
599