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.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.VIBRATE" />
<!-- NOTE: Disabled for now. See issue: #1047
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

View file

@ -94,6 +94,10 @@ public class Preferences {
return _prefs.getBoolean("pref_highlight_entry", false);
}
public boolean isHapticFeedbackEnabled() {
return _prefs.getBoolean("pref_haptic_feedback", true);
}
public boolean isPauseFocusedEnabled() {
boolean dependenciesEnabled = isTapToRevealEnabled() || isEntryHighlightEnabled();
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 com.beemdevelopment.aegis.VibrationPatterns;
public class UiRefresher {
private boolean _running;
private Listener _listener;
@ -23,7 +25,6 @@ public class UiRefresher {
}
_running = true;
_listener.onRefresh();
_handler.postDelayed(new Runnable() {
@Override
public void run() {
@ -31,6 +32,27 @@ public class UiRefresher {
_handler.postDelayed(this, _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() {
@ -40,6 +62,8 @@ public class UiRefresher {
public interface Listener {
void onRefresh();
void onExpiring();
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
protected void onStart() {
super.onStart();
@ -941,14 +948,18 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
// refresh all codes to prevent showing old ones
_entryListView.refresh(false);
_entryListView.onRefreshStart();
} else {
loadEntries();
checkTimeSyncSetting();
checkIconOptimization();
_entryListView.onRefreshStart();
}
_lockBackPressHandler.setEnabled(
_vaultManager.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_BACK_BUTTON)
_vaultManager.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_BACK_BUTTON)
);
handleIncomingIntent();

View file

@ -67,7 +67,6 @@ public class EntryHolder extends RecyclerView.ViewHolder {
private boolean _hidden;
private boolean _paused;
private TotpProgressBar _progressBar;
private MaterialCardView _view;
@ -111,14 +110,22 @@ public class EntryHolder extends RecyclerView.ViewHolder {
refreshCode();
}
@Override
public void onExpiring() { }
@Override
public long getMillisTillNextRefresh() {
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;
_hidden = hidden;
_paused = paused;
@ -140,7 +147,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_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
setShowProgress(showProgress);
setShowProgress(nonUniform);
// only show the button if this entry is of type HotpInfo
_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.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.VibrationPatterns;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
import com.beemdevelopment.aegis.helpers.MetricsHelper;
import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
import com.beemdevelopment.aegis.helpers.UiRefresher;
import com.beemdevelopment.aegis.helpers.VibrationHelper;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.glide.GlideHelper;
import com.beemdevelopment.aegis.ui.models.ErrorCardInfo;
@ -66,13 +68,13 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
private Listener _listener;
private SimpleItemTouchHelperCallback _touchCallback;
private ItemTouchHelper _touchHelper;
private VibrationHelper _vibrationHelper;
private RecyclerView _recyclerView;
private RecyclerView.ItemDecoration _itemDecoration;
private ViewPreloadSizeProvider<VaultEntry> _preloadSizeProvider;
private TotpProgressBar _progressBar;
private boolean _showProgress;
private boolean _showExpirationState;
private ViewMode _viewMode;
private LinearLayout _emptyStateView;
@ -95,6 +97,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_entry_list_view, container, false);
_progressBar = view.findViewById(R.id.progressBar);
_vibrationHelper = new VibrationHelper(getContext());
// set up the recycler view
_recyclerView = view.findViewById(R.id.rvKeyProfiles);
@ -144,12 +147,23 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
@Override
public void onRefresh() {
refresh(false);
_vibrationHelper.vibratePattern(getContext(), VibrationPatterns.REFRESH_CODE);
}
@Override
public void onExpiring() {
_vibrationHelper.vibratePattern(getContext(), VibrationPatterns.EXPIRING);
}
@Override
public long getMillisTillNextRefresh() {
return TotpInfo.getMillisTillNextRotation(_adapter.getMostFrequentPeriod());
}
@Override
public long getPeriodMillis() {
return _adapter.getMostFrequentPeriod() * 1000L;
}
});
final int rvInitialPaddingLeft = _recyclerView.getPaddingLeft();
@ -191,6 +205,16 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
super.onDestroyView();
}
public void onRefreshStop() {
_refresher.stop();
}
public void onRefreshStart() {
if (_adapter.getMostFrequentPeriod() != -1){
_refresher.start();
}
}
public void setGroups(Collection<VaultGroup> groups) {
_adapter.setGroups(groups);
updateDividerDecoration();
@ -355,11 +379,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_progressBar.setVisibility(View.VISIBLE);
_progressBar.setPeriod(period);
_progressBar.start();
_refresher.start();
onRefreshStart();
} else {
_progressBar.setVisibility(View.GONE);
_progressBar.stop();
_refresher.stop();
onRefreshStop();
}
}
@ -391,7 +415,6 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
public void setShowExpirationState(boolean showExpirationState) {
_showExpirationState = 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="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_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>

View file

@ -26,6 +26,13 @@
android:title="@string/pref_copy_behavior_title"
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
android:defaultValue="false"
android:key="pref_groups_multiselect"