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 3192864f67..96c672bde1 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/helpers/SimpleItemTouchHelperCallback.java +++ b/app/src/main/java/com/beemdevelopment/aegis/helpers/SimpleItemTouchHelperCallback.java @@ -69,7 +69,7 @@ public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull Recycle int swipeFlags = 0; if (adapter.isPositionFooter(position) || adapter.isPositionErrorCard(position) - || adapter.getEntryAtPos(position) != _selectedEntry + || adapter.getEntryAtPosition(position) != _selectedEntry || !isLongPressDragEnabled()) { return makeMovementFlags(0, swipeFlags); } 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 d9c03a59ba..579db8f74d 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java @@ -149,7 +149,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) { return; } - onAssignIconsResult(activityResult.getData()); + onAssignIconsResult(); }); private final ActivityResultLauncher preferenceResultLauncher = @@ -160,7 +160,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) { return; } - onEditEntryResult(activityResult.getData()); + onEditEntryResult(); }); private final ActivityResultLauncher addEntryResultLauncher = @@ -255,13 +255,13 @@ public void setGroups(Collection groups) { _prefGroupFilter = null; if (!groupFilter.isEmpty()) { _groupFilter = groupFilter; - _entryListView.setGroupFilter(groupFilter, false); + _entryListView.setGroupFilter(groupFilter); } } else if (_groupFilter != null) { Set groupFilter = cleanGroupFilter(_groupFilter); if (!_groupFilter.equals(groupFilter)) { _groupFilter = groupFilter; - _entryListView.setGroupFilter(groupFilter, true); + _entryListView.setGroupFilter(groupFilter); } } @@ -316,7 +316,7 @@ private void addChipTo(ChipGroup chipGroup, VaultGroupModel group) { if (!isChecked) { group1.setChecked(false); _groupFilter = groupFilter; - _entryListView.setGroupFilter(groupFilter, false); + _entryListView.setGroupFilter(groupFilter); return; } @@ -328,7 +328,7 @@ private void addChipTo(ChipGroup chipGroup, VaultGroupModel group) { } _groupFilter = groupFilter; - _entryListView.setGroupFilter(groupFilter, false); + _entryListView.setGroupFilter(groupFilter); }); chipGroup.addView(chip); @@ -573,31 +573,20 @@ private void onAddEntryResult(Intent data) { if (_loaded) { UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID"); VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID); - _entryListView.addEntry(entry, true); + _entryListView.setEntries(_vaultManager.getVault().getEntries()); + _entryListView.onEntryAdded(entry); } } - private void onEditEntryResult(Intent data) { + private void onEditEntryResult() { if (_loaded) { - UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID"); - - if (data.getBooleanExtra("delete", false)) { - _entryListView.removeEntry(entryUUID); - } else { - VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID); - _entryListView.replaceEntry(entryUUID, entry); - } + _entryListView.setEntries(_vaultManager.getVault().getEntries()); } } - private void onAssignIconsResult(Intent data) { + private void onAssignIconsResult() { if (_loaded) { - ArrayList entryUUIDs = (ArrayList) data.getSerializableExtra("entryUUIDs"); - - for (UUID entryUUID: entryUUIDs) { - VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID); - _entryListView.replaceEntry(entryUUID, entry); - } + _entryListView.setEntries(_vaultManager.getVault().getEntries()); } } @@ -695,14 +684,11 @@ private void importScannedEntries(List entries) { if (entries.size() == 1) { startEditEntryActivityForNew(entries.get(0)); } else if (entries.size() > 1) { - for (VaultEntry entry: entries) { - _vaultManager.getVault().addEntry(entry); - _entryListView.addEntry(entry); - } - if (saveAndBackupVault()) { Toast.makeText(this, getResources().getQuantityString(R.plurals.added_new_entries, entries.size(), entries.size()), Toast.LENGTH_LONG).show(); } + + _entryListView.setEntries(_vaultManager.getVault().getEntries()); } } @@ -925,15 +911,6 @@ protected void onStart() { updateErrorCard(); } - private void deleteEntries(List entries) { - for (VaultEntry entry: entries) { - VaultEntry oldEntry = _vaultManager.getVault().removeEntry(entry); - _entryListView.removeEntry(oldEntry); - } - - saveAndBackupVault(); - } - @Override public boolean onCreateOptionsMenu(Menu menu) { _menu = menu; @@ -1063,7 +1040,7 @@ private void loadEntries() { setGroups(_vaultManager.getVault().getUsedGroups()); _entryListView.setUsageCounts(_prefs.getUsageCounts()); _entryListView.setLastUsedTimestamps(_prefs.getLastUsedTimestamps()); - _entryListView.addEntries(_vaultManager.getVault().getEntries()); + _entryListView.setEntries(_vaultManager.getVault().getEntries()); if (!_isRecreated) { _entryListView.runEntriesAnimation(); } @@ -1291,6 +1268,13 @@ public void onLocked(boolean userInitiated) { } } + @Override + protected boolean saveAndBackupVault() { + boolean res = super.saveAndBackupVault(); + updateErrorCard(); + return res; + } + @SuppressLint("InlinedApi") private void copyEntryCode(VaultEntry entry) { String otp; @@ -1387,12 +1371,13 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { mode.finish(); } else if (itemId == R.id.action_toggle_favorite) { for (VaultEntry entry : _selectedEntries) { - entry.setIsFavorite(!entry.isFavorite()); - _entryListView.replaceEntry(entry.getUUID(), entry); + _vaultManager.getVault().editEntry(entry, newEntry -> { + newEntry.setIsFavorite(!newEntry.isFavorite()); + }); } - _entryListView.refresh(true); saveAndBackupVault(); + _entryListView.setEntries(_vaultManager.getVault().getEntries()); mode.finish(); } else if (itemId == R.id.action_share_qr) { Intent intent = new Intent(getBaseContext(), TransferEntriesActivity.class); @@ -1410,8 +1395,12 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { mode.finish(); } else if (itemId == R.id.action_delete) { Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries, (d, which) -> { - deleteEntries(_selectedEntries); + for (VaultEntry entry : _selectedEntries) { + _vaultManager.getVault().removeEntry(entry); + } + saveAndBackupVault(); _entryListView.setGroups(_vaultManager.getVault().getUsedGroups()); + _entryListView.setEntries(_vaultManager.getVault().getEntries()); mode.finish(); }); } else if (itemId == R.id.action_select_all) { diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/models/ErrorCardInfo.java b/app/src/main/java/com/beemdevelopment/aegis/ui/models/ErrorCardInfo.java index c6385e565f..f46423350e 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/models/ErrorCardInfo.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/models/ErrorCardInfo.java @@ -2,6 +2,10 @@ import android.view.View; +import com.google.common.hash.HashCode; + +import java.util.Objects; + public class ErrorCardInfo { private final String _message; private final View.OnClickListener _listener; @@ -18,4 +22,23 @@ public String getMessage() { public View.OnClickListener getListener() { return _listener; } + + @Override + public int hashCode() { + return HashCode.fromString(_message).asInt(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof ErrorCardInfo)) { + return false; + } + + // This equality check purposefully ignores the onclick listener + ErrorCardInfo info = (ErrorCardInfo) o; + return Objects.equals(getMessage(), info.getMessage()); + } } 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 696c7f4000..d89ef2e7a1 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 @@ -15,6 +15,8 @@ import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.RecyclerView; import com.beemdevelopment.aegis.AccountNamePosition; @@ -49,8 +51,7 @@ public class EntryAdapter extends RecyclerView.Adapter implements ItemTouchHelperAdapter { private EntryListView _view; - private List _entries; - private List _shownEntries; + private EntryList _entryList; private List _selectedEntries; private Collection _groups; private Map _usageCounts; @@ -77,14 +78,12 @@ public class EntryAdapter extends RecyclerView.Adapter private Handler _dimHandler; private Handler _doubleTapHandler; private boolean _pauseFocused; - private ErrorCardInfo _errorCardInfo; // keeps track of the EntryHolders that are currently bound private List _holders; public EntryAdapter(EntryListView view) { - _entries = new ArrayList<>(); - _shownEntries = new ArrayList<>(); + _entryList = new EntryList(); _selectedEntries = new ArrayList<>(); _groupFilter = new TreeSet<>(); _holders = new ArrayList<>(); @@ -145,173 +144,45 @@ public void setPauseFocused(boolean pauseFocused) { } public void setErrorCardInfo(ErrorCardInfo info) { - ErrorCardInfo oldInfo = _errorCardInfo; - _errorCardInfo = info; - - if (oldInfo == null && info != null) { - notifyItemInserted(0); - } else if (oldInfo != null && info == null) { - notifyItemRemoved(0); - } else { - notifyItemChanged(0); + if (Objects.equals(info, _entryList.getErrorCardInfo())) { + return; } - } - public VaultEntry getEntryAtPos(int position) { - return _shownEntries.get(translateEntryPosToIndex(position)); + replaceEntryList(new EntryList( + _entryList.getEntries(), + _entryList.getShownEntries(), + info + )); } - public int addEntry(VaultEntry entry) { - _entries.add(entry); - if (isEntryFiltered(entry)) { - return -1; - } - - int position = -1; - Comparator comparator = _sortCategory.getComparator(); - if (comparator != null) { - // insert the entry in the correct order - // note: this assumes that _shownEntries has already been sorted - for (int i = getShownFavoritesCount(); i < _shownEntries.size(); i++) { - if (comparator.compare(_shownEntries.get(i), entry) > 0) { - _shownEntries.add(i, entry); - position = translateEntryIndexToPos(i); - notifyItemInserted(position); - break; - } - } - } - - if (position < 0) { - _shownEntries.add(entry); - - position = translateEntryIndexToPos(getShownEntriesCount() - 1); - if (position == 0) { - notifyDataSetChanged(); - } else { - notifyItemInserted(position); - } - } + public VaultEntry getEntryAtPosition(int position) { + return _entryList.getShownEntries().get(_entryList.translateEntryPosToIndex(position)); + } - _view.onListChange(); - checkPeriodUniformity(); - updateFooter(); - return position; + public int getEntryPosition(VaultEntry entry) { + return _entryList.translateEntryIndexToPos(_entryList.getShownEntries().indexOf(entry)); } - public void addEntries(Collection entries) { - for (VaultEntry entry: entries) { + public void setEntries(List entries) { + // TODO: Move these fields to separate dedicated model for the UI + for (VaultEntry entry : entries) { entry.setUsageCount(_usageCounts.containsKey(entry.getUUID()) ? _usageCounts.get(entry.getUUID()) : 0); entry.setLastUsedTimestamp(_lastUsedTimestamps.containsKey(entry.getUUID()) ? _lastUsedTimestamps.get(entry.getUUID()) : 0); } - _entries.addAll(entries); - updateShownEntries(); - checkPeriodUniformity(true); - } - - public void removeEntry(VaultEntry entry) { - _entries.remove(entry); - - if (_shownEntries.contains(entry)) { - int index = _shownEntries.indexOf(entry); - _shownEntries.remove(index); - - int position = translateEntryIndexToPos(index); - notifyItemRemoved(position); - - updateFooter(); - } - - _view.onListChange(); - checkPeriodUniformity(); - } - - public void removeEntry(UUID uuid) { - VaultEntry entry = getEntryByUUID(uuid); - removeEntry(entry); + replaceEntryList(new EntryList( + entries, + calculateShownEntries(entries), + _entryList.getErrorCardInfo() + )); } public void clearEntries() { - _entries.clear(); - _shownEntries.clear(); - notifyDataSetChanged(); - checkPeriodUniformity(); - } - - public void replaceEntry(UUID uuid, VaultEntry newEntry) { - VaultEntry oldEntry = getEntryByUUID(uuid); - _entries.set(_entries.indexOf(oldEntry), newEntry); - - if (_shownEntries.contains(oldEntry)) { - int index = _shownEntries.indexOf(oldEntry); - int position = translateEntryIndexToPos(index); - if (isEntryFiltered(newEntry)) { - _shownEntries.remove(index); - notifyItemRemoved(position); - } else { - _shownEntries.set(index, newEntry); - notifyItemChanged(position); - } - - sortShownEntries(); - int newIndex = _shownEntries.indexOf(newEntry); - int newPosition = translateEntryIndexToPos(newIndex); - if (newPosition != NO_POSITION && position != newPosition) { - notifyItemMoved(position, newPosition); - } - } else if (!isEntryFiltered(newEntry)) { - // NOTE: This logic is wrong, because sorting is not taken into account. This code - // path is currently never hit though, because it is not possible to edit an entry - // that is not shown. - _shownEntries.add(newEntry); - - int position = getItemCount() - 1; - notifyItemInserted(position); - } - - checkPeriodUniformity(); - updateFooter(); + replaceEntryList(new EntryList()); } - private VaultEntry getEntryByUUID(UUID uuid) { - for (VaultEntry entry : _entries) { - if (entry.getUUID().equals(uuid)) { - return entry; - } - } - - return null; - } - - /** - * Translates the given entry position in the recycler view, to its index in the shown entries list. - */ public int translateEntryPosToIndex(int position) { - if (position == NO_POSITION) { - return NO_POSITION; - } - - if (isErrorCardShown()) { - position -= 1; - } - - return position; - } - - /** - * Translates the given entry index in the shown entries list, to its position in the recycler view. - */ - private int translateEntryIndexToPos(int index) { - if (index == NO_POSITION) { - return NO_POSITION; - } - - if (isErrorCardShown()) { - index += 1; - } - - return index; + return _entryList.translateEntryPosToIndex(position); } private boolean isEntryFiltered(VaultEntry entry) { @@ -348,7 +219,7 @@ private boolean doesAnyGroupMatchSearchFilter(Set entryGroupUUIDs, String public void refresh(boolean hard) { if (hard) { - updateShownEntries(); + refreshEntryList(); } else { for (EntryHolder holder : _holders) { holder.refresh(); @@ -363,8 +234,7 @@ public void setGroupFilter(@NonNull Set groups) { } _groupFilter = groups; - updateShownEntries(); - checkPeriodUniformity(); + refreshEntryList(); } public void setSortCategory(SortCategory category, boolean apply) { @@ -374,7 +244,7 @@ public void setSortCategory(SortCategory category, boolean apply) { _sortCategory = category; if (apply) { - updateShownEntries(); + refreshEntryList(); } } @@ -383,25 +253,59 @@ public String getSearchFilter() { } public void setSearchFilter(String search) { - _searchFilter = (search != null && !search.isEmpty()) ? search.toLowerCase().trim() : null; - updateShownEntries(); + String newSearchFilter = (search != null && !search.isEmpty()) + ? search.toLowerCase().trim() : null; + + if (!Objects.equals(_searchFilter, newSearchFilter)) { + _searchFilter = newSearchFilter; + refreshEntryList(); + } } - private void updateShownEntries() { - // clear the list of shown entries first - _shownEntries.clear(); + private void refreshEntryList() { + replaceEntryList(new EntryList( + _entryList.getEntries(), + calculateShownEntries(_entryList.getEntries()), + _entryList.getErrorCardInfo() + )); + } - // add entries back that are not filtered out - for (VaultEntry entry : _entries) { + private void replaceEntryList(EntryList newEntryList) { + DiffUtil.DiffResult diffRes = DiffUtil.calculateDiff(new DiffCallback(_entryList, newEntryList)); + _entryList = newEntryList; + updatePeriodUniformity(); + + // This scroll position trick is required in order to not have the recycler view + // jump to some random position after a large change (like resorting entries) + // Related: https://issuetracker.google.com/issues/70149059 + int scrollPos = _view.getScrollPosition(); + diffRes.dispatchUpdatesTo(this); + _view.scrollToPosition(scrollPos); + _view.onListChange(); + } + + private List calculateShownEntries(List entries) { + List res = new ArrayList<>(); + for (VaultEntry entry : entries) { if (!isEntryFiltered(entry)) { - _shownEntries.add(entry); + res.add(entry); } } - sortShownEntries(); - checkPeriodUniformity(); - _view.onListChange(); - notifyDataSetChanged(); + sortEntries(res, _sortCategory); + return res; + } + + private static void sortEntries(List entries, SortCategory sortCategory) { + if (sortCategory != null) { + Comparator comparator = sortCategory.getComparator(); + if (comparator != null) { + Collections.sort(entries, comparator); + } + } + + Comparator favoriteComparator = new FavoriteComparator(); + Collections.sort(entries, favoriteComparator); } private boolean isEntryDraggable(VaultEntry entry) { @@ -412,18 +316,6 @@ && isDragAndDropAllowed() && _selectedEntries.get(0) == entry; } - private void sortShownEntries() { - if (_sortCategory != null) { - Comparator comparator = _sortCategory.getComparator(); - if (comparator != null) { - Collections.sort(_shownEntries, comparator); - } - } - - Comparator favoriteComparator = new FavoriteComparator(); - Collections.sort(_shownEntries, favoriteComparator); - } - public void setViewMode(ViewMode viewMode) { _viewMode = viewMode; } @@ -439,7 +331,7 @@ public void setViewMode(ViewMode viewMode) { public Map getLastUsedTimestamps() { return _lastUsedTimestamps; } public int getShownFavoritesCount() { - return (int) _shownEntries.stream().filter(VaultEntry::isFavorite).count(); + return (int) _entryList.getShownEntries().stream().filter(VaultEntry::isFavorite).count(); } @Override @@ -451,43 +343,48 @@ public void onItemDismiss(int position) { public void onItemDrop(int position) { // moving entries is not allowed when a filter is applied // footer cant be moved, nor can items be moved below it - if (!_groupFilter.isEmpty() || isPositionFooter(position) || isPositionErrorCard(position)) { + if (!_groupFilter.isEmpty() || _entryList.isPositionFooter(position) || _entryList.isPositionErrorCard(position)) { return; } - int index = translateEntryPosToIndex(position); - _view.onEntryDrop(_shownEntries.get(index)); + int index = _entryList.translateEntryPosToIndex(position); + _view.onEntryDrop(_entryList.getShownEntries().get(index)); } @Override public void onItemMove(int firstPosition, int secondPosition) { - // moving entries is not allowed when a filter is applied - // footer cant be moved, nor can items be moved below it + // Moving entries is not allowed when a filter is applied. The footer can't be + // moved, nor can items be moved below it if (!_groupFilter.isEmpty() - || isPositionFooter(firstPosition) || isPositionFooter(secondPosition) - || isPositionErrorCard(firstPosition) || isPositionErrorCard(secondPosition)) { + || _entryList.isPositionFooter(firstPosition) || _entryList.isPositionFooter(secondPosition) + || _entryList.isPositionErrorCard(firstPosition) || _entryList.isPositionErrorCard(secondPosition)) { return; } - // notify the vault first - int firstIndex = translateEntryPosToIndex(firstPosition); - int secondIndex = translateEntryPosToIndex(secondPosition); - _view.onEntryMove(_entries.get(firstIndex), _entries.get(secondIndex)); - - // then update our end - CollectionUtils.move(_entries, firstIndex, secondIndex); - CollectionUtils.move(_shownEntries, firstIndex, secondIndex); + // Notify the vault about the entry position change first + int firstIndex = _entryList.translateEntryPosToIndex(firstPosition); + int secondIndex = _entryList.translateEntryPosToIndex(secondPosition); + VaultEntry firstEntry = _entryList.getShownEntries().get(firstIndex); + VaultEntry secondEntry = _entryList.getShownEntries().get(secondIndex); + _view.onEntryMove(firstEntry, secondEntry); - notifyItemMoved(firstPosition, secondPosition); + // Then update the visual end + List newEntries = new ArrayList<>(_entryList.getEntries()); + CollectionUtils.move(newEntries, newEntries.indexOf(firstEntry), newEntries.indexOf(secondEntry)); + replaceEntryList(new EntryList( + newEntries, + calculateShownEntries(newEntries), + _entryList.getErrorCardInfo() + )); } @Override public int getItemViewType(int position) { - if (isPositionErrorCard(position)) { + if (_entryList.isPositionErrorCard(position)) { return R.layout.card_error; } - if (isPositionFooter(position)) { + if (_entryList.isPositionFooter(position)) { return R.layout.card_footer; } @@ -502,7 +399,7 @@ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType View view = inflater.inflate(viewType, parent, false); if (viewType == R.layout.card_error) { - holder = new ErrorCardHolder(view, _errorCardInfo); + holder = new ErrorCardHolder(view, Objects.requireNonNull(_entryList.getErrorCardInfo())); } else if (viewType == R.layout.card_footer) { holder = new FooterView(view); } else { @@ -528,8 +425,8 @@ public void onViewRecycled(RecyclerView.ViewHolder holder) { public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) { if (holder instanceof EntryHolder) { EntryHolder entryHolder = (EntryHolder) holder; - int index = translateEntryPosToIndex(position); - VaultEntry entry = _shownEntries.get(index); + int index = _entryList.translateEntryPosToIndex(position); + VaultEntry entry = _entryList.getShownEntries().get(index); boolean hidden = _tapToReveal && entry != _focusedEntry; boolean paused = _pauseFocused && entry == _focusedEntry; @@ -538,7 +435,7 @@ public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) boolean showAccountName = true; if (_onlyShowNecessaryAccountNames) { // Only show account name when there's multiple entries found with the same issuer. - showAccountName = _entries.stream() + showAccountName = _entryList.getEntries().stream() .filter(x -> x.getIssuer().equals(entry.getIssuer())) .count() > 1; } @@ -616,8 +513,8 @@ public boolean onLongClick(View v) { entryHolder.setFocusedAndAnimate(true); } - int index = translateEntryPosToIndex(position); - boolean returnVal = _view.onLongEntryClick(_shownEntries.get(index)); + int index = _entryList.translateEntryPosToIndex(position); + boolean returnVal = _view.onLongEntryClick(_entryList.getShownEntries().get(index)); if (_selectedEntries.size() == 0 || isEntryDraggable(entry)) { _view.startDrag(entryHolder); } @@ -663,15 +560,10 @@ public void onClick(View v) { } } - private void checkPeriodUniformity() { - checkPeriodUniformity(false); - } - - private void checkPeriodUniformity(boolean force) { + private void updatePeriodUniformity() { int mostFrequentPeriod = getMostFrequentPeriod(); boolean uniform = isPeriodUniform(); - - if (!force && uniform == _isPeriodUniform && mostFrequentPeriod == _uniformPeriod) { + if (uniform == _isPeriodUniform && mostFrequentPeriod == _uniformPeriod) { return; } @@ -689,7 +581,7 @@ private void checkPeriodUniformity(boolean force) { public int getMostFrequentPeriod() { List infos = new ArrayList<>(); - for (VaultEntry entry : _shownEntries) { + for (VaultEntry entry : _entryList.getShownEntries()) { OtpInfo info = entry.getInfo(); if (info instanceof TotpInfo) { infos.add((TotpInfo) info); @@ -804,7 +696,7 @@ public void addSelectedEntry(VaultEntry entry) { public List selectAllEntries() { _selectedEntries.clear(); - for (VaultEntry entry: _shownEntries) { + for (VaultEntry entry: _entryList.getShownEntries()) { for (EntryHolder holder: _holders) { if (holder.getEntry() == entry) { holder.setFocused(true); @@ -858,34 +750,23 @@ private static boolean isPeriodUniform(int period) { @Override public int getItemCount() { - // Always at least one item because of the footer - // Two in case there's also an error card - int baseCount = 1; - if (isErrorCardShown()) { - baseCount++; - } - - return baseCount + getShownEntriesCount(); + return _entryList.getItemCount(); } public int getShownEntriesCount() { - return _shownEntries.size(); + return _entryList.getShownEntries().size(); } public boolean isPositionFooter(int position) { - return position == (getItemCount() - 1); + return _entryList.isPositionFooter(position); } public boolean isPositionErrorCard(int position) { - return isErrorCardShown() && position == 0; + return _entryList.isPositionErrorCard(position); } public boolean isErrorCardShown() { - return _errorCardInfo != null; - } - - private void updateFooter() { - notifyItemChanged(getItemCount() - 1); + return _entryList.isErrorCardShown(); } private class FooterView extends RecyclerView.ViewHolder { @@ -912,6 +793,151 @@ public void refresh() { } } + private static class EntryList { + private final List _entries; + private final List _shownEntries; + private final ErrorCardInfo _errorCardInfo; + + public EntryList() { + this(new ArrayList<>(), new ArrayList<>(), null); + } + + public EntryList( + @NonNull List entries, + @NonNull List shownEntries, + @Nullable ErrorCardInfo errorCardInfo + ) { + _entries = entries; + _shownEntries = shownEntries; + _errorCardInfo = errorCardInfo; + } + + public List getEntries() { + return _entries; + } + + public List getShownEntries() { + return _shownEntries; + } + + public int getItemCount() { + // Always at least one item because of the footer + // Two in case there's also an error card + int baseCount = 1; + if (isErrorCardShown()) { + baseCount++; + } + + return baseCount + getShownEntries().size(); + } + + @Nullable + public ErrorCardInfo getErrorCardInfo() { + return _errorCardInfo; + } + + public boolean isErrorCardShown() { + return _errorCardInfo != null; + } + + public boolean isPositionErrorCard(int position) { + return isErrorCardShown() && position == 0; + } + + public boolean isPositionFooter(int position) { + return position == (getItemCount() - 1); + } + + /** + * Translates the given entry position in the recycler view, to its index in the shown entries list. + */ + public int translateEntryPosToIndex(int position) { + if (position == NO_POSITION) { + return NO_POSITION; + } + + if (isErrorCardShown()) { + position -= 1; + } + + return position; + } + + /** + * Translates the given entry index in the shown entries list, to its position in the recycler view. + */ + public int translateEntryIndexToPos(int index) { + if (index == NO_POSITION) { + return NO_POSITION; + } + + if (isErrorCardShown()) { + index += 1; + } + + return index; + } + + } + + private static class DiffCallback extends DiffUtil.Callback { + private final EntryList _old; + private final EntryList _new; + + public DiffCallback(EntryList oldList, EntryList newList) { + _old = oldList; + _new = newList; + } + + @Override + public int getOldListSize() { + return _old.getItemCount(); + } + + @Override + public int getNewListSize() { + return _new.getItemCount(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + if (_old.isPositionErrorCard(oldItemPosition) != _new.isPositionErrorCard(newItemPosition) + || _old.isPositionFooter(oldItemPosition) != _new.isPositionFooter(newItemPosition)) { + return false; + } + + if ((_old.isPositionFooter(oldItemPosition) && _new.isPositionFooter(newItemPosition)) + || (_old.isPositionErrorCard(oldItemPosition) && _new.isPositionErrorCard(newItemPosition))) { + return true; + } + + int oldEntryIndex = _old.translateEntryPosToIndex(oldItemPosition); + int newEntryIndex = _new.translateEntryPosToIndex(newItemPosition); + if (oldEntryIndex < 0 || newEntryIndex < 0) { + return false; + } + + return _old.getShownEntries().get(oldEntryIndex).getUUID() + .equals(_new.getShownEntries().get(newEntryIndex).getUUID()); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + if (_old.isPositionFooter(oldItemPosition) && _new.isPositionFooter(newItemPosition)) { + return _old.getShownEntries().size() == _new.getShownEntries().size(); + } + + if (_old.isPositionErrorCard(oldItemPosition) && _new.isPositionErrorCard(newItemPosition)) { + return Objects.equals(_old.getErrorCardInfo(), _new.getErrorCardInfo()); + } + + int oldEntryIndex = _old.translateEntryPosToIndex(oldItemPosition); + int newEntryIndex = _new.translateEntryPosToIndex(newItemPosition); + return _old.getShownEntries().get(oldEntryIndex) + .equals(_new.getShownEntries().get(newEntryIndex)); + } + } + public interface Listener { void onEntryClick(VaultEntry entry); boolean onLongEntryClick(VaultEntry entry); 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 af829d6dac..a5b47c54a2 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 @@ -50,6 +50,7 @@ import com.google.android.material.shape.ShapeAppearanceModel; import com.google.common.base.Strings; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -156,6 +157,14 @@ public void setPreloadView(View view) { _preloadSizeProvider.setView(view); } + public int getScrollPosition() { + return ((LinearLayoutManager) _recyclerView.getLayoutManager()).findFirstVisibleItemPosition(); + } + + public void scrollToPosition(int position) { + _recyclerView.getLayoutManager().scrollToPosition(position); + } + @Override public void onDestroyView() { _refresher.destroy(); @@ -167,14 +176,10 @@ public void setGroups(Collection groups) { updateDividerDecoration(); } - public void setGroupFilter(Set groups, boolean animate) { + public void setGroupFilter(Set groups) { _adapter.setGroupFilter(groups); _touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed()); updateEmptyState(); - - if (animate) { - runEntriesAnimation(); - } } public void setIsLongPressDragEnabled(boolean enabled) { @@ -207,10 +212,6 @@ public void setActionModeState(boolean enabled, VaultEntry entry) { public void setSortCategory(SortCategory sortCategory, boolean apply) { _adapter.setSortCategory(sortCategory, apply); _touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed()); - - if (apply) { - runEntriesAnimation(); - } } public void setUsageCounts(Map usageCounts) { @@ -388,61 +389,57 @@ public void setErrorCardInfo(ErrorCardInfo info) { _adapter.setErrorCardInfo(info); } - public void addEntry(VaultEntry entry) { - addEntry(entry, false); - } - @SuppressLint("ClickableViewAccessibility") - public void addEntry(VaultEntry entry, boolean focusEntry) { - int position = _adapter.addEntry(entry); - updateEmptyState(); + public void onEntryAdded(VaultEntry entry) { + int position = _adapter.getEntryPosition(entry); + if (position < 0) { + return; + } LinearLayoutManager layoutManager = (LinearLayoutManager) _recyclerView.getLayoutManager(); - if (focusEntry && position >= 0) { - if ((_recyclerView.canScrollVertically(1) && position > layoutManager.findLastCompletelyVisibleItemPosition()) - || (_recyclerView.canScrollVertically(-1) && position < layoutManager.findFirstCompletelyVisibleItemPosition())) { - boolean smoothScroll = !AnimationsHelper.Scale.TRANSITION.isZero(requireContext()); - RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() { - private void handleScroll() { - _recyclerView.removeOnScrollListener(this); - _recyclerView.setOnTouchListener(null); - tempHighlightEntry(entry); - } + if ((_recyclerView.canScrollVertically(1) && position > layoutManager.findLastCompletelyVisibleItemPosition()) + || (_recyclerView.canScrollVertically(-1) && position < layoutManager.findFirstCompletelyVisibleItemPosition())) { + boolean smoothScroll = !AnimationsHelper.Scale.TRANSITION.isZero(requireContext()); + RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() { + private void handleScroll() { + _recyclerView.removeOnScrollListener(this); + _recyclerView.setOnTouchListener(null); + tempHighlightEntry(entry); + } - @Override - public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { - if (smoothScroll && newState == RecyclerView.SCROLL_STATE_IDLE) { - handleScroll(); - } + @Override + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { + if (smoothScroll && newState == RecyclerView.SCROLL_STATE_IDLE) { + handleScroll(); } + } - @Override - public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { - if (!smoothScroll) { - handleScroll(); - } + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (!smoothScroll) { + handleScroll(); } - }; - _recyclerView.addOnScrollListener(scrollListener); - _recyclerView.setOnTouchListener((v, event) -> { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - _recyclerView.removeOnScrollListener(scrollListener); - _recyclerView.stopScroll(); - _recyclerView.setOnTouchListener(null); - } - - return false; - }); - // We can't easily control the speed of the smooth scroll animation, but we - // can at least disable it if animations are disabled - if (smoothScroll) { - _recyclerView.smoothScrollToPosition(position); - } else { - _recyclerView.scrollToPosition(position); } + }; + _recyclerView.addOnScrollListener(scrollListener); + _recyclerView.setOnTouchListener((v, event) -> { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + _recyclerView.removeOnScrollListener(scrollListener); + _recyclerView.stopScroll(); + _recyclerView.setOnTouchListener(null); + } + + return false; + }); + // We can't easily control the speed of the smooth scroll animation, but we + // can at least disable it if animations are disabled + if (smoothScroll) { + _recyclerView.smoothScrollToPosition(position); } else { - tempHighlightEntry(entry); + _recyclerView.scrollToPosition(position); } + } else { + tempHighlightEntry(entry); } } @@ -453,27 +450,14 @@ public void tempHighlightEntry(VaultEntry entry) { _adapter.focusEntry(entry, secondsToFocus); } - public void addEntries(Collection entries) { - _adapter.addEntries(entries); - updateEmptyState(); - } - - public void removeEntry(VaultEntry entry) { - _adapter.removeEntry(entry); - updateEmptyState(); - } - - public void removeEntry(UUID uuid) { - _adapter.removeEntry(uuid); + public void setEntries(Collection entries) { + _adapter.setEntries(new ArrayList<>(entries)); updateEmptyState(); } public void clearEntries() { _adapter.clearEntries(); - } - - public void replaceEntry(UUID uuid, VaultEntry newEntry) { - _adapter.replaceEntry(uuid, newEntry); + updateEmptyState(); } public void runEntriesAnimation() { @@ -572,7 +556,7 @@ public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull R // Only non-favorite entries have a bottom margin, except for the final favorite entry int totalFavorites = _adapter.getShownFavoritesCount(); if (totalFavorites == 0 - || (entryIndex < _adapter.getShownEntriesCount() && !_adapter.getEntryAtPos(adapterPosition).isFavorite()) + || (entryIndex < _adapter.getShownEntriesCount() && !_adapter.getEntryAtPosition(adapterPosition).isFavorite()) || totalFavorites == entryIndex + 1) { outRect.bottom = _offset; } @@ -665,7 +649,7 @@ public List getPreloadItems(int position) { return Collections.emptyList(); } - VaultEntry entry = _adapter.getEntryAtPos(position); + VaultEntry entry = _adapter.getEntryAtPosition(position); if (!entry.hasIcon()) { return Collections.emptyList(); } 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 b96522f0b1..7792c953bc 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java @@ -7,6 +7,7 @@ import androidx.core.util.AtomicFile; import com.beemdevelopment.aegis.otp.GoogleAuthInfo; +import com.beemdevelopment.aegis.util.Cloner; import com.beemdevelopment.aegis.util.IOUtils; import com.google.zxing.WriterException; @@ -249,6 +250,13 @@ public VaultEntry replaceEntry(VaultEntry entry) { return _vault.getEntries().replace(entry); } + public VaultEntry editEntry(VaultEntry entry, EntryEditor editor) { + VaultEntry newEntry = Cloner.clone(entry); + editor.edit(newEntry); + replaceEntry(newEntry); + return newEntry; + } + /** * Moves entry1 to the position of entry2. */ @@ -344,4 +352,8 @@ public boolean isBackupPasswordSet() { return getCredentials().getSlots().findBackupPasswordSlots().size() > 0; } + + public interface EntryEditor { + void edit(VaultEntry entry); + } }