update payMV QR Design

This commit is contained in:
2026-09-26 04:05:36 +05:00
parent e717360d00
commit 2688118f52
5 changed files with 294 additions and 86 deletions
@@ -7,12 +7,15 @@ import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.text.TextPaint
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.FileProvider
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
@@ -65,7 +68,18 @@ class PayMvQrFragment : Fragment() {
private data class QrTarget(val accountNumber: String, val name: String, val bank: String)
private fun currentTarget(): QrTarget? = contactTarget
?: selectedAccount?.let { QrTarget(it.accountNumber, it.accountBriefName, it.bank) }
?: selectedAccount?.let { QrTarget(it.accountNumber, qrHolderName(it), it.bank) }
/** Name printed on the card and put in the payload (tag 59). */
private fun qrHolderName(account: BankAccount): String = when {
// Fahipay's brief name is the generic "Fahipay Wallet"; the holder's name is on the profile
account.bank == "FAHIPAY" -> account.profileName.takeIf { it.isNotBlank() && it != "Fahipay" }
?: CredentialStore(requireContext())
.loadFahipayUserProfile(sh.sar.basedbank.util.ProfileImageStore.loginIdFromTag(account.loginTag))
?.fullName?.takeIf { it.isNotBlank() }
?: account.accountBriefName
else -> account.accountBriefName
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
contactTarget = arguments?.let { args ->
@@ -146,11 +160,13 @@ class PayMvQrFragment : Fragment() {
"FAHIPAY" -> "FAHIMVMV"
else -> "MADVMVMV"
}
val amountFormatted = binding.etAmount.text?.toString()?.trim()
?.replace(",", "")
val amountRaw = binding.etAmount.text?.toString()?.trim()?.replace(",", "")
val amountFormatted = amountRaw
?.toDoubleOrNull()
?.takeIf { it > 0 }
?.let { "%.2f".format(it) }
// BML shows the amount on the card as typed (no forced decimals)
val amountDisplay = amountRaw?.takeIf { amountFormatted != null }
val ctx = requireContext()
val account = selectedAccount
@@ -165,17 +181,28 @@ class PayMvQrFragment : Fragment() {
when {
m.startsWith("+") -> m
m.length == 7 -> "+960$m"
m.length == 10 && m.startsWith("960") -> "+$m" // Fahipay stores 960XXXXXXX
else -> m
}
}
} else null
val purpose = binding.etReference.text?.toString()?.trim()
?.takeIf { it.isNotBlank() } ?: getString(R.string.paymvqr_reference_default)
?.takeIf { it.isNotBlank() }
// The reference (62/05) is also printed vertically beside the QR, as each bank does
val reference = when (target.bank) {
// BML: base-32 account number followed by the amount as typed
"BML" -> ((target.accountNumber.toBigIntegerOrNull()?.toString(32)?.uppercase() ?: "") +
(amountDisplay ?: "")).take(25).ifEmpty { generateReference(9) }
"FAHIPAY" -> "P" + generateReference(9) // Fahipay's own references are P + 9 chars
else -> generateReference(9)
}
val bmp = withContext(Dispatchers.Default) {
val payload = buildQrPayload(target.accountNumber, target.name, acquirer, amountFormatted, mobile, purpose)
renderQrCard(ctx, target, payload, amountFormatted)
val payload = buildQrPayload(target.accountNumber, target.name, acquirer, amountFormatted, mobile, purpose, reference, target.bank)
if (target.bank == "FAHIPAY") renderFahipayQrCard(ctx, target, payload, reference)
else renderQrCard(ctx, target, payload, reference)
}
if (_binding == null) return
generatedBitmap = bmp
@@ -194,14 +221,19 @@ class PayMvQrFragment : Fragment() {
acquirer: String,
amountStr: String?,
mobile: String?,
purpose: String
purpose: String?,
ref: String,
bank: String
): String {
fun tlv(tag: String, value: String): String {
val len = value.length
return tag + (if (len < 10) "0$len" else "$len") + value
}
val format = tlv("00", "01")
val poi = tlv("01", "11")
// Fahipay's own QRs are dynamic (12) when they carry an amount and mask the amount
// as "***" when they don't; its scanner may reject QRs that differ
val fahipay = bank == "FAHIPAY"
val poi = tlv("01", if (fahipay && !amountStr.isNullOrBlank()) "12" else "11")
val sub00 = tlv("00", "mv.favara.mpqr")
val sub01 = tlv("01", acquirer)
val sub02 = tlv("02", acquirer) // repeated acquirer, as per official PayMV app
@@ -211,21 +243,28 @@ class PayMvQrFragment : Fragment() {
val merchantAcct = tlv("26", sub00 + sub01 + sub02 + sub03 + sub05 + sub10)
val mcc = tlv("52", "0000")
val currency = tlv("53", "462")
val amountTLV = if (!amountStr.isNullOrBlank()) tlv("54", amountStr) else ""
val amountTLV = when {
!amountStr.isNullOrBlank() -> tlv("54", amountStr)
fahipay -> tlv("54", "***")
else -> ""
}
val country = tlv("58", "MV")
val name = tlv("59", accountName.take(25))
val ref = generateReference()
val addlData = tlv("62", tlv("05", ref) + tlv("08", purpose))
val name = tlv("59", accountName.uppercase().take(25))
// Fahipay's QRs always carry a city ("LD" + 4 digits) and default the purpose to PAYMENT
val city = if (fahipay) tlv("60", "LD" + (0..9999).random().toString().padStart(4, '0')) else ""
val purposeText = purpose?.takeIf { it.isNotBlank() } ?: if (fahipay) "PAYMENT" else null
val purposeTLV = if (purposeText != null) tlv("08", purposeText) else ""
val addlData = tlv("62", tlv("05", ref) + purposeTLV)
val timestamp = java.time.LocalDateTime.now()
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.00000"))
val tag80 = tlv("80", tlv("00", "mv.favara.mpqr") + tlv("01", timestamp))
val prefix = format + poi + merchantAcct + mcc + currency + amountTLV + country + name + addlData + tag80 + "6304"
val prefix = format + poi + merchantAcct + mcc + currency + amountTLV + country + name + city + addlData + tag80 + "6304"
return prefix + crc16(prefix)
}
private fun generateReference(): String {
private fun generateReference(length: Int): String {
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return (1..9).map { chars.random() }.joinToString("")
return (1..length).map { chars.random() }.joinToString("")
}
private fun crc16(data: String): String {
@@ -242,88 +281,127 @@ class PayMvQrFragment : Fragment() {
// ── QR card rendering ────────────────────────────────────────────────────
/**
* Replicates the BML app's ReceiveCard (React Native, v2.1.47) 1:1. All measurements are in
* dp, as in BML's StyleSheet, laid out for BML's reference screen width and drawn at
* [PX_PER_DP] pixels per dp.
*/
private fun renderQrCard(
ctx: Context,
target: QrTarget,
qrPayload: String,
amountStr: String?
qrId: String
): Bitmap {
val W = 900
val H = 1080
val outerCorner = 48f
val boxBlue = Color.parseColor("#2272B7")
val footerBlue = Color.parseColor("#1A5799")
val boxL = 24f; val boxT = 110f; val boxR = 876f; val boxB = 962f
val sw = SCREEN_WIDTH_DP
fun px(dp: Float) = dp * PX_PER_DP
val mmaBlue = Color.parseColor("#0E5CA4")
val bm = Bitmap.createBitmap(W, H, Bitmap.Config.ARGB_8888)
val cardW = sw - 48f // screen's horizontal margins, spacing[5] each side
val qrSize = sw * 0.5f
val railW = sw / 1.85f
val namePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
textSize = px(14f)
typeface = Typeface.DEFAULT
textAlign = Paint.Align.CENTER
}
val footerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
textSize = px(sw * 0.046f)
typeface = ResourcesCompat.getFont(ctx, R.font.sofia_pro_bold) ?: Typeface.DEFAULT_BOLD
letterSpacing = 1.2f / (sw * 0.046f) // RN letterSpacing is in dp, Paint's is in em
textAlign = Paint.Align.CENTER
}
// Android RN Text lines include font padding: line height = bottom - top
fun lineHeight(p: Paint) = p.fontMetrics.let { it.bottom - it.top } / PX_PER_DP
// --- Vertical layout (dp, card-local) ---
val topCardTop = 2f
val brandTop = topCardTop + 32f // brandRow marginTop spacing[6]
val logoBoxW = sw * 0.38f // bml-logo-paymv box: 0.38·sw wide
val logoBoxH = logoBoxW * 0.1116751269035533f
val payMvBoxW = sw * 0.2f // paymv-logo box: 0.2·sw wide
val payMvBoxH = payMvBoxW * 0.17333333333333334f
val brandH = maxOf(logoBoxH, payMvBoxH)
val nameText = target.name.uppercase()
val hasName = nameText.isNotBlank()
val qrCardTop = brandTop + brandH + if (hasName) 24f else 32f
val nameTop = qrCardTop + 8f + 12f // qrCard paddingTop spacing[2], name marginTop spacing[3]
val nameH = if (hasName) lineHeight(namePaint) else 0f
val qrTop = if (hasName) nameTop + nameH + 16f else qrCardTop + 37f
val qrCardBottom = qrTop + qrSize + 37f // paddingBottom spacing[6] + 5
val topCardBottom = qrCardBottom + 24f + 8f // qrCard marginBottom, topCard paddingBottom
val footerLineH = lineHeight(footerPaint)
val cardH = topCardBottom + 12f + footerLineH + 12f + 2f
val bm = Bitmap.createBitmap(px(cardW).toInt(), px(cardH).toInt(), Bitmap.Config.ARGB_8888)
val canvas = Canvas(bm)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
// Clip to outer rounded card shape
val outerPath = Path()
outerPath.addRoundRect(RectF(0f, 0f, W.toFloat(), H.toFloat()), outerCorner, outerCorner, Path.Direction.CW)
canvas.clipPath(outerPath)
canvas.drawColor(Color.WHITE)
// --- Bank logo top-left ---
val logoRes = when (target.bank) {
"BML" -> R.drawable.bml_logo_vector
"MIB" -> R.drawable.mib_faisanet_logo
else -> R.drawable.fahipay_logo_long
// captureWrapper: mmaBlue, radius 20 — shows as a 2dp border around the white top card
val outerPath = Path().apply {
addRoundRect(RectF(0f, 0f, px(cardW), px(cardH)), px(20f), px(20f), Path.Direction.CW)
}
canvas.clipPath(outerPath)
canvas.drawColor(mmaBlue)
// topCard: white, 2dp inset, top corners 18
paint.color = Color.WHITE
val r = px(18f)
canvas.drawPath(Path().apply {
addRoundRect(
RectF(px(2f), px(topCardTop), px(cardW - 2f), px(topCardBottom)),
floatArrayOf(r, r, r, r, 0f, 0f, 0f, 0f), Path.Direction.CW
)
}, paint)
// --- brandRow: "BANK OF MALDIVES" wordmark left, "PayMV QR" right, 40dp side margins ---
val rowL = 2f + 40f
val rowR = cardW - 2f - 40f
val rowCenterY = brandTop + brandH / 2
val logoRes = if (target.bank == "BML") R.drawable.bml_logo_paymv else R.drawable.mib_faisanet_logo
AppCompatResources.getDrawable(ctx, logoRes)?.let { d ->
val nW = d.intrinsicWidth.coerceAtLeast(1)
val nH = d.intrinsicHeight.coerceAtLeast(1)
val maxW = 180f; val maxH = 76f
val scale = minOf(maxW / nW, maxH / nH)
// resizeMode "contain" inside the logo box, left-aligned in the row
val scale = minOf(px(logoBoxW) / nW, px(logoBoxH) / nH)
val lW = (nW * scale).toInt()
val lH = (nH * scale).toInt()
val lTop = ((boxT - lH) / 2).toInt().coerceAtLeast(10)
d.setBounds(24, lTop, 24 + lW, lTop + lH)
val lLeft = (px(rowL) + (px(logoBoxW) - lW) / 2f).toInt()
val lTop = (px(rowCenterY) - lH / 2f).toInt()
d.setBounds(lLeft, lTop, lLeft + lW, lTop + lH)
d.draw(canvas)
}
// --- "PayMV QR" top-right ---
paint.color = Color.parseColor("#1A1A2E")
paint.textSize = 36f
paint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
// BML draws the paymv-logo image (fills its box exactly), nudged down 1dp
paint.color = mmaBlue
paint.typeface = footerPaint.typeface
paint.textAlign = Paint.Align.RIGHT
canvas.drawText("PayMV QR", W - 28f, 66f, paint)
paint.textSize = px(payMvBoxH)
paint.textSize *= px(payMvBoxW) / paint.measureText("PayMV QR")
val payMvBounds = Rect().also { paint.getTextBounds("PayMV QR", 0, 8, it) }
canvas.drawText(
"PayMV QR", px(rowR),
px(rowCenterY + 1f) - payMvBounds.exactCenterY(), paint
)
// --- Blue rounded box ---
paint.color = boxBlue
paint.textAlign = Paint.Align.LEFT
canvas.drawRoundRect(RectF(boxL, boxT, boxR, boxB), 36f, 36f, paint)
// --- qrCard: mmaBlue, radius 16, 40dp side margins ---
val qrCardL = 2f + 40f
val qrCardR = cardW - 2f - 40f
paint.color = mmaBlue
canvas.drawRoundRect(RectF(px(qrCardL), px(qrCardTop), px(qrCardR), px(qrCardBottom)), px(16f), px(16f), paint)
// Account name (white, bold, uppercase, auto-scaled to fit)
paint.color = Color.WHITE
paint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
paint.textAlign = Paint.Align.CENTER
val nameText = target.name.uppercase()
paint.textSize = 36f
val maxNameW = boxR - boxL - 48f
if (paint.measureText(nameText) > maxNameW) {
paint.textSize = 36f * maxNameW / paint.measureText(nameText)
}
val nameBaseline = boxT + 68f
canvas.drawText(nameText, W / 2f, nameBaseline, paint)
// Optional amount below name
val qrTopY: Float
if (!amountStr.isNullOrBlank()) {
paint.textSize = 28f
paint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
val amtBaseline = nameBaseline + 42f
canvas.drawText("MVR $amountStr", W / 2f, amtBaseline, paint)
qrTopY = amtBaseline + 20f
} else {
qrTopY = nameBaseline + 26f
if (hasName) {
val maxNameW = px(qrCardR - qrCardL)
if (namePaint.measureText(nameText) > maxNameW) {
namePaint.textSize *= maxNameW / namePaint.measureText(nameText)
}
canvas.drawText(nameText, px(cardW / 2), px(nameTop) - namePaint.fontMetrics.top, namePaint)
}
// QR code — white modules on the same blue as the box background
val availH = boxB - qrTopY - 24f
val qrPx = minOf(availH, boxR - boxL - 48f).toInt().coerceAtMost(700).coerceAtLeast(200)
val qrLeft = ((W - qrPx) / 2).toFloat()
// QR — white modules on mmaBlue, ECL M, no quiet zone (react-native-qrcode-svg defaults)
val qrPx = px(qrSize).toInt()
val qrLeft = px(cardW / 2) - qrPx / 2f
try {
val hints = mapOf(
EncodeHintType.MARGIN to 0,
@@ -333,23 +411,150 @@ class PayMvQrFragment : Fragment() {
val pixels = IntArray(qrPx * qrPx)
for (y in 0 until qrPx) {
for (x in 0 until qrPx) {
pixels[y * qrPx + x] = if (matrix[x, y]) Color.WHITE else boxBlue
pixels[y * qrPx + x] = if (matrix[x, y]) Color.WHITE else mmaBlue
}
}
val qrBm = Bitmap.createBitmap(pixels, qrPx, qrPx, Bitmap.Config.ARGB_8888)
canvas.drawBitmap(qrBm, qrLeft, qrTopY, null)
canvas.drawBitmap(qrBm, qrLeft, px(qrTop), null)
qrBm.recycle()
} catch (_: Exception) { /* skip if encoding fails */ }
// --- Dark blue footer ---
paint.color = footerBlue
paint.textAlign = Paint.Align.LEFT
canvas.drawRect(RectF(0f, 970f, W.toFloat(), H.toFloat()), paint)
// qrIdRail: the reference, rotated -90°, beside the QR's right edge
if (qrId.isNotEmpty()) {
val idPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLACK
alpha = (255 * 0.8f).toInt()
textSize = px(10f)
typeface = Typeface.DEFAULT
textAlign = Paint.Align.CENTER
}
val qrRight = cardW / 2 + qrSize / 2
val cx = px(qrRight + railW / 1.37f - railW / 2)
val cy = px(qrTop + (qrSize + railW / 1.5f) / 2)
val idText = TextUtils.ellipsize(qrId, TextPaint(idPaint), px(railW), TextUtils.TruncateAt.END).toString()
canvas.save()
canvas.rotate(-90f, cx, cy)
val fm = idPaint.fontMetrics
canvas.drawText(idText, cx, cy - (fm.bottom + fm.top) / 2, idPaint)
canvas.restore()
}
// --- footer: SofiaPro-Bold, letterSpacing 1.2 ---
canvas.drawText(
"MALDIVES NATIONAL QR", px(cardW / 2),
px(topCardBottom + 12f) - footerPaint.fontMetrics.top, footerPaint
)
return bm
}
/**
* Replicates the card Fahipay's server renders for PayMV QR (api/app/qr/), measured
* in pixels on its 1240×1322 image. Fonts follow the BML card (Sofia Pro Bold, Roboto),
* except the vertical reference, which is Montserrat as on Fahipay's.
*/
private fun renderFahipayQrCard(
ctx: Context,
target: QrTarget,
qrPayload: String,
reference: String
): Bitmap {
val w = 1240f
val h = 1322f
val blue = Color.parseColor("#005DA3")
val sofiaBold = ResourcesCompat.getFont(ctx, R.font.sofia_pro_bold) ?: Typeface.DEFAULT_BOLD
val montserrat = ResourcesCompat.getFont(ctx, R.font.montserrat_regular) ?: Typeface.DEFAULT
val bm = Bitmap.createBitmap(w.toInt(), h.toInt(), Bitmap.Config.ARGB_8888)
val canvas = Canvas(bm)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
// Blue card with a 6px border around the white area; footer is the blue below it
canvas.clipPath(Path().apply {
addRoundRect(RectF(0f, 0f, w, h), 50f, 50f, Path.Direction.CW)
})
canvas.drawColor(blue)
paint.color = Color.WHITE
paint.textSize = 32f
paint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
paint.textAlign = Paint.Align.CENTER
canvas.drawText("MALDIVES NATIONAL QR", W / 2f, 1038f, paint)
canvas.drawPath(Path().apply {
addRoundRect(
RectF(6f, 6f, w - 6f, 1174f),
floatArrayOf(44f, 44f, 44f, 44f, 0f, 0f, 0f, 0f), Path.Direction.CW
)
}, paint)
// Square app icon, then the "FahiPay" wordmark, in one row
AppCompatResources.getDrawable(ctx, R.drawable.fahipay_logo)?.let { d ->
d.setBounds(94, 94, 163, 163)
d.draw(canvas)
}
AppCompatResources.getDrawable(ctx, R.drawable.fahipay_logo_long)?.let { d ->
val lH = 48
val lW = (lH * d.intrinsicWidth.toFloat() / d.intrinsicHeight.coerceAtLeast(1)).toInt()
d.setBounds(178, 104, 178 + lW, 104 + lH)
d.draw(canvas)
}
// Sized so capitals match the reference's cap heights; positions below are cap tops
fun textPaint(tf: Typeface, capH: Float, spacingEm: Float, align: Paint.Align) =
Paint(Paint.ANTI_ALIAS_FLAG).apply {
typeface = tf
letterSpacing = spacingEm
textAlign = align
textSize = 100f
val h = Rect().also { getTextBounds("H", 0, 1, it) }.height().coerceAtLeast(1)
textSize = 100f * capH / h
}
fun Paint.capHeight() = Rect().also { getTextBounds("H", 0, 1, it) }.height()
// "PayMV QR" top-right
val payMvPaint = textPaint(sofiaBold, 30f, 0f, Paint.Align.RIGHT).apply { color = blue }
canvas.drawText("PayMV QR", 1147f, 101f + 30f, payMvPaint)
// Blue QR panel
paint.color = blue
canvas.drawRoundRect(RectF(166f, 218f, 1074f, 1126f), 55f, 55f, paint)
// Account name
val nameText = target.name.uppercase()
if (nameText.isNotBlank()) {
val namePaint = textPaint(Typeface.DEFAULT, 30f, 0f, Paint.Align.CENTER).apply { color = Color.WHITE }
val maxNameW = 1074f - 166f - 80f
if (namePaint.measureText(nameText) > maxNameW) {
namePaint.textSize *= maxNameW / namePaint.measureText(nameText)
}
canvas.drawText(nameText, 619f, 314f + namePaint.capHeight(), namePaint)
}
// QR — white modules on blue, no quiet zone
val qrPx = 562
try {
val hints = mapOf(
EncodeHintType.MARGIN to 0,
EncodeHintType.ERROR_CORRECTION to ErrorCorrectionLevel.M
)
val matrix = QRCodeWriter().encode(qrPayload, BarcodeFormat.QR_CODE, qrPx, qrPx, hints)
val pixels = IntArray(qrPx * qrPx)
for (y in 0 until qrPx) {
for (x in 0 until qrPx) {
pixels[y * qrPx + x] = if (matrix[x, y]) Color.WHITE else blue
}
}
val qrBm = Bitmap.createBitmap(pixels, qrPx, qrPx, Bitmap.Config.ARGB_8888)
canvas.drawBitmap(qrBm, 338f, 417f, null)
qrBm.recycle()
} catch (_: Exception) { /* skip if encoding fails */ }
// Reference, blue, reading bottom-to-top in the white margin right of the panel,
// starting level with y=1087 and with its baseline at x=1171
val refPaint = textPaint(montserrat, 27f, 0.005f, Paint.Align.LEFT).apply { color = blue }
canvas.save()
canvas.rotate(-90f, 1171f, 1087f)
canvas.drawText(reference, 1171f, 1087f, refPaint)
canvas.restore()
// Footer
val footerPaint = textPaint(sofiaBold, 55.5f, 1.2f / 25.76f, Paint.Align.CENTER).apply { color = Color.WHITE }
canvas.drawText("MALDIVES NATIONAL QR", w / 2, 1220f + 55.5f, footerPaint)
return bm
}
@@ -437,6 +642,10 @@ class PayMvQrFragment : Fragment() {
private const val ARG_ACCOUNT_NAME = "account_name"
private const val ARG_BANK = "bank"
/** BML's layout reference: the screen width (dp) its ReceiveCard sizes were captured at. */
private const val SCREEN_WIDTH_DP = 560f
private const val PX_PER_DP = 2f
/** QR for a contact's account. [bank] is "BML" / "MIB" / "FAHIPAY", as on [BankAccount.bank]. */
fun forContact(accountNumber: String, name: String, bank: String) = PayMvQrFragment().apply {
arguments = Bundle().apply {
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.
Binary file not shown.
-1
View File
@@ -127,7 +127,6 @@
<string name="paymvqr_save_failed">Failed to save image</string>
<string name="paymvqr_include_phone">Include phone number</string>
<string name="paymvqr_reference_hint">Reference (optional)</string>
<string name="paymvqr_reference_default">PayMV QR Transfer</string>
<!-- Toolbar -->
<string name="action_lock">Lock app</string>