| /* |
| * Copyright 2020 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 androidx.compose.ui.platform |
| |
| import android.content.ClipData |
| import android.content.ClipDescription.MIMETYPE_TEXT_PLAIN |
| import android.content.Context |
| import android.os.Parcel |
| import android.text.Annotation |
| import android.text.SpannableString |
| import android.text.Spanned |
| import android.util.Base64 |
| import androidx.compose.ui.geometry.Offset |
| import androidx.compose.ui.graphics.Color |
| import androidx.compose.ui.graphics.Shadow |
| import androidx.compose.ui.text.AnnotatedString |
| import androidx.compose.ui.text.SpanStyle |
| import androidx.compose.ui.text.font.FontFamily |
| import androidx.compose.ui.text.font.FontStyle |
| import androidx.compose.ui.text.font.FontSynthesis |
| import androidx.compose.ui.text.font.FontWeight |
| import androidx.compose.ui.text.intl.LocaleList |
| import androidx.compose.ui.text.style.BaselineShift |
| import androidx.compose.ui.text.style.TextDecoration |
| import androidx.compose.ui.text.style.TextGeometricTransform |
| import androidx.compose.ui.unit.ExperimentalUnitApi |
| import androidx.compose.ui.unit.TextUnit |
| import androidx.compose.ui.unit.TextUnitType |
| import androidx.compose.ui.util.fastForEach |
| |
| private const val PLAIN_TEXT_LABEL = "plain text" |
| |
| /** |
| * Android implementation for [ClipboardManager]. |
| */ |
| internal class AndroidClipboardManager internal constructor( |
| private val clipboardManager: android.content.ClipboardManager |
| ) : ClipboardManager { |
| |
| internal constructor(context: Context) : this( |
| context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager |
| ) |
| |
| override fun setText(annotatedString: AnnotatedString) { |
| clipboardManager.setPrimaryClip( |
| ClipData.newPlainText( |
| PLAIN_TEXT_LABEL, |
| annotatedString.convertToCharSequence() |
| ) |
| ) |
| } |
| |
| override fun getText(): AnnotatedString? { |
| return clipboardManager.primaryClip?.let { primaryClip -> |
| if (primaryClip.itemCount > 0) { |
| // note: text may be null, ensure this is null-safe |
| primaryClip.getItemAt(0)?.text.convertToAnnotatedString() |
| } else { |
| null |
| } |
| } |
| } |
| |
| fun hasText() = |
| clipboardManager.primaryClipDescription?.hasMimeType(MIMETYPE_TEXT_PLAIN) ?: false |
| } |
| |
| internal fun CharSequence?.convertToAnnotatedString(): AnnotatedString? { |
| if (this == null) return null |
| if (this !is Spanned) { |
| return AnnotatedString(text = toString()) |
| } |
| val annotations = getSpans(0, length, Annotation::class.java) |
| val spanStyleRanges = mutableListOf<AnnotatedString.Range<SpanStyle>>() |
| for (i in 0..annotations.lastIndex) { |
| val span = annotations[i] |
| if (span.key != "androidx.compose.text.SpanStyle") { |
| continue |
| } |
| val start = getSpanStart(span) |
| val end = getSpanEnd(span) |
| val decodeHelper = DecodeHelper(span.value) |
| val spanStyle = decodeHelper.decodeSpanStyle() |
| spanStyleRanges.add(AnnotatedString.Range(spanStyle, start, end)) |
| } |
| return AnnotatedString(text = toString(), spanStyles = spanStyleRanges) |
| } |
| |
| internal fun AnnotatedString.convertToCharSequence(): CharSequence { |
| if (spanStyles.isEmpty()) { |
| return text |
| } |
| val spannableString = SpannableString(text) |
| // Normally a SpanStyle will take less than 100 bytes. However, fontFeatureSettings is a string |
| // and doesn't have a maximum length defined. Here we set tentatively set maxSize to be 1024. |
| val encodeHelper = EncodeHelper() |
| spanStyles.fastForEach { (spanStyle, start, end) -> |
| encodeHelper.apply { |
| reset() |
| encode(spanStyle) |
| } |
| spannableString.setSpan( |
| Annotation("androidx.compose.text.SpanStyle", encodeHelper.encodedString()), |
| start, |
| end, |
| Spanned.SPAN_EXCLUSIVE_EXCLUSIVE |
| ) |
| } |
| return spannableString |
| } |
| |
| /** |
| * A helper class used to encode SpanStyles into bytes. |
| * Each field of SpanStyle is assigned with an ID. And if a field is not null or Unspecified, it |
| * will be encoded. Otherwise, it will simply be omitted to save space. |
| */ |
| internal class EncodeHelper { |
| private var parcel = Parcel.obtain() |
| |
| fun reset() { |
| parcel.recycle() |
| parcel = Parcel.obtain() |
| } |
| |
| fun encodedString(): String { |
| val bytes = parcel.marshall() |
| return Base64.encodeToString(bytes, Base64.DEFAULT) |
| } |
| |
| fun encode(spanStyle: SpanStyle) { |
| if (spanStyle.color != Color.Unspecified) { |
| encode(COLOR_ID) |
| encode(spanStyle.color) |
| } |
| if (spanStyle.fontSize != TextUnit.Unspecified) { |
| encode(FONT_SIZE_ID) |
| encode(spanStyle.fontSize) |
| } |
| spanStyle.fontWeight?.let { |
| encode(FONT_WEIGHT_ID) |
| encode(it) |
| } |
| |
| spanStyle.fontStyle?.let { |
| encode(FONT_STYLE_ID) |
| encode(it) |
| } |
| |
| spanStyle.fontSynthesis?.let { |
| encode(FONT_SYNTHESIS_ID) |
| encode(it) |
| } |
| |
| spanStyle.fontFeatureSettings?.let { |
| encode(FONT_FEATURE_SETTINGS_ID) |
| encode(it) |
| } |
| |
| if (spanStyle.letterSpacing != TextUnit.Unspecified) { |
| encode(LETTER_SPACING_ID) |
| encode(spanStyle.letterSpacing) |
| } |
| |
| spanStyle.baselineShift?.let { |
| encode(BASELINE_SHIFT_ID) |
| encode(it) |
| } |
| |
| spanStyle.textGeometricTransform?.let { |
| encode(TEXT_GEOMETRIC_TRANSFORM_ID) |
| encode(it) |
| } |
| |
| if (spanStyle.background != Color.Unspecified) { |
| encode(BACKGROUND_ID) |
| encode(spanStyle.background) |
| } |
| |
| spanStyle.textDecoration?.let { |
| encode(TEXT_DECORATION_ID) |
| encode(it) |
| } |
| |
| spanStyle.shadow?.let { |
| encode(SHADOW_ID) |
| encode(it) |
| } |
| } |
| |
| fun encode(color: Color) { |
| encode(color.value) |
| } |
| |
| fun encode(textUnit: TextUnit) { |
| val typeCode = when (textUnit.type) { |
| TextUnitType.Unspecified -> UNIT_TYPE_UNSPECIFIED |
| TextUnitType.Sp -> UNIT_TYPE_SP |
| TextUnitType.Em -> UNIT_TYPE_EM |
| else -> UNIT_TYPE_UNSPECIFIED |
| } |
| encode(typeCode) |
| if (textUnit.type != TextUnitType.Unspecified) { |
| encode(textUnit.value) |
| } |
| } |
| |
| fun encode(fontWeight: FontWeight) { |
| encode(fontWeight.weight) |
| } |
| |
| fun encode(fontStyle: FontStyle) { |
| encode( |
| when (fontStyle) { |
| FontStyle.Normal -> FONT_STYLE_NORMAL |
| FontStyle.Italic -> FONT_STYLE_ITALIC |
| else -> FONT_STYLE_NORMAL |
| } |
| ) |
| } |
| |
| fun encode(fontSynthesis: FontSynthesis) { |
| val value = when (fontSynthesis) { |
| FontSynthesis.None -> FONT_SYNTHESIS_NONE |
| FontSynthesis.All -> FONT_SYNTHESIS_ALL |
| FontSynthesis.Weight -> FONT_SYNTHESIS_WEIGHT |
| FontSynthesis.Style -> FONT_SYNTHESIS_STYLE |
| else -> FONT_SYNTHESIS_NONE |
| } |
| encode(value) |
| } |
| |
| fun encode(baselineShift: BaselineShift) { |
| encode(baselineShift.multiplier) |
| } |
| |
| fun encode(textGeometricTransform: TextGeometricTransform) { |
| encode(textGeometricTransform.scaleX) |
| encode(textGeometricTransform.skewX) |
| } |
| |
| fun encode(textDecoration: TextDecoration) { |
| encode(textDecoration.mask) |
| } |
| |
| fun encode(shadow: Shadow) { |
| encode(shadow.color) |
| encode(shadow.offset.x) |
| encode(shadow.offset.y) |
| encode(shadow.blurRadius) |
| } |
| |
| fun encode(byte: Byte) { |
| parcel.writeByte(byte) |
| } |
| |
| fun encode(int: Int) { |
| parcel.writeInt(int) |
| } |
| |
| fun encode(float: Float) { |
| parcel.writeFloat(float) |
| } |
| |
| fun encode(uLong: ULong) { |
| parcel.writeLong(uLong.toLong()) |
| } |
| |
| fun encode(string: String) { |
| parcel.writeString(string) |
| } |
| } |
| |
| /** |
| * The helper class to decode SpanStyle from a string encoded by [EncodeHelper]. |
| */ |
| internal class DecodeHelper(string: String) { |
| private val parcel = Parcel.obtain() |
| |
| init { |
| val bytes = Base64.decode(string, Base64.DEFAULT) |
| parcel.unmarshall(bytes, 0, bytes.size) |
| parcel.setDataPosition(0) |
| } |
| |
| /** Decode a SpanStyle from a string. */ |
| fun decodeSpanStyle(): SpanStyle { |
| val mutableSpanStyle = MutableSpanStyle() |
| while (parcel.dataAvail() > BYTE_SIZE) { |
| when (decodeByte()) { |
| COLOR_ID -> |
| if (dataAvailable() >= COLOR_SIZE) { |
| mutableSpanStyle.color = decodeColor() |
| } else { |
| break |
| } |
| FONT_SIZE_ID -> |
| if (dataAvailable() >= TEXT_UNIT_SIZE) { |
| mutableSpanStyle.fontSize = decodeTextUnit() |
| } else { |
| break |
| } |
| FONT_WEIGHT_ID -> |
| if (dataAvailable() >= FONT_WEIGHT_SIZE) { |
| mutableSpanStyle.fontWeight = decodeFontWeight() |
| } else { |
| break |
| } |
| FONT_STYLE_ID -> |
| if (dataAvailable() >= FONT_STYLE_SIZE) { |
| mutableSpanStyle.fontStyle = decodeFontStyle() |
| } else { |
| break |
| } |
| FONT_SYNTHESIS_ID -> |
| if (dataAvailable() >= FONT_SYNTHESIS_SIZE) { |
| mutableSpanStyle.fontSynthesis = decodeFontSynthesis() |
| } else { |
| break |
| } |
| FONT_FEATURE_SETTINGS_ID -> |
| mutableSpanStyle.fontFeatureSettings = decodeString() |
| LETTER_SPACING_ID -> |
| if (dataAvailable() >= TEXT_UNIT_SIZE) { |
| mutableSpanStyle.letterSpacing = decodeTextUnit() |
| } else { |
| break |
| } |
| BASELINE_SHIFT_ID -> |
| if (dataAvailable() >= BASELINE_SHIFT_SIZE) { |
| mutableSpanStyle.baselineShift = decodeBaselineShift() |
| } else { |
| break |
| } |
| TEXT_GEOMETRIC_TRANSFORM_ID -> |
| if (dataAvailable() >= TEXT_GEOMETRIC_TRANSFORM_SIZE) { |
| mutableSpanStyle.textGeometricTransform = decodeTextGeometricTransform() |
| } else { |
| break |
| } |
| BACKGROUND_ID -> |
| if (dataAvailable() >= COLOR_SIZE) { |
| mutableSpanStyle.background = decodeColor() |
| } else { |
| break |
| } |
| TEXT_DECORATION_ID -> |
| if (dataAvailable() >= TEXT_DECORATION_SIZE) { |
| mutableSpanStyle.textDecoration = decodeTextDecoration() |
| } else { |
| break |
| } |
| SHADOW_ID -> |
| if (dataAvailable() >= SHADOW_SIZE) { |
| mutableSpanStyle.shadow = decodeShadow() |
| } else { |
| break |
| } |
| } |
| } |
| |
| return mutableSpanStyle.toSpanStyle() |
| } |
| |
| fun decodeColor(): Color { |
| return Color(decodeULong()) |
| } |
| |
| @OptIn(ExperimentalUnitApi::class) |
| fun decodeTextUnit(): TextUnit { |
| val type = when (decodeByte()) { |
| UNIT_TYPE_SP -> TextUnitType.Sp |
| UNIT_TYPE_EM -> TextUnitType.Em |
| else -> TextUnitType.Unspecified |
| } |
| if (type == TextUnitType.Unspecified) { |
| return TextUnit.Unspecified |
| } |
| val value = decodeFloat() |
| return TextUnit(value, type) |
| } |
| |
| @OptIn(ExperimentalUnitApi::class) |
| fun decodeFontWeight(): FontWeight { |
| return FontWeight(decodeInt()) |
| } |
| |
| fun decodeFontStyle(): FontStyle { |
| return when (decodeByte()) { |
| FONT_STYLE_NORMAL -> FontStyle.Normal |
| FONT_STYLE_ITALIC -> FontStyle.Italic |
| else -> FontStyle.Normal |
| } |
| } |
| |
| fun decodeFontSynthesis(): FontSynthesis { |
| return when (decodeByte()) { |
| FONT_SYNTHESIS_NONE -> FontSynthesis.None |
| FONT_SYNTHESIS_ALL -> FontSynthesis.All |
| FONT_SYNTHESIS_STYLE -> FontSynthesis.Style |
| FONT_SYNTHESIS_WEIGHT -> FontSynthesis.Weight |
| else -> FontSynthesis.None |
| } |
| } |
| |
| private fun decodeBaselineShift(): BaselineShift { |
| return BaselineShift(decodeFloat()) |
| } |
| |
| private fun decodeTextGeometricTransform(): TextGeometricTransform { |
| return TextGeometricTransform( |
| scaleX = decodeFloat(), |
| skewX = decodeFloat() |
| ) |
| } |
| |
| private fun decodeTextDecoration(): TextDecoration { |
| val mask = decodeInt() |
| val hasLineThrough = mask and TextDecoration.LineThrough.mask != 0 |
| val hasUnderline = mask and TextDecoration.Underline.mask != 0 |
| return if (hasLineThrough && hasUnderline) { |
| TextDecoration.combine(listOf(TextDecoration.LineThrough, TextDecoration.Underline)) |
| } else if (hasLineThrough) { |
| TextDecoration.LineThrough |
| } else if (hasUnderline) { |
| TextDecoration.Underline |
| } else { |
| TextDecoration.None |
| } |
| } |
| |
| private fun decodeShadow(): Shadow { |
| return Shadow( |
| color = decodeColor(), |
| offset = Offset(decodeFloat(), decodeFloat()), |
| blurRadius = decodeFloat() |
| ) |
| } |
| |
| private fun decodeByte(): Byte { |
| return parcel.readByte() |
| } |
| |
| private fun decodeInt(): Int { |
| return parcel.readInt() |
| } |
| |
| private fun decodeULong(): ULong { |
| return parcel.readLong().toULong() |
| } |
| |
| private fun decodeFloat(): Float { |
| return parcel.readFloat() |
| } |
| |
| private fun decodeString(): String? { |
| return parcel.readString() |
| } |
| |
| private fun dataAvailable(): Int { |
| return parcel.dataAvail() |
| } |
| } |
| |
| private class MutableSpanStyle( |
| var color: Color = Color.Unspecified, |
| var fontSize: TextUnit = TextUnit.Unspecified, |
| var fontWeight: FontWeight? = null, |
| var fontStyle: FontStyle? = null, |
| var fontSynthesis: FontSynthesis? = null, |
| var fontFamily: FontFamily? = null, |
| var fontFeatureSettings: String? = null, |
| var letterSpacing: TextUnit = TextUnit.Unspecified, |
| var baselineShift: BaselineShift? = null, |
| var textGeometricTransform: TextGeometricTransform? = null, |
| var localeList: LocaleList? = null, |
| var background: Color = Color.Unspecified, |
| var textDecoration: TextDecoration? = null, |
| var shadow: Shadow? = null |
| ) { |
| fun toSpanStyle(): SpanStyle { |
| return SpanStyle( |
| color = color, |
| fontSize = fontSize, |
| fontWeight = fontWeight, |
| fontStyle = fontStyle, |
| fontSynthesis = fontSynthesis, |
| fontFamily = fontFamily, |
| fontFeatureSettings = fontFeatureSettings, |
| letterSpacing = letterSpacing, |
| baselineShift = baselineShift, |
| textGeometricTransform = textGeometricTransform, |
| localeList = localeList, |
| background = background, |
| textDecoration = textDecoration, |
| shadow = shadow |
| ) |
| } |
| } |
| |
| private const val UNIT_TYPE_UNSPECIFIED: Byte = 0 |
| private const val UNIT_TYPE_SP: Byte = 1 |
| private const val UNIT_TYPE_EM: Byte = 2 |
| |
| private const val FONT_STYLE_NORMAL: Byte = 0 |
| private const val FONT_STYLE_ITALIC: Byte = 1 |
| |
| private const val FONT_SYNTHESIS_NONE: Byte = 0 |
| private const val FONT_SYNTHESIS_ALL: Byte = 1 |
| private const val FONT_SYNTHESIS_WEIGHT: Byte = 2 |
| private const val FONT_SYNTHESIS_STYLE: Byte = 3 |
| |
| private const val COLOR_ID: Byte = 1 |
| private const val FONT_SIZE_ID: Byte = 2 |
| private const val FONT_WEIGHT_ID: Byte = 3 |
| private const val FONT_STYLE_ID: Byte = 4 |
| private const val FONT_SYNTHESIS_ID: Byte = 5 |
| private const val FONT_FEATURE_SETTINGS_ID: Byte = 6 |
| private const val LETTER_SPACING_ID: Byte = 7 |
| private const val BASELINE_SHIFT_ID: Byte = 8 |
| private const val TEXT_GEOMETRIC_TRANSFORM_ID: Byte = 9 |
| private const val BACKGROUND_ID: Byte = 10 |
| private const val TEXT_DECORATION_ID: Byte = 11 |
| private const val SHADOW_ID: Byte = 12 |
| |
| private const val BYTE_SIZE = 1 |
| private const val INT_SIZE = 4 |
| private const val FLOAT_SIZE = 4 |
| private const val LONG_SIZE = 8 |
| private const val COLOR_SIZE = LONG_SIZE |
| private const val TEXT_UNIT_SIZE = BYTE_SIZE + FLOAT_SIZE |
| private const val FONT_WEIGHT_SIZE = INT_SIZE |
| private const val FONT_STYLE_SIZE = BYTE_SIZE |
| private const val FONT_SYNTHESIS_SIZE = BYTE_SIZE |
| private const val BASELINE_SHIFT_SIZE = FLOAT_SIZE |
| private const val TEXT_GEOMETRIC_TRANSFORM_SIZE = FLOAT_SIZE * 2 |
| private const val TEXT_DECORATION_SIZE = INT_SIZE |
| private const val SHADOW_SIZE = COLOR_SIZE + FLOAT_SIZE * 3 |