From 9c151d83c1784733e46279186efb1ba1d3c22982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Sch=C3=A4ttgen?= Date: Tue, 27 Aug 2024 22:53:25 +0200 Subject: [PATCH] Improve group filters --- .../beemdevelopment/aegis/OverallTest.java | 6 +- .../aegis/ui/MainActivity.java | 179 +++++++++++++++++- .../aegis/ui/views/EntryListView.java | 158 +--------------- app/src/main/res/layout/activity_main.xml | 44 +++-- .../main/res/layout/dialog_select_groups.xml | 1 - .../res/layout/fragment_entry_list_view.xml | 15 -- app/src/main/res/values/strings.xml | 1 + 7 files changed, 215 insertions(+), 189 deletions(-) diff --git a/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java b/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java index cc921be732..903efeda7e 100644 --- a/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java +++ b/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java @@ -177,12 +177,10 @@ private void changeSort(@IdRes int resId) { } private void changeGroupFilter(String text) { - onView(withId(R.id.chip_group)).perform(click()); if (text == null) { - onView(withId(R.id.btnClear)).perform(click()); + onView(allOf(withText(R.string.all), isDescendantOfA(withId(R.id.groupChipGroup)))).perform(click()); } else { - onView(withText(text)).perform(click()); - onView(isRoot()).perform(pressBack()); + onView(allOf(withText(text), isDescendantOfA(withId(R.id.groupChipGroup)))).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 82680e4f3e..1418c72e9a 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java @@ -32,6 +32,7 @@ import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.view.ActionMode; import androidx.appcompat.widget.SearchView; @@ -50,21 +51,29 @@ import com.beemdevelopment.aegis.ui.fragments.preferences.BackupsPreferencesFragment; import com.beemdevelopment.aegis.ui.fragments.preferences.PreferencesFragment; import com.beemdevelopment.aegis.ui.models.ErrorCardInfo; +import com.beemdevelopment.aegis.ui.models.VaultGroupModel; import com.beemdevelopment.aegis.ui.tasks.QrDecodeTask; import com.beemdevelopment.aegis.ui.views.EntryListView; import com.beemdevelopment.aegis.util.TimeUtils; +import com.beemdevelopment.aegis.util.UUIDMap; import com.beemdevelopment.aegis.vault.VaultEntry; import com.beemdevelopment.aegis.vault.VaultFile; +import com.beemdevelopment.aegis.vault.VaultGroup; import com.beemdevelopment.aegis.vault.VaultRepository; import com.beemdevelopment.aegis.vault.VaultRepositoryException; import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.google.android.material.chip.Chip; +import com.google.android.material.chip.ChipGroup; +import com.google.android.material.color.MaterialColors; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.common.base.Strings; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -91,6 +100,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene private SearchView _searchView; private EntryListView _entryListView; + private Collection _groups; + private ChipGroup _groupChip; + private Set _groupFilter; + private Set _prefGroupFilter; + private FabScrollHelper _fabScrollHelper; private ActionMode _actionMode; @@ -196,7 +210,7 @@ protected void onCreate(Bundle savedInstanceState) { _entryListView.setViewMode(_prefs.getCurrentViewMode()); _entryListView.setCopyBehavior(_prefs.getCopyBehavior()); _entryListView.setSearchBehaviorMask(_prefs.getSearchBehaviorMask()); - _entryListView.setPrefGroupFilter(_prefs.getGroupFilter()); + _prefGroupFilter = _prefs.getGroupFilter(); FloatingActionButton fab = findViewById(R.id.fab); fab.setOnClickListener(v -> { @@ -220,10 +234,150 @@ protected void onCreate(Bundle savedInstanceState) { Dialogs.showSecureDialog(dialog); }); + _groupChip = findViewById(R.id.groupChipGroup); _fabScrollHelper = new FabScrollHelper(fab); _selectedEntries = new ArrayList<>(); } + public void setGroups(Collection groups) { + _groups = groups; + _groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE); + + if (_prefGroupFilter != null) { + Set groupFilter = cleanGroupFilter(_prefGroupFilter); + _prefGroupFilter = null; + if (!groupFilter.isEmpty()) { + _groupFilter = groupFilter; + _entryListView.setGroupFilter(groupFilter, false); + } + } else if (_groupFilter != null) { + Set groupFilter = cleanGroupFilter(_groupFilter); + if (!_groupFilter.equals(groupFilter)) { + _groupFilter = groupFilter; + _entryListView.setGroupFilter(groupFilter, true); + } + } + + _entryListView.setGroups(groups); + initializeGroups(); + } + + private void initializeGroups() { + _groupChip.removeAllViews(); + + addChipTo(_groupChip, new VaultGroupModel(getString(R.string.all))); + + for (VaultGroup group : _groups) { + addChipTo(_groupChip, new VaultGroupModel(group)); + } + + addSaveChip(_groupChip); + } + + private Set cleanGroupFilter(Set groupFilter) { + Set groupUuids = _groups.stream().map(UUIDMap.Value::getUUID).collect(Collectors.toSet()); + + return groupFilter.stream() + .filter(g -> g == null || groupUuids.contains(g)) + .collect(Collectors.toSet()); + } + + private void addChipTo(ChipGroup chipGroup, VaultGroupModel group) { + Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false); + chip.setText(group.getName()); + chip.setCheckable(true); + chip.setCheckedIconVisible(false); + chip.setChecked(_groupFilter != null && _groupFilter.contains(group.getUUID())); + + if (group.isPlaceholder()) { + chip.setId(0); + chip.setTag(null); + chip.setChecked(_groupFilter == null); + chip.setOnClickListener(v -> { + setSaveChipVisibility(true); + + Chip checkedChip = (Chip) chipGroup.getChildAt(0); + boolean checkedState = checkedChip.isChecked(); + chipGroup.clearCheck(); + + Set groupFilter = getGroupFilter(chipGroup); + if (!checkedState) { + groupFilter = new HashSet<>(); + groupFilter.add(null); + + checkedChip.setChecked(false); + } else { + checkedChip.setChecked(true); + } + + _groupFilter = groupFilter; + _entryListView.setGroupFilter(groupFilter, true); + }); + + chipGroup.addView(chip); + return; + } + + + chip.setOnCheckedChangeListener((group1, checkedId) -> { + setSaveChipVisibility(true); + Set groupFilter = getGroupFilter(chipGroup); + + if (groupFilter.isEmpty()) { + groupFilter.add(null); + } else { + Chip allGroupsChip = (Chip) chipGroup.getChildAt(0); + allGroupsChip.setChecked(false); + } + + _groupFilter = groupFilter; + _entryListView.setGroupFilter(groupFilter, true); + }); + + chip.setTag(group); + chipGroup.addView(chip); + } + + private void addSaveChip(ChipGroup chipGroup) { + Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false); + + chip.setText(getString(R.string.save)); + chip.setVisibility(View.GONE); + chip.setChipStrokeWidth(0); + chip.setCheckable(false); + chip.setChipBackgroundColorResource(android.R.color.transparent); + chip.setTextColor(MaterialColors.getColor(chip.getRootView(), com.google.android.material.R.attr.colorSecondary)); + chip.setClickable(true); + chip.setCheckedIconVisible(false); + chip.setOnClickListener(v -> { + onSaveGroupFilter(_groupFilter); + setSaveChipVisibility(false); + }); + + chipGroup.addView(chip); + } + + private void setSaveChipVisibility(boolean visible) { + Chip saveChip = (Chip) _groupChip.getChildAt(_groupChip.getChildCount() - 1); + saveChip.setChecked(false); + saveChip.setVisibility(visible ? View.VISIBLE : View.GONE); + } + + private static Set getGroupFilter(ChipGroup chipGroup) { + return chipGroup.getCheckedChipIds().stream() + .map(i -> { + Chip chip = chipGroup.findViewById(i); + if (chip.getTag() != null) { + VaultGroupModel group = (VaultGroupModel) chip.getTag(); + return group.getUUID(); + } + + return null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + @Override protected void onDestroy() { _entryListView.setListener(null); @@ -252,6 +406,10 @@ protected void onSaveInstanceState(@NonNull Bundle instance) { instance.putString("submittedSearchQuery", _submittedSearchQuery); instance.putBoolean("isDoingIntro", _isDoingIntro); instance.putBoolean("isAuthenticating", _isAuthenticating); + + if (_groupFilter != null) { + instance.putSerializable("prefGroupFilter", new HashSet<>(_groupFilter)); + } } @Override @@ -677,7 +835,7 @@ protected void onStart() { startAuthActivity(false); } else if (_loaded) { // update the list of groups in the entry list view so that the chip gets updated - _entryListView.setGroups(_vaultManager.getVault().getUsedGroups()); + setGroups(_vaultManager.getVault().getUsedGroups()); // update the usage counts in case they are edited outside of the EntryListView _entryListView.setUsageCounts(_prefs.getUsageCounts()); @@ -717,7 +875,7 @@ public boolean onCreateOptionsMenu(Menu menu) { updateLockIcon(); if (_loaded) { - _entryListView.setGroups(_vaultManager.getVault().getUsedGroups()); + setGroups(_vaultManager.getVault().getUsedGroups()); updateSortCategoryMenu(); } @@ -836,7 +994,7 @@ private void collapseSearchView() { private void loadEntries() { if (!_loaded) { - _entryListView.setGroups(_vaultManager.getVault().getUsedGroups()); + setGroups(_vaultManager.getVault().getUsedGroups()); _entryListView.setUsageCounts(_prefs.getUsageCounts()); _entryListView.setLastUsedTimestamps(_prefs.getLastUsedTimestamps()); _entryListView.addEntries(_vaultManager.getVault().getEntries()); @@ -930,6 +1088,19 @@ private void showPlaintextExportWarningOptions() { Dialogs.showSecureDialog(dialog); } + @Override + public void onRestoreInstanceState(@Nullable Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + if (savedInstanceState == null) { + return; + } + + HashSet filter = (HashSet) savedInstanceState.getSerializable("prefGroupFilter"); + if (filter != null) { + _prefGroupFilter = filter; + } + } + @Override public void onEntryClick(VaultEntry entry) { if (_actionMode != null) { 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 6bd4fb6847..b44db60ecb 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 @@ -13,15 +13,12 @@ import android.view.View; import android.view.ViewGroup; import android.view.animation.LayoutAnimationController; -import android.widget.Button; import android.widget.LinearLayout; import androidx.annotation.AttrRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StyleRes; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.core.widget.NestedScrollView; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.ItemTouchHelper; @@ -39,10 +36,8 @@ import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback; import com.beemdevelopment.aegis.helpers.UiRefresher; import com.beemdevelopment.aegis.otp.TotpInfo; -import com.beemdevelopment.aegis.ui.dialogs.Dialogs; import com.beemdevelopment.aegis.ui.glide.GlideHelper; import com.beemdevelopment.aegis.ui.models.ErrorCardInfo; -import com.beemdevelopment.aegis.ui.models.VaultGroupModel; import com.beemdevelopment.aegis.util.UUIDMap; import com.beemdevelopment.aegis.vault.VaultEntry; import com.beemdevelopment.aegis.vault.VaultGroup; @@ -51,11 +46,7 @@ import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader; import com.bumptech.glide.util.ViewPreloadSizeProvider; -import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.android.material.card.MaterialCardView; -import com.google.android.material.chip.Chip; -import com.google.android.material.chip.ChipGroup; import com.google.android.material.shape.CornerFamily; import com.google.android.material.shape.ShapeAppearanceModel; import com.google.common.base.Strings; @@ -81,11 +72,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener { private TotpProgressBar _progressBar; private boolean _showProgress; private ViewMode _viewMode; - private Collection _groups; private LinearLayout _emptyStateView; - private Chip _groupChip; - private Set _groupFilter; - private Set _prefGroupFilter; private UiRefresher _refresher; @@ -106,8 +93,6 @@ public void onDestroy() { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_entry_list_view, container, false); _progressBar = view.findViewById(R.id.progressBar); - _groupChip = view.findViewById(R.id.chip_group); - initializeGroupChip(); // set up the recycler view _recyclerView = view.findViewById(R.id.rvKeyProfiles); @@ -173,41 +158,21 @@ public void setPreloadView(View view) { _preloadSizeProvider.setView(view); } - @Override - public void onViewStateRestored(@Nullable Bundle savedInstanceState) { - super.onViewStateRestored(savedInstanceState); - if (savedInstanceState == null) { - return; - } - - HashSet filter = (HashSet) savedInstanceState.getSerializable("prefGroupFilter"); - if (filter != null) { - _prefGroupFilter = filter; - } - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - // user can apply _groupFilter without saving - // restore _groupFilter as _prefGroupFilter in order to reapply correct filter after screen rotate - if (_groupFilter != null) { - outState.putSerializable("prefGroupFilter", new HashSet<>(_groupFilter)); - } - } - @Override public void onDestroyView() { _refresher.destroy(); super.onDestroyView(); } + public void setGroups(Collection groups) { + _adapter.setGroups(groups); + updateDividerDecoration(); + } + public void setGroupFilter(Set groups, boolean animate) { - _groupFilter = groups; _adapter.setGroupFilter(groups); _touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed()); updateEmptyState(); - updateGroupChip(); if (animate) { runEntriesAnimation(); @@ -384,10 +349,6 @@ public void onListChange() { } } - public void setPrefGroupFilter(Set groupFilter) { - _prefGroupFilter = groupFilter; - } - public void setCodeGroupSize(Preferences.CodeGrouping codeGrouping) { _adapter.setCodeGroupSize(codeGrouping); } @@ -519,116 +480,11 @@ public void runEntriesAnimation() { _recyclerView.scheduleLayoutAnimation(); } - private void addChipTo(ChipGroup chipGroup, VaultGroupModel group) { - Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false); - chip.setText(group.getName()); - chip.setCheckable(true); - chip.setChecked(_groupFilter != null && _groupFilter.contains(group.getUUID())); - chip.setCheckedIconVisible(true); - chip.setOnCheckedChangeListener((group1, checkedId) -> { - Set groupFilter = getGroupFilter(chipGroup); - setGroupFilter(groupFilter, true); - }); - chip.setTag(group); - chipGroup.addView(chip); - } - - private void initializeGroupChip() { - View view = getLayoutInflater().inflate(R.layout.dialog_select_groups, null); - BottomSheetDialog dialog = new BottomSheetDialog(requireContext()); - NestedScrollView scrollView = view.findViewById(R.id.scrollView); - ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) scrollView.getLayoutParams(); - layoutParams.matchConstraintMaxHeight = getResources().getConfiguration().screenHeightDp; - - dialog.getBehavior().setState(BottomSheetBehavior.STATE_EXPANDED); - dialog.getBehavior().setSkipCollapsed(false); - dialog.setContentView(view); - - ChipGroup chipGroup = view.findViewById(R.id.groupChipGroup); - Button clearButton = view.findViewById(R.id.btnClear); - Button saveButton = view.findViewById(R.id.btnSave); - clearButton.setOnClickListener(v -> { - chipGroup.clearCheck(); - Set groupFilter = Collections.emptySet(); - if (_listener != null) { - _listener.onSaveGroupFilter(groupFilter); - } - setGroupFilter(groupFilter, true); - dialog.dismiss(); - }); - - saveButton.setOnClickListener(v -> { - Set groupFilter = getGroupFilter(chipGroup); - if (_listener != null) { - _listener.onSaveGroupFilter(groupFilter); - } - setGroupFilter(groupFilter, true); - dialog.dismiss(); - }); - - _groupChip.setOnClickListener(v -> { - chipGroup.removeAllViews(); - - for (VaultGroup group : _groups) { - addChipTo(chipGroup, new VaultGroupModel(group)); - } - addChipTo(chipGroup, new VaultGroupModel(getString(R.string.no_group))); - - Dialogs.showSecureDialog(dialog); - }); - } - - private static Set getGroupFilter(ChipGroup chipGroup) { - return chipGroup.getCheckedChipIds().stream() - .map(i -> { - Chip chip = chipGroup.findViewById(i); - VaultGroupModel group = (VaultGroupModel) chip.getTag(); - return group.getUUID(); - }) - .collect(Collectors.toSet()); - } - - private void updateGroupChip() { - if (_groupFilter.isEmpty()) { - _groupChip.setText(R.string.groups); - } else { - _groupChip.setText(String.format("%s (%d)", getString(R.string.groups), _groupFilter.size())); - } - } - private void setShowProgress(boolean showProgress) { _showProgress = showProgress; updateDividerDecoration(); } - public void setGroups(Collection groups) { - _groups = groups; - _adapter.setGroups(groups); - _groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE); - updateDividerDecoration(); - - if (_prefGroupFilter != null) { - Set groupFilter = cleanGroupFilter(_prefGroupFilter); - _prefGroupFilter = null; - if (!groupFilter.isEmpty()) { - setGroupFilter(groupFilter, false); - } - } else if (_groupFilter != null) { - Set groupFilter = cleanGroupFilter(_groupFilter); - if (!_groupFilter.equals(groupFilter)) { - setGroupFilter(groupFilter, true); - } - } - } - - private Set cleanGroupFilter(Set groupFilter) { - Set groupUuids = _groups.stream().map(UUIDMap.Value::getUUID).collect(Collectors.toSet()); - - return groupFilter.stream() - .filter(g -> g == null || groupUuids.contains(g)) - .collect(Collectors.toSet()); - } - private void updateDividerDecoration() { if (_itemDecoration != null) { _recyclerView.removeItemDecoration(_itemDecoration); @@ -705,8 +561,8 @@ public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull R } int entryIndex = _adapter.translateEntryPosToIndex(adapterPosition); - // The first entry should have a top margin, but only if the group chip is not shown and the error card is not shown - if (entryIndex == 0 && (_groups == null || _groups.isEmpty()) && !_adapter.isErrorCardShown()) { + // The first entry should have a top margin, but only if the error card is not shown + if (entryIndex == 0 && !_adapter.isErrorCardShown()) { outRect.top = _offset; } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index ccc10366dd..8462469df5 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,6 +1,5 @@ - + + + + + + + + + + - - - - + android:orientation="vertical" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> @@ -48,6 +64,6 @@ android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="@dimen/fab_margin" - android:src="@drawable/ic_outline_add_24" /> + android:src="@drawable/ic_outline_add_24" /> diff --git a/app/src/main/res/layout/dialog_select_groups.xml b/app/src/main/res/layout/dialog_select_groups.xml index a8900bc799..5e418a4e62 100644 --- a/app/src/main/res/layout/dialog_select_groups.xml +++ b/app/src/main/res/layout/dialog_select_groups.xml @@ -86,7 +86,6 @@ 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 bd452d12bf..eaacfb4b61 100644 --- a/app/src/main/res/layout/fragment_entry_list_view.xml +++ b/app/src/main/res/layout/fragment_entry_list_view.xml @@ -14,21 +14,6 @@ android:visibility="gone" android:max="5000"/> - - Restore default icon Discard Save + All Issuer PIN (4–16 digits) PIN (4 digits)