diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/components/DropdownCheckBoxes.java b/app/src/main/java/com/beemdevelopment/aegis/ui/components/DropdownCheckBoxes.java new file mode 100644 index 00000000..d61998fd --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/components/DropdownCheckBoxes.java @@ -0,0 +1,175 @@ +package com.beemdevelopment.aegis.ui.components; + +import android.content.Context; +import android.content.res.TypedArray; +import android.text.InputType; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.CheckBox; +import android.widget.Filter; +import android.widget.Filterable; + +import androidx.annotation.PluralsRes; +import androidx.appcompat.widget.AppCompatAutoCompleteTextView; + +import com.beemdevelopment.aegis.R; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView { + private @PluralsRes int _selectedCountPlural = R.plurals.dropdown_checkboxes_default_count; + + private boolean _allowFiltering = false; + + private final List _items = new ArrayList<>(); + private List _visibleItems = new ArrayList<>(); + private final Set _checkedItems = new TreeSet<>(); + + private CheckboxAdapter _adapter; + + public DropdownCheckBoxes(Context context) { + super(context); + initialise(context, null); + } + + public DropdownCheckBoxes(Context context, AttributeSet attrs) { + super(context, attrs); + initialise(context, attrs); + } + + public DropdownCheckBoxes(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initialise(context, attrs); + } + + private void initialise(Context context, AttributeSet attrs) { + _adapter = new CheckboxAdapter(); + setAdapter(_adapter); + + if (attrs != null) { + TypedArray a = context.obtainStyledAttributes( + attrs, + R.styleable.DropdownCheckBoxes, + 0, 0); + + _allowFiltering = a.getBoolean(R.styleable.DropdownCheckBoxes_allow_filtering, false); + a.recycle(); + } + + if (!_allowFiltering) { + setInputType(0); + } else { + setInputType(InputType.TYPE_CLASS_TEXT); + } + } + + public void addItems(List items, boolean startChecked) { + _items.addAll(items); + _visibleItems.addAll(items); + + if (startChecked) { + _checkedItems.addAll(items); + } + + updateCheckedItemsCountText(); + _adapter.notifyDataSetChanged(); + } + + private void updateCheckedItemsCountText() { + if (_allowFiltering) { + return; + } + + int count = _checkedItems.size(); + String countString = getResources().getQuantityString(_selectedCountPlural, count, count); + + setText(countString, false); + } + + public void setCheckedItemsCountTextRes(@PluralsRes int resId) { + _selectedCountPlural = resId; + } + + public Set getCheckedItems() { + return _checkedItems; + } + + private class CheckboxAdapter extends BaseAdapter implements Filterable { + + @Override + public int getCount() { + return _visibleItems.size(); + } + + @Override + public String getItem(int i) { + return _visibleItems.get(i); + } + + @Override + public long getItemId(int i) { + return i; + } + + @Override + public View getView(int i, View convertView, ViewGroup viewGroup) { + if (convertView == null) { + convertView = LayoutInflater.from(getContext()).inflate(R.layout.dropdown_checkbox, viewGroup, false); + } + + String item = _visibleItems.get(i); + + CheckBox checkBox = convertView.findViewById(R.id.checkbox_in_dropdown); + checkBox.setText(item); + checkBox.setChecked(_checkedItems.contains(item)); + + checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { + String label = buttonView.getText().toString(); + + if (isChecked) { + _checkedItems.add(label); + } else { + _checkedItems.remove(label); + } + + updateCheckedItemsCountText(); + }); + + return convertView; + } + + @Override + public Filter getFilter() { + return new Filter() { + @Override + protected FilterResults performFiltering(CharSequence query) { + FilterResults results = new FilterResults(); + results.values = (query == null || query.toString().isEmpty()) + ? _items + : _items.stream().filter(str -> { + String q = query.toString().toLowerCase(); + String strLower = str.toLowerCase(); + + return strLower.contains(q); + }) + .collect(Collectors.toList()); + + return results; + } + + @Override + protected void publishResults(CharSequence charSequence, FilterResults filterResults) { + _visibleItems = (List) filterResults.values; + notifyDataSetChanged(); + } + }; + } + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java index f22a6f90..8bdc18bc 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java @@ -9,7 +9,6 @@ import android.view.View; import android.widget.AutoCompleteTextView; import android.widget.Button; import android.widget.CheckBox; -import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; @@ -30,6 +29,7 @@ import com.beemdevelopment.aegis.otp.OtpInfo; import com.beemdevelopment.aegis.otp.TotpInfo; import com.beemdevelopment.aegis.ui.ImportEntriesActivity; import com.beemdevelopment.aegis.ui.TransferEntriesActivity; +import com.beemdevelopment.aegis.ui.components.DropdownCheckBoxes; import com.beemdevelopment.aegis.ui.dialogs.Dialogs; import com.beemdevelopment.aegis.ui.tasks.ExportTask; import com.beemdevelopment.aegis.ui.tasks.ImportFileTask; @@ -41,6 +41,7 @@ import com.beemdevelopment.aegis.vault.VaultRepository; import com.beemdevelopment.aegis.vault.VaultRepositoryException; import com.beemdevelopment.aegis.vault.slots.PasswordSlot; import com.beemdevelopment.aegis.vault.slots.SlotException; +import com.google.android.material.textfield.TextInputLayout; import java.io.File; import java.io.FileOutputStream; @@ -162,8 +163,8 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { CheckBox checkBoxEncrypt = view.findViewById(R.id.checkbox_export_encrypt); CheckBox checkBoxAccept = view.findViewById(R.id.checkbox_accept); CheckBox checkBoxExportAllGroups = view.findViewById(R.id.export_selected_groups); - LinearLayout groupsSelection = view.findViewById(R.id.select_groups); - TextView groupsSelectionDescriptor = view.findViewById(R.id.select_groups_hint); + TextInputLayout groupsSelectionLayout = view.findViewById(R.id.group_selection_layout); + DropdownCheckBoxes groupsSelection = view.findViewById(R.id.group_selection_dropdown); TextView passwordInfoText = view.findViewById(R.id.text_separate_password); passwordInfoText.setVisibility(checkBoxEncrypt.isChecked() && isBackupPasswordSet ? View.VISIBLE : View.GONE); AutoCompleteTextView dropdown = view.findViewById(R.id.dropdown_export_format); @@ -179,12 +180,13 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { TreeSet groups = _vaultManager.getVault().getGroups(); if (groups.size() > 0) { checkBoxExportAllGroups.setVisibility(View.VISIBLE); - for (String group: groups) { - CheckBox box = new CheckBox(requireContext()); - box.setText(group); - box.setChecked(false); - groupsSelection.addView(box); - } + + ArrayList groupsArray = new ArrayList<>(); + groupsArray.add(getString(R.string.no_group)); + groupsArray.addAll(groups); + + groupsSelection.setCheckedItemsCountTextRes(R.plurals.export_groups_selected_count); + groupsSelection.addItems(groupsArray, false); } AlertDialog dialog = new AlertDialog.Builder(requireContext()) @@ -215,8 +217,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { checkBoxExportAllGroups.setOnCheckedChangeListener((button, isChecked) -> { int visibility = isChecked ? View.GONE : View.VISIBLE; - groupsSelection.setVisibility(visibility); - groupsSelectionDescriptor.setVisibility(visibility); + groupsSelectionLayout.setVisibility(visibility); }); btnPos.setOnClickListener(v -> { @@ -303,14 +304,13 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { Dialogs.showSecureDialog(dialog); } - private Vault.EntryFilter getVaultEntryFilter(LinearLayout view) { + private Vault.EntryFilter getVaultEntryFilter(DropdownCheckBoxes dropdownCheckBoxes) { Set groups = new HashSet<>(); - for (int i=0; i - @@ -76,31 +76,21 @@ android:checked="true" android:visibility="gone" /> - + android:layout_marginStart="25dp" + android:layout_marginEnd="25dp" + android:layout_marginTop="15dp" + android:hint="@string/export_choose_groups" + android:visibility="gone" + style="?attr/dropdownStyle"> - - - - - - + app:allow_filtering="false" /> + diff --git a/app/src/main/res/layout/dropdown_checkbox.xml b/app/src/main/res/layout/dropdown_checkbox.xml new file mode 100644 index 00000000..8e398f86 --- /dev/null +++ b/app/src/main/res/layout/dropdown_checkbox.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index e4424480..70a703c7 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -19,4 +19,8 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 41c029ac..31b99132 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -108,7 +108,11 @@ Web page (.HTML) Export format Export all groups - Select which groups to export: + Select which groups to export + + %d group selected + %d groups selected + No groups selected to export Aegis Authenticator Export @@ -500,4 +504,9 @@ %d year ago %d years ago + + + %d item selected + %d items selected +