diff --git a/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java b/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java index 016eb5c6d8..79e1e47509 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 void testOverall() { 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 c3f3b0f9d4..47cfda2f41 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 @@ protected void onCreate(Bundle savedInstanceState) { _isDPadPressed = false; _isDoingIntro = false; _isAuthenticating = false; + _isArchiveEnabled = false; if (savedInstanceState != null) { _isRecreated = true; _pendingSearchQuery = savedInstanceState.getString("pendingSearchQuery"); @@ -249,7 +251,6 @@ protected void onCreate(Bundle savedInstanceState) { 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 void setGroups(Collection groups) { 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 @@ private Set cleanGroupFilter(Set groupFilter) { .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 void onEntryClick(VaultEntry entry) { } else { setFavoriteMenuItemVisiblity(); setIsMultipleSelected(_selectedEntries.size() > 1); + setRestoreMenuItemVisibility(); } } } @@ -1195,6 +1216,11 @@ private void setFavoriteMenuItemVisiblity() { } } + 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 @@ private void startActionMode() { _actionModeBackPressHandler.setEnabled(true); setFavoriteMenuItemVisiblity(); setAssignIconsMenuItemVisibility(); + setRestoreMenuItemVisibility(); } @Override @@ -1298,6 +1325,40 @@ private void copyEntryCode(VaultEntry entry) { } } + 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 boolean onActionItemClicked(ActionMode mode, MenuItem item) { 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 2a76f0832f..c30fede6e9 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 static void showDeleteEntriesDialog(Context context, List ser .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 a811f027f5..08e2229f57 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 @@ private boolean isEntryFiltered(VaultEntry entry) { 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 boolean isErrorCardShown() { 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 500198c026..312398c639 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.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 long getMillisTillNextRefresh() { }); _emptyStateView = view.findViewById(R.id.vEmptyList); + _archiveEmptyText = view.findViewById(R.id.archive_empty_text); return view; } @@ -471,6 +475,12 @@ public void runEntriesAnimation() { _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 @@ private void updateEmptyState() { 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 080580c952..c8ad086ef6 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 JSONObject toJson() { groupUuids.put(uuid.toString()); } obj.put("groups", groupUuids); + obj.put("archived", _isArchived); } catch (JSONException e) { throw new RuntimeException(e); @@ -90,6 +92,7 @@ public static VaultEntry fromJson(JSONObject obj) throws VaultEntryException { 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 boolean isFavorite() { return _isFavorite; } + public boolean isArchived() { + return _isArchived; + } + public void setName(String name) { _name = name; } @@ -200,6 +207,10 @@ public void setIsFavorite(boolean isFavorite) { _isFavorite = isFavorite; } + public void setIsArchived(boolean isArchived) { + _isArchived = isArchived; + } + void setOldGroup(String oldGroup) { _oldGroup = oldGroup; } @@ -230,7 +241,8 @@ && getInfo().equals(entry.getInfo()) && 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 7792c953bc..fb4dbef930 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 Collection getGroups() { 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 eaacfb4b61..4a7911630a 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 ef65ccde8f..7dbe82777d 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.