mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-04-19 13:29:13 +00:00
Implement archive
This commit is contained in:
parent
c8d5be6462
commit
0d5ba3c0ab
10 changed files with 164 additions and 20 deletions
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue