2019-02-07 22:39:33 +01:00
|
|
|
package com.beemdevelopment.aegis.ui;
|
2017-12-27 22:04:22 +01:00
|
|
|
|
|
|
|
import android.content.Intent;
|
2018-12-11 11:44:36 +01:00
|
|
|
import android.content.res.Resources;
|
2018-06-06 21:26:09 +02:00
|
|
|
import android.graphics.Bitmap;
|
2018-06-06 22:22:38 +02:00
|
|
|
import android.graphics.drawable.BitmapDrawable;
|
|
|
|
import android.graphics.drawable.Drawable;
|
2020-05-24 13:43:59 +02:00
|
|
|
import android.net.Uri;
|
2017-12-27 22:04:22 +01:00
|
|
|
import android.os.Bundle;
|
|
|
|
import android.text.TextWatcher;
|
|
|
|
import android.view.Menu;
|
|
|
|
import android.view.MenuItem;
|
|
|
|
import android.view.View;
|
2021-01-27 20:06:34 +01:00
|
|
|
import android.view.ViewGroup;
|
2018-04-05 00:07:48 +02:00
|
|
|
import android.view.animation.AccelerateInterpolator;
|
|
|
|
import android.view.animation.AlphaAnimation;
|
|
|
|
import android.view.animation.Animation;
|
2017-12-27 22:04:22 +01:00
|
|
|
import android.widget.AdapterView;
|
2021-01-17 13:12:43 +01:00
|
|
|
import android.widget.AutoCompleteTextView;
|
2017-12-27 22:04:22 +01:00
|
|
|
import android.widget.ImageView;
|
2021-01-27 20:06:34 +01:00
|
|
|
import android.widget.LinearLayout;
|
2018-04-05 00:07:48 +02:00
|
|
|
import android.widget.RelativeLayout;
|
2017-12-27 22:04:22 +01:00
|
|
|
|
2022-10-10 22:32:30 +02:00
|
|
|
import androidx.activity.OnBackPressedCallback;
|
2019-12-26 20:47:28 +01:00
|
|
|
import androidx.annotation.NonNull;
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
|
import androidx.appcompat.app.ActionBar;
|
|
|
|
import androidx.appcompat.app.AlertDialog;
|
|
|
|
|
2018-01-02 21:50:07 +01:00
|
|
|
import com.amulyakhare.textdrawable.TextDrawable;
|
2018-06-06 21:26:09 +02:00
|
|
|
import com.avito.android.krop.KropView;
|
2019-04-04 14:07:36 +02:00
|
|
|
import com.beemdevelopment.aegis.R;
|
2019-02-07 22:39:33 +01:00
|
|
|
import com.beemdevelopment.aegis.encoding.Base32;
|
2019-12-26 20:47:28 +01:00
|
|
|
import com.beemdevelopment.aegis.encoding.EncodingException;
|
2022-07-26 23:56:04 +01:00
|
|
|
import com.beemdevelopment.aegis.encoding.Hex;
|
2021-01-17 13:12:43 +01:00
|
|
|
import com.beemdevelopment.aegis.helpers.DropdownHelper;
|
2021-01-24 14:20:29 +01:00
|
|
|
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
2020-07-08 11:18:40 +02:00
|
|
|
import com.beemdevelopment.aegis.helpers.IconViewHelper;
|
2022-10-10 22:32:30 +02:00
|
|
|
import com.beemdevelopment.aegis.helpers.SafHelper;
|
2022-10-12 17:36:46 +02:00
|
|
|
import com.beemdevelopment.aegis.helpers.SimpleAnimationEndListener;
|
2022-10-10 22:32:30 +02:00
|
|
|
import com.beemdevelopment.aegis.helpers.SimpleTextWatcher;
|
2019-02-07 22:39:33 +01:00
|
|
|
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
|
2020-07-08 11:18:40 +02:00
|
|
|
import com.beemdevelopment.aegis.icons.IconPack;
|
|
|
|
import com.beemdevelopment.aegis.icons.IconType;
|
2020-10-24 14:21:49 +02:00
|
|
|
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
2019-02-07 22:39:33 +01:00
|
|
|
import com.beemdevelopment.aegis.otp.HotpInfo;
|
2022-07-26 23:56:04 +01:00
|
|
|
import com.beemdevelopment.aegis.otp.MotpInfo;
|
2019-02-07 22:39:33 +01:00
|
|
|
import com.beemdevelopment.aegis.otp.OtpInfo;
|
|
|
|
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
2019-03-31 22:15:03 +02:00
|
|
|
import com.beemdevelopment.aegis.otp.SteamInfo;
|
2019-02-07 22:39:33 +01:00
|
|
|
import com.beemdevelopment.aegis.otp.TotpInfo;
|
2022-01-07 12:49:53 +03:00
|
|
|
import com.beemdevelopment.aegis.otp.YandexInfo;
|
2021-01-27 13:54:11 +01:00
|
|
|
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
2020-07-08 11:18:40 +02:00
|
|
|
import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog;
|
|
|
|
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
2021-04-14 22:52:00 +02:00
|
|
|
import com.beemdevelopment.aegis.ui.tasks.ImportFileTask;
|
2020-07-08 11:18:40 +02:00
|
|
|
import com.beemdevelopment.aegis.ui.views.IconAdapter;
|
2019-12-26 20:47:28 +01:00
|
|
|
import com.beemdevelopment.aegis.util.Cloner;
|
2020-07-08 11:18:40 +02:00
|
|
|
import com.beemdevelopment.aegis.util.IOUtils;
|
2020-05-02 18:51:33 +02:00
|
|
|
import com.beemdevelopment.aegis.vault.VaultEntry;
|
2022-02-06 19:00:01 +01:00
|
|
|
import com.beemdevelopment.aegis.vault.VaultRepository;
|
2019-06-23 10:18:10 +02:00
|
|
|
import com.bumptech.glide.Glide;
|
|
|
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
|
|
|
import com.bumptech.glide.request.target.CustomTarget;
|
|
|
|
import com.bumptech.glide.request.transition.Transition;
|
2020-07-08 11:18:40 +02:00
|
|
|
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
2021-01-17 13:12:43 +01:00
|
|
|
import com.google.android.material.textfield.TextInputEditText;
|
|
|
|
import com.google.android.material.textfield.TextInputLayout;
|
2018-01-02 21:50:07 +01:00
|
|
|
|
2018-06-06 22:22:38 +02:00
|
|
|
import java.io.ByteArrayOutputStream;
|
2021-04-14 22:52:00 +02:00
|
|
|
import java.io.File;
|
2020-07-08 11:18:40 +02:00
|
|
|
import java.io.FileInputStream;
|
|
|
|
import java.io.IOException;
|
2018-12-11 11:44:36 +01:00
|
|
|
import java.util.ArrayList;
|
2021-01-24 14:20:29 +01:00
|
|
|
import java.util.Collections;
|
2020-07-08 11:18:40 +02:00
|
|
|
import java.util.Comparator;
|
2018-12-11 11:44:36 +01:00
|
|
|
import java.util.List;
|
2021-06-23 18:06:01 +02:00
|
|
|
import java.util.Locale;
|
2018-12-11 11:44:36 +01:00
|
|
|
import java.util.TreeSet;
|
2020-05-02 18:51:33 +02:00
|
|
|
import java.util.UUID;
|
2018-09-25 19:36:56 +02:00
|
|
|
import java.util.concurrent.atomic.AtomicReference;
|
2020-07-08 11:18:40 +02:00
|
|
|
import java.util.stream.Collectors;
|
2018-06-06 22:22:38 +02:00
|
|
|
|
2018-06-06 21:57:38 +02:00
|
|
|
import de.hdodenhof.circleimageview.CircleImageView;
|
2017-12-27 22:04:22 +01:00
|
|
|
|
2018-06-06 16:15:31 +02:00
|
|
|
public class EditEntryActivity extends AegisActivity {
|
2018-11-27 20:55:55 +01:00
|
|
|
private static final int PICK_IMAGE_REQUEST = 0;
|
|
|
|
|
2017-12-30 14:21:21 +01:00
|
|
|
private boolean _isNew = false;
|
2021-01-27 20:06:34 +01:00
|
|
|
private boolean _isManual = false;
|
2019-12-25 19:21:34 +01:00
|
|
|
private VaultEntry _origEntry;
|
2018-12-11 11:44:36 +01:00
|
|
|
private TreeSet<String> _groups;
|
2018-09-25 19:36:56 +02:00
|
|
|
private boolean _hasCustomIcon = false;
|
|
|
|
// keep track of icon changes separately as the generated jpeg's are not deterministic
|
|
|
|
private boolean _hasChangedIcon = false;
|
2020-07-08 11:18:40 +02:00
|
|
|
private IconPack.Icon _selectedIcon;
|
2018-06-06 21:57:38 +02:00
|
|
|
private CircleImageView _iconView;
|
|
|
|
private ImageView _saveImageButton;
|
2018-01-02 21:50:07 +01:00
|
|
|
|
2021-01-17 13:12:43 +01:00
|
|
|
private TextInputEditText _textName;
|
|
|
|
private TextInputEditText _textIssuer;
|
|
|
|
private TextInputEditText _textPeriodCounter;
|
|
|
|
private TextInputLayout _textPeriodCounterLayout;
|
|
|
|
private TextInputEditText _textDigits;
|
2021-01-30 16:41:25 +01:00
|
|
|
private TextInputLayout _textDigitsLayout;
|
2021-01-17 13:12:43 +01:00
|
|
|
private TextInputEditText _textSecret;
|
2022-07-26 23:56:04 +01:00
|
|
|
private TextInputEditText _textPin;
|
|
|
|
private LinearLayout _textPinLayout;
|
2021-06-02 18:02:38 +02:00
|
|
|
private TextInputEditText _textUsageCount;
|
2021-08-30 10:21:54 +01:00
|
|
|
private TextInputEditText _textNote;
|
2017-12-27 22:04:22 +01:00
|
|
|
|
2021-01-17 13:12:43 +01:00
|
|
|
private AutoCompleteTextView _dropdownType;
|
|
|
|
private AutoCompleteTextView _dropdownAlgo;
|
2021-01-30 16:41:25 +01:00
|
|
|
private TextInputLayout _dropdownAlgoLayout;
|
2021-01-17 13:12:43 +01:00
|
|
|
private AutoCompleteTextView _dropdownGroup;
|
|
|
|
private List<String> _dropdownGroupList = new ArrayList<>();
|
2017-12-27 22:04:22 +01:00
|
|
|
|
2018-06-06 21:26:09 +02:00
|
|
|
private KropView _kropView;
|
|
|
|
|
2018-04-10 13:35:35 +02:00
|
|
|
private RelativeLayout _advancedSettingsHeader;
|
|
|
|
private RelativeLayout _advancedSettings;
|
|
|
|
|
2022-10-10 22:32:30 +02:00
|
|
|
private BackPressHandler _backPressHandler;
|
|
|
|
private IconBackPressHandler _iconBackPressHandler;
|
|
|
|
|
2017-12-27 22:04:22 +01:00
|
|
|
@Override
|
|
|
|
protected void onCreate(Bundle savedInstanceState) {
|
|
|
|
super.onCreate(savedInstanceState);
|
2022-02-06 19:00:01 +01:00
|
|
|
if (abortIfOrphan(savedInstanceState)) {
|
|
|
|
return;
|
|
|
|
}
|
2018-06-06 16:15:31 +02:00
|
|
|
setContentView(R.layout.activity_edit_entry);
|
2021-01-17 13:12:43 +01:00
|
|
|
setSupportActionBar(findViewById(R.id.toolbar));
|
2017-12-27 22:04:22 +01:00
|
|
|
|
2022-02-06 19:00:01 +01:00
|
|
|
_groups = _vaultManager.getVault().getGroups();
|
2020-05-02 18:51:33 +02:00
|
|
|
|
2017-12-27 22:04:22 +01:00
|
|
|
ActionBar bar = getSupportActionBar();
|
2022-10-10 22:32:30 +02:00
|
|
|
if (bar != null) {
|
|
|
|
bar.setHomeAsUpIndicator(R.drawable.ic_close);
|
|
|
|
bar.setDisplayHomeAsUpEnabled(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
_backPressHandler = new BackPressHandler();
|
|
|
|
getOnBackPressedDispatcher().addCallback(this, _backPressHandler);
|
|
|
|
_iconBackPressHandler = new IconBackPressHandler();
|
|
|
|
getOnBackPressedDispatcher().addCallback(this, _iconBackPressHandler);
|
2017-12-27 22:04:22 +01:00
|
|
|
|
2018-06-06 16:15:31 +02:00
|
|
|
// retrieve info from the calling activity
|
2018-01-02 21:50:07 +01:00
|
|
|
Intent intent = getIntent();
|
2020-05-02 18:51:33 +02:00
|
|
|
UUID entryUUID = (UUID) intent.getSerializableExtra("entryUUID");
|
|
|
|
if (entryUUID != null) {
|
2022-02-06 19:00:01 +01:00
|
|
|
_origEntry = _vaultManager.getVault().getEntryByUUID(entryUUID);
|
2020-05-02 18:51:33 +02:00
|
|
|
} else {
|
|
|
|
_origEntry = (VaultEntry) intent.getSerializableExtra("newEntry");
|
2021-01-27 20:06:34 +01:00
|
|
|
_isManual = intent.getBooleanExtra("isManual", false);
|
2020-05-02 18:51:33 +02:00
|
|
|
_isNew = true;
|
2020-04-11 14:37:28 +02:00
|
|
|
setTitle(R.string.add_new_entry);
|
2017-12-30 00:26:16 +01:00
|
|
|
}
|
|
|
|
|
2018-06-06 16:15:31 +02:00
|
|
|
// set up fields
|
2018-01-02 21:50:07 +01:00
|
|
|
_iconView = findViewById(R.id.profile_drawable);
|
2018-06-06 21:26:09 +02:00
|
|
|
_kropView = findViewById(R.id.krop_view);
|
2018-06-06 21:57:38 +02:00
|
|
|
_saveImageButton = findViewById(R.id.iv_saveImage);
|
2017-12-30 00:26:16 +01:00
|
|
|
_textName = findViewById(R.id.text_name);
|
|
|
|
_textIssuer = findViewById(R.id.text_issuer);
|
2021-01-17 13:12:43 +01:00
|
|
|
_textPeriodCounter = findViewById(R.id.text_period_counter);
|
|
|
|
_textPeriodCounterLayout = findViewById(R.id.text_period_counter_layout);
|
2019-03-25 19:34:57 +01:00
|
|
|
_textDigits = findViewById(R.id.text_digits);
|
2021-01-30 16:41:25 +01:00
|
|
|
_textDigitsLayout = findViewById(R.id.text_digits_layout);
|
2017-12-30 00:26:16 +01:00
|
|
|
_textSecret = findViewById(R.id.text_secret);
|
2022-07-26 23:56:04 +01:00
|
|
|
_textPin = findViewById(R.id.text_pin);
|
|
|
|
_textPinLayout = findViewById(R.id.layout_pin);
|
2021-06-02 18:02:38 +02:00
|
|
|
_textUsageCount = findViewById(R.id.text_usage_count);
|
2021-08-30 10:21:54 +01:00
|
|
|
_textNote = findViewById(R.id.text_note);
|
2021-01-17 13:12:43 +01:00
|
|
|
_dropdownType = findViewById(R.id.dropdown_type);
|
|
|
|
DropdownHelper.fillDropdown(this, _dropdownType, R.array.otp_types_array);
|
2021-01-30 16:41:25 +01:00
|
|
|
_dropdownAlgoLayout = findViewById(R.id.dropdown_algo_layout);
|
2021-01-17 13:12:43 +01:00
|
|
|
_dropdownAlgo = findViewById(R.id.dropdown_algo);
|
|
|
|
DropdownHelper.fillDropdown(this, _dropdownAlgo, R.array.otp_algo_array);
|
|
|
|
_dropdownGroup = findViewById(R.id.dropdown_group);
|
|
|
|
updateGroupDropdownList();
|
|
|
|
DropdownHelper.fillDropdown(this, _dropdownGroup, _dropdownGroupList);
|
2018-06-09 20:23:39 +02:00
|
|
|
|
2021-01-27 20:06:34 +01:00
|
|
|
// if this is NOT a manually entered entry, move the "Secret" field from basic to advanced settings
|
2022-02-06 19:00:01 +01:00
|
|
|
if (!_isNew || !_isManual) {
|
2022-01-07 12:49:53 +03:00
|
|
|
int secretIndex = 0;
|
2021-01-27 20:06:34 +01:00
|
|
|
LinearLayout layoutSecret = findViewById(R.id.layout_secret);
|
|
|
|
LinearLayout layoutBasic = findViewById(R.id.layout_basic);
|
|
|
|
LinearLayout layoutAdvanced = findViewById(R.id.layout_advanced);
|
|
|
|
layoutBasic.removeView(layoutSecret);
|
2022-01-07 12:49:53 +03:00
|
|
|
if (!_isNew) {
|
|
|
|
secretIndex = 1;
|
2022-07-26 23:56:04 +01:00
|
|
|
layoutBasic.removeView(_textPinLayout);
|
|
|
|
layoutAdvanced.addView(_textPinLayout, 0);
|
|
|
|
((LinearLayout.LayoutParams) _textPinLayout.getLayoutParams()).topMargin = 0;
|
2022-01-07 12:49:53 +03:00
|
|
|
} else {
|
|
|
|
((LinearLayout.LayoutParams) layoutSecret.getLayoutParams()).topMargin = 0;
|
|
|
|
}
|
|
|
|
layoutAdvanced.addView(layoutSecret, secretIndex);
|
2021-01-27 20:06:34 +01:00
|
|
|
|
|
|
|
if (_isNew && !_isManual) {
|
|
|
|
setViewEnabled(layoutAdvanced, false);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
LinearLayout layoutTypeAlgo = findViewById(R.id.layout_type_algo);
|
|
|
|
((LinearLayout.LayoutParams) layoutTypeAlgo.getLayoutParams()).topMargin = 0;
|
|
|
|
}
|
|
|
|
|
2018-04-10 13:35:35 +02:00
|
|
|
_advancedSettingsHeader = findViewById(R.id.accordian_header);
|
2021-01-27 20:06:34 +01:00
|
|
|
_advancedSettingsHeader.setOnClickListener(v -> openAdvancedSettings());
|
2018-04-10 13:35:35 +02:00
|
|
|
_advancedSettings = findViewById(R.id.expandableLayout);
|
|
|
|
|
2018-06-06 16:15:31 +02:00
|
|
|
// fill the fields with values if possible
|
2019-09-04 21:42:01 +02:00
|
|
|
if (_origEntry.hasIcon()) {
|
2020-07-08 11:18:40 +02:00
|
|
|
IconViewHelper.setLayerType(_iconView, _origEntry.getIconType());
|
2019-09-04 21:42:01 +02:00
|
|
|
Glide.with(this)
|
|
|
|
.asDrawable()
|
|
|
|
.load(_origEntry)
|
2020-07-08 11:18:40 +02:00
|
|
|
.set(IconLoader.ICON_TYPE, _origEntry.getIconType())
|
2019-09-04 21:42:01 +02:00
|
|
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
|
|
.skipMemoryCache(false)
|
|
|
|
.into(_iconView);
|
|
|
|
_hasCustomIcon = true;
|
|
|
|
} else {
|
|
|
|
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
|
|
|
|
_iconView.setImageDrawable(drawable);
|
|
|
|
}
|
2018-06-06 16:15:31 +02:00
|
|
|
|
2019-09-04 21:42:01 +02:00
|
|
|
_textName.setText(_origEntry.getName());
|
|
|
|
_textIssuer.setText(_origEntry.getIssuer());
|
2021-08-30 10:21:54 +01:00
|
|
|
_textNote.setText(_origEntry.getNote());
|
2018-06-06 16:15:31 +02:00
|
|
|
|
2019-09-04 21:42:01 +02:00
|
|
|
OtpInfo info = _origEntry.getInfo();
|
2022-01-07 12:49:53 +03:00
|
|
|
|
2019-09-04 21:42:01 +02:00
|
|
|
if (info instanceof TotpInfo) {
|
2021-01-17 13:12:43 +01:00
|
|
|
_textPeriodCounterLayout.setHint(R.string.period_hint);
|
|
|
|
_textPeriodCounter.setText(Integer.toString(((TotpInfo) info).getPeriod()));
|
2019-09-04 21:42:01 +02:00
|
|
|
} else if (info instanceof HotpInfo) {
|
2021-01-17 13:12:43 +01:00
|
|
|
_textPeriodCounterLayout.setHint(R.string.counter);
|
|
|
|
_textPeriodCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
|
2019-09-04 21:42:01 +02:00
|
|
|
} else {
|
2020-05-02 18:51:12 +02:00
|
|
|
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", info.getClass()));
|
2019-09-04 21:42:01 +02:00
|
|
|
}
|
|
|
|
_textDigits.setText(Integer.toString(info.getDigits()));
|
|
|
|
|
|
|
|
byte[] secretBytes = _origEntry.getInfo().getSecret();
|
|
|
|
if (secretBytes != null) {
|
2022-07-26 23:56:04 +01:00
|
|
|
String secretString = (info instanceof MotpInfo) ? Hex.encode(secretBytes) : Base32.encode(secretBytes);
|
2019-12-26 20:47:28 +01:00
|
|
|
_textSecret.setText(secretString);
|
2019-09-04 21:42:01 +02:00
|
|
|
}
|
2017-12-30 00:26:16 +01:00
|
|
|
|
2021-01-30 16:41:25 +01:00
|
|
|
_dropdownType.setText(_origEntry.getInfo().getType(), false);
|
2021-01-17 13:12:43 +01:00
|
|
|
_dropdownAlgo.setText(_origEntry.getInfo().getAlgorithm(false), false);
|
2022-01-07 12:49:53 +03:00
|
|
|
|
|
|
|
if (info instanceof YandexInfo) {
|
2022-07-26 23:56:04 +01:00
|
|
|
_textPin.setText(((YandexInfo) info).getPin());
|
|
|
|
} else if (info instanceof MotpInfo) {
|
|
|
|
_textPin.setText(((MotpInfo) info).getPin());
|
2022-01-07 12:49:53 +03:00
|
|
|
}
|
|
|
|
|
2021-01-30 16:41:25 +01:00
|
|
|
updateAdvancedFieldStatus(_origEntry.getInfo().getTypeId());
|
2022-01-07 12:49:53 +03:00
|
|
|
updatePinFieldVisibility(_origEntry.getInfo().getTypeId());
|
2018-06-06 16:15:31 +02:00
|
|
|
|
2019-09-04 21:42:01 +02:00
|
|
|
String group = _origEntry.getGroup();
|
|
|
|
setGroup(group);
|
2018-06-06 16:15:31 +02:00
|
|
|
|
2022-10-10 22:32:30 +02:00
|
|
|
// Update the icon if the issuer or name has changed
|
|
|
|
_textIssuer.addTextChangedListener(_nameChangeListener);
|
|
|
|
_textName.addTextChangedListener(_nameChangeListener);
|
|
|
|
|
|
|
|
// Register listeners to trigger validation
|
|
|
|
_textIssuer.addTextChangedListener(_validationListener);
|
|
|
|
_textName.addTextChangedListener(_validationListener);
|
|
|
|
_textNote.addTextChangedListener(_validationListener);
|
|
|
|
_textSecret.addTextChangedListener(_validationListener);
|
|
|
|
_dropdownType.addTextChangedListener(_validationListener);
|
|
|
|
_dropdownGroup.addTextChangedListener(_validationListener);
|
|
|
|
_dropdownAlgo.addTextChangedListener(_validationListener);
|
|
|
|
_textPeriodCounter.addTextChangedListener(_validationListener);
|
|
|
|
_textDigits.addTextChangedListener(_validationListener);
|
|
|
|
_textPin.addTextChangedListener(_validationListener);
|
2018-04-05 00:07:48 +02:00
|
|
|
|
2018-06-06 16:15:31 +02:00
|
|
|
// show/hide period and counter fields on type change
|
2021-01-17 13:12:43 +01:00
|
|
|
_dropdownType.setOnItemClickListener((parent, view, position, id) -> {
|
2021-06-23 18:06:01 +02:00
|
|
|
String type = _dropdownType.getText().toString().toLowerCase(Locale.ROOT);
|
2021-01-17 13:12:43 +01:00
|
|
|
switch (type) {
|
|
|
|
case SteamInfo.ID:
|
2021-01-30 16:41:25 +01:00
|
|
|
_dropdownAlgo.setText(OtpInfo.DEFAULT_ALGORITHM, false);
|
|
|
|
_textPeriodCounterLayout.setHint(R.string.period_hint);
|
|
|
|
_textPeriodCounter.setText(String.valueOf(TotpInfo.DEFAULT_PERIOD));
|
|
|
|
_textDigits.setText(String.valueOf(SteamInfo.DIGITS));
|
|
|
|
break;
|
|
|
|
case TotpInfo.ID:
|
2022-12-04 18:44:26 +01:00
|
|
|
_dropdownAlgo.setText(OtpInfo.DEFAULT_ALGORITHM, false);
|
2021-01-17 13:12:43 +01:00
|
|
|
_textPeriodCounterLayout.setHint(R.string.period_hint);
|
2021-01-30 16:41:25 +01:00
|
|
|
_textPeriodCounter.setText(String.valueOf(TotpInfo.DEFAULT_PERIOD));
|
|
|
|
_textDigits.setText(String.valueOf(OtpInfo.DEFAULT_DIGITS));
|
2021-01-17 13:12:43 +01:00
|
|
|
break;
|
|
|
|
case HotpInfo.ID:
|
2022-12-04 18:44:26 +01:00
|
|
|
_dropdownAlgo.setText(OtpInfo.DEFAULT_ALGORITHM, false);
|
2021-01-17 13:12:43 +01:00
|
|
|
_textPeriodCounterLayout.setHint(R.string.counter);
|
2021-01-30 16:41:25 +01:00
|
|
|
_textPeriodCounter.setText(String.valueOf(HotpInfo.DEFAULT_COUNTER));
|
|
|
|
_textDigits.setText(String.valueOf(OtpInfo.DEFAULT_DIGITS));
|
2021-01-17 13:12:43 +01:00
|
|
|
break;
|
2022-01-07 12:49:53 +03:00
|
|
|
case YandexInfo.ID:
|
|
|
|
_dropdownAlgo.setText(YandexInfo.DEFAULT_ALGORITHM, false);
|
|
|
|
_textPeriodCounterLayout.setHint(R.string.period_hint);
|
|
|
|
_textPeriodCounter.setText(String.valueOf(TotpInfo.DEFAULT_PERIOD));
|
|
|
|
_textDigits.setText(String.valueOf(YandexInfo.DIGITS));
|
|
|
|
break;
|
2022-07-26 23:56:04 +01:00
|
|
|
case MotpInfo.ID:
|
|
|
|
_dropdownAlgo.setText(MotpInfo.ALGORITHM, false);
|
|
|
|
_textPeriodCounterLayout.setHint(R.string.period_hint);
|
|
|
|
_textPeriodCounter.setText(String.valueOf(MotpInfo.PERIOD));
|
|
|
|
_textDigits.setText(String.valueOf(MotpInfo.DIGITS));
|
|
|
|
break;
|
2021-01-17 13:12:43 +01:00
|
|
|
default:
|
|
|
|
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
|
2018-06-06 16:15:31 +02:00
|
|
|
}
|
2021-01-30 16:41:25 +01:00
|
|
|
|
|
|
|
updateAdvancedFieldStatus(type);
|
2022-01-07 12:49:53 +03:00
|
|
|
updatePinFieldVisibility(type);
|
2021-01-17 13:12:43 +01:00
|
|
|
});
|
2018-06-06 16:15:31 +02:00
|
|
|
|
2021-01-17 13:12:43 +01:00
|
|
|
_iconView.setOnClickListener(v -> {
|
2020-07-08 11:18:40 +02:00
|
|
|
startIconSelection();
|
2018-06-06 16:15:31 +02:00
|
|
|
});
|
|
|
|
|
2021-01-17 13:12:43 +01:00
|
|
|
_dropdownGroup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
|
|
|
private int prevPosition = _dropdownGroupList.indexOf(_dropdownGroup.getText().toString());
|
2018-12-11 11:44:36 +01:00
|
|
|
|
|
|
|
@Override
|
2021-01-17 13:12:43 +01:00
|
|
|
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
|
|
|
if (position == _dropdownGroupList.size() - 1) {
|
|
|
|
Dialogs.showTextInputDialog(EditEntryActivity.this, R.string.set_group, R.string.group_name_hint, text -> {
|
2021-01-27 18:24:52 +01:00
|
|
|
String groupName = new String(text);
|
|
|
|
if (!groupName.isEmpty()) {
|
|
|
|
_groups.add(groupName);
|
|
|
|
updateGroupDropdownList();
|
|
|
|
_dropdownGroup.setText(groupName, false);
|
2018-12-11 11:44:36 +01:00
|
|
|
}
|
|
|
|
});
|
2021-01-17 13:12:43 +01:00
|
|
|
_dropdownGroup.setText(_dropdownGroupList.get(prevPosition), false);
|
2018-12-11 11:44:36 +01:00
|
|
|
} else {
|
|
|
|
prevPosition = position;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2021-06-02 18:02:38 +02:00
|
|
|
|
2022-02-06 19:00:01 +01:00
|
|
|
_textUsageCount.setText(_prefs.getUsageCount(entryUUID).toString());
|
2017-12-30 00:26:16 +01:00
|
|
|
}
|
|
|
|
|
2021-01-30 16:41:25 +01:00
|
|
|
private void updateAdvancedFieldStatus(String otpType) {
|
2022-07-26 23:56:04 +01:00
|
|
|
boolean enabled = !otpType.equals(SteamInfo.ID) && !otpType.equals(YandexInfo.ID)
|
|
|
|
&& !otpType.equals(MotpInfo.ID) && (!_isNew || _isManual);
|
2021-02-17 12:59:59 +01:00
|
|
|
_textDigitsLayout.setEnabled(enabled);
|
|
|
|
_textPeriodCounterLayout.setEnabled(enabled);
|
|
|
|
_dropdownAlgoLayout.setEnabled(enabled);
|
2021-01-30 16:41:25 +01:00
|
|
|
}
|
|
|
|
|
2022-01-07 12:49:53 +03:00
|
|
|
private void updatePinFieldVisibility(String otpType) {
|
2022-07-26 23:56:04 +01:00
|
|
|
boolean visible = otpType.equals(YandexInfo.ID) || otpType.equals(MotpInfo.ID);
|
|
|
|
_textPinLayout.setVisibility(visible ? View.VISIBLE : View.GONE);
|
|
|
|
_textPin.setHint(otpType.equals(MotpInfo.ID) ? R.string.motp_pin : R.string.yandex_pin);
|
2022-01-07 12:49:53 +03:00
|
|
|
}
|
|
|
|
|
2019-08-31 13:46:18 +02:00
|
|
|
private void setGroup(String groupName) {
|
2021-01-17 13:12:43 +01:00
|
|
|
int pos = 0;
|
|
|
|
if (groupName != null) {
|
|
|
|
pos = _groups.contains(groupName) ? _groups.headSet(groupName).size() + 1 : 0;
|
2019-08-31 13:46:18 +02:00
|
|
|
}
|
|
|
|
|
2021-01-17 13:12:43 +01:00
|
|
|
_dropdownGroup.setText(_dropdownGroupList.get(pos), false);
|
2019-08-31 13:46:18 +02:00
|
|
|
}
|
2020-05-02 18:51:33 +02:00
|
|
|
|
2018-05-11 15:12:36 +02:00
|
|
|
private void openAdvancedSettings() {
|
2018-09-25 19:36:56 +02:00
|
|
|
Animation fadeOut = new AlphaAnimation(1, 0);
|
2018-04-10 13:35:35 +02:00
|
|
|
fadeOut.setInterpolator(new AccelerateInterpolator());
|
2018-09-25 19:36:56 +02:00
|
|
|
fadeOut.setDuration(220);
|
2018-04-10 13:35:35 +02:00
|
|
|
_advancedSettingsHeader.startAnimation(fadeOut);
|
|
|
|
|
2018-09-25 19:36:56 +02:00
|
|
|
Animation fadeIn = new AlphaAnimation(0, 1);
|
2018-04-10 13:35:35 +02:00
|
|
|
fadeIn.setInterpolator(new AccelerateInterpolator());
|
2018-09-25 19:36:56 +02:00
|
|
|
fadeIn.setDuration(250);
|
2018-04-10 13:35:35 +02:00
|
|
|
|
2022-10-12 17:36:46 +02:00
|
|
|
fadeOut.setAnimationListener(new SimpleAnimationEndListener((a) -> {
|
|
|
|
_advancedSettingsHeader.setVisibility(View.GONE);
|
|
|
|
_advancedSettings.startAnimation(fadeIn);
|
|
|
|
}));
|
2018-04-10 13:35:35 +02:00
|
|
|
|
2022-10-12 17:36:46 +02:00
|
|
|
fadeIn.setAnimationListener(new SimpleAnimationEndListener((a) -> {
|
|
|
|
_advancedSettings.setVisibility(View.VISIBLE);
|
|
|
|
}));
|
2018-04-10 13:35:35 +02:00
|
|
|
}
|
|
|
|
|
2021-01-17 13:12:43 +01:00
|
|
|
private void updateGroupDropdownList() {
|
2018-12-11 11:44:36 +01:00
|
|
|
Resources res = getResources();
|
2021-01-17 13:12:43 +01:00
|
|
|
_dropdownGroupList.clear();
|
|
|
|
_dropdownGroupList.add(res.getString(R.string.no_group));
|
|
|
|
_dropdownGroupList.addAll(_groups);
|
|
|
|
_dropdownGroupList.add(res.getString(R.string.new_group));
|
2018-12-11 11:44:36 +01:00
|
|
|
}
|
|
|
|
|
2022-10-10 22:32:30 +02:00
|
|
|
private boolean hasUnsavedChanges(VaultEntry newEntry) {
|
|
|
|
return _hasChangedIcon || !_origEntry.equals(newEntry);
|
|
|
|
}
|
2020-05-24 13:43:59 +02:00
|
|
|
|
2022-10-10 22:32:30 +02:00
|
|
|
private void discardAndFinish() {
|
2018-09-25 19:36:56 +02:00
|
|
|
AtomicReference<String> msg = new AtomicReference<>();
|
2019-12-25 19:21:34 +01:00
|
|
|
AtomicReference<VaultEntry> entry = new AtomicReference<>();
|
2018-09-25 19:36:56 +02:00
|
|
|
try {
|
|
|
|
entry.set(parseEntry());
|
|
|
|
} catch (ParseException e) {
|
|
|
|
msg.set(e.getMessage());
|
|
|
|
}
|
|
|
|
|
2022-10-10 22:32:30 +02:00
|
|
|
if (!hasUnsavedChanges(entry.get())) {
|
|
|
|
finish();
|
2017-12-27 22:04:22 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-09-25 19:36:56 +02:00
|
|
|
// ask for confirmation if the entry has been changed
|
2022-10-10 22:32:30 +02:00
|
|
|
Dialogs.showDiscardDialog(EditEntryActivity.this,
|
2018-09-25 19:36:56 +02:00
|
|
|
(dialog, which) -> {
|
|
|
|
// if the entry couldn't be parsed, we show an error dialog
|
|
|
|
if (msg.get() != null) {
|
|
|
|
onSaveError(msg.get());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-05-02 18:51:33 +02:00
|
|
|
addAndFinish(entry.get());
|
2018-09-25 19:36:56 +02:00
|
|
|
},
|
2022-10-10 22:32:30 +02:00
|
|
|
(dialog, which) -> finish()
|
2018-05-14 21:02:35 +02:00
|
|
|
);
|
2017-12-27 22:04:22 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean onOptionsItemSelected(MenuItem item) {
|
|
|
|
switch (item.getItemId()) {
|
|
|
|
case android.R.id.home:
|
2022-10-10 22:32:30 +02:00
|
|
|
discardAndFinish();
|
2018-05-10 23:11:21 +02:00
|
|
|
break;
|
2017-12-27 22:04:22 +01:00
|
|
|
case R.id.action_save:
|
2018-05-10 23:11:21 +02:00
|
|
|
onSave();
|
|
|
|
break;
|
2017-12-27 22:04:22 +01:00
|
|
|
case R.id.action_delete:
|
2021-06-04 14:21:17 +02:00
|
|
|
Dialogs.showDeleteEntriesDialog(this, Collections.singletonList(_origEntry), (dialog, which) -> {
|
2020-05-02 18:51:33 +02:00
|
|
|
deleteAndFinish(_origEntry);
|
2018-05-10 23:11:21 +02:00
|
|
|
});
|
|
|
|
break;
|
2020-05-24 13:43:59 +02:00
|
|
|
case R.id.action_edit_icon:
|
2020-07-08 11:18:40 +02:00
|
|
|
startIconSelection();
|
2020-05-24 13:43:59 +02:00
|
|
|
break;
|
2021-06-02 18:02:38 +02:00
|
|
|
case R.id.action_reset_usage_count:
|
|
|
|
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
|
|
|
.setTitle(R.string.action_reset_usage_count)
|
|
|
|
.setMessage(R.string.action_reset_usage_count_dialog)
|
|
|
|
.setPositiveButton(android.R.string.yes, (dialog, which) -> resetUsageCount())
|
|
|
|
.setNegativeButton(android.R.string.no, null)
|
|
|
|
.create());
|
|
|
|
break;
|
2018-06-09 21:40:18 +02:00
|
|
|
case R.id.action_default_icon:
|
2018-09-25 19:36:56 +02:00
|
|
|
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
|
2018-06-07 12:27:42 +02:00
|
|
|
_iconView.setImageDrawable(drawable);
|
2020-07-08 11:18:40 +02:00
|
|
|
|
|
|
|
_selectedIcon = null;
|
2018-09-25 19:36:56 +02:00
|
|
|
_hasCustomIcon = false;
|
|
|
|
_hasChangedIcon = true;
|
2017-12-27 22:04:22 +01:00
|
|
|
default:
|
|
|
|
return super.onOptionsItemSelected(item);
|
|
|
|
}
|
2018-05-10 23:11:21 +02:00
|
|
|
|
|
|
|
return true;
|
2017-12-27 22:04:22 +01:00
|
|
|
}
|
|
|
|
|
2020-07-08 11:18:40 +02:00
|
|
|
private void startImageSelectionActivity() {
|
2020-05-24 13:43:59 +02:00
|
|
|
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
|
|
|
|
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
|
|
|
|
2020-07-15 12:53:16 +02:00
|
|
|
Intent fileIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
|
|
|
fileIntent.setType("image/*");
|
|
|
|
|
2020-05-24 13:43:59 +02:00
|
|
|
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_icon));
|
2020-07-15 12:53:16 +02:00
|
|
|
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { fileIntent });
|
2022-02-06 19:00:01 +01:00
|
|
|
_vaultManager.startActivityForResult(this, chooserIntent, PICK_IMAGE_REQUEST);
|
2020-05-24 13:43:59 +02:00
|
|
|
}
|
|
|
|
|
2021-06-02 18:02:38 +02:00
|
|
|
private void resetUsageCount() {
|
2022-02-06 19:00:01 +01:00
|
|
|
_prefs.resetUsageCount(_origEntry.getUUID());
|
2021-06-02 18:02:38 +02:00
|
|
|
_textUsageCount.setText("0");
|
|
|
|
}
|
|
|
|
|
2020-07-08 11:18:40 +02:00
|
|
|
private void startIconSelection() {
|
2022-02-06 19:00:01 +01:00
|
|
|
List<IconPack> iconPacks = _iconPackManager.getIconPacks().stream()
|
2020-07-08 11:18:40 +02:00
|
|
|
.sorted(Comparator.comparing(IconPack::getName))
|
|
|
|
.collect(Collectors.toList());
|
|
|
|
if (iconPacks.size() == 0) {
|
|
|
|
startImageSelectionActivity();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
BottomSheetDialog dialog = IconPickerDialog.create(this, iconPacks, _textIssuer.getText().toString(), new IconAdapter.Listener() {
|
|
|
|
@Override
|
|
|
|
public void onIconSelected(IconPack.Icon icon) {
|
2021-04-14 22:52:00 +02:00
|
|
|
selectIcon(icon);
|
2020-07-08 11:18:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onCustomSelected() {
|
|
|
|
startImageSelectionActivity();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
Dialogs.showSecureDialog(dialog);
|
|
|
|
}
|
|
|
|
|
2021-04-14 22:52:00 +02:00
|
|
|
private void selectIcon(IconPack.Icon icon) {
|
|
|
|
_selectedIcon = icon;
|
|
|
|
_hasCustomIcon = true;
|
|
|
|
_hasChangedIcon = true;
|
|
|
|
|
|
|
|
IconViewHelper.setLayerType(_iconView, icon.getIconType());
|
|
|
|
Glide.with(EditEntryActivity.this)
|
|
|
|
.asDrawable()
|
|
|
|
.load(icon.getFile())
|
|
|
|
.set(IconLoader.ICON_TYPE, icon.getIconType())
|
|
|
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
|
|
.skipMemoryCache(false)
|
|
|
|
.into(_iconView);
|
|
|
|
}
|
|
|
|
|
2020-05-24 13:43:59 +02:00
|
|
|
private void startEditingIcon(Uri data) {
|
|
|
|
Glide.with(this)
|
|
|
|
.asBitmap()
|
|
|
|
.load(data)
|
|
|
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
|
|
.skipMemoryCache(false)
|
|
|
|
.into(new CustomTarget<Bitmap>() {
|
|
|
|
@Override
|
|
|
|
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
|
|
|
_kropView.setBitmap(resource);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onLoadCleared(@Nullable Drawable placeholder) {
|
|
|
|
|
|
|
|
}
|
|
|
|
});
|
|
|
|
_iconView.setVisibility(View.GONE);
|
|
|
|
_kropView.setVisibility(View.VISIBLE);
|
|
|
|
|
|
|
|
_saveImageButton.setOnClickListener(v -> {
|
|
|
|
stopEditingIcon(true);
|
|
|
|
});
|
|
|
|
|
2022-10-10 22:32:30 +02:00
|
|
|
_iconBackPressHandler.setEnabled(true);
|
2020-05-24 13:43:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private void stopEditingIcon(boolean save) {
|
2020-07-08 11:18:40 +02:00
|
|
|
if (save && _selectedIcon == null) {
|
2020-05-24 13:43:59 +02:00
|
|
|
_iconView.setImageBitmap(_kropView.getCroppedBitmap());
|
|
|
|
}
|
|
|
|
_iconView.setVisibility(View.VISIBLE);
|
|
|
|
_kropView.setVisibility(View.GONE);
|
2020-07-08 11:18:40 +02:00
|
|
|
|
2020-05-24 13:43:59 +02:00
|
|
|
_hasCustomIcon = _hasCustomIcon || save;
|
|
|
|
_hasChangedIcon = save;
|
2022-10-10 22:32:30 +02:00
|
|
|
_iconBackPressHandler.setEnabled(false);
|
2020-05-24 13:43:59 +02:00
|
|
|
}
|
|
|
|
|
2017-12-27 22:04:22 +01:00
|
|
|
@Override
|
|
|
|
public boolean onCreateOptionsMenu(Menu menu) {
|
|
|
|
getMenuInflater().inflate(R.menu.menu_edit, menu);
|
2017-12-30 14:21:21 +01:00
|
|
|
if (_isNew) {
|
|
|
|
menu.findItem(R.id.action_delete).setVisible(false);
|
|
|
|
}
|
2018-09-25 19:36:56 +02:00
|
|
|
if (!_hasCustomIcon) {
|
2018-06-09 21:40:18 +02:00
|
|
|
menu.findItem(R.id.action_default_icon).setVisible(false);
|
2018-06-07 00:30:49 +02:00
|
|
|
}
|
|
|
|
|
2017-12-27 22:04:22 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-05-02 18:51:33 +02:00
|
|
|
private void addAndFinish(VaultEntry entry) {
|
2020-08-15 18:32:14 +02:00
|
|
|
// It's possible that the new entry was already added to the vault, but writing the
|
|
|
|
// vault to disk failed, causing the user to tap 'Save' again. Calling addEntry
|
|
|
|
// again would cause a crash in that case, so the isEntryDuplicate check prevents
|
|
|
|
// that.
|
2022-02-06 19:00:01 +01:00
|
|
|
VaultRepository vault = _vaultManager.getVault();
|
|
|
|
if (_isNew && !vault.isEntryDuplicate(entry)) {
|
|
|
|
vault.addEntry(entry);
|
2020-05-02 18:51:33 +02:00
|
|
|
} else {
|
2022-02-06 19:00:01 +01:00
|
|
|
vault.replaceEntry(entry);
|
2020-05-02 18:51:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
saveAndFinish(entry, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void deleteAndFinish(VaultEntry entry) {
|
2022-02-06 19:00:01 +01:00
|
|
|
_vaultManager.getVault().removeEntry(entry);
|
2020-05-02 18:51:33 +02:00
|
|
|
saveAndFinish(entry, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void saveAndFinish(VaultEntry entry, boolean delete) {
|
2018-01-01 22:53:10 +01:00
|
|
|
Intent intent = new Intent();
|
2020-05-02 18:51:33 +02:00
|
|
|
intent.putExtra("entryUUID", entry.getUUID());
|
2018-01-01 22:53:10 +01:00
|
|
|
intent.putExtra("delete", delete);
|
2020-05-02 18:51:33 +02:00
|
|
|
|
2022-02-06 19:00:01 +01:00
|
|
|
if (saveAndBackupVault()) {
|
2020-05-02 18:51:33 +02:00
|
|
|
setResult(RESULT_OK, intent);
|
|
|
|
finish();
|
|
|
|
}
|
2018-01-01 22:53:10 +01:00
|
|
|
}
|
Introduce UUIDMap for storing objects that are keyed by a UUID
This patch introduces the new ``UUIDMap`` type, reducing code duplication and
making UUID lookups faster. We currently already use UUIDs as the identifier for
the ``DatabaseEntry`` and ``Slot`` types, but the way lookups by UUID work are
kind of ugly, as we simply iterate over the list until we find a match. As we're
probably going to have more types like this soon (groups and icons, for
example), I figured it'd be good to abstract this away into a separate type and
make it a map instead of a list.
The only thing that has gotten slower is the ``swap`` method. The internal
``LinkedHashMap`` retains insertion order with a linked list, but does not know
about the position of the values, so we basically have to copy the entire map to
simply swap two values. I don't think it's too big of a deal, because swap
operations still take less than a millisecond even with large vaults, but
suggestions for improving this are welcome.
I had to update gradle and JUnit to be able to use the new ``assertThrows``
assertion method, so this patch includes that as well.
2019-06-10 18:25:44 +02:00
|
|
|
|
2018-06-06 21:26:09 +02:00
|
|
|
@Override
|
|
|
|
protected void onActivityResult(int requestCode, final int resultCode, Intent data) {
|
2018-11-27 20:55:55 +01:00
|
|
|
if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) {
|
2022-10-10 22:32:30 +02:00
|
|
|
String fileType = SafHelper.getMimeType(this, data.getData());
|
2021-05-20 21:46:56 +02:00
|
|
|
if (fileType != null && fileType.equals(IconType.SVG.toMimeType())) {
|
2021-04-14 22:52:00 +02:00
|
|
|
ImportFileTask.Params params = new ImportFileTask.Params(data.getData(), "icon", null);
|
|
|
|
ImportFileTask task = new ImportFileTask(this, result -> {
|
2022-11-28 18:50:49 +01:00
|
|
|
if (result.getError() == null) {
|
2021-04-14 22:52:00 +02:00
|
|
|
CustomSvgIcon icon = new CustomSvgIcon(result.getFile());
|
|
|
|
selectIcon(icon);
|
|
|
|
} else {
|
2022-11-28 18:50:49 +01:00
|
|
|
Dialogs.showErrorDialog(this, R.string.reading_file_error, result.getError());
|
2021-04-14 22:52:00 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
task.execute(getLifecycle(), params);
|
|
|
|
} else {
|
|
|
|
startEditingIcon(data.getData());
|
|
|
|
}
|
2018-06-06 21:26:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
super.onActivityResult(requestCode, resultCode, data);
|
|
|
|
}
|
|
|
|
|
2019-03-31 22:15:03 +02:00
|
|
|
private int parsePeriod() throws ParseException {
|
|
|
|
try {
|
2021-01-17 13:12:43 +01:00
|
|
|
return Integer.parseInt(_textPeriodCounter.getText().toString());
|
2019-03-31 22:15:03 +02:00
|
|
|
} catch (NumberFormatException e) {
|
|
|
|
throw new ParseException("Period is not an integer.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-25 19:21:34 +01:00
|
|
|
private VaultEntry parseEntry() throws ParseException {
|
2018-01-01 22:14:11 +01:00
|
|
|
if (_textSecret.length() == 0) {
|
2018-09-25 19:36:56 +02:00
|
|
|
throw new ParseException("Secret is a required field.");
|
2018-01-01 22:14:11 +01:00
|
|
|
}
|
|
|
|
|
2021-01-17 13:12:43 +01:00
|
|
|
String type = _dropdownType.getText().toString();
|
|
|
|
String algo = _dropdownAlgo.getText().toString();
|
2022-01-07 12:49:53 +03:00
|
|
|
String lowerCasedType = type.toLowerCase(Locale.ROOT);
|
|
|
|
|
2022-07-26 23:56:04 +01:00
|
|
|
if (lowerCasedType.equals(YandexInfo.ID) || lowerCasedType.equals(MotpInfo.ID)) {
|
|
|
|
int pinLength = _textPin.length();
|
2022-01-07 12:49:53 +03:00
|
|
|
if (pinLength < 4) {
|
2022-02-02 17:55:09 +01:00
|
|
|
throw new ParseException("PIN is a required field. Must have a minimum length of 4 digits.");
|
2022-01-07 12:49:53 +03:00
|
|
|
}
|
2022-07-26 23:56:04 +01:00
|
|
|
if (pinLength != 4 && lowerCasedType.equals(MotpInfo.ID)) {
|
|
|
|
throw new ParseException("PIN must have a length of 4 digits.");
|
|
|
|
}
|
2022-01-07 12:49:53 +03:00
|
|
|
}
|
2017-12-27 22:04:22 +01:00
|
|
|
|
|
|
|
int digits;
|
|
|
|
try {
|
2019-03-25 19:34:57 +01:00
|
|
|
digits = Integer.parseInt(_textDigits.getText().toString());
|
2017-12-27 22:04:22 +01:00
|
|
|
} catch (NumberFormatException e) {
|
2018-09-25 19:36:56 +02:00
|
|
|
throw new ParseException("Digits is not an integer.");
|
2017-12-27 22:04:22 +01:00
|
|
|
}
|
|
|
|
|
2018-06-06 16:15:31 +02:00
|
|
|
byte[] secret;
|
|
|
|
try {
|
2020-10-24 14:21:49 +02:00
|
|
|
String secretString = new String(EditTextHelper.getEditTextChars(_textSecret));
|
2022-07-26 23:56:04 +01:00
|
|
|
|
|
|
|
secret = (lowerCasedType.equals(MotpInfo.ID)) ?
|
|
|
|
Hex.decode(secretString) : GoogleAuthInfo.parseSecret(secretString);
|
|
|
|
|
2019-12-26 20:47:28 +01:00
|
|
|
if (secret.length == 0) {
|
|
|
|
throw new ParseException("Secret cannot be empty");
|
|
|
|
}
|
|
|
|
} catch (EncodingException e) {
|
2022-07-26 23:56:04 +01:00
|
|
|
String exceptionMessage = (lowerCasedType.equals(MotpInfo.ID)) ?
|
|
|
|
"Secret is not valid hexadecimal" : "Secret is not valid base32.";
|
|
|
|
|
|
|
|
throw new ParseException(exceptionMessage);
|
2018-06-06 16:15:31 +02:00
|
|
|
}
|
2018-01-01 22:14:11 +01:00
|
|
|
|
2018-06-06 16:15:31 +02:00
|
|
|
OtpInfo info;
|
2018-01-01 22:14:11 +01:00
|
|
|
try {
|
2021-06-23 18:06:01 +02:00
|
|
|
switch (type.toLowerCase(Locale.ROOT)) {
|
2019-03-31 22:15:03 +02:00
|
|
|
case TotpInfo.ID:
|
|
|
|
info = new TotpInfo(secret, algo, digits, parsePeriod());
|
|
|
|
break;
|
|
|
|
case SteamInfo.ID:
|
|
|
|
info = new SteamInfo(secret, algo, digits, parsePeriod());
|
2018-06-06 16:15:31 +02:00
|
|
|
break;
|
2019-03-31 22:15:03 +02:00
|
|
|
case HotpInfo.ID:
|
2018-06-06 16:15:31 +02:00
|
|
|
long counter;
|
|
|
|
try {
|
2021-01-17 13:12:43 +01:00
|
|
|
counter = Long.parseLong(_textPeriodCounter.getText().toString());
|
2018-06-06 16:15:31 +02:00
|
|
|
} catch (NumberFormatException e) {
|
2018-09-25 19:36:56 +02:00
|
|
|
throw new ParseException("Counter is not an integer.");
|
2018-06-06 16:15:31 +02:00
|
|
|
}
|
|
|
|
info = new HotpInfo(secret, algo, digits, counter);
|
|
|
|
break;
|
2022-01-07 12:49:53 +03:00
|
|
|
case YandexInfo.ID:
|
2022-07-26 23:56:04 +01:00
|
|
|
info = new YandexInfo(secret, _textPin.getText().toString());
|
|
|
|
break;
|
|
|
|
case MotpInfo.ID:
|
|
|
|
info = new MotpInfo(secret, _textPin.getText().toString());
|
2022-01-07 12:49:53 +03:00
|
|
|
break;
|
2018-06-06 16:15:31 +02:00
|
|
|
default:
|
2020-05-02 18:51:12 +02:00
|
|
|
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
|
2018-06-06 16:15:31 +02:00
|
|
|
}
|
|
|
|
|
2018-01-01 22:14:11 +01:00
|
|
|
info.setDigits(digits);
|
|
|
|
info.setAlgorithm(algo);
|
2018-06-06 16:15:31 +02:00
|
|
|
} catch (OtpInfoException e) {
|
2018-09-25 19:36:56 +02:00
|
|
|
throw new ParseException("The entered info is incorrect: " + e.getMessage());
|
2018-01-01 22:14:11 +01:00
|
|
|
}
|
2017-12-27 22:04:22 +01:00
|
|
|
|
2019-12-25 19:21:34 +01:00
|
|
|
VaultEntry entry = Cloner.clone(_origEntry);
|
2019-09-04 21:42:01 +02:00
|
|
|
entry.setInfo(info);
|
2018-06-06 16:15:31 +02:00
|
|
|
entry.setIssuer(_textIssuer.getText().toString());
|
|
|
|
entry.setName(_textName.getText().toString());
|
2021-08-30 10:21:54 +01:00
|
|
|
entry.setNote(_textNote.getText().toString());
|
2018-06-06 16:15:31 +02:00
|
|
|
|
2021-01-17 13:12:43 +01:00
|
|
|
int groupPos = _dropdownGroupList.indexOf(_dropdownGroup.getText().toString());
|
2018-12-11 11:44:36 +01:00
|
|
|
if (groupPos != 0) {
|
2021-01-17 13:12:43 +01:00
|
|
|
String group = _dropdownGroupList.get(groupPos);
|
2018-12-11 11:44:36 +01:00
|
|
|
entry.setGroup(group);
|
|
|
|
} else {
|
|
|
|
entry.setGroup(null);
|
|
|
|
}
|
|
|
|
|
2018-09-25 19:36:56 +02:00
|
|
|
if (_hasChangedIcon) {
|
|
|
|
if (_hasCustomIcon) {
|
2020-07-08 11:18:40 +02:00
|
|
|
if (_selectedIcon == null) {
|
|
|
|
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
|
|
|
|
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
|
|
|
// the quality parameter is ignored for PNG
|
|
|
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
|
|
|
|
byte[] data = stream.toByteArray();
|
|
|
|
entry.setIcon(data, IconType.PNG);
|
|
|
|
} else {
|
|
|
|
byte[] iconBytes;
|
|
|
|
try (FileInputStream inStream = new FileInputStream(_selectedIcon.getFile())){
|
|
|
|
iconBytes = IOUtils.readFile(inStream);
|
|
|
|
} catch (IOException e) {
|
2021-04-14 22:52:00 +02:00
|
|
|
throw new ParseException(e.getMessage());
|
2020-07-08 11:18:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
entry.setIcon(iconBytes, _selectedIcon.getIconType());
|
|
|
|
}
|
2018-09-25 19:36:56 +02:00
|
|
|
} else {
|
2020-07-08 11:18:40 +02:00
|
|
|
entry.setIcon(null, IconType.INVALID);
|
2018-09-25 19:36:56 +02:00
|
|
|
}
|
2018-06-07 12:27:42 +02:00
|
|
|
}
|
|
|
|
|
2018-09-25 19:36:56 +02:00
|
|
|
return entry;
|
2017-12-27 22:04:22 +01:00
|
|
|
}
|
|
|
|
|
2018-09-25 19:36:56 +02:00
|
|
|
private void onSaveError(String msg) {
|
2018-11-15 22:20:31 +01:00
|
|
|
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
2018-10-09 23:13:51 +02:00
|
|
|
.setTitle(getString(R.string.saving_profile_error))
|
2017-12-27 22:04:22 +01:00
|
|
|
.setMessage(msg)
|
|
|
|
.setPositiveButton(android.R.string.ok, null)
|
2018-11-15 22:20:31 +01:00
|
|
|
.create());
|
2017-12-27 22:04:22 +01:00
|
|
|
}
|
|
|
|
|
2018-09-25 19:36:56 +02:00
|
|
|
private boolean onSave() {
|
2022-10-10 22:32:30 +02:00
|
|
|
if (_iconBackPressHandler.isEnabled()) {
|
2020-05-24 13:43:59 +02:00
|
|
|
stopEditingIcon(true);
|
|
|
|
}
|
|
|
|
|
2019-12-25 19:21:34 +01:00
|
|
|
VaultEntry entry;
|
2018-09-25 19:36:56 +02:00
|
|
|
try {
|
|
|
|
entry = parseEntry();
|
|
|
|
} catch (ParseException e) {
|
|
|
|
onSaveError(e.getMessage());
|
|
|
|
return false;
|
2017-12-27 22:04:22 +01:00
|
|
|
}
|
|
|
|
|
2020-05-02 18:51:33 +02:00
|
|
|
addAndFinish(entry);
|
2018-09-25 19:36:56 +02:00
|
|
|
return true;
|
|
|
|
}
|
2017-12-27 22:04:22 +01:00
|
|
|
|
2021-01-27 20:06:34 +01:00
|
|
|
private static void setViewEnabled(View view, boolean enabled) {
|
|
|
|
view.setEnabled(enabled);
|
|
|
|
|
|
|
|
if (view instanceof ViewGroup) {
|
|
|
|
ViewGroup group = (ViewGroup) view;
|
|
|
|
for (int i = 0; i < group.getChildCount(); i++) {
|
|
|
|
setViewEnabled(group.getChildAt(i), enabled);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-10 22:32:30 +02:00
|
|
|
private final TextWatcher _validationListener = new SimpleTextWatcher((s) -> {
|
|
|
|
updateBackPressHandlerState();
|
|
|
|
});
|
|
|
|
|
|
|
|
private final TextWatcher _nameChangeListener = new SimpleTextWatcher((s) -> {
|
|
|
|
if (!_hasCustomIcon) {
|
|
|
|
TextDrawable drawable = TextDrawableHelper.generate(_textIssuer.getText().toString(), _textName.getText().toString(), _iconView);
|
|
|
|
_iconView.setImageDrawable(drawable);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
private void updateBackPressHandlerState() {
|
|
|
|
VaultEntry entry = null;
|
|
|
|
try {
|
|
|
|
entry = parseEntry();
|
|
|
|
} catch (ParseException ignored) {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
boolean backEnabled = hasUnsavedChanges(entry);
|
|
|
|
_backPressHandler.setEnabled(backEnabled);
|
|
|
|
}
|
|
|
|
|
|
|
|
private class BackPressHandler extends OnBackPressedCallback {
|
|
|
|
public BackPressHandler() {
|
|
|
|
super(false);
|
2018-06-06 16:27:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2022-10-10 22:32:30 +02:00
|
|
|
public void handleOnBackPressed() {
|
|
|
|
discardAndFinish();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private class IconBackPressHandler extends OnBackPressedCallback {
|
|
|
|
public IconBackPressHandler() {
|
|
|
|
super(false);
|
2018-06-06 16:27:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2022-10-10 22:32:30 +02:00
|
|
|
public void handleOnBackPressed() {
|
|
|
|
stopEditingIcon(false);
|
2018-06-06 16:27:56 +02:00
|
|
|
}
|
2022-10-10 22:32:30 +02:00
|
|
|
}
|
2018-06-06 16:27:56 +02:00
|
|
|
|
2018-09-25 19:36:56 +02:00
|
|
|
private static class ParseException extends Exception {
|
|
|
|
public ParseException(String message) {
|
|
|
|
super(message);
|
|
|
|
}
|
|
|
|
}
|
2021-04-14 22:52:00 +02:00
|
|
|
|
|
|
|
private static class CustomSvgIcon extends IconPack.Icon {
|
|
|
|
private final File _file;
|
|
|
|
|
|
|
|
protected CustomSvgIcon(File file) {
|
|
|
|
super(file.getAbsolutePath(), null, null);
|
|
|
|
_file = file;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Nullable
|
|
|
|
public File getFile() {
|
|
|
|
return _file;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public IconType getIconType() {
|
|
|
|
return IconType.SVG;
|
|
|
|
}
|
|
|
|
}
|
2017-12-27 22:04:22 +01:00
|
|
|
}
|