diff --git a/app/src/main/java/me/impy/aegis/EditProfileActivity.java b/app/src/main/java/me/impy/aegis/EditProfileActivity.java new file mode 100644 index 00000000..969824e3 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/EditProfileActivity.java @@ -0,0 +1,220 @@ +package me.impy.aegis; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AlertDialog; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.widget.AdapterView; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.Spinner; + +import me.impy.aegis.crypto.KeyInfo; +import me.impy.aegis.db.DatabaseEntry; +import me.impy.aegis.encoding.Base32; +import me.impy.aegis.helpers.SpinnerHelper; + +public class EditProfileActivity extends AegisActivity { + private boolean _edited = false; + private KeyProfile _profile; + + private EditText _textName; + private EditText _textIssuer; + private EditText _textPeriod; + private EditText _textSecret; + + private Spinner _spinnerType; + private Spinner _spinnerAlgo; + private Spinner _spinnerDigits; + private SpinnerItemSelectedListener _selectedListener = new SpinnerItemSelectedListener(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_edit_profile); + + _profile = (KeyProfile) getIntent().getSerializableExtra("KeyProfile"); + + ActionBar bar = getSupportActionBar(); + bar.setHomeAsUpIndicator(R.drawable.ic_close); + bar.setDisplayHomeAsUpEnabled(true); + + ImageView imageView = findViewById(R.id.profile_drawable); + imageView.setImageDrawable(_profile.getDrawable()); + + DatabaseEntry entry = _profile.getEntry(); + _textName = findViewById(R.id.text_name); + _textName.setText(entry.getName()); + _textName.addTextChangedListener(watcher); + + _textIssuer = findViewById(R.id.text_issuer); + _textIssuer.setText(entry.getInfo().getIssuer()); + _textIssuer.addTextChangedListener(watcher); + + _textPeriod = findViewById(R.id.text_period); + _textPeriod.setText(Integer.toString(entry.getInfo().getPeriod())); + _textPeriod.addTextChangedListener(watcher); + + _textSecret = findViewById(R.id.text_secret); + _textSecret.setText(Base32.encodeOriginal(entry.getInfo().getSecret())); + _textSecret.addTextChangedListener(watcher); + + _spinnerType = findViewById(R.id.spinner_type); + SpinnerHelper.fillSpinner(this, _spinnerType, R.array.otp_types_array); + _spinnerType.setOnTouchListener(_selectedListener); + _spinnerType.setOnItemSelectedListener(_selectedListener); + + _spinnerAlgo = findViewById(R.id.spinner_algo); + SpinnerHelper.fillSpinner(this, _spinnerAlgo, R.array.otp_algo_array); + _spinnerAlgo.setOnTouchListener(_selectedListener); + _spinnerAlgo.setOnItemSelectedListener(_selectedListener); + + _spinnerDigits = findViewById(R.id.spinner_digits); + SpinnerHelper.fillSpinner(this, _spinnerDigits, R.array.otp_digits_array); + _spinnerDigits.setOnTouchListener(_selectedListener); + _spinnerDigits.setOnItemSelectedListener(_selectedListener); + } + + @Override + protected void setPreferredTheme(boolean nightMode) { + if (nightMode) { + setTheme(R.style.AppTheme_Dark_TransparentActionBar); + } else { + setTheme(R.style.AppTheme_Default_TransparentActionBar); + } + } + + @Override + public void onBackPressed() { + if (!_edited) { + super.onBackPressed(); + return; + } + + new AlertDialog.Builder(this) + .setMessage("Your changes have not been saved") + .setPositiveButton(R.string.save, (dialog, which) -> onSave()) + .setNegativeButton(R.string.discard, (dialog, which) -> super.onBackPressed()) + .show(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + onBackPressed(); + return true; + case R.id.action_save: + return onSave(); + case R.id.action_delete: + return onDelete(); + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_edit, menu); + return true; + } + + private boolean onDelete() { + return false; + } + + private boolean onSave() { + int period; + try { + period = Integer.parseInt(_textPeriod.getText().toString()); + } catch (NumberFormatException e) { + onError("Period is not an integer."); + return false; + } + + String type = _spinnerType.getSelectedItem().toString(); + String algo = _spinnerAlgo.getSelectedItem().toString(); + + int digits; + try { + digits = Integer.parseInt(_spinnerDigits.getSelectedItem().toString()); + } catch (NumberFormatException e) { + onError("Digits is not an integer."); + return false; + } + + DatabaseEntry entry = _profile.getEntry(); + entry.setName(_textName.getText().toString()); + KeyInfo info = entry.getInfo(); + info.setIssuer(_textIssuer.getText().toString()); + info.setSecret(Base32.decode(_textSecret.getText().toString())); + info.setPeriod(period); + info.setDigits(digits); + info.setAlgorithm(algo); + info.setType(type); + + Intent intent = new Intent(); + intent.putExtra("KeyProfile", _profile); + setResult(RESULT_OK, intent); + finish(); + return true; + } + + private void onError(String msg) { + new AlertDialog.Builder(this) + .setTitle("Error saving profile") + .setMessage(msg) + .setPositiveButton(android.R.string.ok, null) + .show(); + } + + private void onFieldEdited() { + _edited = true; + } + + private TextWatcher watcher = new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + onFieldEdited(); + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + onFieldEdited(); + } + + @Override + public void afterTextChanged(Editable s) { + onFieldEdited(); + } + }; + + private class SpinnerItemSelectedListener implements AdapterView.OnItemSelectedListener, View.OnTouchListener { + private boolean _userSelect = false; + + @Override + public boolean onTouch(View v, MotionEvent event) { + _userSelect = true; + return false; + } + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (_userSelect) { + onFieldEdited(); + _userSelect = false; + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + } +} diff --git a/app/src/main/java/me/impy/aegis/KeyProfile.java b/app/src/main/java/me/impy/aegis/KeyProfile.java index e5b0d232..bf472dc6 100644 --- a/app/src/main/java/me/impy/aegis/KeyProfile.java +++ b/app/src/main/java/me/impy/aegis/KeyProfile.java @@ -1,5 +1,8 @@ package me.impy.aegis; +import com.amulyakhare.textdrawable.TextDrawable; +import com.amulyakhare.textdrawable.util.ColorGenerator; + import java.io.Serializable; import java.lang.reflect.UndeclaredThrowableException; @@ -29,4 +32,16 @@ public class KeyProfile implements Serializable { } return _code; } + + public TextDrawable getDrawable() { + String name = _entry.getName(); + if (name == null) { + return null; + } + + ColorGenerator generator = ColorGenerator.MATERIAL; + int color = generator.getColor(name); + + return TextDrawable.builder().buildRound(name.substring(0, 1).toUpperCase(), color); + } } diff --git a/app/src/main/java/me/impy/aegis/KeyProfileAdapter.java b/app/src/main/java/me/impy/aegis/KeyProfileAdapter.java index eb541009..ecd6430d 100644 --- a/app/src/main/java/me/impy/aegis/KeyProfileAdapter.java +++ b/app/src/main/java/me/impy/aegis/KeyProfileAdapter.java @@ -46,6 +46,18 @@ public class KeyProfileAdapter extends RecyclerView.Adapter im notifyDataSetChanged(); } + public void replaceKey(KeyProfile newProfile) { + for (KeyProfile oldProfile : _keyProfiles) { + if (oldProfile.getEntry().getID() == newProfile.getEntry().getID()) { + int position = _keyProfiles.indexOf(oldProfile); + _keyProfiles.set(position, newProfile); + notifyItemChanged(position); + return; + } + } + throw new AssertionError("no key profile found with the same id"); + } + @Override public void onItemDismiss(int position) { diff --git a/app/src/main/java/me/impy/aegis/KeyProfileHolder.java b/app/src/main/java/me/impy/aegis/KeyProfileHolder.java index 39aac446..0d92b196 100644 --- a/app/src/main/java/me/impy/aegis/KeyProfileHolder.java +++ b/app/src/main/java/me/impy/aegis/KeyProfileHolder.java @@ -46,7 +46,8 @@ public class KeyProfileHolder extends RecyclerView.ViewHolder { _profileIssuer.setText(" - " + profile.getEntry().getInfo().getIssuer()); } - _profileDrawable.setImageDrawable(generateTextDrawable(profile)); + TextDrawable drawable = profile.getDrawable(); + _profileDrawable.setImageDrawable(drawable); } public void startUpdateLoop() { @@ -88,15 +89,4 @@ public class KeyProfileHolder extends RecyclerView.ViewHolder { animation.start(); return true; } - - private TextDrawable generateTextDrawable(KeyProfile profile) { - if (_profileName == null) { - return null; - } - - ColorGenerator generator = ColorGenerator.MATERIAL; - int profileKeyColor = generator.getColor(profile.getEntry().getName()); - - return TextDrawable.builder().buildRound(profile.getEntry().getName().substring(0, 1).toUpperCase(), profileKeyColor); - } } diff --git a/app/src/main/java/me/impy/aegis/KeyProfileView.java b/app/src/main/java/me/impy/aegis/KeyProfileView.java index 08301d9f..214834f3 100644 --- a/app/src/main/java/me/impy/aegis/KeyProfileView.java +++ b/app/src/main/java/me/impy/aegis/KeyProfileView.java @@ -79,6 +79,10 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen _adapter.clearKeys(); } + public void replaceKey(KeyProfile profile) { + _adapter.replaceKey(profile); + } + public interface Listener { void onEntryClick(KeyProfile profile); void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2); diff --git a/app/src/main/java/me/impy/aegis/MainActivity.java b/app/src/main/java/me/impy/aegis/MainActivity.java index b359bd0d..a5c6fdcf 100644 --- a/app/src/main/java/me/impy/aegis/MainActivity.java +++ b/app/src/main/java/me/impy/aegis/MainActivity.java @@ -139,6 +139,9 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen case CODE_ADD_KEYINFO: onAddKeyInfoResult(resultCode, data); break; + case CODE_EDIT_KEYINFO: + onEditKeyInfoResult(resultCode, data); + break; case CODE_DO_INTRO: onDoIntroResult(resultCode, data); break; @@ -319,6 +322,23 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen } } + private void onEditKeyInfoResult(int resultCode, Intent data) { + if (resultCode == RESULT_OK) { + // this profile has been serialized/deserialized and is no longer the same instance it once was + // to deal with this, the replaceKey functions are used + KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile"); + try { + _db.replaceKey(profile.getEntry()); + } catch (Exception e) { + e.printStackTrace(); + Toast.makeText(this, "An error occurred while trying to update an entry", Toast.LENGTH_SHORT).show(); + return; + } + _keyProfileView.replaceKey(profile); + saveDatabase(); + } + } + private void addKey(KeyProfile profile) { profile.refreshCode(); diff --git a/app/src/main/java/me/impy/aegis/db/Database.java b/app/src/main/java/me/impy/aegis/db/Database.java index becf2c9e..db79c8e0 100644 --- a/app/src/main/java/me/impy/aegis/db/Database.java +++ b/app/src/main/java/me/impy/aegis/db/Database.java @@ -67,6 +67,16 @@ public class Database { _entries.remove(entry); } + public void replaceKey(DatabaseEntry newEntry) { + for (DatabaseEntry oldEntry : _entries) { + if (oldEntry.getID() == newEntry.getID()) { + _entries.set(_entries.indexOf(oldEntry), newEntry); + return; + } + } + throw new AssertionError("no entry found with the same id"); + } + public void swapKeys(DatabaseEntry entry1, DatabaseEntry entry2) { Collections.swap(_entries, _entries.indexOf(entry1), _entries.indexOf(entry2)); } 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 16a24683..02cfe27a 100644 --- a/app/src/main/java/me/impy/aegis/db/DatabaseManager.java +++ b/app/src/main/java/me/impy/aegis/db/DatabaseManager.java @@ -152,6 +152,11 @@ public class DatabaseManager { _db.removeKey(entry); } + public void replaceKey(DatabaseEntry entry) throws Exception { + assertState(false, true); + _db.replaceKey(entry); + } + public void swapKeys(DatabaseEntry entry1, DatabaseEntry entry2) throws Exception { assertState(false, true); _db.swapKeys(entry1, entry2); diff --git a/app/src/main/java/me/impy/aegis/helpers/SpinnerHelper.java b/app/src/main/java/me/impy/aegis/helpers/SpinnerHelper.java new file mode 100644 index 00000000..479fe241 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/helpers/SpinnerHelper.java @@ -0,0 +1,19 @@ +package me.impy.aegis.helpers; + +import android.content.Context; +import android.support.annotation.ArrayRes; +import android.widget.ArrayAdapter; +import android.widget.Spinner; + +public class SpinnerHelper { + private SpinnerHelper() { + + } + + public static void fillSpinner(Context context, Spinner spinner, @ArrayRes int textArrayResId) { + ArrayAdapter adapter = ArrayAdapter.createFromResource(context, textArrayResId, android.R.layout.simple_spinner_item); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(adapter); + spinner.invalidate(); + } +} diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 00000000..b5c7319f --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_info_outline_black_24dp.xml b/app/src/main/res/drawable/ic_info_outline_black_24dp.xml new file mode 100644 index 00000000..ab95f29e --- /dev/null +++ b/app/src/main/res/drawable/ic_info_outline_black_24dp.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_edit_profile.xml b/app/src/main/res/layout/activity_edit_profile.xml new file mode 100644 index 00000000..f5a6dd6f --- /dev/null +++ b/app/src/main/res/layout/activity_edit_profile.xml @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu_edit.xml b/app/src/main/res/menu/menu_edit.xml new file mode 100644 index 00000000..68ae0392 --- /dev/null +++ b/app/src/main/res/menu/menu_edit.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 91c267d7..5bad2312 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,6 +2,9 @@ Aegis Settings Import + Delete + Discard + Save IntroActivity Preferences Night mode @@ -32,6 +35,22 @@ Password & Fingerprint + + TOTP + HOTP + + + + SHA1 + SHA256 + SHA512 + + + + 6 + 8 + + Hello blank fragment diff --git a/doc/db.md b/doc/db.md index 5c66b12f..c5c9ba15 100644 --- a/doc/db.md +++ b/doc/db.md @@ -114,13 +114,13 @@ The content of the database is a JSON file encoded in UTF-8. ```json { "version": 1, + "counter": 10, "entries": [ { "id": 1, "name": "ACME Co/john@example.com", "url": "otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30", - "order": 0, }, ... ]