This commit is contained in:
r3dh3ck 2024-11-25 02:45:31 +01:00 committed by GitHub
commit 0ad392709d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 164 additions and 20 deletions

View file

@ -142,10 +142,20 @@ public class OverallTest extends AegisTest {
onView(withId(R.id.action_share_qr)).perform(click()); onView(withId(R.id.action_share_qr)).perform(click());
onView(withId(R.id.btnNext)).perform(click()).perform(click()).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(allOf(isDescendantOfA(withClassName(containsString("ActionBarContextView"))), withClassName(containsString("OverflowMenuButton")))).perform(click());
onView(withText(R.string.action_delete)).perform(click()); onView(withText(R.string.action_delete)).perform(click());
onView(withId(android.R.id.button1)).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(); openContextualActionModeOverflowMenu();
onView(withText(R.string.lock)).perform(click()); onView(withText(R.string.lock)).perform(click());

View file

@ -96,6 +96,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private boolean _isDPadPressed; private boolean _isDPadPressed;
private boolean _isDoingIntro; private boolean _isDoingIntro;
private boolean _isAuthenticating; private boolean _isAuthenticating;
private boolean _isArchiveEnabled;
private String _submittedSearchQuery; private String _submittedSearchQuery;
private String _pendingSearchQuery; private String _pendingSearchQuery;
@ -187,6 +188,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_isDPadPressed = false; _isDPadPressed = false;
_isDoingIntro = false; _isDoingIntro = false;
_isAuthenticating = false; _isAuthenticating = false;
_isArchiveEnabled = false;
if (savedInstanceState != null) { if (savedInstanceState != null) {
_isRecreated = true; _isRecreated = true;
_pendingSearchQuery = savedInstanceState.getString("pendingSearchQuery"); _pendingSearchQuery = savedInstanceState.getString("pendingSearchQuery");
@ -249,7 +251,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
public void setGroups(Collection<VaultGroup> groups) { public void setGroups(Collection<VaultGroup> groups) {
_groups = groups; _groups = groups;
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
if (_prefGroupFilter != null) { if (_prefGroupFilter != null) {
Set<UUID> groupFilter = cleanGroupFilter(_prefGroupFilter); Set<UUID> groupFilter = cleanGroupFilter(_prefGroupFilter);
@ -273,14 +274,18 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private void initializeGroups() { private void initializeGroups() {
_groupChip.removeAllViews(); _groupChip.removeAllViews();
addArchiveChip(_groupChip);
for (VaultGroup group : _groups) { for (VaultGroup group : _groups) {
addChipTo(_groupChip, new VaultGroupModel(group)); addChipTo(_groupChip, new VaultGroupModel(group));
} }
if (!_groups.isEmpty()) {
GroupPlaceholderType placeholderType = GroupPlaceholderType.NO_GROUP; GroupPlaceholderType placeholderType = GroupPlaceholderType.NO_GROUP;
addChipTo(_groupChip, new VaultGroupModel(this, placeholderType)); addChipTo(_groupChip, new VaultGroupModel(this, placeholderType));
addSaveChip(_groupChip); addSaveChip(_groupChip);
} }
}
private Set<UUID> cleanGroupFilter(Set<UUID> groupFilter) { private Set<UUID> cleanGroupFilter(Set<UUID> groupFilter) {
Set<UUID> groupUuids = _groups.stream().map(UUIDMap.Value::getUUID).collect(Collectors.toSet()); Set<UUID> groupUuids = _groups.stream().map(UUIDMap.Value::getUUID).collect(Collectors.toSet());
@ -290,6 +295,21 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
.collect(Collectors.toSet()); .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) { private void addChipTo(ChipGroup chipGroup, VaultGroupModel group) {
Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false); Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false);
chip.setText(group.getName()); chip.setText(group.getName());
@ -1149,6 +1169,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
} else { } else {
setFavoriteMenuItemVisiblity(); setFavoriteMenuItemVisiblity();
setIsMultipleSelected(_selectedEntries.size() > 1); setIsMultipleSelected(_selectedEntries.size() > 1);
setRestoreMenuItemVisibility();
} }
} }
} }
@ -1191,6 +1212,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 @Override
public void onLongEntryClick(VaultEntry entry) { public void onLongEntryClick(VaultEntry entry) {
if (!_selectedEntries.isEmpty()) { if (!_selectedEntries.isEmpty()) {
@ -1207,6 +1233,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_actionModeBackPressHandler.setEnabled(true); _actionModeBackPressHandler.setEnabled(true);
setFavoriteMenuItemVisiblity(); setFavoriteMenuItemVisiblity();
setAssignIconsMenuItemVisibility(); setAssignIconsMenuItemVisibility();
setRestoreMenuItemVisibility();
} }
@Override @Override
@ -1294,6 +1321,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 { private class SearchViewBackPressHandler extends OnBackPressedCallback {
public SearchViewBackPressHandler() { public SearchViewBackPressHandler() {
super(false); super(false);
@ -1390,16 +1451,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
startActivity(intent); startActivity(intent);
mode.finish(); mode.finish();
} else if (itemId == R.id.action_restore) {
onActionRestore(mode);
} else if (itemId == R.id.action_delete) { } else if (itemId == R.id.action_delete) {
Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries, (d, which) -> { onActionDelete(mode);
for (VaultEntry entry : _selectedEntries) {
_vaultManager.getVault().removeEntry(entry);
}
saveAndBackupVault();
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
_entryListView.setEntries(_vaultManager.getVault().getEntries());
mode.finish();
});
} else if (itemId == R.id.action_select_all) { } else if (itemId == R.id.action_select_all) {
_selectedEntries = _entryListView.selectAllEntries(); _selectedEntries = _entryListView.selectAllEntries();
setFavoriteMenuItemVisiblity(); setFavoriteMenuItemVisiblity();

View file

@ -96,6 +96,19 @@ public class Dialogs {
.create()); .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) { private static String getVaultEntryName(Context context, VaultEntry entry) {
if (!entry.getIssuer().isEmpty() && !entry.getName().isEmpty()) { if (!entry.getIssuer().isEmpty() && !entry.getName().isEmpty()) {
return String.format("%s (%s)", entry.getIssuer(), entry.getName()); return String.format("%s (%s)", entry.getIssuer(), entry.getName());

View file

@ -79,6 +79,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
private Handler _dimHandler; private Handler _dimHandler;
private Handler _doubleTapHandler; private Handler _doubleTapHandler;
private boolean _pauseFocused; private boolean _pauseFocused;
private boolean _isArchiveEnabled;
// keeps track of the EntryHolders that are currently bound // keeps track of the EntryHolders that are currently bound
private List<EntryHolder> _holders; private List<EntryHolder> _holders;
@ -196,7 +197,13 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
String name = entry.getName().toLowerCase(); String name = entry.getName().toLowerCase();
String note = entry.getNote().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)) { if (groups.isEmpty() && !_groupFilter.contains(null)) {
return true; return true;
} }
@ -774,6 +781,11 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
return _entryList.isErrorCardShown(); return _entryList.isErrorCardShown();
} }
public void enableArchive(boolean enable) {
_isArchiveEnabled = enable;
refreshEntryList();
}
private class FooterView extends RecyclerView.ViewHolder { private class FooterView extends RecyclerView.ViewHolder {
View _footerView; View _footerView;

View file

@ -14,6 +14,7 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.animation.LayoutAnimationController; import android.view.animation.LayoutAnimationController;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.AttrRes; import androidx.annotation.AttrRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -72,6 +73,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
private boolean _showExpirationState; private boolean _showExpirationState;
private ViewMode _viewMode; private ViewMode _viewMode;
private LinearLayout _emptyStateView; private LinearLayout _emptyStateView;
private boolean _isArchiveEnabled;
private TextView _archiveEmptyText;
private UiRefresher _refresher; private UiRefresher _refresher;
@ -150,6 +153,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}); });
_emptyStateView = view.findViewById(R.id.vEmptyList); _emptyStateView = view.findViewById(R.id.vEmptyList);
_archiveEmptyText = view.findViewById(R.id.archive_empty_text);
return view; return view;
} }
@ -471,6 +475,12 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_recyclerView.scheduleLayoutAnimation(); _recyclerView.scheduleLayoutAnimation();
} }
public void enableArchive(boolean enable) {
_isArchiveEnabled = enable;
_adapter.enableArchive(enable);
updateEmptyState();
}
private void setShowProgress(boolean showProgress) { private void setShowProgress(boolean showProgress) {
_showProgress = showProgress; _showProgress = showProgress;
updateDividerDecoration(); updateDividerDecoration();
@ -495,9 +505,12 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
if (_adapter.getShownEntriesCount() > 0) { if (_adapter.getShownEntriesCount() > 0) {
_recyclerView.setVisibility(View.VISIBLE); _recyclerView.setVisibility(View.VISIBLE);
_emptyStateView.setVisibility(View.GONE); _emptyStateView.setVisibility(View.GONE);
} else { _archiveEmptyText.setVisibility(View.GONE);
if (Strings.isNullOrEmpty(_adapter.getSearchFilter())) { } else if (Strings.isNullOrEmpty(_adapter.getSearchFilter())) {
_recyclerView.setVisibility(View.GONE); _recyclerView.setVisibility(View.GONE);
if (_isArchiveEnabled) {
_archiveEmptyText.setVisibility(View.VISIBLE);
} else {
_emptyStateView.setVisibility(View.VISIBLE); _emptyStateView.setVisibility(View.VISIBLE);
} }
} }

View file

@ -27,6 +27,7 @@ public class VaultEntry extends UUIDMap.Value {
private String _note = ""; private String _note = "";
private String _oldGroup; private String _oldGroup;
private Set<UUID> _groups = new TreeSet<>(); private Set<UUID> _groups = new TreeSet<>();
private boolean _isArchived;
private VaultEntry(UUID uuid, OtpInfo info) { private VaultEntry(UUID uuid, OtpInfo info) {
super(uuid); super(uuid);
@ -66,6 +67,7 @@ public class VaultEntry extends UUIDMap.Value {
groupUuids.put(uuid.toString()); groupUuids.put(uuid.toString());
} }
obj.put("groups", groupUuids); obj.put("groups", groupUuids);
obj.put("archived", _isArchived);
} catch (JSONException e) { } catch (JSONException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
@ -90,6 +92,7 @@ public class VaultEntry extends UUIDMap.Value {
entry.setIssuer(obj.getString("issuer")); entry.setIssuer(obj.getString("issuer"));
entry.setNote(obj.optString("note", "")); entry.setNote(obj.optString("note", ""));
entry.setIsFavorite(obj.optBoolean("favorite", false)); 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 // 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. // 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; return _isFavorite;
} }
public boolean isArchived() {
return _isArchived;
}
public void setName(String name) { public void setName(String name) {
_name = name; _name = name;
} }
@ -200,6 +207,10 @@ public class VaultEntry extends UUIDMap.Value {
_isFavorite = isFavorite; _isFavorite = isFavorite;
} }
public void setIsArchived(boolean isArchived) {
_isArchived = isArchived;
}
void setOldGroup(String oldGroup) { void setOldGroup(String oldGroup) {
_oldGroup = oldGroup; _oldGroup = oldGroup;
} }
@ -230,7 +241,8 @@ public class VaultEntry extends UUIDMap.Value {
&& Objects.equals(getIcon(), entry.getIcon()) && Objects.equals(getIcon(), entry.getIcon())
&& getNote().equals(entry.getNote()) && getNote().equals(entry.getNote())
&& isFavorite() == entry.isFavorite() && isFavorite() == entry.isFavorite()
&& getGroups().equals(entry.getGroups()); && getGroups().equals(entry.getGroups())
&& isArchived() == entry.isArchived();
} }
/** /**

View file

@ -321,8 +321,10 @@ public class VaultRepository {
public Collection<VaultGroup> getUsedGroups() { public Collection<VaultGroup> getUsedGroups() {
Set<UUID> usedGroups = new HashSet<>(); Set<UUID> usedGroups = new HashSet<>();
for (VaultEntry entry : getEntries()) { for (VaultEntry entry : getEntries()) {
if (!entry.isArchived()) {
usedGroups.addAll(entry.getGroups()); usedGroups.addAll(entry.getGroups());
} }
}
return getGroups().stream() return getGroups().stream()
.filter(vg -> usedGroups.contains(vg.getUUID())) .filter(vg -> usedGroups.contains(vg.getUUID()))

View file

@ -63,4 +63,13 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<TextView
android:id="@+id/archive_empty_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/archive_empty"
android:gravity="center"
android:visibility="gone" />
</LinearLayout> </LinearLayout>

View file

@ -46,6 +46,13 @@
android:icon="@drawable/ic_outline_qr_code_2_24" android:icon="@drawable/ic_outline_qr_code_2_24"
app:showAsAction="always"/> app:showAsAction="always"/>
<item
android:id="@+id/action_restore"
android:title="@string/action_restore"
android:orderInCategory="120"
android:icon="@drawable/ic_outline_refresh_24"
app:showAsAction="ifRoom"/>
<item <item
android:id="@+id/action_delete" android:id="@+id/action_delete"
android:title="@string/action_delete" android:title="@string/action_delete"

View file

@ -5,6 +5,7 @@
<string name="action_settings">Settings</string> <string name="action_settings">Settings</string>
<string name="action_about">About</string> <string name="action_about">About</string>
<string name="action_delete">Delete</string> <string name="action_delete">Delete</string>
<string name="action_restore">Restore</string>
<string name="action_transfer">Transfer</string> <string name="action_transfer">Transfer</string>
<string name="action_edit_icon">Edit icon</string> <string name="action_edit_icon">Edit icon</string>
<string name="action_reset_usage_count">Reset usage count</string> <string name="action_reset_usage_count">Reset usage count</string>
@ -253,6 +254,14 @@
<item quantity="one">Are you sure you want to delete %d entry?</item> <item quantity="one">Are you sure you want to delete %d entry?</item>
<item quantity="other">Are you sure you want to delete %d entries?</item> <item quantity="other">Are you sure you want to delete %d entries?</item>
</plurals> </plurals>
<plurals name="archive_entry">
<item quantity="one">Archive entry</item>
<item quantity="other">Archive entries</item>
</plurals>
<plurals name="archive_entry_description">
<item quantity="one">Are you sure you want to archive this entry?</item>
<item quantity="other">Are you sure you want to archive these entries?</item>
</plurals>
<string name="discard_changes">Discard changes?</string> <string name="discard_changes">Discard changes?</string>
<string name="discard_changes_description">Your changes have not been saved</string> <string name="discard_changes_description">Your changes have not been saved</string>
<string name="saving_profile_error">Error saving profile</string> <string name="saving_profile_error">Error saving profile</string>
@ -476,6 +485,7 @@
<string name="empty_list">There are no codes to be shown. Start adding entries by tapping the plus sign in the bottom right corner</string> <string name="empty_list">There are no codes to be shown. Start adding entries by tapping the plus sign in the bottom right corner</string>
<string name="empty_list_title">No entries found</string> <string name="empty_list_title">No entries found</string>
<string name="archive_empty">Archive is empty</string>
<string name="empty_group_list">There are no groups to be shown. Add groups in the edit screen of an entry</string> <string name="empty_group_list">There are no groups to be shown. Add groups in the edit screen of an entry</string>
<string name="empty_group_list_title">No groups found</string> <string name="empty_group_list_title">No groups found</string>
<string name="no_icon_packs">No icon packs have been imported yet. Tap the plus sign to import one. Tip: try <a href="https://aegis-icons.github.io">aegis-icons</a>.</string> <string name="no_icon_packs">No icon packs have been imported yet. Tap the plus sign to import one. Tip: try <a href="https://aegis-icons.github.io">aegis-icons</a>.</string>
@ -554,6 +564,7 @@
<string name="importer_help_direct">Import entries directly from %s. This requires the app to be installed on this device and for root access to be granted to Aegis.</string> <string name="importer_help_direct">Import entries directly from %s. This requires the app to be installed on this device and for root access to be granted to Aegis.</string>
<string name="groups">Groups</string> <string name="groups">Groups</string>
<string name="archive">Archive</string>
<string name="pref_focus_search">Focus search on app start</string> <string name="pref_focus_search">Focus search on app start</string>
<string name="pref_focus_search_summary">Focus the search immediately after opening the app.</string> <string name="pref_focus_search_summary">Focus the search immediately after opening the app.</string>