Implement archive

This commit is contained in:
r3dh3ck 2024-11-08 04:54:50 +00:00
parent c8d5be6462
commit 0d5ba3c0ab
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.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());

View file

@ -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<VaultGroup> groups) {
_groups = groups;
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
if (_prefGroupFilter != null) {
Set<UUID> groupFilter = cleanGroupFilter(_prefGroupFilter);
@ -273,14 +274,18 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private void initializeGroups() {
_groupChip.removeAllViews();
addArchiveChip(_groupChip);
for (VaultGroup group : _groups) {
addChipTo(_groupChip, new VaultGroupModel(group));
}
if (!_groups.isEmpty()) {
GroupPlaceholderType placeholderType = GroupPlaceholderType.NO_GROUP;
addChipTo(_groupChip, new VaultGroupModel(this, placeholderType));
addSaveChip(_groupChip);
}
}
private Set<UUID> cleanGroupFilter(Set<UUID> groupFilter) {
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());
}
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();

View file

@ -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());

View file

@ -79,6 +79,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
private Handler _dimHandler;
private Handler _doubleTapHandler;
private boolean _pauseFocused;
private boolean _isArchiveEnabled;
// keeps track of the EntryHolders that are currently bound
private List<EntryHolder> _holders;
@ -196,7 +197,13 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
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<RecyclerView.ViewHolder>
return _entryList.isErrorCardShown();
}
public void enableArchive(boolean enable) {
_isArchiveEnabled = enable;
refreshEntryList();
}
private class FooterView extends RecyclerView.ViewHolder {
View _footerView;

View file

@ -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())) {
_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);
}
}

View file

@ -27,6 +27,7 @@ public class VaultEntry extends UUIDMap.Value {
private String _note = "";
private String _oldGroup;
private Set<UUID> _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();
}
/**

View file

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

View file

@ -63,4 +63,13 @@
</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>

View file

@ -46,6 +46,13 @@
android:icon="@drawable/ic_outline_qr_code_2_24"
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
android:id="@+id/action_delete"
android:title="@string/action_delete"

View file

@ -5,6 +5,7 @@
<string name="action_settings">Settings</string>
<string name="action_about">About</string>
<string name="action_delete">Delete</string>
<string name="action_restore">Restore</string>
<string name="action_transfer">Transfer</string>
<string name="action_edit_icon">Edit icon</string>
<string name="action_reset_usage_count">Reset usage count</string>
@ -247,6 +248,14 @@
<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>
</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_description">Your changes have not been saved</string>
<string name="saving_profile_error">Error saving profile</string>
@ -470,6 +479,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_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_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>
@ -546,6 +556,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="groups">Groups</string>
<string name="archive">Archive</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>