Merge pull request #1599 from michaelschattgen/feature/haptic-feedback

Add haptic feedback toggle for code refresh
This commit is contained in:
Alexander Bakker 2025-05-29 12:41:57 +02:00 committed by GitHub
commit 6f270144e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 144 additions and 9 deletions

View file

@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.VIBRATE" />
<!-- NOTE: Disabled for now. See issue: #1047 <!-- NOTE: Disabled for now. See issue: #1047
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

View file

@ -94,6 +94,10 @@ public class Preferences {
return _prefs.getBoolean("pref_highlight_entry", false); return _prefs.getBoolean("pref_highlight_entry", false);
} }
public boolean isHapticFeedbackEnabled() {
return _prefs.getBoolean("pref_haptic_feedback", true);
}
public boolean isPauseFocusedEnabled() { public boolean isPauseFocusedEnabled() {
boolean dependenciesEnabled = isTapToRevealEnabled() || isEntryHighlightEnabled(); boolean dependenciesEnabled = isTapToRevealEnabled() || isEntryHighlightEnabled();
if (!dependenciesEnabled) return false; if (!dependenciesEnabled) return false;

View file

@ -0,0 +1,12 @@
package com.beemdevelopment.aegis;
import java.util.Arrays;
public class VibrationPatterns {
public static final long[] EXPIRING = {475, 20, 5, 20, 965, 20, 5, 20, 965, 20, 5, 20, 420};
public static final long[] REFRESH_CODE = {0, 100};
public static long getLengthInMillis(long[] pattern) {
return Arrays.stream(pattern).sum();
}
}

View file

@ -2,6 +2,8 @@ package com.beemdevelopment.aegis.helpers;
import android.os.Handler; import android.os.Handler;
import com.beemdevelopment.aegis.VibrationPatterns;
public class UiRefresher { public class UiRefresher {
private boolean _running; private boolean _running;
private Listener _listener; private Listener _listener;
@ -23,7 +25,6 @@ public class UiRefresher {
} }
_running = true; _running = true;
_listener.onRefresh();
_handler.postDelayed(new Runnable() { _handler.postDelayed(new Runnable() {
@Override @Override
public void run() { public void run() {
@ -31,6 +32,27 @@ public class UiRefresher {
_handler.postDelayed(this, _listener.getMillisTillNextRefresh()); _handler.postDelayed(this, _listener.getMillisTillNextRefresh());
} }
}, _listener.getMillisTillNextRefresh()); }, _listener.getMillisTillNextRefresh());
_handler.postDelayed(new Runnable() {
@Override
public void run() {
_listener.onExpiring();
_handler.postDelayed(this, getNextRun());
}
}, getInitialRun());
}
private long getInitialRun() {
long sum = _listener.getMillisTillNextRefresh() - VibrationPatterns.getLengthInMillis(VibrationPatterns.EXPIRING);
if (sum < 0) {
return getNextRun();
}
return sum;
}
private long getNextRun() {
return (_listener.getMillisTillNextRefresh() + _listener.getPeriodMillis()) - VibrationPatterns.getLengthInMillis(VibrationPatterns.EXPIRING);
} }
public void stop() { public void stop() {
@ -40,6 +62,8 @@ public class UiRefresher {
public interface Listener { public interface Listener {
void onRefresh(); void onRefresh();
void onExpiring();
long getMillisTillNextRefresh(); long getMillisTillNextRefresh();
long getPeriodMillis();
} }
} }

View file

@ -0,0 +1,44 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.os.Build;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.os.VibratorManager;
import com.beemdevelopment.aegis.Preferences;
public class VibrationHelper {
private Preferences _preferences;
public VibrationHelper(Context context) {
_preferences = new Preferences(context);
}
public void vibratePattern(Context context, long[] pattern) {
if (!isHapticFeedbackEnabled()) {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
VibratorManager vibratorManager = (VibratorManager) context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE);
if (vibratorManager != null) {
Vibrator vibrator = vibratorManager.getDefaultVibrator();
VibrationEffect effect = VibrationEffect.createWaveform(pattern, -1);
vibrator.vibrate(effect);
}
} else {
Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
VibrationEffect effect = VibrationEffect.createWaveform(pattern, -1);
vibrator.vibrate(effect);
}
}
}
}
public boolean isHapticFeedbackEnabled() {
return _preferences.isHapticFeedbackEnabled();
}
}

View file

