mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-06-25 10:30:57 +00:00
Merge pull request #1505 from alexbakker/diffutil
Use DiffUtil for the RecyclerView of the entry list
This commit is contained in:
commit
bc29242f55
6 changed files with 386 additions and 352 deletions
|
@ -69,7 +69,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||||
int swipeFlags = 0;
|
int swipeFlags = 0;
|
||||||
if (adapter.isPositionFooter(position)
|
if (adapter.isPositionFooter(position)
|
||||||
|| adapter.isPositionErrorCard(position)
|
|| adapter.isPositionErrorCard(position)
|
||||||
|| adapter.getEntryAtPos(position) != _selectedEntry
|
|| adapter.getEntryAtPosition(position) != _selectedEntry
|
||||||
|| !isLongPressDragEnabled()) {
|
|| !isLongPressDragEnabled()) {
|
||||||
return makeMovementFlags(0, swipeFlags);
|
return makeMovementFlags(0, swipeFlags);
|
||||||
}
|
}
|
||||||
|
|
|
@ -149,7 +149,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
|
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onAssignIconsResult(activityResult.getData());
|
onAssignIconsResult();
|
||||||
});
|
});
|
||||||
|
|
||||||
private final ActivityResultLauncher<Intent> preferenceResultLauncher =
|
private final ActivityResultLauncher<Intent> preferenceResultLauncher =
|
||||||
|
@ -160,7 +160,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
|
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onEditEntryResult(activityResult.getData());
|
onEditEntryResult();
|
||||||
});
|
});
|
||||||
|
|
||||||
private final ActivityResultLauncher<Intent> addEntryResultLauncher =
|
private final ActivityResultLauncher<Intent> addEntryResultLauncher =
|
||||||
|
@ -256,13 +256,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
_prefGroupFilter = null;
|
_prefGroupFilter = null;
|
||||||
if (!groupFilter.isEmpty()) {
|
if (!groupFilter.isEmpty()) {
|
||||||
_groupFilter = groupFilter;
|
_groupFilter = groupFilter;
|
||||||
_entryListView.setGroupFilter(groupFilter, false);
|
_entryListView.setGroupFilter(groupFilter);
|
||||||
}
|
}
|
||||||
} else if (_groupFilter != null) {
|
} else if (_groupFilter != null) {
|
||||||
Set<UUID> groupFilter = cleanGroupFilter(_groupFilter);
|
Set<UUID> groupFilter = cleanGroupFilter(_groupFilter);
|
||||||
if (!_groupFilter.equals(groupFilter)) {
|
if (!_groupFilter.equals(groupFilter)) {
|
||||||
_groupFilter = groupFilter;
|
_groupFilter = groupFilter;
|
||||||
_entryListView.setGroupFilter(groupFilter, true);
|
_entryListView.setGroupFilter(groupFilter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,7 +317,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
if (!isChecked) {
|
if (!isChecked) {
|
||||||
group1.setChecked(false);
|
group1.setChecked(false);
|
||||||
_groupFilter = groupFilter;
|
_groupFilter = groupFilter;
|
||||||
_entryListView.setGroupFilter(groupFilter, false);
|
_entryListView.setGroupFilter(groupFilter);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -329,7 +329,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
}
|
}
|
||||||
|
|
||||||
_groupFilter = groupFilter;
|
_groupFilter = groupFilter;
|
||||||
_entryListView.setGroupFilter(groupFilter, false);
|
_entryListView.setGroupFilter(groupFilter);
|
||||||
});
|
});
|
||||||
|
|
||||||
chipGroup.addView(chip);
|
chipGroup.addView(chip);
|
||||||
|
@ -574,31 +574,20 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
if (_loaded) {
|
if (_loaded) {
|
||||||
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
||||||
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(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) {
|
if (_loaded) {
|
||||||
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||||
|
|
||||||
if (data.getBooleanExtra("delete", false)) {
|
|
||||||
_entryListView.removeEntry(entryUUID);
|
|
||||||
} else {
|
|
||||||
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID);
|
|
||||||
_entryListView.replaceEntry(entryUUID, entry);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onAssignIconsResult(Intent data) {
|
private void onAssignIconsResult() {
|
||||||
if (_loaded) {
|
if (_loaded) {
|
||||||
ArrayList<UUID> entryUUIDs = (ArrayList<UUID>) data.getSerializableExtra("entryUUIDs");
|
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||||
|
|
||||||
for (UUID entryUUID: entryUUIDs) {
|
|
||||||
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID);
|
|
||||||
_entryListView.replaceEntry(entryUUID, entry);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -696,14 +685,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
if (entries.size() == 1) {
|
if (entries.size() == 1) {
|
||||||
startEditEntryActivityForNew(entries.get(0));
|
startEditEntryActivityForNew(entries.get(0));
|
||||||
} else if (entries.size() > 1) {
|
} else if (entries.size() > 1) {
|
||||||
for (VaultEntry entry: entries) {
|
|
||||||
_vaultManager.getVault().addEntry(entry);
|
|
||||||
_entryListView.addEntry(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (saveAndBackupVault()) {
|
if (saveAndBackupVault()) {
|
||||||
Toast.makeText(this, getResources().getQuantityString(R.plurals.added_new_entries, entries.size(), entries.size()), Toast.LENGTH_LONG).show();
|
Toast.makeText(this, getResources().getQuantityString(R.plurals.added_new_entries, entries.size(), entries.size()), Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -926,15 +912,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
updateErrorCard();
|
updateErrorCard();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteEntries(List<VaultEntry> entries) {
|
|
||||||
for (VaultEntry entry: entries) {
|
|
||||||
VaultEntry oldEntry = _vaultManager.getVault().removeEntry(entry);
|
|
||||||
_entryListView.removeEntry(oldEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
saveAndBackupVault();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
_menu = menu;
|
_menu = menu;
|
||||||
|
@ -1064,7 +1041,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
setGroups(_vaultManager.getVault().getUsedGroups());
|
setGroups(_vaultManager.getVault().getUsedGroups());
|
||||||
_entryListView.setUsageCounts(_prefs.getUsageCounts());
|
_entryListView.setUsageCounts(_prefs.getUsageCounts());
|
||||||
_entryListView.setLastUsedTimestamps(_prefs.getLastUsedTimestamps());
|
_entryListView.setLastUsedTimestamps(_prefs.getLastUsedTimestamps());
|
||||||
_entryListView.addEntries(_vaultManager.getVault().getEntries());
|
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||||
if (!_isRecreated) {
|
if (!_isRecreated) {
|
||||||
_entryListView.runEntriesAnimation();
|
_entryListView.runEntriesAnimation();
|
||||||
}
|
}
|
||||||
|
@ -1292,6 +1269,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean saveAndBackupVault() {
|
||||||
|
boolean res = super.saveAndBackupVault();
|
||||||
|
updateErrorCard();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
private void copyEntryCode(VaultEntry entry) {
|
private void copyEntryCode(VaultEntry entry) {
|
||||||
String otp;
|
String otp;
|
||||||
|
@ -1388,12 +1372,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
mode.finish();
|
mode.finish();
|
||||||
} else if (itemId == R.id.action_toggle_favorite) {
|
} else if (itemId == R.id.action_toggle_favorite) {
|
||||||
for (VaultEntry entry : _selectedEntries) {
|
for (VaultEntry entry : _selectedEntries) {
|
||||||
entry.setIsFavorite(!entry.isFavorite());
|
_vaultManager.getVault().editEntry(entry, newEntry -> {
|
||||||
_entryListView.replaceEntry(entry.getUUID(), entry);
|
newEntry.setIsFavorite(!newEntry.isFavorite());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
_entryListView.refresh(true);
|
|
||||||
|
|
||||||
saveAndBackupVault();
|
saveAndBackupVault();
|
||||||
|
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||||
mode.finish();
|
mode.finish();
|
||||||
} else if (itemId == R.id.action_share_qr) {
|
} else if (itemId == R.id.action_share_qr) {
|
||||||
Intent intent = new Intent(getBaseContext(), TransferEntriesActivity.class);
|
Intent intent = new Intent(getBaseContext(), TransferEntriesActivity.class);
|
||||||
|
@ -1411,8 +1396,12 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
mode.finish();
|
mode.finish();
|
||||||
} else if (itemId == R.id.action_delete) {
|
} else if (itemId == R.id.action_delete) {
|
||||||
Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries, (d, which) -> {
|
Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries, (d, which) -> {
|
||||||
deleteEntries(_selectedEntries);
|
for (VaultEntry entry : _selectedEntries) {
|
||||||
|
_vaultManager.getVault().removeEntry(entry);
|
||||||
|
}
|
||||||
|
saveAndBackupVault();
|
||||||
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
|
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
|
||||||
|
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||||
mode.finish();
|
mode.finish();
|
||||||
});
|
});
|
||||||
} else if (itemId == R.id.action_select_all) {
|
} else if (itemId == R.id.action_select_all) {
|
||||||
|
|
|
@ -2,6 +2,10 @@ package com.beemdevelopment.aegis.ui.models;
|
||||||
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashCode;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class ErrorCardInfo {
|
public class ErrorCardInfo {
|
||||||
private final String _message;
|
private final String _message;
|
||||||
private final View.OnClickListener _listener;
|
private final View.OnClickListener _listener;
|
||||||
|
@ -18,4 +22,23 @@ public class ErrorCardInfo {
|
||||||
public View.OnClickListener getListener() {
|
public View.OnClickListener getListener() {
|
||||||
return _listener;
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@ import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.recyclerview.widget.DiffUtil;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.beemdevelopment.aegis.AccountNamePosition;
|
import com.beemdevelopment.aegis.AccountNamePosition;
|
||||||
|
@ -49,8 +51,7 @@ import java.util.UUID;
|
||||||
|
|
||||||
public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements ItemTouchHelperAdapter {
|
public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements ItemTouchHelperAdapter {
|
||||||
private EntryListView _view;
|
private EntryListView _view;
|
||||||
private List<VaultEntry> _entries;
|
private EntryList _entryList;
|
||||||
private List<VaultEntry> _shownEntries;
|
|
||||||
private List<VaultEntry> _selectedEntries;
|
private List<VaultEntry> _selectedEntries;
|
||||||
private Collection<VaultGroup> _groups;
|
private Collection<VaultGroup> _groups;
|
||||||
private Map<UUID, Integer> _usageCounts;
|
private Map<UUID, Integer> _usageCounts;
|
||||||
|
@ -78,14 +79,12 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
private Handler _dimHandler;
|
private Handler _dimHandler;
|
||||||
private Handler _doubleTapHandler;
|
private Handler _doubleTapHandler;
|
||||||
private boolean _pauseFocused;
|
private boolean _pauseFocused;
|
||||||
private ErrorCardInfo _errorCardInfo;
|
|
||||||
|
|
||||||
// keeps track of the EntryHolders that are currently bound
|
// keeps track of the EntryHolders that are currently bound
|
||||||
private List<EntryHolder> _holders;
|
private List<EntryHolder> _holders;
|
||||||
|
|
||||||
public EntryAdapter(EntryListView view) {
|
public EntryAdapter(EntryListView view) {
|
||||||
_entries = new ArrayList<>();
|
_entryList = new EntryList();
|
||||||
_shownEntries = new ArrayList<>();
|
|
||||||
_selectedEntries = new ArrayList<>();
|
_selectedEntries = new ArrayList<>();
|
||||||
_groupFilter = new TreeSet<>();
|
_groupFilter = new TreeSet<>();
|
||||||
_holders = new ArrayList<>();
|
_holders = new ArrayList<>();
|
||||||
|
@ -150,173 +149,45 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setErrorCardInfo(ErrorCardInfo info) {
|
public void setErrorCardInfo(ErrorCardInfo info) {
|
||||||
ErrorCardInfo oldInfo = _errorCardInfo;
|
if (Objects.equals(info, _entryList.getErrorCardInfo())) {
|
||||||
_errorCardInfo = info;
|
return;
|
||||||
|
|
||||||
if (oldInfo == null && info != null) {
|
|
||||||
notifyItemInserted(0);
|
|
||||||
} else if (oldInfo != null && info == null) {
|
|
||||||
notifyItemRemoved(0);
|
|
||||||
} else {
|
|
||||||
notifyItemChanged(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
replaceEntryList(new EntryList(
|
||||||
|
_entryList.getEntries(),
|
||||||
|
_entryList.getShownEntries(),
|
||||||
|
info
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public VaultEntry getEntryAtPos(int position) {
|
public VaultEntry getEntryAtPosition(int position) {
|
||||||
return _shownEntries.get(translateEntryPosToIndex(position));
|
return _entryList.getShownEntries().get(_entryList.translateEntryPosToIndex(position));
|
||||||
}
|
}
|
||||||
|
|
||||||
public int addEntry(VaultEntry entry) {
|
public int getEntryPosition(VaultEntry entry) {
|
||||||
_entries.add(entry);
|
return _entryList.translateEntryIndexToPos(_entryList.getShownEntries().indexOf(entry));
|
||||||
if (isEntryFiltered(entry)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int position = -1;
|
|
||||||
Comparator<VaultEntry> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_view.onListChange();
|
|
||||||
checkPeriodUniformity();
|
|
||||||
updateFooter();
|
|
||||||
return position;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addEntries(Collection<VaultEntry> entries) {
|
public void setEntries(List<VaultEntry> entries) {
|
||||||
for (VaultEntry entry: 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.setUsageCount(_usageCounts.containsKey(entry.getUUID()) ? _usageCounts.get(entry.getUUID()) : 0);
|
||||||
entry.setLastUsedTimestamp(_lastUsedTimestamps.containsKey(entry.getUUID()) ? _lastUsedTimestamps.get(entry.getUUID()) : 0);
|
entry.setLastUsedTimestamp(_lastUsedTimestamps.containsKey(entry.getUUID()) ? _lastUsedTimestamps.get(entry.getUUID()) : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
_entries.addAll(entries);
|
replaceEntryList(new EntryList(
|
||||||
updateShownEntries();
|
entries,
|
||||||
checkPeriodUniformity(true);
|
calculateShownEntries(entries),
|
||||||
}
|
_entryList.getErrorCardInfo()
|
||||||
|
));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clearEntries() {
|
public void clearEntries() {
|
||||||
_entries.clear();
|
replaceEntryList(new EntryList());
|
||||||
_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();
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
public int translateEntryPosToIndex(int position) {
|
||||||
if (position == NO_POSITION) {
|
return _entryList.translateEntryPosToIndex(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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isEntryFiltered(VaultEntry entry) {
|
private boolean isEntryFiltered(VaultEntry entry) {
|
||||||
|
@ -353,7 +224,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
|
|
||||||
public void refresh(boolean hard) {
|
public void refresh(boolean hard) {
|
||||||
if (hard) {
|
if (hard) {
|
||||||
updateShownEntries();
|
refreshEntryList();
|
||||||
} else {
|
} else {
|
||||||
for (EntryHolder holder : _holders) {
|
for (EntryHolder holder : _holders) {
|
||||||
holder.refresh();
|
holder.refresh();
|
||||||
|
@ -368,8 +239,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
}
|
}
|
||||||
|
|
||||||
_groupFilter = groups;
|
_groupFilter = groups;
|
||||||
updateShownEntries();
|
refreshEntryList();
|
||||||
checkPeriodUniformity();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSortCategory(SortCategory category, boolean apply) {
|
public void setSortCategory(SortCategory category, boolean apply) {
|
||||||
|
@ -379,7 +249,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
|
|
||||||
_sortCategory = category;
|
_sortCategory = category;
|
||||||
if (apply) {
|
if (apply) {
|
||||||
updateShownEntries();
|
refreshEntryList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -388,25 +258,59 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSearchFilter(String search) {
|
public void setSearchFilter(String search) {
|
||||||
_searchFilter = (search != null && !search.isEmpty()) ? search.toLowerCase().trim() : null;
|
String newSearchFilter = (search != null && !search.isEmpty())
|
||||||
updateShownEntries();
|
? search.toLowerCase().trim() : null;
|
||||||
|
|
||||||
|
if (!Objects.equals(_searchFilter, newSearchFilter)) {
|
||||||
|
_searchFilter = newSearchFilter;
|
||||||
|
refreshEntryList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateShownEntries() {
|
private void refreshEntryList() {
|
||||||
// clear the list of shown entries first
|
replaceEntryList(new EntryList(
|
||||||
_shownEntries.clear();
|
_entryList.getEntries(),
|
||||||
|
calculateShownEntries(_entryList.getEntries()),
|
||||||
|
_entryList.getErrorCardInfo()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// add entries back that are not filtered out
|
private void replaceEntryList(EntryList newEntryList) {
|
||||||
for (VaultEntry entry : _entries) {
|
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<VaultEntry> calculateShownEntries(List<VaultEntry> entries) {
|
||||||
|
List<VaultEntry> res = new ArrayList<>();
|
||||||
|
for (VaultEntry entry : entries) {
|
||||||
if (!isEntryFiltered(entry)) {
|
if (!isEntryFiltered(entry)) {
|
||||||
_shownEntries.add(entry);
|
res.add(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sortShownEntries();
|
sortEntries(res, _sortCategory);
|
||||||
checkPeriodUniformity();
|
return res;
|
||||||
_view.onListChange();
|
}
|
||||||
notifyDataSetChanged();
|
|
||||||
|
private static void sortEntries(List<VaultEntry> entries, SortCategory sortCategory) {
|
||||||
|
if (sortCategory != null) {
|
||||||
|
Comparator<VaultEntry> comparator = sortCategory.getComparator();
|
||||||
|
if (comparator != null) {
|
||||||
|
Collections.sort(entries, comparator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Comparator<VaultEntry> favoriteComparator = new FavoriteComparator();
|
||||||
|
Collections.sort(entries, favoriteComparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isEntryDraggable(VaultEntry entry) {
|
private boolean isEntryDraggable(VaultEntry entry) {
|
||||||
|
@ -417,18 +321,6 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
&& _selectedEntries.get(0) == entry;
|
&& _selectedEntries.get(0) == entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sortShownEntries() {
|
|
||||||
if (_sortCategory != null) {
|
|
||||||
Comparator<VaultEntry> comparator = _sortCategory.getComparator();
|
|
||||||
if (comparator != null) {
|
|
||||||
Collections.sort(_shownEntries, comparator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Comparator<VaultEntry> favoriteComparator = new FavoriteComparator();
|
|
||||||
Collections.sort(_shownEntries, favoriteComparator);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setViewMode(ViewMode viewMode) {
|
public void setViewMode(ViewMode viewMode) {
|
||||||
_viewMode = viewMode;
|
_viewMode = viewMode;
|
||||||
}
|
}
|
||||||
|
@ -444,7 +336,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
public Map<UUID, Long> getLastUsedTimestamps() { return _lastUsedTimestamps; }
|
public Map<UUID, Long> getLastUsedTimestamps() { return _lastUsedTimestamps; }
|
||||||
|
|
||||||
public int getShownFavoritesCount() {
|
public int getShownFavoritesCount() {
|
||||||
return (int) _shownEntries.stream().filter(VaultEntry::isFavorite).count();
|
return (int) _entryList.getShownEntries().stream().filter(VaultEntry::isFavorite).count();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -456,43 +348,48 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
public void onItemDrop(int position) {
|
public void onItemDrop(int position) {
|
||||||
// moving entries is not allowed when a filter is applied
|
// moving entries is not allowed when a filter is applied
|
||||||
// footer cant be moved, nor can items be moved below it
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int index = translateEntryPosToIndex(position);
|
int index = _entryList.translateEntryPosToIndex(position);
|
||||||
_view.onEntryDrop(_shownEntries.get(index));
|
_view.onEntryDrop(_entryList.getShownEntries().get(index));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onItemMove(int firstPosition, int secondPosition) {
|
public void onItemMove(int firstPosition, int secondPosition) {
|
||||||
// moving entries is not allowed when a filter is applied
|
// Moving entries is not allowed when a filter is applied. The footer can't be
|
||||||
// footer cant be moved, nor can items be moved below it
|
// moved, nor can items be moved below it
|
||||||
if (!_groupFilter.isEmpty()
|
if (!_groupFilter.isEmpty()
|
||||||
|| isPositionFooter(firstPosition) || isPositionFooter(secondPosition)
|
|| _entryList.isPositionFooter(firstPosition) || _entryList.isPositionFooter(secondPosition)
|
||||||
|| isPositionErrorCard(firstPosition) || isPositionErrorCard(secondPosition)) {
|
|| _entryList.isPositionErrorCard(firstPosition) || _entryList.isPositionErrorCard(secondPosition)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// notify the vault first
|
// Notify the vault about the entry position change first
|
||||||
int firstIndex = translateEntryPosToIndex(firstPosition);
|
int firstIndex = _entryList.translateEntryPosToIndex(firstPosition);
|
||||||
int secondIndex = translateEntryPosToIndex(secondPosition);
|
int secondIndex = _entryList.translateEntryPosToIndex(secondPosition);
|
||||||
_view.onEntryMove(_entries.get(firstIndex), _entries.get(secondIndex));
|
VaultEntry firstEntry = _entryList.getShownEntries().get(firstIndex);
|
||||||
|
VaultEntry secondEntry = _entryList.getShownEntries().get(secondIndex);
|
||||||
|
_view.onEntryMove(firstEntry, secondEntry);
|
||||||
|
|
||||||
// then update our end
|
// Then update the visual end
|
||||||
CollectionUtils.move(_entries, firstIndex, secondIndex);
|
List<VaultEntry> newEntries = new ArrayList<>(_entryList.getEntries());
|
||||||
CollectionUtils.move(_shownEntries, firstIndex, secondIndex);
|
CollectionUtils.move(newEntries, newEntries.indexOf(firstEntry), newEntries.indexOf(secondEntry));
|
||||||
|
replaceEntryList(new EntryList(
|
||||||
notifyItemMoved(firstPosition, secondPosition);
|
newEntries,
|
||||||
|
calculateShownEntries(newEntries),
|
||||||
|
_entryList.getErrorCardInfo()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemViewType(int position) {
|
public int getItemViewType(int position) {
|
||||||
if (isPositionErrorCard(position)) {
|
if (_entryList.isPositionErrorCard(position)) {
|
||||||
return R.layout.card_error;
|
return R.layout.card_error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPositionFooter(position)) {
|
if (_entryList.isPositionFooter(position)) {
|
||||||
return R.layout.card_footer;
|
return R.layout.card_footer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -507,7 +404,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
View view = inflater.inflate(viewType, parent, false);
|
View view = inflater.inflate(viewType, parent, false);
|
||||||
|
|
||||||
if (viewType == R.layout.card_error) {
|
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) {
|
} else if (viewType == R.layout.card_footer) {
|
||||||
holder = new FooterView(view);
|
holder = new FooterView(view);
|
||||||
} else {
|
} else {
|
||||||
|
@ -533,8 +430,8 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
|
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
|
||||||
if (holder instanceof EntryHolder) {
|
if (holder instanceof EntryHolder) {
|
||||||
EntryHolder entryHolder = (EntryHolder) holder;
|
EntryHolder entryHolder = (EntryHolder) holder;
|
||||||
int index = translateEntryPosToIndex(position);
|
int index = _entryList.translateEntryPosToIndex(position);
|
||||||
VaultEntry entry = _shownEntries.get(index);
|
VaultEntry entry = _entryList.getShownEntries().get(index);
|
||||||
|
|
||||||
boolean hidden = _tapToReveal && entry != _focusedEntry;
|
boolean hidden = _tapToReveal && entry != _focusedEntry;
|
||||||
boolean paused = _pauseFocused && entry == _focusedEntry;
|
boolean paused = _pauseFocused && entry == _focusedEntry;
|
||||||
|
@ -543,7 +440,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
boolean showAccountName = true;
|
boolean showAccountName = true;
|
||||||
if (_onlyShowNecessaryAccountNames) {
|
if (_onlyShowNecessaryAccountNames) {
|
||||||
// Only show account name when there's multiple entries found with the same issuer.
|
// 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()))
|
.filter(x -> x.getIssuer().equals(entry.getIssuer()))
|
||||||
.count() > 1;
|
.count() > 1;
|
||||||
}
|
}
|
||||||
|
@ -621,8 +518,8 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
entryHolder.setFocusedAndAnimate(true);
|
entryHolder.setFocusedAndAnimate(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
int index = translateEntryPosToIndex(position);
|
int index = _entryList.translateEntryPosToIndex(position);
|
||||||
boolean returnVal = _view.onLongEntryClick(_shownEntries.get(index));
|
boolean returnVal = _view.onLongEntryClick(_entryList.getShownEntries().get(index));
|
||||||
if (_selectedEntries.size() == 0 || isEntryDraggable(entry)) {
|
if (_selectedEntries.size() == 0 || isEntryDraggable(entry)) {
|
||||||
_view.startDrag(entryHolder);
|
_view.startDrag(entryHolder);
|
||||||
}
|
}
|
||||||
|
@ -668,15 +565,10 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkPeriodUniformity() {
|
private void updatePeriodUniformity() {
|
||||||
checkPeriodUniformity(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkPeriodUniformity(boolean force) {
|
|
||||||
int mostFrequentPeriod = getMostFrequentPeriod();
|
int mostFrequentPeriod = getMostFrequentPeriod();
|
||||||
boolean uniform = isPeriodUniform();
|
boolean uniform = isPeriodUniform();
|
||||||
|
if (uniform == _isPeriodUniform && mostFrequentPeriod == _uniformPeriod) {
|
||||||
if (!force && uniform == _isPeriodUniform && mostFrequentPeriod == _uniformPeriod) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -694,7 +586,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
|
|
||||||
public int getMostFrequentPeriod() {
|
public int getMostFrequentPeriod() {
|
||||||
List<TotpInfo> infos = new ArrayList<>();
|
List<TotpInfo> infos = new ArrayList<>();
|
||||||
for (VaultEntry entry : _shownEntries) {
|
for (VaultEntry entry : _entryList.getShownEntries()) {
|
||||||
OtpInfo info = entry.getInfo();
|
OtpInfo info = entry.getInfo();
|
||||||
if (info instanceof TotpInfo) {
|
if (info instanceof TotpInfo) {
|
||||||
infos.add((TotpInfo) info);
|
infos.add((TotpInfo) info);
|
||||||
|
@ -809,7 +701,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
public List<VaultEntry> selectAllEntries() {
|
public List<VaultEntry> selectAllEntries() {
|
||||||
_selectedEntries.clear();
|
_selectedEntries.clear();
|
||||||
|
|
||||||
for (VaultEntry entry: _shownEntries) {
|
for (VaultEntry entry: _entryList.getShownEntries()) {
|
||||||
for (EntryHolder holder: _holders) {
|
for (EntryHolder holder: _holders) {
|
||||||
if (holder.getEntry() == entry) {
|
if (holder.getEntry() == entry) {
|
||||||
holder.setFocused(true);
|
holder.setFocused(true);
|
||||||
|
@ -863,34 +755,23 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemCount() {
|
public int getItemCount() {
|
||||||
// Always at least one item because of the footer
|
return _entryList.getItemCount();
|
||||||
// Two in case there's also an error card
|
|
||||||
int baseCount = 1;
|
|
||||||
if (isErrorCardShown()) {
|
|
||||||
baseCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseCount + getShownEntriesCount();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getShownEntriesCount() {
|
public int getShownEntriesCount() {
|
||||||
return _shownEntries.size();
|
return _entryList.getShownEntries().size();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPositionFooter(int position) {
|
public boolean isPositionFooter(int position) {
|
||||||
return position == (getItemCount() - 1);
|
return _entryList.isPositionFooter(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPositionErrorCard(int position) {
|
public boolean isPositionErrorCard(int position) {
|
||||||
return isErrorCardShown() && position == 0;
|
return _entryList.isPositionErrorCard(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isErrorCardShown() {
|
public boolean isErrorCardShown() {
|
||||||
return _errorCardInfo != null;
|
return _entryList.isErrorCardShown();
|
||||||
}
|
|
||||||
|
|
||||||
private void updateFooter() {
|
|
||||||
notifyItemChanged(getItemCount() - 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FooterView extends RecyclerView.ViewHolder {
|
private class FooterView extends RecyclerView.ViewHolder {
|
||||||
|
@ -917,6 +798,151 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class EntryList {
|
||||||
|
private final List<VaultEntry> _entries;
|
||||||
|
private final List<VaultEntry> _shownEntries;
|
||||||
|
private final ErrorCardInfo _errorCardInfo;
|
||||||
|
|
||||||
|
public EntryList() {
|
||||||
|
this(new ArrayList<>(), new ArrayList<>(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntryList(
|
||||||
|
@NonNull List<VaultEntry> entries,
|
||||||
|
@NonNull List<VaultEntry> shownEntries,
|
||||||
|
@Nullable ErrorCardInfo errorCardInfo
|
||||||
|
) {
|
||||||
|
_entries = entries;
|
||||||
|
_shownEntries = shownEntries;
|
||||||
|
_errorCardInfo = errorCardInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<VaultEntry> getEntries() {
|
||||||
|
return _entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<VaultEntry> 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 {
|
public interface Listener {
|
||||||
void onEntryClick(VaultEntry entry);
|
void onEntryClick(VaultEntry entry);
|
||||||
boolean onLongEntryClick(VaultEntry entry);
|
boolean onLongEntryClick(VaultEntry entry);
|
||||||
|
|
|
@ -50,6 +50,7 @@ import com.google.android.material.shape.CornerFamily;
|
||||||
import com.google.android.material.shape.ShapeAppearanceModel;
|
import com.google.android.material.shape.ShapeAppearanceModel;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -156,6 +157,14 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||||
_preloadSizeProvider.setView(view);
|
_preloadSizeProvider.setView(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getScrollPosition() {
|
||||||
|
return ((LinearLayoutManager) _recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void scrollToPosition(int position) {
|
||||||
|
_recyclerView.getLayoutManager().scrollToPosition(position);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
_refresher.destroy();
|
_refresher.destroy();
|
||||||
|
@ -167,14 +176,10 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||||
updateDividerDecoration();
|
updateDividerDecoration();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setGroupFilter(Set<UUID> groups, boolean animate) {
|
public void setGroupFilter(Set<UUID> groups) {
|
||||||
_adapter.setGroupFilter(groups);
|
_adapter.setGroupFilter(groups);
|
||||||
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
||||||
updateEmptyState();
|
updateEmptyState();
|
||||||
|
|
||||||
if (animate) {
|
|
||||||
runEntriesAnimation();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setIsLongPressDragEnabled(boolean enabled) {
|
public void setIsLongPressDragEnabled(boolean enabled) {
|
||||||
|
@ -207,10 +212,6 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||||
public void setSortCategory(SortCategory sortCategory, boolean apply) {
|
public void setSortCategory(SortCategory sortCategory, boolean apply) {
|
||||||
_adapter.setSortCategory(sortCategory, apply);
|
_adapter.setSortCategory(sortCategory, apply);
|
||||||
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
||||||
|
|
||||||
if (apply) {
|
|
||||||
runEntriesAnimation();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUsageCounts(Map<UUID, Integer> usageCounts) {
|
public void setUsageCounts(Map<UUID, Integer> usageCounts) {
|
||||||
|
@ -392,61 +393,57 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||||
_adapter.setErrorCardInfo(info);
|
_adapter.setErrorCardInfo(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addEntry(VaultEntry entry) {
|
|
||||||
addEntry(entry, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
public void addEntry(VaultEntry entry, boolean focusEntry) {
|
public void onEntryAdded(VaultEntry entry) {
|
||||||
int position = _adapter.addEntry(entry);
|
int position = _adapter.getEntryPosition(entry);
|
||||||
updateEmptyState();
|
if (position < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
LinearLayoutManager layoutManager = (LinearLayoutManager) _recyclerView.getLayoutManager();
|
LinearLayoutManager layoutManager = (LinearLayoutManager) _recyclerView.getLayoutManager();
|
||||||
if (focusEntry && position >= 0) {
|
if ((_recyclerView.canScrollVertically(1) && position > layoutManager.findLastCompletelyVisibleItemPosition())
|
||||||
if ((_recyclerView.canScrollVertically(1) && position > layoutManager.findLastCompletelyVisibleItemPosition())
|
|| (_recyclerView.canScrollVertically(-1) && position < layoutManager.findFirstCompletelyVisibleItemPosition())) {
|
||||||
|| (_recyclerView.canScrollVertically(-1) && position < layoutManager.findFirstCompletelyVisibleItemPosition())) {
|
boolean smoothScroll = !AnimationsHelper.Scale.TRANSITION.isZero(requireContext());
|
||||||
boolean smoothScroll = !AnimationsHelper.Scale.TRANSITION.isZero(requireContext());
|
RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
|
||||||
RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
|
private void handleScroll() {
|
||||||
private void handleScroll() {
|
_recyclerView.removeOnScrollListener(this);
|
||||||
_recyclerView.removeOnScrollListener(this);
|
_recyclerView.setOnTouchListener(null);
|
||||||
_recyclerView.setOnTouchListener(null);
|
tempHighlightEntry(entry);
|
||||||
tempHighlightEntry(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
@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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
_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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_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 {
|
} else {
|
||||||
tempHighlightEntry(entry);
|
_recyclerView.scrollToPosition(position);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
tempHighlightEntry(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -457,27 +454,14 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||||
_adapter.focusEntry(entry, secondsToFocus);
|
_adapter.focusEntry(entry, secondsToFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addEntries(Collection<VaultEntry> entries) {
|
public void setEntries(Collection<VaultEntry> entries) {
|
||||||
_adapter.addEntries(entries);
|
_adapter.setEntries(new ArrayList<>(entries));
|
||||||
updateEmptyState();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeEntry(VaultEntry entry) {
|
|
||||||
_adapter.removeEntry(entry);
|
|
||||||
updateEmptyState();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeEntry(UUID uuid) {
|
|
||||||
_adapter.removeEntry(uuid);
|
|
||||||
updateEmptyState();
|
updateEmptyState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clearEntries() {
|
public void clearEntries() {
|
||||||
_adapter.clearEntries();
|
_adapter.clearEntries();
|
||||||
}
|
updateEmptyState();
|
||||||
|
|
||||||
public void replaceEntry(UUID uuid, VaultEntry newEntry) {
|
|
||||||
_adapter.replaceEntry(uuid, newEntry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void runEntriesAnimation() {
|
public void runEntriesAnimation() {
|
||||||
|
@ -576,7 +560,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||||
// Only non-favorite entries have a bottom margin, except for the final favorite entry
|
// Only non-favorite entries have a bottom margin, except for the final favorite entry
|
||||||
int totalFavorites = _adapter.getShownFavoritesCount();
|
int totalFavorites = _adapter.getShownFavoritesCount();
|
||||||
if (totalFavorites == 0
|
if (totalFavorites == 0
|
||||||
|| (entryIndex < _adapter.getShownEntriesCount() && !_adapter.getEntryAtPos(adapterPosition).isFavorite())
|
|| (entryIndex < _adapter.getShownEntriesCount() && !_adapter.getEntryAtPosition(adapterPosition).isFavorite())
|
||||||
|| totalFavorites == entryIndex + 1) {
|
|| totalFavorites == entryIndex + 1) {
|
||||||
outRect.bottom = _offset;
|
outRect.bottom = _offset;
|
||||||
}
|
}
|
||||||
|
@ -669,7 +653,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
VaultEntry entry = _adapter.getEntryAtPos(position);
|
VaultEntry entry = _adapter.getEntryAtPosition(position);
|
||||||
if (!entry.hasIcon()) {
|
if (!entry.hasIcon()) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.core.util.AtomicFile;
|
import androidx.core.util.AtomicFile;
|
||||||
|
|
||||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||||
|
import com.beemdevelopment.aegis.util.Cloner;
|
||||||
import com.beemdevelopment.aegis.util.IOUtils;
|
import com.beemdevelopment.aegis.util.IOUtils;
|
||||||
import com.google.zxing.WriterException;
|
import com.google.zxing.WriterException;
|
||||||
|
|
||||||
|
@ -249,6 +250,13 @@ public class VaultRepository {
|
||||||
return _vault.getEntries().replace(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.
|
* Moves entry1 to the position of entry2.
|
||||||
*/
|
*/
|
||||||
|
@ -344,4 +352,8 @@ public class VaultRepository {
|
||||||
|
|
||||||
return getCredentials().getSlots().findBackupPasswordSlots().size() > 0;
|
return getCredentials().getSlots().findBackupPasswordSlots().size() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface EntryEditor {
|
||||||
|
void edit(VaultEntry entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue