Add a 'group' field to entries for filtering from the main view

pull/120/head
Alexander Bakker 6 years ago
parent d0e60cec75
commit 2ce259255d

@ -5,6 +5,7 @@ import org.json.JSONObject;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Objects;
import java.util.UUID;
import me.impy.aegis.encoding.Base64;
@ -17,6 +18,7 @@ public class DatabaseEntry implements Serializable {
private UUID _uuid;
private String _name = "";
private String _issuer = "";
private String _group;
private OtpInfo _info;
private byte[] _icon;
@ -47,6 +49,7 @@ public class DatabaseEntry implements Serializable {
obj.put("uuid", _uuid.toString());
obj.put("name", _name);
obj.put("issuer", _issuer);
obj.put("group", _group);
obj.put("icon", _icon == null ? JSONObject.NULL : Base64.encode(_icon));
obj.put("info", _info.toJson());
} catch (JSONException e) {
@ -69,6 +72,7 @@ public class DatabaseEntry implements Serializable {
DatabaseEntry entry = new DatabaseEntry(uuid, info);
entry.setName(obj.getString("name"));
entry.setIssuer(obj.getString("issuer"));
entry.setGroup(obj.optString("group", null));
Object icon = obj.get("icon");
if (icon != JSONObject.NULL) {
@ -94,6 +98,10 @@ public class DatabaseEntry implements Serializable {
return _issuer;
}
public String getGroup() {
return _group;
}
public byte[] getIcon() {
return _icon;
}
@ -110,6 +118,10 @@ public class DatabaseEntry implements Serializable {
_issuer = issuer;
}
public void setGroup(String group) {
_group = group;
}
public void setInfo(OtpInfo info) {
_info = info;
}
@ -135,6 +147,7 @@ public class DatabaseEntry implements Serializable {
return getUUID().equals(entry.getUUID())
&& getName().equals(entry.getName())
&& getIssuer().equals(entry.getIssuer())
&& Objects.equals(getGroup(), entry.getGroup())
&& getInfo().equals(entry.getInfo())
&& Arrays.equals(getIcon(), entry.getIcon());
}

@ -10,7 +10,9 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.Collator;
import java.util.List;
import java.util.TreeSet;
import java.util.UUID;
import me.impy.aegis.BuildConfig;
@ -159,6 +161,17 @@ public class DatabaseManager {
return _db.getEntryByUUID(uuid);
}
public TreeSet<String> getGroups() {
TreeSet<String> groups = new TreeSet<>(Collator.getInstance());
for (DatabaseEntry entry : getEntries()) {
String group = entry.getGroup();
if (group != null) {
groups.add(group);
}
}
return groups;
}
public DatabaseFileCredentials getCredentials() {
assertState(false, true);
return _creds;

@ -7,6 +7,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
private final ItemTouchHelperAdapter _adapter;
private boolean _positionChanged = false;
private boolean _isLongPressDragEnabled = true;
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
_adapter = adapter;
@ -14,7 +15,11 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
@Override
public boolean isLongPressDragEnabled() {
return true;
return _isLongPressDragEnabled;
}
public void setIsLongPressDragEnabled(boolean enabled) {
_isLongPressDragEnabled = enabled;
}
@Override

@ -5,6 +5,8 @@ import androidx.annotation.ArrayRes;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import java.util.List;
public class SpinnerHelper {
private SpinnerHelper() {
@ -12,6 +14,15 @@ public class SpinnerHelper {
public static void fillSpinner(Context context, Spinner spinner, @ArrayRes int textArrayResId) {
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context, textArrayResId, android.R.layout.simple_spinner_item);
initSpinner(spinner, adapter);
}
public static <T> void fillSpinner(Context context, Spinner spinner, List<T> items) {
ArrayAdapter adapter = new ArrayAdapter<>(context, android.R.layout.simple_spinner_item, items);
initSpinner(spinner, adapter);
}
private static void initSpinner(Spinner spinner, ArrayAdapter adapter) {
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
spinner.invalidate();

@ -2,6 +2,7 @@ package me.impy.aegis.ui;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.hardware.fingerprint.FingerprintManager;
import android.text.Editable;
@ -19,6 +20,7 @@ import java.util.concurrent.atomic.AtomicReference;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import me.impy.aegis.R;
@ -47,6 +49,21 @@ public class Dialogs {
dialog.show();
}
public static void showTextInputDialog(Context context, @StringRes int titleId, TextInputListener listener) {
EditText input = new EditText(context);
showSecureDialog(new AlertDialog.Builder(context)
.setTitle(titleId)
.setView(input)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
listener.onTextInputResult(input.getText().toString());
}
})
.create());
}
public static void showDeleteEntryDialog(Activity activity, DialogInterface.OnClickListener onDelete) {
showSecureDialog(new AlertDialog.Builder(activity)
.setTitle(activity.getString(R.string.delete_entry))
@ -165,6 +182,10 @@ public class Dialogs {
showSecureDialog(dialog);
}
public interface TextInputListener {
void onTextInputResult(String text);
}
public interface SlotListener {
void onSlotResult(Slot slot, Cipher cipher);
void onException(Exception e);

@ -1,6 +1,8 @@
package me.impy.aegis.ui;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
@ -36,6 +38,11 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.text.Collator;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicReference;
import androidx.appcompat.app.AlertDialog;
@ -57,6 +64,7 @@ public class EditEntryActivity extends AegisActivity {
private boolean _isNew = false;
private DatabaseEntry _origEntry;
private TreeSet<String> _groups;
private boolean _hasCustomIcon = false;
// keep track of icon changes separately as the generated jpeg's are not deterministic
private boolean _hasChangedIcon = false;
@ -75,6 +83,8 @@ public class EditEntryActivity extends AegisActivity {
private Spinner _spinnerType;
private Spinner _spinnerAlgo;
private Spinner _spinnerDigits;
private Spinner _spinnerGroup;
private List<String> _spinnerGroupList = new ArrayList<>();
private KropView _kropView;
@ -94,6 +104,8 @@ public class EditEntryActivity extends AegisActivity {
Intent intent = getIntent();
_origEntry = (DatabaseEntry) intent.getSerializableExtra("entry");
_isNew = intent.getBooleanExtra("isNew", false);
_groups = new TreeSet<String>(Collator.getInstance());
_groups.addAll(intent.getStringArrayListExtra("groups"));
if (_isNew) {
setTitle(R.string.add_new_profile);
}
@ -115,6 +127,9 @@ public class EditEntryActivity extends AegisActivity {
SpinnerHelper.fillSpinner(this, _spinnerAlgo, R.array.otp_algo_array);
_spinnerDigits = findViewById(R.id.spinner_digits);
SpinnerHelper.fillSpinner(this, _spinnerDigits, R.array.otp_digits_array);
_spinnerGroup = findViewById(R.id.spinner_group);
updateGroupSpinnerList();
SpinnerHelper.fillSpinner(this, _spinnerGroup, _spinnerGroupList);
_advancedSettingsHeader = findViewById(R.id.accordian_header);
_advancedSettings = findViewById(R.id.expandableLayout);
@ -159,6 +174,12 @@ public class EditEntryActivity extends AegisActivity {
String digits = Integer.toString(_origEntry.getInfo().getDigits());
_spinnerDigits.setSelection(getStringResourceIndex(R.array.otp_digits_array, digits), false);
String group = _origEntry.getGroup();
if (group != null) {
int pos = _groups.contains(group) ? _groups.headSet(group).size() : -1;
_spinnerGroup.setSelection(pos + 1, false);
}
}
// update the icon if the text changed
@ -191,6 +212,38 @@ public class EditEntryActivity extends AegisActivity {
}
});
final Activity activity = this;
_spinnerGroup.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
private int prevPosition;
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (position == _spinnerGroupList.size() - 1) {
Dialogs.showTextInputDialog(activity, R.string.enter_group_name, new Dialogs.TextInputListener() {
@Override
public void onTextInputResult(String text) {
if (text.isEmpty()) {
return;
}
_groups.add(text);
// reset the selection to "No group" to work around a quirk
_spinnerGroup.setSelection(0, false);
updateGroupSpinnerList();
_spinnerGroup.setSelection(_spinnerGroupList.indexOf(text), false);
}
});
_spinnerGroup.setSelection(prevPosition, false);
} else {
prevPosition = position;
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
_iconView.setOnClickListener(v -> {
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
@ -262,6 +315,14 @@ public class EditEntryActivity extends AegisActivity {
});
}
private void updateGroupSpinnerList() {
Resources res = getResources();
_spinnerGroupList.clear();
_spinnerGroupList.add(res.getString(R.string.no_group));
_spinnerGroupList.addAll(_groups);
_spinnerGroupList.add(res.getString(R.string.new_group));
}
@Override
public void onBackPressed() {
AtomicReference<String> msg = new AtomicReference<>();
@ -437,6 +498,14 @@ public class EditEntryActivity extends AegisActivity {
entry.setIssuer(_textIssuer.getText().toString());
entry.setName(_textName.getText().toString());
int groupPos = _spinnerGroup.getSelectedItemPosition();
if (groupPos != 0) {
String group = _spinnerGroupList.get(_spinnerGroup.getSelectedItemPosition());
entry.setGroup(group);
} else {
entry.setGroup(null);
}
if (_hasChangedIcon) {
if (_hasCustomIcon) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();

@ -11,12 +11,15 @@ import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.widget.LinearLayout;
import android.widget.Toast;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.ArrayList;
import java.util.TreeSet;
import me.impy.aegis.AegisApplication;
import me.impy.aegis.R;
@ -43,6 +46,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private AegisApplication _app;
private DatabaseManager _db;
private boolean _loaded;
private String _checkedGroup;
private Menu _menu;
private FloatingActionsMenu _fabMenu;
@ -164,6 +168,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
intent.putExtra("entry", entry);
}
intent.putExtra("isNew", isNew);
intent.putExtra("groups", new ArrayList<>(_db.getGroups()));
startActivityForResult(intent, requestCode);
}
@ -185,14 +190,14 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private void onEditEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
if (!data.getBooleanExtra("delete", false)) {
if (data.getBooleanExtra("delete", false)) {
deleteEntry(entry);
} else {
// this profile has been serialized/deserialized and is no longer the same instance it once was
// to deal with this, the replaceEntry functions are used
_db.replaceEntry(entry);
_entryListView.replaceEntry(entry);
saveDatabase();
} else {
deleteEntry(entry);
}
}
}
@ -205,6 +210,38 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
}
private void updateGroupFilterMenu() {
SubMenu menu = _menu.findItem(R.id.action_filter).getSubMenu();
for (int i = menu.size() - 1; i >= 0; i--) {
MenuItem item = menu.getItem(i);
if (item.getItemId() == R.id.menu_filter_all) {
continue;
}
menu.removeItem(item.getItemId());
}
// if the group no longer exists, switch back to 'All'
TreeSet<String> groups = _db.getGroups();
if (_checkedGroup != null && !groups.contains(_checkedGroup)) {
menu.findItem(R.id.menu_filter_all).setChecked(true);
setGroupFilter(null);
}
for (String group : groups) {
MenuItem item = menu.add(R.id.action_filter_group, Menu.NONE, Menu.NONE, group);
if (group.equals(_checkedGroup)) {
item.setChecked(true);
}
}
menu.setGroupCheckable(R.id.action_filter_group, true, true);
}
private void setGroupFilter(String group) {
_checkedGroup = group;
_entryListView.setGroupFilter(group);
}
private void addEntry(DatabaseEntry entry) {
_db.addEntry(entry);
_entryListView.addEntry(entry);
@ -281,6 +318,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
unlockDatabase(null);
}
} else if (_loaded) {
// update the list of groups in the filter menu
if (_menu != null) {
updateGroupFilterMenu();
}
// refresh all codes to prevent showing old ones
_entryListView.refresh(true);
} else {
@ -310,6 +352,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
dialog.dismiss();
Dialogs.showDeleteEntryDialog(this, (d, which) -> {
deleteEntry(entry);
// update the filter list if the group no longer exists
if (!_db.getGroups().contains(entry.getGroup())) {
updateGroupFilterMenu();
}
});
});
@ -333,6 +379,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_menu = menu;
getMenuInflater().inflate(R.menu.menu_main, menu);
updateLockIcon();
updateGroupFilterMenu();
return true;
}
@ -347,6 +394,15 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
lockDatabase();
return true;
default:
if (item.getGroupId() == R.id.action_filter_group) {
item.setChecked(true);
String group = null;
if (item.getItemId() != R.id.menu_filter_all) {
group = item.getTitle().toString();
}
setGroupFilter(group);
}
return super.onOptionsItemSelected(item);
}
}

@ -20,14 +20,17 @@ import me.impy.aegis.otp.TotpInfo;
public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements ItemTouchHelperAdapter {
private List<DatabaseEntry> _entries;
private List<DatabaseEntry> _shownEntries;
private static Listener _listener;
private boolean _showAccountName;
private String _groupFilter;
// keeps track of the viewholders that are currently bound
private List<EntryHolder> _holders;
public EntryAdapter(Listener listener) {
_entries = new ArrayList<>();
_shownEntries = new ArrayList<>();
_holders = new ArrayList<>();
_listener = listener;
}
@ -38,6 +41,9 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
public void addEntry(DatabaseEntry entry) {
_entries.add(entry);
if (!isEntryFiltered(entry)) {
_shownEntries.add(entry);
}
int position = getItemCount() - 1;
if (position == 0) {
@ -49,26 +55,59 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
public void addEntries(List<DatabaseEntry> entries) {
_entries.addAll(entries);
for (DatabaseEntry entry : entries) {
if (!isEntryFiltered(entry)) {
_shownEntries.add(entry);
}
}
notifyDataSetChanged();
}
public void removeEntry(DatabaseEntry entry) {
entry = getEntryByUUID(entry.getUUID());
int position = _entries.indexOf(entry);
_entries.remove(position);
notifyItemRemoved(position);
_entries.remove(entry);
if (_shownEntries.contains(entry)) {
int position = _shownEntries.indexOf(entry);
_shownEntries.remove(position);
notifyItemRemoved(position);
}
}
public void clearEntries() {
_entries.clear();
_shownEntries.clear();
notifyDataSetChanged();
}
public void replaceEntry(DatabaseEntry newEntry) {
DatabaseEntry oldEntry = getEntryByUUID(newEntry.getUUID());
int position = _entries.indexOf(oldEntry);
_entries.set(position, newEntry);
notifyItemChanged(position);
_entries.set(_entries.indexOf(oldEntry), newEntry);
if (_shownEntries.contains(oldEntry)) {
int position = _shownEntries.indexOf(oldEntry);
if (isEntryFiltered(newEntry)) {
_shownEntries.remove(position);
notifyItemRemoved(position);
} else {
_shownEntries.set(position, newEntry);
notifyItemChanged(position);
}
} else if (!isEntryFiltered(newEntry)) {
// TODO: preserve order
_shownEntries.add(newEntry);
int position = getItemCount() - 1;
notifyItemInserted(position);
}
}
private boolean isEntryFiltered(DatabaseEntry entry) {
String group = entry.getGroup();
if (_groupFilter == null) {
return false;
}
return group == null || !group.equals(_groupFilter);
}
private DatabaseEntry getEntryByUUID(UUID uuid) {
@ -90,6 +129,17 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
}
public void setGroupFilter(String group) {
_groupFilter = group;
_shownEntries.clear();
for (DatabaseEntry entry : _entries) {
if (!isEntryFiltered(entry)) {
_shownEntries.add(entry);
}
}
notifyDataSetChanged();
}
@Override
public void onItemDismiss(int position) {
@ -97,16 +147,27 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
@Override
public void onItemDrop(int position) {
_listener.onEntryDrop(_entries.get(position));
// moving entries is not allowed when a filter is applied
if (_groupFilter != null) {
return;
}
_listener.onEntryDrop(_shownEntries.get(position));
}
@Override
public void onItemMove(int firstPosition, int secondPosition) {
// moving entries is not allowed when a filter is applied
if (_groupFilter != null) {
return;
}
// notify the database first
_listener.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition));
// update our side of things
Collections.swap(_entries, firstPosition, secondPosition);
Collections.swap(_shownEntries, firstPosition, secondPosition);
notifyItemMoved(firstPosition, secondPosition);
}
@ -124,7 +185,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
@Override
public void onBindViewHolder(final EntryHolder holder, int position) {
DatabaseEntry entry = _entries.get(position);
DatabaseEntry entry = _shownEntries.get(position);
boolean showProgress = !isPeriodUniform() && entry.getInfo() instanceof TotpInfo;
holder.setData(entry, _showAccountName, showProgress);
if (showProgress) {
@ -135,14 +196,14 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
@Override
public void onClick(View v) {
int position = holder.getAdapterPosition();
_listener.onEntryClick(_entries.get(position));
_listener.onEntryClick(_shownEntries.get(position));
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
int position = holder.getAdapterPosition();
return _listener.onLongEntryClick(_entries.get(position));
return _listener.onLongEntryClick(_shownEntries.get(position));
}
});
holder.setOnRefreshClickListener(new View.OnClickListener() {
@ -169,7 +230,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
public int getUniformPeriod() {
List<TotpInfo> infos = new ArrayList<>();
for (DatabaseEntry entry : _entries) {
for (DatabaseEntry entry : _shownEntries) {
OtpInfo info = entry.getInfo();
if (info instanceof TotpInfo) {
infos.add((TotpInfo) info);
@ -196,7 +257,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
@Override
public int getItemCount() {
return _entries.size();
return _shownEntries.size();
}
public interface Listener {

@ -21,6 +21,7 @@ import me.impy.aegis.otp.TotpInfo;
public class EntryListView extends Fragment implements EntryAdapter.Listener {
private EntryAdapter _adapter;
private Listener _listener;
private SimpleItemTouchHelperCallback _touchCallback;
private PeriodProgressBar _progressBar;
private boolean _showProgress;
@ -46,8 +47,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
RecyclerView rvKeyProfiles = view.findViewById(R.id.rvKeyProfiles);
LinearLayoutManager mLayoutManager = new LinearLayoutManager(view.getContext());
rvKeyProfiles.setLayoutManager(mLayoutManager);
ItemTouchHelper.Callback callback = new SimpleItemTouchHelperCallback(_adapter);
ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
_touchCallback = new SimpleItemTouchHelperCallback(_adapter);
ItemTouchHelper touchHelper = new ItemTouchHelper(_touchCallback);
touchHelper.attachToRecyclerView(rvKeyProfiles);
rvKeyProfiles.setAdapter(_adapter);
@ -66,6 +67,12 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
return view;
}
public void setGroupFilter(String group) {
_adapter.setGroupFilter(group);
_touchCallback.setIsLongPressDragEnabled(group == null);
checkPeriodUniformity();
}
public void refresh(boolean hard) {
if (_showProgress) {
_progressBar.refresh();

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z"/>
</vector>

@ -74,7 +74,7 @@
<EditText android:layout_column="1"
android:id="@+id/text_name"
android:hint="Name"
android:hint="@string/name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:backgroundTint="#949494"
@ -84,13 +84,27 @@
<TableRow
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp">
<EditText android:layout_column="1"
android:id="@+id/text_issuer"
android:hint="@string/issuer"
<LinearLayout android:layout_column="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:backgroundTint="#949494"
android:layout_weight="1"/>
android:layout_weight="1"
android:orientation="horizontal"
android:gravity="center_vertical">
<EditText
android:id="@+id/text_issuer"
android:hint="@string/issuer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:backgroundTint="#949494"
android:layout_weight="1"/>
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/spinner_group"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:backgroundTint="#949494"
style="@style/Base.Widget.AppCompat.Spinner.Underlined"/>
</LinearLayout>
</TableRow>
</TableLayout>

@ -6,7 +6,23 @@
android:id="@+id/action_lock"
android:icon="@drawable/ic_lock"
app:showAsAction="ifRoom"
android:title=""/>
android:title="@string/lock"/>
<item
android:id="@+id/action_filter"
android:icon="@drawable/ic_baseline_filter_list_24dp"
app:showAsAction="ifRoom"
android:title="@string/filter">
<menu>
<group
android:id="@+id/action_filter_group"
android:checkableBehavior="single">
<item
android:id="@+id/menu_filter_all"
android:title="@string/all"
android:checked="true" />
</group>
</menu>
</item>
<item
android:id="@+id/action_settings"
android:orderInCategory="100"

@ -134,4 +134,11 @@
<string name="remove_slot">Remove slot</string>
<string name="remove_slot_description">Are you sure you want to remove this slot?</string>
<string name="adding_new_slot_error">An error occurred while trying to add a new slot:</string>
<string name="filter">Filter</string>
<string name="lock">Lock</string>
<string name="all">All</string>
<string name="name">Name</string>
<string name="no_group">No group</string>
<string name="new_group">New group</string>
<string name="enter_group_name">Enter a group name</string>
</resources>

Loading…
Cancel
Save