diff --git a/app/src/main/java/com/beemdevelopment/aegis/ViewMode.java b/app/src/main/java/com/beemdevelopment/aegis/ViewMode.java index e1e4e6941e..9d3498507b 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 @@ public enum ViewMode { NORMAL, COMPACT, - SMALL; + SMALL, + TILES; private static ViewMode[] _values; @@ -26,6 +27,8 @@ public int getLayoutId() { 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 int getLayoutId() { 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 eb84351f00..959f2cf801 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 boolean isItemViewSwipeEnabled() { 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 int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull Recycle } 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 boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHol 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 void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHol 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 c26bbbbd31..497957ad78 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java @@ -937,8 +937,8 @@ private void startActionMode() { } @Override - public void onEntryMove(VaultEntry entry1, VaultEntry entry2) { - _vaultManager.getVault().swapEntries(entry1, entry2); + public void onEntryMove(VaultEntry entry1, VaultEntry entry2, boolean adjacent) { + _vaultManager.getVault().swapEntries(entry1, entry2, adjacent); } @Override 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 d482f67ab1..b57bdfaa38 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 @@ -26,6 +26,7 @@ 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; @@ -368,11 +369,17 @@ public void onItemMove(int firstPosition, int secondPosition) { } // notify the vault first - _view.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition)); + int difference = firstPosition - secondPosition; + _view.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition), Math.abs(difference) == 1); + + if (Math.abs(difference) > 1) { + CollectionUtils.move(_entries, firstPosition, secondPosition); + CollectionUtils.move(_shownEntries, firstPosition, secondPosition); + } else { + Collections.swap(_entries, firstPosition, secondPosition); + Collections.swap(_shownEntries, firstPosition, secondPosition); + } - // update our side of things - Collections.swap(_entries, firstPosition, secondPosition); - Collections.swap(_shownEntries, firstPosition, secondPosition); notifyItemMoved(firstPosition, secondPosition); } @@ -417,7 +424,7 @@ public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) boolean paused = _pauseFocused && entry == _focusedEntry; boolean dimmed = (_highlightEntry || _tempHighlightEntry) && _focusedEntry != null && _focusedEntry != entry; boolean showProgress = entry.getInfo() instanceof TotpInfo && ((TotpInfo) entry.getInfo()).getPeriod() != getMostFrequentPeriod(); - entryHolder.setData(entry, _codeGroupSize, _showAccountName, _showIcon, showProgress, hidden, paused, dimmed); + entryHolder.setData(entry, _codeGroupSize, _viewMode, _showAccountName, _showIcon, showProgress, hidden, paused, dimmed); entryHolder.setFocused(_selectedEntries.contains(entry)); entryHolder.setShowDragHandle(isEntryDraggable(entry)); @@ -435,7 +442,7 @@ public void onClick(View v) { if (_copyOnTap && !entryHolder.isHidden() && !(entry == _copiedEntry)) { _view.onEntryCopy(entry); - entryHolder.animateCopyText(); + entryHolder.animateCopyText(_viewMode != ViewMode.TILES); _copiedEntry = entry; copiedThisClick = true; handled = true; @@ -737,7 +744,7 @@ public void refresh() { public interface Listener { void onEntryClick(VaultEntry entry); boolean onLongEntryClick(VaultEntry entry); - void onEntryMove(VaultEntry entry1, VaultEntry entry2); + void onEntryMove(VaultEntry entry1, VaultEntry entry2, boolean adjacent); void onEntryDrop(VaultEntry entry); void onEntryChange(VaultEntry entry); void onEntryCopy(VaultEntry 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 03e1910f21..4cba03a585 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 @@ -15,6 +15,7 @@ import com.amulyakhare.textdrawable.TextDrawable; 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; @@ -105,7 +106,7 @@ public long getMillisTillNextRefresh() { }); } - public void setData(VaultEntry entry, Preferences.CodeGrouping groupSize, boolean showAccountName, boolean showIcon, boolean showProgress, boolean hidden, boolean paused, boolean dimmed) { + public void setData(VaultEntry entry, Preferences.CodeGrouping groupSize, ViewMode viewMode, boolean showAccountName, boolean showIcon, boolean showProgress, boolean hidden, boolean paused, boolean dimmed) { _entry = entry; _hidden = hidden; _paused = paused; @@ -127,7 +128,7 @@ public void setData(VaultEntry entry, Preferences.CodeGrouping groupSize, boolea String profileIssuer = entry.getIssuer(); String profileName = showAccountName ? entry.getName() : ""; if (!profileIssuer.isEmpty() && !profileName.isEmpty()) { - profileName = String.format(" (%s)", profileName); + profileName = viewMode.getFormattedAccountName(profileName); } _profileIssuer.setText(profileIssuer); _profileName.setText(profileName); @@ -328,7 +329,7 @@ public void highlight() { 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); @@ -336,13 +337,23 @@ public void animateCopyText() { Animation fadeOut = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.fade_out); Animation fadeIn = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.fade_in); - _profileCopied.startAnimation(slideDownFadeIn); - _description.startAnimation(slideDownFadeOut); + if (includeSlideAnimation) { + _profileCopied.startAnimation(slideDownFadeIn); + _description.startAnimation(slideDownFadeOut); - _animationHandler.postDelayed(() -> { - _profileCopied.startAnimation(fadeOut); - _description.startAnimation(fadeIn); - }, 3000); + _animationHandler.postDelayed(() -> { + _profileCopied.startAnimation(fadeOut); + _description.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 38aa7ded6b..7abfca9a98 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.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; @@ -61,7 +62,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; @@ -119,7 +121,17 @@ public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 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); @@ -217,6 +229,13 @@ public void setViewMode(ViewMode mode) { _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) { @@ -250,9 +269,9 @@ public boolean onLongEntryClick(VaultEntry entry) { } @Override - public void onEntryMove(VaultEntry entry1, VaultEntry entry2) { + public void onEntryMove(VaultEntry entry1, VaultEntry entry2, boolean adjacent) { if (_listener != null) { - _listener.onEntryMove(entry1, entry2); + _listener.onEntryMove(entry1, entry2, adjacent); } } @@ -527,18 +546,28 @@ private List cleanGroupFilter(List groupFilter) { } 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() { @@ -553,7 +582,7 @@ private void updateEmptyState() { public interface Listener { void onEntryClick(VaultEntry entry); - void onEntryMove(VaultEntry entry1, VaultEntry entry2); + void onEntryMove(VaultEntry entry1, VaultEntry entry2, boolean adjacent); void onEntryDrop(VaultEntry entry); void onEntryChange(VaultEntry entry); void onEntryCopy(VaultEntry entry); @@ -642,6 +671,30 @@ public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull R } } + 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 0000000000..ef0cd776aa --- /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 c5b29f09a2..fb186328f8 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/util/UUIDMap.java +++ b/app/src/main/java/com/beemdevelopment/aegis/util/UUIDMap.java @@ -63,6 +63,25 @@ public T replace(T newValue) { return oldValue; } + public void move(T value1, T value2) { + List values = new ArrayList<>(); + + for (T value : _map.values()) { + if (!value.getUUID().equals(value1.getUUID())) { + values.add(value); + + if (value.getUUID().equals(value2.getUUID())) { + values.add(value1); + } + } + } + + _map.clear(); + for (T value : values) { + _map.put(value.getUUID(), value); + } + } + /** * 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. 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 6b96fe7908..7a1ce1f1aa 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,12 @@ public VaultEntry replaceEntry(VaultEntry entry) { return _vault.getEntries().replace(entry); } - public void swapEntries(VaultEntry entry1, VaultEntry entry2) { - _vault.getEntries().swap(entry1, entry2); + public void swapEntries(VaultEntry entry1, VaultEntry entry2, boolean adjacent) { + if (adjacent) { + _vault.getEntries().swap(entry1, entry2); + } else { + _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 481e19e056..763c8a54bf 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.xml b/app/src/main/res/layout/card_entry.xml index 6ffff7c437..dfe4825169 100644 --- a/app/src/main/res/layout/card_entry.xml +++ b/app/src/main/res/layout/card_entry.xml @@ -98,7 +98,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toEndOf="@id/profile_issuer" - android:layout_marginStart="2dp" + android:layout_marginStart="4dp" android:ellipsize="end" android:includeFontPadding="false" android:maxLines="1" diff --git a/app/src/main/res/layout/card_entry_compact.xml b/app/src/main/res/layout/card_entry_compact.xml index e52ec9e8fc..1ae171e835 100644 --- a/app/src/main/res/layout/card_entry_compact.xml +++ b/app/src/main/res/layout/card_entry_compact.xml @@ -99,7 +99,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toEndOf="@id/profile_issuer" - android:layout_marginStart="2dp" + android:layout_marginStart="4dp" android:ellipsize="end" android:includeFontPadding="false" android:maxLines="1" diff --git a/app/src/main/res/layout/card_entry_small.xml b/app/src/main/res/layout/card_entry_small.xml index a0fb0a911f..8e528dcd44 100644 --- a/app/src/main/res/layout/card_entry_small.xml +++ b/app/src/main/res/layout/card_entry_small.xml @@ -98,7 +98,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toEndOf="@id/profile_issuer" - android:layout_marginStart="2dp" + android:layout_marginStart="4dp" android:ellipsize="end" android:includeFontPadding="false" android:maxLines="1" 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 0000000000..18748c0b45 --- /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 11f4f1621c..465e6c3939 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 31b99132e8..09af465779 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -321,6 +321,7 @@ Normal Compact Small + Tiles Unknown issuer Unknown account name