From 36119106a9d3a29ff40f214d8b46190d17d14245 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Fri, 15 Sep 2023 11:16:30 +0100 Subject: [PATCH 1/2] [Rich text editor] Update design (#1332) * Fix composer height and padding * Update plus and cancel icons with design system icons * Update border so that it is always visible * Update placeholder color * Update send and tick icons * Update design of formatting buttons * Update RTE icons * Reduce bottom padding --------- Co-authored-by: ElementBot --- changelog.d/1332.feature | 1 + .../libraries/designsystem/VectorIcons.kt | 2 + .../designsystem/theme/ColorAliases.kt | 11 +++ .../src/main/res/drawable/ic_bold.xml | 10 +-- .../src/main/res/drawable/ic_bullet_list.xml | 10 +-- .../src/main/res/drawable/ic_cancel.xml | 26 +++++++ .../src/main/res/drawable/ic_code_block.xml | 10 +-- .../main/res/drawable/ic_indent_decrease.xml | 10 +-- .../main/res/drawable/ic_indent_increase.xml | 10 +-- .../src/main/res/drawable/ic_inline_code.xml | 14 ++-- .../src/main/res/drawable/ic_italic.xml | 10 +-- .../src/main/res/drawable/ic_link.xml | 10 +-- .../main/res/drawable/ic_numbered_list.xml | 10 +-- .../src/main/res/drawable/ic_plus.xml | 29 ++++++++ .../src/main/res/drawable/ic_quote.xml | 16 ++--- .../main/res/drawable/ic_strikethrough.xml | 10 +-- .../src/main/res/drawable/ic_underline.xml | 10 +-- .../libraries/textcomposer/TextComposer.kt | 29 ++++---- .../components/FormattingOption.kt | 69 ++++++++++++++++--- .../impl/src/main/res/drawable/ic_cancel.xml | 9 --- .../impl/src/main/res/drawable/ic_plus.xml | 9 --- .../impl/src/main/res/drawable/ic_send.xml | 28 ++++++-- .../impl/src/main/res/drawable/ic_tick.xml | 10 +-- ...poserViewDark_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...oserViewLight_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...sagesViewDark_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...sagesViewDark_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...sagesViewDark_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...sagesViewDark_0_null_4,NEXUS_5,1.0,en].png | 4 +- ...sagesViewDark_0_null_5,NEXUS_5,1.0,en].png | 4 +- ...agesViewLight_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...agesViewLight_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...agesViewLight_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...agesViewLight_0_null_4,NEXUS_5,1.0,en].png | 4 +- ...agesViewLight_0_null_5,NEXUS_5,1.0,en].png | 4 +- ...olorAliasesDark_0_null,NEXUS_5,1.0,en].png | 4 +- ...lorAliasesLight_0_null,NEXUS_5,1.0,en].png | 4 +- ...ttingButton-D-4_5_null,NEXUS_5,1.0,en].png | 3 + ...ttingButton-N-4_6_null,NEXUS_5,1.0,en].png | 3 + ...omposerEdit-D-2_3_null,NEXUS_5,1.0,en].png | 4 +- ...omposerEdit-N-2_4_null,NEXUS_5,1.0,en].png | 4 +- ...rFormatting-D-1_2_null,NEXUS_5,1.0,en].png | 4 +- ...rFormatting-N-1_3_null,NEXUS_5,1.0,en].png | 4 +- ...mposerReply-D-3_4_null,NEXUS_5,1.0,en].png | 4 +- ...mposerReply-N-3_5_null,NEXUS_5,1.0,en].png | 4 +- ...poserSimple-D-0_1_null,NEXUS_5,1.0,en].png | 4 +- ...poserSimple-N-0_2_null,NEXUS_5,1.0,en].png | 4 +- 47 files changed, 282 insertions(+), 165 deletions(-) create mode 100644 changelog.d/1332.feature create mode 100644 libraries/designsystem/src/main/res/drawable/ic_cancel.xml create mode 100644 libraries/designsystem/src/main/res/drawable/ic_plus.xml delete mode 100644 libraries/textcomposer/impl/src/main/res/drawable/ic_cancel.xml delete mode 100644 libraries/textcomposer/impl/src/main/res/drawable/ic_plus.xml create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-D-4_5_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-N-4_6_null,NEXUS_5,1.0,en].png diff --git a/changelog.d/1332.feature b/changelog.d/1332.feature new file mode 100644 index 0000000000..2a08ae9d25 --- /dev/null +++ b/changelog.d/1332.feature @@ -0,0 +1 @@ +[Rich text editor] Update design \ No newline at end of file diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt index 0194e86518..5f9764d8f9 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt @@ -42,4 +42,6 @@ object VectorIcons { val Strikethrough = R.drawable.ic_strikethrough val Underline = R.drawable.ic_underline val ThreadDecoration = R.drawable.ic_thread_decoration + val Plus = R.drawable.ic_plus + val Cancel = R.drawable.ic_cancel } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt index 77ca4bd010..e7878f9bed 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt @@ -75,6 +75,16 @@ val SemanticColors.progressIndicatorTrackColor Color(0x25F4F7FA) } +// This color is not present in Semantic color, so put hard-coded value for now +val SemanticColors.iconSuccessPrimaryBackground + get() = if (isLight) { + // We want LightDesignTokens.colorGreen300 + Color(0xffe3f7ed) + } else { + // We want DarkDesignTokens.colorGreen300 + Color(0xff002513) + } + // Temporary color, which is not in the token right now val SemanticColors.temporaryColorBgSpecial get() = if (isLight) Color(0xFFE4E8F0) else Color(0xFF3A4048) @@ -102,6 +112,7 @@ private fun ContentToPreview() { "messageFromOtherBackground" to ElementTheme.colors.messageFromOtherBackground, "progressIndicatorTrackColor" to ElementTheme.colors.progressIndicatorTrackColor, "temporaryColorBgSpecial" to ElementTheme.colors.temporaryColorBgSpecial, + "iconSuccessPrimaryBackground" to ElementTheme.colors.iconSuccessPrimaryBackground, ) ) } diff --git a/libraries/designsystem/src/main/res/drawable/ic_bold.xml b/libraries/designsystem/src/main/res/drawable/ic_bold.xml index c361f85d3d..5a08fee2f3 100644 --- a/libraries/designsystem/src/main/res/drawable/ic_bold.xml +++ b/libraries/designsystem/src/main/res/drawable/ic_bold.xml @@ -1,9 +1,9 @@ + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> diff --git a/libraries/designsystem/src/main/res/drawable/ic_bullet_list.xml b/libraries/designsystem/src/main/res/drawable/ic_bullet_list.xml index 72d8324622..103d0b380d 100644 --- a/libraries/designsystem/src/main/res/drawable/ic_bullet_list.xml +++ b/libraries/designsystem/src/main/res/drawable/ic_bullet_list.xml @@ -1,9 +1,9 @@ + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> diff --git a/libraries/designsystem/src/main/res/drawable/ic_cancel.xml b/libraries/designsystem/src/main/res/drawable/ic_cancel.xml new file mode 100644 index 0000000000..3e4ee21aee --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_cancel.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_code_block.xml b/libraries/designsystem/src/main/res/drawable/ic_code_block.xml index 6e622f5b27..18279bd8b5 100644 --- a/libraries/designsystem/src/main/res/drawable/ic_code_block.xml +++ b/libraries/designsystem/src/main/res/drawable/ic_code_block.xml @@ -1,9 +1,9 @@ + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> diff --git a/libraries/designsystem/src/main/res/drawable/ic_indent_decrease.xml b/libraries/designsystem/src/main/res/drawable/ic_indent_decrease.xml index 5a0b284223..181f94c012 100644 --- a/libraries/designsystem/src/main/res/drawable/ic_indent_decrease.xml +++ b/libraries/designsystem/src/main/res/drawable/ic_indent_decrease.xml @@ -1,9 +1,9 @@ + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> diff --git a/libraries/designsystem/src/main/res/drawable/ic_indent_increase.xml b/libraries/designsystem/src/main/res/drawable/ic_indent_increase.xml index 367686ceac..06a9ede8d5 100644 --- a/libraries/designsystem/src/main/res/drawable/ic_indent_increase.xml +++ b/libraries/designsystem/src/main/res/drawable/ic_indent_increase.xml @@ -1,9 +1,9 @@ + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> diff --git a/libraries/designsystem/src/main/res/drawable/ic_inline_code.xml b/libraries/designsystem/src/main/res/drawable/ic_inline_code.xml index c0dc504ed9..c15248f8ea 100644 --- a/libraries/designsystem/src/main/res/drawable/ic_inline_code.xml +++ b/libraries/designsystem/src/main/res/drawable/ic_inline_code.xml @@ -1,15 +1,15 @@ + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> diff --git a/libraries/designsystem/src/main/res/drawable/ic_italic.xml b/libraries/designsystem/src/main/res/drawable/ic_italic.xml index f640c109f4..0a389dbf15 100644 --- a/libraries/designsystem/src/main/res/drawable/ic_italic.xml +++ b/libraries/designsystem/src/main/res/drawable/ic_italic.xml @@ -1,9 +1,9 @@ + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> diff --git a/libraries/designsystem/src/main/res/drawable/ic_link.xml b/libraries/designsystem/src/main/res/drawable/ic_link.xml index fd69ec4703..c8a37cdda2 100644 --- a/libraries/designsystem/src/main/res/drawable/ic_link.xml +++ b/libraries/designsystem/src/main/res/drawable/ic_link.xml @@ -1,9 +1,9 @@ + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> diff --git a/libraries/designsystem/src/main/res/drawable/ic_numbered_list.xml b/libraries/designsystem/src/main/res/drawable/ic_numbered_list.xml index f4f5862656..63e7269508 100644 --- a/libraries/designsystem/src/main/res/drawable/ic_numbered_list.xml +++ b/libraries/designsystem/src/main/res/drawable/ic_numbered_list.xml @@ -1,9 +1,9 @@ + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> diff --git a/libraries/designsystem/src/main/res/drawable/ic_plus.xml b/libraries/designsystem/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000000..159ed32e1a --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_quote.xml b/libraries/designsystem/src/main/res/drawable/ic_quote.xml index e17565a6cc..8f4768f818 100644 --- a/libraries/designsystem/src/main/res/drawable/ic_quote.xml +++ b/libraries/designsystem/src/main/res/drawable/ic_quote.xml @@ -1,18 +1,18 @@ + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> diff --git a/libraries/designsystem/src/main/res/drawable/ic_strikethrough.xml b/libraries/designsystem/src/main/res/drawable/ic_strikethrough.xml index d1994f8045..4469c5572d 100644 --- a/libraries/designsystem/src/main/res/drawable/ic_strikethrough.xml +++ b/libraries/designsystem/src/main/res/drawable/ic_strikethrough.xml @@ -1,9 +1,9 @@ + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> diff --git a/libraries/designsystem/src/main/res/drawable/ic_underline.xml b/libraries/designsystem/src/main/res/drawable/ic_underline.xml index 09f92f2104..9da2f2e0b4 100644 --- a/libraries/designsystem/src/main/res/drawable/ic_underline.xml +++ b/libraries/designsystem/src/main/res/drawable/ic_underline.xml @@ -1,9 +1,9 @@ + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index b7c2b8ea40..d8906a49b0 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -114,7 +114,7 @@ fun TextComposer( start = 3.dp, end = 6.dp, top = 8.dp, - bottom = 5.dp, + bottom = 4.dp, ) .fillMaxWidth(), ) { @@ -137,7 +137,7 @@ fun TextComposer( ) { Icon( modifier = Modifier.size(30.dp.applyScaleUp()), - resourceId = R.drawable.ic_plus, // TODO Replace with design system icon when available + resourceId = VectorIcons.Plus, contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment), tint = ElementTheme.colors.iconPrimary, ) @@ -146,7 +146,7 @@ fun TextComposer( val roundCornerLarge = 28.dp.applyScaleUp() val roundedCornerSize = remember(state.lineCount, composerMode) { - if (state.lineCount > 1 || composerMode is MessageComposerMode.Special) { + if (composerMode is MessageComposerMode.Special) { roundCornerSmall } else { roundCornerLarge @@ -156,17 +156,13 @@ fun TextComposer( targetValue = roundedCornerSize, animationSpec = tween( durationMillis = 100, - ) + ), + label = "roundedCornerSizeAnimation" ) val roundedCorners = RoundedCornerShape(roundedCornerSizeState.value) val colors = ElementTheme.colors val bgColor = colors.bgSubtleSecondary - - val borderColor by remember(state.hasFocus, colors) { - derivedStateOf { - if (state.hasFocus) colors.borderDisabled else bgColor - } - } + val borderColor = colors.borderDisabled Column( modifier = Modifier @@ -180,7 +176,7 @@ fun TextComposer( .fillMaxWidth() .clip(roundedCorners) .background(color = bgColor) - .border(1.dp, borderColor, roundedCorners) + .border(0.5.dp, borderColor, roundedCorners) ) { if (composerMode is MessageComposerMode.Special) { ComposerModeView(composerMode = composerMode, onResetComposerMode = onResetComposerMode) @@ -272,7 +268,7 @@ private fun TextInput( Text( placeholder, style = defaultTypography.copy( - color = ElementTheme.colors.textDisabled, + color = ElementTheme.colors.textSecondary, ), ) } @@ -280,6 +276,7 @@ private fun TextInput( RichTextEditor( state = state, modifier = Modifier + .padding(top = 6.dp, bottom = 6.dp) .fillMaxWidth(), style = RichTextEditorDefaults.style( text = RichTextEditorDefaults.textStyle( @@ -323,7 +320,7 @@ private fun TextFormatting( ) { Icon( modifier = Modifier.size(30.dp.applyScaleUp()), - resourceId = R.drawable.ic_cancel, // TODO Replace with design system icon when available + resourceId = VectorIcons.Cancel, contentDescription = stringResource(CommonStrings.action_close), tint = ElementTheme.colors.iconPrimary, ) @@ -335,8 +332,8 @@ private fun TextFormatting( .constrainAs(formatting) { top.linkTo(parent.top) bottom.linkTo(parent.bottom) - start.linkTo(close.end, margin = 3.dp) - end.linkTo(send.start, margin = 20.dp) + start.linkTo(close.end, margin = 1.dp) + end.linkTo(send.start, margin = 14.dp) width = fillToConstraints } .horizontalScroll(scrollState), @@ -589,7 +586,7 @@ private fun SendButton( ) { Icon( modifier = Modifier - .height(18.dp.applyScaleUp()) + .height(24.dp.applyScaleUp()) .align(Alignment.Center), resourceId = iconId, contentDescription = contentDescription, diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt index 7eb1d08293..a3635b28c3 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt @@ -18,17 +18,26 @@ package io.element.android.libraries.textcomposer.components import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.VectorIcons +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.text.applyScaleUp import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.iconSuccessPrimaryBackground import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.theme.compound.generated.SemanticColors @@ -42,27 +51,65 @@ internal fun FormattingOption( colors: SemanticColors = ElementTheme.colors, ) { val backgroundColor = when (state) { - FormattingOptionState.Selected -> colors.bgActionPrimaryRest + FormattingOptionState.Selected -> colors.iconSuccessPrimaryBackground FormattingOptionState.Default, FormattingOptionState.Disabled -> Color.Transparent } val foregroundColor = when (state) { - FormattingOptionState.Selected -> colors.iconOnSolidPrimary - FormattingOptionState.Default -> colors.iconPrimary + FormattingOptionState.Selected -> colors.iconSuccessPrimary + FormattingOptionState.Default -> colors.iconSecondary FormattingOptionState.Disabled -> colors.iconDisabled } Box( modifier = modifier - .clickable { onClick() } - .size(44.dp.applyScaleUp()) - .background(backgroundColor, shape = RoundedCornerShape(4.dp.applyScaleUp())) + .clickable( + onClick = onClick, + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple( + bounded = false, + radius = 20.dp, + ), + ) + .size(48.dp.applyScaleUp()) ) { - Icon( - modifier = Modifier.align(Alignment.Center), - imageVector = imageVector, - contentDescription = contentDescription, - tint = foregroundColor, + Box( + modifier = Modifier + .size(36.dp.applyScaleUp()) + .align(Alignment.Center) + .background(backgroundColor, shape = RoundedCornerShape(8.dp.applyScaleUp())) + ) { + Icon( + modifier = Modifier.align(Alignment.Center), + imageVector = imageVector, + contentDescription = contentDescription, + tint = foregroundColor, + ) + } + } +} + +@DayNightPreviews +@Composable +internal fun FormattingButtonPreview() = ElementPreview { + Row { + FormattingOption( + state = FormattingOptionState.Default, + onClick = { }, + imageVector = ImageVector.vectorResource(VectorIcons.Bold), + contentDescription = "", + ) + FormattingOption( + state = FormattingOptionState.Selected, + onClick = { }, + imageVector = ImageVector.vectorResource(VectorIcons.Italic), + contentDescription = "", + ) + FormattingOption( + state = FormattingOptionState.Disabled, + onClick = { }, + imageVector = ImageVector.vectorResource(VectorIcons.Underline), + contentDescription = "", ) } } diff --git a/libraries/textcomposer/impl/src/main/res/drawable/ic_cancel.xml b/libraries/textcomposer/impl/src/main/res/drawable/ic_cancel.xml deleted file mode 100644 index 5c27ba82d9..0000000000 --- a/libraries/textcomposer/impl/src/main/res/drawable/ic_cancel.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/libraries/textcomposer/impl/src/main/res/drawable/ic_plus.xml b/libraries/textcomposer/impl/src/main/res/drawable/ic_plus.xml deleted file mode 100644 index ead38721dc..0000000000 --- a/libraries/textcomposer/impl/src/main/res/drawable/ic_plus.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/libraries/textcomposer/impl/src/main/res/drawable/ic_send.xml b/libraries/textcomposer/impl/src/main/res/drawable/ic_send.xml index bf346b3a01..2ed6e6e53e 100644 --- a/libraries/textcomposer/impl/src/main/res/drawable/ic_send.xml +++ b/libraries/textcomposer/impl/src/main/res/drawable/ic_send.xml @@ -1,9 +1,25 @@ + + + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + android:pathData="M21.829,13.085 L6.259,20.867c-1.049,0.525 -2.141,-0.601 -1.628,-1.627 0,0 1.93,-3.897 2.461,-4.918 0.531,-1.021 1.138,-1.197 6.781,-1.927 0.209,-0.027 0.38,-0.185 0.38,-0.395 0,-0.21 -0.171,-0.368 -0.38,-0.395C8.23,10.876 7.622,10.699 7.091,9.678c-0.531,-1.02 -2.461,-4.918 -2.461,-4.918 -0.513,-1.026 0.579,-2.152 1.628,-1.627L21.829,10.915c0.894,0.446 0.894,1.723 0,2.17z" + android:fillColor="#ffffff"/> diff --git a/libraries/textcomposer/impl/src/main/res/drawable/ic_tick.xml b/libraries/textcomposer/impl/src/main/res/drawable/ic_tick.xml index cf1d71a56f..dd7863bab8 100644 --- a/libraries/textcomposer/impl/src/main/res/drawable/ic_tick.xml +++ b/libraries/textcomposer/impl/src/main/res/drawable/ic_tick.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewDark_0_null_0,NEXUS_5,1.0,en].png index 67b7525ddc..c5fc8a7df4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6bf428927e9a3493284d9fa7ba307b51315ed52b317a60ac345e87ba70849d0f -size 10523 +oid sha256:b237f45b9db8f1a7ac6dde6a2f513d28d697f6251e4a384a4b343daa417d3f15 +size 10441 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewLight_0_null_0,NEXUS_5,1.0,en].png index 8007500f81..f88e231886 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6eced1d7173c2d0100351f5bb9cd14c649a826b2e355e426b9c6d4add90015d9 -size 10833 +oid sha256:933d3530f5267dd015c1a80536c92428b4b5018a76952f5911782acc2d82b53a +size 10813 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_0,NEXUS_5,1.0,en].png index a80b2ce36d..e01bc2261a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b55dbcb731dfc40a1e100e8caf26802f759b4bcd186dbbc4432644db08f8316 -size 52141 +oid sha256:ed4ba249dab6dc1e906bfb70886b531c4bf76e3acae8b8162496afe3b5a0f572 +size 52085 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_1,NEXUS_5,1.0,en].png index 03f50fe82f..bf8898fadc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5953ae4bb021c39157b4118ea628ca9bb2277f9672749cc6313f0b378d31cb8 -size 53312 +oid sha256:9cfd42ac1e4931d4fb01ac8cecffa635adc92d67f1107a4c4051d66eeaab954a +size 53336 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_2,NEXUS_5,1.0,en].png index 8d7e2dcf95..78bffbdb73 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c329165fa341a2130b43a448b8bba465f1e5f458136efa6da80f2cdeee9d6caa -size 52369 +oid sha256:7eac088a0d14a2b1b85f541d0df5a1d38bdca3f7612560770bbfcffe0c1b2389 +size 52442 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_4,NEXUS_5,1.0,en].png index 35d5965ff3..7a830c8020 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc662d180d1766706f249f1585fb100f5933206b65d4644fae65abc6a121a633 -size 51331 +oid sha256:9c65887eeeff0a815136ddfe9dbfae86d60e6abf4f409192d7950aad01c0bb0c +size 51098 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_5,NEXUS_5,1.0,en].png index b2e621fc71..04a7cbfe71 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db0107e648e2acfc253d83f5e3b2f3e41de1abc9bdc4922dfd923ad4e50b8f5f -size 49700 +oid sha256:eddb7e59898d7aaf644f958ae0b05578b7e1ef3802e352d7fb78651c2de97cf6 +size 49638 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_0,NEXUS_5,1.0,en].png index 65cdf717bb..a960aed41d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3eb1479510a8cd04d133265e33ea15115158ac55c93387d1dafa93a3a3a858f0 -size 53620 +oid sha256:53bafb3688148dbeb3859aef1b8a9bf086340fc799f3d825ed246eba0cdda0f6 +size 53463 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_1,NEXUS_5,1.0,en].png index a5959f6d60..87c5b51154 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0d5272b14b347f18b2d67d5fae6eaf507b8d6f7c9d361317bb6b8a062e63ceb -size 54899 +oid sha256:22854689cac05b6ff1ec58a2b086c751516938161b844694b9300641de5e6ca9 +size 54871 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_2,NEXUS_5,1.0,en].png index ec09599aca..6d8c175733 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b4e5e9d920ea2a7733453e030bb365c9e2af7263de8fe216ab56088e0861fa5 -size 53997 +oid sha256:ea6bebc608259c37a2b88c14e6b247696a2f9967152bcf2eb0c2a5d70f540ee4 +size 53944 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_4,NEXUS_5,1.0,en].png index 8212494315..7071e778c7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dff46ed537c81602be0dea8782e37bb78669ee798ec4d422c23ff29cbbdebc17 -size 55927 +oid sha256:885000bd08b05ca49eddbae17aa672fbf10a3acdcaf64d71fef77888229ae5c5 +size 55813 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_5,NEXUS_5,1.0,en].png index cbdda6b794..1bac3210f9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e162503c27609f01e05bb2c634c2d3123abf6e786ecfe763c902d400ead050f -size 51283 +oid sha256:f1fd5d62a43ccfca8e157371068dcf125a72cd22920df447689b50464d8d42e6 +size 51137 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_null_ColorAliasesDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_null_ColorAliasesDark_0_null,NEXUS_5,1.0,en].png index b1c6c996d8..014374fdb1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_null_ColorAliasesDark_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_null_ColorAliasesDark_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d57924194a017902912825d4f43fcd290db11430d4d64abd89abe39b9e2ffc27 -size 48726 +oid sha256:e6625c102a88cc1096fea5425d044a96ef6e403317fa944cef8ec1ce5cd63a23 +size 52632 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_null_ColorAliasesLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_null_ColorAliasesLight_0_null,NEXUS_5,1.0,en].png index eaf130c209..c23cc8a0e2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_null_ColorAliasesLight_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme_null_ColorAliasesLight_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0eaa20c7c00c18cf08135aca70407d55ea9fc33a7f262aee3fa6ced272f7ab16 -size 48465 +oid sha256:dc7900cfc6a214acef16f642a2273088abef88f99cbf68b1aa99e43daab837ca +size 52528 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-D-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-D-4_5_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3c7c46d2da --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-D-4_5_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4a44b93e0791e4fdf7d9bd11e0d9a0fff44a271530d01ff0e6aa1e8789742ef +size 6297 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-N-4_6_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-N-4_6_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..36ec55a50b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-N-4_6_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2236ffed5666f85ce3b1093437dc8c0d72ec630f6c707394d99ff4b19d7c3f47 +size 6158 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-2_3_null,NEXUS_5,1.0,en].png index 9d906ebe35..3fa65b5075 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-2_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-2_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46683edccf9c7686a07d7098c4387f296556ad2678381aad35efe2787cf6ad0a -size 14173 +oid sha256:192e86517a6add5eb3d58d9a3d4633afc455666fff4472f112c605dc441f24d1 +size 13843 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-2_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-2_4_null,NEXUS_5,1.0,en].png index 4b9d77d2b1..c11445bc3f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-2_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-2_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6485de8235af05a9815a558b1af03b51e8c4b30ad0372aea0aa9fb2303525c9b -size 13286 +oid sha256:ea17e3d7f26d1b3f233ee9db5875dcb363b22de12ed07d32693a2c0aa727fbb6 +size 13001 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_2_null,NEXUS_5,1.0,en].png index f9dc3f8b9a..4569825f69 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f065f63fb37fa9a5441cfaeca04e867e4e8f6f744dd02a83c459fc128ee353d -size 38384 +oid sha256:31013d59ca9755e316bfcb0f5bccf5206446223b1e4f5ce79060a442bb256292 +size 40936 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_3_null,NEXUS_5,1.0,en].png index ca3a85e52c..cc6aa467f3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5ded1cbc536c544b0cb188ba0a333a541157eaf1772cd0e06c7338307549adc -size 36483 +oid sha256:569a5c4b667e259ca895b41a1248234c339d16d7776a827ded5e151472fe2c5d +size 38601 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_4_null,NEXUS_5,1.0,en].png index 0d9c311dcc..c244d01fb3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69e98a3521ae6545700e395bff211b06bb02095b01d9c254b3e0d2d0b8b88d26 -size 80484 +oid sha256:1b8da842d61ebcdede9a5337890279d740288c1a07b1b42505457ab65408cc76 +size 84240 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_5_null,NEXUS_5,1.0,en].png index ef035d6cd5..a50bba4ac8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_5_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_5_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27dee9eaae6736a128107c9fd93048a677287a40d24c334223243b3c55f1cf69 -size 77686 +oid sha256:feae2409d9887cd752d2cff4be059a566c071094955b824faa72555e69269d91 +size 80648 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_1_null,NEXUS_5,1.0,en].png index 06fbfc6c7f..de4ca7c908 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:267f482ceddeadea4c3b580d6783fed1048015fa05a11e385dd547e60f72e1e8 -size 44206 +oid sha256:ed1aa73f94dac6839f91729bcea59b4b3b14b885fa32a90335faf483e9f886f8 +size 45176 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_2_null,NEXUS_5,1.0,en].png index a237bcce80..f5a53317ca 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f5e7ab52469b406509964d71f12475df546973f1382944ce7f2e1a437e4f880 -size 41536 +oid sha256:76f54b7c7ab5c54b53a0fa9e410db942a2bce35fc0552569707ba2f3928ebd3b +size 42306 From a06bea4d71a123c016ecdf8e148cb33e51490cbb Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Fri, 15 Sep 2023 16:39:44 +0200 Subject: [PATCH 2/2] Element Call SPA integration (#1283) * Integrate Element Call into EX, being able to open its URLs and handle the call in-app. * Add custom scheme support with format `element:call?url=...`. * Update androix.webkit * Silence the foreground service notification. - Allow foreground service tap action to re-open the ongoing call. - Unify notification small icons in different modules using a vector one. --------- Co-authored-by: ElementBot --- app/build.gradle.kts | 1 + changelog.d/1300.feature | 1 + features/call/build.gradle.kts | 37 ++++ features/call/src/main/AndroidManifest.xml | 61 +++++ .../features/call/CallForegroundService.kt | 89 ++++++++ .../features/call/CallIntentDataParser.kt | 45 ++++ .../android/features/call/CallScreenView.kt | 152 +++++++++++++ .../features/call/ElementCallActivity.kt | 208 ++++++++++++++++++ .../android/features/call/di/CallBindings.kt | 26 +++ .../src/main/res/values-fr/translations.xml | 6 + .../src/main/res/values/do_not_translate.xml | 20 ++ .../call/src/main/res/values/localazy.xml | 6 + .../call/CallIntentDataParserTests.kt | 105 +++++++++ .../features/call/MapWebkitPermissionsTest.kt | 44 ++++ gradle/libs.versions.toml | 1 + .../designsystem/utils/CommonResources.kt | 21 ++ .../res/drawable/ic_notification_small.xml | 7 + libraries/push/impl/build.gradle.kts | 1 + .../factories/NotificationFactory.kt | 13 +- .../res/drawable-xxhdpi/ic_notification.png | Bin 1269 -> 0 bytes .../src/main/res/values-cs/translations.xml | 4 +- .../src/main/res/values-fr/translations.xml | 3 - .../src/main/res/values-ru/translations.xml | 4 +- .../src/main/res/values/localazy.xml | 8 +- tests/uitests/build.gradle.kts | 1 + ...lScreenView-D-0_0_null,NEXUS_5,1.0,en].png | 3 + ...lScreenView-N-0_1_null,NEXUS_5,1.0,en].png | 3 + tools/lint/lint.xml | 2 + tools/localazy/config.json | 6 + 29 files changed, 862 insertions(+), 16 deletions(-) create mode 100644 changelog.d/1300.feature create mode 100644 features/call/build.gradle.kts create mode 100644 features/call/src/main/AndroidManifest.xml create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/CallForegroundService.kt create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/CallScreenView.kt create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/ElementCallActivity.kt create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/di/CallBindings.kt create mode 100644 features/call/src/main/res/values-fr/translations.xml create mode 100644 features/call/src/main/res/values/do_not_translate.xml create mode 100644 features/call/src/main/res/values/localazy.xml create mode 100644 features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt create mode 100644 features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/CommonResources.kt create mode 100644 libraries/designsystem/src/main/res/drawable/ic_notification_small.xml delete mode 100644 libraries/push/impl/src/main/res/drawable-xxhdpi/ic_notification.png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2821fcbd04..839a5095dd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -198,6 +198,7 @@ dependencies { allLibrariesImpl() allServicesImpl() allFeaturesImpl(rootDir, logger) + implementation(projects.features.call) implementation(projects.anvilannotations) implementation(projects.appnav) anvil(projects.anvilcodegen) diff --git a/changelog.d/1300.feature b/changelog.d/1300.feature new file mode 100644 index 0000000000..bfa40bfc3b --- /dev/null +++ b/changelog.d/1300.feature @@ -0,0 +1 @@ +Integrate Element Call into EX by embedding a call in a WebView. diff --git a/features/call/build.gradle.kts b/features/call/build.gradle.kts new file mode 100644 index 0000000000..69046e33b4 --- /dev/null +++ b/features/call/build.gradle.kts @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + alias(libs.plugins.ksp) +} + +android { + namespace = "io.element.android.features.call" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.network) + implementation(libs.androidx.webkit) + ksp(libs.showkase.processor) + + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + testImplementation(libs.test.robolectric) +} diff --git a/features/call/src/main/AndroidManifest.xml b/features/call/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..1aed77cd95 --- /dev/null +++ b/features/call/src/main/AndroidManifest.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/features/call/src/main/kotlin/io/element/android/features/call/CallForegroundService.kt b/features/call/src/main/kotlin/io/element/android/features/call/CallForegroundService.kt new file mode 100644 index 0000000000..12355290e3 --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/CallForegroundService.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 io.element.android.features.call + +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationChannelCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.PendingIntentCompat +import androidx.core.graphics.drawable.IconCompat +import io.element.android.libraries.designsystem.utils.CommonDrawables + +class CallForegroundService : Service() { + + companion object { + fun start(context: Context) { + val intent = Intent(context, CallForegroundService::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } + + fun stop(context: Context) { + val intent = Intent(context, CallForegroundService::class.java) + context.stopService(intent) + } + } + + private lateinit var notificationManagerCompat: NotificationManagerCompat + + override fun onCreate() { + super.onCreate() + + notificationManagerCompat = NotificationManagerCompat.from(this) + + val foregroundServiceChannel = NotificationChannelCompat.Builder( + "call_foreground_service_channel", + NotificationManagerCompat.IMPORTANCE_LOW, + ).setName( + getString(R.string.call_foreground_service_channel_title_android).ifEmpty { "Ongoing call" } + ).build() + notificationManagerCompat.createNotificationChannel(foregroundServiceChannel) + + val callActivityIntent = Intent(this, ElementCallActivity::class.java) + val pendingIntent = PendingIntentCompat.getActivity(this, 0, callActivityIntent, 0, false) + val notification = NotificationCompat.Builder(this, foregroundServiceChannel.id) + .setSmallIcon(IconCompat.createWithResource(this, CommonDrawables.ic_notification_small)) + .setContentTitle(getString(R.string.call_foreground_service_title_android)) + .setContentText(getString(R.string.call_foreground_service_message_android)) + .setContentIntent(pendingIntent) + .build() + startForeground(1, notification) + } + + @Suppress("DEPRECATION") + override fun onDestroy() { + super.onDestroy() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + stopForeground(STOP_FOREGROUND_REMOVE) + } else { + stopForeground(true) + } + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } +} diff --git a/features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt b/features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt new file mode 100644 index 0000000000..a664e562f3 --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 io.element.android.features.call + +import android.net.Uri +import java.net.URLDecoder + +object CallIntentDataParser { + + private val validHttpSchemes = sequenceOf("http", "https") + + fun parse(data: String?): String? { + val parsedUrl = data?.let { Uri.parse(data) } ?: return null + val scheme = parsedUrl.scheme + return when { + scheme in validHttpSchemes && parsedUrl.host == "call.element.io" -> data + scheme == "element" && parsedUrl.host == "call" -> { + // We use this custom scheme to load arbitrary URLs for other instances of Element Call, + // so we can only verify it's an HTTP/HTTPs URL with a non-empty host + parsedUrl.getQueryParameter("url") + ?.let { URLDecoder.decode(it, "utf-8") } + ?.takeIf { + val internalUri = Uri.parse(it) + internalUri.scheme in validHttpSchemes && !internalUri.host.isNullOrBlank() + } + } + // This should never be possible, but we still need to take into account the possibility + else -> null + } + } +} diff --git a/features/call/src/main/kotlin/io/element/android/features/call/CallScreenView.kt b/features/call/src/main/kotlin/io/element/android/features/call/CallScreenView.kt new file mode 100644 index 0000000000..08ad687f91 --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/CallScreenView.kt @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 io.element.android.features.call + +import android.annotation.SuppressLint +import android.view.ViewGroup +import android.webkit.PermissionRequest +import android.webkit.WebChromeClient +import android.webkit.WebView +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.viewinterop.AndroidView +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.theme.ElementTheme + +typealias RequestPermissionCallback = (Array) -> Unit + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun CallScreenView( + url: String?, + userAgent: String, + requestPermissions: (Array, RequestPermissionCallback) -> Unit, + onClose: () -> Unit, + modifier: Modifier = Modifier, +) { + ElementTheme { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.element_call)) }, + navigationIcon = { + BackButton( + imageVector = Icons.Default.Close, + onClick = onClose + ) + } + ) + } + ) { padding -> + CallWebView( + modifier = Modifier + .padding(padding) + .consumeWindowInsets(padding) + .fillMaxSize(), + url = url, + userAgent = userAgent, + onPermissionsRequested = { request -> + val androidPermissions = mapWebkitPermissions(request.resources) + val callback: RequestPermissionCallback = { request.grant(it) } + requestPermissions(androidPermissions.toTypedArray(), callback) + } + ) + } + } +} + +@Composable +private fun CallWebView( + url: String?, + userAgent: String, + onPermissionsRequested: (PermissionRequest) -> Unit, + modifier: Modifier = Modifier, +) { + val isInpectionMode = LocalInspectionMode.current + AndroidView( + modifier = modifier, + factory = { context -> + WebView(context).apply { + if (!isInpectionMode) { + setup(userAgent, onPermissionsRequested) + if (url != null) { + loadUrl(url) + } + } + } + }, + update = { webView -> + if (!isInpectionMode && url != null) { + webView.loadUrl(url) + } + }, + onRelease = { webView -> + webView.destroy() + } + ) +} + +@SuppressLint("SetJavaScriptEnabled") +private fun WebView.setup(userAgent: String, onPermissionsRequested: (PermissionRequest) -> Unit) { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + + with(settings) { + javaScriptEnabled = true + allowContentAccess = true + allowFileAccess = true + domStorageEnabled = true + mediaPlaybackRequiresUserGesture = false + databaseEnabled = true + loadsImagesAutomatically = true + userAgentString = userAgent + } + + webChromeClient = object : WebChromeClient() { + override fun onPermissionRequest(request: PermissionRequest) { + onPermissionsRequested(request) + } + } +} + +@DayNightPreviews +@Composable +internal fun CallScreenViewPreview() { + ElementTheme { + CallScreenView( + url = "https://call.element.io/some-actual-call?with=parameters", + userAgent = "", + requestPermissions = { _, _ -> }, + onClose = { }, + ) + } +} diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ElementCallActivity.kt b/features/call/src/main/kotlin/io/element/android/features/call/ElementCallActivity.kt new file mode 100644 index 0000000000..69ef3963cb --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/ElementCallActivity.kt @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 io.element.android.features.call + +import android.Manifest +import android.content.Intent +import android.content.res.Configuration +import android.media.AudioAttributes +import android.media.AudioFocusRequest +import android.media.AudioManager +import android.os.Build +import android.os.Bundle +import android.view.WindowManager +import android.webkit.PermissionRequest +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.mutableStateOf +import io.element.android.features.call.di.CallBindings +import io.element.android.libraries.architecture.bindings +import io.element.android.libraries.network.useragent.UserAgentProvider +import javax.inject.Inject + +class ElementCallActivity : ComponentActivity() { + + @Inject lateinit var userAgentProvider: UserAgentProvider + + private lateinit var audioManager: AudioManager + + private var requestPermissionCallback: RequestPermissionCallback? = null + + private var audiofocusRequest: AudioFocusRequest? = null + private var audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null + + private val requestPermissionsLauncher = registerPermissionResultLauncher() + + private var isDarkMode = false + private val urlState = mutableStateOf(null) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + applicationContext.bindings().inject(this) + + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + + urlState.value = intent?.dataString?.let(::parseUrl) ?: run { + finish() + return + } + + if (savedInstanceState == null) { + updateUiMode(resources.configuration) + } + + audioManager = getSystemService(AUDIO_SERVICE) as AudioManager + requestAudioFocus() + + val userAgent = userAgentProvider.provide() + + setContent { + CallScreenView( + url = urlState.value, + userAgent = userAgent, + onClose = this::finish, + requestPermissions = { permissions, callback -> + requestPermissionCallback = callback + requestPermissionsLauncher.launch(permissions) + } + ) + } + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + + updateUiMode(newConfig) + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + + val intentUrl = intent?.dataString?.let(::parseUrl) + when { + // New URL, update it and reload the webview + intentUrl != null -> urlState.value = intentUrl + // Re-opened the activity but we have no url to load or a cached one, finish the activity + intent?.dataString == null && urlState.value == null -> finish() + // Coming back from notification, do nothing + else -> return + } + } + + override fun onStart() { + super.onStart() + CallForegroundService.stop(this) + } + + override fun onStop() { + super.onStop() + if (!isFinishing && !isChangingConfigurations) { + CallForegroundService.start(this) + } + } + + override fun onDestroy() { + super.onDestroy() + releaseAudioFocus() + CallForegroundService.stop(this) + } + + override fun finish() { + // Also remove the task from recents + finishAndRemoveTask() + } + + private fun parseUrl(url: String?): String? = CallIntentDataParser.parse(url) + + private fun registerPermissionResultLauncher(): ActivityResultLauncher> { + return registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + val callback = requestPermissionCallback ?: return@registerForActivityResult + val permissionsToGrant = mutableListOf() + permissions.forEach { (permission, granted) -> + if (granted) { + val webKitPermission = when (permission) { + Manifest.permission.CAMERA -> PermissionRequest.RESOURCE_VIDEO_CAPTURE + Manifest.permission.RECORD_AUDIO -> PermissionRequest.RESOURCE_AUDIO_CAPTURE + else -> return@forEach + } + permissionsToGrant.add(webKitPermission) + } + } + callback(permissionsToGrant.toTypedArray()) + } + } + + @Suppress("DEPRECATION") + private fun requestAudioFocus() { + val audioAttributes = AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) + .build() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) + .setAudioAttributes(audioAttributes) + .build() + audioManager.requestAudioFocus(request) + audiofocusRequest = request + } else { + val listener = AudioManager.OnAudioFocusChangeListener { } + audioManager.requestAudioFocus( + listener, + AudioManager.STREAM_VOICE_CALL, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, + ) + + audioFocusChangeListener = listener + } + } + + @Suppress("DEPRECATION") + private fun releaseAudioFocus() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + audiofocusRequest?.let { audioManager.abandonAudioFocusRequest(it) } + } else { + audioFocusChangeListener?.let { audioManager.abandonAudioFocus(it) } + } + } + + private fun updateUiMode(configuration: Configuration) { + val prevDarkMode = isDarkMode + val currentNightMode = configuration.uiMode and Configuration.UI_MODE_NIGHT_YES + isDarkMode = currentNightMode != 0 + if (prevDarkMode != isDarkMode) { + if (isDarkMode) { + window.setBackgroundDrawableResource(android.R.drawable.screen_background_dark) + } else { + window.setBackgroundDrawableResource(android.R.drawable.screen_background_light) + } + } + } +} + +internal fun mapWebkitPermissions(permissions: Array): List { + return permissions.mapNotNull { permission -> + when (permission) { + PermissionRequest.RESOURCE_AUDIO_CAPTURE -> Manifest.permission.RECORD_AUDIO + PermissionRequest.RESOURCE_VIDEO_CAPTURE -> Manifest.permission.CAMERA + else -> null + } + } +} diff --git a/features/call/src/main/kotlin/io/element/android/features/call/di/CallBindings.kt b/features/call/src/main/kotlin/io/element/android/features/call/di/CallBindings.kt new file mode 100644 index 0000000000..1e261cc225 --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/di/CallBindings.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 io.element.android.features.call.di + +import com.squareup.anvil.annotations.ContributesTo +import io.element.android.features.call.ElementCallActivity +import io.element.android.libraries.di.AppScope + +@ContributesTo(AppScope::class) +interface CallBindings { + fun inject(callActivity: ElementCallActivity) +} diff --git a/features/call/src/main/res/values-fr/translations.xml b/features/call/src/main/res/values-fr/translations.xml new file mode 100644 index 0000000000..e35473b31b --- /dev/null +++ b/features/call/src/main/res/values-fr/translations.xml @@ -0,0 +1,6 @@ + + + "Appel en cours" + "Appuyez pour retourner à l\'appel." + "☎️ Appel en cours" + diff --git a/features/call/src/main/res/values/do_not_translate.xml b/features/call/src/main/res/values/do_not_translate.xml new file mode 100644 index 0000000000..c1fe10cdfb --- /dev/null +++ b/features/call/src/main/res/values/do_not_translate.xml @@ -0,0 +1,20 @@ + + + + + Element Call + diff --git a/features/call/src/main/res/values/localazy.xml b/features/call/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..cfe40526f4 --- /dev/null +++ b/features/call/src/main/res/values/localazy.xml @@ -0,0 +1,6 @@ + + + "Ongoing call" + "Tap to return to the call" + "☎️ Call in progress" + diff --git a/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt b/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt new file mode 100644 index 0000000000..da41692b40 --- /dev/null +++ b/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 io.element.android.features.call + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import java.net.URLEncoder + +@RunWith(RobolectricTestRunner::class) +class CallIntentDataParserTests { + + @Test + fun `a null data returns null`() { + val url: String? = null + assertThat(CallIntentDataParser.parse(url)).isNull() + } + + @Test + fun `empty data returns null`() { + val url = "" + assertThat(CallIntentDataParser.parse(url)).isNull() + } + + @Test + fun `invalid data returns null`() { + val url = "!" + assertThat(CallIntentDataParser.parse(url)).isNull() + } + + @Test + fun `data with no scheme returns null`() { + val url = "test" + assertThat(CallIntentDataParser.parse(url)).isNull() + } + + @Test + fun `Element Call urls will be returned as is`() { + val httpBaseUrl = "http://call.element.io" + val httpCallUrl = "http://call.element.io/some-actual-call?with=parameters" + val httpsBaseUrl = "https://call.element.io" + val httpsCallUrl = "https://call.element.io/some-actual-call?with=parameters" + assertThat(CallIntentDataParser.parse(httpBaseUrl)).isEqualTo(httpBaseUrl) + assertThat(CallIntentDataParser.parse(httpCallUrl)).isEqualTo(httpCallUrl) + assertThat(CallIntentDataParser.parse(httpsBaseUrl)).isEqualTo(httpsBaseUrl) + assertThat(CallIntentDataParser.parse(httpsCallUrl)).isEqualTo(httpsCallUrl) + } + + @Test + fun `HTTP and HTTPS urls that don't come from EC return null`() { + val httpBaseUrl = "http://app.element.io" + val httpsBaseUrl = "https://app.element.io" + val httpInvalidUrl = "http://" + val httpsInvalidUrl = "http://" + assertThat(CallIntentDataParser.parse(httpBaseUrl)).isNull() + assertThat(CallIntentDataParser.parse(httpsBaseUrl)).isNull() + assertThat(CallIntentDataParser.parse(httpInvalidUrl)).isNull() + assertThat(CallIntentDataParser.parse(httpsInvalidUrl)).isNull() + } + + @Test + fun `element scheme with call host and url param gets url extracted`() { + val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" + val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") + val url = "element://call?url=$encodedUrl" + assertThat(CallIntentDataParser.parse(url)).isEqualTo(embeddedUrl) + } + + @Test + fun `element scheme with call host and no url param returns null`() { + val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" + val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") + val url = "element://call?no-url=$encodedUrl" + assertThat(CallIntentDataParser.parse(url)).isNull() + } + + @Test + fun `element scheme with no call host returns null`() { + val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" + val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") + val url = "element://no-call?url=$encodedUrl" + assertThat(CallIntentDataParser.parse(url)).isNull() + } + + @Test + fun `element scheme with no data returns null`() { + val url = "element://call?url=" + assertThat(CallIntentDataParser.parse(url)).isNull() + } +} diff --git a/features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt b/features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt new file mode 100644 index 0000000000..f82e31c068 --- /dev/null +++ b/features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 io.element.android.features.call + +import android.Manifest +import android.webkit.PermissionRequest +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class MapWebkitPermissionsTest { + + @Test + fun `given Webkit's RESOURCE_AUDIO_CAPTURE returns Android's RECORD_AUDIO permission`() { + val permission = mapWebkitPermissions(arrayOf(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) + assertThat(permission).isEqualTo(listOf(Manifest.permission.RECORD_AUDIO)) + } + + @Test + fun `given Webkit's RESOURCE_VIDEO_CAPTURE returns Android's CAMERA permission`() { + val permission = mapWebkitPermissions(arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) + assertThat(permission).isEqualTo(listOf(Manifest.permission.CAMERA)) + } + + @Test + fun `given any other permission, it returns nothing`() { + val permission = mapWebkitPermissions(arrayOf(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID)) + assertThat(permission).isEqualTo(emptyList()) + } + +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ebd4f4c888..b164dfbb95 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -91,6 +91,7 @@ androidx_activity_activity = { module = "androidx.activity:activity", version.re androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" } androidx_startup = { module = "androidx.startup:startup-runtime", version.ref = "startup" } androidx_preference = "androidx.preference:preference:1.2.1" +androidx_webkit = "androidx.webkit:webkit:1.8.0" androidx_compose_bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose_bom" } # Warning: issue on alpha07, make sure this is working when upgrading diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/CommonResources.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/CommonResources.kt new file mode 100644 index 0000000000..adcfd93af8 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/CommonResources.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 io.element.android.libraries.designsystem.utils + +import io.element.android.libraries.designsystem.R + +typealias CommonDrawables = R.drawable diff --git a/libraries/designsystem/src/main/res/drawable/ic_notification_small.xml b/libraries/designsystem/src/main/res/drawable/ic_notification_small.xml new file mode 100644 index 0000000000..cf84d679cd --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_notification_small.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index b961146e78..c7e3251ffb 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.core) + implementation(projects.libraries.designsystem) implementation(projects.libraries.di) implementation(projects.libraries.androidutils) implementation(projects.libraries.network) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt index b359f540f8..105b5789e4 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt @@ -25,6 +25,7 @@ import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.user.MatrixUser @@ -67,7 +68,7 @@ class NotificationFactory @Inject constructor( else -> pendingIntentFactory.createOpenRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId) } - val smallIcon = R.drawable.ic_notification + val smallIcon = CommonDrawables.ic_notification_small val channelId = notificationChannels.getChannelIdForMessage(roomInfo.shouldBing) return NotificationCompat.Builder(context, channelId) @@ -141,7 +142,7 @@ class NotificationFactory @Inject constructor( inviteNotifiableEvent: InviteNotifiableEvent ): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) - val smallIcon = R.drawable.ic_notification + val smallIcon = CommonDrawables.ic_notification_small val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy) return NotificationCompat.Builder(context, channelId) .setOnlyAlertOnce(true) @@ -185,7 +186,7 @@ class NotificationFactory @Inject constructor( simpleNotifiableEvent: SimpleNotifiableEvent, ): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) - val smallIcon = R.drawable.ic_notification + val smallIcon = CommonDrawables.ic_notification_small val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy) return NotificationCompat.Builder(context, channelId) @@ -220,7 +221,7 @@ class NotificationFactory @Inject constructor( fallbackNotifiableEvent: FallbackNotifiableEvent, ): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) - val smallIcon = R.drawable.ic_notification + val smallIcon = CommonDrawables.ic_notification_small val channelId = notificationChannels.getChannelIdForMessage(false) return NotificationCompat.Builder(context, channelId) @@ -261,7 +262,7 @@ class NotificationFactory @Inject constructor( lastMessageTimestamp: Long ): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) - val smallIcon = R.drawable.ic_notification + val smallIcon = CommonDrawables.ic_notification_small val channelId = notificationChannels.getChannelIdForMessage(noisy) return NotificationCompat.Builder(context, channelId) .setOnlyAlertOnce(true) @@ -301,7 +302,7 @@ class NotificationFactory @Inject constructor( return NotificationCompat.Builder(context, notificationChannels.getChannelIdForTest()) .setContentTitle(buildMeta.applicationName) .setContentText(stringProvider.getString(R.string.notification_test_push_notification_content)) - .setSmallIcon(R.drawable.ic_notification) + .setSmallIcon(CommonDrawables.ic_notification_small) .setLargeIcon(getBitmap(R.drawable.element_logo_green)) .setColor(ContextCompat.getColor(context, R.color.notification_accent_color)) .setPriority(NotificationCompat.PRIORITY_MAX) diff --git a/libraries/push/impl/src/main/res/drawable-xxhdpi/ic_notification.png b/libraries/push/impl/src/main/res/drawable-xxhdpi/ic_notification.png deleted file mode 100644 index a86508b71b432a095780bc8fd6b8122816c10e47..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1269 zcmV1Jw|1DI%CxRJEgpPxkR`%a>$bk(*HW;Aqq7G$E4ZYbsNeFH$D1x3 z$C*1^e?oo3Hv!or`BIW2&`Xl@wSy60Vs>%X7Y<`jC?yd6(j|1!MnR51mbAvYrIR9v z)KmydMV>%h01XAj2BLstFsBYc_rm5B6Enay;;^EQKqp0O^2EAXh4>Sf)Qty{V0;sZ ze=_aPY%cGqBM=48%haam&x0odI!F#HPd3FgLxk!p4F^ z0kKvtu*A%$Cm^nejXMef-1Ao@G62sGb9EPGC5(?FeL3e2DFV-(KOR)46P& zEc4|Udghc7P`ubZzuRYssyCAa{r)B!$!YMJAlP;7!liSAx6>69C``S_Z??O6F-!K*pBnDXlxx8LWmcg@>B6qq(R9@8XS{daDS<>BtDXBUvB!MF zu5+uVqb5vxK}Q5+Rh(J+*y2g+QGJoEWxPkT(L1LBQA?F=Rs3WnJs~?`-1NNA)ER9~ zlpTjSK~qJawMvs?fOUe<#k0>?f(pyESpPg680hTSCJ+)EviU%p{{c4GcfsYJLD))^ zJN@`NM^QKcU9@7>bhdHVGQgpbNM0%5S3m-K8j9uzHkK`e*fxd}$`E+u(pU%C0@+V3 z-`Si-jfLogThdjseQW-~6ZFH<*~i46Qf5G^&onMg`6Anb!}?^gXE5hw*(Y#IK#bci zz>~DAS9qPy4-9Bq$JscRa7$g;Un~?p4FUwnV}rMDh>br8<{bK7{JTwe5R?+FQ^;w zs`H_XLUxW}Ly~wgh+CBpUA1W^Ylb*QTYr%v>4^lH5QH!HV^cn%R6yrvu<|U(5rjRZ z$Wrqisa+4~v#~z4(3+AK&BXjdpV0;Ay;WzWh;DV*{Z32++={K7B98DoYtP1`pqzkP zO_7r%;q6A6+13-sTOM{2*>TPObWa66>+IfgY;a_CjlBT3qe=}Gjx6ur3HTrVW{Fxt zHlGD$LX>0SkRt|NrGhuPoBESmuq;tmdoR9?LKavFGPw6=@&%O}X f5!@Uq=nBn0jKJO8Fdhew00000NkvXXu0mjfM&?#H diff --git a/libraries/ui-strings/src/main/res/values-cs/translations.xml b/libraries/ui-strings/src/main/res/values-cs/translations.xml index 0ff43bceb7..271b614dc4 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -195,9 +195,9 @@ "Další nastavení" "Halsové a video hovory" "Neshoda konfigurace" - "Zjednodušili jsme nastavení oznámení, abychom usnadnili hledání možností. + "Zjednodušili jsme nastavení oznámení, abychom usnadnili hledání možností. -Některá vlastní nastavení, která jste si vybrali v minulosti, se zde nezobrazují, ale jsou stále aktivní. +Některá vlastní nastavení, která jste si vybrali v minulosti, se zde nezobrazují, ale jsou stále aktivní. Pokud budete pokračovat, některá nastavení se mohou změnit." "Přímé zprávy" diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index 67b738cc21..4978cdb68c 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -67,9 +67,6 @@ "Prendre une photo" "Afficher la source" "Oui" - "Appel en cours" - "Appuyez pour retourner à l\'appel." - "☎️ Appel en cours" "À propos" "Politique d\'utilisation acceptable" "Paramètres avancés" diff --git a/libraries/ui-strings/src/main/res/values-ru/translations.xml b/libraries/ui-strings/src/main/res/values-ru/translations.xml index 798515fa6a..d37e28dc7a 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -194,9 +194,9 @@ "Дополнительные параметры" "Аудио и видео звонки" "Несоответствие конфигурации" - "Мы упростили настройки уведомлений, чтобы упростить поиск опций. + "Мы упростили настройки уведомлений, чтобы упростить поиск опций. -Некоторые пользовательские настройки, выбранные вами ранее, не отображаются в данном меню, но они все еще активны. +Некоторые пользовательские настройки, выбранные вами ранее, не отображаются в данном меню, но они все еще активны. Если вы продолжите, некоторые настройки могут быть изменены." "Прямые чаты" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 2355e4b603..0cd0b7df0e 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -67,9 +67,6 @@ "Take photo" "View Source" "Yes" - "Ongoing call" - "Tap to return to the call" - "☎️ Call in progress" "About" "Acceptable use policy" "Advanced settings" @@ -207,6 +204,11 @@ "This is the beginning of this conversation." "New" "Share analytics data" + "Display name" + "Your display name" + "An unknown error was encountered and the information couldn\'t be changed." + "Unable to update profile" + "Updating profile…" "Failed selecting media, please try again." "Failed processing media to upload, please try again." "Failed uploading media, please try again." diff --git a/tests/uitests/build.gradle.kts b/tests/uitests/build.gradle.kts index ffa3e50aff..9556d653bf 100644 --- a/tests/uitests/build.gradle.kts +++ b/tests/uitests/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { // `testOptions { unitTests.isIncludeAndroidResources = true }` in the app build.gradle.kts file // implementation(projects.app) implementation(projects.appnav) + implementation(projects.features.call) allLibrariesImpl() allServicesImpl() allFeaturesImpl(rootDir, logger) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9fdfd38cee --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26df292602cdf27ddd42b3b4c1dcc3fc7ae41e207af48d76c7b65bd66babf649 +size 10561 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e524d75c96 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f28b5214727111f39cef9a1d625469f30481fe2cbbe02df0efc53807a968d210 +size 9787 diff --git a/tools/lint/lint.xml b/tools/lint/lint.xml index db1a20701c..ce49e50a7a 100644 --- a/tools/lint/lint.xml +++ b/tools/lint/lint.xml @@ -48,6 +48,8 @@ + + diff --git a/tools/localazy/config.json b/tools/localazy/config.json index b92e02f670..c2cb5cef3e 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -128,6 +128,12 @@ "includeRegex": [ "screen_create_poll_.*" ] + }, + { + "name": ":features:call", + "includeRegex": [ + "call_.*" + ] } ] }