@ -890,6 +890,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
} }
} }
@Override
protected void onStop() {
super.onStop();
_entryListView.onRefreshStop();
}
@Override @Override
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
@ -941,14 +948,18 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
// refresh all codes to prevent showing old ones // refresh all codes to prevent showing old ones
_entryListView.refresh(false); _entryListView.refresh(false);
_entryListView.onRefreshStart();
} else { } else {
loadEntries(); loadEntries();
checkTimeSyncSetting(); checkTimeSyncSetting();
checkIconOptimization(); checkIconOptimization();
_entryListView.onRefreshStart();
} }
_lockBackPressHandler.setEnabled( _lockBackPressHandler.setEnabled(
_vaultManager.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_BACK_BUTTON) _vaultManager.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_BACK_BUTTON)
); );
handleIncomingIntent(); handleIncomingIntent();

View file

@ -67,7 +67,6 @@ public class EntryHolder extends RecyclerView.ViewHolder {
private boolean _hidden; private boolean _hidden;
private boolean _paused; private boolean _paused;
private TotpProgressBar _progressBar; private TotpProgressBar _progressBar;
private MaterialCardView _view; private MaterialCardView _view;
@ -111,14 +110,22 @@ public class EntryHolder extends RecyclerView.ViewHolder {
refreshCode(); refreshCode();
} }
@Override
public void onExpiring() { }
@Override @Override
public long getMillisTillNextRefresh() { public long getMillisTillNextRefresh() {
return ((TotpInfo) _entry.getInfo()).getMillisTillNextRotation(); return ((TotpInfo) _entry.getInfo()).getMillisTillNextRotation();
} }
@Override
public long getPeriodMillis() {
return ((TotpInfo) _entry.getInfo()).getPeriod() * 1000L;
}
}); });
} }
public void setData(VaultEntry entry, Preferences.CodeGrouping groupSize, ViewMode viewMode, AccountNamePosition accountNamePosition, boolean showIcon, boolean showProgress, boolean hidden, boolean paused, boolean dimmed, boolean showExpirationState, boolean showNextCode) { public void setData(VaultEntry entry, Preferences.CodeGrouping groupSize, ViewMode viewMode, AccountNamePosition accountNamePosition, boolean showIcon, boolean nonUniform, boolean hidden, boolean paused, boolean dimmed, boolean showExpirationState, boolean showNextCode) {
_entry = entry; _entry = entry;
_hidden = hidden; _hidden = hidden;
_paused = paused; _paused = paused;
@ -140,7 +147,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_favoriteIndicator.setVisibility(_entry.isFavorite() ? View.VISIBLE : View.INVISIBLE); _favoriteIndicator.setVisibility(_entry.isFavorite() ? View.VISIBLE : View.INVISIBLE);
// only show the progress bar if there is no uniform period and the entry type is TotpInfo // only show the progress bar if there is no uniform period and the entry type is TotpInfo
setShowProgress(showProgress); setShowProgress(nonUniform);
// only show the button if this entry is of type HotpInfo // only show the button if this entry is of type HotpInfo
_buttonRefresh.setVisibility(entry.getInfo() instanceof HotpInfo ? View.VISIBLE : View.GONE); _buttonRefresh.setVisibility(entry.getInfo() instanceof HotpInfo ? View.VISIBLE : View.GONE);

View file

@ -33,11 +33,13 @@ import com.beemdevelopment.aegis.CopyBehavior;
import com.beemdevelopment.aegis.Preferences; import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory; import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.VibrationPatterns;
import com.beemdevelopment.aegis.ViewMode; import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.helpers.AnimationsHelper; import com.beemdevelopment.aegis.helpers.AnimationsHelper;
import com.beemdevelopment.aegis.helpers.MetricsHelper; import com.beemdevelopment.aegis.helpers.MetricsHelper;
import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback; import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
import com.beemdevelopment.aegis.helpers.UiRefresher; import com.beemdevelopment.aegis.helpers.UiRefresher;
import com.beemdevelopment.aegis.helpers.VibrationHelper;
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;
@ -66,13 +68,13 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
private Listener _listener; private Listener _listener;
private SimpleItemTouchHelperCallback _touchCallback; private SimpleItemTouchHelperCallback _touchCallback;
private ItemTouchHelper _touchHelper; private ItemTouchHelper _touchHelper;
private VibrationHelper _vibrationHelper;
private RecyclerView _recyclerView; private RecyclerView _recyclerView;
private RecyclerView.ItemDecoration _itemDecoration; private RecyclerView.ItemDecoration _itemDecoration;
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;
@ -95,6 +97,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_entry_list_view, container, false); View view = inflater.inflate(R.layout.fragment_entry_list_view, container, false);
_progressBar = view.findViewById(R.id.progressBar); _progressBar = view.findViewById(R.id.progressBar);
_vibrationHelper = new VibrationHelper(getContext());
// set up the recycler view // set up the recycler view
_recyclerView = view.findViewById(R.id.rvKeyProfiles); _recyclerView = view.findViewById(R.id.rvKeyProfiles);
@ -144,12 +147,23 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
@Override @Override
public void onRefresh() { public void onRefresh() {
refresh(false); refresh(false);
_vibrationHelper.vibratePattern(getContext(), VibrationPatterns.REFRESH_CODE);
}
@Override
public void onExpiring() {
_vibrationHelper.vibratePattern(getContext(), VibrationPatterns.EXPIRING);
} }
@Override @Override
public long getMillisTillNextRefresh() { public long getMillisTillNextRefresh() {
return TotpInfo.getMillisTillNextRotation(_adapter.getMostFrequentPeriod()); return TotpInfo.getMillisTillNextRotation(_adapter.getMostFrequentPeriod());
} }
@Override
public long getPeriodMillis() {
return _adapter.getMostFrequentPeriod() * 1000L;
}
}); });
final int rvInitialPaddingLeft = _recyclerView.getPaddingLeft(); final int rvInitialPaddingLeft = _recyclerView.getPaddingLeft();
@ -191,6 +205,16 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
super.onDestroyView(); super.onDestroyView();
} }
public void onRefreshStop() {
_refresher.stop();
}
public void onRefreshStart() {
if (_adapter.getMostFrequentPeriod() != -1){
_refresher.start();
}
}
public void setGroups(Collection<VaultGroup> groups) { public void setGroups(Collection<VaultGroup> groups) {
_adapter.setGroups(groups); _adapter.setGroups(groups);
updateDividerDecoration(); updateDividerDecoration();
@ -355,11 +379,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_progressBar.setVisibility(View.VISIBLE); _progressBar.setVisibility(View.VISIBLE);
_progressBar.setPeriod(period); _progressBar.setPeriod(period);
_progressBar.start(); _progressBar.start();
_refresher.start(); onRefreshStart();
} else { } else {
_progressBar.setVisibility(View.GONE); _progressBar.setVisibility(View.GONE);
_progressBar.stop(); _progressBar.stop();
_refresher.stop(); onRefreshStop();
} }
} }
@ -391,7 +415,6 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
} }
public void setShowExpirationState(boolean showExpirationState) { public void setShowExpirationState(boolean showExpirationState) {
_showExpirationState = showExpirationState;
_adapter.setShowExpirationState(showExpirationState); _adapter.setShowExpirationState(showExpirationState);
} }

