From c24b691a2697f2e15eba1a58a6535d0de3fb5c73 Mon Sep 17 00:00:00 2001 From: Alexander Bakker Date: Fri, 9 Feb 2018 17:31:07 +0100 Subject: [PATCH] Add an activity to manage database key slots --- .idea/misc.xml | 2 +- app/src/main/AndroidManifest.xml | 5 + .../main/java/me/impy/aegis/AuthActivity.java | 8 +- .../impy/aegis/FingerprintDialogFragment.java | 79 +++++++++ .../main/java/me/impy/aegis/MainActivity.java | 33 +++- .../me/impy/aegis/PasswordDialogFragment.java | 82 +++++++++ .../me/impy/aegis/PreferencesActivity.java | 16 +- .../main/java/me/impy/aegis/SlotAdapter.java | 70 ++++++++ .../me/impy/aegis/SlotCollectionTask.java | 5 +- .../me/impy/aegis/SlotDialogFragment.java | 32 ++++ .../main/java/me/impy/aegis/SlotHolder.java | 52 ++++++ .../me/impy/aegis/SlotManagerActivity.java | 156 ++++++++++++++++++ .../me/impy/aegis/db/DatabaseManager.java | 5 + .../drawable/ic_fingerprint_black_24dp.xml | 7 + .../main/res/drawable/ic_plus_black_24dp.xml | 7 + app/src/main/res/layout/activity_auth.xml | 2 +- app/src/main/res/layout/activity_slots.xml | 126 ++++++++++++++ app/src/main/res/layout/card_slot.xml | 66 ++++++++ .../main/res/layout/dialog_fingerprint.xml | 21 +++ .../main/res/layout/dialog_new_profile.xml | 19 --- app/src/main/res/layout/dialog_password.xml | 21 +++ .../layout/fragment_authenticated_slide.xml | 2 +- app/src/main/res/menu/menu_slots.xml | 10 ++ app/src/main/res/xml/preferences.xml | 5 + 24 files changed, 798 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/me/impy/aegis/FingerprintDialogFragment.java create mode 100644 app/src/main/java/me/impy/aegis/PasswordDialogFragment.java create mode 100644 app/src/main/java/me/impy/aegis/SlotAdapter.java create mode 100644 app/src/main/java/me/impy/aegis/SlotDialogFragment.java create mode 100644 app/src/main/java/me/impy/aegis/SlotHolder.java create mode 100644 app/src/main/java/me/impy/aegis/SlotManagerActivity.java create mode 100644 app/src/main/res/drawable/ic_fingerprint_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_plus_black_24dp.xml create mode 100644 app/src/main/res/layout/activity_slots.xml create mode 100644 app/src/main/res/layout/card_slot.xml create mode 100644 app/src/main/res/layout/dialog_fingerprint.xml delete mode 100644 app/src/main/res/layout/dialog_new_profile.xml create mode 100644 app/src/main/res/layout/dialog_password.xml create mode 100644 app/src/main/res/menu/menu_slots.xml diff --git a/.idea/misc.xml b/.idea/misc.xml index 53a3fb16..635999df 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -24,7 +24,7 @@ - + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 64703425..771e4699 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -49,5 +49,10 @@ + + diff --git a/app/src/main/java/me/impy/aegis/AuthActivity.java b/app/src/main/java/me/impy/aegis/AuthActivity.java index acb062ec..8fd31ebf 100644 --- a/app/src/main/java/me/impy/aegis/AuthActivity.java +++ b/app/src/main/java/me/impy/aegis/AuthActivity.java @@ -1,6 +1,5 @@ package me.impy.aegis; -import android.content.DialogInterface; import android.content.Intent; import android.hardware.fingerprint.FingerprintManager; import android.os.Build; @@ -98,12 +97,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C builder.setTitle("Decryption error"); builder.setMessage("Master key integrity check failed for every slot. Make sure you didn't mistype your password."); builder.setCancelable(false); - builder.setPositiveButton("OK", - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }); + builder.setPositiveButton(android.R.string.ok, null); builder.create().show(); } diff --git a/app/src/main/java/me/impy/aegis/FingerprintDialogFragment.java b/app/src/main/java/me/impy/aegis/FingerprintDialogFragment.java new file mode 100644 index 00000000..476d4f80 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/FingerprintDialogFragment.java @@ -0,0 +1,79 @@ +package me.impy.aegis; + +import android.app.Dialog; +import android.hardware.fingerprint.FingerprintManager; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v7.app.AlertDialog; +import android.view.View; +import android.widget.TextView; + +import com.mattprecious.swirl.SwirlView; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; + +import me.impy.aegis.crypto.KeyStoreHandle; +import me.impy.aegis.crypto.slots.FingerprintSlot; +import me.impy.aegis.crypto.slots.Slot; +import me.impy.aegis.helpers.FingerprintHelper; +import me.impy.aegis.helpers.FingerprintUiHelper; + +public class FingerprintDialogFragment extends SlotDialogFragment implements FingerprintUiHelper.Callback { + private Cipher _cipher; + private FingerprintUiHelper _helper; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + View view = getActivity().getLayoutInflater().inflate(R.layout.dialog_fingerprint, null); + TextView textFingerprint = view.findViewById(R.id.text_fingerprint); + SwirlView imgFingerprint = view.findViewById(R.id.img_fingerprint); + + FingerprintManager manager = FingerprintHelper.getManager(getContext()); + try { + KeyStoreHandle handle = new KeyStoreHandle(); + SecretKey key = handle.getKey(); + _cipher = Slot.createCipher(key, Cipher.ENCRYPT_MODE); + _helper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this); + } catch (Exception e) { + throw new RuntimeException(e); + } + + return new AlertDialog.Builder(getActivity()) + .setTitle("Register a new fingerprint") + .setView(view) + .setNegativeButton(android.R.string.cancel, null) + .create(); + } + + @Override + public void onResume() { + super.onResume(); + + if (_helper != null) { + _helper.startListening(new FingerprintManager.CryptoObject(_cipher)); + } + } + + @Override + public void onPause() { + super.onPause(); + + if (_helper != null) { + _helper.stopListening(); + } + } + + @Override + public void onAuthenticated() { + FingerprintSlot slot = new FingerprintSlot(); + getListener().onSlotResult(slot, _cipher); + dismiss(); + } + + @Override + public void onError() { + + } +} diff --git a/app/src/main/java/me/impy/aegis/MainActivity.java b/app/src/main/java/me/impy/aegis/MainActivity.java index 1c3537f5..74fd09b6 100644 --- a/app/src/main/java/me/impy/aegis/MainActivity.java +++ b/app/src/main/java/me/impy/aegis/MainActivity.java @@ -26,6 +26,7 @@ import java.lang.reflect.UndeclaredThrowableException; import java.util.List; import me.impy.aegis.crypto.MasterKey; +import me.impy.aegis.crypto.slots.SlotCollection; import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.db.DatabaseManager; import me.impy.aegis.helpers.PermissionHelper; @@ -42,6 +43,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen private static final int CODE_DECRYPT = 5; private static final int CODE_IMPORT = 6; private static final int CODE_PREFERENCES = 7; + private static final int CODE_SLOTS = 8; // permission request codes private static final int CODE_PERM_EXPORT = 0; @@ -183,6 +185,8 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen case CODE_PREFERENCES: onPreferencesResult(resultCode, data); break; + case CODE_SLOTS: + onSlotManagerResult(resultCode, data); } } @@ -206,6 +210,16 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen } } + private void onSlotManagerResult(int resultCode, Intent data) { + if (resultCode != RESULT_OK) { + return; + } + + SlotCollection slots = (SlotCollection) data.getSerializableExtra("slots"); + _db.getFile().setSlots(slots); + saveDatabase(); + } + private void onPreferencesResult(int resultCode, Intent data) { // refresh the entire key profile list if needed if (data.getBooleanExtra("needsRefresh", false)) { @@ -219,6 +233,20 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen case PreferencesActivity.ACTION_EXPORT: onExport(); break; + case PreferencesActivity.ACTION_SLOTS: + MasterKey masterKey; + try { + masterKey = _db.getMasterKey(); + } catch (Exception e) { + e.printStackTrace(); + Toast.makeText(this, "An error occurred while trying to obtain the database key", Toast.LENGTH_SHORT).show(); + break; + } + Intent intent = new Intent(this, SlotManagerActivity.class); + intent.putExtra("masterKey", masterKey); + intent.putExtra("slots", _db.getFile().getSlots()); + startActivityForResult(intent, CODE_SLOTS); + break; } } @@ -554,8 +582,9 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_settings: - Intent preferencesActivity = new Intent(this, PreferencesActivity.class); - startActivityForResult(preferencesActivity, CODE_PREFERENCES); + Intent intent = new Intent(this, PreferencesActivity.class); + intent.putExtra("encrypted", _db.getFile().isEncrypted()); + startActivityForResult(intent, CODE_PREFERENCES); return true; case R.id.action_import: if (PermissionHelper.request(this, CODE_PERM_IMPORT, Manifest.permission.READ_EXTERNAL_STORAGE)) { diff --git a/app/src/main/java/me/impy/aegis/PasswordDialogFragment.java b/app/src/main/java/me/impy/aegis/PasswordDialogFragment.java new file mode 100644 index 00000000..9cfdf59b --- /dev/null +++ b/app/src/main/java/me/impy/aegis/PasswordDialogFragment.java @@ -0,0 +1,82 @@ +package me.impy.aegis; + +import android.app.Dialog; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v7.app.AlertDialog; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; + +import javax.crypto.Cipher; + +import me.impy.aegis.crypto.slots.PasswordSlot; +import me.impy.aegis.crypto.slots.Slot; +import me.impy.aegis.helpers.AuthHelper; + +public class PasswordDialogFragment extends SlotDialogFragment { + private Button _buttonOK; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + View view = getActivity().getLayoutInflater().inflate(R.layout.dialog_password, null); + EditText textPassword = view.findViewById(R.id.text_password); + EditText textPasswordConfirm = view.findViewById(R.id.text_password_confirm); + + AlertDialog alert = new AlertDialog.Builder(getActivity()) + .setTitle("Enter a new password") + .setView(view) + .setPositiveButton(android.R.string.ok, null) + .setNegativeButton(android.R.string.cancel, null) + .create(); + + alert.setOnShowListener(dialog -> { + _buttonOK = alert.getButton(AlertDialog.BUTTON_POSITIVE); + _buttonOK.setEnabled(false); + + // replace the default listener + _buttonOK.setOnClickListener(v -> { + if (!AuthHelper.arePasswordsEqual(textPassword, textPasswordConfirm)) { + return; + } + + AuthHelper.clearPassword(textPasswordConfirm); + char[] password = AuthHelper.getPassword(textPassword, true); + + PasswordSlot slot = new PasswordSlot(); + DerivationTask task = new DerivationTask(getContext(), key -> { + Cipher cipher; + try { + cipher = Slot.createCipher(key, Cipher.ENCRYPT_MODE); + } catch (Exception e) { + getListener().onException(e); + dialog.cancel(); + return; + } + getListener().onSlotResult(slot, cipher); + dialog.dismiss(); + }); + task.execute(new DerivationTask.Params() {{ + Slot = slot; + Password = password; + }}); + }); + }); + + TextWatcher watcher = new TextWatcher() { + public void onTextChanged(CharSequence c, int start, int before, int count) { + boolean equal = AuthHelper.arePasswordsEqual(textPassword, textPasswordConfirm); + _buttonOK.setEnabled(equal); + } + public void beforeTextChanged(CharSequence c, int start, int count, int after) { } + public void afterTextChanged(Editable c) { } + }; + textPassword.addTextChangedListener(watcher); + textPasswordConfirm.addTextChangedListener(watcher); + + return alert; + } +} diff --git a/app/src/main/java/me/impy/aegis/PreferencesActivity.java b/app/src/main/java/me/impy/aegis/PreferencesActivity.java index 25be06ce..6920f514 100644 --- a/app/src/main/java/me/impy/aegis/PreferencesActivity.java +++ b/app/src/main/java/me/impy/aegis/PreferencesActivity.java @@ -9,12 +9,15 @@ import android.widget.Toast; public class PreferencesActivity extends AegisActivity { public static final int ACTION_EXPORT = 0; + public static final int ACTION_SLOTS = 1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - getFragmentManager().beginTransaction().replace(android.R.id.content, new PreferencesFragment()).commit(); + PreferencesFragment fragment = new PreferencesFragment(); + fragment.setArguments(getIntent().getExtras()); + getFragmentManager().beginTransaction().replace(android.R.id.content, fragment).commit(); } @Override @@ -65,6 +68,17 @@ public class PreferencesActivity extends AegisActivity { } }); + Preference slotsPreference = findPreference("pref_slots"); + slotsPreference.setEnabled(getArguments().getBoolean("encrypted")); + slotsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + _result.putExtra("action", ACTION_SLOTS); + finish(); + return true; + } + }); + Preference issuerPreference = findPreference("pref_issuer"); issuerPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override diff --git a/app/src/main/java/me/impy/aegis/SlotAdapter.java b/app/src/main/java/me/impy/aegis/SlotAdapter.java new file mode 100644 index 00000000..5e650e3b --- /dev/null +++ b/app/src/main/java/me/impy/aegis/SlotAdapter.java @@ -0,0 +1,70 @@ +package me.impy.aegis; + +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; + +import me.impy.aegis.crypto.slots.Slot; + +public class SlotAdapter extends RecyclerView.Adapter { + private Listener _listener; + private ArrayList _slots; + + public SlotAdapter(Listener listener) { + _listener = listener; + _slots = new ArrayList<>(); + } + + public void addSlot(Slot slot) { + _slots.add(slot); + + int position = getItemCount() - 1; + if (position == 0) { + notifyDataSetChanged(); + } else { + notifyItemInserted(position); + } + } + + public void updateSlot(Slot slot) { + notifyItemChanged(_slots.indexOf(slot)); + } + + public void removeSlot(Slot slot) { + int position = _slots.indexOf(slot); + _slots.remove(position); + notifyItemRemoved(position); + } + + @Override + public SlotHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_slot, parent, false); + return new SlotHolder(view); + } + + @Override + public void onBindViewHolder(SlotHolder holder, int position) { + holder.setData(_slots.get(position)); + holder.setOnEditClickListener(v -> { + int position1 = holder.getAdapterPosition(); + _listener.onEditSlot(_slots.get(position1)); + }); + holder.setOnDeleteClickListener(v -> { + int position12 = holder.getAdapterPosition(); + _listener.onRemoveSlot(_slots.get(position12)); + }); + } + + @Override + public int getItemCount() { + return _slots.size(); + } + + public interface Listener { + void onEditSlot(Slot slot); + void onRemoveSlot(Slot slot); + } +} diff --git a/app/src/main/java/me/impy/aegis/SlotCollectionTask.java b/app/src/main/java/me/impy/aegis/SlotCollectionTask.java index f323a80e..30bc70ee 100644 --- a/app/src/main/java/me/impy/aegis/SlotCollectionTask.java +++ b/app/src/main/java/me/impy/aegis/SlotCollectionTask.java @@ -42,7 +42,6 @@ public class SlotCollectionTask extends ProgressDialogTask extends ProgressDialogTask { + FingerprintDialogFragment dialog = new FingerprintDialogFragment(); + dialog.show(getSupportFragmentManager(), null); + }); + } else { + findViewById(R.id.button_add_fingerprint).setVisibility(View.GONE); + } + + findViewById(R.id.button_add_password).setOnClickListener(view -> { + PasswordDialogFragment dialog = new PasswordDialogFragment(); + dialog.show(getSupportFragmentManager(), null); + }); + + // set up the recycler view + _adapter = new SlotAdapter(this); + RecyclerView slotsView = findViewById(R.id.list_slots); + LinearLayoutManager layoutManager = new LinearLayoutManager(this); + slotsView.setLayoutManager(layoutManager); + slotsView.setAdapter(_adapter); + slotsView.setNestedScrollingEnabled(false); + + // load the slots and masterKey + _masterKey = (MasterKey) getIntent().getSerializableExtra("masterKey"); + _slots = (SlotCollection) getIntent().getSerializableExtra("slots"); + for (Slot slot : _slots) { + _adapter.addSlot(slot); + } + } + + private boolean onSave() { + Intent intent = new Intent(); + intent.putExtra("slots", _slots); + setResult(RESULT_OK, intent); + finish(); + return true; + } + + @Override + protected void setPreferredTheme(boolean nightMode) { + if (nightMode) { + setTheme(R.style.AppTheme_Dark_NoActionBar); + } else { + setTheme(R.style.AppTheme_Default_NoActionBar); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + onBackPressed(); + return true; + case R.id.action_save: + return onSave(); + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_slots, menu); + return true; + } + + @Override + public void onEditSlot(Slot slot) { + EditText textName = new EditText(this); + textName.setHint("Name"); + + new AlertDialog.Builder(this) + .setTitle("Edit slot name") + .setView(textName) + .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> { + String name = textName.getText().toString(); + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + @Override + public void onRemoveSlot(Slot slot) { + if (slot instanceof PasswordSlot && _slots.findAll(PasswordSlot.class).size() <= 1) { + Toast.makeText(this, "You must have at least one password slot", Toast.LENGTH_SHORT).show(); + return; + } + + new AlertDialog.Builder(this) + .setTitle("Remove slot") + .setMessage("Are you sure you want to remove this slot?") + .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> { + _slots.remove(slot); + _adapter.removeSlot(slot); + }) + .setNegativeButton(android.R.string.no, null) + .show(); + } + + @Override + public void onSlotResult(Slot slot, Cipher cipher) { + try { + _slots.encrypt(slot, _masterKey, cipher); + } catch (Exception e) { + onException(e); + return; + } + _slots.add(slot); + _adapter.addSlot(slot); + } + + @Override + public void onException(Exception e) { + Toast.makeText(this, "An error occurred while trying to add a new slot: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + } +} diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseManager.java b/app/src/main/java/me/impy/aegis/db/DatabaseManager.java index d48c14c6..5ff276af 100644 --- a/app/src/main/java/me/impy/aegis/db/DatabaseManager.java +++ b/app/src/main/java/me/impy/aegis/db/DatabaseManager.java @@ -167,6 +167,11 @@ public class DatabaseManager { return _db.getKeys(); } + public MasterKey getMasterKey() throws Exception { + assertState(false, true); + return _key; + } + public DatabaseFile getFile() { return _file; } diff --git a/app/src/main/res/drawable/ic_fingerprint_black_24dp.xml b/app/src/main/res/drawable/ic_fingerprint_black_24dp.xml new file mode 100644 index 00000000..d0f76e34 --- /dev/null +++ b/app/src/main/res/drawable/ic_fingerprint_black_24dp.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus_black_24dp.xml b/app/src/main/res/drawable/ic_plus_black_24dp.xml new file mode 100644 index 00000000..ddd6dfe6 --- /dev/null +++ b/app/src/main/res/drawable/ic_plus_black_24dp.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/layout/activity_auth.xml b/app/src/main/res/layout/activity_auth.xml index b7cda272..c2e0e3af 100644 --- a/app/src/main/res/layout/activity_auth.xml +++ b/app/src/main/res/layout/activity_auth.xml @@ -62,7 +62,7 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginLeft="15dp" - android:text="Touch sensor" + android:text="@string/fingerprint_hint" android:textColor="?attr/secondaryText"/> diff --git a/app/src/main/res/layout/activity_slots.xml b/app/src/main/res/layout/activity_slots.xml new file mode 100644 index 00000000..7e27a845 --- /dev/null +++ b/app/src/main/res/layout/activity_slots.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/card_slot.xml b/app/src/main/res/layout/card_slot.xml new file mode 100644 index 00000000..cc72f4e7 --- /dev/null +++ b/app/src/main/res/layout/card_slot.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_fingerprint.xml b/app/src/main/res/layout/dialog_fingerprint.xml new file mode 100644 index 00000000..2c2a339f --- /dev/null +++ b/app/src/main/res/layout/dialog_fingerprint.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/layout/dialog_new_profile.xml b/app/src/main/res/layout/dialog_new_profile.xml deleted file mode 100644 index a67fa6d3..00000000 --- a/app/src/main/res/layout/dialog_new_profile.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_password.xml b/app/src/main/res/layout/dialog_password.xml new file mode 100644 index 00000000..f66ba768 --- /dev/null +++ b/app/src/main/res/layout/dialog_password.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/layout/fragment_authenticated_slide.xml b/app/src/main/res/layout/fragment_authenticated_slide.xml index 81c36a02..a79b7b93 100644 --- a/app/src/main/res/layout/fragment_authenticated_slide.xml +++ b/app/src/main/res/layout/fragment_authenticated_slide.xml @@ -95,7 +95,7 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginLeft="15dp" - android:text="Touch sensor"/> + android:text="@string/fingerprint_hint"/> diff --git a/app/src/main/res/menu/menu_slots.xml b/app/src/main/res/menu/menu_slots.xml new file mode 100644 index 00000000..f02adc7e --- /dev/null +++ b/app/src/main/res/menu/menu_slots.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 0eb2e2c0..ad24c9c2 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -14,6 +14,11 @@ android:title="@string/pref_issuers" android:summary="@string/pref_issuers_description"/> + +