Add a new activity that allows editing key profiles

pull/41/head
Alexander Bakker 7 years ago
parent 05cfc0bc5f
commit 07c3e43160

@ -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) {
}
}
}

@ -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);
}
}

@ -46,6 +46,18 @@ public class KeyProfileAdapter extends RecyclerView.Adapter<KeyProfileHolder> 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) {

@ -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);
}
}

@ -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);

@ -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();

@ -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));
}

@ -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);

@ -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<CharSequence> 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();
}
}

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
<path
android:pathData="M0 0h24v24H0z" />
</vector>

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M0 0h24v24H0z" />
<path
android:fillColor="#000000"
android:pathData="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z" />
</vector>

@ -0,0 +1,185 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/viewA"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="0.66"
android:background="@color/colorPrimary"
android:orientation="horizontal">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:id="@+id/profile_drawable"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true"/>
</RelativeLayout>
<TableLayout
android:id="@+id/viewB"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="0.33"
android:background="?attr/background"
android:orientation="vertical"
android:stretchColumns="1"
android:layout_marginEnd="35dp">
<TableRow
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp">
<ImageView android:layout_column="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_person_black_24dp"
android:layout_weight="1"
android:layout_marginTop="10dp"
android:tint="@color/cardview_dark_background"
android:layout_marginStart="15dp"
android:layout_marginEnd="20dp"/>
<EditText android:layout_column="1"
android:id="@+id/text_name"
android:hint="Name"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</TableRow>
<TableRow
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp">
<EditText android:layout_column="1"
android:id="@+id/text_issuer"
android:hint="Issuer"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</TableRow>
<TableRow
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp">
<ImageView android:layout_column="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_info_outline_black_24dp"
android:layout_weight="1"
android:layout_marginTop="10dp"
android:tint="@color/cardview_dark_background"
android:layout_marginStart="15dp"
android:layout_marginEnd="20dp"/>
<LinearLayout android:layout_column="1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Spinner
android:id="@+id/spinner_type"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
style="@style/Base.Widget.AppCompat.Spinner.Underlined"/>
<Spinner
android:id="@+id/spinner_algo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
style="@style/Base.Widget.AppCompat.Spinner.Underlined"/>
<Spinner
android:id="@+id/spinner_digits"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
style="@style/Base.Widget.AppCompat.Spinner.Underlined"/>
</LinearLayout>
</TableRow>
<TableRow
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp">
<ImageView android:layout_column="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_timelapse_black_24dp"
android:layout_weight="1"
android:layout_marginTop="10dp"
android:tint="@color/cardview_dark_background"
android:layout_marginStart="15dp"
android:layout_marginEnd="20dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<EditText
android:id="@+id/text_period"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_gravity="center"
android:inputType="numberDecimal"/>
<TextView
android:id="@+id/text_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:text="seconds"
android:textSize="18sp"
android:layout_marginEnd="10dp"
android:textColor="#808080"/>
</RelativeLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>
</TableRow>
<TableRow
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp">
<ImageView android:layout_column="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_vpn_key_black_24dp"
android:layout_weight="1"
android:layout_marginTop="10dp"
android:tint="@color/cardview_dark_background"
android:layout_marginStart="15dp"
android:layout_marginEnd="20dp"/>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:hintEnabled="false"
app:passwordToggleEnabled="true">
<EditText
android:id="@+id/text_secret"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Secret (base32)"
android:inputType="textPassword"/>
</android.support.design.widget.TextInputLayout>
</TableRow>
</TableLayout>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="me.impy.aegis.EditProfileActivity">
<item
android:id="@+id/action_save"
app:showAsAction="ifRoom"
android:title="@string/save"/>
<item
android:id="@+id/action_delete"
android:title="@string/action_delete"
app:showAsAction="never"/>
</menu>

@ -2,6 +2,9 @@
<string name="app_name">Aegis</string>
<string name="action_settings">Settings</string>
<string name="action_import">Import</string>
<string name="action_delete">Delete</string>
<string name="discard">Discard</string>
<string name="save">Save</string>
<string name="title_activity_intro">IntroActivity</string>
<string name="settings">Preferences</string>
<string name="pref_night_mode">Night mode</string>
@ -32,6 +35,22 @@
<item>Password &amp; Fingerprint</item>
</string-array>
<string-array name="otp_types_array">
<item>TOTP</item>
<item>HOTP</item>
</string-array>
<string-array name="otp_algo_array">
<item>SHA1</item>
<item>SHA256</item>
<item>SHA512</item>
</string-array>
<string-array name="otp_digits_array">
<item>6</item>
<item>8</item>
</string-array>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
</resources>

@ -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,
},
...
]

Loading…
Cancel
Save