package com.beemdevelopment.aegis.ui; import android.Manifest; import android.annotation.SuppressLint; import android.content.ClipData; import android.content.ClipDescription; import android.content.ClipboardManager; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.graphics.Typeface; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.PersistableBundle; import android.provider.Settings; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.style.StyleSpan; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.AutoCompleteTextView; import android.widget.Button; import android.widget.CheckBox; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.view.ActionMode; import androidx.appcompat.widget.SearchView; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import com.beemdevelopment.aegis.GroupPlaceholderType; import com.beemdevelopment.aegis.Preferences; import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.SortCategory; import com.beemdevelopment.aegis.helpers.DropdownHelper; import com.beemdevelopment.aegis.helpers.FabScrollHelper; import com.beemdevelopment.aegis.helpers.PermissionHelper; import com.beemdevelopment.aegis.otp.GoogleAuthInfo; import com.beemdevelopment.aegis.otp.GoogleAuthInfoException; import com.beemdevelopment.aegis.otp.OtpInfoException; import com.beemdevelopment.aegis.ui.dialogs.Dialogs; import com.beemdevelopment.aegis.ui.fragments.preferences.BackupsPreferencesFragment; import com.beemdevelopment.aegis.ui.fragments.preferences.PreferencesFragment; import com.beemdevelopment.aegis.ui.models.ErrorCardInfo; import com.beemdevelopment.aegis.ui.models.VaultGroupModel; import com.beemdevelopment.aegis.ui.tasks.QrDecodeTask; import com.beemdevelopment.aegis.ui.views.EntryListView; import com.beemdevelopment.aegis.util.TimeUtils; import com.beemdevelopment.aegis.util.UUIDMap; import com.beemdevelopment.aegis.vault.VaultEntry; import com.beemdevelopment.aegis.vault.VaultFile; import com.beemdevelopment.aegis.vault.VaultGroup; import com.beemdevelopment.aegis.vault.VaultRepository; import com.beemdevelopment.aegis.vault.VaultRepositoryException; import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.android.material.chip.Chip; import com.google.android.material.chip.ChipGroup; import com.google.android.material.color.MaterialColors; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; import com.google.common.base.Strings; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; public class MainActivity extends AegisActivity implements EntryListView.Listener { // Permission request codes private static final int CODE_PERM_CAMERA = 0; private boolean _loaded; private boolean _isRecreated; private boolean _isDPadPressed; private boolean _isDoingIntro; private boolean _isAuthenticating; private String _submittedSearchQuery; private String _pendingSearchQuery; private List _selectedEntries; private Menu _menu; private SearchView _searchView; private EntryListView _entryListView; private Collection _groups; private ChipGroup _groupChip; private Set _groupFilter; private Set _prefGroupFilter; private FabScrollHelper _fabScrollHelper; private ActionMode _actionMode; private ActionMode.Callback _actionModeCallbacks = new ActionModeCallbacks(); private LockBackPressHandler _lockBackPressHandler; private SearchViewBackPressHandler _searchViewBackPressHandler; private ActionModeBackPressHandler _actionModeBackPressHandler; private final ActivityResultLauncher authResultLauncher = registerForActivityResult(new StartActivityForResult(), activityResult -> { _isAuthenticating = false; if (activityResult.getResultCode() == RESULT_OK) { onDecryptResult(); } }); private final ActivityResultLauncher introResultLauncher = registerForActivityResult(new StartActivityForResult(), activityResult -> { _isDoingIntro = false; if (activityResult.getResultCode() == RESULT_OK) { onIntroResult(); } }); private final ActivityResultLauncher scanResultLauncher = registerForActivityResult(new StartActivityForResult(), activityResult -> { if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) { return; } onScanResult(activityResult.getData()); }); private final ActivityResultLauncher assignIconsResultLauncher = registerForActivityResult(new StartActivityForResult(), activityResult -> { if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) { return; } onAssignIconsResult(activityResult.getData()); }); private final ActivityResultLauncher preferenceResultLauncher = registerForActivityResult(new StartActivityForResult(), activityResult -> onPreferencesResult()); private final ActivityResultLauncher editEntryResultLauncher = registerForActivityResult(new StartActivityForResult(), activityResult -> { if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) { return; } onEditEntryResult(activityResult.getData()); }); private final ActivityResultLauncher addEntryResultLauncher = registerForActivityResult(new StartActivityForResult(), activityResult -> { if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) { return; } onAddEntryResult(activityResult.getData()); }); private final ActivityResultLauncher codeScanResultLauncher = registerForActivityResult(new StartActivityForResult(), activityResult -> { if (activityResult.getResultCode() == RESULT_OK && activityResult.getData() != null) { onScanImageResult(activityResult.getData()); } }); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); setSupportActionBar(findViewById(R.id.toolbar)); _loaded = false; _isDPadPressed = false; _isDoingIntro = false; _isAuthenticating = false; if (savedInstanceState != null) { _isRecreated = true; _pendingSearchQuery = savedInstanceState.getString("pendingSearchQuery"); _submittedSearchQuery = savedInstanceState.getString("submittedSearchQuery"); _isDoingIntro = savedInstanceState.getBoolean("isDoingIntro"); _isAuthenticating = savedInstanceState.getBoolean("isAuthenticating"); } _lockBackPressHandler = new LockBackPressHandler(); getOnBackPressedDispatcher().addCallback(this, _lockBackPressHandler); _searchViewBackPressHandler = new SearchViewBackPressHandler(); getOnBackPressedDispatcher().addCallback(this, _searchViewBackPressHandler); _actionModeBackPressHandler = new ActionModeBackPressHandler(); getOnBackPressedDispatcher().addCallback(this, _actionModeBackPressHandler); _entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles); _entryListView.setListener(this); _entryListView.setCodeGroupSize(_prefs.getCodeGroupSize()); _entryListView.setAccountNamePosition(_prefs.getAccountNamePosition()); _entryListView.setShowIcon(_prefs.isIconVisible()); _entryListView.setShowExpirationState(_prefs.getShowExpirationState()); _entryListView.setOnlyShowNecessaryAccountNames(_prefs.onlyShowNecessaryAccountNames()); _entryListView.setHighlightEntry(_prefs.isEntryHighlightEnabled()); _entryListView.setPauseFocused(_prefs.isPauseFocusedEnabled()); _entryListView.setTapToReveal(_prefs.isTapToRevealEnabled()); _entryListView.setTapToRevealTime(_prefs.getTapToRevealTime()); _entryListView.setSortCategory(_prefs.getCurrentSortCategory(), false); _entryListView.setViewMode(_prefs.getCurrentViewMode()); _entryListView.setCopyBehavior(_prefs.getCopyBehavior()); _entryListView.setSearchBehaviorMask(_prefs.getSearchBehaviorMask()); _prefGroupFilter = _prefs.getGroupFilter(); FloatingActionButton fab = findViewById(R.id.fab); fab.setOnClickListener(v -> { View view = getLayoutInflater().inflate(R.layout.dialog_add_entry, null); BottomSheetDialog dialog = new BottomSheetDialog(this); dialog.setContentView(view); view.findViewById(R.id.fab_enter).setOnClickListener(v1 -> { dialog.dismiss(); startEditEntryActivityForManual(); }); view.findViewById(R.id.fab_scan_image).setOnClickListener(v2 -> { dialog.dismiss(); startScanImageActivity(); }); view.findViewById(R.id.fab_scan).setOnClickListener(v3 -> { dialog.dismiss(); startScanActivity(); }); Dialogs.showSecureDialog(dialog); }); _groupChip = findViewById(R.id.groupChipGroup); _fabScrollHelper = new FabScrollHelper(fab); _selectedEntries = new ArrayList<>(); } public void setGroups(Collection groups) { _groups = groups; _groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE); if (_prefGroupFilter != null) { Set groupFilter = cleanGroupFilter(_prefGroupFilter); _prefGroupFilter = null; if (!groupFilter.isEmpty()) { _groupFilter = groupFilter; _entryListView.setGroupFilter(groupFilter, false); } } else if (_groupFilter != null) { Set groupFilter = cleanGroupFilter(_groupFilter); if (!_groupFilter.equals(groupFilter)) { _groupFilter = groupFilter; _entryListView.setGroupFilter(groupFilter, true); } } _entryListView.setGroups(groups); initializeGroups(); } private void initializeGroups() { _groupChip.removeAllViews(); for (VaultGroup group : _groups) { addChipTo(_groupChip, new VaultGroupModel(group)); } GroupPlaceholderType placeholderType = GroupPlaceholderType.NO_GROUP; addChipTo(_groupChip, new VaultGroupModel(this, placeholderType)); addSaveChip(_groupChip); } private Set cleanGroupFilter(Set groupFilter) { Set groupUuids = _groups.stream().map(UUIDMap.Value::getUUID).collect(Collectors.toSet()); return groupFilter.stream() .filter(g -> g == null || groupUuids.contains(g)) .collect(Collectors.toSet()); } private void addChipTo(ChipGroup chipGroup, VaultGroupModel group) { Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false); chip.setText(group.getName()); chip.setCheckable(true); chip.setCheckedIconVisible(false); chip.setChecked(_groupFilter != null && _groupFilter.contains(group.getUUID())); if (group.isPlaceholder()) { GroupPlaceholderType groupPlaceholderType = group.getPlaceholderType(); chip.setTag(groupPlaceholderType); if (groupPlaceholderType == GroupPlaceholderType.ALL) { chip.setChecked(_groupFilter == null); } else if (groupPlaceholderType == GroupPlaceholderType.NO_GROUP) { chip.setChecked(_groupFilter != null && _groupFilter.contains(null)); } } else { chip.setTag(group); } chip.setOnCheckedChangeListener((group1, isChecked) -> { Set groupFilter = new HashSet<>(); setSaveChipVisibility(true); if (!isChecked) { group1.setChecked(false); _groupFilter = groupFilter; _entryListView.setGroupFilter(groupFilter, false); return; } Object chipTag = group1.getTag(); if (chipTag == GroupPlaceholderType.NO_GROUP) { groupFilter.add(null); } else { groupFilter = getGroupFilter(chipGroup); } _groupFilter = groupFilter; _entryListView.setGroupFilter(groupFilter, false); }); chipGroup.addView(chip); } private void addSaveChip(ChipGroup chipGroup) { Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false); chip.setText(getString(R.string.save)); chip.setVisibility(View.GONE); chip.setChipStrokeWidth(0); chip.setCheckable(false); chip.setChipBackgroundColorResource(android.R.color.transparent); chip.setTextColor(MaterialColors.getColor(chip.getRootView(), com.google.android.material.R.attr.colorSecondary)); chip.setClickable(true); chip.setCheckedIconVisible(false); chip.setOnClickListener(v -> { onSaveGroupFilter(_groupFilter); setSaveChipVisibility(false); }); chipGroup.addView(chip); } private void setSaveChipVisibility(boolean visible) { Chip saveChip = (Chip) _groupChip.getChildAt(_groupChip.getChildCount() - 1); saveChip.setChecked(false); saveChip.setVisibility(visible ? View.VISIBLE : View.GONE); } private static Set getGroupFilter(ChipGroup chipGroup) { return chipGroup.getCheckedChipIds().stream() .map(i -> { Chip chip = chipGroup.findViewById(i); if (chip.getTag() instanceof VaultGroupModel) { VaultGroupModel group = (VaultGroupModel) chip.getTag(); return group.getUUID(); } return null; }) .filter(Objects::nonNull) .collect(Collectors.toSet()); } @Override protected void onDestroy() { _entryListView.setListener(null); super.onDestroy(); } @Override protected void onPause() { Map usageMap = _entryListView.getUsageCounts(); if (usageMap != null) { _prefs.setUsageCount(usageMap); } Map lastUsedMap = _entryListView.getLastUsedTimestamps(); if (lastUsedMap != null) { _prefs.setLastUsedTimestamps(lastUsedMap); } super.onPause(); } @Override protected void onSaveInstanceState(@NonNull Bundle instance) { super.onSaveInstanceState(instance); instance.putString("pendingSearchQuery", _pendingSearchQuery); instance.putString("submittedSearchQuery", _submittedSearchQuery); instance.putBoolean("isDoingIntro", _isDoingIntro); instance.putBoolean("isAuthenticating", _isAuthenticating); if (_groupFilter != null) { instance.putSerializable("prefGroupFilter", new HashSet<>(_groupFilter)); } } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { if (!PermissionHelper.checkResults(grantResults)) { Toast.makeText(this, getString(R.string.permission_denied), Toast.LENGTH_SHORT).show(); return; } if (requestCode == CODE_PERM_CAMERA) { startScanActivity(); } super.onRequestPermissionsResult(requestCode, permissions, grantResults); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { _isDPadPressed = isDPadKey(keyCode); return super.onKeyDown(keyCode, event); } private static boolean isDPadKey(int keyCode) { return keyCode == KeyEvent.KEYCODE_DPAD_DOWN || keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || keyCode == KeyEvent.KEYCODE_DPAD_LEFT; } @Override public void onEntryListTouch() { _isDPadPressed = false; if (_searchView != null && !_searchView.isIconified()) { if (ViewCompat.getRootWindowInsets(findViewById(android.R.id.content).getRootView()).isVisible(WindowInsetsCompat.Type.ime())) { InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); if (inputMethodManager != null && getCurrentFocus() != null) { inputMethodManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0); } } } } private void onPreferencesResult() { // refresh the entire entry list if needed if (_loaded) { recreate(); } } private void startEditEntryActivityForNew(VaultEntry entry) { Intent intent = new Intent(this, EditEntryActivity.class); intent.putExtra("newEntry", entry); intent.putExtra("isManual", false); addEntryResultLauncher.launch(intent); } private void startEditEntryActivityForManual() { Intent intent = new Intent(this, EditEntryActivity.class); intent.putExtra("newEntry", VaultEntry.getDefault()); intent.putExtra("isManual", true); addEntryResultLauncher.launch(intent); } private void startEditEntryActivity(VaultEntry entry) { Intent intent = new Intent(this, EditEntryActivity.class); intent.putExtra("entryUUID", entry.getUUID()); editEntryResultLauncher.launch(intent); } private void startAssignIconsActivity(List entries) { ArrayList assignIconEntriesIds = new ArrayList<>(); Intent assignIconIntent = new Intent(getBaseContext(), AssignIconsActivity.class); for (VaultEntry entry : entries) { assignIconEntriesIds.add(entry.getUUID()); } assignIconIntent.putExtra("entries", assignIconEntriesIds); assignIconsResultLauncher.launch(assignIconIntent); } private void startAssignGroupsDialog() { View view = LayoutInflater.from(this).inflate(R.layout.dialog_select_group, null); TextInputLayout groupSelectionLayout = view.findViewById(R.id.group_selection_layout); AutoCompleteTextView groupsSelection = view.findViewById(R.id.group_selection_dropdown); TextInputLayout newGroupLayout = view.findViewById(R.id.text_group_name_layout); TextInputEditText newGroupText = view.findViewById(R.id.text_group_name); Collection groups = _vaultManager.getVault().getUsedGroups(); List groupModels = new ArrayList<>(); groupModels.add(new VaultGroupModel(this, GroupPlaceholderType.NEW_GROUP)); groupModels.addAll(groups.stream().map(VaultGroupModel::new).collect(Collectors.toList())); DropdownHelper.fillDropdown(this, groupsSelection, groupModels); AtomicReference groupModelRef = new AtomicReference<>(); groupsSelection.setOnItemClickListener((parent, view1, position, id) -> { VaultGroupModel groupModel = (VaultGroupModel) parent.getItemAtPosition(position); groupModelRef.set(groupModel); if (groupModel.isPlaceholder()) { newGroupLayout.setVisibility(View.VISIBLE); newGroupText.requestFocus(); } else { newGroupLayout.setVisibility(View.GONE); } groupSelectionLayout.setError(null); }); AlertDialog dialog = new MaterialAlertDialogBuilder(this) .setTitle(R.string.assign_groups) .setView(view) .setPositiveButton(android.R.string.ok, null) .setNegativeButton(android.R.string.cancel, null) .create(); dialog.setOnShowListener(d -> { Button btnPos = dialog.getButton(AlertDialog.BUTTON_POSITIVE); btnPos.setOnClickListener(v -> { VaultGroupModel groupModel = groupModelRef.get(); if (groupModel == null) { groupSelectionLayout.setError(getString(R.string.error_required_field)); return; } if (groupModel.isPlaceholder()) { String newGroupName = newGroupText.getText().toString().trim(); if (newGroupName.isEmpty()) { newGroupLayout.setError(getString(R.string.error_required_field)); return; } VaultGroup group = new VaultGroup(newGroupName); _vaultManager.getVault().addGroup(group); groupModel = new VaultGroupModel(group); } for (VaultEntry selectedEntry : _selectedEntries) { selectedEntry.addGroup(groupModel.getUUID()); } dialog.dismiss(); saveAndBackupVault(); _actionMode.finish(); setGroups(_vaultManager.getVault().getUsedGroups()); }); }); Dialogs.showSecureDialog(dialog); } private void startIntroActivity() { if (!_isDoingIntro) { Intent intro = new Intent(this, IntroActivity.class); introResultLauncher.launch(intro); _isDoingIntro = true; } } private void onScanResult(Intent data) { List entries = (ArrayList) data.getSerializableExtra("entries"); if (entries != null) { importScannedEntries(entries); } } private void onAddEntryResult(Intent data) { if (_loaded) { UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID"); VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID); _entryListView.addEntry(entry, true); } } private void onEditEntryResult(Intent data) { if (_loaded) { UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID"); if (data.getBooleanExtra("delete", false)) { _entryListView.removeEntry(entryUUID); } else { VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID); _entryListView.replaceEntry(entryUUID, entry); } } } private void onAssignIconsResult(Intent data) { if (_loaded) { ArrayList entryUUIDs = (ArrayList) data.getSerializableExtra("entryUUIDs"); for (UUID entryUUID: entryUUIDs) { VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID); _entryListView.replaceEntry(entryUUID, entry); } } } private void onScanImageResult(Intent intent) { if (intent.getData() != null) { startDecodeQrCodeImages(Collections.singletonList(intent.getData())); return; } if (intent.getClipData() != null) { ClipData data = intent.getClipData(); List uris = new ArrayList<>(); for (int i = 0; i < data.getItemCount(); i++) { ClipData.Item item = data.getItemAt(i); if (item.getUri() != null) { uris.add(item.getUri()); } } if (uris.size() > 0) { startDecodeQrCodeImages(uris); } } } private static CharSequence buildImportError(String fileName, Throwable e) { SpannableStringBuilder builder = new SpannableStringBuilder(String.format("%s:\n%s", fileName, e)); builder.setSpan(new StyleSpan(Typeface.BOLD), 0, fileName.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); return builder; } private void startDecodeQrCodeImages(List uris) { QrDecodeTask task = new QrDecodeTask(this, (results) -> { List errors = new ArrayList<>(); List entries = new ArrayList<>(); List googleAuthExports = new ArrayList<>(); for (QrDecodeTask.Result res : results) { if (res.getException() != null) { errors.add(buildImportError(res.getFileName(), res.getException())); continue; } try { Uri scanned = Uri.parse(res.getResult().getText()); if (Objects.equals(scanned.getScheme(), GoogleAuthInfo.SCHEME_EXPORT)) { GoogleAuthInfo.Export export = GoogleAuthInfo.parseExportUri(scanned); for (GoogleAuthInfo info: export.getEntries()) { VaultEntry entry = new VaultEntry(info); entries.add(entry); } googleAuthExports.add(export); } else { GoogleAuthInfo info = GoogleAuthInfo.parseUri(res.getResult().getText()); VaultEntry entry = new VaultEntry(info); entries.add(entry); } } catch (GoogleAuthInfoException e) { errors.add(buildImportError(res.getFileName(), e)); } } final DialogInterface.OnClickListener dialogDismissHandler = (dialog, which) -> importScannedEntries(entries); if (!googleAuthExports.isEmpty()) { boolean isSingleBatch = GoogleAuthInfo.Export.isSingleBatch(googleAuthExports); if (!isSingleBatch && errors.size() > 0) { errors.add(getString(R.string.unrelated_google_auth_batches_error)); Dialogs.showMultiErrorDialog(this, R.string.import_error_title, getString(R.string.no_tokens_can_be_imported), errors, null); return; } else if (!isSingleBatch) { Dialogs.showErrorDialog(this, R.string.import_google_auth_failure, getString(R.string.unrelated_google_auth_batches_error)); return; } else { List missingIndices = GoogleAuthInfo.Export.getMissingIndices(googleAuthExports); if (missingIndices.size() != 0) { Dialogs.showPartialGoogleAuthImportWarningDialog(this, missingIndices, entries.size(), errors, dialogDismissHandler); return; } } } if ((errors.size() > 0 && results.size() > 1) || errors.size() > 1) { Dialogs.showMultiErrorDialog(this, R.string.import_error_title, getString(R.string.unable_to_read_qrcode_files, uris.size() - errors.size(), uris.size()), errors, dialogDismissHandler); } else if (errors.size() > 0) { Dialogs.showErrorDialog(this, getString(R.string.unable_to_read_qrcode_file, results.get(0).getFileName()), errors.get(0), dialogDismissHandler); } else { importScannedEntries(entries); } }); task.execute(getLifecycle(), uris); } private void importScannedEntries(List entries) { if (entries.size() == 1) { startEditEntryActivityForNew(entries.get(0)); } else if (entries.size() > 1) { for (VaultEntry entry: entries) { _vaultManager.getVault().addEntry(entry); _entryListView.addEntry(entry); } if (saveAndBackupVault()) { Toast.makeText(this, getResources().getQuantityString(R.plurals.added_new_entries, entries.size(), entries.size()), Toast.LENGTH_LONG).show(); } } } private void updateSortCategoryMenu() { SortCategory category = _prefs.getCurrentSortCategory(); _menu.findItem(category.getMenuItem()).setChecked(true); } private void onIntroResult() { loadEntries(); } private void checkTimeSyncSetting() { boolean autoTime = Settings.Global.getInt(getContentResolver(), Settings.Global.AUTO_TIME, 1) == 1; if (!autoTime && _prefs.isTimeSyncWarningEnabled()) { Dialogs.showTimeSyncWarningDialog(this, (dialog, which) -> { Intent intent = new Intent(Settings.ACTION_DATE_SETTINGS); startActivity(intent); }); } } private void onDecryptResult() { _auditLogRepository.addVaultUnlockedEvent(); loadEntries(); } private void startScanActivity() { if (!PermissionHelper.request(this, CODE_PERM_CAMERA, Manifest.permission.CAMERA)) { return; } Intent scannerActivity = new Intent(getApplicationContext(), ScannerActivity.class); scanResultLauncher.launch(scannerActivity); } private void startScanImageActivity() { Intent galleryIntent = new Intent(Intent.ACTION_PICK); galleryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*"); Intent fileIntent = new Intent(Intent.ACTION_GET_CONTENT); fileIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); fileIntent.setType("image/*"); Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_picture)); chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { fileIntent }); _vaultManager.fireIntentLauncher(this, chooserIntent, codeScanResultLauncher); } private void startPreferencesActivity() { startPreferencesActivity(null, null); } private void startPreferencesActivity(Class fragmentType, String preference) { Intent intent = new Intent(this, PreferencesActivity.class); intent.putExtra("fragment", fragmentType); intent.putExtra("pref", preference); preferenceResultLauncher.launch(intent); } private void doShortcutActions() { Intent intent = getIntent(); String action = intent.getStringExtra("action"); if (action == null || !_vaultManager.isVaultLoaded()) { return; } switch (action) { case "scan": startScanActivity(); break; } intent.removeExtra("action"); } private void handleIncomingIntent() { if (!_vaultManager.isVaultLoaded()) { return; } Intent intent = getIntent(); if (intent.getAction() == null) { return; } Uri uri; switch (intent.getAction()) { case Intent.ACTION_VIEW: uri = intent.getData(); if (uri != null) { intent.setData(null); intent.setAction(null); GoogleAuthInfo info; try { info = GoogleAuthInfo.parseUri(uri); } catch (GoogleAuthInfoException e) { e.printStackTrace(); Dialogs.showErrorDialog(this, R.string.unable_to_process_deeplink, e); break; } VaultEntry entry = new VaultEntry(info); startEditEntryActivityForNew(entry); } break; case Intent.ACTION_SEND: if (intent.hasExtra(Intent.EXTRA_STREAM)) { uri = intent.getParcelableExtra(Intent.EXTRA_STREAM); intent.setAction(null); intent.removeExtra(Intent.EXTRA_STREAM); if (uri != null) { startDecodeQrCodeImages(Collections.singletonList(uri)); } } if (intent.hasExtra(Intent.EXTRA_TEXT)) { String stringExtra = intent.getStringExtra(Intent.EXTRA_TEXT); intent.setAction(null); intent.removeExtra(Intent.EXTRA_TEXT); if (stringExtra != null) { GoogleAuthInfo info; try { info = GoogleAuthInfo.parseUri(stringExtra); } catch (GoogleAuthInfoException e) { Dialogs.showErrorDialog(this, R.string.unable_to_process_shared_text, e); break; } VaultEntry entry = new VaultEntry(info); startEditEntryActivityForNew(entry); } } break; case Intent.ACTION_SEND_MULTIPLE: if (intent.hasExtra(Intent.EXTRA_STREAM)) { List uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); intent.setAction(null); intent.removeExtra(Intent.EXTRA_STREAM); if (uris != null) { uris = uris.stream() .filter(Objects::nonNull) .collect(Collectors.toList()); startDecodeQrCodeImages(uris); } } break; } } @Override protected void onStart() { super.onStart(); if (_vaultManager.isVaultInitNeeded()) { if (_prefs.isIntroDone()) { Toast.makeText(this, getString(R.string.vault_not_found), Toast.LENGTH_SHORT).show(); } startIntroActivity(); return; } // If the vault is not loaded yet, try to load it now in case it's plain text if (!_vaultManager.isVaultLoaded()) { VaultFile vaultFile; try { vaultFile = VaultRepository.readVaultFile(this); } catch (VaultRepositoryException e) { e.printStackTrace(); Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> { finish(); }); return; } if (!vaultFile.isEncrypted()) { try { _vaultManager.loadFrom(vaultFile); } catch (VaultRepositoryException e) { e.printStackTrace(); Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> { finish(); }); return; } } } if (!_vaultManager.isVaultLoaded()) { startAuthActivity(false); } else if (_loaded) { // update the list of groups in the entry list view so that the chip gets updated setGroups(_vaultManager.getVault().getUsedGroups()); // update the usage counts in case they are edited outside of the EntryListView _entryListView.setUsageCounts(_prefs.getUsageCounts()); _entryListView.setLastUsedTimestamps(_prefs.getLastUsedTimestamps()); // refresh all codes to prevent showing old ones _entryListView.refresh(false); } else { loadEntries(); checkTimeSyncSetting(); } _lockBackPressHandler.setEnabled( _vaultManager.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_BACK_BUTTON) ); handleIncomingIntent(); updateLockIcon(); doShortcutActions(); updateErrorCard(); } private void deleteEntries(List entries) { for (VaultEntry entry: entries) { VaultEntry oldEntry = _vaultManager.getVault().removeEntry(entry); _entryListView.removeEntry(oldEntry); } saveAndBackupVault(); } @Override public boolean onCreateOptionsMenu(Menu menu) { _menu = menu; getMenuInflater().inflate(R.menu.menu_main, menu); updateLockIcon(); if (_loaded) { setGroups(_vaultManager.getVault().getUsedGroups()); updateSortCategoryMenu(); } MenuItem searchViewMenuItem = menu.findItem(R.id.mi_search); _searchView = (SearchView) searchViewMenuItem.getActionView(); _searchView.setMaxWidth(Integer.MAX_VALUE); _searchView.setOnQueryTextFocusChangeListener((v, hasFocus) -> { boolean enabled = _submittedSearchQuery != null || hasFocus; _searchViewBackPressHandler.setEnabled(enabled); }); _searchView.setOnCloseListener(() -> { boolean enabled = _submittedSearchQuery != null; _searchViewBackPressHandler.setEnabled(enabled); return false; }); _searchView.setQueryHint(getString(R.string.search)); _searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String s) { setTitle(getString(R.string.search)); getSupportActionBar().setSubtitle(s); _entryListView.setSearchFilter(s); _pendingSearchQuery = null; _submittedSearchQuery = s; collapseSearchView(); _searchViewBackPressHandler.setEnabled(true); return false; } @Override public boolean onQueryTextChange(String s) { if (_submittedSearchQuery == null) { _entryListView.setSearchFilter(s); } _pendingSearchQuery = Strings.isNullOrEmpty(s) && !_searchView.isIconified() ? null : s; if (_pendingSearchQuery != null) { _entryListView.setSearchFilter(_pendingSearchQuery); } return false; } }); _searchView.setOnSearchClickListener(v -> { String query = _submittedSearchQuery != null ? _submittedSearchQuery : _pendingSearchQuery; _searchView.setQuery(query, false); }); if (_pendingSearchQuery != null) { _searchView.setIconified(false); _searchView.setQuery(_pendingSearchQuery, false); _searchViewBackPressHandler.setEnabled(true); } else if (_submittedSearchQuery != null) { setTitle(getString(R.string.search)); getSupportActionBar().setSubtitle(_submittedSearchQuery); _entryListView.setSearchFilter(_submittedSearchQuery); _searchViewBackPressHandler.setEnabled(true); } else if (_prefs.getFocusSearchEnabled() && !_isRecreated) { _searchView.setIconified(false); _searchView.setFocusable(true); _searchView.requestFocus(); _searchView.requestFocusFromTouch(); } return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); if (itemId == R.id.action_settings) { startPreferencesActivity(); } else if (itemId == R.id.action_about) { Intent intent = new Intent(this, AboutActivity.class); startActivity(intent); } else if (itemId == R.id.action_lock) { _vaultManager.lock(true); } else { if (item.getGroupId() == R.id.action_sort_category) { item.setChecked(true); SortCategory sortCategory; int subItemId = item.getItemId(); if (subItemId == R.id.menu_sort_alphabetically) { sortCategory = SortCategory.ISSUER; } else if (subItemId == R.id.menu_sort_alphabetically_reverse) { sortCategory = SortCategory.ISSUER_REVERSED; } else if (subItemId == R.id.menu_sort_alphabetically_name) { sortCategory = SortCategory.ACCOUNT; } else if (subItemId == R.id.menu_sort_alphabetically_name_reverse) { sortCategory = SortCategory.ACCOUNT_REVERSED; } else if (subItemId == R.id.menu_sort_usage_count) { sortCategory = SortCategory.USAGE_COUNT; } else if (subItemId == R.id.menu_sort_last_used) { sortCategory = SortCategory.LAST_USED; } else { sortCategory = SortCategory.CUSTOM; } _entryListView.setSortCategory(sortCategory, true); _prefs.setCurrentSortCategory(sortCategory); } return super.onOptionsItemSelected(item); } return true; } private void collapseSearchView() { _searchView.setQuery(null, false); _searchView.setIconified(true); } private void loadEntries() { if (!_loaded) { setGroups(_vaultManager.getVault().getUsedGroups()); _entryListView.setUsageCounts(_prefs.getUsageCounts()); _entryListView.setLastUsedTimestamps(_prefs.getLastUsedTimestamps()); _entryListView.addEntries(_vaultManager.getVault().getEntries()); if (!_isRecreated) { _entryListView.runEntriesAnimation(); } _loaded = true; } } private void startAuthActivity(boolean inhibitBioPrompt) { if (!_isAuthenticating) { Intent intent = new Intent(this, AuthActivity.class); intent.putExtra("inhibitBioPrompt", inhibitBioPrompt); authResultLauncher.launch(intent); _isAuthenticating = true; } } private void updateLockIcon() { // hide the lock icon if the vault is not unlocked if (_menu != null && _vaultManager.isVaultLoaded()) { MenuItem item = _menu.findItem(R.id.action_lock); item.setVisible(_vaultManager.getVault().isEncryptionEnabled()); } } private void updateErrorCard() { ErrorCardInfo info = null; Preferences.BackupResult backupRes = _prefs.getErroredBackupResult(); if (backupRes != null) { info = new ErrorCardInfo(getString(R.string.backup_error_bar_message), view -> { Dialogs.showBackupErrorDialog(this, backupRes, (dialog, which) -> { startPreferencesActivity(BackupsPreferencesFragment.class, "pref_backups"); }); }); } else if (_prefs.isBackupsReminderNeeded() && _prefs.isBackupReminderEnabled()) { String text; Date date = _prefs.getLatestBackupOrExportTime(); if (date != null) { text = getString(R.string.backup_reminder_bar_message_with_latest, TimeUtils.getElapsedSince(this, date)); } else { text = getString(R.string.backup_reminder_bar_message); } info = new ErrorCardInfo(text, view -> { Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Error) .setTitle(R.string.backup_reminder_bar_dialog_title) .setMessage(R.string.backup_reminder_bar_dialog_summary) .setIconAttribute(android.R.attr.alertDialogIcon) .setPositiveButton(R.string.backup_reminder_bar_dialog_accept, (dialog, whichButton) -> { startPreferencesActivity(BackupsPreferencesFragment.class, "pref_backups"); }) .setNegativeButton(android.R.string.cancel, null) .create()); }); } else if (_prefs.isPlaintextBackupWarningNeeded()) { info = new ErrorCardInfo(getString(R.string.backup_plaintext_export_warning), view -> showPlaintextExportWarningOptions()); } _entryListView.setErrorCardInfo(info); } private void showPlaintextExportWarningOptions() { View view = LayoutInflater.from(this).inflate(R.layout.dialog_plaintext_warning, null); AlertDialog dialog = new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning) .setTitle(R.string.backup_plaintext_export_warning) .setView(view) .setIconAttribute(android.R.attr.alertDialogIcon) .setPositiveButton(android.R.string.ok, null) .setNegativeButton(android.R.string.cancel, null) .create(); CheckBox checkBox = view.findViewById(R.id.checkbox_plaintext_warning); checkBox.setChecked(false); dialog.setOnShowListener(d -> { Button btnPos = dialog.getButton(AlertDialog.BUTTON_POSITIVE); btnPos.setOnClickListener(l -> { dialog.dismiss(); _prefs.setIsPlaintextBackupWarningDisabled(checkBox.isChecked()); _prefs.setIsPlaintextBackupWarningNeeded(false); updateErrorCard(); }); }); Dialogs.showSecureDialog(dialog); } @Override public void onRestoreInstanceState(@Nullable Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); if (savedInstanceState == null) { return; } HashSet filter = (HashSet) savedInstanceState.getSerializable("prefGroupFilter"); if (filter != null) { _prefGroupFilter = filter; } } @Override public void onEntryClick(VaultEntry entry) { if (_actionMode != null) { if (_selectedEntries.isEmpty()) { _actionMode.finish(); } else { setFavoriteMenuItemVisiblity(); setIsMultipleSelected(_selectedEntries.size() > 1); } } } @Override public void onSelect(VaultEntry entry) { _selectedEntries.add(entry); } @Override public void onDeselect(VaultEntry entry) { _selectedEntries.remove(entry); } private void setIsMultipleSelected(boolean multipleSelected) { _entryListView.setIsLongPressDragEnabled(!multipleSelected); _actionMode.getMenu().findItem(R.id.action_edit).setVisible(!multipleSelected); _actionMode.getMenu().findItem(R.id.action_copy).setVisible(!multipleSelected); } private void setAssignIconsMenuItemVisibility() { MenuItem assignIconsMenuItem = _actionMode.getMenu().findItem(R.id.action_assign_icons); assignIconsMenuItem.setVisible(_iconPackManager.hasIconPack()); } private void setFavoriteMenuItemVisiblity() { MenuItem toggleFavoriteMenuItem = _actionMode.getMenu().findItem(R.id.action_toggle_favorite); if (_selectedEntries.size() == 1){ if (_selectedEntries.get(0).isFavorite()) { toggleFavoriteMenuItem.setIcon(R.drawable.ic_filled_star_24); toggleFavoriteMenuItem.setTitle(R.string.unfavorite); } else { toggleFavoriteMenuItem.setIcon(R.drawable.ic_outline_star_24); toggleFavoriteMenuItem.setTitle(R.string.favorite); } } else { toggleFavoriteMenuItem.setIcon(R.drawable.ic_outline_star_24); toggleFavoriteMenuItem.setTitle(String.format("%s / %s", getString(R.string.favorite), getString(R.string.unfavorite))); } } @Override public void onLongEntryClick(VaultEntry entry) { if (!_selectedEntries.isEmpty()) { return; } _selectedEntries.add(entry); _entryListView.setActionModeState(true, entry); startActionMode(); } private void startActionMode() { _actionMode = startSupportActionMode(_actionModeCallbacks); _actionModeBackPressHandler.setEnabled(true); setFavoriteMenuItemVisiblity(); setAssignIconsMenuItemVisibility(); } @Override public void onEntryMove(VaultEntry entry1, VaultEntry entry2) { _vaultManager.getVault().moveEntry(entry1, entry2); } @Override public void onEntryDrop(VaultEntry entry) { saveVault(); } @Override public void onEntryChange(VaultEntry entry) { saveAndBackupVault(); } public void onEntryCopy(VaultEntry entry) { copyEntryCode(entry); } @Override public void onScroll(int dx, int dy) { if (!_isDPadPressed) { _fabScrollHelper.onScroll(dx, dy); } } @Override public void onListChange() { _fabScrollHelper.setVisible(true); } @Override public void onSaveGroupFilter(Set groupFilter) { if (_vaultManager.getVault().isGroupsMigrationFresh()) { saveAndBackupVault(); } _prefs.setGroupFilter(groupFilter); } @Override public void onLocked(boolean userInitiated) { if (_actionMode != null) { _actionMode.finish(); } if (_searchView != null && !_searchView.isIconified()) { collapseSearchView(); } _entryListView.clearEntries(); _loaded = false; if (userInitiated) { startAuthActivity(true); } else { super.onLocked(false); } } @SuppressLint("InlinedApi") private void copyEntryCode(VaultEntry entry) { String otp; try { otp = entry.getInfo().getOtp(); } catch (OtpInfoException e) { return; } ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText("text/plain", otp); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { PersistableBundle extras = new PersistableBundle(); extras.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true); clip.getDescription().setExtras(extras); } clipboard.setPrimaryClip(clip); if (_prefs.isMinimizeOnCopyEnabled()) { moveTaskToBack(true); } } private class SearchViewBackPressHandler extends OnBackPressedCallback { public SearchViewBackPressHandler() { super(false); } @Override public void handleOnBackPressed() { if (!_searchView.isIconified() || _submittedSearchQuery != null) { _submittedSearchQuery = null; _pendingSearchQuery = null; _entryListView.setSearchFilter(null); collapseSearchView(); setTitle(R.string.app_name); getSupportActionBar().setSubtitle(null); } } } private class LockBackPressHandler extends OnBackPressedCallback { public LockBackPressHandler() { super(false); } @Override public void handleOnBackPressed() { if (_vaultManager.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_BACK_BUTTON)) { _vaultManager.lock(false); } } } private class ActionModeBackPressHandler extends OnBackPressedCallback { public ActionModeBackPressHandler() { super(false); } @Override public void handleOnBackPressed() { if (_actionMode != null) { _actionMode.finish(); } } } private class ActionModeCallbacks implements ActionMode.Callback { @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu_action_mode, menu); return true; } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return false; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { if (_selectedEntries.size() == 0) { mode.finish(); return true; } int itemId = item.getItemId(); if (itemId == R.id.action_copy) { copyEntryCode(_selectedEntries.get(0)); mode.finish(); } else if (itemId == R.id.action_edit) { startEditEntryActivity(_selectedEntries.get(0)); mode.finish(); } else if (itemId == R.id.action_toggle_favorite) { for (VaultEntry entry : _selectedEntries) { entry.setIsFavorite(!entry.isFavorite()); _entryListView.replaceEntry(entry.getUUID(), entry); } _entryListView.refresh(true); saveAndBackupVault(); mode.finish(); } else if (itemId == R.id.action_share_qr) { Intent intent = new Intent(getBaseContext(), TransferEntriesActivity.class); ArrayList authInfos = new ArrayList<>(); for (VaultEntry entry : _selectedEntries) { GoogleAuthInfo authInfo = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer()); authInfos.add(authInfo); _auditLogRepository.addEntrySharedEvent(entry.getUUID().toString()); } intent.putExtra("authInfos", authInfos); startActivity(intent); mode.finish(); } else if (itemId == R.id.action_delete) { Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries, (d, which) -> { deleteEntries(_selectedEntries); _entryListView.setGroups(_vaultManager.getVault().getUsedGroups()); mode.finish(); }); } else if (itemId == R.id.action_select_all) { _selectedEntries = _entryListView.selectAllEntries(); setFavoriteMenuItemVisiblity(); setIsMultipleSelected(_selectedEntries.size() > 1); } else if (itemId == R.id.action_assign_icons) { startAssignIconsActivity(_selectedEntries); mode.finish(); } else if (itemId == R.id.action_assign_groups) { startAssignGroupsDialog(); } else { return false; } return true; } @Override public void onDestroyActionMode(ActionMode mode) { _entryListView.setActionModeState(false, null); _actionModeBackPressHandler.setEnabled(false); _selectedEntries.clear(); _actionMode = null; } } }