From 4ddc42ea51d1ba0d8b0e5882058f6dd992e14821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Sch=C3=A4ttgen?= Date: Thu, 22 Aug 2024 01:05:35 +0200 Subject: [PATCH] Fix sizing inconsistency of the dots in hidden view --- .../aegis/helpers/CenterVerticalSpan.java | 30 ++++++++++ .../aegis/ui/views/EntryHolder.java | 59 +++++++++++++++++-- 2 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/beemdevelopment/aegis/helpers/CenterVerticalSpan.java diff --git a/app/src/main/java/com/beemdevelopment/aegis/helpers/CenterVerticalSpan.java b/app/src/main/java/com/beemdevelopment/aegis/helpers/CenterVerticalSpan.java new file mode 100644 index 0000000000..bc94a21cfa --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/helpers/CenterVerticalSpan.java @@ -0,0 +1,30 @@ +package com.beemdevelopment.aegis.helpers; + +import android.graphics.Rect; +import android.text.TextPaint; +import android.text.style.MetricAffectingSpan; + +import androidx.annotation.NonNull; + +public class CenterVerticalSpan extends MetricAffectingSpan { + Rect _substringBounds; + + public CenterVerticalSpan(Rect substringBounds) { + _substringBounds = substringBounds; + } + + @Override + public void updateMeasureState(@NonNull TextPaint textPaint) { + applyBaselineShift(textPaint); + } + + @Override + public void updateDrawState(@NonNull TextPaint textPaint) { + applyBaselineShift(textPaint); + } + + private void applyBaselineShift(TextPaint textPaint) { + float topDifference = textPaint.getFontMetrics().top - _substringBounds.top; + textPaint.baselineShift -= (topDifference / 2f); + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryHolder.java b/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryHolder.java index b9ce5c1871..ebfc9c290a 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryHolder.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryHolder.java @@ -1,6 +1,11 @@ package com.beemdevelopment.aegis.ui.views; +import android.graphics.Paint; +import android.graphics.Rect; import android.os.Handler; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.RelativeSizeSpan; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; @@ -17,6 +22,7 @@ import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.ViewMode; import com.beemdevelopment.aegis.helpers.AnimationsHelper; +import com.beemdevelopment.aegis.helpers.CenterVerticalSpan; import com.beemdevelopment.aegis.helpers.SimpleAnimationEndListener; import com.beemdevelopment.aegis.helpers.UiRefresher; import com.beemdevelopment.aegis.otp.HotpInfo; @@ -276,6 +282,10 @@ public void refreshCode() { } private void updateCode() { + _profileCode.setText(getOtp()); + } + + private String getOtp() { OtpInfo info = _entry.getInfo(); // In previous versions of Aegis, it was possible to import entries with an empty @@ -292,7 +302,7 @@ private void updateCode() { otp = _view.getResources().getString(R.string.error_all_caps); } - _profileCode.setText(otp); + return otp; } private String formatCode(String code) { @@ -330,12 +340,53 @@ public void revealCode() { } public void hideCode() { - String hiddenText = new String(new char[_entry.getInfo().getDigits()]).replace("\0", Character.toString(HIDDEN_CHAR)); - hiddenText = formatCode(hiddenText); - _profileCode.setText(hiddenText); + String code = getOtp(); + String hiddenText = code.replaceAll("\\S", Character.toString(HIDDEN_CHAR)); + updateTextViewWithDots(_profileCode, hiddenText, code); + _hidden = true; } + private void updateTextViewWithDots(TextView textView, String hiddenCode, String code) { + Paint paint = new Paint(); + paint.setTextSize(_profileCode.getTextSize()); + + // Calculate the difference between the actual code width and the dots width + float codeWidth = paint.measureText(code); + float dotsWidth = paint.measureText(hiddenCode); + float scaleFactor = codeWidth / dotsWidth; + scaleFactor = (float)(Math.round(scaleFactor * 10.0) / 10.0); + + // If scale is higher or equal to 0.8, do nothing and proceed with the normal text rendering + if (scaleFactor >= 0.8) { + textView.setText(hiddenCode); + return; + } + + // We need to use an invisible character in order to get the height of the profileCode textview consistent + // Tokens without a space (ie Steam TOTP) will get misaligned without this + SpannableString dotsString = new SpannableString("\u200B" + hiddenCode); + + // Only scale the digits/characters, skip the spaces + int start = 1; + for (int i = 0; i <= dotsString.length(); i++) { + if (i == dotsString.length() || dotsString.charAt(i) == ' ') { + if (i > start) { + dotsString.setSpan(new RelativeSizeSpan(scaleFactor), start, i, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + start = i + 1; + } + } + + Rect dotsRectBounds = new Rect(); + paint.getTextBounds(hiddenCode, 1, hiddenCode.length(), dotsRectBounds); + + // Use custom CenterVerticalSpan to make sure the dots are vertically aligned + dotsString.setSpan(new CenterVerticalSpan(dotsRectBounds), 1, dotsString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + textView.setText(dotsString); + } + public void showIcon(boolean show) { if (show) { _profileDrawable.setVisibility(View.VISIBLE);