From d90303cf0e518ccce84bd8c6827240347aa13a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Sch=C3=A4ttgen?= Date: Wed, 8 Feb 2023 02:51:03 +0100 Subject: [PATCH] Add tiles view mode Minor UI improvements Fix animations Fix typo Improvements made after PR review PR improvements Co-authored-by: Alexander Bakkker --- .../com/beemdevelopment/aegis/ViewMode.java | 31 ++- .../SimpleItemTouchHelperCallback.java | 17 +- .../aegis/ui/MainActivity.java | 2 +- .../AppearancePreferencesFragment.java | 22 ++- .../aegis/ui/views/EntryAdapter.java | 14 +- .../aegis/ui/views/EntryHolder.java | 40 ++-- .../aegis/ui/views/EntryListView.java | 67 ++++++- .../aegis/util/CollectionUtils.java | 15 ++ .../beemdevelopment/aegis/util/UUIDMap.java | 33 ++-- .../aegis/vault/VaultRepository.java | 7 +- app/src/main/res/anim/fade_in.xml | 4 +- app/src/main/res/anim/fade_out.xml | 6 +- app/src/main/res/layout/card_entry_tile.xml | 177 ++++++++++++++++++ app/src/main/res/values/arrays.xml | 1 + app/src/main/res/values/strings.xml | 2 + .../aegis/util/UUIDMapTest.java | 130 ++++++++----- 16 files changed, 461 insertions(+), 107 deletions(-) create mode 100644 app/src/main/java/com/beemdevelopment/aegis/util/CollectionUtils.java create mode 100644 app/src/main/res/layout/card_entry_tile.xml diff --git a/app/src/main/java/com/beemdevelopment/aegis/ViewMode.java b/app/src/main/java/com/beemdevelopment/aegis/ViewMode.java index e1e4e694..9d349850 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ViewMode.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ViewMode.java @@ -5,7 +5,8 @@ import androidx.annotation.LayoutRes; public enum ViewMode { NORMAL, COMPACT, - SMALL; + SMALL, + TILES; private static ViewMode[] _values; @@ -26,6 +27,8 @@ public enum ViewMode { return R.layout.card_entry_compact; case SMALL: return R.layout.card_entry_small; + case TILES: + return R.layout.card_entry_tile; default: return R.layout.card_entry; } @@ -37,8 +40,34 @@ public enum ViewMode { public float getDividerHeight() { if (this == ViewMode.COMPACT) { return 0; + } else if (this == ViewMode.TILES) { + return 4; } return 20; } + + public int getColumnSpan() { + if (this == ViewMode.TILES) { + return 2; + } + + return 1; + } + + public float getDividerWidth() { + if (this == ViewMode.TILES) { + return 4; + } + + return 0; + } + + public String getFormattedAccountName(String accountName) { + if (this == ViewMode.TILES) { + return accountName; + } + + return String.format("(%s)", accountName); + } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/helpers/SimpleItemTouchHelperCallback.java b/app/src/main/java/com/beemdevelopment/aegis/helpers/SimpleItemTouchHelperCallback.java index eb84351f..959f2cf8 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/helpers/SimpleItemTouchHelperCallback.java +++ b/app/src/main/java/com/beemdevelopment/aegis/helpers/SimpleItemTouchHelperCallback.java @@ -16,6 +16,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback { private final EntryAdapter _adapter; private boolean _positionChanged = false; private boolean _isLongPressDragEnabled = true; + private int _dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; public SimpleItemTouchHelperCallback(EntryAdapter adapter) { _adapter = adapter; @@ -46,6 +47,10 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback { return false; } + public void setDragFlags(int dragFlags) { + _dragFlags = dragFlags; + } + @Override public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { // It's not clear when this can happen, but sometimes the ViewHolder @@ -57,16 +62,15 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback { } int swipeFlags = 0; - int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; EntryAdapter adapter = (EntryAdapter) recyclerView.getAdapter(); if (adapter.isPositionFooter(position) || adapter.getEntryAt(position) != _selectedEntry || !isLongPressDragEnabled()) { - dragFlags = 0; + return makeMovementFlags(0, swipeFlags); } - return makeMovementFlags(dragFlags, swipeFlags); + return makeMovementFlags(_dragFlags, swipeFlags); } @Override @@ -75,7 +79,11 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback { if (target.getAdapterPosition() < _adapter.getShownFavoritesCount()){ return false; } - _adapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition()); + + int firstPosition = viewHolder.getLayoutPosition(); + int secondPosition = target.getAdapterPosition(); + + _adapter.onItemMove(firstPosition, secondPosition); _positionChanged = true; return true; } @@ -92,6 +100,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback { if (_positionChanged) { _adapter.onItemDrop(viewHolder.getAdapterPosition()); _positionChanged = false; + _adapter.refresh(false); } } } 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 50ddbac8..a43b1643 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java @@ -952,7 +952,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene @Override public void onEntryMove(VaultEntry entry1, VaultEntry entry2) { - _vaultManager.getVault().swapEntries(entry1, entry2); + _vaultManager.getVault().moveEntry(entry1, entry2); } @Override diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/AppearancePreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/AppearancePreferencesFragment.java index 4b8dc4b8..3c99f55b 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/AppearancePreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/AppearancePreferencesFragment.java @@ -17,6 +17,7 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs; public class AppearancePreferencesFragment extends PreferencesFragment { private Preference _groupsPreference; private Preference _resetUsageCountPreference; + private Preference _currentAccountNamePositionPreference; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { @@ -89,6 +90,7 @@ public class AppearancePreferencesFragment extends PreferencesFragment { _prefs.setCurrentViewMode(ViewMode.fromInteger(i)); viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[i])); getResult().putExtra("needsRefresh", true); + overrideAccountNamePosition(ViewMode.fromInteger(i) == ViewMode.TILES); dialog.dismiss(); }) .setNegativeButton(android.R.string.cancel, null) @@ -110,9 +112,9 @@ public class AppearancePreferencesFragment extends PreferencesFragment { }); int currentAccountNamePosition = _prefs.getAccountNamePosition().ordinal(); - Preference currentAccountNamePositionPreference = requirePreference("pref_account_name_position"); - currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[currentAccountNamePosition])); - currentAccountNamePositionPreference.setOnPreferenceClickListener(preference -> { + _currentAccountNamePositionPreference = requirePreference("pref_account_name_position"); + _currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[currentAccountNamePosition])); + _currentAccountNamePositionPreference.setOnPreferenceClickListener(preference -> { int currentAccountNamePosition1 = _prefs.getAccountNamePosition().ordinal(); Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext()) @@ -120,7 +122,7 @@ public class AppearancePreferencesFragment extends PreferencesFragment { .setSingleChoiceItems(R.array.account_name_position_titles, currentAccountNamePosition1, (dialog, which) -> { int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition(); _prefs.setAccountNamePosition(AccountNamePosition.fromInteger(i)); - currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[i])); + _currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[i])); getResult().putExtra("needsRefresh", true); dialog.dismiss(); }) @@ -135,5 +137,17 @@ public class AppearancePreferencesFragment extends PreferencesFragment { getResult().putExtra("needsRefresh", true); return true; }); + + overrideAccountNamePosition(_prefs.getCurrentViewMode() == ViewMode.TILES); + } + + private void overrideAccountNamePosition(boolean override) { + if (override) { + _currentAccountNamePositionPreference.setEnabled(false); + _currentAccountNamePositionPreference.setSummary(getString(R.string.pref_account_name_position_summary_override)); + } else { + _currentAccountNamePositionPreference.setEnabled(true); + _currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[_prefs.getAccountNamePosition().ordinal()])); + } } } 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 8d284524..3d9abfed 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 @@ -29,6 +29,7 @@ import com.beemdevelopment.aegis.otp.HotpInfo; import com.beemdevelopment.aegis.otp.OtpInfo; import com.beemdevelopment.aegis.otp.OtpInfoException; import com.beemdevelopment.aegis.otp.TotpInfo; +import com.beemdevelopment.aegis.util.CollectionUtils; import com.beemdevelopment.aegis.vault.VaultEntry; import java.util.ArrayList; @@ -382,9 +383,10 @@ public class EntryAdapter extends RecyclerView.Adapter // notify the vault first _view.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition)); - // update our side of things - Collections.swap(_entries, firstPosition, secondPosition); - Collections.swap(_shownEntries, firstPosition, secondPosition); + // then update our end + CollectionUtils.move(_entries, firstPosition, secondPosition); + CollectionUtils.move(_shownEntries, firstPosition, secondPosition); + notifyItemMoved(firstPosition, secondPosition); } @@ -438,7 +440,7 @@ public class EntryAdapter extends RecyclerView.Adapter } AccountNamePosition accountNamePosition = showAccountName ? _accountNamePosition : AccountNamePosition.HIDDEN; - entryHolder.setData(entry, _codeGroupSize, accountNamePosition, _showIcon, showProgress, hidden, paused, dimmed); + entryHolder.setData(entry, _codeGroupSize, _viewMode, accountNamePosition, _showIcon, showProgress, hidden, paused, dimmed); entryHolder.setFocused(_selectedEntries.contains(entry)); entryHolder.setShowDragHandle(isEntryDraggable(entry)); @@ -467,7 +469,7 @@ public class EntryAdapter extends RecyclerView.Adapter case SINGLETAP: if (!handled) { _view.onEntryCopy(entry); - entryHolder.animateCopyText(); + entryHolder.animateCopyText(_viewMode != ViewMode.TILES); _clickedEntry = null; } break; @@ -476,7 +478,7 @@ public class EntryAdapter extends RecyclerView.Adapter if(entry == _clickedEntry) { _view.onEntryCopy(entry); - entryHolder.animateCopyText(); + entryHolder.animateCopyText(_viewMode != ViewMode.TILES); _clickedEntry = null; } else { _clickedEntry = 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 4c5070c3..05e5a4f0 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 @@ -16,6 +16,7 @@ import com.amulyakhare.textdrawable.TextDrawable; import com.beemdevelopment.aegis.AccountNamePosition; import com.beemdevelopment.aegis.Preferences; import com.beemdevelopment.aegis.R; +import com.beemdevelopment.aegis.ViewMode; import com.beemdevelopment.aegis.helpers.IconViewHelper; import com.beemdevelopment.aegis.helpers.TextDrawableHelper; import com.beemdevelopment.aegis.helpers.ThemeHelper; @@ -46,6 +47,7 @@ public class EntryHolder extends RecyclerView.ViewHolder { private ImageView _buttonRefresh; private RelativeLayout _description; private ImageView _dragHandle; + private ViewMode _viewMode; private final ImageView _selected; private final Handler _selectedHandler; @@ -107,11 +109,12 @@ public class EntryHolder extends RecyclerView.ViewHolder { }); } - public void setData(VaultEntry entry, Preferences.CodeGrouping groupSize, 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) { _entry = entry; _hidden = hidden; _paused = paused; _codeGrouping = groupSize; + _viewMode = viewMode; _accountNamePosition = accountNamePosition; _selected.clearAnimation(); @@ -129,12 +132,12 @@ public class EntryHolder extends RecyclerView.ViewHolder { String profileIssuer = entry.getIssuer(); String profileName = entry.getName(); - if (!profileIssuer.isEmpty() && !profileName.isEmpty() && accountNamePosition == AccountNamePosition.END) { - profileName = String.format(" (%s)", profileName); + if (!profileIssuer.isEmpty() && !profileName.isEmpty() && _accountNamePosition == AccountNamePosition.END) { + profileName = _viewMode.getFormattedAccountName(profileName); } _profileIssuer.setText(profileIssuer); _profileName.setText(profileName); - setAccountNameLayout(accountNamePosition); + setAccountNameLayout(_accountNamePosition); if (_hidden) { hideCode(); @@ -148,6 +151,10 @@ public class EntryHolder extends RecyclerView.ViewHolder { } private void setAccountNameLayout(AccountNamePosition accountNamePosition) { + if (_viewMode == ViewMode.TILES) { + return; + } + RelativeLayout.LayoutParams profileNameLayoutParams; RelativeLayout.LayoutParams copiedLayoutParams; switch (accountNamePosition) { @@ -367,7 +374,7 @@ public class EntryHolder extends RecyclerView.ViewHolder { animateAlphaTo(DEFAULT_ALPHA); } - public void animateCopyText() { + public void animateCopyText(boolean includeSlideAnimation) { _animationHandler.removeCallbacksAndMessages(null); Animation slideDownFadeIn = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.slide_down_fade_in); @@ -375,16 +382,25 @@ public class EntryHolder extends RecyclerView.ViewHolder { Animation fadeOut = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.fade_out); Animation fadeIn = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.fade_in); - _profileCopied.startAnimation(slideDownFadeIn); - - View fadeOutView = (_accountNamePosition == AccountNamePosition.BELOW) ? _profileName : _description; + if (includeSlideAnimation) { + _profileCopied.startAnimation(slideDownFadeIn); + View fadeOutView = (_accountNamePosition == AccountNamePosition.BELOW) ? _profileName : _description; fadeOutView.startAnimation(slideDownFadeOut); - _animationHandler.postDelayed(() -> { - _profileCopied.startAnimation(fadeOut); - fadeOutView.startAnimation(fadeIn); - }, 3000); + _animationHandler.postDelayed(() -> { + _profileCopied.startAnimation(fadeOut); + fadeOutView.startAnimation(fadeIn); + }, 3000); + } else { + _profileCopied.startAnimation(fadeIn); + _profileName.startAnimation(fadeOut); + + _animationHandler.postDelayed(() -> { + _profileCopied.startAnimation(fadeOut); + _profileName.startAnimation(fadeIn); + }, 3000); + } } private void animateAlphaTo(float alpha) { 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 feacee14..ac5567ef 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 @@ -19,6 +19,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -64,7 +65,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener { private ItemTouchHelper _touchHelper; private RecyclerView _recyclerView; - private RecyclerView.ItemDecoration _dividerDecoration; + private RecyclerView.ItemDecoration _verticalDividerDecoration; + private RecyclerView.ItemDecoration _horizontalDividerDecoration; private ViewPreloadSizeProvider _preloadSizeProvider; private TotpProgressBar _progressBar; private boolean _showProgress; @@ -122,7 +124,17 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener { RecyclerViewPreloader preloader = new RecyclerViewPreloader<>(Glide.with(this), modelProvider, _preloadSizeProvider, 10); _recyclerView.addOnScrollListener(preloader); - LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext()); + GridLayoutManager layoutManager = new GridLayoutManager(requireContext(), 1); + layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + if (_viewMode == ViewMode.TILES && position == _adapter.getEntriesCount()) { + return 2; + } + + return 1; + } + }); _recyclerView.setLayoutManager(layoutManager); _touchCallback = new SimpleItemTouchHelperCallback(_adapter); _touchHelper = new ItemTouchHelper(_touchCallback); @@ -222,6 +234,13 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener { _viewMode = mode; updateDividerDecoration(); _adapter.setViewMode(_viewMode); + if (_viewMode == ViewMode.TILES) { + _touchCallback.setDragFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT); + } else { + _touchCallback.setDragFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN); + } + + ((GridLayoutManager)_recyclerView.getLayoutManager()).setSpanCount(mode.getColumnSpan()); } public void startDrag(RecyclerView.ViewHolder viewHolder) { @@ -536,18 +555,28 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener { } private void updateDividerDecoration() { - if (_dividerDecoration != null) { - _recyclerView.removeItemDecoration(_dividerDecoration); + if (_verticalDividerDecoration != null) { + _recyclerView.removeItemDecoration(_verticalDividerDecoration); + } + + if(_horizontalDividerDecoration != null) { + _recyclerView.removeItemDecoration(_horizontalDividerDecoration); } float height = _viewMode.getDividerHeight(); + float width = _viewMode.getDividerWidth(); if (_showProgress && height == 0) { - _dividerDecoration = new CompactDividerDecoration(); + _verticalDividerDecoration = new CompactDividerDecoration(); } else { - _dividerDecoration = new VerticalSpaceItemDecoration(height); + _verticalDividerDecoration = new VerticalSpaceItemDecoration(height); } - _recyclerView.addItemDecoration(_dividerDecoration); + if (width != 0) { + _horizontalDividerDecoration = new TileSpaceItemDecoration(width, height); + _recyclerView.addItemDecoration(_horizontalDividerDecoration); + } else { + _recyclerView.addItemDecoration(_verticalDividerDecoration); + } } private void updateEmptyState() { @@ -653,6 +682,30 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener { } } + private class TileSpaceItemDecoration extends RecyclerView.ItemDecoration { + private final int _width; + private final int _height; + + private TileSpaceItemDecoration(float width, float height) { + // convert dp to pixels + _width = MetricsHelper.convertDpToPixels(requireContext(), width); + _height = MetricsHelper.convertDpToPixels(requireContext(), height); + } + + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + int adapterPosition = parent.getChildAdapterPosition(view); + if (adapterPosition == NO_POSITION) { + return; + } + + outRect.left = _width; + outRect.right = _width; + outRect.top = _height; + outRect.bottom = _height; + } + } + private class IconPreloadProvider implements ListPreloader.PreloadModelProvider { @NonNull @Override diff --git a/app/src/main/java/com/beemdevelopment/aegis/util/CollectionUtils.java b/app/src/main/java/com/beemdevelopment/aegis/util/CollectionUtils.java new file mode 100644 index 00000000..ef0cd776 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/util/CollectionUtils.java @@ -0,0 +1,15 @@ +package com.beemdevelopment.aegis.util; + +import java.util.List; + +public class CollectionUtils { + + public static void move(List list, int fromIndex, int toIndex) { + if (fromIndex == toIndex) { + return; + } + + T item = list.remove(fromIndex); + list.add(toIndex, item); + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/util/UUIDMap.java b/app/src/main/java/com/beemdevelopment/aegis/util/UUIDMap.java index c5b29f09..a1aea132 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/util/UUIDMap.java +++ b/app/src/main/java/com/beemdevelopment/aegis/util/UUIDMap.java @@ -64,34 +64,31 @@ public class UUIDMap implements Iterable, Serializa } /** - * Swaps the position of value1 and value2 in the internal map. This operation is - * quite expensive because it has to reallocate the entire underlying LinkedHashMap. - * @throws AssertionError if no map value exists with the UUID of the given entries. - */ - public void swap(T value1, T value2) { - boolean found1 = false; - boolean found2 = false; - List values = new ArrayList<>(); + * Moves value1 to the position of value2. + */ + public void move(T value1, T value2) { + List values = new ArrayList<>(_map.values()); - for (T value : _map.values()) { + int vi1 = -1, vi2 = -1; + for (int i = 0; i < values.size(); i++) { + T value = values.get(i); if (value.getUUID().equals(value1.getUUID())) { - values.add(value2); - found1 = true; - } else if (value.getUUID().equals(value2.getUUID())) { - values.add(value1); - found2 = true; - } else { - values.add(value); + vi1 = i; + } + if (value.getUUID().equals(value2.getUUID())) { + vi2 = i; } } - if (!found1) { + if (vi1 < 0) { throw new AssertionError(String.format("No value found for value1 with UUID: %s", value1.getUUID())); } - if (!found2) { + if (vi2 < 0) { throw new AssertionError(String.format("No value found for value2 with UUID: %s", value2.getUUID())); } + CollectionUtils.move(values, vi1, vi2); + _map.clear(); for (T value : values) { _map.put(value.getUUID(), value); diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java index 6b96fe79..94571e55 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java @@ -239,8 +239,11 @@ public class VaultRepository { return _vault.getEntries().replace(entry); } - public void swapEntries(VaultEntry entry1, VaultEntry entry2) { - _vault.getEntries().swap(entry1, entry2); + /** + * Moves entry1 to the position of entry2. + */ + public void moveEntry(VaultEntry entry1, VaultEntry entry2) { + _vault.getEntries().move(entry1, entry2); } public boolean isEntryDuplicate(VaultEntry entry) { diff --git a/app/src/main/res/anim/fade_in.xml b/app/src/main/res/anim/fade_in.xml index 481e19e0..763c8a54 100644 --- a/app/src/main/res/anim/fade_in.xml +++ b/app/src/main/res/anim/fade_in.xml @@ -1,5 +1,7 @@ - + - + + - \ No newline at end of file diff --git a/app/src/main/res/layout/card_entry_tile.xml b/app/src/main/res/layout/card_entry_tile.xml new file mode 100644 index 00000000..18748c0b --- /dev/null +++ b/app/src/main/res/layout/card_entry_tile.xml @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index e01112f8..81d6abbc 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -32,6 +32,7 @@ @string/normal_viewmode_title @string/compact_mode_title @string/small_mode_title + @string/tiles_mode_title diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 233d1bca..65323954 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -45,6 +45,7 @@ Show the account name 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 always be shown below the issuer. Import from file Import tokens from a file Android cloud backups @@ -329,6 +330,7 @@ Normal Compact Small + Tiles Unknown issuer Unknown account name diff --git a/app/src/test/java/com/beemdevelopment/aegis/util/UUIDMapTest.java b/app/src/test/java/com/beemdevelopment/aegis/util/UUIDMapTest.java index 4498bf91..db5a84bc 100644 --- a/app/src/test/java/com/beemdevelopment/aegis/util/UUIDMapTest.java +++ b/app/src/test/java/com/beemdevelopment/aegis/util/UUIDMapTest.java @@ -1,103 +1,135 @@ package com.beemdevelopment.aegis.util; -import org.junit.Before; -import org.junit.Test; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import org.junit.Test; + public class UUIDMapTest { - private UUIDMap _map; - - @Before - public void init() { - _map = new UUIDMap<>(); - } - @Test public void addValue() { // try adding a new value - Value value = addNewValue(); + UUIDMap map = new UUIDMap<>(); + Value value = addNewValue(map); // try re-adding the value - assertThrows(AssertionError.class, () -> _map.add(value)); + assertThrows(AssertionError.class, () -> map.add(value)); // try adding a clone of the value - assertThrows(AssertionError.class, () -> _map.add(Cloner.clone(value))); + assertThrows(AssertionError.class, () -> map.add(Cloner.clone(value))); } @Test public void removeValue() { // try removing a value - final Value value = addNewValue(); - Value oldValue = _map.remove(value); - assertFalse(_map.has(value)); + UUIDMap map = new UUIDMap<>(); + final Value value = addNewValue(map); + Value oldValue = map.remove(value); + assertFalse(map.has(value)); // ensure we got the original value back assertEquals(value, oldValue); // try removing a non-existent value - assertThrows(AssertionError.class, () -> _map.remove(value)); + assertThrows(AssertionError.class, () -> map.remove(value)); // try removing a value using a clone - Value value2 = addNewValue(); - _map.remove(Cloner.clone(value2)); - assertFalse(_map.has(value2)); + Value value2 = addNewValue(map); + map.remove(Cloner.clone(value2)); + assertFalse(map.has(value2)); } @Test public void replaceValue() { - Value value = addNewValue(); + UUIDMap map = new UUIDMap<>(); + Value value = addNewValue(map); // replace the value with a clone Value valueClone = Cloner.clone(value); - Value oldValue = _map.replace(valueClone); + Value oldValue = map.replace(valueClone); // ensure we got the original value back assertEquals(value, oldValue); // ensure that the clone is now stored in the map - assertSame(_map.getByUUID(value.getUUID()), valueClone); + assertSame(map.getByUUID(value.getUUID()), valueClone); } @Test - public void swapValue() { - Collection values = _map.getValues(); + public void moveValue() { + // move the first value to the last value + UUIDMap map = fillNewMap(4); + Value[] values = map.getValues().toArray(new Value[0]); + map.move(values[0], values[3]); + assertArrayEquals(map.getValues().toArray(new Value[0]), new Value[]{ + values[1], + values[2], + values[3], + values[0] + }); - // set up the map with some values - Value value1 = addNewValue(); - Value value2 = addNewValue(); - Value value3 = addNewValue(); - Value value4 = addNewValue(); + // move the last value to the first value + map = fillNewMap(4); + values = map.getValues().toArray(new Value[0]); + map.move(values[3], values[0]); + assertArrayEquals(map.getValues().toArray(new Value[0]), new Value[]{ + values[3], + values[0], + values[1], + values[2] + }); - // set up a reference list with the reverse order - List ref = new ArrayList<>(values); - Collections.reverse(ref); + // move the second value to the third value + map = fillNewMap(4); + values = map.getValues().toArray(new Value[0]); + map.move(values[1], values[2]); + assertArrayEquals(map.getValues().toArray(new Value[0]), new Value[]{ + values[0], + values[2], + values[1], + values[3] + }); - // the lists should not be equal at this point - assertNotEquals(values, ref); + // move the third value to the second value + map = fillNewMap(4); + values = map.getValues().toArray(new Value[0]); + map.move(values[2], values[1]); + assertArrayEquals(map.getValues().toArray(new Value[0]), new Value[]{ + values[0], + values[2], + values[1], + values[3] + }); - // swap the values and see if the lists are equal now - _map.swap(value1, value4); - _map.swap(value2, value3); - assertArrayEquals(values.toArray(), ref.toArray()); + // move the third value to the first value + map = fillNewMap(4); + values = map.getValues().toArray(new Value[0]); + map.move(values[2], values[0]); + assertArrayEquals(map.getValues().toArray(new Value[0]), new Value[]{ + values[2], + values[0], + values[1], + values[3] + }); } - private Value addNewValue() { + private UUIDMap fillNewMap(int n) { + UUIDMap map = new UUIDMap<>(); + for (int i = 0; i < n; i++) { + addNewValue(map); + } + return map; + } + + private Value addNewValue(UUIDMap map) { Value value = new Value(); - assertFalse(_map.has(value)); - _map.add(value); - assertTrue(_map.has(value)); + assertFalse(map.has(value)); + map.add(value); + assertTrue(map.has(value)); return value; }