mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-20 00:35:47 +01:00
Update signal login pdf export layout.
This commit is contained in:
committed by
Cody Henthorne
parent
99c2686151
commit
f2b98bbe5b
+169
-72
@@ -12,8 +12,12 @@ import android.graphics.RectF
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.pdf.PdfDocument
|
||||
import android.net.Uri
|
||||
import android.text.Layout
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.withTranslation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.Result
|
||||
@@ -23,14 +27,13 @@ import org.signal.signallogin.fonts.MonoTypeface
|
||||
import org.signal.signallogin.viewdetails.SignalLoginViewDetailsState
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
import kotlin.math.ceil
|
||||
|
||||
/**
|
||||
* Renders the keys that make up a Signal Login into a single-page PDF, styled to match
|
||||
* [org.signal.signallogin.viewdetails.SignalLoginViewDetailsScreen]: the Signal logo up top, then each key
|
||||
* in a rounded block, with the recovery key broken into character groups.
|
||||
* Renders the keys that make up a Signal Login into a single-page PDF: a miniature of the credential card up top,
|
||||
* a title and explainer, then each key in a rounded block, with the recovery key broken into character groups.
|
||||
*
|
||||
* All dimensions are in PostScript points (1/72 inch), on an A4 page.
|
||||
* All dimensions are in PostScript points (1/72 inch), on a US Letter page, and are taken directly from the design.
|
||||
*/
|
||||
object SignalLoginPdfRenderer {
|
||||
|
||||
@@ -41,30 +44,50 @@ object SignalLoginPdfRenderer {
|
||||
return context.getString(R.string.SignalLoginViewDetailsScreen__signal_login_pdf)
|
||||
}
|
||||
|
||||
private const val PAGE_WIDTH = 595
|
||||
private const val PAGE_HEIGHT = 842
|
||||
private const val MARGIN = 56f
|
||||
private const val PAGE_WIDTH = 612
|
||||
private const val PAGE_HEIGHT = 792
|
||||
|
||||
private const val LOGO_WIDTH = 140f
|
||||
private const val LOGO_BOTTOM_SPACING = 28f
|
||||
private const val CARD_TOP = 120f
|
||||
private const val CARD_WIDTH = 98f
|
||||
private const val CARD_HEIGHT = 56f
|
||||
|
||||
private const val HEADER_TEXT_SIZE = 12f
|
||||
private const val HEADER_TOP_PADDING = 16f
|
||||
private const val HEADER_BOTTOM_PADDING = 12f
|
||||
private const val TITLE_TOP_SPACING = 16f
|
||||
private const val TITLE_TEXT_SIZE = 18f
|
||||
private const val TITLE_LINE_HEIGHT = 24f
|
||||
private const val TITLE_LETTER_SPACING_EM = -0.014f
|
||||
|
||||
private const val KEY_TEXT_SIZE = 13f
|
||||
private const val KEY_LINE_HEIGHT = 21f
|
||||
private const val KEY_LETTER_SPACING_EM = 0.08f
|
||||
private const val BODY_TOP_SPACING = 8f
|
||||
private const val BODY_WIDTH = 390f
|
||||
private const val BODY_TEXT_SIZE = 14f
|
||||
private const val BODY_LINE_HEIGHT = 22f
|
||||
private const val BODY_LETTER_SPACING_EM = -0.006f
|
||||
|
||||
private const val HEADER_TOP_SPACING = 32f
|
||||
private const val HEADER_TEXT_SIZE = 14f
|
||||
private const val HEADER_LINE_HEIGHT = 22f
|
||||
private const val HEADER_LETTER_SPACING_EM = -0.006f
|
||||
private const val HEADER_BOTTOM_SPACING = 12f
|
||||
|
||||
private const val BLOCK_WIDTH = 380f
|
||||
private const val BLOCK_CORNER_RADIUS = 18f
|
||||
private const val BLOCK_HORIZONTAL_PADDING = 28f
|
||||
private const val BLOCK_VERTICAL_PADDING = 20f
|
||||
private const val BLOCK_HORIZONTAL_PADDING = 24f
|
||||
private const val BLOCK_VERTICAL_PADDING = 16f
|
||||
|
||||
private const val KEY_TEXT_SIZE = 14f
|
||||
private const val KEY_LETTER_SPACING_EM = 0.07f
|
||||
private const val ACCOUNT_KEY_LINE_HEIGHT = 20f
|
||||
private const val RECOVERY_KEY_LINE_HEIGHT = 24f
|
||||
|
||||
private const val GROUPS_PER_ROW = 4
|
||||
|
||||
/** Light-theme colorSurface2 and onSurface, respectively. The PDF is always rendered as if in light theme. */
|
||||
private const val BLOCK_COLOR = 0xFFEDF0F6.toInt()
|
||||
private const val TEXT_COLOR = 0xFF1B1B1D.toInt()
|
||||
private const val BLOCK_LEFT = (PAGE_WIDTH - BLOCK_WIDTH) / 2f
|
||||
private const val CONTENT_LEFT = BLOCK_LEFT + BLOCK_HORIZONTAL_PADDING
|
||||
private const val CONTENT_WIDTH = BLOCK_WIDTH - 2 * BLOCK_HORIZONTAL_PADDING
|
||||
|
||||
/** The PDF is always rendered as if in light theme, so these are fixed rather than pulled from the theme. */
|
||||
private const val BLOCK_COLOR = 0xFFF4F4F5.toInt()
|
||||
private const val TEXT_COLOR = 0xFF000000.toInt()
|
||||
private const val BODY_TEXT_COLOR = 0xFF4D4D4D.toInt()
|
||||
|
||||
/**
|
||||
* Renders the credentials in [state] to a PDF and writes it to [uri].
|
||||
@@ -108,76 +131,150 @@ object SignalLoginPdfRenderer {
|
||||
}
|
||||
|
||||
private fun drawPage(context: Context, canvas: Canvas, state: SignalLoginViewDetailsState) {
|
||||
val headerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
typeface = Typeface.create("sans-serif-medium", Typeface.NORMAL)
|
||||
textSize = HEADER_TEXT_SIZE
|
||||
color = TEXT_COLOR
|
||||
}
|
||||
|
||||
val keyPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
val titlePaint = textPaint(TITLE_TEXT_SIZE, TITLE_LETTER_SPACING_EM, TEXT_COLOR, semiBold = true)
|
||||
val bodyPaint = textPaint(BODY_TEXT_SIZE, BODY_LETTER_SPACING_EM, BODY_TEXT_COLOR, semiBold = false)
|
||||
val headerPaint = textPaint(HEADER_TEXT_SIZE, HEADER_LETTER_SPACING_EM, TEXT_COLOR, semiBold = true)
|
||||
val keyPaint = textPaint(KEY_TEXT_SIZE, KEY_LETTER_SPACING_EM, TEXT_COLOR, semiBold = false).apply {
|
||||
typeface = MonoTypeface.typeface(context)
|
||||
textSize = KEY_TEXT_SIZE
|
||||
letterSpacing = KEY_LETTER_SPACING_EM
|
||||
color = TEXT_COLOR
|
||||
}
|
||||
|
||||
var y = drawLogo(context, canvas, top = MARGIN) + LOGO_BOTTOM_SPACING
|
||||
var y = drawCard(context, canvas, top = CARD_TOP)
|
||||
|
||||
y = drawSectionHeader(canvas, headerPaint, context.getString(R.string.SignalLoginViewDetailsScreen__account_key), y)
|
||||
y = drawKeyBlock(canvas, keyPaint, rows = listOf(listOf(state.accountKey)), top = y)
|
||||
y += TITLE_TOP_SPACING
|
||||
y += drawText(
|
||||
canvas = canvas,
|
||||
text = context.getString(R.string.SignalLoginPdf__your_signal_login),
|
||||
paint = titlePaint,
|
||||
left = (PAGE_WIDTH - BODY_WIDTH) / 2f,
|
||||
top = y,
|
||||
width = BODY_WIDTH,
|
||||
lineHeight = TITLE_LINE_HEIGHT,
|
||||
alignment = Layout.Alignment.ALIGN_CENTER
|
||||
)
|
||||
|
||||
y = drawSectionHeader(canvas, headerPaint, context.getString(R.string.SignalLoginViewDetailsScreen__recovery_key), y)
|
||||
drawKeyBlock(canvas, keyPaint, rows = state.recoveryKeyGroups.chunked(GROUPS_PER_ROW), top = y)
|
||||
y += BODY_TOP_SPACING
|
||||
y += drawText(
|
||||
canvas = canvas,
|
||||
text = context.getString(R.string.SignalLoginPdf__store_this_in_a_safe_place),
|
||||
paint = bodyPaint,
|
||||
left = (PAGE_WIDTH - BODY_WIDTH) / 2f,
|
||||
top = y,
|
||||
width = BODY_WIDTH,
|
||||
lineHeight = BODY_LINE_HEIGHT,
|
||||
alignment = Layout.Alignment.ALIGN_CENTER
|
||||
)
|
||||
|
||||
y = drawSectionHeader(canvas, headerPaint, context.getString(R.string.SignalLoginPdf__account_id), top = y)
|
||||
y = drawKeyBlock(canvas, top = y, rowCount = 1, lineHeight = ACCOUNT_KEY_LINE_HEIGHT) { contentTop ->
|
||||
drawText(
|
||||
canvas = canvas,
|
||||
text = state.accountKey,
|
||||
paint = keyPaint,
|
||||
left = CONTENT_LEFT,
|
||||
top = contentTop,
|
||||
width = CONTENT_WIDTH,
|
||||
lineHeight = ACCOUNT_KEY_LINE_HEIGHT,
|
||||
alignment = Layout.Alignment.ALIGN_CENTER
|
||||
)
|
||||
}
|
||||
|
||||
val rows = state.recoveryKeyGroups.chunked(GROUPS_PER_ROW)
|
||||
y = drawSectionHeader(canvas, headerPaint, context.getString(R.string.SignalLoginPdf__recovery_key), top = y)
|
||||
drawKeyBlock(canvas, top = y, rowCount = rows.size, lineHeight = RECOVERY_KEY_LINE_HEIGHT) { contentTop ->
|
||||
drawRecoveryKeyGroups(canvas, keyPaint, rows, contentTop)
|
||||
}
|
||||
}
|
||||
|
||||
/** Draws the Signal logo centered at the top of the page, returning the y position of its bottom edge. */
|
||||
private fun drawLogo(context: Context, canvas: Canvas, top: Float): Float {
|
||||
val logo = requireNotNull(ContextCompat.getDrawable(context, CoreUiR.drawable.image_signal_logo_wordmark_light))
|
||||
val height = LOGO_WIDTH * logo.intrinsicHeight / logo.intrinsicWidth
|
||||
val left = (PAGE_WIDTH - LOGO_WIDTH) / 2f
|
||||
/** Draws the miniature credential card artwork centered at the top of the page, returning the y position of its bottom edge. */
|
||||
private fun drawCard(context: Context, canvas: Canvas, top: Float): Float {
|
||||
val card = requireNotNull(ContextCompat.getDrawable(context, R.drawable.image_signal_login_card_x_small))
|
||||
val left = (PAGE_WIDTH - CARD_WIDTH) / 2f
|
||||
|
||||
logo.setBounds(left.toInt(), top.toInt(), (left + LOGO_WIDTH).toInt(), (top + height).toInt())
|
||||
logo.draw(canvas)
|
||||
card.setBounds(left.toInt(), top.toInt(), (left + CARD_WIDTH).toInt(), (top + CARD_HEIGHT).toInt())
|
||||
card.draw(canvas)
|
||||
|
||||
return top + CARD_HEIGHT
|
||||
}
|
||||
|
||||
/** Draws a section header above a key block, returning the y position the block should start at. */
|
||||
private fun drawSectionHeader(canvas: Canvas, paint: TextPaint, text: String, top: Float): Float {
|
||||
val headerTop = top + HEADER_TOP_SPACING
|
||||
val height = drawText(
|
||||
canvas = canvas,
|
||||
text = text,
|
||||
paint = paint,
|
||||
left = CONTENT_LEFT,
|
||||
top = headerTop,
|
||||
width = CONTENT_WIDTH,
|
||||
lineHeight = HEADER_LINE_HEIGHT,
|
||||
alignment = Layout.Alignment.ALIGN_NORMAL
|
||||
)
|
||||
|
||||
return headerTop + height + HEADER_BOTTOM_SPACING
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a rounded block sized to hold [rowCount] rows of [lineHeight], invoking [drawContent] with the y position
|
||||
* its content starts at. Returns the y position of the block's bottom edge.
|
||||
*/
|
||||
private fun drawKeyBlock(canvas: Canvas, top: Float, rowCount: Int, lineHeight: Float, drawContent: (Float) -> Unit): Float {
|
||||
val height = 2 * BLOCK_VERTICAL_PADDING + rowCount * lineHeight
|
||||
val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = BLOCK_COLOR }
|
||||
|
||||
canvas.drawRoundRect(RectF(BLOCK_LEFT, top, BLOCK_LEFT + BLOCK_WIDTH, top + height), BLOCK_CORNER_RADIUS, BLOCK_CORNER_RADIUS, backgroundPaint)
|
||||
drawContent(top + BLOCK_VERTICAL_PADDING)
|
||||
|
||||
return top + height
|
||||
}
|
||||
|
||||
/** Draws a section header above a key block, returning the y position content below it should start at. */
|
||||
private fun drawSectionHeader(canvas: Canvas, paint: Paint, text: String, top: Float): Float {
|
||||
val textTop = top + HEADER_TOP_PADDING
|
||||
canvas.drawText(text, MARGIN, textTop - paint.fontMetrics.ascent, paint)
|
||||
return textTop + paint.fontMetrics.let { it.descent - it.ascent } + HEADER_BOTTOM_PADDING
|
||||
/** Draws the recovery key groups as evenly-spaced columns spanning the width of the block's content area. */
|
||||
private fun drawRecoveryKeyGroups(canvas: Canvas, paint: TextPaint, rows: List<List<String>>, top: Float) {
|
||||
val groupWidth = rows.flatten().maxOfOrNull { paint.measureText(it) } ?: 0f
|
||||
val columnSpacing = (CONTENT_WIDTH - groupWidth) / (GROUPS_PER_ROW - 1)
|
||||
|
||||
rows.forEachIndexed { rowIndex, row ->
|
||||
row.forEachIndexed { columnIndex, group ->
|
||||
drawText(
|
||||
canvas = canvas,
|
||||
text = group,
|
||||
paint = paint,
|
||||
left = CONTENT_LEFT + columnIndex * columnSpacing,
|
||||
top = top + rowIndex * RECOVERY_KEY_LINE_HEIGHT,
|
||||
width = groupWidth,
|
||||
lineHeight = RECOVERY_KEY_LINE_HEIGHT,
|
||||
alignment = Layout.Alignment.ALIGN_NORMAL
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a rounded block containing rows of key text, returning the y position of the block's bottom edge.
|
||||
* Rows with a single entry are drawn left-aligned; rows with multiple groups are spaced evenly across the
|
||||
* block, mirroring how the screen lays out recovery key groups.
|
||||
* Draws [text] wrapped to [width], with each line occupying [lineHeight] and its glyphs centered within that,
|
||||
* matching how the design lays text out. Returns the total height consumed.
|
||||
*/
|
||||
private fun drawKeyBlock(canvas: Canvas, paint: Paint, rows: List<List<String>>, top: Float): Float {
|
||||
val blockWidth = PAGE_WIDTH - 2 * MARGIN
|
||||
val blockHeight = 2 * BLOCK_VERTICAL_PADDING + rows.size * KEY_LINE_HEIGHT
|
||||
private fun drawText(canvas: Canvas, text: String, paint: TextPaint, left: Float, top: Float, width: Float, lineHeight: Float, alignment: Layout.Alignment): Float {
|
||||
val glyphHeight = paint.fontMetrics.let { it.descent - it.ascent }
|
||||
val layout = StaticLayout.Builder
|
||||
.obtain(text, 0, text.length, paint, ceil(width).toInt())
|
||||
.setAlignment(alignment)
|
||||
.setIncludePad(false)
|
||||
.setLineSpacing(lineHeight - glyphHeight, 1f)
|
||||
.build()
|
||||
|
||||
val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = BLOCK_COLOR }
|
||||
canvas.drawRoundRect(RectF(MARGIN, top, MARGIN + blockWidth, top + blockHeight), BLOCK_CORNER_RADIUS, BLOCK_CORNER_RADIUS, backgroundPaint)
|
||||
|
||||
val innerLeft = MARGIN + BLOCK_HORIZONTAL_PADDING
|
||||
val innerWidth = blockWidth - 2 * BLOCK_HORIZONTAL_PADDING
|
||||
val groupWidth = rows.flatten().maxOfOrNull { paint.measureText(it) } ?: 0f
|
||||
|
||||
var lineTop = top + BLOCK_VERTICAL_PADDING
|
||||
for (row in rows) {
|
||||
val spacing = if (row.size > 1) (innerWidth - GROUPS_PER_ROW * groupWidth) / (GROUPS_PER_ROW - 1) else 0f
|
||||
val baseline = lineTop + (KEY_LINE_HEIGHT - (paint.fontMetrics.descent - paint.fontMetrics.ascent)) / 2f - paint.fontMetrics.ascent
|
||||
|
||||
row.forEachIndexed { index, group ->
|
||||
canvas.drawText(group, innerLeft + index * (groupWidth + spacing), baseline, paint)
|
||||
}
|
||||
|
||||
lineTop += KEY_LINE_HEIGHT
|
||||
canvas.withTranslation(left, top + (lineHeight - glyphHeight) / 2f) {
|
||||
layout.draw(this)
|
||||
}
|
||||
|
||||
return top + blockHeight
|
||||
return layout.lineCount * lineHeight
|
||||
}
|
||||
|
||||
private fun textPaint(textSize: Float, letterSpacing: Float, color: Int, semiBold: Boolean): TextPaint {
|
||||
return TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
this.typeface = if (semiBold) Typeface.create("sans-serif-medium", Typeface.NORMAL) else Typeface.SANS_SERIF
|
||||
this.textSize = textSize
|
||||
this.letterSpacing = letterSpacing
|
||||
this.color = color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:width="98dp"
|
||||
android:height="56dp"
|
||||
android:viewportWidth="98"
|
||||
android:viewportHeight="56"
|
||||
tools:ignore="VectorRaster">
|
||||
<path
|
||||
android:fillColor="#FF343DBF"
|
||||
android:pathData="M8 0h82c4.42 0 8 3.58 8 8v40c0 4.42 -3.58 8 -8 8H8c-4.42 0 -8 -3.58 -8 -8V8c0 -4.42 3.58 -8 8 -8z"/>
|
||||
<path
|
||||
android:fillColor="#FF3A42C0"
|
||||
android:pathData="M90 0C94.4183 0.00000109524 98 3.58172 98 8V48C98 52.4183 94.4183 56 90 56H8C3.58172 56 0.000000193283 52.4183 0 48V32.8477C5.65568 33.043 13.2368 30.8514 20.5537 22.4004C34.6249 6.14818 54.4938 9.4255 65.9336 8.0459C73.3433 7.1523 79.6847 3.9108 85.208 0H90Z"/>
|
||||
<path
|
||||
android:fillColor="#FF4950C7"
|
||||
android:pathData="M97.6348 5.60547C97.8717 6.36157 98 7.16577 98 8V48C98 52.4183 94.4183 56 90 56H8C3.58172 56 0.000000193283 52.4183 0 48V43.5742C5.83005 44.9192 15.4133 44.004 24.5898 33.5996C38.9241 17.3474 59.1647 20.6257 70.8184 19.2461C81.7992 17.9461 90.4765 11.676 97.6348 5.60547Z"/>
|
||||
<path
|
||||
android:fillColor="#FF575FCE"
|
||||
android:fillAlpha="0.6"
|
||||
android:pathData="M98 48C98 52.4183 94.4183 56 90 56H18.6367C22.198 54.4289 25.974 51.7642 29.6826 47.5107C43.8196 31.2967 63.7812 34.5668 75.2744 33.1904C84.269 32.1133 91.6961 27.615 98 22.6523V48Z"/>
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M21.0328 12.3813C20.4074 12.3813 19.8914 11.8936 19.8914 11.3024C19.8914 10.7111 20.4074 10.2234 21.0328 10.2234C21.6582 10.2234 22.1742 10.7111 22.1742 11.3024C22.1742 11.8936 21.6582 12.3813 21.0328 12.3813Z"/>
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M20.0869 21.6486V13.3864H21.9788V21.6486H20.0869Z"/>
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M52.4371 21.6486V10.3564H50.5452V21.6486H52.4371Z"/>
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M41.0739 17.5175C41.0739 20.2593 42.6375 21.7817 44.7092 21.7817C45.8036 21.7817 46.6792 21.2422 47.2265 20.3184V21.6486H49.1418V13.3864H47.2265V14.7092C46.6792 13.7855 45.8271 13.2534 44.7639 13.2534C42.6375 13.2534 41.0739 14.7758 41.0739 17.5175ZM47.3359 17.5175C47.3359 19.1803 46.5932 20.3184 45.2017 20.3184C43.7945 20.3184 43.0518 19.1138 43.0518 17.5175C43.0518 15.9212 43.7945 14.7684 45.2017 14.7684C46.5932 14.7684 47.3359 15.8547 47.3359 17.5175Z"/>
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M34.4561 16.7933V21.6486H32.5252V13.3864H34.3545L34.3779 15.0049C34.9095 13.852 35.8086 13.2829 37.075 13.2829C38.8731 13.2829 40.0614 14.3915 40.0614 16.3942V21.6486H38.1304V16.6603C38.1304 15.5222 37.4659 14.8423 36.3793 14.8423C35.2613 14.8423 34.4561 15.5591 34.4561 16.7933Z"/>
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M27.08 24.9151C29.308 24.9151 31.1217 23.9396 31.1217 21.523V13.3864H29.2298V14.8201C28.6826 13.8372 27.7836 13.2534 26.6265 13.2534C24.5548 13.2534 22.9913 14.7388 22.9913 17.3919C22.9913 20.045 24.5548 21.5304 26.6265 21.5304C27.7679 21.5304 28.6591 20.9688 29.2064 20.0006V21.6191C29.2064 22.8089 28.4559 23.5036 27.08 23.5036C26.1106 23.5036 25.3835 23.1489 25.1412 22.432H23.304C23.562 23.9766 24.9223 24.9151 27.08 24.9151ZM29.2533 17.3919C29.2533 18.9734 28.5106 20.0671 27.119 20.0671C25.7119 20.0671 24.9692 18.9069 24.9692 17.3919C24.9692 15.8769 25.7119 14.7684 27.119 14.7684C28.5106 14.7684 29.2533 15.8104 29.2533 17.3919Z"/>
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M10.2091 18.4856C10.3107 20.5919 11.9915 21.826 14.6652 21.826C17.4248 21.826 19.0274 20.4884 19.0274 18.5743C19.0274 16.6307 17.2528 15.8178 15.6345 15.4409L14.5244 15.1674C13.6098 14.9457 12.4918 14.5688 12.4918 13.5933C12.4918 12.7139 13.3361 12.0709 14.6964 12.0709C15.9785 12.0709 16.8619 12.6326 16.9792 13.6007H18.8711C18.8163 11.7975 17.1746 10.4894 14.7277 10.4894C12.3277 10.4894 10.5296 11.7753 10.5296 13.6968C10.5296 15.234 11.671 16.1577 13.5941 16.6381L14.9388 16.978C16.1896 17.2884 17.0886 17.6579 17.0886 18.5595C17.0886 19.5646 16.0645 20.2297 14.6495 20.2297C13.3205 20.2297 12.2417 19.6755 12.1479 18.4856H10.2091Z"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M8 0.5h82c4.14 0 7.5 3.36 7.5 7.5v40c0 4.14 -3.36 7.5 -7.5 7.5H8c-4.14 0 -7.5 -3.36 -7.5 -7.5V8c0 -4.14 3.36 -7.5 7.5 -7.5z">
|
||||
<aapt:attr name="android:strokeColor">
|
||||
<gradient
|
||||
android:startX="49"
|
||||
android:startY="0"
|
||||
android:endX="49"
|
||||
android:endY="56"
|
||||
android:type="linear">
|
||||
<item android:color="#FF333BA8" android:offset="0"/>
|
||||
<item android:color="#FF454CBA" android:offset="1"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
||||
@@ -27,4 +27,14 @@
|
||||
<string name="SignalLoginViewDetailsScreen__unable_to_save_pdf">Unable to save PDF. Please try again.</string>
|
||||
<!-- Toast shown when the location the user picked for the Signal Login PDF refuses the write. -->
|
||||
<string name="SignalLoginViewDetailsScreen__cant_save_pdf_to_the_selected_location">Can\'t save PDF to the selected location.</string>
|
||||
|
||||
<!-- Signal Login PDF -->
|
||||
<!-- Title at the top of the PDF that holds the user's Signal Login. -->
|
||||
<string name="SignalLoginPdf__your_signal_login">Your Signal Login</string>
|
||||
<!-- Explainer below the title of the PDF that holds the user's Signal Login. -->
|
||||
<string name="SignalLoginPdf__store_this_in_a_safe_place">Store this in a safe place, your login info is required to recover your account and data. Never share it with anyone.</string>
|
||||
<!-- Section header above the account key in the PDF. -->
|
||||
<string name="SignalLoginPdf__account_id">Account ID</string>
|
||||
<!-- Section header above the recovery key in the PDF. -->
|
||||
<string name="SignalLoginPdf__recovery_key">Recovery Key</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user