mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-05-04 20:30:36 +00:00
Refer to groups by UUID
- Also lays the foundations for adding entries to multiple groups and changing group names Co-authored-by: Alexander Bakker <ab@alexbakker.me>
This commit is contained in:
parent
b84ecf15da
commit
5c86e5c099
24 changed files with 782 additions and 197 deletions
|
@ -20,9 +20,11 @@ import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public class Preferences {
|
public class Preferences {
|
||||||
|
@ -478,26 +480,26 @@ public class Preferences {
|
||||||
return _prefs.getBoolean("pref_minimize_on_copy", false);
|
return _prefs.getBoolean("pref_minimize_on_copy", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setGroupFilter(List<String> groupFilter) {
|
public void setGroupFilter(Set<UUID> groupFilter) {
|
||||||
JSONArray json = new JSONArray(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<String> getGroupFilter() {
|
public Set<UUID> getGroupFilter() {
|
||||||
String raw = _prefs.getString("pref_group_filter", null);
|
String raw = _prefs.getString("pref_group_filter_uuids", null);
|
||||||
if (raw == null || raw.isEmpty()) {
|
if (raw == null || raw.isEmpty()) {
|
||||||
return Collections.emptyList();
|
return Collections.emptySet();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
JSONArray json = new JSONArray(raw);
|
JSONArray json = new JSONArray(raw);
|
||||||
List<String> filter = new ArrayList<>();
|
Set<UUID> filter = new HashSet<>();
|
||||||
for (int i = 0; i < json.length(); i++) {
|
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;
|
return filter;
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
return Collections.emptyList();
|
return Collections.emptySet();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import com.beemdevelopment.aegis.vault.VaultEntryException;
|
||||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||||
import com.beemdevelopment.aegis.vault.VaultFileException;
|
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.PasswordSlot;
|
||||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||||
import com.topjohnwu.superuser.io.SuFile;
|
import com.topjohnwu.superuser.io.SuFile;
|
||||||
|
@ -27,6 +28,7 @@ import org.json.JSONObject;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public class AegisImporter extends DatabaseImporter {
|
public class AegisImporter extends DatabaseImporter {
|
||||||
|
|
||||||
|
@ -132,11 +134,31 @@ public class AegisImporter extends DatabaseImporter {
|
||||||
Result result = new Result();
|
Result result = new Result();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
JSONArray array = _obj.getJSONArray("entries");
|
if (_obj.has("groups")) {
|
||||||
for (int i = 0; i < array.length(); i++) {
|
JSONArray groupArray = _obj.getJSONArray("groups");
|
||||||
JSONObject entryObj = array.getJSONObject(i);
|
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 {
|
try {
|
||||||
VaultEntry entry = convertEntry(entryObj);
|
VaultEntry entry = convertEntry(entryObj);
|
||||||
|
for (UUID groupUuid : entry.getGroups()) {
|
||||||
|
if (!result.getGroups().has(groupUuid)) {
|
||||||
|
entry.getGroups().remove(groupUuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
result.addEntry(entry);
|
result.addEntry(entry);
|
||||||
} catch (DatabaseImporterEntryException e) {
|
} catch (DatabaseImporterEntryException e) {
|
||||||
result.addError(e);
|
result.addError(e);
|
||||||
|
@ -156,5 +178,13 @@ public class AegisImporter extends DatabaseImporter {
|
||||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import androidx.annotation.StringRes;
|
||||||
import com.beemdevelopment.aegis.R;
|
import com.beemdevelopment.aegis.R;
|
||||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||||
|
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||||
import com.topjohnwu.superuser.Shell;
|
import com.topjohnwu.superuser.Shell;
|
||||||
import com.topjohnwu.superuser.io.SuFile;
|
import com.topjohnwu.superuser.io.SuFile;
|
||||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||||
|
@ -168,12 +169,17 @@ public abstract class DatabaseImporter {
|
||||||
|
|
||||||
public static class Result {
|
public static class Result {
|
||||||
private UUIDMap<VaultEntry> _entries = new UUIDMap<>();
|
private UUIDMap<VaultEntry> _entries = new UUIDMap<>();
|
||||||
|
private UUIDMap<VaultGroup> _groups = new UUIDMap<>();
|
||||||
private List<DatabaseImporterEntryException> _errors = new ArrayList<>();
|
private List<DatabaseImporterEntryException> _errors = new ArrayList<>();
|
||||||
|
|
||||||
public void addEntry(VaultEntry entry) {
|
public void addEntry(VaultEntry entry) {
|
||||||
_entries.add(entry);
|
_entries.add(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addGroup(VaultGroup group) {
|
||||||
|
_groups.add(group);
|
||||||
|
}
|
||||||
|
|
||||||
public void addError(DatabaseImporterEntryException error) {
|
public void addError(DatabaseImporterEntryException error) {
|
||||||
_errors.add(error);
|
_errors.add(error);
|
||||||
}
|
}
|
||||||
|
@ -182,6 +188,10 @@ public abstract class DatabaseImporter {
|
||||||
return _entries;
|
return _entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UUIDMap<VaultGroup> getGroups() {
|
||||||
|
return _groups;
|
||||||
|
}
|
||||||
|
|
||||||
public List<DatabaseImporterEntryException> getErrors() {
|
public List<DatabaseImporterEntryException> getErrors() {
|
||||||
return _errors;
|
return _errors;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package com.beemdevelopment.aegis.ui;
|
package com.beemdevelopment.aegis.ui;
|
||||||
|
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
@ -15,7 +15,6 @@ import android.view.ViewGroup;
|
||||||
import android.view.animation.AccelerateInterpolator;
|
import android.view.animation.AccelerateInterpolator;
|
||||||
import android.view.animation.AlphaAnimation;
|
import android.view.animation.AlphaAnimation;
|
||||||
import android.view.animation.Animation;
|
import android.view.animation.Animation;
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.AutoCompleteTextView;
|
import android.widget.AutoCompleteTextView;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
@ -54,11 +53,13 @@ import com.beemdevelopment.aegis.otp.YandexInfo;
|
||||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||||
import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog;
|
import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog;
|
||||||
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
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.tasks.ImportFileTask;
|
||||||
import com.beemdevelopment.aegis.ui.views.IconAdapter;
|
import com.beemdevelopment.aegis.ui.views.IconAdapter;
|
||||||
import com.beemdevelopment.aegis.util.Cloner;
|
import com.beemdevelopment.aegis.util.Cloner;
|
||||||
import com.beemdevelopment.aegis.util.IOUtils;
|
import com.beemdevelopment.aegis.util.IOUtils;
|
||||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||||
|
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
|
@ -73,11 +74,14 @@ import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.TreeSet;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
@ -90,7 +94,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
private boolean _isNew = false;
|
private boolean _isNew = false;
|
||||||
private boolean _isManual = false;
|
private boolean _isManual = false;
|
||||||
private VaultEntry _origEntry;
|
private VaultEntry _origEntry;
|
||||||
private TreeSet<String> _groups;
|
private Collection<VaultGroup> _groups;
|
||||||
private boolean _hasCustomIcon = false;
|
private boolean _hasCustomIcon = false;
|
||||||
// keep track of icon changes separately as the generated jpeg's are not deterministic
|
// keep track of icon changes separately as the generated jpeg's are not deterministic
|
||||||
private boolean _hasChangedIcon = false;
|
private boolean _hasChangedIcon = false;
|
||||||
|
@ -114,7 +118,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
private AutoCompleteTextView _dropdownAlgo;
|
private AutoCompleteTextView _dropdownAlgo;
|
||||||
private TextInputLayout _dropdownAlgoLayout;
|
private TextInputLayout _dropdownAlgoLayout;
|
||||||
private AutoCompleteTextView _dropdownGroup;
|
private AutoCompleteTextView _dropdownGroup;
|
||||||
private List<String> _dropdownGroupList = new ArrayList<>();
|
private List<VaultGroupModel> _dropdownGroupList = new ArrayList<>();
|
||||||
|
|
||||||
private KropView _kropView;
|
private KropView _kropView;
|
||||||
|
|
||||||
|
@ -262,8 +266,13 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
updateAdvancedFieldStatus(_origEntry.getInfo().getTypeId());
|
updateAdvancedFieldStatus(_origEntry.getInfo().getTypeId());
|
||||||
updatePinFieldVisibility(_origEntry.getInfo().getTypeId());
|
updatePinFieldVisibility(_origEntry.getInfo().getTypeId());
|
||||||
|
|
||||||
String group = _origEntry.getGroup();
|
Set<UUID> groups = _origEntry.getGroups();
|
||||||
setGroup(group);
|
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
|
// Update the icon if the issuer or name has changed
|
||||||
_textIssuer.addTextChangedListener(_nameChangeListener);
|
_textIssuer.addTextChangedListener(_nameChangeListener);
|
||||||
|
@ -327,24 +336,31 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
startIconSelection();
|
startIconSelection();
|
||||||
});
|
});
|
||||||
|
|
||||||
_dropdownGroup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
_dropdownGroup.setOnItemClickListener((parent, view, position, id) -> {
|
||||||
private int prevPosition = _dropdownGroupList.indexOf(_dropdownGroup.getText().toString());
|
VaultGroupModel selectedGroup = _dropdownGroupList.get(position);
|
||||||
|
if (selectedGroup.isPlaceholder() && Objects.equals(selectedGroup.getName(), getString(R.string.new_group))) {
|
||||||
@Override
|
Dialogs.TextInputListener onAddGroup = text -> {
|
||||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
String groupName = new String(text).trim();
|
||||||
if (position == _dropdownGroupList.size() - 1) {
|
if (!groupName.isEmpty()) {
|
||||||
Dialogs.showTextInputDialog(EditEntryActivity.this, R.string.set_group, R.string.group_name_hint, text -> {
|
VaultGroup group = _vaultManager.getVault().findGroupByName(groupName);
|
||||||
String groupName = new String(text);
|
if (group == null) {
|
||||||
if (!groupName.isEmpty()) {
|
group = new VaultGroup(groupName);
|
||||||
_groups.add(groupName);
|
_vaultManager.getVault().addGroup(group);
|
||||||
updateGroupDropdownList();
|
|
||||||
_dropdownGroup.setText(groupName, false);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
_dropdownGroup.setText(_dropdownGroupList.get(prevPosition), false);
|
updateGroupDropdownList();
|
||||||
} else {
|
setGroup(new VaultGroupModel(group));
|
||||||
prevPosition = position;
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
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 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
_textPin.setHint(otpType.equals(MotpInfo.ID) ? R.string.motp_pin : R.string.yandex_pin);
|
_textPin.setHint(otpType.equals(MotpInfo.ID) ? R.string.motp_pin : R.string.yandex_pin);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setGroup(String groupName) {
|
private void setGroup(VaultGroupModel group) {
|
||||||
int pos = 0;
|
_dropdownGroup.setText(group.getName(), false);
|
||||||
if (groupName != null) {
|
_dropdownGroup.setTag(group);
|
||||||
pos = _groups.contains(groupName) ? _groups.headSet(groupName).size() + 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
_dropdownGroup.setText(_dropdownGroupList.get(pos), false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openAdvancedSettings() {
|
private void openAdvancedSettings() {
|
||||||
|
@ -395,11 +407,10 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateGroupDropdownList() {
|
private void updateGroupDropdownList() {
|
||||||
Resources res = getResources();
|
|
||||||
_dropdownGroupList.clear();
|
_dropdownGroupList.clear();
|
||||||
_dropdownGroupList.add(res.getString(R.string.no_group));
|
_dropdownGroupList.add(new VaultGroupModel(getString(R.string.new_group)));
|
||||||
_dropdownGroupList.addAll(_groups);
|
_dropdownGroupList.addAll(_groups.stream().map(VaultGroupModel::new).collect(Collectors.toList()));
|
||||||
_dropdownGroupList.add(res.getString(R.string.new_group));
|
_dropdownGroupList.add(new VaultGroupModel(getString(R.string.no_group)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasUnsavedChanges(VaultEntry newEntry) {
|
private boolean hasUnsavedChanges(VaultEntry newEntry) {
|
||||||
|
@ -726,12 +737,13 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
entry.setName(_textName.getText().toString());
|
entry.setName(_textName.getText().toString());
|
||||||
entry.setNote(_textNote.getText().toString());
|
entry.setNote(_textNote.getText().toString());
|
||||||
|
|
||||||
int groupPos = _dropdownGroupList.indexOf(_dropdownGroup.getText().toString());
|
VaultGroupModel group = (VaultGroupModel) _dropdownGroup.getTag();
|
||||||
if (groupPos != 0) {
|
if (group.isPlaceholder()) {
|
||||||
String group = _dropdownGroupList.get(groupPos);
|
entry.setGroups(new HashSet<>());
|
||||||
entry.setGroup(group);
|
|
||||||
} else {
|
} else {
|
||||||
entry.setGroup(null);
|
Set<UUID> groups = new HashSet<>();
|
||||||
|
groups.add(group.getUUID());
|
||||||
|
entry.setGroups(groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_hasChangedIcon) {
|
if (_hasChangedIcon) {
|
||||||
|
|
|
@ -14,16 +14,17 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||||
import com.beemdevelopment.aegis.R;
|
import com.beemdevelopment.aegis.R;
|
||||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||||
import com.beemdevelopment.aegis.ui.views.GroupAdapter;
|
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.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public class GroupManagerActivity extends AegisActivity implements GroupAdapter.Listener {
|
public class GroupManagerActivity extends AegisActivity implements GroupAdapter.Listener {
|
||||||
private GroupAdapter _adapter;
|
private GroupAdapter _adapter;
|
||||||
private HashSet<String> _removedGroups;
|
private HashSet<UUID> _removedGroups;
|
||||||
private RecyclerView _groupsView;
|
private RecyclerView _groupsView;
|
||||||
private View _emptyStateView;
|
private View _emptyStateView;
|
||||||
private BackPressHandler _backPressHandler;
|
private BackPressHandler _backPressHandler;
|
||||||
|
@ -43,11 +44,14 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||||
_backPressHandler = new BackPressHandler();
|
_backPressHandler = new BackPressHandler();
|
||||||
getOnBackPressedDispatcher().addCallback(this, _backPressHandler);
|
getOnBackPressedDispatcher().addCallback(this, _backPressHandler);
|
||||||
|
|
||||||
|
_removedGroups = new HashSet<>();
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
List<String> groups = savedInstanceState.getStringArrayList("removedGroups");
|
List<String> groups = savedInstanceState.getStringArrayList("removedGroups");
|
||||||
_removedGroups = new HashSet<>(Objects.requireNonNull(groups));
|
if (groups != null) {
|
||||||
} else {
|
for (String uuid : groups) {
|
||||||
_removedGroups = new HashSet<>();
|
_removedGroups.add(UUID.fromString(uuid));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_adapter = new GroupAdapter(this);
|
_adapter = new GroupAdapter(this);
|
||||||
|
@ -57,8 +61,10 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||||
_groupsView.setAdapter(_adapter);
|
_groupsView.setAdapter(_adapter);
|
||||||
_groupsView.setNestedScrollingEnabled(false);
|
_groupsView.setNestedScrollingEnabled(false);
|
||||||
|
|
||||||
for (String group : _vaultManager.getVault().getGroups()) {
|
for (VaultGroup group : _vaultManager.getVault().getGroups()) {
|
||||||
_adapter.addGroup(group);
|
if (!_removedGroups.contains(group.getUUID())) {
|
||||||
|
_adapter.addGroup(group);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_emptyStateView = findViewById(R.id.vEmptyList);
|
_emptyStateView = findViewById(R.id.vEmptyList);
|
||||||
|
@ -68,16 +74,21 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
outState.putStringArrayList("removedGroups", new ArrayList<>(_removedGroups));
|
ArrayList<String> removed = new ArrayList<>();
|
||||||
|
for (UUID uuid : _removedGroups) {
|
||||||
|
removed.add(uuid.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
outState.putStringArrayList("removedGroups", removed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRemoveGroup(String group) {
|
public void onRemoveGroup(VaultGroup group) {
|
||||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||||
.setTitle(R.string.remove_group)
|
.setTitle(R.string.remove_group)
|
||||||
.setMessage(R.string.remove_group_description)
|
.setMessage(R.string.remove_group_description)
|
||||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||||
_removedGroups.add(group);
|
_removedGroups.add(group.getUUID());
|
||||||
_adapter.removeGroup(group);
|
_adapter.removeGroup(group);
|
||||||
_backPressHandler.setEnabled(true);
|
_backPressHandler.setEnabled(true);
|
||||||
updateEmptyState();
|
updateEmptyState();
|
||||||
|
@ -86,12 +97,29 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||||
.create());
|
.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<VaultGroup> 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() {
|
private void saveAndFinish() {
|
||||||
if (!_removedGroups.isEmpty()) {
|
if (!_removedGroups.isEmpty()) {
|
||||||
for (VaultEntry entry : _vaultManager.getVault().getEntries()) {
|
for (UUID uuid : _removedGroups) {
|
||||||
if (_removedGroups.contains(entry.getGroup())) {
|
_vaultManager.getVault().removeGroup(uuid);
|
||||||
entry.setGroup(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
saveAndBackupVault();
|
saveAndBackupVault();
|
||||||
|
@ -126,6 +154,9 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||||
case R.id.action_save:
|
case R.id.action_save:
|
||||||
saveAndFinish();
|
saveAndFinish();
|
||||||
break;
|
break;
|
||||||
|
case R.id.action_delete_unused_groups:
|
||||||
|
onRemoveUnusedGroups();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import com.beemdevelopment.aegis.ui.tasks.RootShellTask;
|
||||||
import com.beemdevelopment.aegis.ui.views.ImportEntriesAdapter;
|
import com.beemdevelopment.aegis.ui.views.ImportEntriesAdapter;
|
||||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||||
|
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
@ -37,6 +38,7 @@ import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public class ImportEntriesActivity extends AegisActivity {
|
public class ImportEntriesActivity extends AegisActivity {
|
||||||
|
@ -45,6 +47,8 @@ public class ImportEntriesActivity extends AegisActivity {
|
||||||
private ImportEntriesAdapter _adapter;
|
private ImportEntriesAdapter _adapter;
|
||||||
private FabScrollHelper _fabScrollHelper;
|
private FabScrollHelper _fabScrollHelper;
|
||||||
|
|
||||||
|
private UUIDMap<VaultGroup> _importedGroups;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
@ -205,6 +209,8 @@ public class ImportEntriesActivity extends AegisActivity {
|
||||||
importEntries.add(importEntry);
|
importEntries.add(importEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_importedGroups = result.getGroups();
|
||||||
|
|
||||||
List<DatabaseImporterEntryException> errors = result.getErrors();
|
List<DatabaseImporterEntryException> errors = result.getErrors();
|
||||||
if (errors.size() > 0) {
|
if (errors.size() > 0) {
|
||||||
String message = getResources().getQuantityString(R.plurals.import_error_dialog, errors.size(), errors.size());
|
String message = getResources().getQuantityString(R.plurals.import_error_dialog, errors.size(), errors.size());
|
||||||
|
@ -225,10 +231,43 @@ public class ImportEntriesActivity extends AegisActivity {
|
||||||
private void saveAndFinish(boolean wipeEntries) {
|
private void saveAndFinish(boolean wipeEntries) {
|
||||||
VaultRepository vault = _vaultManager.getVault();
|
VaultRepository vault = _vaultManager.getVault();
|
||||||
if (wipeEntries) {
|
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<ImportEntry> selectedEntries = _adapter.getCheckedEntries();
|
List<ImportEntry> selectedEntries = _adapter.getCheckedEntries();
|
||||||
|
List<UUID> 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<UUID> entryGroups = entry.getEntry().getGroups();
|
||||||
|
if (entryGroups.contains(importedGroup.getUUID())) {
|
||||||
|
entryGroups.remove(importedGroup.getUUID());
|
||||||
|
entryGroups.add(existingGroup.getUUID());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (ImportEntry selectedEntry : selectedEntries) {
|
for (ImportEntry selectedEntry : selectedEntries) {
|
||||||
VaultEntry entry = selectedEntry.getEntry();
|
VaultEntry entry = selectedEntry.getEntry();
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.TreeSet;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@ -632,7 +632,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
startAuthActivity(false);
|
startAuthActivity(false);
|
||||||
} else if (_loaded) {
|
} else if (_loaded) {
|
||||||
// update the list of groups in the entry list view so that the chip gets updated
|
// 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
|
// update the usage counts in case they are edited outside of the EntryListView
|
||||||
_entryListView.setUsageCounts(_prefs.getUsageCounts());
|
_entryListView.setUsageCounts(_prefs.getUsageCounts());
|
||||||
|
@ -670,7 +670,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
|
|
||||||
updateLockIcon();
|
updateLockIcon();
|
||||||
if (_loaded) {
|
if (_loaded) {
|
||||||
_entryListView.setGroups(_vaultManager.getVault().getGroups());
|
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
|
||||||
updateSortCategoryMenu();
|
updateSortCategoryMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -980,7 +980,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
public void onListChange() { _fabScrollHelper.setVisible(true); }
|
public void onListChange() { _fabScrollHelper.setVisible(true); }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveGroupFilter(List<String> groupFilter) {
|
public void onSaveGroupFilter(Set<UUID> groupFilter) {
|
||||||
_prefs.setGroupFilter(groupFilter);
|
_prefs.setGroupFilter(groupFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1128,17 +1128,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
case R.id.action_delete:
|
case R.id.action_delete:
|
||||||
Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries, (d, which) -> {
|
Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries, (d, which) -> {
|
||||||
deleteEntries(_selectedEntries);
|
deleteEntries(_selectedEntries);
|
||||||
|
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
|
||||||
for (VaultEntry entry : _selectedEntries) {
|
|
||||||
if (entry.getGroup() != null) {
|
|
||||||
TreeSet<String> groups = _vaultManager.getVault().getGroups();
|
|
||||||
if (!groups.contains(entry.getGroup())) {
|
|
||||||
_entryListView.setGroups(groups);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mode.finish();
|
mode.finish();
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -18,19 +18,19 @@ import androidx.appcompat.widget.AppCompatAutoCompleteTextView;
|
||||||
import com.beemdevelopment.aegis.R;
|
import com.beemdevelopment.aegis.R;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
public class DropdownCheckBoxes<T> extends AppCompatAutoCompleteTextView {
|
||||||
private @PluralsRes int _selectedCountPlural = R.plurals.dropdown_checkboxes_default_count;
|
private @PluralsRes int _selectedCountPlural = R.plurals.dropdown_checkboxes_default_count;
|
||||||
|
|
||||||
private boolean _allowFiltering = false;
|
private boolean _allowFiltering = false;
|
||||||
|
|
||||||
private final List<String> _items = new ArrayList<>();
|
private final List<T> _items = new ArrayList<>();
|
||||||
private List<String> _visibleItems = new ArrayList<>();
|
private List<T> _visibleItems = new ArrayList<>();
|
||||||
private final Set<String> _checkedItems = new TreeSet<>();
|
private final Set<T> _checkedItems = new HashSet<>();
|
||||||
|
|
||||||
private CheckboxAdapter _adapter;
|
private CheckboxAdapter _adapter;
|
||||||
|
|
||||||
|
@ -70,7 +70,15 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addItems(List<String> 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<T> items, boolean startChecked) {
|
||||||
_items.addAll(items);
|
_items.addAll(items);
|
||||||
_visibleItems.addAll(items);
|
_visibleItems.addAll(items);
|
||||||
|
|
||||||
|
@ -97,7 +105,7 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
||||||
_selectedCountPlural = resId;
|
_selectedCountPlural = resId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<String> getCheckedItems() {
|
public Set<T> getCheckedItems() {
|
||||||
return _checkedItems;
|
return _checkedItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +117,7 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getItem(int i) {
|
public T getItem(int i) {
|
||||||
return _visibleItems.get(i);
|
return _visibleItems.get(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,19 +132,18 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
||||||
convertView = LayoutInflater.from(getContext()).inflate(R.layout.dropdown_checkbox, viewGroup, false);
|
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 checkBox = convertView.findViewById(R.id.checkbox_in_dropdown);
|
||||||
checkBox.setText(item);
|
checkBox.setText(item.toString());
|
||||||
|
checkBox.setTag(item);
|
||||||
checkBox.setChecked(_checkedItems.contains(item));
|
checkBox.setChecked(_checkedItems.contains(item));
|
||||||
|
|
||||||
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||||
String label = buttonView.getText().toString();
|
|
||||||
|
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
_checkedItems.add(label);
|
_checkedItems.add((T) buttonView.getTag());
|
||||||
} else {
|
} else {
|
||||||
_checkedItems.remove(label);
|
_checkedItems.remove((T) buttonView.getTag());
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCheckedItemsCountText();
|
updateCheckedItemsCountText();
|
||||||
|
@ -153,9 +160,9 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
||||||
FilterResults results = new FilterResults();
|
FilterResults results = new FilterResults();
|
||||||
results.values = (query == null || query.toString().isEmpty())
|
results.values = (query == null || query.toString().isEmpty())
|
||||||
? _items
|
? _items
|
||||||
: _items.stream().filter(str -> {
|
: _items.stream().filter(item -> {
|
||||||
String q = query.toString().toLowerCase();
|
String q = query.toString().toLowerCase();
|
||||||
String strLower = str.toLowerCase();
|
String strLower = item.toString().toLowerCase();
|
||||||
|
|
||||||
return strLower.contains(q);
|
return strLower.contains(q);
|
||||||
})
|
})
|
||||||
|
@ -166,7 +173,7 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
|
protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
|
||||||
_visibleItems = (List<String>) filterResults.values;
|
_visibleItems = (List<T>) filterResults.values;
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,6 +25,7 @@ import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity;
|
import androidx.activity.ComponentActivity;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
|
||||||
|
@ -242,8 +243,8 @@ public class Dialogs {
|
||||||
showTextInputDialog(context, titleId, 0, hintId, listener, null, isSecret);
|
showTextInputDialog(context, titleId, 0, hintId, listener, null, isSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) {
|
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, @Nullable DialogInterface.OnCancelListener onCancel) {
|
||||||
showTextInputDialog(context, titleId, hintId, listener, false);
|
showTextInputDialog(context, titleId, 0, hintId, listener, onCancel, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void showPasswordInputDialog(Context context, TextInputListener listener) {
|
public static void showPasswordInputDialog(Context context, TextInputListener listener) {
|
||||||
|
|
|
@ -31,12 +31,14 @@ import com.beemdevelopment.aegis.ui.ImportEntriesActivity;
|
||||||
import com.beemdevelopment.aegis.ui.TransferEntriesActivity;
|
import com.beemdevelopment.aegis.ui.TransferEntriesActivity;
|
||||||
import com.beemdevelopment.aegis.ui.components.DropdownCheckBoxes;
|
import com.beemdevelopment.aegis.ui.components.DropdownCheckBoxes;
|
||||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
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.ExportTask;
|
||||||
import com.beemdevelopment.aegis.ui.tasks.ImportFileTask;
|
import com.beemdevelopment.aegis.ui.tasks.ImportFileTask;
|
||||||
import com.beemdevelopment.aegis.vault.Vault;
|
import com.beemdevelopment.aegis.vault.Vault;
|
||||||
import com.beemdevelopment.aegis.vault.VaultBackupManager;
|
import com.beemdevelopment.aegis.vault.VaultBackupManager;
|
||||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||||
|
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||||
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
|
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
|
||||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||||
|
@ -48,12 +50,14 @@ import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
|
|
||||||
|
@ -164,7 +168,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||||
CheckBox checkBoxAccept = view.findViewById(R.id.checkbox_accept);
|
CheckBox checkBoxAccept = view.findViewById(R.id.checkbox_accept);
|
||||||
CheckBox checkBoxExportAllGroups = view.findViewById(R.id.export_selected_groups);
|
CheckBox checkBoxExportAllGroups = view.findViewById(R.id.export_selected_groups);
|
||||||
TextInputLayout groupsSelectionLayout = view.findViewById(R.id.group_selection_layout);
|
TextInputLayout groupsSelectionLayout = view.findViewById(R.id.group_selection_layout);
|
||||||
DropdownCheckBoxes groupsSelection = view.findViewById(R.id.group_selection_dropdown);
|
DropdownCheckBoxes<VaultGroupModel> groupsSelection = view.findViewById(R.id.group_selection_dropdown);
|
||||||
TextView passwordInfoText = view.findViewById(R.id.text_separate_password);
|
TextView passwordInfoText = view.findViewById(R.id.text_separate_password);
|
||||||
passwordInfoText.setVisibility(checkBoxEncrypt.isChecked() && isBackupPasswordSet ? View.VISIBLE : View.GONE);
|
passwordInfoText.setVisibility(checkBoxEncrypt.isChecked() && isBackupPasswordSet ? View.VISIBLE : View.GONE);
|
||||||
AutoCompleteTextView dropdown = view.findViewById(R.id.dropdown_export_format);
|
AutoCompleteTextView dropdown = view.findViewById(R.id.dropdown_export_format);
|
||||||
|
@ -177,13 +181,13 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||||
passwordInfoText.setVisibility(checkBoxEncrypt.isChecked() && isBackupPasswordSet ? View.VISIBLE : View.GONE);
|
passwordInfoText.setVisibility(checkBoxEncrypt.isChecked() && isBackupPasswordSet ? View.VISIBLE : View.GONE);
|
||||||
});
|
});
|
||||||
|
|
||||||
TreeSet<String> groups = _vaultManager.getVault().getGroups();
|
Collection<VaultGroup> groups = _vaultManager.getVault().getUsedGroups();
|
||||||
if (groups.size() > 0) {
|
if (groups.size() > 0) {
|
||||||
checkBoxExportAllGroups.setVisibility(View.VISIBLE);
|
checkBoxExportAllGroups.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
ArrayList<String> groupsArray = new ArrayList<>();
|
ArrayList<VaultGroupModel> groupsArray = new ArrayList<>();
|
||||||
groupsArray.add(getString(R.string.no_group));
|
groupsArray.add(new VaultGroupModel(getString(R.string.no_group)));
|
||||||
groupsArray.addAll(groups);
|
groupsArray.addAll(groups.stream().map(VaultGroupModel::new).collect(Collectors.toList()));
|
||||||
|
|
||||||
groupsSelection.setCheckedItemsCountTextRes(R.plurals.export_groups_selected_count);
|
groupsSelection.setCheckedItemsCountTextRes(R.plurals.export_groups_selected_count);
|
||||||
groupsSelection.addItems(groupsArray, false);
|
groupsSelection.addItems(groupsArray, false);
|
||||||
|
@ -319,17 +323,19 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||||
Dialogs.showSecureDialog(dialog);
|
Dialogs.showSecureDialog(dialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vault.EntryFilter getVaultEntryFilter(DropdownCheckBoxes dropdownCheckBoxes) {
|
private Vault.EntryFilter getVaultEntryFilter(DropdownCheckBoxes<VaultGroupModel> dropdownCheckBoxes) {
|
||||||
Set<String> groups = new HashSet<>();
|
Set<UUID> groups = new HashSet<>();
|
||||||
for (String group: dropdownCheckBoxes.getCheckedItems()) {
|
for (VaultGroupModel group : dropdownCheckBoxes.getCheckedItems()) {
|
||||||
if (group.equals(getString(R.string.no_group))) {
|
groups.add(group.getUUID());
|
||||||
groups.add(null);
|
|
||||||
} else {
|
|
||||||
groups.add(group);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
private void startGoogleAuthenticatorStyleExport() {
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,6 +39,8 @@ import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ -59,7 +61,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
private boolean _tapToReveal;
|
private boolean _tapToReveal;
|
||||||
private int _tapToRevealTime;
|
private int _tapToRevealTime;
|
||||||
private CopyBehavior _copyBehavior;
|
private CopyBehavior _copyBehavior;
|
||||||
private List<String> _groupFilter;
|
private Set<UUID> _groupFilter;
|
||||||
private SortCategory _sortCategory;
|
private SortCategory _sortCategory;
|
||||||
private ViewMode _viewMode;
|
private ViewMode _viewMode;
|
||||||
private String _searchFilter;
|
private String _searchFilter;
|
||||||
|
@ -76,7 +78,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
_entries = new ArrayList<>();
|
_entries = new ArrayList<>();
|
||||||
_shownEntries = new ArrayList<>();
|
_shownEntries = new ArrayList<>();
|
||||||
_selectedEntries = new ArrayList<>();
|
_selectedEntries = new ArrayList<>();
|
||||||
_groupFilter = new ArrayList<>();
|
_groupFilter = new TreeSet<>();
|
||||||
_holders = new ArrayList<>();
|
_holders = new ArrayList<>();
|
||||||
_dimHandler = new Handler();
|
_dimHandler = new Handler();
|
||||||
_doubleTapHandler = new Handler();
|
_doubleTapHandler = new Handler();
|
||||||
|
@ -246,12 +248,15 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isEntryFiltered(VaultEntry entry) {
|
private boolean isEntryFiltered(VaultEntry entry) {
|
||||||
String group = entry.getGroup();
|
Set<UUID> groups = entry.getGroups();
|
||||||
String issuer = entry.getIssuer().toLowerCase();
|
String issuer = entry.getIssuer().toLowerCase();
|
||||||
String name = entry.getName().toLowerCase();
|
String name = entry.getName().toLowerCase();
|
||||||
|
|
||||||
if (!_groupFilter.isEmpty()) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -274,7 +279,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setGroupFilter(@NonNull List<String> groups) {
|
public void setGroupFilter(@NonNull Set<UUID> groups) {
|
||||||
if (_groupFilter.equals(groups)) {
|
if (_groupFilter.equals(groups)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -352,10 +357,6 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
return (int) _shownEntries.stream().filter(VaultEntry::isFavorite).count();
|
return (int) _shownEntries.stream().filter(VaultEntry::isFavorite).count();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setGroups(TreeSet<String> groups) {
|
|
||||||
_view.setGroups(groups);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onItemDismiss(int position) {
|
public void onItemDismiss(int position) {
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,10 @@ import com.beemdevelopment.aegis.helpers.UiRefresher;
|
||||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||||
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
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.VaultEntry;
|
||||||
|
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
import com.bumptech.glide.ListPreloader;
|
import com.bumptech.glide.ListPreloader;
|
||||||
import com.bumptech.glide.RequestBuilder;
|
import com.bumptech.glide.RequestBuilder;
|
||||||
|
@ -54,7 +57,7 @@ import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TreeSet;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@ -71,11 +74,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||||
private TotpProgressBar _progressBar;
|
private TotpProgressBar _progressBar;
|
||||||
private boolean _showProgress;
|
private boolean _showProgress;
|
||||||
private ViewMode _viewMode;
|
private ViewMode _viewMode;
|
||||||
private TreeSet<String> _groups;
|
private Collection<VaultGroup> _groups;
|
||||||
private LinearLayout _emptyStateView;
|
private LinearLayout _emptyStateView;
|
||||||
private Chip _groupChip;
|
private Chip _groupChip;
|
||||||
private List<String> _groupFilter;
|
private Set<UUID> _groupFilter;
|
||||||
private List<String> _prefGroupFilter;
|
private Set<UUID> _prefGroupFilter;
|
||||||
|
|
||||||
private UiRefresher _refresher;
|
private UiRefresher _refresher;
|
||||||
|
|
||||||
|
@ -168,7 +171,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setGroupFilter(List<String> groups, boolean animate) {
|
public void setGroupFilter(Set<UUID> groups, boolean animate) {
|
||||||
_groupFilter = groups;
|
_groupFilter = groups;
|
||||||
_adapter.setGroupFilter(groups);
|
_adapter.setGroupFilter(groups);
|
||||||
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
||||||
|
@ -334,7 +337,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPrefGroupFilter(List<String> groupFilter) {
|
public void setPrefGroupFilter(Set<UUID> groupFilter) {
|
||||||
_prefGroupFilter = groupFilter;
|
_prefGroupFilter = groupFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -465,17 +468,17 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||||
_recyclerView.scheduleLayoutAnimation();
|
_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 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.setCheckable(true);
|
||||||
chip.setChecked(_groupFilter != null && _groupFilter.contains(group));
|
chip.setChecked(_groupFilter != null && _groupFilter.contains(group.getUUID()));
|
||||||
chip.setCheckedIconVisible(true);
|
chip.setCheckedIconVisible(true);
|
||||||
chip.setOnCheckedChangeListener((group1, checkedId) -> {
|
chip.setOnCheckedChangeListener((group1, checkedId) -> {
|
||||||
List<String> groupFilter = getGroupFilter(chipGroup);
|
Set<UUID> groupFilter = getGroupFilter(chipGroup);
|
||||||
setGroupFilter(groupFilter, true);
|
setGroupFilter(groupFilter, true);
|
||||||
});
|
});
|
||||||
chip.setTag(group == null ? new Object() : null);
|
chip.setTag(group);
|
||||||
chipGroup.addView(chip);
|
chipGroup.addView(chip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -489,7 +492,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||||
Button saveButton = view.findViewById(R.id.btnSave);
|
Button saveButton = view.findViewById(R.id.btnSave);
|
||||||
clearButton.setOnClickListener(v -> {
|
clearButton.setOnClickListener(v -> {
|
||||||
chipGroup.clearCheck();
|
chipGroup.clearCheck();
|
||||||
List<String> groupFilter = Collections.emptyList();
|
Set<UUID> groupFilter = Collections.emptySet();
|
||||||
if (_listener != null) {
|
if (_listener != null) {
|
||||||
_listener.onSaveGroupFilter(groupFilter);
|
_listener.onSaveGroupFilter(groupFilter);
|
||||||
}
|
}
|
||||||
|
@ -498,7 +501,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||||
});
|
});
|
||||||
|
|
||||||
saveButton.setOnClickListener(v -> {
|
saveButton.setOnClickListener(v -> {
|
||||||
List<String> groupFilter = getGroupFilter(chipGroup);
|
Set<UUID> groupFilter = getGroupFilter(chipGroup);
|
||||||
if (_listener != null) {
|
if (_listener != null) {
|
||||||
_listener.onSaveGroupFilter(groupFilter);
|
_listener.onSaveGroupFilter(groupFilter);
|
||||||
}
|
}
|
||||||
|
@ -509,25 +512,23 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||||
_groupChip.setOnClickListener(v -> {
|
_groupChip.setOnClickListener(v -> {
|
||||||
chipGroup.removeAllViews();
|
chipGroup.removeAllViews();
|
||||||
|
|
||||||
for (String group : _groups) {
|
for (VaultGroup group : _groups) {
|
||||||
addChipTo(chipGroup, group);
|
addChipTo(chipGroup, new VaultGroupModel(group));
|
||||||
}
|
}
|
||||||
addChipTo(chipGroup, null);
|
addChipTo(chipGroup, new VaultGroupModel(getString(R.string.no_group)));
|
||||||
|
|
||||||
Dialogs.showSecureDialog(dialog);
|
Dialogs.showSecureDialog(dialog);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<String> getGroupFilter(ChipGroup chipGroup) {
|
private static Set<UUID> getGroupFilter(ChipGroup chipGroup) {
|
||||||
return chipGroup.getCheckedChipIds().stream()
|
return chipGroup.getCheckedChipIds().stream()
|
||||||
.map(i -> {
|
.map(i -> {
|
||||||
Chip chip = chipGroup.findViewById(i);
|
Chip chip = chipGroup.findViewById(i);
|
||||||
if (chip.getTag() != null) {
|
VaultGroupModel group = (VaultGroupModel) chip.getTag();
|
||||||
return null;
|
return group.getUUID();
|
||||||
}
|
|
||||||
return chip. getText().toString();
|
|
||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateGroupChip() {
|
private void updateGroupChip() {
|
||||||
|
@ -543,29 +544,31 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||||
updateDividerDecoration();
|
updateDividerDecoration();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setGroups(TreeSet<String> groups) {
|
public void setGroups(Collection<VaultGroup> groups) {
|
||||||
_groups = groups;
|
_groups = groups;
|
||||||
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
|
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
|
||||||
updateDividerDecoration();
|
updateDividerDecoration();
|
||||||
|
|
||||||
if (_prefGroupFilter != null) {
|
if (_prefGroupFilter != null) {
|
||||||
List<String> groupFilter = cleanGroupFilter(_prefGroupFilter);
|
Set<UUID> groupFilter = cleanGroupFilter(_prefGroupFilter);
|
||||||
_prefGroupFilter = null;
|
_prefGroupFilter = null;
|
||||||
if (!groupFilter.isEmpty()) {
|
if (!groupFilter.isEmpty()) {
|
||||||
setGroupFilter(groupFilter, false);
|
setGroupFilter(groupFilter, false);
|
||||||
}
|
}
|
||||||
} else if (_groupFilter != null) {
|
} else if (_groupFilter != null) {
|
||||||
List<String> groupFilter = cleanGroupFilter(_groupFilter);
|
Set<UUID> groupFilter = cleanGroupFilter(_groupFilter);
|
||||||
if (!_groupFilter.equals(groupFilter)) {
|
if (!_groupFilter.equals(groupFilter)) {
|
||||||
setGroupFilter(groupFilter, true);
|
setGroupFilter(groupFilter, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> cleanGroupFilter(List<String> groupFilter) {
|
private Set<UUID> cleanGroupFilter(Set<UUID> groupFilter) {
|
||||||
return groupFilter.stream()
|
Set<UUID> groupUuids = _groups.stream().map(UUIDMap.Value::getUUID).collect(Collectors.toSet());
|
||||||
.filter(g -> g == null || _groups.contains(g))
|
|
||||||
.collect(Collectors.toList());
|
return groupFilter.stream()
|
||||||
|
.filter(g -> g == null || groupUuids.contains(g))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateDividerDecoration() {
|
private void updateDividerDecoration() {
|
||||||
|
@ -616,7 +619,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||||
void onSelect(VaultEntry entry);
|
void onSelect(VaultEntry entry);
|
||||||
void onDeselect(VaultEntry entry);
|
void onDeselect(VaultEntry entry);
|
||||||
void onListChange();
|
void onListChange();
|
||||||
void onSaveGroupFilter(List<String> groupFilter);
|
void onSaveGroupFilter(Set<UUID> groupFilter);
|
||||||
void onEntryListTouch();
|
void onEntryListTouch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,19 +7,20 @@ import android.view.ViewGroup;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.beemdevelopment.aegis.R;
|
import com.beemdevelopment.aegis.R;
|
||||||
|
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
|
public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
|
||||||
private GroupAdapter.Listener _listener;
|
private GroupAdapter.Listener _listener;
|
||||||
private ArrayList<String> _groups;
|
private ArrayList<VaultGroup> _groups;
|
||||||
|
|
||||||
public GroupAdapter(GroupAdapter.Listener listener) {
|
public GroupAdapter(GroupAdapter.Listener listener) {
|
||||||
_listener = listener;
|
_listener = listener;
|
||||||
_groups = new ArrayList<>();
|
_groups = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addGroup(String group) {
|
public void addGroup(VaultGroup group) {
|
||||||
_groups.add(group);
|
_groups.add(group);
|
||||||
|
|
||||||
int position = getItemCount() - 1;
|
int position = getItemCount() - 1;
|
||||||
|
@ -30,7 +31,7 @@ public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeGroup(String group) {
|
public void removeGroup(VaultGroup group) {
|
||||||
int position = _groups.indexOf(group);
|
int position = _groups.indexOf(group);
|
||||||
_groups.remove(position);
|
_groups.remove(position);
|
||||||
notifyItemRemoved(position);
|
notifyItemRemoved(position);
|
||||||
|
@ -57,6 +58,6 @@ public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface Listener {
|
public interface Listener {
|
||||||
void onRemoveGroup(String group);
|
void onRemoveGroup(VaultGroup group);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import android.widget.TextView;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.beemdevelopment.aegis.R;
|
import com.beemdevelopment.aegis.R;
|
||||||
|
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||||
|
|
||||||
public class GroupHolder extends RecyclerView.ViewHolder {
|
public class GroupHolder extends RecyclerView.ViewHolder {
|
||||||
private TextView _slotName;
|
private TextView _slotName;
|
||||||
|
@ -18,8 +19,8 @@ public class GroupHolder extends RecyclerView.ViewHolder {
|
||||||
_buttonDelete = view.findViewById(R.id.button_delete);
|
_buttonDelete = view.findViewById(R.id.button_delete);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setData(String groupName) {
|
public void setData(VaultGroup group) {
|
||||||
_slotName.setText(groupName);
|
_slotName.setText(group.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOnDeleteClickListener(View.OnClickListener listener) {
|
public void setOnDeleteClickListener(View.OnClickListener listener) {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import java.util.Collections;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -99,7 +100,14 @@ public class UUIDMap <T extends UUIDMap.Value> implements Iterable<T>, Serializa
|
||||||
* Reports whether the internal map contains a value with the UUID of the given value.
|
* Reports whether the internal map contains a value with the UUID of the given value.
|
||||||
*/
|
*/
|
||||||
public boolean has(T 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 class UUIDMap <T extends UUIDMap.Value> implements Iterable<T>, Serializa
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getUUID().equals(((Value) o).getUUID());
|
return Objects.equals(getUUID(), ((Value) o).getUUID());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,13 @@ import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public class Vault {
|
public class Vault {
|
||||||
private static final int VERSION = 2;
|
private static final int VERSION = 3;
|
||||||
private UUIDMap<VaultEntry> _entries = new UUIDMap<>();
|
private final UUIDMap<VaultEntry> _entries = new UUIDMap<>();
|
||||||
|
private final UUIDMap<VaultGroup> _groups = new UUIDMap<>();
|
||||||
|
|
||||||
public JSONObject toJson() {
|
public JSONObject toJson() {
|
||||||
return toJson(null);
|
return toJson(null);
|
||||||
|
@ -18,16 +22,24 @@ public class Vault {
|
||||||
|
|
||||||
public JSONObject toJson(@Nullable EntryFilter filter) {
|
public JSONObject toJson(@Nullable EntryFilter filter) {
|
||||||
try {
|
try {
|
||||||
JSONArray array = new JSONArray();
|
JSONArray entriesArray = new JSONArray();
|
||||||
for (VaultEntry e : _entries) {
|
for (VaultEntry e : _entries) {
|
||||||
if (filter == null || filter.includeEntry(e)) {
|
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();
|
JSONObject obj = new JSONObject();
|
||||||
obj.put("version", VERSION);
|
obj.put("version", VERSION);
|
||||||
obj.put("entries", array);
|
obj.put("entries", entriesArray);
|
||||||
|
obj.put("groups", groupsArray);
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
@ -37,6 +49,7 @@ public class Vault {
|
||||||
public static Vault fromJson(JSONObject obj) throws VaultException {
|
public static Vault fromJson(JSONObject obj) throws VaultException {
|
||||||
Vault vault = new Vault();
|
Vault vault = new Vault();
|
||||||
UUIDMap<VaultEntry> entries = vault.getEntries();
|
UUIDMap<VaultEntry> entries = vault.getEntries();
|
||||||
|
UUIDMap<VaultGroup> groups = vault.getGroups();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
int ver = obj.getInt("version");
|
int ver = obj.getInt("version");
|
||||||
|
@ -44,9 +57,28 @@ public class Vault {
|
||||||
throw new VaultException("Unsupported version");
|
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");
|
JSONArray array = obj.getJSONArray("entries");
|
||||||
for (int i = 0; i < array.length(); i++) {
|
for (int i = 0; i < array.length(); i++) {
|
||||||
VaultEntry entry = VaultEntry.fromJson(array.getJSONObject(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);
|
entries.add(entry);
|
||||||
}
|
}
|
||||||
} catch (VaultEntryException | JSONException e) {
|
} catch (VaultEntryException | JSONException e) {
|
||||||
|
@ -56,10 +88,33 @@ public class Vault {
|
||||||
return vault;
|
return vault;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void migrateOldGroup(VaultEntry entry) {
|
||||||
|
if (entry.getOldGroup() != null) {
|
||||||
|
Optional<VaultGroup> 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<VaultEntry> getEntries() {
|
public UUIDMap<VaultEntry> getEntries() {
|
||||||
return _entries;
|
return _entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UUIDMap<VaultGroup> getGroups() {
|
||||||
|
return _groups;
|
||||||
|
}
|
||||||
|
|
||||||
public interface EntryFilter {
|
public interface EntryFilter {
|
||||||
boolean includeEntry(VaultEntry entry);
|
boolean includeEntry(VaultEntry entry);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,23 +10,26 @@ import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||||
import com.beemdevelopment.aegis.util.JsonUtils;
|
import com.beemdevelopment.aegis.util.JsonUtils;
|
||||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Objects;
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public class VaultEntry extends UUIDMap.Value {
|
public class VaultEntry extends UUIDMap.Value {
|
||||||
private String _name = "";
|
private String _name = "";
|
||||||
private String _issuer = "";
|
private String _issuer = "";
|
||||||
private String _group;
|
|
||||||
private OtpInfo _info;
|
private OtpInfo _info;
|
||||||
private byte[] _icon;
|
private byte[] _icon;
|
||||||
private IconType _iconType = IconType.INVALID;
|
private IconType _iconType = IconType.INVALID;
|
||||||
private boolean _isFavorite;
|
private boolean _isFavorite;
|
||||||
private int _usageCount;
|
private int _usageCount;
|
||||||
private String _note = "";
|
private String _note = "";
|
||||||
|
private String _oldGroup;
|
||||||
|
private Set<UUID> _groups = new TreeSet<>();
|
||||||
|
|
||||||
private VaultEntry(UUID uuid, OtpInfo info) {
|
private VaultEntry(UUID uuid, OtpInfo info) {
|
||||||
super(uuid);
|
super(uuid);
|
||||||
|
@ -44,13 +47,6 @@ public class VaultEntry extends UUIDMap.Value {
|
||||||
setIssuer(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) {
|
public VaultEntry(GoogleAuthInfo info) {
|
||||||
this(info.getOtpInfo(), info.getAccountName(), info.getIssuer());
|
this(info.getOtpInfo(), info.getAccountName(), info.getIssuer());
|
||||||
}
|
}
|
||||||
|
@ -63,12 +59,18 @@ public class VaultEntry extends UUIDMap.Value {
|
||||||
obj.put("uuid", getUUID().toString());
|
obj.put("uuid", getUUID().toString());
|
||||||
obj.put("name", _name);
|
obj.put("name", _name);
|
||||||
obj.put("issuer", _issuer);
|
obj.put("issuer", _issuer);
|
||||||
obj.put("group", _group);
|
|
||||||
obj.put("note", _note);
|
obj.put("note", _note);
|
||||||
obj.put("favorite", _isFavorite);
|
obj.put("favorite", _isFavorite);
|
||||||
obj.put("icon", _icon == null ? JSONObject.NULL : Base64.encode(_icon));
|
obj.put("icon", _icon == null ? JSONObject.NULL : Base64.encode(_icon));
|
||||||
obj.put("icon_mime", _icon == null ? null : _iconType.toMimeType());
|
obj.put("icon_mime", _icon == null ? null : _iconType.toMimeType());
|
||||||
obj.put("info", _info.toJson());
|
obj.put("info", _info.toJson());
|
||||||
|
|
||||||
|
JSONArray groupUuids = new JSONArray();
|
||||||
|
for (UUID uuid : _groups) {
|
||||||
|
groupUuids.put(uuid.toString());
|
||||||
|
}
|
||||||
|
obj.put("groups", groupUuids);
|
||||||
|
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
@ -90,10 +92,21 @@ public class VaultEntry extends UUIDMap.Value {
|
||||||
VaultEntry entry = new VaultEntry(uuid, info);
|
VaultEntry entry = new VaultEntry(uuid, info);
|
||||||
entry.setName(obj.getString("name"));
|
entry.setName(obj.getString("name"));
|
||||||
entry.setIssuer(obj.getString("issuer"));
|
entry.setIssuer(obj.getString("issuer"));
|
||||||
entry.setGroup(obj.optString("group", null));
|
|
||||||
entry.setNote(obj.optString("note", ""));
|
entry.setNote(obj.optString("note", ""));
|
||||||
entry.setIsFavorite(obj.optBoolean("favorite", false));
|
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(JsonUtils.optString(obj, "group"));
|
||||||
|
}
|
||||||
|
|
||||||
Object icon = obj.get("icon");
|
Object icon = obj.get("icon");
|
||||||
if (icon != JSONObject.NULL) {
|
if (icon != JSONObject.NULL) {
|
||||||
String mime = JsonUtils.optString(obj, "icon_mime");
|
String mime = JsonUtils.optString(obj, "icon_mime");
|
||||||
|
@ -121,8 +134,8 @@ public class VaultEntry extends UUIDMap.Value {
|
||||||
return _issuer;
|
return _issuer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getGroup() {
|
public Set<UUID> getGroups() {
|
||||||
return _group;
|
return _groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getIcon() {
|
public byte[] getIcon() {
|
||||||
|
@ -153,8 +166,22 @@ public class VaultEntry extends UUIDMap.Value {
|
||||||
_issuer = issuer;
|
_issuer = issuer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setGroup(String group) {
|
public void addGroup(UUID group) {
|
||||||
_group = 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<UUID> 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) {
|
public void setInfo(OtpInfo info) {
|
||||||
|
@ -176,6 +203,14 @@ public class VaultEntry extends UUIDMap.Value {
|
||||||
|
|
||||||
public void setIsFavorite(boolean isFavorite) { _isFavorite = isFavorite; }
|
public void setIsFavorite(boolean isFavorite) { _isFavorite = isFavorite; }
|
||||||
|
|
||||||
|
void setOldGroup(String oldGroup) {
|
||||||
|
_oldGroup = oldGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getOldGroup() {
|
||||||
|
return _oldGroup;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (!(o instanceof VaultEntry)) {
|
if (!(o instanceof VaultEntry)) {
|
||||||
|
@ -194,12 +229,12 @@ public class VaultEntry extends UUIDMap.Value {
|
||||||
public boolean equivalates(VaultEntry entry) {
|
public boolean equivalates(VaultEntry entry) {
|
||||||
return getName().equals(entry.getName())
|
return getName().equals(entry.getName())
|
||||||
&& getIssuer().equals(entry.getIssuer())
|
&& getIssuer().equals(entry.getIssuer())
|
||||||
&& Objects.equals(getGroup(), entry.getGroup())
|
|
||||||
&& getInfo().equals(entry.getInfo())
|
&& getInfo().equals(entry.getInfo())
|
||||||
&& Arrays.equals(getIcon(), entry.getIcon())
|
&& Arrays.equals(getIcon(), entry.getIcon())
|
||||||
&& getIconType().equals(entry.getIconType())
|
&& getIconType().equals(entry.getIconType())
|
||||||
&& getNote().equals(entry.getNote())
|
&& getNote().equals(entry.getNote())
|
||||||
&& isFavorite() == entry.isFavorite();
|
&& isFavorite() == entry.isFavorite()
|
||||||
|
&& getGroups().equals(entry.getGroups());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,9 +20,9 @@ import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.PrintStream;
|
import java.io.PrintStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.text.Collator;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.TreeSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@ -220,6 +220,8 @@ public class VaultRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addEntry(VaultEntry entry) {
|
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);
|
_vault.getEntries().add(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,8 +233,12 @@ public class VaultRepository {
|
||||||
return _vault.getEntries().remove(entry);
|
return _vault.getEntries().remove(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void wipeEntries() {
|
/**
|
||||||
|
* Wipes all entries and groups from the vault.
|
||||||
|
*/
|
||||||
|
public void wipeContents() {
|
||||||
_vault.getEntries().wipe();
|
_vault.getEntries().wipe();
|
||||||
|
_vault.getGroups().wipe();
|
||||||
}
|
}
|
||||||
|
|
||||||
public VaultEntry replaceEntry(VaultEntry entry) {
|
public VaultEntry replaceEntry(VaultEntry entry) {
|
||||||
|
@ -240,8 +246,8 @@ public class VaultRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moves entry1 to the position of entry2.
|
* Moves entry1 to the position of entry2.
|
||||||
*/
|
*/
|
||||||
public void moveEntry(VaultEntry entry1, VaultEntry entry2) {
|
public void moveEntry(VaultEntry entry1, VaultEntry entry2) {
|
||||||
_vault.getEntries().move(entry1, entry2);
|
_vault.getEntries().move(entry1, entry2);
|
||||||
}
|
}
|
||||||
|
@ -254,15 +260,54 @@ public class VaultRepository {
|
||||||
return _vault.getEntries().getValues();
|
return _vault.getEntries().getValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
public TreeSet<String> getGroups() {
|
public void addGroup(VaultGroup group) {
|
||||||
TreeSet<String> groups = new TreeSet<>(Collator.getInstance());
|
_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()) {
|
for (VaultEntry entry : getEntries()) {
|
||||||
String group = entry.getGroup();
|
entry.removeGroup(group.getUUID());
|
||||||
if (group != null) {
|
|
||||||
groups.add(group);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return groups;
|
|
||||||
|
_vault.getGroups().remove(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<VaultGroup> getGroups() {
|
||||||
|
return _vault.getGroups().getValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<VaultGroup> getUsedGroups() {
|
||||||
|
Set<UUID> usedGroups = new HashSet<>();
|
||||||
|
for (VaultEntry entry : getEntries()) {
|
||||||
|
usedGroups.addAll(entry.getGroups());
|
||||||
|
}
|
||||||
|
|
||||||
|
return getGroups().stream()
|
||||||
|
.filter(vg -> usedGroups.contains(vg.getUUID()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public VaultFileCredentials getCredentials() {
|
public VaultFileCredentials getCredentials() {
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
tools:context="com.beemdevelopment.aegis.ui.GroupManagerActivity">
|
tools:context="com.beemdevelopment.aegis.ui.GroupManagerActivity">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_delete_unused_groups"
|
||||||
|
app:showAsAction="collapseActionView"
|
||||||
|
android:title="@string/remove_unused_groups"/>
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_save"
|
android:id="@+id/action_save"
|
||||||
app:showAsAction="ifRoom"
|
app:showAsAction="ifRoom"
|
||||||
|
|
|
@ -283,6 +283,8 @@
|
||||||
<string name="unlocking_vault">Unlocking the vault</string>
|
<string name="unlocking_vault">Unlocking the vault</string>
|
||||||
<string name="remove_group">Remove group</string>
|
<string name="remove_group">Remove group</string>
|
||||||
<string name="remove_group_description">Are you sure you want to remove this group? Entries in this group will automatically switch to \'No group\'.</string>
|
<string name="remove_group_description">Are you sure you want to remove this group? Entries in this group will automatically switch to \'No group\'.</string>
|
||||||
|
<string name="remove_unused_groups">Delete unused groups</string>
|
||||||
|
<string name="remove_unused_groups_description">Are you sure you want to delete all groups that are not assigned to an entry?</string>
|
||||||
<string name="remove_icon_pack">Remove icon pack</string>
|
<string name="remove_icon_pack">Remove icon pack</string>
|
||||||
<string name="remove_icon_pack_description">Are you sure you want to remove this icon pack? Entries that use icons from this pack will not be affected.</string>
|
<string name="remove_icon_pack_description">Are you sure you want to remove this icon pack? Entries that use icons from this pack will not be affected.</string>
|
||||||
<string name="details">Details</string>
|
<string name="details">Details</string>
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
package com.beemdevelopment.aegis.vault;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
import com.beemdevelopment.aegis.util.IOUtils;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class VaultTest {
|
||||||
|
@Test
|
||||||
|
public void testGroupConversion()
|
||||||
|
throws IOException, VaultFileException, VaultException {
|
||||||
|
Vault vault;
|
||||||
|
try (InputStream inStream = getClass().getResourceAsStream("aegis_plain_grouped_v2.json")) {
|
||||||
|
byte[] bytes = IOUtils.readAll(inStream);
|
||||||
|
VaultFile file = VaultFile.fromBytes(bytes);
|
||||||
|
vault = Vault.fromJson(file.getContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
checkGroups(vault);
|
||||||
|
|
||||||
|
// After saving to and loading from the new format, the same checks should still pass
|
||||||
|
vault = Vault.fromJson(vault.toJson());
|
||||||
|
checkGroups(vault);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkGroups(Vault vault) {
|
||||||
|
// No old groups should be present anymore
|
||||||
|
assertEquals(0, vault.getEntries().getValues().stream()
|
||||||
|
.filter(e -> e.getOldGroup() != null)
|
||||||
|
.count());
|
||||||
|
|
||||||
|
// New groups should have been created, and groups with the same name
|
||||||
|
// should have been merged into a single group
|
||||||
|
assertEquals(2, vault.getGroups().getValues().size());
|
||||||
|
|
||||||
|
// Only one group with name group1
|
||||||
|
List<VaultGroup> foundGroups = vault.getGroups().getValues().stream()
|
||||||
|
.filter(g -> g.getName().equals("group1"))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
assertEquals(1, foundGroups.size());
|
||||||
|
VaultGroup group1 = foundGroups.get(0);
|
||||||
|
|
||||||
|
// Only one group with name group2
|
||||||
|
foundGroups = vault.getGroups().getValues().stream()
|
||||||
|
.filter(g -> g.getName().equals("group2"))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
assertEquals(1, foundGroups.size());
|
||||||
|
VaultGroup group2 = foundGroups.get(0);
|
||||||
|
|
||||||
|
// Two entries in group1
|
||||||
|
assertEquals(2, vault.getEntries().getValues().stream()
|
||||||
|
.filter(e -> e.getGroups().contains(group1.getUUID()))
|
||||||
|
.count());
|
||||||
|
|
||||||
|
// One entry in group2
|
||||||
|
assertEquals(1, vault.getEntries().getValues().stream()
|
||||||
|
.filter(e -> e.getGroups().contains(group2.getUUID()))
|
||||||
|
.count());
|
||||||
|
|
||||||
|
// Rest of the entries in no groups
|
||||||
|
assertEquals(vault.getEntries().getValues().size() - 3, vault.getEntries().getValues().stream()
|
||||||
|
.filter(e -> e.getGroups().isEmpty())
|
||||||
|
.count());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"header": {
|
||||||
|
"slots": null,
|
||||||
|
"params": null
|
||||||
|
},
|
||||||
|
"db": {
|
||||||
|
"version": 2,
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"type": "totp",
|
||||||
|
"uuid": "3ae6f1ad-2e65-4ed2-a953-1ec0dff2386d",
|
||||||
|
"name": "Mason",
|
||||||
|
"issuer": "Deno",
|
||||||
|
"icon": null,
|
||||||
|
"icon_mime": null,
|
||||||
|
"group": "group1",
|
||||||
|
"info": {
|
||||||
|
"secret": "4SJHB4GSD43FZBAI7C2HLRJGPQ",
|
||||||
|
"algo": "SHA1",
|
||||||
|
"digits": 6,
|
||||||
|
"period": 30
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "totp",
|
||||||
|
"uuid": "84b55971-a3d2-4173-a5bb-0aea113dbc17",
|
||||||
|
"name": "James",
|
||||||
|
"issuer": "SPDX",
|
||||||
|
"icon": null,
|
||||||
|
"icon_mime": null,
|
||||||
|
"group": null,
|
||||||
|
"info": {
|
||||||
|
"secret": "5OM4WOOGPLQEF6UGN3CPEOOLWU",
|
||||||
|
"algo": "SHA256",
|
||||||
|
"digits": 7,
|
||||||
|
"period": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "totp",
|
||||||
|
"uuid": "3deaff2e-f181-4837-80e1-fdf0c54e9363",
|
||||||
|
"name": "Elijah",
|
||||||
|
"issuer": "Airbnb",
|
||||||
|
"icon": null,
|
||||||
|
"icon_mime": null,
|
||||||
|
"group": "group1",
|
||||||
|
"info": {
|
||||||
|
"secret": "7ELGJSGXNCCTV3O6LKJWYFV2RA",
|
||||||
|
"algo": "SHA512",
|
||||||
|
"digits": 8,
|
||||||
|
"period": 50
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "hotp",
|
||||||
|
"uuid": "0a8c0571-ff6f-4b02-aa4b-50553b4fb4fe",
|
||||||
|
"name": "James",
|
||||||
|
"issuer": "Issuu",
|
||||||
|
"icon": null,
|
||||||
|
"icon_mime": null,
|
||||||
|
"group": "group2",
|
||||||
|
"info": {
|
||||||
|
"secret": "YOOMIXWS5GN6RTBPUFFWKTW5M4",
|
||||||
|
"algo": "SHA1",
|
||||||
|
"digits": 6,
|
||||||
|
"counter": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "hotp",
|
||||||
|
"uuid": "03e572f2-8ebd-44b0-a57e-e958af74815d",
|
||||||
|
"name": "Benjamin",
|
||||||
|
"issuer": "Air Canada",
|
||||||
|
"icon": null,
|
||||||
|
"icon_mime": null,
|
||||||
|
"group": null,
|
||||||
|
"info": {
|
||||||
|
"secret": "KUVJJOM753IHTNDSZVCNKL7GII",
|
||||||
|
"algo": "SHA256",
|
||||||
|
"digits": 7,
|
||||||
|
"counter": 50
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "hotp",
|
||||||
|
"uuid": "b25f8815-007f-40f7-a700-ce058ac05435",
|
||||||
|
"name": "Mason",
|
||||||
|
"issuer": "WWE",
|
||||||
|
"icon": null,
|
||||||
|
"icon_mime": null,
|
||||||
|
"group": null,
|
||||||
|
"info": {
|
||||||
|
"secret": "5VAML3X35THCEBVRLV24CGBKOY",
|
||||||
|
"algo": "SHA512",
|
||||||
|
"digits": 8,
|
||||||
|
"counter": 10300
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "steam",
|
||||||
|
"uuid": "5b11ae3b-6fc3-4d46-8ca7-cf0aea7de920",
|
||||||
|
"name": "Sophia",
|
||||||
|
"issuer": "Boeing",
|
||||||
|
"icon": null,
|
||||||
|
"icon_mime": null,
|
||||||
|
"group": null,
|
||||||
|
"info": {
|
||||||
|
"secret": "JRZCL47CMXVOQMNPZR2F7J4RGI",
|
||||||
|
"algo": "SHA1",
|
||||||
|
"digits": 5,
|
||||||
|
"period": 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue