mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-05-16 23:12:51 +00:00
Bunch of refactoring
- Get rid of KeyProfile and use DatabaseEntry directly - Don't store Google auth style urls in the db, but use separate fields - Update testdata to reflect db format changes - Lay the ground work for HOTP support - Refactor KeyInfo and split it into OtpInfo, TotpInto and HotpInfo - Surely some other stuff I forgot about
This commit is contained in:
parent
9859011a6d
commit
4a4ab1a82c
47 changed files with 1230 additions and 861 deletions
|
@ -19,32 +19,39 @@ import android.widget.EditText;
|
|||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TableRow;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
|
||||
import me.impy.aegis.R;
|
||||
import me.impy.aegis.crypto.KeyInfo;
|
||||
import me.impy.aegis.crypto.KeyInfoException;
|
||||
import me.impy.aegis.db.DatabaseEntry;
|
||||
import me.impy.aegis.encoding.Base32;
|
||||
import me.impy.aegis.encoding.Base32Exception;
|
||||
import me.impy.aegis.helpers.EditTextHelper;
|
||||
import me.impy.aegis.helpers.SpinnerHelper;
|
||||
import me.impy.aegis.helpers.TextDrawableHelper;
|
||||
import me.impy.aegis.otp.HotpInfo;
|
||||
import me.impy.aegis.otp.OtpInfo;
|
||||
import me.impy.aegis.otp.OtpInfoException;
|
||||
import me.impy.aegis.otp.TotpInfo;
|
||||
import me.impy.aegis.ui.dialogs.Dialogs;
|
||||
import me.impy.aegis.ui.views.KeyProfile;
|
||||
|
||||
public class EditProfileActivity extends AegisActivity {
|
||||
public class EditEntryActivity extends AegisActivity {
|
||||
private boolean _isNew = false;
|
||||
private boolean _edited = false;
|
||||
private KeyProfile _profile;
|
||||
private DatabaseEntry _entry;
|
||||
|
||||
private ImageView _iconView;
|
||||
|
||||
private EditText _textName;
|
||||
private EditText _textIssuer;
|
||||
private EditText _textPeriod;
|
||||
private EditText _textCounter;
|
||||
private EditText _textSecret;
|
||||
|
||||
private TableRow _rowPeriod;
|
||||
private TableRow _rowCounter;
|
||||
|
||||
private Spinner _spinnerType;
|
||||
private Spinner _spinnerAlgo;
|
||||
private Spinner _spinnerDigits;
|
||||
|
@ -56,27 +63,28 @@ public class EditProfileActivity extends AegisActivity {
|
|||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_edit_profile);
|
||||
setContentView(R.layout.activity_edit_entry);
|
||||
|
||||
ActionBar bar = getSupportActionBar();
|
||||
bar.setHomeAsUpIndicator(R.drawable.ic_close);
|
||||
bar.setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
// if the intent doesn't contain a KeyProfile, create a new one
|
||||
// retrieve info from the calling activity
|
||||
Intent intent = getIntent();
|
||||
_profile = (KeyProfile) intent.getSerializableExtra("KeyProfile");
|
||||
_entry = (DatabaseEntry) intent.getSerializableExtra("entry");
|
||||
_isNew = intent.getBooleanExtra("isNew", false);
|
||||
if (_profile == null) {
|
||||
_profile = new KeyProfile();
|
||||
}
|
||||
if (_isNew) {
|
||||
setTitle("Add profile");
|
||||
}
|
||||
|
||||
// set up fields
|
||||
_iconView = findViewById(R.id.profile_drawable);
|
||||
_textName = findViewById(R.id.text_name);
|
||||
_textIssuer = findViewById(R.id.text_issuer);
|
||||
_textPeriod = findViewById(R.id.text_period);
|
||||
_rowPeriod = findViewById(R.id.row_period);
|
||||
_textCounter = findViewById(R.id.text_counter);
|
||||
_rowCounter = findViewById(R.id.row_counter);
|
||||
_textSecret = findViewById(R.id.text_secret);
|
||||
_spinnerType = findViewById(R.id.spinner_type);
|
||||
SpinnerHelper.fillSpinner(this, _spinnerType, R.array.otp_types_array);
|
||||
|
@ -84,18 +92,50 @@ public class EditProfileActivity 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);
|
||||
|
||||
_advancedSettingsHeader = findViewById(R.id.accordian_header);
|
||||
_advancedSettings = findViewById(R.id.expandableLayout);
|
||||
|
||||
updateFields();
|
||||
// fill the fields with values if possible
|
||||
if (_entry != null) {
|
||||
TextDrawable drawable = TextDrawableHelper.generate(_entry.getName());
|
||||
_iconView.setImageDrawable(drawable);
|
||||
|
||||
_textName.setText(_entry.getName());
|
||||
_textIssuer.setText(_entry.getIssuer());
|
||||
|
||||
OtpInfo info = _entry.getInfo();
|
||||
if (info instanceof TotpInfo) {
|
||||
_textPeriod.setText(Integer.toString(((TotpInfo) info).getPeriod()));
|
||||
_rowPeriod.setVisibility(View.VISIBLE);
|
||||
} else if (info instanceof HotpInfo) {
|
||||
_textCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
|
||||
_rowCounter.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
byte[] secretBytes = _entry.getInfo().getSecret();
|
||||
if (secretBytes != null) {
|
||||
char[] secretChars = Base32.encode(secretBytes);
|
||||
_textSecret.setText(secretChars, 0, secretChars.length);
|
||||
}
|
||||
|
||||
String type = _entry.getInfo().getType();
|
||||
_spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type.toUpperCase()), false);
|
||||
|
||||
String algo = _entry.getInfo().getAlgorithm(false);
|
||||
_spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false);
|
||||
|
||||
String digits = Integer.toString(_entry.getInfo().getDigits());
|
||||
_spinnerDigits.setSelection(getStringResourceIndex(R.array.otp_digits_array, digits), false);
|
||||
}
|
||||
|
||||
// listen for changes to any of the fields
|
||||
_textName.addTextChangedListener(_textListener);
|
||||
_textIssuer.addTextChangedListener(_textListener);
|
||||
_textPeriod.addTextChangedListener(_textListener);
|
||||
_textCounter.addTextChangedListener(_textListener);
|
||||
_textSecret.addTextChangedListener(_textListener);
|
||||
_spinnerType.setOnTouchListener(_selectedListener);
|
||||
_spinnerType.setOnItemSelectedListener(_selectedListener);
|
||||
_spinnerAlgo.setOnTouchListener(_selectedListener);
|
||||
_spinnerAlgo.setOnItemSelectedListener(_selectedListener);
|
||||
_spinnerDigits.setOnTouchListener(_selectedListener);
|
||||
|
@ -118,40 +158,44 @@ public class EditProfileActivity extends AegisActivity {
|
|||
}
|
||||
});
|
||||
|
||||
// show/hide period and counter fields on type change
|
||||
_spinnerType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
String type = _spinnerType.getSelectedItem().toString();
|
||||
|
||||
switch (type.toLowerCase()) {
|
||||
case "totp":
|
||||
_rowCounter.setVisibility(View.GONE);
|
||||
_rowPeriod.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
case "hotp":
|
||||
_rowPeriod.setVisibility(View.GONE);
|
||||
_rowCounter.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
_selectedListener.onItemSelected(parent, view, position, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
_advancedSettingsHeader.setOnClickListener(v -> {
|
||||
openAdvancedSettings();
|
||||
});
|
||||
|
||||
// Automatically open advanced settings since 'Secret' is required.
|
||||
// automatically open advanced settings since 'Secret' is required.
|
||||
if(_isNew){
|
||||
openAdvancedSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFields() {
|
||||
DatabaseEntry entry = _profile.getEntry();
|
||||
_iconView.setImageDrawable(_profile.getDrawable());
|
||||
|
||||
_textName.setText(entry.getName());
|
||||
_textIssuer.setText(entry.getInfo().getIssuer());
|
||||
_textPeriod.setText(Integer.toString(entry.getInfo().getPeriod()));
|
||||
|
||||
byte[] secretBytes = entry.getInfo().getSecret();
|
||||
if (secretBytes != null) {
|
||||
char[] secretChars = Base32.encode(secretBytes);
|
||||
_textSecret.setText(secretChars, 0, secretChars.length);
|
||||
}
|
||||
|
||||
String type = entry.getInfo().getType();
|
||||
_spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type), false);
|
||||
|
||||
String algo = entry.getInfo().getAlgorithm(false);
|
||||
_spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false);
|
||||
|
||||
String digits = Integer.toString(entry.getInfo().getDigits());
|
||||
_spinnerDigits.setSelection(getStringResourceIndex(R.array.otp_digits_array, digits), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setPreferredTheme(boolean darkMode) {
|
||||
if (darkMode) {
|
||||
|
@ -252,7 +296,7 @@ public class EditProfileActivity extends AegisActivity {
|
|||
|
||||
private void finish(boolean delete) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("KeyProfile", _profile);
|
||||
intent.putExtra("entry", _entry);
|
||||
intent.putExtra("delete", delete);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
|
@ -264,14 +308,6 @@ public class EditProfileActivity extends AegisActivity {
|
|||
return false;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
|
@ -283,24 +319,60 @@ public class EditProfileActivity extends AegisActivity {
|
|||
return false;
|
||||
}
|
||||
|
||||
DatabaseEntry entry = _profile.getEntry();
|
||||
entry.setName(_textName.getText().toString());
|
||||
KeyInfo info = entry.getInfo();
|
||||
|
||||
byte[] secret;
|
||||
try {
|
||||
char[] secret = EditTextHelper.getEditTextChars(_textSecret);
|
||||
info.setSecret(secret);
|
||||
info.setIssuer(_textIssuer.getText().toString());
|
||||
info.setPeriod(period);
|
||||
secret = Base32.decode(EditTextHelper.getEditTextChars(_textSecret));
|
||||
} catch (Base32Exception e) {
|
||||
onError("Secret is not valid base32.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// set otp info
|
||||
OtpInfo info;
|
||||
try {
|
||||
switch (type.toLowerCase()) {
|
||||
case "totp":
|
||||
int period;
|
||||
try {
|
||||
period = Integer.parseInt(_textPeriod.getText().toString());
|
||||
} catch (NumberFormatException e) {
|
||||
onError("Period is not an integer.");
|
||||
return false;
|
||||
}
|
||||
info = new TotpInfo(secret, algo, digits, period);
|
||||
break;
|
||||
case "hotp":
|
||||
long counter;
|
||||
try {
|
||||
counter = Long.parseLong(_textCounter.getText().toString());
|
||||
} catch (NumberFormatException e) {
|
||||
onError("Counter is not an integer.");
|
||||
return false;
|
||||
}
|
||||
info = new HotpInfo(secret, algo, digits, counter);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
info.setDigits(digits);
|
||||
info.setAlgorithm(algo);
|
||||
info.setType(type);
|
||||
info.setAccountName(_textName.getText().toString());
|
||||
} catch (KeyInfoException e) {
|
||||
} catch (OtpInfoException e) {
|
||||
onError("The entered info is incorrect: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
// set database entry info
|
||||
DatabaseEntry entry = _entry;
|
||||
if (entry == null) {
|
||||
entry = new DatabaseEntry(info);
|
||||
} else {
|
||||
entry.setInfo(info);
|
||||
}
|
||||
entry.setIssuer(_textIssuer.getText().toString());
|
||||
entry.setName(_textName.getText().toString());
|
||||
|
||||
_entry = entry;
|
||||
finish(false);
|
||||
return true;
|
||||
}
|
|
@ -18,7 +18,6 @@ import javax.crypto.SecretKey;
|
|||
import me.impy.aegis.Preferences;
|
||||
import me.impy.aegis.R;
|
||||
import me.impy.aegis.crypto.MasterKey;
|
||||
import me.impy.aegis.db.DatabaseException;
|
||||
import me.impy.aegis.db.DatabaseFileException;
|
||||
import me.impy.aegis.db.DatabaseManagerException;
|
||||
import me.impy.aegis.db.slots.FingerprintSlot;
|
||||
|
@ -186,7 +185,7 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback {
|
|||
_databaseFile.setContent(obj, masterKey);
|
||||
}
|
||||
DatabaseManager.save(getApplicationContext(), _databaseFile);
|
||||
} catch (DatabaseException | DatabaseManagerException | DatabaseFileException e) {
|
||||
} catch (DatabaseManagerException | DatabaseFileException e) {
|
||||
setException(e);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -26,15 +26,14 @@ import me.impy.aegis.db.DatabaseEntry;
|
|||
import me.impy.aegis.db.DatabaseManager;
|
||||
import me.impy.aegis.helpers.PermissionHelper;
|
||||
import me.impy.aegis.ui.dialogs.Dialogs;
|
||||
import me.impy.aegis.ui.views.KeyProfile;
|
||||
import me.impy.aegis.ui.views.KeyProfileView;
|
||||
import me.impy.aegis.ui.views.EntryListView;
|
||||
|
||||
public class MainActivity extends AegisActivity implements KeyProfileView.Listener {
|
||||
public class MainActivity extends AegisActivity implements EntryListView.Listener {
|
||||
// activity request codes
|
||||
private static final int CODE_SCAN_KEYINFO = 0;
|
||||
private static final int CODE_ADD_KEYINFO = 1;
|
||||
private static final int CODE_EDIT_KEYINFO = 2;
|
||||
private static final int CODE_ENTER_KEYINFO = 3;
|
||||
private static final int CODE_SCAN = 0;
|
||||
private static final int CODE_ADD_ENTRY = 1;
|
||||
private static final int CODE_EDIT_ENTRY = 2;
|
||||
private static final int CODE_ENTER_ENTRY = 3;
|
||||
private static final int CODE_DO_INTRO = 4;
|
||||
private static final int CODE_DECRYPT = 5;
|
||||
private static final int CODE_PREFERENCES = 6;
|
||||
|
@ -44,7 +43,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
|
||||
private AegisApplication _app;
|
||||
private DatabaseManager _db;
|
||||
private KeyProfileView _keyProfileView;
|
||||
private EntryListView _entryListView;
|
||||
|
||||
private Menu _menu;
|
||||
private FloatingActionsMenu _fabMenu;
|
||||
|
@ -58,20 +57,20 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
// set up the main view
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
// set up the key profile view
|
||||
_keyProfileView = (KeyProfileView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
|
||||
_keyProfileView.setListener(this);
|
||||
_keyProfileView.setShowIssuer(getPreferences().isIssuerVisible());
|
||||
// set up the entry view
|
||||
_entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
|
||||
_entryListView.setListener(this);
|
||||
_entryListView.setShowIssuer(getPreferences().isIssuerVisible());
|
||||
|
||||
// set up the floating action button
|
||||
_fabMenu = findViewById(R.id.fab);
|
||||
findViewById(R.id.fab_enter).setOnClickListener(view -> {
|
||||
_fabMenu.collapse();
|
||||
onEnterKeyInfo();
|
||||
onEnterEntry();
|
||||
});
|
||||
findViewById(R.id.fab_scan).setOnClickListener(view -> {
|
||||
_fabMenu.collapse();
|
||||
onScanKeyInfo();
|
||||
onScan();
|
||||
});
|
||||
|
||||
// skip this part if this is the not initial startup and the database has been unlocked
|
||||
|
@ -101,9 +100,9 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
}
|
||||
}
|
||||
|
||||
// if the database has been decrypted at this point, we can load the key profiles
|
||||
// if the database has been decrypted at this point, we can load the entries
|
||||
if (!_db.isLocked()) {
|
||||
loadKeyProfiles();
|
||||
loadEntries();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -141,17 +140,17 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
}
|
||||
|
||||
switch (requestCode) {
|
||||
case CODE_SCAN_KEYINFO:
|
||||
onScanKeyInfoResult(resultCode, data);
|
||||
case CODE_SCAN:
|
||||
onScanResult(resultCode, data);
|
||||
break;
|
||||
case CODE_ADD_KEYINFO:
|
||||
onAddKeyInfoResult(resultCode, data);
|
||||
case CODE_ADD_ENTRY:
|
||||
onAddEntryResult(resultCode, data);
|
||||
break;
|
||||
case CODE_EDIT_KEYINFO:
|
||||
onEditKeyInfoResult(resultCode, data);
|
||||
case CODE_EDIT_ENTRY:
|
||||
onEditEntryResult(resultCode, data);
|
||||
break;
|
||||
case CODE_ENTER_KEYINFO:
|
||||
onEnterKeyInfoResult(resultCode, data);
|
||||
case CODE_ENTER_ENTRY:
|
||||
onEnterEntryResult(resultCode, data);
|
||||
break;
|
||||
case CODE_DO_INTRO:
|
||||
onDoIntroResult(resultCode, data);
|
||||
|
@ -174,35 +173,35 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
|
||||
switch (requestCode) {
|
||||
case CODE_PERM_CAMERA:
|
||||
onScanKeyInfo();
|
||||
onScan();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void onPreferencesResult(int resultCode, Intent data) {
|
||||
// refresh the entire key profile list if needed
|
||||
// refresh the entire entry list if needed
|
||||
if (data.getBooleanExtra("needsRecreate", false)) {
|
||||
recreate();
|
||||
} else if (data.getBooleanExtra("needsRefresh", false)) {
|
||||
boolean showIssuer = getPreferences().isIssuerVisible();
|
||||
_keyProfileView.setShowIssuer(showIssuer);
|
||||
_entryListView.setShowIssuer(showIssuer);
|
||||
}
|
||||
}
|
||||
|
||||
private void startEditProfileActivity(int requestCode, KeyProfile profile, boolean isNew) {
|
||||
Intent intent = new Intent(this, EditProfileActivity.class);
|
||||
if (profile != null) {
|
||||
intent.putExtra("KeyProfile", profile);
|
||||
private void startEditProfileActivity(int requestCode, DatabaseEntry entry, boolean isNew) {
|
||||
Intent intent = new Intent(this, EditEntryActivity.class);
|
||||
if (entry != null) {
|
||||
intent.putExtra("entry", entry);
|
||||
}
|
||||
intent.putExtra("isNew", isNew);
|
||||
startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
private void onEnterKeyInfo() {
|
||||
startEditProfileActivity(CODE_ENTER_KEYINFO, null, true);
|
||||
private void onEnterEntry() {
|
||||
startEditProfileActivity(CODE_ENTER_ENTRY, null, true);
|
||||
}
|
||||
|
||||
private void onScanKeyInfo() {
|
||||
private void onScan() {
|
||||
if (!PermissionHelper.request(this, CODE_PERM_CAMERA, Manifest.permission.CAMERA)) {
|
||||
return;
|
||||
}
|
||||
|
@ -210,49 +209,47 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
startScanActivity();
|
||||
}
|
||||
|
||||
private void onScanKeyInfoResult(int resultCode, Intent data) {
|
||||
private void onScanResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
KeyProfile profile = (KeyProfile)data.getSerializableExtra("KeyProfile");
|
||||
startEditProfileActivity(CODE_ADD_KEYINFO, profile, true);
|
||||
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
|
||||
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void onAddKeyInfoResult(int resultCode, Intent data) {
|
||||
private void onAddEntryResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile");
|
||||
addKey(profile);
|
||||
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
|
||||
addEntry(entry);
|
||||
saveDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
private void onEditKeyInfoResult(int resultCode, Intent data) {
|
||||
private void onEditEntryResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile");
|
||||
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
|
||||
if (!data.getBooleanExtra("delete", false)) {
|
||||
// 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
|
||||
_db.replaceKey(profile.getEntry());
|
||||
_keyProfileView.replaceKey(profile);
|
||||
// to deal with this, the replaceEntry functions are used
|
||||
_db.replaceEntry(entry);
|
||||
_entryListView.replaceEntry(entry);
|
||||
saveDatabase();
|
||||
} else {
|
||||
deleteProfile(profile);
|
||||
deleteEntry(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onEnterKeyInfoResult(int resultCode, Intent data) {
|
||||
private void onEnterEntryResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile");
|
||||
addKey(profile);
|
||||
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
|
||||
addEntry(entry);
|
||||
saveDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
private void addKey(KeyProfile profile) {
|
||||
DatabaseEntry entry = profile.getEntry();
|
||||
entry.setName(entry.getInfo().getAccountName());
|
||||
_db.addKey(entry);
|
||||
_keyProfileView.addKey(profile);
|
||||
private void addEntry(DatabaseEntry entry) {
|
||||
_db.addEntry(entry);
|
||||
_entryListView.addEntry(entry);
|
||||
}
|
||||
|
||||
private void onDoIntroResult(int resultCode, Intent data) {
|
||||
|
@ -275,7 +272,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
return;
|
||||
}
|
||||
|
||||
loadKeyProfiles();
|
||||
loadEntries();
|
||||
}
|
||||
|
||||
private void onDecryptResult(int resultCode, Intent intent) {
|
||||
|
@ -289,13 +286,13 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
return;
|
||||
}
|
||||
|
||||
loadKeyProfiles();
|
||||
loadEntries();
|
||||
doShortcutActions();
|
||||
}
|
||||
|
||||
private void startScanActivity() {
|
||||
Intent scannerActivity = new Intent(getApplicationContext(), ScannerActivity.class);
|
||||
startActivityForResult(scannerActivity, CODE_SCAN_KEYINFO);
|
||||
startActivityForResult(scannerActivity, CODE_SCAN);
|
||||
}
|
||||
|
||||
private boolean doShortcutActions() {
|
||||
|
@ -330,12 +327,12 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
updateLockIcon();
|
||||
|
||||
// refresh all codes to prevent showing old ones
|
||||
_keyProfileView.refresh();
|
||||
_entryListView.refresh();
|
||||
}
|
||||
|
||||
private BottomSheetDialog createBottomSheet(final KeyProfile profile) {
|
||||
private BottomSheetDialog createBottomSheet(final DatabaseEntry entry) {
|
||||
BottomSheetDialog dialog = new BottomSheetDialog(this);
|
||||
dialog.setContentView(R.layout.bottom_sheet_edit_profile);
|
||||
dialog.setContentView(R.layout.bottom_sheet_edit_entry);
|
||||
dialog.setCancelable(true);
|
||||
dialog.getWindow().setLayout(LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
|
@ -344,7 +341,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
dialog.findViewById(R.id.copy_button).setOnClickListener(view -> {
|
||||
dialog.dismiss();
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("text/plain", profile.getCode());
|
||||
ClipData clip = ClipData.newPlainText("text/plain", entry.getInfo().getOtp());
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(this, "Code copied to the clipboard", Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
|
@ -352,23 +349,23 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
dialog.findViewById(R.id.delete_button).setOnClickListener(view -> {
|
||||
dialog.dismiss();
|
||||
Dialogs.showDeleteEntryDialog(this, (d, which) -> {
|
||||
deleteProfile(profile);
|
||||
deleteEntry(entry);
|
||||
});
|
||||
});
|
||||
|
||||
dialog.findViewById(R.id.edit_button).setOnClickListener(view -> {
|
||||
dialog.dismiss();
|
||||
startEditProfileActivity(CODE_EDIT_KEYINFO, profile, false);
|
||||
startEditProfileActivity(CODE_EDIT_ENTRY, entry, false);
|
||||
});
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
private void deleteProfile(KeyProfile profile) {
|
||||
_db.removeKey(profile.getEntry());
|
||||
private void deleteEntry(DatabaseEntry entry) {
|
||||
_db.removeEntry(entry);
|
||||
saveDatabase();
|
||||
|
||||
_keyProfileView.removeKey(profile);
|
||||
_entryListView.removeEntry(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -387,7 +384,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
startActivityForResult(intent, CODE_PREFERENCES);
|
||||
return true;
|
||||
case R.id.action_lock:
|
||||
_keyProfileView.clearKeys();
|
||||
_entryListView.clearEntries();
|
||||
_db.lock();
|
||||
startAuthActivity();
|
||||
return true;
|
||||
|
@ -411,11 +408,11 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
}
|
||||
}
|
||||
|
||||
private void loadKeyProfiles() {
|
||||
private void loadEntries() {
|
||||
updateLockIcon();
|
||||
|
||||
for (DatabaseEntry entry : _db.getKeys()) {
|
||||
_keyProfileView.addKey(new KeyProfile(entry));
|
||||
for (DatabaseEntry entry : _db.getEntries()) {
|
||||
_entryListView.addEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -428,13 +425,13 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onEntryClick(KeyProfile profile) {
|
||||
createBottomSheet(profile).show();
|
||||
public void onEntryClick(DatabaseEntry entry) {
|
||||
createBottomSheet(entry).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2) {
|
||||
_db.swapKeys(entry1, entry2);
|
||||
_db.swapEntries(entry1, entry2);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -313,7 +313,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas
|
|||
private void importDatabase(DatabaseImporter importer) throws DatabaseImporterException {
|
||||
List<DatabaseEntry> entries = importer.convert();
|
||||
for (DatabaseEntry entry : entries) {
|
||||
_db.addKey(entry);
|
||||
_db.addEntry(entry);
|
||||
}
|
||||
|
||||
if (!saveDatabase()) {
|
||||
|
|
|
@ -3,6 +3,7 @@ package me.impy.aegis.ui;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.hardware.Camera;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
@ -16,11 +17,10 @@ import java.util.Collections;
|
|||
import me.dm7.barcodescanner.core.IViewFinder;
|
||||
import me.dm7.barcodescanner.zxing.ZXingScannerView;
|
||||
import me.impy.aegis.R;
|
||||
import me.impy.aegis.crypto.KeyInfo;
|
||||
import me.impy.aegis.crypto.KeyInfoException;
|
||||
import me.impy.aegis.db.DatabaseEntry;
|
||||
import me.impy.aegis.helpers.SquareFinderView;
|
||||
import me.impy.aegis.ui.views.KeyProfile;
|
||||
import me.impy.aegis.otp.GoogleAuthInfo;
|
||||
import me.impy.aegis.otp.GoogleAuthInfoException;
|
||||
|
||||
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK;
|
||||
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT;
|
||||
|
@ -107,16 +107,19 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
|
|||
@Override
|
||||
public void handleResult(Result rawResult) {
|
||||
try {
|
||||
KeyInfo info = KeyInfo.fromURL(rawResult.getText());
|
||||
KeyProfile profile = new KeyProfile(new DatabaseEntry(info));
|
||||
profile.getEntry().setName(info.getAccountName());
|
||||
// parse google auth uri
|
||||
Uri uri = Uri.parse(rawResult.getText());
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(uri);
|
||||
|
||||
Intent resultIntent = new Intent();
|
||||
resultIntent.putExtra("KeyProfile", profile);
|
||||
DatabaseEntry entry = new DatabaseEntry(info.getOtpInfo());
|
||||
entry.setIssuer(info.getIssuer());
|
||||
entry.setName(info.getAccountName());
|
||||
|
||||
setResult(RESULT_OK, resultIntent);
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("entry", entry);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
} catch (KeyInfoException e) {
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
Toast.makeText(this, "An error occurred while trying to parse the QR code contents", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ import javax.crypto.SecretKey;
|
|||
|
||||
import me.impy.aegis.crypto.CryptoUtils;
|
||||
import me.impy.aegis.db.slots.PasswordSlot;
|
||||
import me.impy.aegis.db.slots.SlotException;
|
||||
|
||||
public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, SecretKey> {
|
||||
private Callback _cb;
|
||||
|
|
167
app/src/main/java/me/impy/aegis/ui/views/EntryAdapter.java
Normal file
167
app/src/main/java/me/impy/aegis/ui/views/EntryAdapter.java
Normal file
|
@ -0,0 +1,167 @@
|
|||
package me.impy.aegis.ui.views;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import me.impy.aegis.R;
|
||||
import me.impy.aegis.db.DatabaseEntry;
|
||||
import me.impy.aegis.helpers.ItemTouchHelperAdapter;
|
||||
import me.impy.aegis.otp.OtpInfo;
|
||||
import me.impy.aegis.otp.TotpInfo;
|
||||
|
||||
public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements ItemTouchHelperAdapter {
|
||||
private List<DatabaseEntry> _entries;
|
||||
private static Listener _listener;
|
||||
private boolean _showIssuer;
|
||||
|
||||
public EntryAdapter(Listener listener) {
|
||||
_entries = new ArrayList<>();
|
||||
_listener = listener;
|
||||
}
|
||||
|
||||
public void setShowIssuer(boolean showIssuer) {
|
||||
_showIssuer = showIssuer;
|
||||
}
|
||||
|
||||
public void addEntry(DatabaseEntry entry) {
|
||||
_entries.add(entry);
|
||||
|
||||
int position = getItemCount() - 1;
|
||||
if (position == 0) {
|
||||
notifyDataSetChanged();
|
||||
} else {
|
||||
notifyItemInserted(position);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeEntry(DatabaseEntry entry) {
|
||||
entry = getEntryByUUID(entry.getUUID());
|
||||
int position = _entries.indexOf(entry);
|
||||
_entries.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
}
|
||||
|
||||
public void clearEntries() {
|
||||
_entries.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void replaceEntry(DatabaseEntry newEntry) {
|
||||
DatabaseEntry oldEntry = getEntryByUUID(newEntry.getUUID());
|
||||
int position = _entries.indexOf(oldEntry);
|
||||
_entries.set(position, newEntry);
|
||||
notifyItemChanged(position);
|
||||
}
|
||||
|
||||
private DatabaseEntry getEntryByUUID(UUID uuid) {
|
||||
for (DatabaseEntry entry : _entries) {
|
||||
if (entry.getUUID().equals(uuid)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("no entry found with the same id");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemDismiss(int position) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemDrop(int position) {
|
||||
_listener.onEntryDrop(_entries.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemMove(int firstPosition, int secondPosition) {
|
||||
// notify the database first
|
||||
_listener.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition));
|
||||
|
||||
// update our side of things
|
||||
Collections.swap(_entries, firstPosition, secondPosition);
|
||||
notifyItemMoved(firstPosition, secondPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntryHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_entry, parent, false);
|
||||
return new EntryHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(EntryHolder holder) {
|
||||
holder.stopRefreshLoop();
|
||||
super.onViewRecycled(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(final EntryHolder holder, int position) {
|
||||
DatabaseEntry entry = _entries.get(position);
|
||||
boolean showProgress = !isPeriodUniform() && entry.getInfo() instanceof TotpInfo;
|
||||
holder.setData(entry, _showIssuer, showProgress);
|
||||
if (showProgress) {
|
||||
holder.startRefreshLoop();
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
int position = holder.getAdapterPosition();
|
||||
_listener.onEntryClick(_entries.get(position));
|
||||
}
|
||||
});
|
||||
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
int position = holder.getAdapterPosition();
|
||||
return _listener.onLongEntryClick(_entries.get(position));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public int getUniformPeriod() {
|
||||
List<TotpInfo> infos = new ArrayList<>();
|
||||
for (DatabaseEntry entry : _entries) {
|
||||
OtpInfo info = entry.getInfo();
|
||||
if (info instanceof TotpInfo) {
|
||||
infos.add((TotpInfo) info);
|
||||
}
|
||||
}
|
||||
|
||||
if (infos.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int period = infos.get(0).getPeriod();
|
||||
for (TotpInfo info : infos) {
|
||||
if (period != info.getPeriod()) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return period;
|
||||
}
|
||||
|
||||
public boolean isPeriodUniform() {
|
||||
return getUniformPeriod() != -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return _entries.size();
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onEntryClick(DatabaseEntry entry);
|
||||
boolean onLongEntryClick(DatabaseEntry entry);
|
||||
void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2);
|
||||
void onEntryDrop(DatabaseEntry entry);
|
||||
}
|
||||
}
|
|
@ -9,20 +9,23 @@ import android.widget.TextView;
|
|||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
|
||||
import me.impy.aegis.R;
|
||||
import me.impy.aegis.helpers.UIRefresher;
|
||||
import me.impy.aegis.db.DatabaseEntry;
|
||||
import me.impy.aegis.helpers.TextDrawableHelper;
|
||||
import me.impy.aegis.helpers.UiRefresher;
|
||||
import me.impy.aegis.otp.TotpInfo;
|
||||
|
||||
public class KeyProfileHolder extends RecyclerView.ViewHolder {
|
||||
public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
private TextView _profileName;
|
||||
private TextView _profileCode;
|
||||
private TextView _profileIssuer;
|
||||
private ImageView _profileDrawable;
|
||||
private KeyProfile _profile;
|
||||
private DatabaseEntry _entry;
|
||||
|
||||
private PeriodProgressBar _progressBar;
|
||||
|
||||
private UIRefresher _refresher;
|
||||
private UiRefresher _refresher;
|
||||
|
||||
public KeyProfileHolder(final View view) {
|
||||
public EntryHolder(final View view) {
|
||||
super(view);
|
||||
_profileName = view.findViewById(R.id.profile_name);
|
||||
_profileCode = view.findViewById(R.id.profile_code);
|
||||
|
@ -33,7 +36,7 @@ public class KeyProfileHolder extends RecyclerView.ViewHolder {
|
|||
int primaryColorId = view.getContext().getResources().getColor(R.color.colorPrimary);
|
||||
_progressBar.getProgressDrawable().setColorFilter(primaryColorId, PorterDuff.Mode.SRC_IN);
|
||||
|
||||
_refresher = new UIRefresher(new UIRefresher.Listener() {
|
||||
_refresher = new UiRefresher(new UiRefresher.Listener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
refreshCode();
|
||||
|
@ -42,26 +45,26 @@ public class KeyProfileHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
@Override
|
||||
public long getMillisTillNextRefresh() {
|
||||
return _profile.getEntry().getInfo().getMillisTillNextRotation();
|
||||
return ((TotpInfo)_entry.getInfo()).getMillisTillNextRotation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setData(KeyProfile profile, boolean showIssuer, boolean showProgress) {
|
||||
_profile = profile;
|
||||
public void setData(DatabaseEntry entry, boolean showIssuer, boolean showProgress) {
|
||||
_entry = entry;
|
||||
|
||||
_progressBar.setVisibility(showProgress ? View.VISIBLE : View.INVISIBLE);
|
||||
if (showProgress) {
|
||||
_progressBar.setPeriod(profile.getEntry().getInfo().getPeriod());
|
||||
_progressBar.setPeriod(((TotpInfo)entry.getInfo()).getPeriod());
|
||||
}
|
||||
|
||||
_profileName.setText(profile.getEntry().getName());
|
||||
_profileName.setText(entry.getName());
|
||||
_profileIssuer.setText("");
|
||||
if (showIssuer) {
|
||||
_profileIssuer.setText(" - " + profile.getEntry().getInfo().getIssuer());
|
||||
_profileIssuer.setText(" - " + entry.getIssuer());
|
||||
}
|
||||
|
||||
TextDrawable drawable = profile.getDrawable();
|
||||
TextDrawable drawable = TextDrawableHelper.generate(entry.getName());
|
||||
_profileDrawable.setImageDrawable(drawable);
|
||||
|
||||
refreshCode();
|
||||
|
@ -76,7 +79,7 @@ public class KeyProfileHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
|
||||
private void refreshCode() {
|
||||
String otp = _profile.refreshCode();
|
||||
String otp = _entry.getInfo().getOtp();
|
||||
_profileCode.setText(otp.substring(0, otp.length() / 2) + " " + otp.substring(otp.length() / 2));
|
||||
}
|
||||
}
|
|
@ -11,29 +11,29 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
|
||||
import me.impy.aegis.R;
|
||||
import me.impy.aegis.crypto.KeyInfo;
|
||||
import me.impy.aegis.db.DatabaseEntry;
|
||||
import me.impy.aegis.helpers.SimpleItemTouchHelperCallback;
|
||||
import me.impy.aegis.helpers.UIRefresher;
|
||||
import me.impy.aegis.helpers.UiRefresher;
|
||||
import me.impy.aegis.otp.TotpInfo;
|
||||
|
||||
public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listener {
|
||||
private KeyProfileAdapter _adapter;
|
||||
public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
private EntryAdapter _adapter;
|
||||
private Listener _listener;
|
||||
|
||||
private PeriodProgressBar _progressBar;
|
||||
private boolean _showProgress = false;
|
||||
|
||||
private UIRefresher _refresher;
|
||||
private UiRefresher _refresher;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
_adapter = new KeyProfileAdapter(this);
|
||||
_adapter = new EntryAdapter(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_keyprofile_view, container, false);
|
||||
View view = inflater.inflate(R.layout.fragment_entry_list_view, container, false);
|
||||
|
||||
_progressBar = view.findViewById(R.id.progressBar);
|
||||
int primaryColorId = getResources().getColor(R.color.colorPrimary);
|
||||
|
@ -48,7 +48,7 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen
|
|||
touchHelper.attachToRecyclerView(rvKeyProfiles);
|
||||
rvKeyProfiles.setAdapter(_adapter);
|
||||
|
||||
_refresher = new UIRefresher(new UIRefresher.Listener() {
|
||||
_refresher = new UiRefresher(new UiRefresher.Listener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
refresh();
|
||||
|
@ -56,7 +56,7 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen
|
|||
|
||||
@Override
|
||||
public long getMillisTillNextRefresh() {
|
||||
return KeyInfo.getMillisTillNextRotation(_adapter.getUniformPeriod());
|
||||
return TotpInfo.getMillisTillNextRotation(_adapter.getUniformPeriod());
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -101,23 +101,23 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onKeyProfileClick(KeyProfile profile) {
|
||||
_listener.onEntryClick(profile);
|
||||
public void onEntryClick(DatabaseEntry entry) {
|
||||
_listener.onEntryClick(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongKeyProfileClick(KeyProfile profile) {
|
||||
public boolean onLongEntryClick(DatabaseEntry entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyProfileMove(KeyProfile profile1, KeyProfile profile2) {
|
||||
_listener.onEntryMove(profile1.getEntry(), profile2.getEntry());
|
||||
public void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2) {
|
||||
_listener.onEntryMove(entry1, entry2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyProfileDrop(KeyProfile profile) {
|
||||
_listener.onEntryDrop(profile.getEntry());
|
||||
public void onEntryDrop(DatabaseEntry entry) {
|
||||
_listener.onEntryDrop(entry);
|
||||
}
|
||||
|
||||
public void setShowIssuer(boolean showIssuer) {
|
||||
|
@ -125,28 +125,28 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen
|
|||
_adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addKey(KeyProfile profile) {
|
||||
_adapter.addKey(profile);
|
||||
public void addEntry(DatabaseEntry entry) {
|
||||
_adapter.addEntry(entry);
|
||||
checkPeriodUniformity();
|
||||
}
|
||||
|
||||
public void removeKey(KeyProfile profile) {
|
||||
_adapter.removeKey(profile);
|
||||
public void removeEntry(DatabaseEntry entry) {
|
||||
_adapter.removeEntry(entry);
|
||||
checkPeriodUniformity();
|
||||
}
|
||||
|
||||
public void clearKeys() {
|
||||
_adapter.clearKeys();
|
||||
public void clearEntries() {
|
||||
_adapter.clearEntries();
|
||||
checkPeriodUniformity();
|
||||
}
|
||||
|
||||
public void replaceKey(KeyProfile profile) {
|
||||
_adapter.replaceKey(profile);
|
||||
public void replaceEntry(DatabaseEntry entry) {
|
||||
_adapter.replaceEntry(entry);
|
||||
checkPeriodUniformity();
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onEntryClick(KeyProfile profile);
|
||||
void onEntryClick(DatabaseEntry entry);
|
||||
void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2);
|
||||
void onEntryDrop(DatabaseEntry entry);
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package me.impy.aegis.ui.views;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.UndeclaredThrowableException;
|
||||
|
||||
import me.impy.aegis.crypto.otp.OTP;
|
||||
import me.impy.aegis.crypto.otp.OTPException;
|
||||
import me.impy.aegis.db.DatabaseEntry;
|
||||
import me.impy.aegis.helpers.TextDrawableHelper;
|
||||
|
||||
public class KeyProfile implements Serializable {
|
||||
private String _code;
|
||||
private DatabaseEntry _entry;
|
||||
|
||||
public KeyProfile() {
|
||||
this(new DatabaseEntry());
|
||||
}
|
||||
|
||||
public KeyProfile(DatabaseEntry entry) {
|
||||
_entry = entry;
|
||||
}
|
||||
|
||||
public DatabaseEntry getEntry() {
|
||||
return _entry;
|
||||
}
|
||||
public String getCode() {
|
||||
return _code;
|
||||
}
|
||||
|
||||
public String refreshCode() {
|
||||
try {
|
||||
_code = OTP.generateOTP(_entry.getInfo());
|
||||
} catch (OTPException e) {
|
||||
throw new UndeclaredThrowableException(e);
|
||||
}
|
||||
return _code;
|
||||
}
|
||||
|
||||
public TextDrawable getDrawable() {
|
||||
return TextDrawableHelper.generate(getEntry().getName());
|
||||
}
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
package me.impy.aegis.ui.views;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
|
||||
import me.impy.aegis.R;
|
||||
import me.impy.aegis.helpers.ItemTouchHelperAdapter;
|
||||
|
||||
public class KeyProfileAdapter extends RecyclerView.Adapter<KeyProfileHolder> implements ItemTouchHelperAdapter {
|
||||
private ArrayList<KeyProfile> _keyProfiles;
|
||||
private static Listener _listener;
|
||||
private boolean _showIssuer;
|
||||
|
||||
public KeyProfileAdapter(Listener listener) {
|
||||
_keyProfiles = new ArrayList<>();
|
||||
_listener = listener;
|
||||
}
|
||||
|
||||
public void setShowIssuer(boolean showIssuer) {
|
||||
_showIssuer = showIssuer;
|
||||
}
|
||||
|
||||
public void addKey(KeyProfile profile) {
|
||||
_keyProfiles.add(profile);
|
||||
|
||||
int position = getItemCount() - 1;
|
||||
if (position == 0) {
|
||||
notifyDataSetChanged();
|
||||
} else {
|
||||
notifyItemInserted(position);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeKey(KeyProfile profile) {
|
||||
profile = getKeyByUUID(profile.getEntry().getUUID());
|
||||
int position = _keyProfiles.indexOf(profile);
|
||||
_keyProfiles.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
}
|
||||
|
||||
public void clearKeys() {
|
||||
_keyProfiles.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void replaceKey(KeyProfile newProfile) {
|
||||
KeyProfile oldProfile = getKeyByUUID(newProfile.getEntry().getUUID());
|
||||
int position = _keyProfiles.indexOf(oldProfile);
|
||||
_keyProfiles.set(position, newProfile);
|
||||
notifyItemChanged(position);
|
||||
}
|
||||
|
||||
private KeyProfile getKeyByUUID(UUID uuid) {
|
||||
for (KeyProfile profile : _keyProfiles) {
|
||||
if (profile.getEntry().getUUID().equals(uuid)) {
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("no key profile found with the same id");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemDismiss(int position) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemDrop(int position) {
|
||||
_listener.onKeyProfileDrop(_keyProfiles.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemMove(int firstPosition, int secondPosition) {
|
||||
// notify the database first
|
||||
_listener.onKeyProfileMove(_keyProfiles.get(firstPosition), _keyProfiles.get(secondPosition));
|
||||
|
||||
// update our side of things
|
||||
Collections.swap(_keyProfiles, firstPosition, secondPosition);
|
||||
notifyItemMoved(firstPosition, secondPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyProfileHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_keyprofile, parent, false);
|
||||
return new KeyProfileHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(KeyProfileHolder holder) {
|
||||
holder.stopRefreshLoop();
|
||||
super.onViewRecycled(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(final KeyProfileHolder holder, int position) {
|
||||
boolean uniform = isPeriodUniform();
|
||||
final KeyProfile profile = _keyProfiles.get(position);
|
||||
holder.setData(profile, _showIssuer, !uniform);
|
||||
if (!uniform) {
|
||||
holder.startRefreshLoop();
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
int position = holder.getAdapterPosition();
|
||||
_listener.onKeyProfileClick(_keyProfiles.get(position));
|
||||
}
|
||||
});
|
||||
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
int position = holder.getAdapterPosition();
|
||||
return _listener.onLongKeyProfileClick(_keyProfiles.get(position));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public int getUniformPeriod() {
|
||||
if (_keyProfiles.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int period = _keyProfiles.get(0).getEntry().getInfo().getPeriod();
|
||||
for (KeyProfile profile : _keyProfiles) {
|
||||
if (period != profile.getEntry().getInfo().getPeriod()) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return period;
|
||||
}
|
||||
|
||||
public boolean isPeriodUniform() {
|
||||
return getUniformPeriod() != -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return _keyProfiles.size();
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onKeyProfileClick(KeyProfile profile);
|
||||
boolean onLongKeyProfileClick(KeyProfile profile);
|
||||
void onKeyProfileMove(KeyProfile profile1, KeyProfile profile2);
|
||||
void onKeyProfileDrop(KeyProfile profile);
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ import android.util.AttributeSet;
|
|||
import android.view.animation.LinearInterpolator;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import me.impy.aegis.crypto.KeyInfo;
|
||||
import me.impy.aegis.otp.TotpInfo;
|
||||
|
||||
public class PeriodProgressBar extends ProgressBar {
|
||||
private int _period;
|
||||
|
@ -40,7 +40,7 @@ public class PeriodProgressBar extends ProgressBar {
|
|||
setProgress(maxProgress);
|
||||
|
||||
// calculate the progress the bar should start at
|
||||
long millisTillRotation = KeyInfo.getMillisTillNextRotation(_period);
|
||||
long millisTillRotation = TotpInfo.getMillisTillNextRotation(_period);
|
||||
long period = _period * maxProgress;
|
||||
int currentProgress = maxProgress - (int) ((((double) period - millisTillRotation) / period) * maxProgress);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue