Merge pull request #1494 from michaelschattgen/feature/show-code-expiration

Show when codes are about to expire
This commit is contained in:
Alexander Bakker 2024-09-24 20:56:21 +02:00 committed by GitHub
commit 92de13b176
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 118 additions and 12 deletions

View file

@ -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");

View file

@ -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());

View file

@ -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");

View file

@ -60,6 +60,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
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<RecyclerView.ViewHolder>
_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<RecyclerView.ViewHolder>
}
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));

View file

@ -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();
}
}

View file

@ -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<VaultEntry> _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);
}

View file

@ -48,6 +48,9 @@
<string name="pref_code_group_size_title">Code digit grouping</string>
<string name="pref_code_group_size_summary">Select number of digits to group codes by</string>
<string name="pref_account_name_position_title">Show the account name</string>
<string name="pref_expiration_state_title">Indicate when codes are about to expire</string>
<string name="pref_expiration_state_summary">Change the color of the codes and have them blink when they are about to expire</string>
<string name="pref_expiration_state_fallback">Change the color of the codes when they are about to expire</string>
<string name="pref_shared_issuer_account_name_title">Only show account name when necessary</string>
<string name="pref_shared_issuer_account_name_summary">Only show account names whenever they share the same issuer. Other account names will be hidden.</string>
<string name="pref_account_name_position_summary_override">This setting is overridden by the tiles view mode. Account name will be shown below the issuer.</string>

View file

@ -41,6 +41,13 @@
android:summary="@string/pref_show_icons_summary"
app:iconSpaceReserved="false"/>
<androidx.preference.SwitchPreferenceCompat
android:defaultValue="true"
android:key="pref_expiration_state"
android:title="@string/pref_expiration_state_title"
android:summary="@string/pref_expiration_state_summary"
app:iconSpaceReserved="false"/>
<Preference
android:key="pref_code_group_size_string"
android:title="@string/pref_code_group_size_title"