From c3465070924ce746122376927c50ea125c2798c1 Mon Sep 17 00:00:00 2001 From: elena Date: Sun, 30 Apr 2023 15:31:38 +0100 Subject: [PATCH] Refer to groups by UUID - Also lays the foundations for adding entries to multiple groups and changing group names Co-authored-by: Alexander Bakker --- .../beemdevelopment/aegis/Preferences.java | 18 ++-- .../aegis/importers/AegisImporter.java | 36 +++++++- .../aegis/importers/DatabaseImporter.java | 10 ++ .../aegis/ui/EditEntryActivity.java | 92 +++++++++++-------- .../aegis/ui/GroupManagerActivity.java | 61 +++++++++--- .../aegis/ui/ImportEntriesActivity.java | 41 ++++++++- .../aegis/ui/MainActivity.java | 20 +--- .../ui/components/DropdownCheckBoxes.java | 41 +++++---- .../aegis/ui/dialogs/Dialogs.java | 5 +- .../ImportExportPreferencesFragment.java | 36 +++++--- .../aegis/ui/models/VaultGroupModel.java | 45 +++++++++ .../aegis/ui/views/EntryAdapter.java | 19 ++-- .../aegis/ui/views/EntryListView.java | 63 +++++++------ .../aegis/ui/views/GroupAdapter.java | 9 +- .../aegis/ui/views/GroupHolder.java | 5 +- .../beemdevelopment/aegis/util/UUIDMap.java | 12 ++- .../beemdevelopment/aegis/vault/Vault.java | 65 ++++++++++++- .../aegis/vault/VaultEntry.java | 69 ++++++++++---- .../aegis/vault/VaultGroup.java | 69 ++++++++++++++ .../aegis/vault/VaultRepository.java | 69 +++++++++++--- app/src/main/res/menu/menu_groups.xml | 4 + app/src/main/res/values/strings.xml | 2 + 22 files changed, 594 insertions(+), 197 deletions(-) create mode 100644 app/src/main/java/com/beemdevelopment/aegis/ui/models/VaultGroupModel.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/vault/VaultGroup.java diff --git a/app/src/main/java/com/beemdevelopment/aegis/Preferences.java b/app/src/main/java/com/beemdevelopment/aegis/Preferences.java index e2339212b..3bf8d965a 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/Preferences.java +++ b/app/src/main/java/com/beemdevelopment/aegis/Preferences.java @@ -20,9 +20,11 @@ import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.UUID; public class Preferences { @@ -478,26 +480,26 @@ public boolean isMinimizeOnCopyEnabled() { return _prefs.getBoolean("pref_minimize_on_copy", false); } - public void setGroupFilter(List groupFilter) { + public void setGroupFilter(Set groupFilter) { JSONArray json = new JSONArray(groupFilter); - _prefs.edit().putString("pref_group_filter", json.toString()).apply(); + _prefs.edit().putString("pref_group_filter_uuids", json.toString()).apply(); } - public List getGroupFilter() { - String raw = _prefs.getString("pref_group_filter", null); + public Set getGroupFilter() { + String raw = _prefs.getString("pref_group_filter_uuids", null); if (raw == null || raw.isEmpty()) { - return Collections.emptyList(); + return Collections.emptySet(); } try { JSONArray json = new JSONArray(raw); - List filter = new ArrayList<>(); + Set filter = new HashSet<>(); for (int i = 0; i < json.length(); i++) { - filter.add(json.isNull(i) ? null : json.optString(i)); + filter.add(json.isNull(i) ? null : UUID.fromString(json.getString(i))); } return filter; } catch (JSONException e) { - return Collections.emptyList(); + return Collections.emptySet(); } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/AegisImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/AegisImporter.java index c01b13dfe..ac42cc872 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/AegisImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/AegisImporter.java @@ -16,6 +16,7 @@ import com.beemdevelopment.aegis.vault.VaultFile; import com.beemdevelopment.aegis.vault.VaultFileCredentials; import com.beemdevelopment.aegis.vault.VaultFileException; +import com.beemdevelopment.aegis.vault.VaultGroup; import com.beemdevelopment.aegis.vault.slots.PasswordSlot; import com.beemdevelopment.aegis.vault.slots.SlotList; import com.topjohnwu.superuser.io.SuFile; @@ -27,6 +28,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.List; +import java.util.UUID; public class AegisImporter extends DatabaseImporter { @@ -132,11 +134,31 @@ public Result convert() throws DatabaseImporterException { Result result = new Result(); try { - JSONArray array = _obj.getJSONArray("entries"); - for (int i = 0; i < array.length(); i++) { - JSONObject entryObj = array.getJSONObject(i); + if (_obj.has("groups")) { + JSONArray groupArray = _obj.getJSONArray("groups"); + for (int i = 0; i < groupArray.length(); i++) { + JSONObject groupObj = groupArray.getJSONObject(i); + try { + VaultGroup group = convertGroup(groupObj); + if (!result.getGroups().has(group)) { + result.addGroup(group); + } + } catch (DatabaseImporterEntryException e) { + result.addError(e); + } + } + } + + JSONArray entryArray = _obj.getJSONArray("entries"); + for (int i = 0; i < entryArray.length(); i++) { + JSONObject entryObj = entryArray.getJSONObject(i); try { VaultEntry entry = convertEntry(entryObj); + for (UUID groupUuid : entry.getGroups()) { + if (!result.getGroups().has(groupUuid)) { + entry.getGroups().remove(groupUuid); + } + } result.addEntry(entry); } catch (DatabaseImporterEntryException e) { result.addError(e); @@ -156,5 +178,13 @@ private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEn throw new DatabaseImporterEntryException(e, obj.toString()); } } + + private static VaultGroup convertGroup(JSONObject obj) throws DatabaseImporterEntryException { + try { + return VaultGroup.fromJson(obj); + } catch (VaultEntryException e) { + throw new DatabaseImporterEntryException(e, obj.toString()); + } + } } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java index f3a9de4de..fcf8e8d43 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java @@ -8,6 +8,7 @@ import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.util.UUIDMap; import com.beemdevelopment.aegis.vault.VaultEntry; +import com.beemdevelopment.aegis.vault.VaultGroup; import com.topjohnwu.superuser.Shell; import com.topjohnwu.superuser.io.SuFile; import com.topjohnwu.superuser.io.SuFileInputStream; @@ -168,12 +169,17 @@ public Result convert() throws DatabaseImporterException { public static class Result { private UUIDMap _entries = new UUIDMap<>(); + private UUIDMap _groups = new UUIDMap<>(); private List _errors = new ArrayList<>(); public void addEntry(VaultEntry entry) { _entries.add(entry); } + public void addGroup(VaultGroup group) { + _groups.add(group); + } + public void addError(DatabaseImporterEntryException error) { _errors.add(error); } @@ -182,6 +188,10 @@ public UUIDMap getEntries() { return _entries; } + public UUIDMap getGroups() { + return _groups; + } + public List getErrors() { return _errors; } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java index 8812592b7..e58e2070c 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java @@ -1,7 +1,7 @@ package com.beemdevelopment.aegis.ui; +import android.content.DialogInterface; import android.content.Intent; -import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; @@ -15,7 +15,6 @@ import android.view.animation.AccelerateInterpolator; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; -import android.widget.AdapterView; import android.widget.AutoCompleteTextView; import android.widget.ImageView; import android.widget.LinearLayout; @@ -54,11 +53,13 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs; import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog; import com.beemdevelopment.aegis.ui.glide.IconLoader; +import com.beemdevelopment.aegis.ui.models.VaultGroupModel; import com.beemdevelopment.aegis.ui.tasks.ImportFileTask; import com.beemdevelopment.aegis.ui.views.IconAdapter; import com.beemdevelopment.aegis.util.Cloner; import com.beemdevelopment.aegis.util.IOUtils; import com.beemdevelopment.aegis.vault.VaultEntry; +import com.beemdevelopment.aegis.vault.VaultGroup; import com.beemdevelopment.aegis.vault.VaultRepository; import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; @@ -73,11 +74,14 @@ import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.List; import java.util.Locale; -import java.util.TreeSet; +import java.util.Objects; +import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -90,7 +94,7 @@ public class EditEntryActivity extends AegisActivity { private boolean _isNew = false; private boolean _isManual = false; private VaultEntry _origEntry; - private TreeSet _groups; + private Collection _groups; private boolean _hasCustomIcon = false; // keep track of icon changes separately as the generated jpeg's are not deterministic private boolean _hasChangedIcon = false; @@ -114,7 +118,7 @@ public class EditEntryActivity extends AegisActivity { private AutoCompleteTextView _dropdownAlgo; private TextInputLayout _dropdownAlgoLayout; private AutoCompleteTextView _dropdownGroup; - private List _dropdownGroupList = new ArrayList<>(); + private List _dropdownGroupList = new ArrayList<>(); private KropView _kropView; @@ -262,8 +266,13 @@ protected void onCreate(Bundle savedInstanceState) { updateAdvancedFieldStatus(_origEntry.getInfo().getTypeId()); updatePinFieldVisibility(_origEntry.getInfo().getTypeId()); - String group = _origEntry.getGroup(); - setGroup(group); + Set groups = _origEntry.getGroups(); + if (groups.isEmpty()) { + setGroup(new VaultGroupModel(getString(R.string.no_group))); + } else { + VaultGroup group = _vaultManager.getVault().getGroupByUUID(groups.iterator().next()); + setGroup(new VaultGroupModel(group)); + } // Update the icon if the issuer or name has changed _textIssuer.addTextChangedListener(_nameChangeListener); @@ -327,24 +336,31 @@ protected void onCreate(Bundle savedInstanceState) { startIconSelection(); }); - _dropdownGroup.setOnItemClickListener(new AdapterView.OnItemClickListener() { - private int prevPosition = _dropdownGroupList.indexOf(_dropdownGroup.getText().toString()); - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - if (position == _dropdownGroupList.size() - 1) { - Dialogs.showTextInputDialog(EditEntryActivity.this, R.string.set_group, R.string.group_name_hint, text -> { - String groupName = new String(text); - if (!groupName.isEmpty()) { - _groups.add(groupName); - updateGroupDropdownList(); - _dropdownGroup.setText(groupName, false); + _dropdownGroup.setOnItemClickListener((parent, view, position, id) -> { + VaultGroupModel selectedGroup = _dropdownGroupList.get(position); + if (selectedGroup.isPlaceholder() && Objects.equals(selectedGroup.getName(), getString(R.string.new_group))) { + Dialogs.TextInputListener onAddGroup = text -> { + String groupName = new String(text).trim(); + if (!groupName.isEmpty()) { + VaultGroup group = _vaultManager.getVault().findGroupByName(groupName); + if (group == null) { + group = new VaultGroup(groupName); + _vaultManager.getVault().addGroup(group); } - }); - _dropdownGroup.setText(_dropdownGroupList.get(prevPosition), false); - } else { - prevPosition = position; - } + + updateGroupDropdownList(); + setGroup(new VaultGroupModel(group)); + } + }; + + DialogInterface.OnCancelListener onCancel = dialogInterface -> { + VaultGroupModel previous = (VaultGroupModel) _dropdownGroup.getTag(); + _dropdownGroup.setText(previous.getName(), false); + }; + + Dialogs.showTextInputDialog(EditEntryActivity.this, R.string.set_group, R.string.group_name_hint, onAddGroup, onCancel); + } else { + setGroup(_dropdownGroupList.get(position)); } }); @@ -365,13 +381,9 @@ private void updatePinFieldVisibility(String otpType) { _textPin.setHint(otpType.equals(MotpInfo.ID) ? R.string.motp_pin : R.string.yandex_pin); } - private void setGroup(String groupName) { - int pos = 0; - if (groupName != null) { - pos = _groups.contains(groupName) ? _groups.headSet(groupName).size() + 1 : 0; - } - - _dropdownGroup.setText(_dropdownGroupList.get(pos), false); + private void setGroup(VaultGroupModel group) { + _dropdownGroup.setText(group.getName(), false); + _dropdownGroup.setTag(group); } private void openAdvancedSettings() { @@ -395,11 +407,10 @@ private void openAdvancedSettings() { } private void updateGroupDropdownList() { - Resources res = getResources(); _dropdownGroupList.clear(); - _dropdownGroupList.add(res.getString(R.string.no_group)); - _dropdownGroupList.addAll(_groups); - _dropdownGroupList.add(res.getString(R.string.new_group)); + _dropdownGroupList.add(new VaultGroupModel(getString(R.string.new_group))); + _dropdownGroupList.addAll(_groups.stream().map(VaultGroupModel::new).collect(Collectors.toList())); + _dropdownGroupList.add(new VaultGroupModel(getString(R.string.no_group))); } private boolean hasUnsavedChanges(VaultEntry newEntry) { @@ -726,12 +737,13 @@ private VaultEntry parseEntry() throws ParseException { entry.setName(_textName.getText().toString()); entry.setNote(_textNote.getText().toString()); - int groupPos = _dropdownGroupList.indexOf(_dropdownGroup.getText().toString()); - if (groupPos != 0) { - String group = _dropdownGroupList.get(groupPos); - entry.setGroup(group); + VaultGroupModel group = (VaultGroupModel) _dropdownGroup.getTag(); + if (group.isPlaceholder()) { + entry.setGroups(new HashSet<>()); } else { - entry.setGroup(null); + Set groups = new HashSet<>(); + groups.add(group.getUUID()); + entry.setGroups(groups); } if (_hasChangedIcon) { diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/GroupManagerActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/GroupManagerActivity.java index a894c1a56..f18157112 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/GroupManagerActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/GroupManagerActivity.java @@ -14,16 +14,17 @@ import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.ui.dialogs.Dialogs; import com.beemdevelopment.aegis.ui.views.GroupAdapter; -import com.beemdevelopment.aegis.vault.VaultEntry; +import com.beemdevelopment.aegis.vault.VaultGroup; import java.util.ArrayList; import java.util.HashSet; import java.util.List; -import java.util.Objects; +import java.util.Set; +import java.util.UUID; public class GroupManagerActivity extends AegisActivity implements GroupAdapter.Listener { private GroupAdapter _adapter; - private HashSet _removedGroups; + private HashSet _removedGroups; private RecyclerView _groupsView; private View _emptyStateView; private BackPressHandler _backPressHandler; @@ -43,11 +44,14 @@ protected void onCreate(Bundle savedInstanceState) { _backPressHandler = new BackPressHandler(); getOnBackPressedDispatcher().addCallback(this, _backPressHandler); + _removedGroups = new HashSet<>(); if (savedInstanceState != null) { List groups = savedInstanceState.getStringArrayList("removedGroups"); - _removedGroups = new HashSet<>(Objects.requireNonNull(groups)); - } else { - _removedGroups = new HashSet<>(); + if (groups != null) { + for (String uuid : groups) { + _removedGroups.add(UUID.fromString(uuid)); + } + } } _adapter = new GroupAdapter(this); @@ -57,8 +61,10 @@ protected void onCreate(Bundle savedInstanceState) { _groupsView.setAdapter(_adapter); _groupsView.setNestedScrollingEnabled(false); - for (String group : _vaultManager.getVault().getGroups()) { - _adapter.addGroup(group); + for (VaultGroup group : _vaultManager.getVault().getGroups()) { + if (!_removedGroups.contains(group.getUUID())) { + _adapter.addGroup(group); + } } _emptyStateView = findViewById(R.id.vEmptyList); @@ -68,16 +74,21 @@ protected void onCreate(Bundle savedInstanceState) { @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); - outState.putStringArrayList("removedGroups", new ArrayList<>(_removedGroups)); + ArrayList removed = new ArrayList<>(); + for (UUID uuid : _removedGroups) { + removed.add(uuid.toString()); + } + + outState.putStringArrayList("removedGroups", removed); } @Override - public void onRemoveGroup(String group) { + public void onRemoveGroup(VaultGroup group) { Dialogs.showSecureDialog(new AlertDialog.Builder(this) .setTitle(R.string.remove_group) .setMessage(R.string.remove_group_description) .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> { - _removedGroups.add(group); + _removedGroups.add(group.getUUID()); _adapter.removeGroup(group); _backPressHandler.setEnabled(true); updateEmptyState(); @@ -86,12 +97,29 @@ public void onRemoveGroup(String group) { .create()); } + public void onRemoveUnusedGroups() { + Dialogs.showSecureDialog(new AlertDialog.Builder(this) + .setTitle(R.string.remove_unused_groups) + .setMessage(R.string.remove_unused_groups_description) + .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> { + Set unusedGroups = new HashSet<>(_vaultManager.getVault().getGroups()); + unusedGroups.removeAll(_vaultManager.getVault().getUsedGroups()); + + for (VaultGroup group : unusedGroups) { + _removedGroups.add(group.getUUID()); + _adapter.removeGroup(group); + } + _backPressHandler.setEnabled(true); + updateEmptyState(); + }) + .setNegativeButton(android.R.string.no, null) + .create()); + } + private void saveAndFinish() { if (!_removedGroups.isEmpty()) { - for (VaultEntry entry : _vaultManager.getVault().getEntries()) { - if (_removedGroups.contains(entry.getGroup())) { - entry.setGroup(null); - } + for (UUID uuid : _removedGroups) { + _vaultManager.getVault().removeGroup(uuid); } saveAndBackupVault(); @@ -126,6 +154,9 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.action_save: saveAndFinish(); break; + case R.id.action_delete_unused_groups: + onRemoveUnusedGroups(); + break; default: return super.onOptionsItemSelected(item); } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/ImportEntriesActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/ImportEntriesActivity.java index 8e1261899..0a992b1ad 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/ImportEntriesActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/ImportEntriesActivity.java @@ -25,6 +25,7 @@ import com.beemdevelopment.aegis.ui.views.ImportEntriesAdapter; import com.beemdevelopment.aegis.util.UUIDMap; import com.beemdevelopment.aegis.vault.VaultEntry; +import com.beemdevelopment.aegis.vault.VaultGroup; import com.beemdevelopment.aegis.vault.VaultRepository; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.snackbar.Snackbar; @@ -37,6 +38,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.UUID; public class ImportEntriesActivity extends AegisActivity { @@ -45,6 +47,8 @@ public class ImportEntriesActivity extends AegisActivity { private ImportEntriesAdapter _adapter; private FabScrollHelper _fabScrollHelper; + private UUIDMap _importedGroups; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -205,6 +209,8 @@ private void importDatabase(DatabaseImporter.State state) { importEntries.add(importEntry); } + _importedGroups = result.getGroups(); + List errors = result.getErrors(); if (errors.size() > 0) { String message = getResources().getQuantityString(R.plurals.import_error_dialog, errors.size(), errors.size()); @@ -225,10 +231,43 @@ private void showWipeEntriesDialog() { private void saveAndFinish(boolean wipeEntries) { VaultRepository vault = _vaultManager.getVault(); if (wipeEntries) { - vault.wipeEntries(); + vault.wipeContents(); } + // Given the list of selected entries, collect the UUID's of all groups + // that we're actually going to import List selectedEntries = _adapter.getCheckedEntries(); + List selectedGroupUuids = new ArrayList<>(); + for (ImportEntry entry : selectedEntries) { + selectedGroupUuids.addAll(entry.getEntry().getGroups()); + } + + // Add all of the new groups to the vault. If a group with the same name already + // exists in the vault, rewrite all entries in that group to reference the existing group. + for (VaultGroup importedGroup : _importedGroups) { + if (!selectedGroupUuids.contains(importedGroup.getUUID())) { + continue; + } + + VaultGroup existingGroup = vault.findGroupByUUID(importedGroup.getUUID()); + if (existingGroup != null) { + continue; + } + + existingGroup = vault.findGroupByName(importedGroup.getName()); + if (existingGroup == null) { + vault.addGroup(importedGroup); + } else { + for (ImportEntry entry : selectedEntries) { + Set entryGroups = entry.getEntry().getGroups(); + if (entryGroups.contains(importedGroup.getUUID())) { + entryGroups.remove(importedGroup.getUUID()); + entryGroups.add(existingGroup.getUUID()); + } + } + } + } + for (ImportEntry selectedEntry : selectedEntries) { VaultEntry entry = selectedEntry.getEntry(); 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 a43b16431..2a43b6340 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java @@ -63,7 +63,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.TreeSet; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -632,7 +632,7 @@ protected void onResume() { startAuthActivity(false); } else if (_loaded) { // update the list of groups in the entry list view so that the chip gets updated - _entryListView.setGroups(_vaultManager.getVault().getGroups()); + _entryListView.setGroups(_vaultManager.getVault().getUsedGroups()); // update the usage counts in case they are edited outside of the EntryListView _entryListView.setUsageCounts(_prefs.getUsageCounts()); @@ -670,7 +670,7 @@ public boolean onCreateOptionsMenu(Menu menu) { updateLockIcon(); if (_loaded) { - _entryListView.setGroups(_vaultManager.getVault().getGroups()); + _entryListView.setGroups(_vaultManager.getVault().getUsedGroups()); updateSortCategoryMenu(); } @@ -980,7 +980,7 @@ public void onScroll(int dx, int dy) { public void onListChange() { _fabScrollHelper.setVisible(true); } @Override - public void onSaveGroupFilter(List groupFilter) { + public void onSaveGroupFilter(Set groupFilter) { _prefs.setGroupFilter(groupFilter); } @@ -1128,17 +1128,7 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { case R.id.action_delete: Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries, (d, which) -> { deleteEntries(_selectedEntries); - - for (VaultEntry entry : _selectedEntries) { - if (entry.getGroup() != null) { - TreeSet groups = _vaultManager.getVault().getGroups(); - if (!groups.contains(entry.getGroup())) { - _entryListView.setGroups(groups); - break; - } - } - } - + _entryListView.setGroups(_vaultManager.getVault().getUsedGroups()); mode.finish(); }); return true; diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/components/DropdownCheckBoxes.java b/app/src/main/java/com/beemdevelopment/aegis/ui/components/DropdownCheckBoxes.java index d61998fda..06249985f 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/components/DropdownCheckBoxes.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/components/DropdownCheckBoxes.java @@ -18,19 +18,19 @@ import com.beemdevelopment.aegis.R; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.TreeSet; import java.util.stream.Collectors; -public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView { +public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView { private @PluralsRes int _selectedCountPlural = R.plurals.dropdown_checkboxes_default_count; private boolean _allowFiltering = false; - private final List _items = new ArrayList<>(); - private List _visibleItems = new ArrayList<>(); - private final Set _checkedItems = new TreeSet<>(); + private final List _items = new ArrayList<>(); + private List _visibleItems = new ArrayList<>(); + private final Set _checkedItems = new HashSet<>(); private CheckboxAdapter _adapter; @@ -70,7 +70,15 @@ private void initialise(Context context, AttributeSet attrs) { } } - public void addItems(List items, boolean startChecked) { + /** + * Add parameterized items to be displayed as a checkbox in the dropdown view + * the label for the checkbox is determined by the toString() method of the items + * you add. + * + * @param items a list of the items you want to show in the dropdown + * @param startChecked whether the checkbox should be checked initially + */ + public void addItems(List items, boolean startChecked) { _items.addAll(items); _visibleItems.addAll(items); @@ -97,7 +105,7 @@ public void setCheckedItemsCountTextRes(@PluralsRes int resId) { _selectedCountPlural = resId; } - public Set getCheckedItems() { + public Set getCheckedItems() { return _checkedItems; } @@ -109,7 +117,7 @@ public int getCount() { } @Override - public String getItem(int i) { + public T getItem(int i) { return _visibleItems.get(i); } @@ -124,19 +132,18 @@ public View getView(int i, View convertView, ViewGroup viewGroup) { convertView = LayoutInflater.from(getContext()).inflate(R.layout.dropdown_checkbox, viewGroup, false); } - String item = _visibleItems.get(i); + T item = _visibleItems.get(i); CheckBox checkBox = convertView.findViewById(R.id.checkbox_in_dropdown); - checkBox.setText(item); + checkBox.setText(item.toString()); + checkBox.setTag(item); checkBox.setChecked(_checkedItems.contains(item)); checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { - String label = buttonView.getText().toString(); - if (isChecked) { - _checkedItems.add(label); + _checkedItems.add((T) buttonView.getTag()); } else { - _checkedItems.remove(label); + _checkedItems.remove((T) buttonView.getTag()); } updateCheckedItemsCountText(); @@ -153,9 +160,9 @@ protected FilterResults performFiltering(CharSequence query) { FilterResults results = new FilterResults(); results.values = (query == null || query.toString().isEmpty()) ? _items - : _items.stream().filter(str -> { + : _items.stream().filter(item -> { String q = query.toString().toLowerCase(); - String strLower = str.toLowerCase(); + String strLower = item.toString().toLowerCase(); return strLower.contains(q); }) @@ -166,7 +173,7 @@ protected FilterResults performFiltering(CharSequence query) { @Override protected void publishResults(CharSequence charSequence, FilterResults filterResults) { - _visibleItems = (List) filterResults.values; + _visibleItems = (List) filterResults.values; notifyDataSetChanged(); } }; diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/dialogs/Dialogs.java b/app/src/main/java/com/beemdevelopment/aegis/ui/dialogs/Dialogs.java index 3b9d8ab02..1dd7c873e 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/dialogs/Dialogs.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/dialogs/Dialogs.java @@ -25,6 +25,7 @@ import android.widget.Toast; import androidx.activity.ComponentActivity; +import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; @@ -242,8 +243,8 @@ private static void showTextInputDialog(Context context, @StringRes int titleId, showTextInputDialog(context, titleId, 0, hintId, listener, null, isSecret); } - public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) { - showTextInputDialog(context, titleId, hintId, listener, false); + public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, @Nullable DialogInterface.OnCancelListener onCancel) { + showTextInputDialog(context, titleId, 0, hintId, listener, onCancel, false); } public static void showPasswordInputDialog(Context context, TextInputListener listener) { diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java index 7e15f04fc..43c5da61c 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java @@ -31,12 +31,14 @@ import com.beemdevelopment.aegis.ui.TransferEntriesActivity; import com.beemdevelopment.aegis.ui.components.DropdownCheckBoxes; import com.beemdevelopment.aegis.ui.dialogs.Dialogs; +import com.beemdevelopment.aegis.ui.models.VaultGroupModel; import com.beemdevelopment.aegis.ui.tasks.ExportTask; import com.beemdevelopment.aegis.ui.tasks.ImportFileTask; import com.beemdevelopment.aegis.vault.Vault; import com.beemdevelopment.aegis.vault.VaultBackupManager; import com.beemdevelopment.aegis.vault.VaultEntry; import com.beemdevelopment.aegis.vault.VaultFileCredentials; +import com.beemdevelopment.aegis.vault.VaultGroup; import com.beemdevelopment.aegis.vault.VaultRepository; import com.beemdevelopment.aegis.vault.VaultRepositoryException; import com.beemdevelopment.aegis.vault.slots.PasswordSlot; @@ -48,12 +50,14 @@ import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Random; import java.util.Set; -import java.util.TreeSet; +import java.util.UUID; +import java.util.stream.Collectors; import javax.crypto.Cipher; @@ -164,7 +168,7 @@ private void startExport() { CheckBox checkBoxAccept = view.findViewById(R.id.checkbox_accept); CheckBox checkBoxExportAllGroups = view.findViewById(R.id.export_selected_groups); TextInputLayout groupsSelectionLayout = view.findViewById(R.id.group_selection_layout); - DropdownCheckBoxes groupsSelection = view.findViewById(R.id.group_selection_dropdown); + DropdownCheckBoxes groupsSelection = view.findViewById(R.id.group_selection_dropdown); TextView passwordInfoText = view.findViewById(R.id.text_separate_password); passwordInfoText.setVisibility(checkBoxEncrypt.isChecked() && isBackupPasswordSet ? View.VISIBLE : View.GONE); AutoCompleteTextView dropdown = view.findViewById(R.id.dropdown_export_format); @@ -177,13 +181,13 @@ private void startExport() { passwordInfoText.setVisibility(checkBoxEncrypt.isChecked() && isBackupPasswordSet ? View.VISIBLE : View.GONE); }); - TreeSet groups = _vaultManager.getVault().getGroups(); + Collection groups = _vaultManager.getVault().getUsedGroups(); if (groups.size() > 0) { checkBoxExportAllGroups.setVisibility(View.VISIBLE); - ArrayList groupsArray = new ArrayList<>(); - groupsArray.add(getString(R.string.no_group)); - groupsArray.addAll(groups); + ArrayList groupsArray = new ArrayList<>(); + groupsArray.add(new VaultGroupModel(getString(R.string.no_group))); + groupsArray.addAll(groups.stream().map(VaultGroupModel::new).collect(Collectors.toList())); groupsSelection.setCheckedItemsCountTextRes(R.plurals.export_groups_selected_count); groupsSelection.addItems(groupsArray, false); @@ -319,17 +323,19 @@ private void startExport() { Dialogs.showSecureDialog(dialog); } - private Vault.EntryFilter getVaultEntryFilter(DropdownCheckBoxes dropdownCheckBoxes) { - Set groups = new HashSet<>(); - for (String group: dropdownCheckBoxes.getCheckedItems()) { - if (group.equals(getString(R.string.no_group))) { - groups.add(null); - } else { - groups.add(group); - } + private Vault.EntryFilter getVaultEntryFilter(DropdownCheckBoxes dropdownCheckBoxes) { + Set groups = new HashSet<>(); + for (VaultGroupModel group : dropdownCheckBoxes.getCheckedItems()) { + groups.add(group.getUUID()); } - return groups.isEmpty() ? null : entry -> groups.contains(entry.getGroup()); + return groups.isEmpty() ? null : entry -> { + if (entry.getGroups().isEmpty()) { + return groups.contains(null); + } else { + return entry.getGroups().stream().anyMatch(groups::contains); + } + }; } private void startGoogleAuthenticatorStyleExport() { diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/models/VaultGroupModel.java b/app/src/main/java/com/beemdevelopment/aegis/ui/models/VaultGroupModel.java new file mode 100644 index 000000000..2cdcadca1 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/models/VaultGroupModel.java @@ -0,0 +1,45 @@ +package com.beemdevelopment.aegis.ui.models; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.beemdevelopment.aegis.vault.VaultGroup; +import java.io.Serializable; +import java.util.UUID; + +public class VaultGroupModel implements Serializable { + private final VaultGroup _group; + private final String _placeholderName; + + public VaultGroupModel(VaultGroup group) { + _group = group; + _placeholderName = null; + } + + public VaultGroupModel(String placeholderName) { + _group = null; + _placeholderName = placeholderName; + } + + public VaultGroup getGroup() { + return _group; + } + + public String getName() { + return _group != null ? _group.getName() : _placeholderName; + } + + public boolean isPlaceholder() { + return _group == null; + } + + @Nullable + public UUID getUUID() { + return _group == null ? null : _group.getUUID(); + } + + @NonNull + @Override + public String toString() { + return getName(); + } +} 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 3d9abfede..5697a295a 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 @@ -39,6 +39,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.TreeSet; import java.util.UUID; @@ -59,7 +61,7 @@ public class EntryAdapter extends RecyclerView.Adapter private boolean _tapToReveal; private int _tapToRevealTime; private CopyBehavior _copyBehavior; - private List _groupFilter; + private Set _groupFilter; private SortCategory _sortCategory; private ViewMode _viewMode; private String _searchFilter; @@ -76,7 +78,7 @@ public EntryAdapter(EntryListView view) { _entries = new ArrayList<>(); _shownEntries = new ArrayList<>(); _selectedEntries = new ArrayList<>(); - _groupFilter = new ArrayList<>(); + _groupFilter = new TreeSet<>(); _holders = new ArrayList<>(); _dimHandler = new Handler(); _doubleTapHandler = new Handler(); @@ -246,12 +248,15 @@ private VaultEntry getEntryByUUID(UUID uuid) { } private boolean isEntryFiltered(VaultEntry entry) { - String group = entry.getGroup(); + Set groups = entry.getGroups(); String issuer = entry.getIssuer().toLowerCase(); String name = entry.getName().toLowerCase(); if (!_groupFilter.isEmpty()) { - if (!_groupFilter.contains(group)) { + if (groups.isEmpty() && !_groupFilter.contains(null)) { + return true; + } + if (!groups.isEmpty() && _groupFilter.stream().filter(Objects::nonNull).noneMatch(groups::contains)) { return true; } } @@ -274,7 +279,7 @@ public void refresh(boolean hard) { } } - public void setGroupFilter(@NonNull List groups) { + public void setGroupFilter(@NonNull Set groups) { if (_groupFilter.equals(groups)) { return; } @@ -352,10 +357,6 @@ public int getShownFavoritesCount() { return (int) _shownEntries.stream().filter(VaultEntry::isFavorite).count(); } - public void setGroups(TreeSet groups) { - _view.setGroups(groups); - } - @Override public void onItemDismiss(int position) { 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 4a9183a93..36edc0b6c 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 @@ -37,7 +37,10 @@ import com.beemdevelopment.aegis.otp.TotpInfo; import com.beemdevelopment.aegis.ui.dialogs.Dialogs; import com.beemdevelopment.aegis.ui.glide.IconLoader; +import com.beemdevelopment.aegis.ui.models.VaultGroupModel; +import com.beemdevelopment.aegis.util.UUIDMap; import com.beemdevelopment.aegis.vault.VaultEntry; +import com.beemdevelopment.aegis.vault.VaultGroup; import com.bumptech.glide.Glide; import com.bumptech.glide.ListPreloader; import com.bumptech.glide.RequestBuilder; @@ -54,7 +57,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.TreeSet; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -71,11 +74,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener { private TotpProgressBar _progressBar; private boolean _showProgress; private ViewMode _viewMode; - private TreeSet _groups; + private Collection _groups; private LinearLayout _emptyStateView; private Chip _groupChip; - private List _groupFilter; - private List _prefGroupFilter; + private Set _groupFilter; + private Set _prefGroupFilter; private UiRefresher _refresher; @@ -168,7 +171,7 @@ public void onDestroyView() { super.onDestroyView(); } - public void setGroupFilter(List groups, boolean animate) { + public void setGroupFilter(Set groups, boolean animate) { _groupFilter = groups; _adapter.setGroupFilter(groups); _touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed()); @@ -334,7 +337,7 @@ public void onListChange() { } } - public void setPrefGroupFilter(List groupFilter) { + public void setPrefGroupFilter(Set groupFilter) { _prefGroupFilter = groupFilter; } @@ -465,17 +468,17 @@ public void runEntriesAnimation() { _recyclerView.scheduleLayoutAnimation(); } - private void addChipTo(ChipGroup chipGroup, String group) { + private void addChipTo(ChipGroup chipGroup, VaultGroupModel group) { Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_material, null, false); - chip.setText(group == null ? getString(R.string.no_group) : group); + chip.setText(group.getName()); chip.setCheckable(true); - chip.setChecked(_groupFilter != null && _groupFilter.contains(group)); + chip.setChecked(_groupFilter != null && _groupFilter.contains(group.getUUID())); chip.setCheckedIconVisible(true); chip.setOnCheckedChangeListener((group1, checkedId) -> { - List groupFilter = getGroupFilter(chipGroup); + Set groupFilter = getGroupFilter(chipGroup); setGroupFilter(groupFilter, true); }); - chip.setTag(group == null ? new Object() : null); + chip.setTag(group); chipGroup.addView(chip); } @@ -489,7 +492,7 @@ private void initializeGroupChip() { Button saveButton = view.findViewById(R.id.btnSave); clearButton.setOnClickListener(v -> { chipGroup.clearCheck(); - List groupFilter = Collections.emptyList(); + Set groupFilter = Collections.emptySet(); if (_listener != null) { _listener.onSaveGroupFilter(groupFilter); } @@ -498,7 +501,7 @@ private void initializeGroupChip() { }); saveButton.setOnClickListener(v -> { - List groupFilter = getGroupFilter(chipGroup); + Set groupFilter = getGroupFilter(chipGroup); if (_listener != null) { _listener.onSaveGroupFilter(groupFilter); } @@ -509,25 +512,23 @@ private void initializeGroupChip() { _groupChip.setOnClickListener(v -> { chipGroup.removeAllViews(); - for (String group : _groups) { - addChipTo(chipGroup, group); + for (VaultGroup group : _groups) { + addChipTo(chipGroup, new VaultGroupModel(group)); } - addChipTo(chipGroup, null); + addChipTo(chipGroup, new VaultGroupModel(getString(R.string.no_group))); Dialogs.showSecureDialog(dialog); }); } - private static List getGroupFilter(ChipGroup chipGroup) { + private static Set getGroupFilter(ChipGroup chipGroup) { return chipGroup.getCheckedChipIds().stream() .map(i -> { Chip chip = chipGroup.findViewById(i); - if (chip.getTag() != null) { - return null; - } - return chip. getText().toString(); + VaultGroupModel group = (VaultGroupModel) chip.getTag(); + return group.getUUID(); }) - .collect(Collectors.toList()); + .collect(Collectors.toSet()); } private void updateGroupChip() { @@ -543,29 +544,31 @@ private void setShowProgress(boolean showProgress) { updateDividerDecoration(); } - public void setGroups(TreeSet groups) { + public void setGroups(Collection groups) { _groups = groups; _groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE); updateDividerDecoration(); if (_prefGroupFilter != null) { - List groupFilter = cleanGroupFilter(_prefGroupFilter); + Set groupFilter = cleanGroupFilter(_prefGroupFilter); _prefGroupFilter = null; if (!groupFilter.isEmpty()) { setGroupFilter(groupFilter, false); } } else if (_groupFilter != null) { - List groupFilter = cleanGroupFilter(_groupFilter); + Set groupFilter = cleanGroupFilter(_groupFilter); if (!_groupFilter.equals(groupFilter)) { setGroupFilter(groupFilter, true); } } } - private List cleanGroupFilter(List groupFilter) { - return groupFilter.stream() - .filter(g -> g == null || _groups.contains(g)) - .collect(Collectors.toList()); + private Set cleanGroupFilter(Set groupFilter) { + Set groupUuids = _groups.stream().map(UUIDMap.Value::getUUID).collect(Collectors.toSet()); + + return groupFilter.stream() + .filter(g -> g == null || groupUuids.contains(g)) + .collect(Collectors.toSet()); } private void updateDividerDecoration() { @@ -616,7 +619,7 @@ public interface Listener { void onSelect(VaultEntry entry); void onDeselect(VaultEntry entry); void onListChange(); - void onSaveGroupFilter(List groupFilter); + void onSaveGroupFilter(Set groupFilter); void onEntryListTouch(); } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/views/GroupAdapter.java b/app/src/main/java/com/beemdevelopment/aegis/ui/views/GroupAdapter.java index 3d04e3a97..60f6b4c39 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/views/GroupAdapter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/views/GroupAdapter.java @@ -7,19 +7,20 @@ import androidx.recyclerview.widget.RecyclerView; import com.beemdevelopment.aegis.R; +import com.beemdevelopment.aegis.vault.VaultGroup; import java.util.ArrayList; public class GroupAdapter extends RecyclerView.Adapter { private GroupAdapter.Listener _listener; - private ArrayList _groups; + private ArrayList _groups; public GroupAdapter(GroupAdapter.Listener listener) { _listener = listener; _groups = new ArrayList<>(); } - public void addGroup(String group) { + public void addGroup(VaultGroup group) { _groups.add(group); int position = getItemCount() - 1; @@ -30,7 +31,7 @@ public void addGroup(String group) { } } - public void removeGroup(String group) { + public void removeGroup(VaultGroup group) { int position = _groups.indexOf(group); _groups.remove(position); notifyItemRemoved(position); @@ -57,6 +58,6 @@ public int getItemCount() { } public interface Listener { - void onRemoveGroup(String group); + void onRemoveGroup(VaultGroup group); } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/views/GroupHolder.java b/app/src/main/java/com/beemdevelopment/aegis/ui/views/GroupHolder.java index ed4905003..5068d9bd1 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/views/GroupHolder.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/views/GroupHolder.java @@ -7,6 +7,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.beemdevelopment.aegis.R; +import com.beemdevelopment.aegis.vault.VaultGroup; public class GroupHolder extends RecyclerView.ViewHolder { private TextView _slotName; @@ -18,8 +19,8 @@ public GroupHolder(final View view) { _buttonDelete = view.findViewById(R.id.button_delete); } - public void setData(String groupName) { - _slotName.setText(groupName); + public void setData(VaultGroup group) { + _slotName.setText(group.getName()); } public void setOnDeleteClickListener(View.OnClickListener listener) { diff --git a/app/src/main/java/com/beemdevelopment/aegis/util/UUIDMap.java b/app/src/main/java/com/beemdevelopment/aegis/util/UUIDMap.java index a1aea132a..a8294d816 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/util/UUIDMap.java +++ b/app/src/main/java/com/beemdevelopment/aegis/util/UUIDMap.java @@ -9,6 +9,7 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; +import java.util.Objects; import java.util.UUID; /** @@ -99,7 +100,14 @@ public void move(T value1, T value2) { * Reports whether the internal map contains a value with the UUID of the given value. */ public boolean has(T value) { - return _map.containsKey(value.getUUID()); + return has(value.getUUID()); + } + + /** + * Reports whether the internal map contains a value with the given UUID. + */ + public boolean has(UUID uuid) { + return _map.containsKey(uuid); } /** @@ -161,7 +169,7 @@ public boolean equals(Object o) { return false; } - return getUUID().equals(((Value) o).getUUID()); + return Objects.equals(getUUID(), ((Value) o).getUUID()); } } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/Vault.java b/app/src/main/java/com/beemdevelopment/aegis/vault/Vault.java index 74ea038c0..de7e434db 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/Vault.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/Vault.java @@ -8,9 +8,13 @@ import org.json.JSONException; import org.json.JSONObject; +import java.util.Optional; +import java.util.UUID; + public class Vault { - private static final int VERSION = 2; - private UUIDMap _entries = new UUIDMap<>(); + private static final int VERSION = 3; + private final UUIDMap _entries = new UUIDMap<>(); + private final UUIDMap _groups = new UUIDMap<>(); public JSONObject toJson() { return toJson(null); @@ -18,16 +22,24 @@ public JSONObject toJson() { public JSONObject toJson(@Nullable EntryFilter filter) { try { - JSONArray array = new JSONArray(); + JSONArray entriesArray = new JSONArray(); for (VaultEntry e : _entries) { if (filter == null || filter.includeEntry(e)) { - array.put(e.toJson()); + entriesArray.put(e.toJson()); } } + // Always include all groups, even if they're not assigned to any entry (before or after the entry filter) + JSONArray groupsArray = new JSONArray(); + for (VaultGroup group : _groups) { + groupsArray.put(group.toJson()); + } + JSONObject obj = new JSONObject(); obj.put("version", VERSION); - obj.put("entries", array); + obj.put("entries", entriesArray); + obj.put("groups", groupsArray); + return obj; } catch (JSONException e) { throw new RuntimeException(e); @@ -37,6 +49,7 @@ public JSONObject toJson(@Nullable EntryFilter filter) { public static Vault fromJson(JSONObject obj) throws VaultException { Vault vault = new Vault(); UUIDMap entries = vault.getEntries(); + UUIDMap groups = vault.getGroups(); try { int ver = obj.getInt("version"); @@ -44,9 +57,28 @@ public static Vault fromJson(JSONObject obj) throws VaultException { throw new VaultException("Unsupported version"); } + if (obj.has("groups")) { + JSONArray groupsArray = obj.getJSONArray("groups"); + for (int i = 0; i < groupsArray.length(); i++) { + VaultGroup group = VaultGroup.fromJson(groupsArray.getJSONObject(i)); + if (!groups.has(group)) { + groups.add(group); + } + } + } + JSONArray array = obj.getJSONArray("entries"); for (int i = 0; i < array.length(); i++) { VaultEntry entry = VaultEntry.fromJson(array.getJSONObject(i)); + vault.migrateOldGroup(entry); + + // check the vault has a group corresponding to each one the entry claims to be in + for (UUID groupUuid: entry.getGroups()) { + if (!groups.has(groupUuid)) { + entry.removeGroup(groupUuid); + } + } + entries.add(entry); } } catch (VaultEntryException | JSONException e) { @@ -56,10 +88,33 @@ public static Vault fromJson(JSONObject obj) throws VaultException { return vault; } + public void migrateOldGroup(VaultEntry entry) { + if (entry.getOldGroup() != null) { + Optional optGroup = getGroups().getValues() + .stream() + .filter(g -> g.getName().equals(entry.getOldGroup())) + .findFirst(); + + if (optGroup.isPresent()) { + entry.addGroup(optGroup.get().getUUID()); + } else { + VaultGroup group = new VaultGroup(entry.getOldGroup()); + getGroups().add(group); + entry.addGroup(group.getUUID()); + } + + entry.setOldGroup(null); + } + } + public UUIDMap getEntries() { return _entries; } + public UUIDMap getGroups() { + return _groups; + } + public interface EntryFilter { boolean includeEntry(VaultEntry entry); } diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultEntry.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultEntry.java index 9ad2d70be..302d139ba 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultEntry.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultEntry.java @@ -10,23 +10,26 @@ import com.beemdevelopment.aegis.util.JsonUtils; import com.beemdevelopment.aegis.util.UUIDMap; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.Arrays; -import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; import java.util.UUID; public class VaultEntry extends UUIDMap.Value { private String _name = ""; private String _issuer = ""; - private String _group; private OtpInfo _info; private byte[] _icon; private IconType _iconType = IconType.INVALID; private boolean _isFavorite; private int _usageCount; private String _note = ""; + private String _oldGroup; + private Set _groups = new TreeSet<>(); private VaultEntry(UUID uuid, OtpInfo info) { super(uuid); @@ -44,13 +47,6 @@ public VaultEntry(OtpInfo info, String name, String issuer) { setIssuer(issuer); } - public VaultEntry(OtpInfo info, String name, String issuer, String group) { - this(info); - setName(name); - setIssuer(issuer); - setGroup(group); - } - public VaultEntry(GoogleAuthInfo info) { this(info.getOtpInfo(), info.getAccountName(), info.getIssuer()); } @@ -63,12 +59,18 @@ public JSONObject toJson() { obj.put("uuid", getUUID().toString()); obj.put("name", _name); obj.put("issuer", _issuer); - obj.put("group", _group); obj.put("note", _note); obj.put("favorite", _isFavorite); obj.put("icon", _icon == null ? JSONObject.NULL : Base64.encode(_icon)); obj.put("icon_mime", _icon == null ? null : _iconType.toMimeType()); obj.put("info", _info.toJson()); + + JSONArray groupUuids = new JSONArray(); + for (UUID uuid : _groups) { + groupUuids.put(uuid.toString()); + } + obj.put("groups", groupUuids); + } catch (JSONException e) { throw new RuntimeException(e); } @@ -90,10 +92,21 @@ public static VaultEntry fromJson(JSONObject obj) throws VaultEntryException { VaultEntry entry = new VaultEntry(uuid, info); entry.setName(obj.getString("name")); entry.setIssuer(obj.getString("issuer")); - entry.setGroup(obj.optString("group", null)); entry.setNote(obj.optString("note", "")); entry.setIsFavorite(obj.optBoolean("favorite", false)); + // If the entry contains a list of group UUID's, assume conversion from the + // old group system has already taken place and ignore the old group field. + if (obj.has("groups")) { + JSONArray groups = obj.getJSONArray("groups"); + for (int i = 0; i < groups.length(); i++) { + String groupUuid = groups.getString(i); + entry.addGroup(UUID.fromString(groupUuid)); + } + } else if (obj.has("group")) { + entry.setOldGroup(obj.getString("group")); + } + Object icon = obj.get("icon"); if (icon != JSONObject.NULL) { String mime = JsonUtils.optString(obj, "icon_mime"); @@ -121,8 +134,8 @@ public String getIssuer() { return _issuer; } - public String getGroup() { - return _group; + public Set getGroups() { + return _groups; } public byte[] getIcon() { @@ -153,8 +166,22 @@ public void setIssuer(String issuer) { _issuer = issuer; } - public void setGroup(String group) { - _group = group; + public void addGroup(UUID group) { + if (group == null) { + throw new AssertionError("Attempt to add null group to entry's group list"); + } + _groups.add(group); + } + + public void removeGroup(UUID group) { + _groups.remove(group); + } + + public void setGroups(Set groups) { + if (groups.contains(null)) { + throw new AssertionError("Attempt to add null group to entry's group list"); + } + _groups = groups; } public void setInfo(OtpInfo info) { @@ -176,6 +203,14 @@ public boolean hasIcon() { public void setIsFavorite(boolean isFavorite) { _isFavorite = isFavorite; } + void setOldGroup(String oldGroup) { + _oldGroup = oldGroup; + } + + String getOldGroup() { + return _oldGroup; + } + @Override public boolean equals(Object o) { if (!(o instanceof VaultEntry)) { @@ -194,12 +229,12 @@ public boolean equals(Object o) { public boolean equivalates(VaultEntry entry) { return getName().equals(entry.getName()) && getIssuer().equals(entry.getIssuer()) - && Objects.equals(getGroup(), entry.getGroup()) && getInfo().equals(entry.getInfo()) && Arrays.equals(getIcon(), entry.getIcon()) && getIconType().equals(entry.getIconType()) && getNote().equals(entry.getNote()) - && isFavorite() == entry.isFavorite(); + && isFavorite() == entry.isFavorite() + && getGroups().equals(entry.getGroups()); } /** diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultGroup.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultGroup.java new file mode 100644 index 000000000..263b4eb7c --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultGroup.java @@ -0,0 +1,69 @@ +package com.beemdevelopment.aegis.vault; + +import com.beemdevelopment.aegis.util.UUIDMap; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.UUID; + +public class VaultGroup extends UUIDMap.Value { + private String _name; + + private VaultGroup(UUID uuid, String name) { + super(uuid); + _name = name; + } + + public VaultGroup(String name) { + super(); + _name = name; + } + + public JSONObject toJson() { + JSONObject obj = new JSONObject(); + + try { + obj.put("uuid", getUUID().toString()); + obj.put("name", _name); + } catch (JSONException e) { + throw new RuntimeException(e); + } + + return obj; + } + + public static VaultGroup fromJson(JSONObject obj) throws VaultEntryException { + try { + UUID uuid = UUID.fromString(obj.getString("uuid")); + String groupName = obj.getString("name"); + + return new VaultGroup(uuid, groupName); + } catch (JSONException e) { + throw new VaultEntryException(e); + } + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof VaultGroup)) { + return false; + } + + VaultGroup entry = (VaultGroup) o; + return super.equals(entry) && getName().equals(entry.getName()); + } + + public String getName() { + return _name; + } + + public void setName(String name) { + _name = name; + } + + @Override + public String toString() { + return _name; + } +} 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 94571e559..4e74023aa 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java @@ -20,9 +20,9 @@ import java.io.OutputStream; import java.io.PrintStream; import java.nio.charset.StandardCharsets; -import java.text.Collator; import java.util.Collection; -import java.util.TreeSet; +import java.util.HashSet; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -220,6 +220,8 @@ public void exportHtml(OutputStream outStream, @Nullable Vault.EntryFilter filte } public void addEntry(VaultEntry entry) { + // Entries added by importing a file may contain an old group that needs to be migrated + _vault.migrateOldGroup(entry); _vault.getEntries().add(entry); } @@ -231,8 +233,12 @@ public VaultEntry removeEntry(VaultEntry entry) { return _vault.getEntries().remove(entry); } - public void wipeEntries() { + /** + * Wipes all entries and groups from the vault. + */ + public void wipeContents() { _vault.getEntries().wipe(); + _vault.getGroups().wipe(); } public VaultEntry replaceEntry(VaultEntry entry) { @@ -240,8 +246,8 @@ public VaultEntry replaceEntry(VaultEntry entry) { } /** - * Moves entry1 to the position of entry2. - */ + * Moves entry1 to the position of entry2. + */ public void moveEntry(VaultEntry entry1, VaultEntry entry2) { _vault.getEntries().move(entry1, entry2); } @@ -254,15 +260,54 @@ public Collection getEntries() { return _vault.getEntries().getValues(); } - public TreeSet getGroups() { - TreeSet groups = new TreeSet<>(Collator.getInstance()); + public void addGroup(VaultGroup group) { + _vault.getGroups().add(group); + } + + public VaultGroup getGroupByUUID(UUID uuid) { + return _vault.getGroups().getByUUID(uuid); + } + + @Nullable + public VaultGroup findGroupByUUID(UUID uuid) { + return _vault.getGroups().has(uuid) ? _vault.getGroups().getByUUID(uuid) : null; + } + + @Nullable + public VaultGroup findGroupByName(String name) { + return _vault.getGroups().getValues() + .stream() + .filter(g -> g.getName().equals(name)) + .findFirst() + .orElse(null); + } + + public void removeGroup(UUID groupUuid) { + VaultGroup group = _vault.getGroups().getByUUID(groupUuid); + removeGroup(group); + } + + public void removeGroup(VaultGroup group) { for (VaultEntry entry : getEntries()) { - String group = entry.getGroup(); - if (group != null) { - groups.add(group); - } + entry.removeGroup(group.getUUID()); + } + + _vault.getGroups().remove(group); + } + + public Collection getGroups() { + return _vault.getGroups().getValues(); + } + + public Collection getUsedGroups() { + Set usedGroups = new HashSet<>(); + for (VaultEntry entry : getEntries()) { + usedGroups.addAll(entry.getGroups()); } - return groups; + + return getGroups().stream() + .filter(vg -> usedGroups.contains(vg.getUUID())) + .collect(Collectors.toList()); } public VaultFileCredentials getCredentials() { diff --git a/app/src/main/res/menu/menu_groups.xml b/app/src/main/res/menu/menu_groups.xml index 5e614e22c..d0e52da29 100644 --- a/app/src/main/res/menu/menu_groups.xml +++ b/app/src/main/res/menu/menu_groups.xml @@ -3,6 +3,10 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" tools:context="com.beemdevelopment.aegis.ui.GroupManagerActivity"> + Unlocking the vault Remove group Are you sure you want to remove this group? Entries in this group will automatically switch to \'No group\'. + Delete unused groups + Are you sure you want to delete all groups that are not assigned to an entry? Remove icon pack Are you sure you want to remove this icon pack? Entries that use icons from this pack will not be affected. Details