diff --git a/app/src/main/java/com/beemdevelopment/aegis/Preferences.java b/app/src/main/java/com/beemdevelopment/aegis/Preferences.java index 913b6f03..10f98e73 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/Preferences.java +++ b/app/src/main/java/com/beemdevelopment/aegis/Preferences.java @@ -153,6 +153,10 @@ public class Preferences { return _prefs.getBoolean("pref_show_icons", true); } + public boolean getShowExpirationState() { + return _prefs.getBoolean("pref_expiration_state", true); + } + public CodeGrouping getCodeGroupSize() { String value = _prefs.getString("pref_code_group_size_string", "GROUPING_THREES"); diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java index 1418c72e..1490c6e8 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java @@ -201,6 +201,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene _entryListView.setCodeGroupSize(_prefs.getCodeGroupSize()); _entryListView.setAccountNamePosition(_prefs.getAccountNamePosition()); _entryListView.setShowIcon(_prefs.isIconVisible()); + _entryListView.setShowExpirationState(_prefs.getShowExpirationState()); _entryListView.setOnlyShowNecessaryAccountNames(_prefs.onlyShowNecessaryAccountNames()); _entryListView.setHighlightEntry(_prefs.isEntryHighlightEnabled()); _entryListView.setPauseFocused(_prefs.isPauseFocusedEnabled()); diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/AppearancePreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/AppearancePreferencesFragment.java index b8cb7809..d2c68462 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/AppearancePreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/AppearancePreferencesFragment.java @@ -125,6 +125,11 @@ public class AppearancePreferencesFragment extends PreferencesFragment { return true; }); + Preference showExpirationStatePreference = requirePreference("pref_expiration_state"); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + showExpirationStatePreference.setSummary(getString(R.string.pref_expiration_state_fallback)); + } + String[] codeGroupings = getResources().getStringArray(R.array.pref_code_groupings_values); String[] codeGroupingNames = getResources().getStringArray(R.array.pref_code_groupings); Preference codeDigitGroupingPreference = requirePreference("pref_code_group_size_string"); diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryAdapter.java b/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryAdapter.java index d3c0cbe1..696c7f40 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryAdapter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryAdapter.java @@ -60,6 +60,7 @@ public class EntryAdapter extends RecyclerView.Adapter private Preferences.CodeGrouping _codeGroupSize; private AccountNamePosition _accountNamePosition; private boolean _showIcon; + private boolean _showExpirationState; private boolean _onlyShowNecessaryAccountNames; private boolean _highlightEntry; private boolean _tempHighlightEntry; @@ -115,6 +116,10 @@ public class EntryAdapter extends RecyclerView.Adapter _showIcon = showIcon; } + public void setShowExpirationState(boolean showExpirationState) { + _showExpirationState = showExpirationState; + } + public void setTapToReveal(boolean tapToReveal) { _tapToReveal = tapToReveal; } @@ -539,7 +544,7 @@ public class EntryAdapter extends RecyclerView.Adapter } AccountNamePosition accountNamePosition = showAccountName ? _accountNamePosition : AccountNamePosition.HIDDEN; - entryHolder.setData(entry, _codeGroupSize, _viewMode, accountNamePosition, _showIcon, showProgress, hidden, paused, dimmed); + entryHolder.setData(entry, _codeGroupSize, _viewMode, accountNamePosition, _showIcon, showProgress, hidden, paused, dimmed, _showExpirationState); entryHolder.setFocused(_selectedEntries.contains(entry)); entryHolder.setShowDragHandle(isEntryDraggable(entry)); 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 ebfc9c29..1f44f423 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,7 +1,12 @@ package com.beemdevelopment.aegis.ui.views; +import android.animation.AnimatorSet; +import android.animation.ArgbEvaluator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; import android.graphics.Paint; import android.graphics.Rect; +import android.os.Build; import android.os.Handler; import android.text.Spannable; import android.text.SpannableString; @@ -35,6 +40,7 @@ import com.beemdevelopment.aegis.ui.glide.GlideHelper; import com.beemdevelopment.aegis.vault.VaultEntry; import com.bumptech.glide.Glide; import com.google.android.material.card.MaterialCardView; +import com.google.android.material.color.MaterialColors; public class EntryHolder extends RecyclerView.ViewHolder { private static final float DEFAULT_ALPHA = 1.0f; @@ -52,7 +58,7 @@ public class EntryHolder extends RecyclerView.ViewHolder { private RelativeLayout _description; private ImageView _dragHandle; private ViewMode _viewMode; - + private final ImageView _selected; private final Handler _selectedHandler; @@ -67,6 +73,8 @@ public class EntryHolder extends RecyclerView.ViewHolder { private UiRefresher _refresher; private Handler _animationHandler; + private AnimatorSet _expirationAnimSet; + private boolean _showExpirationState; private Animation _scaleIn; private Animation _scaleOut; @@ -97,9 +105,7 @@ public class EntryHolder extends RecyclerView.ViewHolder { _refresher = new UiRefresher(new UiRefresher.Listener() { @Override public void onRefresh() { - if (!_hidden && !_paused) { - refreshCode(); - } + refreshCode(); } @Override @@ -109,12 +115,13 @@ public class EntryHolder extends RecyclerView.ViewHolder { }); } - public void setData(VaultEntry entry, Preferences.CodeGrouping groupSize, ViewMode viewMode, AccountNamePosition accountNamePosition, boolean showIcon, boolean showProgress, boolean hidden, boolean paused, boolean dimmed) { + public void setData(VaultEntry entry, Preferences.CodeGrouping groupSize, ViewMode viewMode, AccountNamePosition accountNamePosition, boolean showIcon, boolean showProgress, boolean hidden, boolean paused, boolean dimmed, boolean showExpirationState) { _entry = entry; _hidden = hidden; _paused = paused; _codeGrouping = groupSize; _viewMode = viewMode; + _accountNamePosition = accountNamePosition; if (viewMode.equals(ViewMode.TILES) && _accountNamePosition == AccountNamePosition.END) { _accountNamePosition = AccountNamePosition.BELOW; @@ -124,6 +131,7 @@ public class EntryHolder extends RecyclerView.ViewHolder { _selected.setVisibility(View.GONE); _selectedHandler.removeCallbacksAndMessages(null); _animationHandler.removeCallbacksAndMessages(null); + _showExpirationState = _entry.getInfo() instanceof TotpInfo && showExpirationState; _favoriteIndicator.setVisibility(_entry.isFavorite() ? View.VISIBLE : View.INVISIBLE); @@ -149,7 +157,6 @@ public class EntryHolder extends RecyclerView.ViewHolder { } showIcon(showIcon); - itemView.setAlpha(dimmed ? DIMMED_ALPHA : DEFAULT_ALPHA); } @@ -278,6 +285,7 @@ public class EntryHolder extends RecyclerView.ViewHolder { public void refreshCode() { if (!_hidden && !_paused) { updateCode(); + startExpirationAnimation(); } } @@ -336,6 +344,7 @@ public class EntryHolder extends RecyclerView.ViewHolder { public void revealCode() { updateCode(); + startExpirationAnimation(); _hidden = false; } @@ -343,7 +352,7 @@ public class EntryHolder extends RecyclerView.ViewHolder { String code = getOtp(); String hiddenText = code.replaceAll("\\S", Character.toString(HIDDEN_CHAR)); updateTextViewWithDots(_profileCode, hiddenText, code); - + stopExpirationAnimation(); _hidden = true; } @@ -387,6 +396,72 @@ public class EntryHolder extends RecyclerView.ViewHolder { textView.setText(dotsString); } + public void startExpirationAnimation() { + stopExpirationAnimation(); + if (!_showExpirationState) { + return; + } + + final int totalStateDuration = 7000; + TotpInfo info = (TotpInfo) _entry.getInfo(); + if (info.getPeriod() * 1000 < totalStateDuration) { + _profileCode.setTextColor(MaterialColors.getColor(_profileCode, com.google.android.material.R.attr.colorError)); + return; + } + + // Workaround for when animations are disabled or Android version being too old + float durationScale = AnimationsHelper.Scale.ANIMATOR.getValue(itemView.getContext()); + if (durationScale == 0.0 || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + int color = MaterialColors.getColor(_profileCode, com.google.android.material.R.attr.colorError); + if (info.getMillisTillNextRotation() < totalStateDuration) { + _profileCode.setTextColor(color); + } else { + _animationHandler.postDelayed(() -> { + _profileCode.setTextColor(color); + }, info.getMillisTillNextRotation() - totalStateDuration); + } + + return; + } + + final int colorShiftDuration = 300; + long delayAnimDuration = info.getPeriod() * 1000L - totalStateDuration - colorShiftDuration; + ValueAnimator delayAnim = ValueAnimator.ofFloat(0f, 0f); + delayAnim.setDuration((long) (delayAnimDuration / durationScale)); + + int colorFrom = _profileCode.getCurrentTextColor(); + int colorTo = MaterialColors.getColor(_profileCode, com.google.android.material.R.attr.colorError); + ValueAnimator colorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); + colorAnim.setDuration((long) (colorShiftDuration / durationScale)); + colorAnim.addUpdateListener(a -> _profileCode.setTextColor((int) a.getAnimatedValue())); + + final int blinkDuration = 3000; + ValueAnimator delayAnim2 = ValueAnimator.ofFloat(0f, 0f); + delayAnim2.setDuration((long) ((totalStateDuration - blinkDuration) / durationScale)); + + ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(_profileCode, "alpha", 1f, .5f); + alphaAnim.setDuration((long) (500 / durationScale)); + alphaAnim.setRepeatCount(blinkDuration / 500 - 1); + alphaAnim.setRepeatMode(ValueAnimator.REVERSE); + + _expirationAnimSet = new AnimatorSet(); + _expirationAnimSet.playSequentially(delayAnim, colorAnim, delayAnim2, alphaAnim); + _expirationAnimSet.start(); + long currentPlayTime = (info.getPeriod() * 1000L) - info.getMillisTillNextRotation(); + _expirationAnimSet.setCurrentPlayTime((long) (currentPlayTime / durationScale)); + } + + private void stopExpirationAnimation() { + if (_expirationAnimSet != null) { + _expirationAnimSet.cancel(); + _expirationAnimSet = null; + } + + int colorTo = MaterialColors.getColor(_profileCode, R.attr.colorCode); + _profileCode.setTextColor(colorTo); + _profileCode.setAlpha(1f); + } + public void showIcon(boolean show) { if (show) { _profileDrawable.setVisibility(View.VISIBLE); @@ -402,8 +477,11 @@ public class EntryHolder extends RecyclerView.ViewHolder { public void setPaused(boolean paused) { _paused = paused; - if (!_hidden && !_paused) { + if (_paused) { + stopExpirationAnimation(); + } else if (!_hidden) { updateCode(); + startExpirationAnimation(); } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryListView.java b/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryListView.java index b44db60e..af829d6d 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryListView.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryListView.java @@ -38,7 +38,6 @@ import com.beemdevelopment.aegis.helpers.UiRefresher; import com.beemdevelopment.aegis.otp.TotpInfo; import com.beemdevelopment.aegis.ui.glide.GlideHelper; import com.beemdevelopment.aegis.ui.models.ErrorCardInfo; -import com.beemdevelopment.aegis.util.UUIDMap; import com.beemdevelopment.aegis.vault.VaultEntry; import com.beemdevelopment.aegis.vault.VaultGroup; import com.bumptech.glide.Glide; @@ -53,12 +52,10 @@ import com.google.common.base.Strings; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.stream.Collectors; public class EntryListView extends Fragment implements EntryAdapter.Listener { private EntryAdapter _adapter; @@ -71,6 +68,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener { private ViewPreloadSizeProvider _preloadSizeProvider; private TotpProgressBar _progressBar; private boolean _showProgress; + private boolean _showExpirationState; private ViewMode _viewMode; private LinearLayout _emptyStateView; @@ -365,6 +363,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener { _adapter.setShowIcon(showIcon); } + public void setShowExpirationState(boolean showExpirationState) { + _showExpirationState = showExpirationState; + _adapter.setShowExpirationState(showExpirationState); + } + public void setHighlightEntry(boolean highlightEntry) { _adapter.setHighlightEntry(highlightEntry); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 79d77d9f..b2c78288 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -48,6 +48,9 @@ Code digit grouping Select number of digits to group codes by Show the account name + Indicate when codes are about to expire + Change the color of the codes and have them blink when they are about to expire + Change the color of the codes when they are about to expire Only show account name when necessary Only show account names whenever they share the same issuer. Other account names will be hidden. This setting is overridden by the tiles view mode. Account name will be shown below the issuer. diff --git a/app/src/main/res/xml/preferences_appearance.xml b/app/src/main/res/xml/preferences_appearance.xml index c47677e1..e857f4ea 100644 --- a/app/src/main/res/xml/preferences_appearance.xml +++ b/app/src/main/res/xml/preferences_appearance.xml @@ -41,6 +41,13 @@ android:summary="@string/pref_show_icons_summary" app:iconSpaceReserved="false"/> + +