diff --git a/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java b/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java index 6c12809e..cc921be7 100644 --- a/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java +++ b/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java @@ -7,6 +7,7 @@ import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard; import static androidx.test.espresso.action.ViewActions.longClick; import static androidx.test.espresso.action.ViewActions.pressBack; +import static androidx.test.espresso.action.ViewActions.scrollTo; import static androidx.test.espresso.action.ViewActions.typeText; import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant; import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA; @@ -118,10 +119,11 @@ public class OverallTest extends AegisTest { onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 1, longClick())); onView(withId(R.id.action_edit)).perform(click()); onView(withId(R.id.text_name)).perform(clearText(), typeText("Bob"), closeSoftKeyboard()); - onView(withId(R.id.dropdown_group)).perform(click()); - onView(withText(R.string.new_group)).inRoot(RootMatchers.isPlatformPopup()).perform(click()); + onView(withId(R.id.text_group)).perform(click()); + onView(withId(R.id.addGroup)).inRoot(RootMatchers.isDialog()).perform(click()); onView(withId(R.id.text_input)).perform(typeText(_groupName), closeSoftKeyboard()); onView(withId(android.R.id.button1)).perform(click()); + onView(withText(R.string.save)).perform(click()); onView(isRoot()).perform(pressBack()); onView(withId(android.R.id.button1)).perform(click()); @@ -188,7 +190,7 @@ public class OverallTest extends AegisTest { onView(withId(R.id.fab)).perform(click()); onView(withId(R.id.fab_enter)).perform(click()); - onView(withId(R.id.accordian_header)).perform(click()); + onView(withId(R.id.accordian_header)).perform(scrollTo(), click()); onView(withId(R.id.text_name)).perform(typeText(entry.getName()), closeSoftKeyboard()); onView(withId(R.id.text_issuer)).perform(typeText(entry.getIssuer()), closeSoftKeyboard()); @@ -208,7 +210,7 @@ public class OverallTest extends AegisTest { throw new RuntimeException(String.format("Unexpected entry type: %s", entry.getInfo().getClass().getSimpleName())); } - onView(withId(R.id.dropdown_type)).perform(click()); + onView(withId(R.id.dropdown_type)).perform(scrollTo(), click()); onView(withText(otpType)).inRoot(RootMatchers.isPlatformPopup()).perform(click()); } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java index c2280cc5..5c690e6a 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java @@ -1,6 +1,5 @@ package com.beemdevelopment.aegis.ui; -import android.content.DialogInterface; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; @@ -16,6 +15,7 @@ import android.view.animation.AccelerateInterpolator; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.widget.AutoCompleteTextView; +import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; @@ -68,6 +68,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; 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.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; @@ -85,7 +87,6 @@ import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Locale; -import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; @@ -107,6 +108,8 @@ public class EditEntryActivity extends AegisActivity { private TextInputEditText _textName; private TextInputEditText _textIssuer; + private TextInputLayout _textGroupLayout; + private TextInputEditText _textGroup; private TextInputEditText _textPeriodCounter; private TextInputLayout _textPeriodCounterLayout; private TextInputEditText _textDigits; @@ -121,8 +124,7 @@ public class EditEntryActivity extends AegisActivity { private AutoCompleteTextView _dropdownType; private AutoCompleteTextView _dropdownAlgo; private TextInputLayout _dropdownAlgoLayout; - private AutoCompleteTextView _dropdownGroup; - private List _dropdownGroupList = new ArrayList<>(); + private List _selectedGroups = new ArrayList<>(); private KropView _kropView; @@ -195,6 +197,8 @@ public class EditEntryActivity extends AegisActivity { _saveImageButton = findViewById(R.id.iv_saveImage); _textName = findViewById(R.id.text_name); _textIssuer = findViewById(R.id.text_issuer); + _textGroup = findViewById(R.id.text_group); + _textGroupLayout = findViewById(R.id.text_group_layout); _textPeriodCounter = findViewById(R.id.text_period_counter); _textPeriodCounterLayout = findViewById(R.id.text_period_counter_layout); _textDigits = findViewById(R.id.text_digits); @@ -210,9 +214,6 @@ public class EditEntryActivity extends AegisActivity { _dropdownAlgoLayout = findViewById(R.id.dropdown_algo_layout); _dropdownAlgo = findViewById(R.id.dropdown_algo); DropdownHelper.fillDropdown(this, _dropdownAlgo, R.array.otp_algo_array); - _dropdownGroup = findViewById(R.id.dropdown_group); - updateGroupDropdownList(); - DropdownHelper.fillDropdown(this, _dropdownGroup, _dropdownGroupList); // if this is NOT a manually entered entry, move the "Secret" field from basic to advanced settings if (!_isNew || !_isManual) { @@ -286,10 +287,15 @@ public class EditEntryActivity extends AegisActivity { Set groups = _origEntry.getGroups(); if (groups.isEmpty()) { - setGroup(new VaultGroupModel(getString(R.string.no_group))); + _textGroup.setText(getString(R.string.no_group)); } else { - VaultGroup group = _vaultManager.getVault().getGroupByUUID(groups.iterator().next()); - setGroup(new VaultGroupModel(group)); + String text = groups.stream().map(uuid -> { + VaultGroup group = _vaultManager.getVault().getGroupByUUID(uuid); + return group.getName(); + }) + .collect(Collectors.joining(", ")); + _selectedGroups.addAll(groups); + _textGroup.setText(text); } // Update the icon if the issuer or name has changed @@ -298,11 +304,11 @@ public class EditEntryActivity extends AegisActivity { // Register listeners to trigger validation _textIssuer.addTextChangedListener(_validationListener); + _textGroup.addTextChangedListener(_validationListener); _textName.addTextChangedListener(_validationListener); _textNote.addTextChangedListener(_validationListener); _textSecret.addTextChangedListener(_validationListener); _dropdownType.addTextChangedListener(_validationListener); - _dropdownGroup.addTextChangedListener(_validationListener); _dropdownAlgo.addTextChangedListener(_validationListener); _textPeriodCounter.addTextChangedListener(_validationListener); _textDigits.addTextChangedListener(_validationListener); @@ -354,38 +360,109 @@ public class EditEntryActivity extends AegisActivity { startIconSelection(); }); - _dropdownGroup.setOnItemClickListener((parent, view, position, id) -> { - VaultGroupModel selectedGroup = _dropdownGroupList.get(position); - if (selectedGroup.isPlaceholder() && Objects.equals(selectedGroup.getName(), getString(R.string.new_group))) { - Dialogs.TextInputListener onAddGroup = text -> { - String groupName = new String(text).trim(); - if (!groupName.isEmpty()) { - VaultGroup group = _vaultManager.getVault().findGroupByName(groupName); - if (group == null) { - group = new VaultGroup(groupName); - _vaultManager.getVault().addGroup(group); - } - - updateGroupDropdownList(); - setGroup(new VaultGroupModel(group)); - } - }; - - DialogInterface.OnCancelListener onCancel = dialogInterface -> { - VaultGroupModel previous = (VaultGroupModel) _dropdownGroup.getTag(); - _dropdownGroup.setText(previous.getName(), false); - }; - - Dialogs.showTextInputDialog(EditEntryActivity.this, R.string.set_group, R.string.group_name_hint, onAddGroup, onCancel); - } else { - setGroup(_dropdownGroupList.get(position)); + _textGroup.setShowSoftInputOnFocus(false); + _textGroup.setOnClickListener(v -> showGroupSelectionDialog()); + _textGroup.setOnFocusChangeListener((v, hasFocus) -> { + if (hasFocus) { + showGroupSelectionDialog(); } }); + _textGroupLayout.setOnClickListener(v -> { + showGroupSelectionDialog(); + }); + _textUsageCount.setText(_prefs.getUsageCount(entryUUID).toString()); setLastUsedTimestamp(_prefs.getLastUsedTimestamp(entryUUID)); } + private void showGroupSelectionDialog() { + BottomSheetDialog dialog = new BottomSheetDialog(this); + View view = getLayoutInflater().inflate(R.layout.dialog_select_groups, null); + dialog.setContentView(view); + + ChipGroup chipGroup = view.findViewById(R.id.groupChipGroup); + TextView addGroupInfo = view.findViewById(R.id.addGroupInfo); + LinearLayout addGroup = view.findViewById(R.id.addGroup); + Button clearButton = view.findViewById(R.id.btnClear); + Button saveButton = view.findViewById(R.id.btnSave); + + chipGroup.removeAllViews(); + addGroupInfo.setVisibility(View.VISIBLE); + addGroup.setVisibility(View.VISIBLE); + + for (VaultGroup group : _groups) { + addChipTo(chipGroup, new VaultGroupModel(group), false); + } + + addGroup.setOnClickListener(v1 -> { + Dialogs.TextInputListener onAddGroup = text -> { + String groupName = new String(text).trim(); + if (!groupName.isEmpty()) { + VaultGroup group = _vaultManager.getVault().findGroupByName(groupName); + if (group == null) { + group = new VaultGroup(groupName); + _vaultManager.getVault().addGroup(group); + } + + _selectedGroups.add(group.getUUID()); + addChipTo(chipGroup, new VaultGroupModel(group), true); + } + }; + + Dialogs.showTextInputDialog(EditEntryActivity.this, R.string.set_group, R.string.group_name_hint, onAddGroup); + }); + + saveButton.setOnClickListener(v1 -> { + if(getCheckedUUID(chipGroup).isEmpty()) { + _selectedGroups.clear(); + _textGroup.setText(getString(R.string.no_group)); + } else { + _selectedGroups.clear(); + _selectedGroups.addAll(getCheckedUUID(chipGroup)); + _textGroup.setText(getCheckedNames(chipGroup)); + } + dialog.dismiss(); + }); + + clearButton.setOnClickListener(v1 -> { + chipGroup.clearCheck(); + }); + + Dialogs.showSecureDialog(dialog); + } + + private void addChipTo(ChipGroup chipGroup, VaultGroupModel group, Boolean isNew) { + Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false); + chip.setText(group.getName()); + chip.setCheckable(true); + + chip.setChecked((!_selectedGroups.isEmpty() && _selectedGroups.contains(group.getUUID())) || isNew); + chip.setCheckedIconVisible(true); + chip.setTag(group); + chipGroup.addView(chip); + } + + private static Set getCheckedUUID(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 static String getCheckedNames(ChipGroup chipGroup) { + return chipGroup.getCheckedChipIds().stream() + .map(i -> { + Chip chip = chipGroup.findViewById(i); + VaultGroupModel group = (VaultGroupModel) chip.getTag(); + return group.getName(); + }) + .collect(Collectors.joining(", ")); + } + private void updateAdvancedFieldStatus(String otpType) { boolean enabled = !otpType.equals(SteamInfo.ID) && !otpType.equals(YandexInfo.ID) && !otpType.equals(MotpInfo.ID) && (!_isNew || _isManual); @@ -400,11 +477,6 @@ public class EditEntryActivity extends AegisActivity { _textPin.setHint(otpType.equals(MotpInfo.ID) ? R.string.motp_pin : R.string.yandex_pin); } - private void setGroup(VaultGroupModel group) { - _dropdownGroup.setText(group.getName(), false); - _dropdownGroup.setTag(group); - } - private void openAdvancedSettings() { Animation fadeOut = new AlphaAnimation(1, 0); fadeOut.setInterpolator(new AccelerateInterpolator()); @@ -425,13 +497,6 @@ public class EditEntryActivity extends AegisActivity { })); } - private void updateGroupDropdownList() { - _dropdownGroupList.clear(); - _dropdownGroupList.add(new VaultGroupModel(getString(R.string.new_group))); - _dropdownGroupList.addAll(_groups.stream().map(VaultGroupModel::new).collect(Collectors.toList())); - _dropdownGroupList.add(new VaultGroupModel(getString(R.string.no_group))); - } - private boolean hasUnsavedChanges(VaultEntry newEntry) { return _hasChangedIcon || !_origEntry.equals(newEntry); } @@ -731,13 +796,10 @@ public class EditEntryActivity extends AegisActivity { entry.setName(_textName.getText().toString()); entry.setNote(_textNote.getText().toString()); - VaultGroupModel group = (VaultGroupModel) _dropdownGroup.getTag(); - if (group.isPlaceholder()) { + if (_selectedGroups.isEmpty()) { entry.setGroups(new HashSet<>()); } else { - Set groups = new HashSet<>(); - groups.add(group.getUUID()); - entry.setGroups(groups); + entry.setGroups(new HashSet<>(_selectedGroups)); } if (_hasChangedIcon) { 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 4d14908d..273fb2f3 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 @@ -244,8 +244,8 @@ public class Dialogs { showTextInputDialog(context, titleId, 0, hintId, listener, null, isSecret, null); } - public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, @Nullable DialogInterface.OnCancelListener onCancel) { - showTextInputDialog(context, titleId, 0, hintId, listener, onCancel, false, null); + public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) { + showTextInputDialog(context, titleId, 0, hintId, listener, null, false, null); } public static void showPasswordInputDialog(Context context, TextInputListener listener) { diff --git a/app/src/main/res/drawable/ic_outline_group_24.xml b/app/src/main/res/drawable/ic_outline_group_24.xml new file mode 100644 index 00000000..5c60ea3e --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_group_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_edit_entry.xml b/app/src/main/res/layout/activity_edit_entry.xml index 509d927e..87ec7b8f 100644 --- a/app/src/main/res/layout/activity_edit_entry.xml +++ b/app/src/main/res/layout/activity_edit_entry.xml @@ -110,7 +110,6 @@ android:hint="@string/issuer" android:layout_width="0dp" android:layout_height="match_parent" - android:layout_marginEnd="5dp" android:layout_weight="1"> + + + + - + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9998e88e..ff77446c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -311,6 +311,7 @@ No tokens can be imported as a result Unlocking the vault Rename Group + If an entry is not part of any group, it can be found under \"No group\". Remove group Are you sure you want to remove this group? Entries in this group will automatically switch to \'No group\'. Delete unused groups