mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-04-20 05:49:13 +00:00
Merge pull request #1494 from michaelschattgen/feature/show-code-expiration
Show when codes are about to expire
This commit is contained in:
commit
92de13b176
8 changed files with 118 additions and 12 deletions
|
@ -153,6 +153,10 @@ public class Preferences {
|
||||||
return _prefs.getBoolean("pref_show_icons", true);
|
return _prefs.getBoolean("pref_show_icons", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean getShowExpirationState() {
|
||||||
|
return _prefs.getBoolean("pref_expiration_state", true);
|
||||||
|
}
|
||||||
|
|
||||||
public CodeGrouping getCodeGroupSize() {
|
public CodeGrouping getCodeGroupSize() {
|
||||||
String value = _prefs.getString("pref_code_group_size_string", "GROUPING_THREES");
|
String value = _prefs.getString("pref_code_group_size_string", "GROUPING_THREES");
|
||||||
|
|
||||||
|
|
|
@ -201,6 +201,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
_entryListView.setCodeGroupSize(_prefs.getCodeGroupSize());
|
_entryListView.setCodeGroupSize(_prefs.getCodeGroupSize());
|
||||||
_entryListView.setAccountNamePosition(_prefs.getAccountNamePosition());
|
_entryListView.setAccountNamePosition(_prefs.getAccountNamePosition());
|
||||||
_entryListView.setShowIcon(_prefs.isIconVisible());
|
_entryListView.setShowIcon(_prefs.isIconVisible());
|
||||||
|
_entryListView.setShowExpirationState(_prefs.getShowExpirationState());
|
||||||
_entryListView.setOnlyShowNecessaryAccountNames(_prefs.onlyShowNecessaryAccountNames());
|
_entryListView.setOnlyShowNecessaryAccountNames(_prefs.onlyShowNecessaryAccountNames());
|
||||||
_entryListView.setHighlightEntry(_prefs.isEntryHighlightEnabled());
|
_entryListView.setHighlightEntry(_prefs.isEntryHighlightEnabled());
|
||||||
_entryListView.setPauseFocused(_prefs.isPauseFocusedEnabled());
|
_entryListView.setPauseFocused(_prefs.isPauseFocusedEnabled());
|
||||||
|
|
|
@ -125,6 +125,11 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
||||||
return true;
|
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[] codeGroupings = getResources().getStringArray(R.array.pref_code_groupings_values);
|
||||||
String[] codeGroupingNames = getResources().getStringArray(R.array.pref_code_groupings);
|
String[] codeGroupingNames = getResources().getStringArray(R.array.pref_code_groupings);
|
||||||
Preference codeDigitGroupingPreference = requirePreference("pref_code_group_size_string");
|
Preference codeDigitGroupingPreference = requirePreference("pref_code_group_size_string");
|
||||||
|
|
|
@ -60,6 +60,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
private Preferences.CodeGrouping _codeGroupSize;
|
private Preferences.CodeGrouping _codeGroupSize;
|
||||||
private AccountNamePosition _accountNamePosition;
|
private AccountNamePosition _accountNamePosition;
|
||||||
private boolean _showIcon;
|
private boolean _showIcon;
|
||||||
|
private boolean _showExpirationState;
|
||||||
private boolean _onlyShowNecessaryAccountNames;
|
private boolean _onlyShowNecessaryAccountNames;
|
||||||
private boolean _highlightEntry;
|
private boolean _highlightEntry;
|
||||||
private boolean _tempHighlightEntry;
|
private boolean _tempHighlightEntry;
|
||||||
|
@ -115,6 +116,10 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
_showIcon = showIcon;
|
_showIcon = showIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setShowExpirationState(boolean showExpirationState) {
|
||||||
|
_showExpirationState = showExpirationState;
|
||||||
|
}
|
||||||
|
|
||||||
public void setTapToReveal(boolean tapToReveal) {
|
public void setTapToReveal(boolean tapToReveal) {
|
||||||
_tapToReveal = tapToReveal;
|
_tapToReveal = tapToReveal;
|
||||||
}
|
}
|
||||||
|
@ -539,7 +544,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
}
|
}
|
||||||
|
|
||||||
AccountNamePosition accountNamePosition = showAccountName ? _accountNamePosition : AccountNamePosition.HIDDEN;
|
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.setFocused(_selectedEntries.contains(entry));
|
||||||
entryHolder.setShowDragHandle(isEntryDraggable(entry));
|
entryHolder.setShowDragHandle(isEntryDraggable(entry));
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
package com.beemdevelopment.aegis.ui.views;
|
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.Paint;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
|
@ -35,6 +40,7 @@ import com.beemdevelopment.aegis.ui.glide.GlideHelper;
|
||||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
import com.google.android.material.card.MaterialCardView;
|
import com.google.android.material.card.MaterialCardView;
|
||||||
|
import com.google.android.material.color.MaterialColors;
|
||||||
|
|
||||||
public class EntryHolder extends RecyclerView.ViewHolder {
|
public class EntryHolder extends RecyclerView.ViewHolder {
|
||||||
private static final float DEFAULT_ALPHA = 1.0f;
|
private static final float DEFAULT_ALPHA = 1.0f;
|
||||||
|
@ -67,6 +73,8 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
private UiRefresher _refresher;
|
private UiRefresher _refresher;
|
||||||
private Handler _animationHandler;
|
private Handler _animationHandler;
|
||||||
|
private AnimatorSet _expirationAnimSet;
|
||||||
|
private boolean _showExpirationState;
|
||||||
|
|
||||||
private Animation _scaleIn;
|
private Animation _scaleIn;
|
||||||
private Animation _scaleOut;
|
private Animation _scaleOut;
|
||||||
|
@ -97,10 +105,8 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||||
_refresher = new UiRefresher(new UiRefresher.Listener() {
|
_refresher = new UiRefresher(new UiRefresher.Listener() {
|
||||||
@Override
|
@Override
|
||||||
public void onRefresh() {
|
public void onRefresh() {
|
||||||
if (!_hidden && !_paused) {
|
|
||||||
refreshCode();
|
refreshCode();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getMillisTillNextRefresh() {
|
public long getMillisTillNextRefresh() {
|
||||||
|
@ -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;
|
_entry = entry;
|
||||||
_hidden = hidden;
|
_hidden = hidden;
|
||||||
_paused = paused;
|
_paused = paused;
|
||||||
_codeGrouping = groupSize;
|
_codeGrouping = groupSize;
|
||||||
_viewMode = viewMode;
|
_viewMode = viewMode;
|
||||||
|
|
||||||
_accountNamePosition = accountNamePosition;
|
_accountNamePosition = accountNamePosition;
|
||||||
if (viewMode.equals(ViewMode.TILES) && _accountNamePosition == AccountNamePosition.END) {
|
if (viewMode.equals(ViewMode.TILES) && _accountNamePosition == AccountNamePosition.END) {
|
||||||
_accountNamePosition = AccountNamePosition.BELOW;
|
_accountNamePosition = AccountNamePosition.BELOW;
|
||||||
|
@ -124,6 +131,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||||
_selected.setVisibility(View.GONE);
|
_selected.setVisibility(View.GONE);
|
||||||
_selectedHandler.removeCallbacksAndMessages(null);
|
_selectedHandler.removeCallbacksAndMessages(null);
|
||||||
_animationHandler.removeCallbacksAndMessages(null);
|
_animationHandler.removeCallbacksAndMessages(null);
|
||||||
|
_showExpirationState = _entry.getInfo() instanceof TotpInfo && showExpirationState;
|
||||||
|
|
||||||
_favoriteIndicator.setVisibility(_entry.isFavorite() ? View.VISIBLE : View.INVISIBLE);
|
_favoriteIndicator.setVisibility(_entry.isFavorite() ? View.VISIBLE : View.INVISIBLE);
|
||||||
|
|
||||||
|
@ -149,7 +157,6 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
showIcon(showIcon);
|
showIcon(showIcon);
|
||||||
|
|
||||||
itemView.setAlpha(dimmed ? DIMMED_ALPHA : DEFAULT_ALPHA);
|
itemView.setAlpha(dimmed ? DIMMED_ALPHA : DEFAULT_ALPHA);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,6 +285,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||||
public void refreshCode() {
|
public void refreshCode() {
|
||||||
if (!_hidden && !_paused) {
|
if (!_hidden && !_paused) {
|
||||||
updateCode();
|
updateCode();
|
||||||
|
startExpirationAnimation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -336,6 +344,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
public void revealCode() {
|
public void revealCode() {
|
||||||
updateCode();
|
updateCode();
|
||||||
|
startExpirationAnimation();
|
||||||
_hidden = false;
|
_hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -343,7 +352,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||||
String code = getOtp();
|
String code = getOtp();
|
||||||
String hiddenText = code.replaceAll("\\S", Character.toString(HIDDEN_CHAR));
|
String hiddenText = code.replaceAll("\\S", Character.toString(HIDDEN_CHAR));
|
||||||
updateTextViewWithDots(_profileCode, hiddenText, code);
|
updateTextViewWithDots(_profileCode, hiddenText, code);
|
||||||
|
stopExpirationAnimation();
|
||||||
_hidden = true;
|
_hidden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -387,6 +396,72 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||||
textView.setText(dotsString);
|
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) {
|
public void showIcon(boolean show) {
|
||||||
if (show) {
|
if (show) {
|
||||||
_profileDrawable.setVisibility(View.VISIBLE);
|
_profileDrawable.setVisibility(View.VISIBLE);
|
||||||
|
@ -402,8 +477,11 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||||
public void setPaused(boolean paused) {
|
public void setPaused(boolean paused) {
|
||||||
_paused = paused;
|
_paused = paused;
|
||||||
|
|
||||||
if (!_hidden && !_paused) {
|
if (_paused) {
|
||||||
|
stopExpirationAnimation();
|
||||||
|
} else if (!_hidden) {
|
||||||
updateCode();
|
updateCode();
|
||||||
|
startExpirationAnimation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,6 @@ import com.beemdevelopment.aegis.helpers.UiRefresher;
|
||||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||||
import com.beemdevelopment.aegis.ui.glide.GlideHelper;
|
import com.beemdevelopment.aegis.ui.glide.GlideHelper;
|
||||||
import com.beemdevelopment.aegis.ui.models.ErrorCardInfo;
|
import com.beemdevelopment.aegis.ui.models.ErrorCardInfo;
|
||||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
|
||||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
|
@ -53,12 +52,10 @@ import com.google.common.base.Strings;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||||
private EntryAdapter _adapter;
|
private EntryAdapter _adapter;
|
||||||
|
@ -71,6 +68,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||||
private ViewPreloadSizeProvider<VaultEntry> _preloadSizeProvider;
|
private ViewPreloadSizeProvider<VaultEntry> _preloadSizeProvider;
|
||||||
private TotpProgressBar _progressBar;
|
private TotpProgressBar _progressBar;
|
||||||
private boolean _showProgress;
|
private boolean _showProgress;
|
||||||
|
private boolean _showExpirationState;
|
||||||
private ViewMode _viewMode;
|
private ViewMode _viewMode;
|
||||||
private LinearLayout _emptyStateView;
|
private LinearLayout _emptyStateView;
|
||||||
|
|
||||||
|
@ -365,6 +363,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||||
_adapter.setShowIcon(showIcon);
|
_adapter.setShowIcon(showIcon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setShowExpirationState(boolean showExpirationState) {
|
||||||
|
_showExpirationState = showExpirationState;
|
||||||
|
_adapter.setShowExpirationState(showExpirationState);
|
||||||
|
}
|
||||||
|
|
||||||
public void setHighlightEntry(boolean highlightEntry) {
|
public void setHighlightEntry(boolean highlightEntry) {
|
||||||
_adapter.setHighlightEntry(highlightEntry);
|
_adapter.setHighlightEntry(highlightEntry);
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,9 @@
|
||||||
<string name="pref_code_group_size_title">Code digit grouping</string>
|
<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_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_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_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_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>
|
<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>
|
||||||
|
|
|
@ -41,6 +41,13 @@
|
||||||
android:summary="@string/pref_show_icons_summary"
|
android:summary="@string/pref_show_icons_summary"
|
||||||
app:iconSpaceReserved="false"/>
|
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
|
<Preference
|
||||||
android:key="pref_code_group_size_string"
|
android:key="pref_code_group_size_string"
|
||||||
android:title="@string/pref_code_group_size_title"
|
android:title="@string/pref_code_group_size_title"
|
||||||
|
|
Loading…
Add table
Reference in a new issue