From 0d5ba3c0ab54c2035ef19b8eae76ed3676931dfb Mon Sep 17 00:00:00 2001 From: r3dh3ck Date: Fri, 8 Nov 2024 04:54:50 +0000 Subject: [PATCH] Implement archive --- .../beemdevelopment/aegis/OverallTest.java | 12 ++- .../aegis/ui/MainActivity.java | 81 ++++++++++++++++--- .../aegis/ui/dialogs/Dialogs.java | 13 +++ .../aegis/ui/views/EntryAdapter.java | 14 +++- .../aegis/ui/views/EntryListView.java | 19 ++++- .../aegis/vault/VaultEntry.java | 14 +++- .../aegis/vault/VaultRepository.java | 4 +- .../res/layout/fragment_entry_list_view.xml | 9 +++ app/src/main/res/menu/menu_action_mode.xml | 7 ++ app/src/main/res/values/strings.xml | 11 +++ 10 files changed, 164 insertions(+), 20 deletions(-) diff --git a/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java b/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java index 016eb5c6..79e1e475 100644 --- a/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java +++ b/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java @@ -142,10 +142,20 @@ public class OverallTest extends AegisTest { onView(withId(R.id.action_share_qr)).perform(click()); onView(withId(R.id.btnNext)).perform(click()).perform(click()).perform(click()); - onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 0, longClick())); + onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset, longClick())); + onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 1, click())); onView(allOf(isDescendantOfA(withClassName(containsString("ActionBarContextView"))), withClassName(containsString("OverflowMenuButton")))).perform(click()); onView(withText(R.string.action_delete)).perform(click()); onView(withId(android.R.id.button1)).perform(click()); + onView(withText(R.string.archive)).perform(click()); + onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset, longClick())); + onView(allOf(isDescendantOfA(withClassName(containsString("ActionBarContextView"))), withClassName(containsString("OverflowMenuButton")))).perform(click()); + onView(withText(R.string.action_delete)).perform(click()); + onView(withId(android.R.id.button1)).perform(click()); + onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset, longClick())); + onView(allOf(isDescendantOfA(withClassName(containsString("ActionBarContextView"))), withClassName(containsString("OverflowMenuButton")))).perform(click()); + onView(withText(R.string.action_restore)).perform(click()); + onView(withText(R.string.archive)).perform(click()); openContextualActionModeOverflowMenu(); onView(withText(R.string.lock)).perform(click()); diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java index c3f3b0f9..47cfda2f 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java @@ -96,6 +96,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene private boolean _isDPadPressed; private boolean _isDoingIntro; private boolean _isAuthenticating; + private boolean _isArchiveEnabled; private String _submittedSearchQuery; private String _pendingSearchQuery; @@ -187,6 +188,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene _isDPadPressed = false; _isDoingIntro = false; _isAuthenticating = false; + _isArchiveEnabled = false; if (savedInstanceState != null) { _isRecreated = true; _pendingSearchQuery = savedInstanceState.getString("pendingSearchQuery"); @@ -249,7 +251,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene public void setGroups(Collection groups) { _groups = groups; - _groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE); if (_prefGroupFilter != null) { Set groupFilter = cleanGroupFilter(_prefGroupFilter); @@ -273,13 +274,17 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene private void initializeGroups() { _groupChip.removeAllViews(); + addArchiveChip(_groupChip); + for (VaultGroup group : _groups) { addChipTo(_groupChip, new VaultGroupModel(group)); } - GroupPlaceholderType placeholderType = GroupPlaceholderType.NO_GROUP; - addChipTo(_groupChip, new VaultGroupModel(this, placeholderType)); - addSaveChip(_groupChip); + if (!_groups.isEmpty()) { + GroupPlaceholderType placeholderType = GroupPlaceholderType.NO_GROUP; + addChipTo(_groupChip, new VaultGroupModel(this, placeholderType)); + addSaveChip(_groupChip); + } } private Set cleanGroupFilter(Set groupFilter) { @@ -290,6 +295,21 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene .collect(Collectors.toSet()); } + private void addArchiveChip(ChipGroup chipGroup) { + Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false); + chip.setText(getString(R.string.archive)); + chip.setCheckedIconVisible(false); + chip.setOnCheckedChangeListener((button, isChecked) -> { + _isArchiveEnabled = isChecked; + if (_actionMode != null) { + _actionMode.finish(); + } + chip.setChecked(isChecked); + _entryListView.enableArchive(isChecked); + }); + chipGroup.addView(chip); + } + private void addChipTo(ChipGroup chipGroup, VaultGroupModel group) { Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false); chip.setText(group.getName()); @@ -1153,6 +1173,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene } else { setFavoriteMenuItemVisiblity(); setIsMultipleSelected(_selectedEntries.size() > 1); + setRestoreMenuItemVisibility(); } } } @@ -1195,6 +1216,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene } } + private void setRestoreMenuItemVisibility() { + MenuItem restoreMenuItem = _actionMode.getMenu().findItem(R.id.action_restore); + restoreMenuItem.setVisible(_isArchiveEnabled); + } + @Override public void onLongEntryClick(VaultEntry entry) { if (!_selectedEntries.isEmpty()) { @@ -1211,6 +1237,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene _actionModeBackPressHandler.setEnabled(true); setFavoriteMenuItemVisiblity(); setAssignIconsMenuItemVisibility(); + setRestoreMenuItemVisibility(); } @Override @@ -1298,6 +1325,40 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene } } + private void onActionRestore(ActionMode mode) { + for (VaultEntry entry : _selectedEntries) { + entry.setIsArchived(false); + } + saveAndBackupVault(); + _entryListView.setGroups(_vaultManager.getVault().getUsedGroups()); + _entryListView.setEntries(_vaultManager.getVault().getEntries()); + mode.finish(); + } + + private void onActionDelete(ActionMode mode) { + if (_isArchiveEnabled) { + Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries, (d, which) -> { + for (VaultEntry entry : _selectedEntries) { + _vaultManager.getVault().removeEntry(entry); + } + saveAndBackupVault(); + _entryListView.setGroups(_vaultManager.getVault().getUsedGroups()); + _entryListView.setEntries(_vaultManager.getVault().getEntries()); + mode.finish(); + }); + } else { + Dialogs.showArchiveEntriesDialog(MainActivity.this, _selectedEntries.size(), (dialog, which) -> { + for (VaultEntry entry : _selectedEntries) { + entry.setIsArchived(true); + } + saveAndBackupVault(); + _entryListView.setGroups(_vaultManager.getVault().getUsedGroups()); + _entryListView.setEntries(_vaultManager.getVault().getEntries()); + mode.finish(); + }); + } + } + private class SearchViewBackPressHandler extends OnBackPressedCallback { public SearchViewBackPressHandler() { super(false); @@ -1394,16 +1455,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene startActivity(intent); mode.finish(); + } else if (itemId == R.id.action_restore) { + onActionRestore(mode); } else if (itemId == R.id.action_delete) { - Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries, (d, which) -> { - for (VaultEntry entry : _selectedEntries) { - _vaultManager.getVault().removeEntry(entry); - } - saveAndBackupVault(); - _entryListView.setGroups(_vaultManager.getVault().getUsedGroups()); - _entryListView.setEntries(_vaultManager.getVault().getEntries()); - mode.finish(); - }); + onActionDelete(mode); } else if (itemId == R.id.action_select_all) { _selectedEntries = _entryListView.selectAllEntries(); setFavoriteMenuItemVisiblity(); diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/dialogs/Dialogs.java b/app/src/main/java/com/beemdevelopment/aegis/ui/dialogs/Dialogs.java index 2a76f083..c30fede6 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/dialogs/Dialogs.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/dialogs/Dialogs.java @@ -93,6 +93,19 @@ public class Dialogs { .create()); } + public static void showArchiveEntriesDialog(Context context, int count, DialogInterface.OnClickListener onArchive) { + String title = context.getResources().getQuantityString(R.plurals.archive_entry, count); + String message = context.getResources().getQuantityString(R.plurals.archive_entry_description, count); + Dialog dialog = new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Warning) + .setTitle(title) + .setMessage(message) + .setIconAttribute(android.R.attr.alertDialogIcon) + .setPositiveButton(android.R.string.ok, onArchive) + .setNegativeButton(android.R.string.no, null) + .create(); + showSecureDialog(dialog); + } + private static String getVaultEntryName(Context context, VaultEntry entry) { if (!entry.getIssuer().isEmpty() && !entry.getName().isEmpty()) { return String.format("%s (%s)", entry.getIssuer(), entry.getName()); diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryAdapter.java b/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryAdapter.java index a811f027..08e2229f 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryAdapter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryAdapter.java @@ -79,6 +79,7 @@ public class EntryAdapter extends RecyclerView.Adapter private Handler _dimHandler; private Handler _doubleTapHandler; private boolean _pauseFocused; + private boolean _isArchiveEnabled; // keeps track of the EntryHolders that are currently bound private List _holders; @@ -196,7 +197,13 @@ public class EntryAdapter extends RecyclerView.Adapter String name = entry.getName().toLowerCase(); String note = entry.getNote().toLowerCase(); - if (!_groupFilter.isEmpty()) { + if (!_isArchiveEnabled && entry.isArchived()) { + return true; + } + + if (_isArchiveEnabled && !entry.isArchived()) { + return true; + } else if (!_groupFilter.isEmpty()) { if (groups.isEmpty() && !_groupFilter.contains(null)) { return true; } @@ -774,6 +781,11 @@ public class EntryAdapter extends RecyclerView.Adapter return _entryList.isErrorCardShown(); } + public void enableArchive(boolean enable) { + _isArchiveEnabled = enable; + refreshEntryList(); + } + private class FooterView extends RecyclerView.ViewHolder { View _footerView; diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryListView.java b/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryListView.java index 500198c0..312398c6 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryListView.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryListView.java @@ -14,6 +14,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.animation.LayoutAnimationController; import android.widget.LinearLayout; +import android.widget.TextView; import androidx.annotation.AttrRes; import androidx.annotation.NonNull; @@ -72,6 +73,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener { private boolean _showExpirationState; private ViewMode _viewMode; private LinearLayout _emptyStateView; + private boolean _isArchiveEnabled; + private TextView _archiveEmptyText; private UiRefresher _refresher; @@ -150,6 +153,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener { }); _emptyStateView = view.findViewById(R.id.vEmptyList); + _archiveEmptyText = view.findViewById(R.id.archive_empty_text); return view; } @@ -471,6 +475,12 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener { _recyclerView.scheduleLayoutAnimation(); } + public void enableArchive(boolean enable) { + _isArchiveEnabled = enable; + _adapter.enableArchive(enable); + updateEmptyState(); + } + private void setShowProgress(boolean showProgress) { _showProgress = showProgress; updateDividerDecoration(); @@ -495,9 +505,12 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener { if (_adapter.getShownEntriesCount() > 0) { _recyclerView.setVisibility(View.VISIBLE); _emptyStateView.setVisibility(View.GONE); - } else { - if (Strings.isNullOrEmpty(_adapter.getSearchFilter())) { - _recyclerView.setVisibility(View.GONE); + _archiveEmptyText.setVisibility(View.GONE); + } else if (Strings.isNullOrEmpty(_adapter.getSearchFilter())) { + _recyclerView.setVisibility(View.GONE); + if (_isArchiveEnabled) { + _archiveEmptyText.setVisibility(View.VISIBLE); + } else { _emptyStateView.setVisibility(View.VISIBLE); } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultEntry.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultEntry.java index 080580c9..c8ad086e 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultEntry.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultEntry.java @@ -27,6 +27,7 @@ public class VaultEntry extends UUIDMap.Value { private String _note = ""; private String _oldGroup; private Set _groups = new TreeSet<>(); + private boolean _isArchived; private VaultEntry(UUID uuid, OtpInfo info) { super(uuid); @@ -66,6 +67,7 @@ public class VaultEntry extends UUIDMap.Value { groupUuids.put(uuid.toString()); } obj.put("groups", groupUuids); + obj.put("archived", _isArchived); } catch (JSONException e) { throw new RuntimeException(e); @@ -90,6 +92,7 @@ public class VaultEntry extends UUIDMap.Value { entry.setIssuer(obj.getString("issuer")); entry.setNote(obj.optString("note", "")); entry.setIsFavorite(obj.optBoolean("favorite", false)); + entry.setIsArchived(obj.optBoolean("archived", 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. @@ -148,6 +151,10 @@ public class VaultEntry extends UUIDMap.Value { return _isFavorite; } + public boolean isArchived() { + return _isArchived; + } + public void setName(String name) { _name = name; } @@ -200,6 +207,10 @@ public class VaultEntry extends UUIDMap.Value { _isFavorite = isFavorite; } + public void setIsArchived(boolean isArchived) { + _isArchived = isArchived; + } + void setOldGroup(String oldGroup) { _oldGroup = oldGroup; } @@ -230,7 +241,8 @@ public class VaultEntry extends UUIDMap.Value { && Objects.equals(getIcon(), entry.getIcon()) && getNote().equals(entry.getNote()) && isFavorite() == entry.isFavorite() - && getGroups().equals(entry.getGroups()); + && getGroups().equals(entry.getGroups()) + && isArchived() == entry.isArchived(); } /** diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java index 7792c953..fb4dbef9 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java @@ -321,7 +321,9 @@ public class VaultRepository { public Collection getUsedGroups() { Set usedGroups = new HashSet<>(); for (VaultEntry entry : getEntries()) { - usedGroups.addAll(entry.getGroups()); + if (!entry.isArchived()) { + usedGroups.addAll(entry.getGroups()); + } } return getGroups().stream() diff --git a/app/src/main/res/layout/fragment_entry_list_view.xml b/app/src/main/res/layout/fragment_entry_list_view.xml index eaacfb4b..4a791163 100644 --- a/app/src/main/res/layout/fragment_entry_list_view.xml +++ b/app/src/main/res/layout/fragment_entry_list_view.xml @@ -63,4 +63,13 @@ + + + diff --git a/app/src/main/res/menu/menu_action_mode.xml b/app/src/main/res/menu/menu_action_mode.xml index ef65ccde..7dbe8277 100644 --- a/app/src/main/res/menu/menu_action_mode.xml +++ b/app/src/main/res/menu/menu_action_mode.xml @@ -46,6 +46,13 @@ android:icon="@drawable/ic_outline_qr_code_2_24" app:showAsAction="always"/> + + Settings About Delete + Restore Transfer Edit icon Reset usage count @@ -247,6 +248,14 @@ Are you sure you want to delete %d entry? Are you sure you want to delete %d entries? + + Archive entry + Archive entries + + + Are you sure you want to archive this entry? + Are you sure you want to archive these entries? + Discard changes? Your changes have not been saved Error saving profile @@ -470,6 +479,7 @@ There are no codes to be shown. Start adding entries by tapping the plus sign in the bottom right corner No entries found + Archive is empty There are no groups to be shown. Add groups in the edit screen of an entry No groups found No icon packs have been imported yet. Tap the plus sign to import one. Tip: try aegis-icons. @@ -546,6 +556,7 @@ Import entries directly from %s. This requires the app to be installed on this device and for root access to be granted to Aegis. Groups + Archive Focus search on app start Focus the search immediately after opening the app.