View file

@ -373,6 +373,8 @@
<string name="note" comment="Users can add a note to an entry">Note</string> <string name="note" comment="Users can add a note to an entry">Note</string>
<string name="clear">Clear</string> <string name="clear">Clear</string>
<string name="pref_haptic_feedback_summary">Make your device vibrate when codes are refreshing</string>
<string name="pref_haptic_feedback_title">Haptic feedback</string>
<string name="pref_highlight_entry_title">Highlight tokens when tapped</string> <string name="pref_highlight_entry_title">Highlight tokens when tapped</string>
<string name="pref_highlight_entry_summary">Make tokens easier to distinguish from each other by temporarily highlighting them when tapped</string> <string name="pref_highlight_entry_summary">Make tokens easier to distinguish from each other by temporarily highlighting them when tapped</string>
<string name="pref_groups_multiselect_title">Multiselect groups</string> <string name="pref_groups_multiselect_title">Multiselect groups</string>

View file

@ -26,6 +26,13 @@
android:title="@string/pref_copy_behavior_title" android:title="@string/pref_copy_behavior_title"
app:iconSpaceReserved="false"/> app:iconSpaceReserved="false"/>
<androidx.preference.SwitchPreferenceCompat
android:defaultValue="true"
android:key="pref_haptic_feedback"
android:title="@string/pref_haptic_feedback_title"
android:summary="@string/pref_haptic_feedback_summary"
app:iconSpaceReserved="false"/>
<androidx.preference.SwitchPreferenceCompat <androidx.preference.SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"
android:key="pref_groups_multiselect" android:key="pref_groups_multiselect"