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