diff --git a/app/src/main/java/com/beemdevelopment/aegis/Preferences.java b/app/src/main/java/com/beemdevelopment/aegis/Preferences.java index c30776392..cbb86c8f0 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 boolean isIconVisible() { 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 1418c72e9..1490c6e88 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 @@ protected void onCreate(Bundle savedInstanceState) { _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/views/EntryAdapter.java b/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryAdapter.java index d3c0cbe10..2270807c3 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 void setShowIcon(boolean showIcon) { _showIcon = showIcon; } + public void setShowExpirationState(boolean showExpirationState) { + _showExpirationState = showExpirationState; + } + public void setTapToReveal(boolean tapToReveal) { _tapToReveal = tapToReveal; } @@ -348,6 +353,15 @@ public void refresh(boolean hard) { for (EntryHolder holder : _holders) { holder.refresh(); holder.showIcon(_showIcon); + holder.setShowExpirationState(_showExpirationState); + } + } + } + + public void startExpiringAnimation() { + for (EntryHolder holder : _holders) { + if(holder.getEntry().getInfo() instanceof TotpInfo) { + holder.updateExpirationState(); } } } @@ -539,7 +553,7 @@ public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) } 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 ebfc9c290..6ad211ecc 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,5 +1,7 @@ package com.beemdevelopment.aegis.ui.views; +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; import android.graphics.Paint; import android.graphics.Rect; import android.os.Handler; @@ -35,6 +37,7 @@ 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; @@ -66,6 +69,7 @@ public class EntryHolder extends RecyclerView.ViewHolder { private MaterialCardView _view; private UiRefresher _refresher; + private UiRefresher _expiringRefresher; private Handler _animationHandler; private Animation _scaleIn; @@ -107,14 +111,72 @@ public long getMillisTillNextRefresh() { return ((TotpInfo) _entry.getInfo()).getMillisTillNextRotation(); } }); + + _expiringRefresher = new UiRefresher(new UiRefresher.Listener() { + @Override + public void onRefresh() { + updateExpirationState(); + } + + public long getMillisTillNextRefresh() { + long millisTillNextRotation = ((TotpInfo) _entry.getInfo()).getMillisTillNextRotation(); + if (millisTillNextRotation > 7000) { + stopExpirationAnimation(); + return millisTillNextRotation - 7000; + } else { + return 1000; + } + } + }); + } + + public void updateExpirationState() { + long millisTillNextRotation = ((TotpInfo) _entry.getInfo()).getMillisTillNextRotation(); + if (!_hidden && !_paused) { + if (millisTillNextRotation <= 7000) { + setExpirationColor(); + } + + if (millisTillNextRotation <= 3000) { + startExpirationAnimation(); + } + } + } + + private void setExpirationColor() { + int colorFrom = _profileCode.getCurrentTextColor(); + int colorTo = MaterialColors.getColor(_profileCode, com.google.android.material.R.attr.colorError); + + ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); + colorAnimation.setDuration(300); + colorAnimation.addUpdateListener(animator -> _profileCode.setTextColor((int) animator.getAnimatedValue())); + colorAnimation.start(); } - public void setData(VaultEntry entry, Preferences.CodeGrouping groupSize, ViewMode viewMode, AccountNamePosition accountNamePosition, boolean showIcon, boolean showProgress, boolean hidden, boolean paused, boolean dimmed) { + + private void startExpirationAnimation() { + _profileCode.setAlpha(1f); + + _profileCode.animate().alpha(0.5f).setDuration(500).withEndAction(() -> { + long animationDuration = Math.min(((TotpInfo) _entry.getInfo()).getMillisTillNextRotation(), 500); + _profileCode.animate().alpha(1f).setDuration(animationDuration).start(); + }).start(); + } + + private void stopExpirationAnimation() { + int colorTo = MaterialColors.getColor(_profileCode, R.attr.colorCode); + _profileCode.setTextColor(colorTo); + _profileCode.clearAnimation(); + _profileCode.setAlpha(1f); + } + + 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; @@ -129,6 +191,7 @@ public void setData(VaultEntry entry, Preferences.CodeGrouping groupSize, ViewMo // only show the progress bar if there is no uniform period and the entry type is TotpInfo setShowProgress(showProgress); + setShowExpirationState(showExpirationState); // only show the button if this entry is of type HotpInfo _buttonRefresh.setVisibility(entry.getInfo() instanceof HotpInfo ? View.VISIBLE : View.GONE); @@ -258,6 +321,7 @@ public void setFocusedAndAnimate(boolean focused) { public void destroy() { _refresher.destroy(); + _expiringRefresher.destroy(); } public void startRefreshLoop() { @@ -270,6 +334,14 @@ public void stopRefreshLoop() { _progressBar.stop(); } + public void startExpirationStateLoop() { + _expiringRefresher.start(); + } + + public void stopExpirationStateLoop() { + _expiringRefresher.stop(); + } + public void refresh() { _progressBar.restart(); refreshCode(); @@ -278,6 +350,8 @@ public void refresh() { public void refreshCode() { if (!_hidden && !_paused) { updateCode(); + + stopExpirationAnimation(); } } @@ -395,6 +469,18 @@ public void showIcon(boolean show) { } } + public void setShowExpirationState(boolean showExpirationState) { + if (_entry.getInfo() instanceof HotpInfo) { + showExpirationState = false; + } + + if (showExpirationState) { + startExpirationStateLoop(); + } else { + stopExpirationStateLoop(); + } + } + public boolean isHidden() { return _hidden; } 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 b44db60ec..417b2bdc5 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 @@ -71,10 +71,12 @@ 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; private UiRefresher _refresher; + private UiRefresher _expiringRefresher; @Override public void onCreate(Bundle savedInstanceState) { @@ -150,6 +152,23 @@ public long getMillisTillNextRefresh() { } }); + _expiringRefresher = new UiRefresher(new UiRefresher.Listener() { + @Override + public void onRefresh() { + _adapter.startExpiringAnimation(); + } + + @Override + public long getMillisTillNextRefresh() { + long millisTillNextRotation = TotpInfo.getMillisTillNextRotation(_adapter.getMostFrequentPeriod()); + if (millisTillNextRotation > 7000) { + return millisTillNextRotation - 7000; + } else { + return 1000; + } + } + }); + _emptyStateView = view.findViewById(R.id.vEmptyList); return view; } @@ -161,6 +180,7 @@ public void setPreloadView(View view) { @Override public void onDestroyView() { _refresher.destroy(); + _expiringRefresher.destroy(); super.onDestroyView(); } @@ -335,10 +355,18 @@ public void onPeriodUniformityChanged(boolean isUniform, int period) { _progressBar.setPeriod(period); _progressBar.start(); _refresher.start(); + + if(_showExpirationState) { + _expiringRefresher.start(); + } } else { _progressBar.setVisibility(View.GONE); _progressBar.stop(); _refresher.stop(); + + if(_showExpirationState) { + _expiringRefresher.stop(); + } } } @@ -365,6 +393,11 @@ public void setShowIcon(boolean showIcon) { _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 da3ebea7e..61a18ff6e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -48,6 +48,8 @@ Code digit grouping Select number of digits to group codes by Show the account name + Show when codes are about to expire + Change the color of the code and have the codes blink 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 c47677e10..e857f4eac 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"/> + +