diff --git a/CHANGELOG b/CHANGELOG index db2e8b4d3..970fc7563 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,15 @@ +KeePassDX(3.3.0) + * Quick search and dynamic filters #163 #462 #521 + * Keep search context #1141 + * Add searchable groups #905 #1006 + * Search with regular expression #175 + * Merge from file and save as copy #1221 #1204 #840 + * Fix custom data #1236 + * Fix education hints #1192 + * Fix save and app instance in selection mode + * New UI and fix styles + * Add "Simple" and "Reply" themes + KeePassDX(3.2.0) * Manage data merge #840 #977 * Manage Tags #633 diff --git a/app/build.gradle b/app/build.gradle index dd8c95f4b..540967035 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.kunzisoft.keepass" minSdkVersion 15 targetSdkVersion 31 - versionCode = 97 - versionName = "3.2.0" + versionCode = 102 + versionName = "3.3.0" multiDexEnabled true testApplicationId = "com.kunzisoft.keepass.tests" @@ -69,10 +69,14 @@ android { buildConfigField "boolean", "FULL_VERSION", "false" buildConfigField "boolean", "CLOSED_STORE", "true" buildConfigField "String[]", "STYLES_DISABLED", - "{\"KeepassDXStyle_Blue\"," + + "{\"KeepassDXStyle_Simple\"," + + "\"KeepassDXStyle_Simple_Night\"," + + "\"KeepassDXStyle_Blue\"," + "\"KeepassDXStyle_Blue_Night\"," + "\"KeepassDXStyle_Red\"," + "\"KeepassDXStyle_Red_Night\"," + + "\"KeepassDXStyle_Reply\"," + + "\"KeepassDXStyle_Reply_Night\"," + "\"KeepassDXStyle_Purple\"," + "\"KeepassDXStyle_Purple_Dark\"}" buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}" @@ -104,17 +108,18 @@ def room_version = "2.4.1" dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation "com.android.support:multidex:1.0.3" implementation "androidx.appcompat:appcompat:$android_appcompat_version" - implementation 'androidx.preference:preference-ktx:1.1.1' + implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.3' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.biometric:biometric:1.1.0' - implementation 'androidx.media:media:1.4.3' + implementation 'androidx.media:media:1.5.0' // Lifecycle - LiveData - ViewModel - Coroutines implementation "androidx.core:core-ktx:$android_core_version" - implementation 'androidx.fragment:fragment-ktx:1.4.0' + implementation 'androidx.fragment:fragment-ktx:1.4.1' implementation "com.google.android.material:material:$android_material_version" // Token auto complete // From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fef19294a..ac25f88fe 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,7 +40,6 @@ diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt index 10edbbba9..2f5118359 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -66,7 +66,6 @@ import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager import com.kunzisoft.keepass.timeout.TimeoutHelper -import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UuidUtil import com.kunzisoft.keepass.view.changeControlColor @@ -92,6 +91,8 @@ class EntryActivity : DatabaseLockActivity() { private val mEntryViewModel: EntryViewModel by viewModels() + private val mEntryActivityEducation = EntryActivityEducation(this) + private var mMainEntryId: NodeId? = null private var mHistoryPosition: Int = -1 private var mEntryIsHistory: Boolean = false @@ -370,7 +371,6 @@ class EntryActivity : DatabaseLockActivity() { super.onCreateOptionsMenu(menu) if (mEntryLoaded) { val inflater = menuInflater - MenuUtil.contributionMenuInflater(inflater, menu) inflater.inflate(R.menu.entry, menu) inflater.inflate(R.menu.database, menu) @@ -381,11 +381,7 @@ class EntryActivity : DatabaseLockActivity() { // Show education views Handler(Looper.getMainLooper()).post { - performedNextEducation( - EntryActivityEducation( - this - ), menu - ) + performedNextEducation(menu) } } return true @@ -411,31 +407,30 @@ class EntryActivity : DatabaseLockActivity() { return super.onPrepareOptionsMenu(menu) } - private fun performedNextEducation(entryActivityEducation: EntryActivityEducation, - menu: Menu) { + private fun performedNextEducation(menu: Menu) { val entryFragment = supportFragmentManager.findFragmentByTag(ENTRY_FRAGMENT_TAG) as? EntryFragment? val entryFieldCopyView: View? = entryFragment?.firstEntryFieldCopyView() val entryCopyEducationPerformed = entryFieldCopyView != null - && entryActivityEducation.checkAndPerformedEntryCopyEducation( + && mEntryActivityEducation.checkAndPerformedEntryCopyEducation( entryFieldCopyView, { entryFragment.launchEntryCopyEducationAction() }, { - performedNextEducation(entryActivityEducation, menu) + performedNextEducation(menu) }) if (!entryCopyEducationPerformed) { val menuEditView = toolbar?.findViewById(R.id.menu_edit) // entryEditEducationPerformed - menuEditView != null && entryActivityEducation.checkAndPerformedEntryEditEducation( + menuEditView != null && mEntryActivityEducation.checkAndPerformedEntryEditEducation( menuEditView, { onOptionsItemSelected(menu.findItem(R.id.menu_edit)) }, { - performedNextEducation(entryActivityEducation, menu) + performedNextEducation(menu) } ) } @@ -443,10 +438,6 @@ class EntryActivity : DatabaseLockActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.menu_contribute -> { - MenuUtil.onContributionItemSelected(this) - return true - } R.id.menu_edit -> { mDatabase?.let { database -> mMainEntryId?.let { entryId -> diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt index 3afc45150..ee789d4a5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -113,7 +113,7 @@ class EntryEditActivity : DatabaseLockActivity(), private var mExternalFileHelper: ExternalFileHelper? = null private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null // Education - private var entryEditActivityEducation: EntryEditActivityEducation? = null + private var mEntryEditActivityEducation = EntryEditActivityEducation(this) private var mIconSelectionActivityResultLauncher = IconPickerActivity.registerIconSelectionForResult(this) { icon -> mEntryEditViewModel.selectIcon(icon) @@ -183,8 +183,6 @@ class EntryEditActivity : DatabaseLockActivity(), } mAttachmentFileBinderManager = AttachmentFileBinderManager(this) - // Verify the education views - entryEditActivityEducation = EntryEditActivityEducation(this) // Lock button lockView?.setOnClickListener { lockAndExit() } @@ -538,10 +536,8 @@ class EntryEditActivity : DatabaseLockActivity(), super.onCreateOptionsMenu(menu) if (mEntryLoaded) { menuInflater.inflate(R.menu.entry_edit, menu) - entryEditActivityEducation?.let { - Handler(Looper.getMainLooper()).post { - performedNextEducation(it) - } + Handler(Looper.getMainLooper()).post { + performedNextEducation() } } return true @@ -568,19 +564,19 @@ class EntryEditActivity : DatabaseLockActivity(), return super.onPrepareOptionsMenu(menu) } - private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) { + private fun performedNextEducation() { val entryEditFragment = supportFragmentManager.findFragmentById(R.id.entry_edit_content) as? EntryEditFragment? val generatePasswordView = entryEditFragment?.getActionImageView() val generatePasswordEductionPerformed = generatePasswordView != null - && entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation( + && mEntryEditActivityEducation.checkAndPerformedGeneratePasswordEducation( generatePasswordView, { entryEditFragment.launchGeneratePasswordEductionAction() }, { - performedNextEducation(entryEditActivityEducation) + performedNextEducation() } ) @@ -589,33 +585,33 @@ class EntryEditActivity : DatabaseLockActivity(), val addNewFieldEducationPerformed = mAllowCustomFields && addNewFieldView != null && addNewFieldView.isVisible - && entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation( + && mEntryEditActivityEducation.checkAndPerformedEntryNewFieldEducation( addNewFieldView, { addNewCustomField() }, { - performedNextEducation(entryEditActivityEducation) + performedNextEducation() } ) if (!addNewFieldEducationPerformed) { val attachmentView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_attachment) val addAttachmentEducationPerformed = attachmentView != null && attachmentView.isVisible - && entryEditActivityEducation.checkAndPerformedAttachmentEducation( + && mEntryEditActivityEducation.checkAndPerformedAttachmentEducation( attachmentView, { addNewAttachment() }, { - performedNextEducation(entryEditActivityEducation) + performedNextEducation() } ) if (!addAttachmentEducationPerformed) { val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp) setupOtpView != null && setupOtpView.isVisible - && entryEditActivityEducation.checkAndPerformedSetUpOTPEducation( + && mEntryEditActivityEducation.checkAndPerformedSetUpOTPEducation( setupOtpView, { setupOtp() @@ -662,8 +658,8 @@ class EntryEditActivity : DatabaseLockActivity(), override fun acceptPassword(passwordField: Field) { mEntryEditViewModel.selectPassword(passwordField) - entryEditActivityEducation?.let { - Handler(Looper.getMainLooper()).post { performedNextEducation(it) } + Handler(Looper.getMainLooper()).post { + performedNextEducation() } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt index 446a9e127..d9e2ba675 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt @@ -42,7 +42,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator import com.google.android.material.snackbar.Snackbar import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment +import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.SpecialMode @@ -69,7 +69,7 @@ import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel import java.io.FileNotFoundException class FileDatabaseSelectActivity : DatabaseModeActivity(), - AssignMasterKeyDialogFragment.AssignPasswordDialogListener { + SetMainCredentialDialogFragment.AssignMainCredentialDialogListener { // Views private lateinit var coordinatorLayout: CoordinatorLayout @@ -78,6 +78,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), private val databaseFilesViewModel: DatabaseFilesViewModel by viewModels() + private val mFileDatabaseSelectActivityEducation = FileDatabaseSelectActivityEducation(this) + // Adapter to manage database history list private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null @@ -124,7 +126,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri -> mDatabaseFileUri = databaseFileCreatedUri if (mDatabaseFileUri != null) { - AssignMasterKeyDialogFragment.getInstance(true) + SetMainCredentialDialogFragment.getInstance(true) .show(supportFragmentManager, "passwordDialog") } else { val error = getString(R.string.error_create_database) @@ -132,7 +134,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), Log.e(TAG, error) } } - openDatabaseButtonView = findViewById(R.id.open_keyfile_button) + openDatabaseButtonView = findViewById(R.id.open_database_button) openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper) // History list @@ -291,7 +293,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), } private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) { - PasswordActivity.launch(this, + MainCredentialActivity.launch(this, databaseUri, keyFile, { exception -> @@ -392,39 +394,40 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), MenuUtil.defaultMenuInflater(menuInflater, menu) } - Handler(Looper.getMainLooper()).post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) } + Handler(Looper.getMainLooper()).post { + performedNextEducation() + } return true } - private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) { + private fun performedNextEducation() { // If no recent files val createDatabaseEducationPerformed = createDatabaseButtonView != null && createDatabaseButtonView!!.visibility == View.VISIBLE - && mAdapterDatabaseHistory != null - && mAdapterDatabaseHistory!!.itemCount == 0 - && fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation( + && mFileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation( createDatabaseButtonView!!, { createNewFile() }, { // But if the user cancel, it can also select a database - performedNextEducation(fileDatabaseSelectActivityEducation) + performedNextEducation() }) if (!createDatabaseEducationPerformed) { // selectDatabaseEducationPerformed openDatabaseButtonView != null - && fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation( - openDatabaseButtonView!!, - { tapTargetView -> - tapTargetView?.let { - mExternalFileHelper?.openDocument() - } - }, - {} - ) + && mFileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation( + openDatabaseButtonView!!, + { tapTargetView -> + tapTargetView?.let { + mExternalFileHelper?.openDocument() + } + }, + { + + }) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt index 0391243dd..5b14b5a37 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -26,6 +26,7 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.graphics.PorterDuff +import android.net.Uri import android.os.* import android.util.Log import android.view.Menu @@ -36,30 +37,38 @@ import android.widget.* import androidx.activity.result.ActivityResultLauncher import androidx.activity.viewModels import androidx.annotation.RequiresApi +import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.GravityCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.isVisible +import androidx.drawerlayout.widget.DrawerLayout import androidx.recyclerview.widget.RecyclerView import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.* import com.kunzisoft.keepass.activities.fragments.GroupFragment import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper +import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.adapters.BreadcrumbAdapter -import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeId +import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.search.SearchHelper +import com.kunzisoft.keepass.database.search.SearchParameters import com.kunzisoft.keepass.education.GroupActivityEducation import com.kunzisoft.keepass.model.GroupInfo +import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK @@ -67,14 +76,16 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion. import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.settings.SettingsActivity import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.timeout.TimeoutHelper -import com.kunzisoft.keepass.utils.MenuUtil +import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.view.* import com.kunzisoft.keepass.viewmodels.GroupEditViewModel import com.kunzisoft.keepass.viewmodels.GroupViewModel import org.joda.time.DateTime + class GroupActivity : DatabaseLockActivity(), DatePickerDialog.OnDateSetListener, TimePickerDialog.OnTimeSetListener, @@ -82,21 +93,22 @@ class GroupActivity : DatabaseLockActivity(), GroupFragment.NodesActionMenuListener, GroupFragment.OnScrollListener, GroupFragment.GroupRefreshedListener, - SortDialogFragment.SortSelectionListener { + SortDialogFragment.SortSelectionListener, + MainCredentialDialogFragment.AskMainCredentialDialogListener { // Views - private var rootContainerView: ViewGroup? = null + private var drawerLayout: DrawerLayout? = null + private var databaseNavView: NavigationDatabaseView? = null private var coordinatorLayout: CoordinatorLayout? = null private var lockView: View? = null private var toolbar: Toolbar? = null private var databaseNameContainer: ViewGroup? = null + private var databaseModifiedView: ImageView? = null private var databaseColorView: ImageView? = null private var databaseNameView: TextView? = null - private var searchContainer: ViewGroup? = null - private var searchNumbers: TextView? = null - private var searchString: TextView? = null + private var searchView: SearchView? = null + private var searchFiltersView: SearchFiltersView? = null private var toolbarBreadcrumb: Toolbar? = null - private var searchTitleView: View? = null private var toolbarAction: ToolbarAction? = null private var numberChildrenView: TextView? = null private var addNodeButtonView: AddNodeButtonView? = null @@ -106,8 +118,12 @@ class GroupActivity : DatabaseLockActivity(), private val mGroupViewModel: GroupViewModel by viewModels() private val mGroupEditViewModel: GroupEditViewModel by viewModels() + private val mGroupActivityEducation = GroupActivityEducation(this) + private var mBreadcrumbAdapter: BreadcrumbAdapter? = null + private var mSearchMenuItem: MenuItem? = null + private var mGroupFragment: GroupFragment? = null private var mRecyclingBinEnabled = false private var mRecyclingBinIsCurrentGroup = false @@ -115,15 +131,88 @@ class GroupActivity : DatabaseLockActivity(), private var actionNodeMode: ActionMode? = null - // Nodes - private var mCurrentGroupState: GroupState? = null - private var mRootGroup: Group? = null - private var mCurrentGroup: Group? = null + // Manage merge + private var mExternalFileHelper: ExternalFileHelper? = null + + // Manage group + private var mSearchState: SearchState? = null + private var mMainGroupState: GroupState? = null // Group state, not a search + private var mRootGroup: Group? = null // Root group in the tree + private var mMainGroup: Group? = null // Main group currently in memory + private var mCurrentGroup: Group? = null // Group currently visible (search or main group) private var mPreviousGroupsIds = mutableListOf() private var mOldGroupToUpdate: Group? = null - private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = null - private var mOnSuggestionListener: SearchView.OnSuggestionListener? = null + private var mLockSearchListeners = false + private val mOnSearchQueryTextListener = object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + onQueryTextChange(query) + // Collapse the search filters + searchFiltersView?.closeAdvancedFilters() + // Close the keyboard + WindowInsetsControllerCompat(window, window.decorView) + .hide(WindowInsetsCompat.Type.ime()) + return true + } + + override fun onQueryTextChange(newText: String?): Boolean { + if (newText != null && !mLockSearchListeners) { + mSearchState?.let { searchState -> + searchState.searchParameters.searchQuery = newText + loadSearchGroup(searchState) + } + } + return true + } + } + private val mOnSearchFiltersChangeListener = object : ((SearchParameters) -> Unit) { + override fun invoke(searchParameters: SearchParameters) { + mSearchState?.let { searchState -> + searchParameters.searchQuery = searchState.searchParameters.searchQuery + searchState.searchParameters = searchParameters + loadSearchGroup(searchState) + } + } + } + private val mOnSearchActionExpandListener = object : MenuItem.OnActionExpandListener { + override fun onMenuItemActionExpand(p0: MenuItem?): Boolean { + searchFiltersView?.visibility = View.VISIBLE + searchView?.setOnQueryTextListener(mOnSearchQueryTextListener) + searchFiltersView?.onParametersChangeListener = mOnSearchFiltersChangeListener + + addSearch() + //loadGroup() + return true + } + + override fun onMenuItemActionCollapse(p0: MenuItem?): Boolean { + searchFiltersView?.onParametersChangeListener = null + searchView?.setOnQueryTextListener(null) + searchFiltersView?.visibility = View.GONE + + removeSearch() + loadGroup() + return true + } + } + + private fun addSearch() { + finishNodeAction() + if (mSearchState == null) { + mSearchState = SearchState(searchFiltersView?.searchParameters + ?: SearchParameters(), 0) + } + } + + private fun removeSearch() { + finishNodeAction() + mSearchState = null + intent.removeExtra(AUTO_SEARCH_KEY) + if (Intent.ACTION_SEARCH == intent.action) { + intent.action = Intent.ACTION_DEFAULT + intent.removeExtra(SearchManager.QUERY) + } + } private var mIconSelectionActivityResultLauncher = IconPickerActivity.registerIconSelectionForResult(this) { icon -> // To create tree dialog for icon @@ -142,19 +231,18 @@ class GroupActivity : DatabaseLockActivity(), setContentView(layoutInflater.inflate(R.layout.activity_group, null)) // Initialize views - rootContainerView = findViewById(R.id.activity_group_container_view) + drawerLayout = findViewById(R.id.drawer_layout) + databaseNavView = findViewById(R.id.database_nav_view) coordinatorLayout = findViewById(R.id.group_coordinator) numberChildrenView = findViewById(R.id.group_numbers) addNodeButtonView = findViewById(R.id.add_node_button) toolbar = findViewById(R.id.toolbar) databaseNameContainer = findViewById(R.id.database_name_container) + databaseModifiedView = findViewById(R.id.database_modified) databaseColorView = findViewById(R.id.database_color) databaseNameView = findViewById(R.id.database_name) - searchContainer = findViewById(R.id.search_container) - searchNumbers = findViewById(R.id.search_numbers) - searchString = findViewById(R.id.search_string) + searchFiltersView = findViewById(R.id.search_filters) toolbarBreadcrumb = findViewById(R.id.toolbar_breadcrumb) - searchTitleView = findViewById(R.id.search_title) breadcrumbListView = findViewById(R.id.breadcrumb_list) toolbarAction = findViewById(R.id.toolbar_action) lockView = findViewById(R.id.lock_button) @@ -166,12 +254,66 @@ class GroupActivity : DatabaseLockActivity(), toolbar?.title = "" setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + val toggle = ActionBarDrawerToggle( + this, drawerLayout, toolbar, + R.string.navigation_drawer_open, R.string.navigation_drawer_close + ) + drawerLayout?.addDrawerListener(toggle) + toggle.syncState() + + // Manage 'merge from" and "save to" + mExternalFileHelper = ExternalFileHelper(this) + mExternalFileHelper?.buildOpenDocument { uri -> + launchDialogToAskMainCredential(uri) + } + mExternalFileHelper?.buildCreateDocument("application/x-keepass") { uri -> + uri?.let { + saveDatabaseTo(it) + } + } + + // Menu in drawer + databaseNavView?.apply { + inflateMenu(R.menu.settings) + inflateMenu(R.menu.database_extra) + inflateMenu(R.menu.about) + setNavigationItemSelectedListener { menuItem -> + when (menuItem.itemId) { + R.id.menu_app_settings -> { + // To avoid flickering when launch settings in a LockingActivity + SettingsActivity.launch(this@GroupActivity, true) + } + R.id.menu_merge_from -> { + mExternalFileHelper?.openDocument() + } + R.id.menu_save_copy_to -> { + mExternalFileHelper?.createDocument( + getString(R.string.database_file_name_default) + + getString(R.string.database_file_name_copy) + + mDatabase?.defaultFileExtension) + } + R.id.menu_lock_all -> { + lockAndExit() + } + R.id.menu_contribute -> { + UriUtil.gotoUrl(this@GroupActivity, R.string.contribution_url) + } + R.id.menu_about -> { + startActivity(Intent(this@GroupActivity, AboutActivity::class.java)) + } + } + false + } + } + + searchFiltersView?.closeAdvancedFilters() mBreadcrumbAdapter = BreadcrumbAdapter(this).apply { // Open group on breadcrumb click onItemClickListener = { node, _ -> // If last item & not a virtual root group - val currentGroup = mCurrentGroup + val currentGroup = mMainGroup if (currentGroup != null && node == currentGroup && (currentGroup != mDatabase?.rootGroup || mDatabase?.rootGroupIsVirtual == false) @@ -188,7 +330,7 @@ class GroupActivity : DatabaseLockActivity(), } } onLongItemClickListener = { node, position -> - val currentGroup = mCurrentGroup + val currentGroup = mMainGroup if (currentGroup != null && node == currentGroup && (currentGroup != mDatabase?.rootGroup || mDatabase?.rootGroupIsVirtual == false) @@ -244,94 +386,42 @@ class GroupActivity : DatabaseLockActivity(), GROUP_FRAGMENT_TAG ).commit() - // Observe group + // Observe main group + mGroupViewModel.mainGroup.observe(this) { + val mainGroup = it.group + mMainGroup = mainGroup + mRecyclingBinIsCurrentGroup = it.isRecycleBin + // Save group state + mMainGroupState = GroupState(mainGroup.nodeId, it.showFromPosition) + // Update last access time. + mainGroup.touch(modified = false, touchParents = false) + } + + // Observe current group (main or search group visible) mGroupViewModel.group.observe(this) { val currentGroup = it.group - mCurrentGroup = currentGroup - mRecyclingBinIsCurrentGroup = it.isRecycleBin + if (currentGroup.isVirtual) { + val searchParameters = it.searchParameters + mSearchState = SearchState(searchParameters, it.showFromPosition) + } + // Main and search groups in activity are managed with another variables + // to keep values during orientation - if (!currentGroup.isVirtual) { - // Save group id if real group - mCurrentGroupState = GroupState(currentGroup.nodeId, it.showFromPosition) - - // Update last access time. - currentGroup.touch(modified = false, touchParents = false) - - // Add listeners to the add buttons - addNodeButtonView?.setAddGroupClickListener { - launchDialogForGroupCreation(currentGroup) - } - addNodeButtonView?.setAddEntryClickListener { - mDatabase?.let { database -> - EntrySelectionHelper.doSpecialAction(intent, - { - mCurrentGroup?.nodeId?.let { currentParentGroupId -> - mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher -> - EntryEditActivity.launchToCreate( - this@GroupActivity, - database, - currentParentGroupId, - resultLauncher - ) - } - } - }, - { - // Search not used - }, - { searchInfo -> - EntryEditActivity.launchToCreateForSave( - this@GroupActivity, - database, - currentGroup.nodeId, - searchInfo - ) - onLaunchActivitySpecialMode() - }, - { searchInfo -> - EntryEditActivity.launchForKeyboardSelectionResult( - this@GroupActivity, - database, - currentGroup.nodeId, - searchInfo - ) - onLaunchActivitySpecialMode() - }, - { searchInfo, autofillComponent -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - EntryEditActivity.launchForAutofillResult( - this@GroupActivity, - database, - mAutofillActivityResultLauncher, - autofillComponent, - currentGroup.nodeId, - searchInfo - ) - onLaunchActivitySpecialMode() - } else { - onCancelSpecialMode() - } - }, - { searchInfo -> - EntryEditActivity.launchToCreateForRegistration( - this@GroupActivity, - database, - currentGroup.nodeId, - searchInfo - ) - onLaunchActivitySpecialMode() - } - ) - } - } + // Expand the search view if defined in settings + if (mRequestStartupSearch + && PreferencesUtil.automaticallyFocusSearch(this@GroupActivity)) { + // To request search only one time + mRequestStartupSearch = false + mSearchMenuItem?.expandActionView() } loadingView?.hideByFading() } mGroupViewModel.firstPositionVisible.observe(this) { firstPositionVisible -> - mCurrentGroupState?.firstVisibleItem = firstPositionVisible + mSearchState?.firstVisibleItem = firstPositionVisible + mMainGroupState?.firstVisibleItem = firstPositionVisible } mGroupEditViewModel.requestIconSelection.observe(this) { iconImage -> @@ -358,44 +448,125 @@ class GroupActivity : DatabaseLockActivity(), mGroupEditViewModel.onGroupCreated.observe(this) { groupInfo -> if (groupInfo.title.isNotEmpty()) { - mCurrentGroup?.let { currentGroup -> - createGroup(currentGroup, groupInfo) + mMainGroup?.let { parentGroup -> + createGroup(parentGroup, groupInfo) } } } mGroupEditViewModel.onGroupUpdated.observe(this) { groupInfo -> if (groupInfo.title.isNotEmpty()) { - mOldGroupToUpdate?.let { oldGroupToUpdate -> - updateGroup(oldGroupToUpdate, groupInfo) + groupInfo.id?.let { groupId -> + mDatabase?.getGroupById(NodeIdUUID(groupId))?.let { oldGroupToUpdate -> + updateGroup(oldGroupToUpdate, groupInfo) + } + } + } + } + + // Add listeners to the add buttons + addNodeButtonView?.setAddGroupClickListener { + mMainGroup?.let { currentGroup -> + launchDialogForGroupCreation(currentGroup) + } + } + addNodeButtonView?.setAddEntryClickListener { + mDatabase?.let { database -> + mMainGroup?.let { currentGroup -> + EntrySelectionHelper.doSpecialAction(intent, + { + mMainGroup?.nodeId?.let { currentParentGroupId -> + mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher -> + EntryEditActivity.launchToCreate( + this@GroupActivity, + database, + currentParentGroupId, + resultLauncher + ) + } + } + }, + { + // Search not used + }, + { searchInfo -> + EntryEditActivity.launchToCreateForSave( + this@GroupActivity, + database, + currentGroup.nodeId, + searchInfo + ) + onLaunchActivitySpecialMode() + }, + { searchInfo -> + EntryEditActivity.launchForKeyboardSelectionResult( + this@GroupActivity, + database, + currentGroup.nodeId, + searchInfo + ) + onLaunchActivitySpecialMode() + }, + { searchInfo, autofillComponent -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + EntryEditActivity.launchForAutofillResult( + this@GroupActivity, + database, + mAutofillActivityResultLauncher, + autofillComponent, + currentGroup.nodeId, + searchInfo + ) + onLaunchActivitySpecialMode() + } else { + onCancelSpecialMode() + } + }, + { searchInfo -> + EntryEditActivity.launchToCreateForRegistration( + this@GroupActivity, + database, + currentGroup.nodeId, + searchInfo + ) + onLaunchActivitySpecialMode() + } + ) } } } } override fun viewToInvalidateTimeout(): View? { - return rootContainerView + return drawerLayout } - private fun loadGroup(database: Database?) { + private fun loadMainGroup(groupState: GroupState) { + mGroupViewModel.loadMainGroup(mDatabase, + groupState.groupId, + groupState.firstVisibleItem) + } + + private fun loadSearchGroup(searchState: SearchState) { + mGroupViewModel.loadSearchGroup(mDatabase, + searchState.searchParameters, + mMainGroupState?.groupId, + searchState.firstVisibleItem) + } + + private fun loadGroup() { + val searchState = mSearchState + val currentGroupState = mMainGroupState when { - Intent.ACTION_SEARCH == intent.action -> { + searchState != null -> { finishNodeAction() - val searchString = - intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: "" - mGroupViewModel.loadGroupFromSearch( - database, - searchString, - PreferencesUtil.omitBackup(this) - ) + loadSearchGroup(searchState) } - mCurrentGroupState == null -> { - mRootGroup?.let { rootGroup -> - mGroupViewModel.loadGroup(database, rootGroup, 0) - } + currentGroupState != null -> { + loadMainGroup(currentGroupState) } else -> { - mGroupViewModel.loadGroup(database, mCurrentGroupState) + loadMainGroup(GroupState(null, 0)) } } } @@ -409,12 +580,28 @@ class GroupActivity : DatabaseLockActivity(), && database?.isRecycleBinEnabled == true mRootGroup = database?.rootGroup - loadGroup(database) + loadGroup() - // Search suggestion + // Update view database?.let { - databaseNameView?.text = if (it.name.isNotEmpty()) it.name else getString(R.string.database) + mBreadcrumbAdapter?.iconDrawableFactory = it.iconDrawableFactory + } + refreshDatabaseViews() + invalidateOptionsMenu() + } + + private fun refreshDatabaseViews() { + mDatabase?.let { + val databaseName = it.name.ifEmpty { getString(R.string.database) } + databaseNavView?.setDatabaseName(databaseName) + databaseNameView?.text = databaseName + databaseNavView?.setDatabasePath(it.fileUri?.toString()) + databaseNavView?.setDatabaseVersion(it.version) + val modified = it.dataModifiedSinceLastLoading + databaseNavView?.setDatabaseModifiedSinceLastLoading(modified) + databaseModifiedView?.isVisible = modified val customColor = it.customColor + databaseNavView?.setDatabaseColor(customColor) if (customColor != null) { databaseColorView?.visibility = View.VISIBLE databaseColorView?.setColorFilter( @@ -424,25 +611,7 @@ class GroupActivity : DatabaseLockActivity(), } else { databaseColorView?.visibility = View.GONE } - mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, it) - mBreadcrumbAdapter?.iconDrawableFactory = it.iconDrawableFactory - mOnSuggestionListener = object : SearchView.OnSuggestionListener { - override fun onSuggestionClick(position: Int): Boolean { - mSearchSuggestionAdapter?.let { searchAdapter -> - searchAdapter.getEntryFromPosition(position)?.let { entry -> - onNodeClick(database, entry) - } - } - return true - } - - override fun onSuggestionSelect(position: Int): Boolean { - return true - } - } } - - invalidateOptionsMenu() } override fun onDatabaseActionFinished( @@ -503,7 +672,7 @@ class GroupActivity : DatabaseLockActivity(), ACTION_DATABASE_UPDATE_GROUP_TASK -> { if (result.isSuccess) { try { - if (mCurrentGroup == newNodes[0] as Group) + if (mMainGroup == newNodes[0] as Group) reloadCurrentGroup() } catch (e: Exception) { Log.e( @@ -540,12 +709,21 @@ class GroupActivity : DatabaseLockActivity(), private fun manageIntent(intent: Intent?) { intent?.let { if (intent.extras?.containsKey(GROUP_STATE_KEY) == true) { - mCurrentGroupState = intent.getParcelableExtra(GROUP_STATE_KEY) + mMainGroupState = intent.getParcelableExtra(GROUP_STATE_KEY) intent.removeExtra(GROUP_STATE_KEY) } // To transform KEY_SEARCH_INFO in ACTION_SEARCH transformSearchInfoIntent(intent) - loadGroup(mDatabase) + // Get search query + if (intent.action == Intent.ACTION_SEARCH) { + val stringQuery = intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: "" + intent.action = Intent.ACTION_DEFAULT + intent.removeExtra(SearchManager.QUERY) + mSearchState = SearchState(SearchParameters().apply { + searchQuery = stringQuery + }, mSearchState?.firstVisibleItem ?: 0) + } + loadGroup() } } @@ -566,36 +744,24 @@ class GroupActivity : DatabaseLockActivity(), } override fun onGroupRefreshed() { - mCurrentGroup?.let { currentGroup -> - assignGroupViewElements(currentGroup) - } - } - - private fun assignGroupViewElements(group: Group?) { + val group = mCurrentGroup // Assign title if (group?.isVirtual == true) { - searchContainer?.visibility = View.VISIBLE - val title = group.title - searchString?.text = if (title.isNotEmpty()) title else "" - searchNumbers?.text = group.numberOfChildEntries.toString() - databaseNameContainer?.visibility = View.GONE - toolbarBreadcrumb?.navigationIcon = null - toolbarBreadcrumb?.collapse() + searchFiltersView?.setNumbers(group.numberOfChildEntries) + searchFiltersView?.setCurrentGroupText(mMainGroup?.title ?: "") + searchFiltersView?.availableOther(mDatabase?.allowEntryCustomFields() ?: false) + searchFiltersView?.availableTags(mDatabase?.allowTags() ?: false) + searchFiltersView?.enableTags(mDatabase?.tagPool?.isNotEmpty() ?: false) + searchFiltersView?.availableSearchableGroup(mDatabase?.allowCustomSearchableGroup() ?: false) + searchFiltersView?.availableTemplates(mDatabase?.allowTemplatesGroup ?: false) + searchFiltersView?.enableTemplates(mDatabase?.templatesGroup != null) } else { - searchContainer?.visibility = View.GONE - databaseNameContainer?.visibility = View.VISIBLE - // Refresh breadcrumb - if (toolbarBreadcrumb?.isVisible != true) { - toolbarBreadcrumb?.expand { - setBreadcrumbNode(group) - } - } else { - // Add breadcrumb - setBreadcrumbNode(group) - } + // Add breadcrumb + setBreadcrumbNode(group) + refreshDatabaseViews() + invalidateOptionsMenu() } initAddButton(group) - invalidateOptionsMenu() } private fun setBreadcrumbNode(group: Group?) { @@ -638,12 +804,12 @@ class GroupActivity : DatabaseLockActivity(), val group = node as Group // Save the last not virtual group and it's position if (mCurrentGroup?.isVirtual == false) { - mCurrentGroupState?.let { + mMainGroupState?.let { mPreviousGroupsIds.add(it) } } // Open child group - mGroupViewModel.loadGroup(database, group, 0) + loadMainGroup(GroupState(group.nodeId, 0)) } catch (e: ClassCastException) { Log.e(TAG, "Node can't be cast in Group") @@ -672,29 +838,13 @@ class GroupActivity : DatabaseLockActivity(), finish() }, { searchInfo -> - // Recheck search, only to fix #783 because workflow allows to open multiple search elements - SearchHelper.checkAutoSearchInfo(this, - database, - searchInfo, - { openedDatabase, _ -> - // Item in search, don't save - entrySelectedForKeyboardSelection(openedDatabase, entryVersioned) - }, - { - // Item not found, save it if required - if (!database.isReadOnly - && searchInfo != null - && PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity) - ) { - updateEntryWithSearchInfo(database, entryVersioned, searchInfo) - } - entrySelectedForKeyboardSelection(database, entryVersioned) - }, - { - // Normally not append - finish() - } - ) + if (!database.isReadOnly + && searchInfo != null + && PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity) + ) { + updateEntryWithSearchInfo(database, entryVersioned, searchInfo) + } + entrySelectedForKeyboardSelection(database, entryVersioned) }, { searchInfo, _ -> if (!database.isReadOnly @@ -776,14 +926,16 @@ class GroupActivity : DatabaseLockActivity(), searchInfo: SearchInfo ) { val newEntry = Entry(entry) - newEntry.setEntryInfo(database, newEntry.getEntryInfo( + val entryInfo = newEntry.getEntryInfo( database, raw = true, removeTemplateConfiguration = false - ).apply { - saveSearchInfo(database, searchInfo) - }) - updateEntry(entry, newEntry) + ) + val modification = entryInfo.saveSearchInfo(database, searchInfo) + newEntry.setEntryInfo(database, entryInfo) + if (modification) { + updateEntry(entry, newEntry) + } } override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) { @@ -886,6 +1038,11 @@ class GroupActivity : DatabaseLockActivity(), .show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP) } + private fun launchDialogToAskMainCredential(uri: Uri?) { + MainCredentialDialogFragment.getInstance(uri) + .show(supportFragmentManager, MainCredentialDialogFragment.TAG_ASK_MAIN_CREDENTIAL) + } + override fun onCopyMenuClick( database: Database, nodes: List @@ -914,13 +1071,13 @@ class GroupActivity : DatabaseLockActivity(), when (pasteMode) { GroupFragment.PasteMode.PASTE_FROM_COPY -> { // Copy - mCurrentGroup?.let { newParent -> + mMainGroup?.let { newParent -> copyNodes(nodes, newParent) } } GroupFragment.PasteMode.PASTE_FROM_MOVE -> { // Move - mCurrentGroup?.let { newParent -> + mMainGroup?.let { newParent -> moveNodes(nodes, newParent) } } @@ -941,6 +1098,16 @@ class GroupActivity : DatabaseLockActivity(), return true } + override fun onAskMainCredentialDialogPositiveClick(databaseUri: Uri?, + mainCredential: MainCredential) { + databaseUri?.let { + mergeDatabaseFrom(it, mainCredential) + } + } + + override fun onAskMainCredentialDialogNegativeClick(databaseUri: Uri?, + mainCredential: MainCredential) { } + override fun onResume() { super.onResume() @@ -950,8 +1117,6 @@ class GroupActivity : DatabaseLockActivity(), } else { View.GONE } - // Refresh suggestions to change preferences - mSearchSuggestionAdapter?.reInit(this) // Padding if lock button visible toolbarAction?.updateLockPaddingLeft() } @@ -960,6 +1125,26 @@ class GroupActivity : DatabaseLockActivity(), super.onPause() finishNodeAction() + searchView?.setOnQueryTextListener(null) + } + + private fun addSearchQueryInSearchView(searchQuery: String) { + searchView?.setOnQueryTextListener(null) + searchView?.setQuery(searchQuery, false) + searchView?.setOnQueryTextListener(mOnSearchQueryTextListener) + } + + private fun prepareDatabaseNavMenu() { + // hide or show nav menu + databaseNavView?.apply { + // depending on current mode + val modeCondition = mSpecialMode == SpecialMode.DEFAULT + menu.findItem(R.id.menu_app_settings)?.isVisible = modeCondition + menu.findItem(R.id.menu_merge_from)?.isVisible = mMergeDataAllowed && modeCondition + menu.findItem(R.id.menu_save_copy_to)?.isVisible = modeCondition + menu.findItem(R.id.menu_about)?.isVisible = modeCondition + menu.findItem(R.id.menu_contribute)?.isVisible = modeCondition + } } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -974,125 +1159,127 @@ class GroupActivity : DatabaseLockActivity(), if (!mMergeDataAllowed) { menu.findItem(R.id.menu_merge_database)?.isVisible = false } - if (mSpecialMode == SpecialMode.DEFAULT) { - MenuUtil.defaultMenuInflater(inflater, menu) - } else { + if (mSpecialMode != SpecialMode.DEFAULT) { menu.findItem(R.id.menu_merge_database)?.isVisible = false menu.findItem(R.id.menu_reload_database)?.isVisible = false } - // Menu for recycle bin if (mRecyclingBinEnabled && mRecyclingBinIsCurrentGroup) { inflater.inflate(R.menu.recycle_bin, menu) } - // Get the SearchView and set the searchable configuration - val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager? + prepareDatabaseNavMenu() + // Get the SearchView and set the searchable configuration menu.findItem(R.id.menu_search)?.let { - val searchView = it.actionView as SearchView? + mLockSearchListeners = true + mSearchMenuItem = it + it.setOnActionExpandListener(mOnSearchActionExpandListener) + searchView = it.actionView as SearchView? searchView?.apply { + val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager? (searchManager?.getSearchableInfo( ComponentName(this@GroupActivity, GroupActivity::class.java) ))?.let { searchableInfo -> setSearchableInfo(searchableInfo) } - setIconifiedByDefault(false) // Do not iconify the widget; expand it by default - suggestionsAdapter = mSearchSuggestionAdapter - setOnSuggestionListener(mOnSuggestionListener) + val searchState = mSearchState + // already open + if (searchState != null) { + it.expandActionView() + addSearchQueryInSearchView(searchState.searchParameters.searchQuery) + searchFiltersView?.searchParameters = searchState.searchParameters + } } - // Expand the search view if defined in settings - if (mRequestStartupSearch - && PreferencesUtil.automaticallyFocusSearch(this@GroupActivity) - ) { - // To request search only one time - mRequestStartupSearch = false - it.expandActionView() + if (it.isActionViewExpanded) { + toolbarBreadcrumb?.visibility = View.GONE + searchFiltersView?.visibility = View.VISIBLE + } else { + searchFiltersView?.visibility = View.GONE + toolbarBreadcrumb?.visibility = View.VISIBLE } + mLockSearchListeners = false } super.onCreateOptionsMenu(menu) // Launch education screen Handler(Looper.getMainLooper()).post { - performedNextEducation( - GroupActivityEducation(this), - menu - ) + performedNextEducation(menu) } return true } - private fun performedNextEducation( - groupActivityEducation: GroupActivityEducation, - menu: Menu - ) { + private fun performedNextEducation(menu: Menu) { // If no node, show education to add new one val addNodeButtonEducationPerformed = actionNodeMode == null && addNodeButtonView?.addButtonView != null && addNodeButtonView!!.isEnable - && groupActivityEducation.checkAndPerformedAddNodeButtonEducation( + && mGroupActivityEducation.checkAndPerformedAddNodeButtonEducation( addNodeButtonView?.addButtonView!!, { addNodeButtonView?.openButtonIfClose() }, { - performedNextEducation(groupActivityEducation, menu) + performedNextEducation(menu) } ) if (!addNodeButtonEducationPerformed) { val searchMenuEducationPerformed = toolbar != null && toolbar!!.findViewById(R.id.menu_search) != null - && groupActivityEducation.checkAndPerformedSearchMenuEducation( + && mGroupActivityEducation.checkAndPerformedSearchMenuEducation( toolbar!!.findViewById(R.id.menu_search), { menu.findItem(R.id.menu_search).expandActionView() }, { - performedNextEducation(groupActivityEducation, menu) + performedNextEducation(menu) }) if (!searchMenuEducationPerformed) { val sortMenuEducationPerformed = toolbar != null && toolbar!!.findViewById(R.id.menu_sort) != null - && groupActivityEducation.checkAndPerformedSortMenuEducation( + && mGroupActivityEducation.checkAndPerformedSortMenuEducation( toolbar!!.findViewById(R.id.menu_sort), { onOptionsItemSelected(menu.findItem(R.id.menu_sort)) }, { - performedNextEducation(groupActivityEducation, menu) + performedNextEducation(menu) }) if (!sortMenuEducationPerformed) { // lockMenuEducationPerformed val lockButtonView = findViewById(R.id.lock_button) lockButtonView != null - && groupActivityEducation.checkAndPerformedLockMenuEducation( + && mGroupActivityEducation.checkAndPerformedLockMenuEducation( lockButtonView, { lockAndExit() }, { - performedNextEducation(groupActivityEducation, menu) + performedNextEducation(menu) }) } } } } + override fun hideHomeButtonIfModeIsNotDefault(): Boolean { + return false + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { android.R.id.home -> { - // TODO change database + drawerLayout?.openDrawer(GravityCompat.START) return true } R.id.menu_search -> { - //onSearchRequested() return true } R.id.menu_save_database -> { @@ -1109,7 +1296,7 @@ class GroupActivity : DatabaseLockActivity(), } R.id.menu_empty_recycle_bin -> { if (mRecyclingBinEnabled && mRecyclingBinIsCurrentGroup) { - mCurrentGroup?.getChildren()?.let { listChildren -> + mMainGroup?.getChildren()?.let { listChildren -> // Automatically delete all elements deleteNodes(listChildren, true) finishNodeAction() @@ -1118,8 +1305,6 @@ class GroupActivity : DatabaseLockActivity(), return true } else -> { - // Check the time lock before launching settings - MenuUtil.onDefaultMenuOptionsItemSelected(this, item, true) return super.onOptionsItemSelected(item) } } @@ -1148,23 +1333,9 @@ class GroupActivity : DatabaseLockActivity(), } } - private fun removeSearch() { - intent.removeExtra(AUTO_SEARCH_KEY) - if (Intent.ACTION_SEARCH == intent.action) { - intent.action = Intent.ACTION_DEFAULT - intent.removeExtra(SearchManager.QUERY) - } - } - private fun reloadCurrentGroup() { - // Remove search in intent removeSearch() - // Reload real group - try { - mGroupViewModel.loadGroup(mDatabase, mCurrentGroupState) - } catch (e: Exception) { - Log.e(TAG, "Unable to rebuild the group", e) - } + loadGroup() } override fun onBackPressed() { @@ -1183,7 +1354,7 @@ class GroupActivity : DatabaseLockActivity(), } else -> { // Load the previous group - mGroupViewModel.loadGroup(mDatabase, mPreviousGroupsIds.removeLast()) + loadMainGroup(mPreviousGroupsIds.removeLast()) } } } @@ -1202,6 +1373,35 @@ class GroupActivity : DatabaseLockActivity(), } } + data class SearchState( + var searchParameters: SearchParameters, + var firstVisibleItem: Int? + ) : Parcelable { + + private constructor(parcel: Parcel) : this( + parcel.readParcelable + (SearchParameters::class.java.classLoader) ?: SearchParameters(), + parcel.readInt() + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeParcelable(searchParameters, flags) + parcel.writeInt(firstVisibleItem ?: 0) + } + + override fun describeContents() = 0 + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): SearchState { + return SearchState(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } + data class GroupState( var groupId: NodeId<*>?, var firstVisibleItem: Int? diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt similarity index 72% rename from app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt rename to app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt index 79277962c..cb17b3df6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -26,14 +26,14 @@ import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper -import android.text.Editable -import android.text.TextWatcher import android.util.Log -import android.view.* -import android.view.KeyEvent.KEYCODE_ENTER -import android.view.inputmethod.EditorInfo.IME_ACTION_DONE -import android.view.inputmethod.InputMethodManager -import android.widget.* +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.CompoundButton +import android.widget.TextView import androidx.activity.result.ActivityResultLauncher import androidx.activity.viewModels import androidx.annotation.RequiresApi @@ -44,10 +44,11 @@ import androidx.fragment.app.commit import com.google.android.material.snackbar.Snackbar import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog -import com.kunzisoft.keepass.activities.helpers.* +import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper +import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper +import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity -import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment @@ -55,11 +56,9 @@ import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException import com.kunzisoft.keepass.education.PasswordActivityEducation -import com.kunzisoft.keepass.model.MainCredential -import com.kunzisoft.keepass.model.RegisterInfo -import com.kunzisoft.keepass.model.SearchInfo +import com.kunzisoft.keepass.model.* import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK -import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY @@ -68,23 +67,20 @@ import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.UriUtil -import com.kunzisoft.keepass.view.KeyFileSelectionView +import com.kunzisoft.keepass.view.MainCredentialView import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel import java.io.FileNotFoundException -class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener { +class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener { // Views private var toolbar: Toolbar? = null private var filenameView: TextView? = null - private var passwordView: EditText? = null - private var keyFileSelectionView: KeyFileSelectionView? = null + private var mainCredentialView: MainCredentialView? = null private var confirmButtonView: Button? = null - private var checkboxPasswordView: CompoundButton? = null - private var checkboxKeyFileView: CompoundButton? = null private var infoContainerView: ViewGroup? = null private lateinit var coordinatorLayout: CoordinatorLayout private var advancedUnlockFragment: AdvancedUnlockFragment? = null @@ -92,9 +88,10 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels() private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels() + private val mPasswordActivityEducation = PasswordActivityEducation(this) + private var mDefaultDatabase: Boolean = false private var mDatabaseFileUri: Uri? = null - private var mDatabaseKeyFileUri: Uri? = null private var mRememberKeyFile: Boolean = false private var mExternalFileHelper: ExternalFileHelper? = null @@ -110,7 +107,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_password) + setContentView(R.layout.activity_main_credential) toolbar = findViewById(R.id.toolbar) toolbar?.title = getString(R.string.app_name) @@ -118,12 +115,9 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true) - confirmButtonView = findViewById(R.id.activity_password_open_button) filenameView = findViewById(R.id.filename) - passwordView = findViewById(R.id.password) - keyFileSelectionView = findViewById(R.id.keyfile_selection) - checkboxPasswordView = findViewById(R.id.password_checkbox) - checkboxKeyFileView = findViewById(R.id.keyfile_checkox) + mainCredentialView = findViewById(R.id.activity_password_credentials) + confirmButtonView = findViewById(R.id.activity_password_open_button) infoContainerView = findViewById(R.id.activity_password_info_container) coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout) @@ -134,41 +128,19 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL } mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this) - mExternalFileHelper = ExternalFileHelper(this@PasswordActivity) + mExternalFileHelper = ExternalFileHelper(this@MainCredentialActivity) mExternalFileHelper?.buildOpenDocument { uri -> if (uri != null) { - mDatabaseKeyFileUri = uri - populateKeyFileTextView(uri) + mainCredentialView?.populateKeyFileTextView(uri) } } - keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper) - - passwordView?.setOnEditorActionListener(onEditorActionListener) - passwordView?.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} - - override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} - - override fun afterTextChanged(editable: Editable) { - if (editable.toString().isNotEmpty() && checkboxPasswordView?.isChecked != true) - checkboxPasswordView?.isChecked = true - } - }) - passwordView?.setOnKeyListener { _, _, keyEvent -> - var handled = false - if (keyEvent.action == KeyEvent.ACTION_DOWN - && keyEvent?.keyCode == KEYCODE_ENTER) { - verifyCheckboxesAndLoadDatabase() - handled = true - } - handled + mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper) + mainCredentialView?.onValidateListener = { + loadDatabase() } // If is a view intent getUriFromIntent(intent) - if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) { - mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE)) - } // Init Biometric elements advancedUnlockFragment = supportFragmentManager @@ -183,10 +155,11 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL } // Listen password checkbox to init advanced unlock and confirmation button - checkboxPasswordView?.setOnCheckedChangeListener { _, _ -> - mAdvancedUnlockViewModel.checkUnlockAvailability() - enableOrNotTheConfirmationButton() - } + mainCredentialView?.onPasswordChecked = + CompoundButton.OnCheckedChangeListener { _, _ -> + mAdvancedUnlockViewModel.checkUnlockAvailability() + enableConfirmationButton() + } // Observe if default database mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase -> @@ -211,12 +184,13 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL invalidateOptionsMenu() // Post init uri with KeyFile only if needed + val databaseKeyFileUri = mainCredentialView?.getMainCredential()?.keyFileUri val keyFileUri = if (mRememberKeyFile - && (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) { + && (databaseKeyFileUri == null || databaseKeyFileUri.toString().isEmpty())) { databaseFile?.keyFileUri } else { - mDatabaseKeyFileUri + databaseKeyFileUri } // Define title @@ -229,10 +203,10 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL override fun onResume() { super.onResume() - mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@PasswordActivity) + mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity) // Back to previous keyboard is setting activated - if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@PasswordActivity)) { + if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) { sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION)) } @@ -271,7 +245,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL if (result.isSuccess) { launchGroupActivityIfLoaded(database) } else { - passwordView?.requestFocusFromTouch() + mainCredentialView?.requestPasswordFocus() var resultError = "" val resultException = result.exception @@ -288,7 +262,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL var databaseUri: Uri? = null var mainCredential = MainCredential() var readOnly = true - var cipherEntity: CipherDatabaseEntity? = null + var cipherEncryptDatabase: CipherEncryptDatabase? = null result.data?.let { resultData -> databaseUri = resultData.getParcelable(DATABASE_URI_KEY) @@ -296,8 +270,8 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL resultData.getParcelable(MAIN_CREDENTIAL_KEY) ?: mainCredential readOnly = resultData.getBoolean(READ_ONLY_KEY) - cipherEntity = - resultData.getParcelable(CIPHER_ENTITY_KEY) + cipherEncryptDatabase = + resultData.getParcelable(CIPHER_DATABASE_KEY) } databaseUri?.let { databaseFileUri -> @@ -305,7 +279,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL databaseFileUri, mainCredential, readOnly, - cipherEntity, + cipherEncryptDatabase, true ) } @@ -341,11 +315,16 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL if (action != null && action == VIEW_INTENT) { mDatabaseFileUri = intent.data - mDatabaseKeyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE) + mainCredentialView?.populateKeyFileTextView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE)) } else { mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME) - mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE) + intent?.getParcelableExtra(KEY_KEYFILE)?.let { + mainCredentialView?.populateKeyFileTextView(it) + } } + try { + intent?.removeExtra(KEY_KEYFILE) + } catch (e: Exception) {} mDatabaseFileUri?.let { mDatabaseFileViewModel.checkIfIsDefaultDatabase(it) } @@ -380,51 +359,68 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL finish() } - override fun retrieveCredentialForEncryption(): String { - return passwordView?.text?.toString() ?: "" + override fun retrieveCredentialForEncryption(): ByteArray { + return mainCredentialView?.retrieveCredentialForStorage(credentialStorageListener) + ?: byteArrayOf() } override fun conditionToStoreCredential(): Boolean { - return checkboxPasswordView?.isChecked == true + return mainCredentialView?.conditionToStoreCredential() == true } - override fun onCredentialEncrypted(databaseUri: Uri, - encryptedCredential: String, - ivSpec: String) { + override fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) { // Load the database if password is registered with biometric - verifyCheckboxesAndLoadDatabase( - CipherDatabaseEntity( - databaseUri.toString(), - encryptedCredential, - ivSpec) + loadDatabase(mDatabaseFileUri, + mainCredentialView?.getMainCredential(), + cipherEncryptDatabase ) } - override fun onCredentialDecrypted(databaseUri: Uri, - decryptedCredential: String) { - // Load the database if password is retrieve from biometric - // Retrieve from biometric - verifyKeyFileCheckboxAndLoadDatabase(decryptedCredential) + private val credentialStorageListener = object: MainCredentialView.CredentialStorageListener { + override fun passwordToStore(password: String?): ByteArray? { + return password?.toByteArray() + } + + override fun keyfileToStore(keyfile: Uri?): ByteArray? { + // TODO create byte array to store keyfile + return null + } + + override fun hardwareKeyToStore(): ByteArray? { + // TODO create byte array to store hardware key + return null + } } - private val onEditorActionListener = object : TextView.OnEditorActionListener { - override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { - if (actionId == IME_ACTION_DONE) { - verifyCheckboxesAndLoadDatabase() - return true + override fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) { + // Load the database if password is retrieve from biometric + // Retrieve from biometric + val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential() + when (cipherDecryptDatabase.credentialStorage) { + CredentialStorage.PASSWORD -> { + mainCredential.masterPassword = String(cipherDecryptDatabase.decryptedValue) + } + CredentialStorage.KEY_FILE -> { + // TODO advanced unlock key file + } + CredentialStorage.HARDWARE_KEY -> { + // TODO advanced unlock hardware key } - return false } + loadDatabase(mDatabaseFileUri, + mainCredential, + null + ) } private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) { // Define Key File text if (mRememberKeyFile) { - populateKeyFileTextView(keyFileUri) + mainCredentialView?.populateKeyFileTextView(keyFileUri) } // Define listener for validate button - confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() } + confirmButtonView?.setOnClickListener { loadDatabase() } // If Activity is launch with a password and want to open directly val intent = intent @@ -433,66 +429,33 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL intent.removeExtra(KEY_PASSWORD) val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false) if (password != null) { - populatePasswordTextView(password) + mainCredentialView?.populatePasswordTextView(password) } if (launchImmediately) { - verifyCheckboxesAndLoadDatabase(password, keyFileUri) + loadDatabase() } else { // Init Biometric elements mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri) } - enableOrNotTheConfirmationButton() + enableConfirmationButton() - // Auto select the password field and open keyboard - passwordView?.postDelayed({ - passwordView?.requestFocusFromTouch() - val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager? - inputMethodManager?.showSoftInput(passwordView, InputMethodManager.SHOW_IMPLICIT) - }, 100) + mainCredentialView?.focusPasswordFieldAndOpenKeyboard() } - private fun enableOrNotTheConfirmationButton() { + private fun enableConfirmationButton() { // Enable or not the open button if setting is checked - if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) { - checkboxPasswordView?.let { - confirmButtonView?.isEnabled = (checkboxPasswordView?.isChecked == true - || checkboxKeyFileView?.isChecked == true) - } + if (!PreferencesUtil.emptyPasswordAllowed(this@MainCredentialActivity)) { + confirmButtonView?.isEnabled = mainCredentialView?.isFill() ?: false } else { confirmButtonView?.isEnabled = true } } private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) { - populatePasswordTextView(null) + mainCredentialView?.populatePasswordTextView(null) if (clearKeyFile) { - mDatabaseKeyFileUri = null - populateKeyFileTextView(null) - } - } - - private fun populatePasswordTextView(text: String?) { - if (text == null || text.isEmpty()) { - passwordView?.setText("") - if (checkboxPasswordView?.isChecked == true) - checkboxPasswordView?.isChecked = false - } else { - passwordView?.setText(text) - if (checkboxPasswordView?.isChecked != true) - checkboxPasswordView?.isChecked = true - } - } - - private fun populateKeyFileTextView(uri: Uri?) { - if (uri == null || uri.toString().isEmpty()) { - keyFileSelectionView?.uri = null - if (checkboxKeyFileView?.isChecked == true) - checkboxKeyFileView?.isChecked = false - } else { - keyFileSelectionView?.uri = uri - if (checkboxKeyFileView?.isChecked != true) - checkboxKeyFileView?.isChecked = true + mainCredentialView?.populateKeyFileTextView(null) } } @@ -504,41 +467,20 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL } override fun onSaveInstanceState(outState: Bundle) { - mDatabaseKeyFileUri?.let { - outState.putString(KEY_KEYFILE, it.toString()) - } outState.putBoolean(KEY_READ_ONLY, mReadOnly) super.onSaveInstanceState(outState) } - private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) { - val password: String? = passwordView?.text?.toString() - val keyFile: Uri? = keyFileSelectionView?.uri - verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity) - } - - private fun verifyCheckboxesAndLoadDatabase(password: String?, - keyFile: Uri?, - cipherDatabaseEntity: CipherDatabaseEntity? = null) { - val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password - verifyKeyFileCheckbox(keyFile) - loadDatabase(mDatabaseFileUri, keyPassword, mDatabaseKeyFileUri, cipherDatabaseEntity) - } - - private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) { - val keyFile: Uri? = keyFileSelectionView?.uri - verifyKeyFileCheckbox(keyFile) - loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri) - } - - private fun verifyKeyFileCheckbox(keyFile: Uri?) { - mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile + private fun loadDatabase() { + loadDatabase(mDatabaseFileUri, + mainCredentialView?.getMainCredential(), + null + ) } private fun loadDatabase(databaseFileUri: Uri?, - password: String?, - keyFileUri: Uri?, - cipherDatabaseEntity: CipherDatabaseEntity? = null) { + mainCredential: MainCredential?, + cipherEncryptDatabase: CipherEncryptDatabase?) { if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) { clearCredentialsViews() @@ -556,11 +498,12 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL databaseFileUri?.let { databaseUri -> // Show the progress dialog and load the database showProgressDialogAndLoadDatabase( - databaseUri, - MainCredential(password, keyFileUri), - mReadOnly, - cipherDatabaseEntity, - false) + databaseUri, + mainCredential ?: MainCredential(), + mReadOnly, + cipherEncryptDatabase, + false + ) } } } @@ -568,14 +511,14 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL private fun showProgressDialogAndLoadDatabase(databaseUri: Uri, mainCredential: MainCredential, readOnly: Boolean, - cipherDatabaseEntity: CipherDatabaseEntity?, + cipherEncryptDatabase: CipherEncryptDatabase?, fixDuplicateUUID: Boolean) { loadDatabase( - databaseUri, - mainCredential, - readOnly, - cipherDatabaseEntity, - fixDuplicateUUID + databaseUri, + mainCredential, + readOnly, + cipherEncryptDatabase, + fixDuplicateUUID ) } @@ -612,26 +555,27 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL if (!performedEductionInProgress) { performedEductionInProgress = true // Show education views - Handler(Looper.getMainLooper()).post { performedNextEducation(PasswordActivityEducation(this), menu) } + Handler(Looper.getMainLooper()).post { + performedNextEducation(menu) + } } } - private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation, - menu: Menu) { + private fun performedNextEducation(menu: Menu) { val educationToolbar = toolbar val unlockEducationPerformed = educationToolbar != null - && passwordActivityEducation.checkAndPerformedUnlockEducation( + && mPasswordActivityEducation.checkAndPerformedUnlockEducation( educationToolbar, { - performedNextEducation(passwordActivityEducation, menu) + performedNextEducation(menu) }, { - performedNextEducation(passwordActivityEducation, menu) + performedNextEducation(menu) }) if (!unlockEducationPerformed) { val readOnlyEducationPerformed = educationToolbar?.findViewById(R.id.menu_open_file_read_mode_key) != null - && passwordActivityEducation.checkAndPerformedReadOnlyEducation( + && mPasswordActivityEducation.checkAndPerformedReadOnlyEducation( educationToolbar.findViewById(R.id.menu_open_file_read_mode_key), { try { @@ -639,19 +583,19 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL } catch (e: Exception) { Log.e(TAG, "Unable to find read mode menu") } - performedNextEducation(passwordActivityEducation, menu) + performedNextEducation(menu) }, { - performedNextEducation(passwordActivityEducation, menu) + performedNextEducation(menu) }) - advancedUnlockFragment?.performEducation(passwordActivityEducation, + advancedUnlockFragment?.performEducation(mPasswordActivityEducation, readOnlyEducationPerformed, { - performedNextEducation(passwordActivityEducation, menu) + performedNextEducation(menu) }, { - performedNextEducation(passwordActivityEducation, menu) + performedNextEducation(menu) }) } } @@ -682,7 +626,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL companion object { - private val TAG = PasswordActivity::class.java.name + private val TAG = MainCredentialActivity::class.java.name private const val UNLOCK_FRAGMENT_TAG = "UNLOCK_FRAGMENT_TAG" @@ -696,7 +640,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?, intentBuildLauncher: (Intent) -> Unit) { - val intent = Intent(activity, PasswordActivity::class.java) + val intent = Intent(activity, MainCredentialActivity::class.java) intent.putExtra(KEY_FILENAME, databaseFile) if (keyFile != null) intent.putExtra(KEY_KEYFILE, keyFile) @@ -832,30 +776,30 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL try { EntrySelectionHelper.doSpecialAction(activity.intent, { - PasswordActivity.launch(activity, + MainCredentialActivity.launch(activity, databaseUri, keyFile) }, { searchInfo -> // Search Action - PasswordActivity.launchForSearchResult(activity, + MainCredentialActivity.launchForSearchResult(activity, databaseUri, keyFile, searchInfo) onLaunchActivitySpecialMode() }, { searchInfo -> // Save Action - PasswordActivity.launchForSaveResult(activity, + MainCredentialActivity.launchForSaveResult(activity, databaseUri, keyFile, searchInfo) onLaunchActivitySpecialMode() }, { searchInfo -> // Keyboard Selection Action - PasswordActivity.launchForKeyboardResult(activity, + MainCredentialActivity.launchForKeyboardResult(activity, databaseUri, keyFile, searchInfo) onLaunchActivitySpecialMode() }, { searchInfo, autofillComponent -> // Autofill Selection Action if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - PasswordActivity.launchForAutofillResult(activity, + MainCredentialActivity.launchForAutofillResult(activity, databaseUri, keyFile, autofillActivityResultLauncher, autofillComponent, @@ -866,7 +810,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL } }, { registerInfo -> // Registration Action - PasswordActivity.launchForRegistration(activity, + MainCredentialActivity.launchForRegistration(activity, databaseUri, keyFile, registerInfo) onLaunchActivitySpecialMode() diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupDialogFragment.kt index 8c1cb5899..c5c952842 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupDialogFragment.kt @@ -53,6 +53,10 @@ class GroupDialogFragment : DatabaseDialogFragment() { private lateinit var expirationView: DateTimeFieldView private lateinit var creationView: TextView private lateinit var modificationView: TextView + private lateinit var searchableLabelView: TextView + private lateinit var searchableView: TextView + private lateinit var autoTypeLabelView: TextView + private lateinit var autoTypeView: TextView private lateinit var uuidContainerView: ViewGroup private lateinit var uuidReferenceView: TextView @@ -62,6 +66,25 @@ class GroupDialogFragment : DatabaseDialogFragment() { database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor) } mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon) + + if (database?.allowCustomSearchableGroup() == true) { + searchableLabelView.visibility = View.VISIBLE + searchableView.visibility = View.VISIBLE + } else { + searchableLabelView.visibility = View.GONE + searchableView.visibility = View.GONE + } + + // TODO Auto-Type + /* + if (database?.allowAutoType() == true) { + autoTypeLabelView.visibility = View.VISIBLE + autoTypeView.visibility = View.VISIBLE + } else { + autoTypeLabelView.visibility = View.GONE + autoTypeView.visibility = View.GONE + } + */ } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { @@ -75,6 +98,10 @@ class GroupDialogFragment : DatabaseDialogFragment() { expirationView = root.findViewById(R.id.group_expiration) creationView = root.findViewById(R.id.group_created) modificationView = root.findViewById(R.id.group_modified) + searchableLabelView = root.findViewById(R.id.group_searchable_label) + searchableView = root.findViewById(R.id.group_searchable) + autoTypeLabelView = root.findViewById(R.id.group_auto_type_label) + autoTypeView = root.findViewById(R.id.group_auto_type) uuidContainerView = root.findViewById(R.id.group_UUID_container) uuidReferenceView = root.findViewById(R.id.group_UUID_reference) @@ -123,6 +150,9 @@ class GroupDialogFragment : DatabaseDialogFragment() { expirationView.dateTime = mGroupInfo.expiryTime creationView.text = mGroupInfo.creationTime.getDateTimeString(resources) modificationView.text = mGroupInfo.lastModificationTime.getDateTimeString(resources) + searchableView.text = stringFromInheritableBoolean(mGroupInfo.searchable) + autoTypeView.text = stringFromInheritableBoolean(mGroupInfo.enableAutoType, + mGroupInfo.defaultAutoTypeSequence) val uuid = UuidUtil.toHexString(mGroupInfo.id) if (uuid == null || uuid.isEmpty()) { uuidContainerView.visibility = View.GONE @@ -143,6 +173,15 @@ class GroupDialogFragment : DatabaseDialogFragment() { return super.onCreateDialog(savedInstanceState) } + private fun stringFromInheritableBoolean(enable: Boolean?, value: String? = null): String { + val valueString = if (value != null && value.isNotEmpty()) " [$value]" else "" + return when { + enable == null -> getString(R.string.inherited) + valueString + enable -> getString(R.string.enable) + valueString + else -> getString(R.string.disable) + } + } + override fun onSaveInstanceState(outState: Bundle) { outState.putParcelable(KEY_GROUP_INFO, mGroupInfo) super.onSaveInstanceState(outState) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt index 29de1d2c8..97483bbcd 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt @@ -23,9 +23,8 @@ import android.app.Dialog import android.graphics.Color import android.os.Bundle import android.view.View -import android.widget.Button -import android.widget.ImageView -import android.widget.TextView +import android.view.ViewGroup +import android.widget.* import androidx.appcompat.app.AlertDialog import androidx.fragment.app.activityViewModels import com.google.android.material.textfield.TextInputLayout @@ -37,6 +36,7 @@ import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.model.GroupInfo import com.kunzisoft.keepass.view.DateTimeEditFieldView +import com.kunzisoft.keepass.view.InheritedCompletionView import com.kunzisoft.keepass.view.TagsCompletionView import com.kunzisoft.keepass.viewmodels.GroupEditViewModel import com.tokenautocomplete.FilteredArrayAdapter @@ -58,6 +58,12 @@ class GroupEditDialogFragment : DatabaseDialogFragment() { private lateinit var notesTextLayoutView: TextInputLayout private lateinit var notesTextView: TextView private lateinit var expirationView: DateTimeEditFieldView + private lateinit var searchableContainerView: TextInputLayout + private lateinit var searchableView: InheritedCompletionView + private lateinit var autoTypeContainerView: ViewGroup + private lateinit var autoTypeInheritedView: InheritedCompletionView + private lateinit var autoTypeSequenceView: TextView + private lateinit var tagsContainerView: TextInputLayout private lateinit var tagsCompletionView: TagsCompletionView private var tagsAdapter: FilteredArrayAdapter? = null @@ -118,11 +124,24 @@ class GroupEditDialogFragment : DatabaseDialogFragment() { } mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon) + searchableContainerView.visibility = if (database?.allowCustomSearchableGroup() == true) { + View.VISIBLE + } else { + View.GONE + } + + if (database?.allowAutoType() == true) { + autoTypeContainerView.visibility = View.VISIBLE + } else { + autoTypeContainerView.visibility = View.GONE + } + tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool) tagsCompletionView.apply { threshold = 1 setAdapter(tagsAdapter) } + tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { @@ -134,6 +153,12 @@ class GroupEditDialogFragment : DatabaseDialogFragment() { notesTextLayoutView = root.findViewById(R.id.group_edit_note_container) notesTextView = root.findViewById(R.id.group_edit_note) expirationView = root.findViewById(R.id.group_edit_expiration) + searchableContainerView = root.findViewById(R.id.group_edit_searchable_container) + searchableView = root.findViewById(R.id.group_edit_searchable) + autoTypeContainerView = root.findViewById(R.id.group_edit_auto_type_container) + autoTypeInheritedView = root.findViewById(R.id.group_edit_auto_type_inherited) + autoTypeSequenceView = root.findViewById(R.id.group_edit_auto_type_sequence) + tagsContainerView = root.findViewById(R.id.group_tags_label) tagsCompletionView = root.findViewById(R.id.group_tags_completion_view) // Retrieve the textColor to tint the icon @@ -211,6 +236,11 @@ class GroupEditDialogFragment : DatabaseDialogFragment() { expirationView.activation = groupInfo.expires expirationView.dateTime = groupInfo.expiryTime + // Set searchable + searchableView.setValue(groupInfo.searchable) + // Set auto-type + autoTypeInheritedView.setValue(groupInfo.enableAutoType) + autoTypeSequenceView.text = groupInfo.defaultAutoTypeSequence // Set Tags groupInfo.tags.let { tags -> tagsCompletionView.setText("") @@ -229,6 +259,9 @@ class GroupEditDialogFragment : DatabaseDialogFragment() { } mGroupInfo.expires = expirationView.activation mGroupInfo.expiryTime = expirationView.dateTime + mGroupInfo.searchable = searchableView.getValue() + mGroupInfo.enableAutoType = autoTypeInheritedView.getValue() + mGroupInfo.defaultAutoTypeSequence = autoTypeSequenceView.text.toString() mGroupInfo.tags = tagsCompletionView.getTags() } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/MainCredentialDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/MainCredentialDialogFragment.kt new file mode 100644 index 000000000..d45c3ea97 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/MainCredentialDialogFragment.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2022 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePassDX. + * + * KeePassDX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePassDX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePassDX. If not, see . + * + */ +package com.kunzisoft.keepass.activities.dialogs + +import android.app.Dialog +import android.content.Context +import android.net.Uri +import android.os.Bundle +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper +import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.utils.UriUtil +import com.kunzisoft.keepass.view.MainCredentialView + +class MainCredentialDialogFragment : DatabaseDialogFragment() { + + private var mainCredentialView: MainCredentialView? = null + + private var mListener: AskMainCredentialDialogListener? = null + + private var mExternalFileHelper: ExternalFileHelper? = null + + interface AskMainCredentialDialogListener { + fun onAskMainCredentialDialogPositiveClick(databaseUri: Uri?, mainCredential: MainCredential) + fun onAskMainCredentialDialogNegativeClick(databaseUri: Uri?, mainCredential: MainCredential) + } + + override fun onAttach(activity: Context) { + super.onAttach(activity) + try { + mListener = activity as AskMainCredentialDialogListener + } catch (e: ClassCastException) { + throw ClassCastException(activity.toString() + + " must implement " + AskMainCredentialDialogListener::class.java.name) + } + } + + override fun onDetach() { + mListener = null + super.onDetach() + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + activity?.let { activity -> + + var databaseUri: Uri? = null + arguments?.apply { + if (containsKey(KEY_ASK_CREDENTIAL_URI)) + databaseUri = getParcelable(KEY_ASK_CREDENTIAL_URI) + } + + val builder = AlertDialog.Builder(activity) + + val root = activity.layoutInflater.inflate(R.layout.fragment_main_credential, null) + mainCredentialView = root.findViewById(R.id.main_credential_view) + databaseUri?.let { + root.findViewById(R.id.title_database)?.text = + UriUtil.getFileData(requireContext(), it)?.name + } + builder.setView(root) + // Add action buttons + .setPositiveButton(android.R.string.ok) { _, _ -> + mListener?.onAskMainCredentialDialogPositiveClick( + databaseUri, + retrieveMainCredential() + ) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + mListener?.onAskMainCredentialDialogNegativeClick( + databaseUri, + retrieveMainCredential() + ) + } + + + mExternalFileHelper = ExternalFileHelper(this) + mExternalFileHelper?.buildOpenDocument { uri -> + if (uri != null) { + mainCredentialView?.populateKeyFileTextView(uri) + } + } + mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper) + + return builder.create() + } + + return super.onCreateDialog(savedInstanceState) + } + + private fun retrieveMainCredential(): MainCredential { + return mainCredentialView?.getMainCredential() ?: MainCredential() + } + + companion object { + + private const val KEY_ASK_CREDENTIAL_URI = "KEY_ASK_CREDENTIAL_URI" + const val TAG_ASK_MAIN_CREDENTIAL = "TAG_ASK_MAIN_CREDENTIAL" + + fun getInstance(uri: Uri?): MainCredentialDialogFragment { + val fragment = MainCredentialDialogFragment() + val args = Bundle() + args.putParcelable(KEY_ASK_CREDENTIAL_URI, uri) + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetMainCredentialDialogFragment.kt similarity index 94% rename from app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt rename to app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetMainCredentialDialogFragment.kt index b976f1f54..b8b78c0f4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetMainCredentialDialogFragment.kt @@ -39,7 +39,7 @@ import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.view.KeyFileSelectionView -class AssignMasterKeyDialogFragment : DatabaseDialogFragment() { +class SetMainCredentialDialogFragment : DatabaseDialogFragment() { private var mMasterPassword: String? = null private var mKeyFile: Uri? = null @@ -56,7 +56,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() { private var keyFileCheckBox: CompoundButton? = null private var keyFileSelectionView: KeyFileSelectionView? = null - private var mListener: AssignPasswordDialogListener? = null + private var mListener: AssignMainCredentialDialogListener? = null private var mExternalFileHelper: ExternalFileHelper? = null @@ -74,7 +74,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() { } } - interface AssignPasswordDialogListener { + interface AssignMainCredentialDialogListener { fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) } @@ -82,10 +82,10 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() { override fun onAttach(activity: Context) { super.onAttach(activity) try { - mListener = activity as AssignPasswordDialogListener + mListener = activity as AssignMainCredentialDialogListener } catch (e: ClassCastException) { throw ClassCastException(activity.toString() - + " must implement " + AssignPasswordDialogListener::class.java.name) + + " must implement " + AssignMainCredentialDialogListener::class.java.name) } } @@ -112,7 +112,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() { val builder = AlertDialog.Builder(activity) val inflater = activity.layoutInflater - rootView = inflater.inflate(R.layout.fragment_set_password, null) + rootView = inflater.inflate(R.layout.fragment_set_main_credential, null) builder.setView(rootView) // Add action buttons .setPositiveButton(android.R.string.ok) { _, _ -> } @@ -254,7 +254,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() { .setPositiveButton(android.R.string.ok) { _, _ -> if (!verifyKeyFile()) { mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) - this@AssignMasterKeyDialogFragment.dismiss() + this@SetMainCredentialDialogFragment.dismiss() } } .setNegativeButton(android.R.string.cancel) { _, _ -> } @@ -269,7 +269,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() { builder.setMessage(R.string.warning_no_encryption_key) .setPositiveButton(android.R.string.ok) { _, _ -> mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) - this@AssignMasterKeyDialogFragment.dismiss() + this@SetMainCredentialDialogFragment.dismiss() } .setNegativeButton(android.R.string.cancel) { _, _ -> } mNoKeyConfirmationDialog = builder.create() @@ -301,8 +301,8 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() { private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG" - fun getInstance(allowNoMasterKey: Boolean): AssignMasterKeyDialogFragment { - val fragment = AssignMasterKeyDialogFragment() + fun getInstance(allowNoMasterKey: Boolean): SetMainCredentialDialogFragment { + val fragment = SetMainCredentialDialogFragment() val args = Bundle() args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey) fragment.arguments = args diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt index 46b040868..08fb5e20d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt @@ -29,6 +29,7 @@ import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator +import com.google.android.material.textfield.TextInputLayout import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment @@ -55,6 +56,7 @@ class EntryEditFragment: DatabaseFragment() { private lateinit var attachmentsContainerView: ViewGroup private lateinit var attachmentsListView: RecyclerView private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null + private lateinit var tagsContainerView: TextInputLayout private lateinit var tagsCompletionView: TagsCompletionView private var tagsAdapter: FilteredArrayAdapter? = null @@ -89,6 +91,7 @@ class EntryEditFragment: DatabaseFragment() { templateView = view.findViewById(R.id.template_view) attachmentsContainerView = view.findViewById(R.id.entry_attachments_container) attachmentsListView = view.findViewById(R.id.entry_attachments_list) + tagsContainerView = view.findViewById(R.id.entry_tags_label) tagsCompletionView = view.findViewById(R.id.entry_tags_completion_view) attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext()) @@ -157,11 +160,11 @@ class EntryEditFragment: DatabaseFragment() { templateView.setIcon(iconImage) } - mEntryEditViewModel.onBackgroundColorSelected.observe(this) { color -> + mEntryEditViewModel.onBackgroundColorSelected.observe(viewLifecycleOwner) { color -> templateView.setBackgroundColor(color) } - mEntryEditViewModel.onForegroundColorSelected.observe(this) { color -> + mEntryEditViewModel.onForegroundColorSelected.observe(viewLifecycleOwner) { color -> templateView.setForegroundColor(color) } @@ -287,6 +290,7 @@ class EntryEditFragment: DatabaseFragment() { threshold = 1 setAdapter(tagsAdapter) } + tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE } private fun assignEntryInfo(entryInfo: EntryInfo?) { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt index 0e9e9ca2e..cf4b49287 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt @@ -41,6 +41,8 @@ class EntryFragment: DatabaseFragment() { private lateinit var attachmentsListView: RecyclerView private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null + private lateinit var customDataView: TextView + private lateinit var uuidContainerView: View private lateinit var uuidReferenceView: TextView @@ -83,6 +85,9 @@ class EntryFragment: DatabaseFragment() { creationDateView = view.findViewById(R.id.entry_created) modificationDateView = view.findViewById(R.id.entry_modified) + // TODO Custom data + // customDataView = view.findViewById(R.id.entry_custom_data) + uuidContainerView = view.findViewById(R.id.entry_UUID_container) uuidContainerView.apply { visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE @@ -154,11 +159,14 @@ class EntryFragment: DatabaseFragment() { assignAttachments(entryInfo?.attachments ?: listOf()) // Assign dates - assignCreationDate(entryInfo?.creationTime) - assignModificationDate(entryInfo?.lastModificationTime) + creationDateView.text = entryInfo?.creationTime?.getDateTimeString(resources) + modificationDateView.text = entryInfo?.lastModificationTime?.getDateTimeString(resources) + + // TODO Custom data + // customDataView.text = entryInfo?.customData?.toString() // Assign special data - assignUUID(entryInfo?.id) + uuidReferenceView.text = UuidUtil.toHexString(entryInfo?.id) } private fun showClipboardDialog() { @@ -189,18 +197,6 @@ class EntryFragment: DatabaseFragment() { templateView.reload() } - private fun assignCreationDate(date: DateInstant?) { - creationDateView.text = date?.getDateTimeString(resources) - } - - private fun assignModificationDate(date: DateInstant?) { - modificationDateView.text = date?.getDateTimeString(resources) - } - - private fun assignUUID(uuid: UUID?) { - uuidReferenceView.text = UuidUtil.toHexString(uuid) - } - /* ------------- * Attachments * ------------- diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt index f7d857838..8179f3381 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt @@ -152,7 +152,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen mAdapter = NodesAdapter(context, database).apply { setOnNodeClickListener(object : NodesAdapter.NodeClickCallback { override fun onNodeClick(database: Database, node: Node) { - if (nodeActionSelectionMode) { + if (mCurrentGroup?.isVirtual == false + && nodeActionSelectionMode) { if (listActionNodes.contains(node)) { // Remove selected item if already selected listActionNodes.remove(node) @@ -169,7 +170,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen } override fun onNodeLongClick(database: Database, node: Node): Boolean { - if (nodeActionPasteMode == PasteMode.UNDEFINED) { + if (mCurrentGroup?.isVirtual == false + && nodeActionPasteMode == PasteMode.UNDEFINED) { // Select the first item after a long click if (!listActionNodes.contains(node)) listActionNodes.add(node) @@ -257,9 +259,9 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen private fun rebuildList() { try { // Add elements to the list - mCurrentGroup?.let { mainGroup -> + mCurrentGroup?.let { currentGroup -> // Thrown an exception when sort cannot be performed - mAdapter?.rebuildList(mainGroup) + mAdapter?.rebuildList(currentGroup) } } catch (e:Exception) { Log.e(TAG, "Unable to rebuild the list", e) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt index 5f62542de..c1d2e72b9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt @@ -4,9 +4,9 @@ import android.net.Uri import android.os.Bundle import androidx.activity.viewModels import com.kunzisoft.keepass.activities.stylish.StylishActivity -import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.database.action.DatabaseTaskProvider import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.viewmodels.DatabaseViewModel @@ -59,9 +59,9 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval { fun loadDatabase(databaseUri: Uri, mainCredential: MainCredential, readOnly: Boolean, - cipherEntity: CipherDatabaseEntity?, + cipherEncryptDatabase: CipherEncryptDatabase?, fixDuplicateUuid: Boolean) { - mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEntity, fixDuplicateUuid) + mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid) } protected fun closeDatabase() { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt index 6b5e79d64..4b75c1998 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt @@ -88,8 +88,8 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), mDatabaseTaskProvider?.startDatabaseSave(save) } - mDatabaseViewModel.mergeDatabase.observe(this) { fixDuplicateUuid -> - mDatabaseTaskProvider?.startDatabaseMerge(fixDuplicateUuid) + mDatabaseViewModel.mergeDatabase.observe(this) { + mDatabaseTaskProvider?.startDatabaseMerge() } mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid -> @@ -263,8 +263,16 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), mDatabaseTaskProvider?.startDatabaseSave(true) } + fun saveDatabaseTo(uri: Uri) { + mDatabaseTaskProvider?.startDatabaseSave(true, uri) + } + fun mergeDatabase() { - mDatabaseTaskProvider?.startDatabaseMerge(false) + mDatabaseTaskProvider?.startDatabaseMerge() + } + + fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) { + mDatabaseTaskProvider?.startDatabaseMerge(uri, mainCredential) } fun reloadDatabase() { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt index ae7959ba1..573bc7070 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt @@ -1,6 +1,8 @@ package com.kunzisoft.keepass.activities.legacy import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.View import android.widget.Toast import com.kunzisoft.keepass.R @@ -11,6 +13,7 @@ import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.view.SpecialModeView + /** * Activity to manage database special mode (ie: selection mode) */ @@ -63,8 +66,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() { EntrySelectionHelper.removeModesFromIntent(intent) EntrySelectionHelper.removeInfoFromIntent(intent) if (mSpecialMode != SpecialMode.DEFAULT) { - // To move the app in background - moveTaskToBack(true) + backToTheMainAppAndFinish() } } } @@ -77,8 +79,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() { EntrySelectionHelper.removeModesFromIntent(intent) EntrySelectionHelper.removeInfoFromIntent(intent) if (mSpecialMode != SpecialMode.DEFAULT) { - // To move the app in background - moveTaskToBack(true) + backToTheMainAppAndFinish() } } } @@ -88,11 +89,19 @@ abstract class DatabaseModeActivity : DatabaseActivity() { // To get the app caller, only for IntentSender super.onBackPressed() } else { - // To move the app in background - moveTaskToBack(true) + backToTheMainAppAndFinish() } } + private fun backToTheMainAppAndFinish() { + // To move the app in background and return to the main app + moveTaskToBack(true) + // To remove this instance in the OS app selector + Handler(Looper.getMainLooper()).postDelayed({ + finish() + }, 500) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -160,12 +169,17 @@ abstract class DatabaseModeActivity : DatabaseActivity() { } // To hide home button from the regular toolbar in special mode - if (mSpecialMode != SpecialMode.DEFAULT) { + if (mSpecialMode != SpecialMode.DEFAULT + && hideHomeButtonIfModeIsNotDefault()) { supportActionBar?.setDisplayHomeAsUpEnabled(false) supportActionBar?.setDisplayShowHomeEnabled(false) } } + open fun hideHomeButtonIfModeIsNotDefault(): Boolean { + return true + } + private fun blockAutofill(searchInfo: SearchInfo?) { val webDomain = searchInfo?.webDomain val applicationId = searchInfo?.applicationId diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/Stylish.kt b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/Stylish.kt index 9972dc227..dd8a98920 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/Stylish.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/Stylish.kt @@ -69,8 +69,10 @@ object Stylish { context.getString(R.string.list_style_name_night) -> context.getString(R.string.list_style_name_light) context.getString(R.string.list_style_name_black) -> context.getString(R.string.list_style_name_white) context.getString(R.string.list_style_name_dark) -> context.getString(R.string.list_style_name_clear) + context.getString(R.string.list_style_name_simple_night) -> context.getString(R.string.list_style_name_simple) context.getString(R.string.list_style_name_blue_night) -> context.getString(R.string.list_style_name_blue) context.getString(R.string.list_style_name_red_night) -> context.getString(R.string.list_style_name_red) + context.getString(R.string.list_style_name_reply_night) -> context.getString(R.string.list_style_name_reply) context.getString(R.string.list_style_name_purple_dark) -> context.getString(R.string.list_style_name_purple) else -> styleString } @@ -81,8 +83,10 @@ object Stylish { context.getString(R.string.list_style_name_light) -> context.getString(R.string.list_style_name_night) context.getString(R.string.list_style_name_white) -> context.getString(R.string.list_style_name_black) context.getString(R.string.list_style_name_clear) -> context.getString(R.string.list_style_name_dark) + context.getString(R.string.list_style_name_simple) -> context.getString(R.string.list_style_name_simple_night) context.getString(R.string.list_style_name_blue) -> context.getString(R.string.list_style_name_blue_night) context.getString(R.string.list_style_name_red) -> context.getString(R.string.list_style_name_red_night) + context.getString(R.string.list_style_name_reply) -> context.getString(R.string.list_style_name_reply_night) context.getString(R.string.list_style_name_purple) -> context.getString(R.string.list_style_name_purple_dark) else -> styleString } @@ -113,10 +117,14 @@ object Stylish { context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black context.getString(R.string.list_style_name_clear) -> R.style.KeepassDXStyle_Clear context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark + context.getString(R.string.list_style_name_simple) -> R.style.KeepassDXStyle_Simple + context.getString(R.string.list_style_name_simple_night) -> R.style.KeepassDXStyle_Simple_Night context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue context.getString(R.string.list_style_name_blue_night) -> R.style.KeepassDXStyle_Blue_Night context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red context.getString(R.string.list_style_name_red_night) -> R.style.KeepassDXStyle_Red_Night + context.getString(R.string.list_style_name_reply) -> R.style.KeepassDXStyle_Reply + context.getString(R.string.list_style_name_reply_night) -> R.style.KeepassDXStyle_Reply_Night context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple context.getString(R.string.list_style_name_purple_dark) -> R.style.KeepassDXStyle_Purple_Dark else -> R.style.KeepassDXStyle_Light diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishFragment.kt index ddb69d1c6..cf279544a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishFragment.kt @@ -23,11 +23,13 @@ import android.content.Context import android.graphics.Color import android.os.Build import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.StyleRes import androidx.appcompat.view.ContextThemeWrapper +import androidx.core.view.WindowInsetsControllerCompat import androidx.fragment.app.Fragment abstract class StylishFragment : Fragment() { @@ -47,27 +49,41 @@ abstract class StylishFragment : Fragment() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { val window = requireActivity().window val defaultColor = Color.BLACK - + val windowInset = WindowInsetsControllerCompat(window, window.decorView) try { val taStatusBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.statusBarColor)) window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor taStatusBarColor?.recycle() - } catch (e: Exception) {} + } catch (e: Exception) { + Log.e(TAG, "Unable to retrieve theme : status bar color", e) + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { try { val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar)) - if (taWindowStatusLight?.getBoolean(0, false) == true) { - @Suppress("DEPRECATION") - window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - } + windowInset.isAppearanceLightStatusBars = taWindowStatusLight + ?.getBoolean(0, false) == true taWindowStatusLight?.recycle() - } catch (e: Exception) {} + } catch (e: Exception) { + Log.e(TAG, "Unable to retrieve theme : window light status bar", e) + } } try { val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor)) window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor taNavigationBarColor?.recycle() - } catch (e: Exception) {} + } catch (e: Exception) { + Log.e(TAG, "Unable to retrieve theme : navigation bar color", e) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + try { + val taWindowLightNavigationBar = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightNavigationBar)) + windowInset.isAppearanceLightNavigationBars = taWindowLightNavigationBar + ?.getBoolean(0, false) == true + taWindowLightNavigationBar?.recycle() + } catch (e: Exception) { + Log.e(TAG, "Unable to retrieve theme : navigation light navigation bar", e) + } + } } return super.onCreateView(inflater, container, savedInstanceState) } @@ -76,4 +92,8 @@ abstract class StylishFragment : Fragment() { contextThemed = null super.onDetach() } + + companion object { + private val TAG = StylishFragment::class.java.simpleName + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/NodesAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/NodesAdapter.kt index bc304c8ee..c5fe46ada 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/NodesAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/NodesAdapter.kt @@ -29,7 +29,6 @@ import android.widget.ImageView import android.widget.TextView import android.widget.Toast import androidx.annotation.ColorInt -import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SortedList import androidx.recyclerview.widget.SortedListAdapterCallback @@ -87,8 +86,6 @@ class NodesAdapter (private val context: Context, private var mNodeClickCallback: NodeClickCallback? = null private var mClipboardHelper = ClipboardHelper(context) - @ColorInt - private val mContentSelectionColor: Int @ColorInt private val mTextColorPrimary: Int @ColorInt @@ -98,7 +95,7 @@ class NodesAdapter (private val context: Context, @ColorInt private val mColorAccentLight: Int @ColorInt - private val mTextColorSelected: Int + private val mColorOnAccentColor: Int /** * Determine if the adapter contains or not any element @@ -115,8 +112,6 @@ class NodesAdapter (private val context: Context, this.mNodeSortedListCallback = NodeSortedListCallback() this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback) - // Color of content selection - this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white) // Retrieve the color to tint the icon val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary)) this.mTextColorPrimary = taTextColorPrimary.getColor(0, Color.BLACK) @@ -130,13 +125,13 @@ class NodesAdapter (private val context: Context, this.mTextColorSecondary = taTextColorSecondary.getColor(0, Color.BLACK) taTextColorSecondary.recycle() // To get background color for selection - val taSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccentLight)) - this.mColorAccentLight = taSelectionColor.getColor(0, Color.GRAY) - taSelectionColor.recycle() + val taColorAccentLight = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccentLight)) + this.mColorAccentLight = taColorAccentLight.getColor(0, Color.GRAY) + taColorAccentLight.recycle() // To get text color for selection - val taSelectionTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnAccentColor)) - this.mTextColorSelected = taSelectionTextColor.getColor(0, Color.WHITE) - taSelectionTextColor.recycle() + val taColorOnAccentColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnAccentColor)) + this.mColorOnAccentColor = taColorOnAccentColor.getColor(0, Color.WHITE) + taColorOnAccentColor.recycle() } private fun assignPreferences() { @@ -352,23 +347,6 @@ class NodesAdapter (private val context: Context, isSelected = mActionNodesList.contains(subNode) } - // Assign image - val iconColor = if (holder.container.isSelected) - mContentSelectionColor - else when (subNode.type) { - Type.GROUP -> mTextColorPrimary - Type.ENTRY -> mTextColor - } - holder.imageIdentifier?.setColorFilter(iconColor) - holder.icon.apply { - database.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor) - // Relative size of the icon - layoutParams?.apply { - height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt() - width = (mIconDefaultDimension * mPrefSizeMultiplier).toInt() - } - } - // Assign text holder.text.apply { text = subNode.title @@ -396,6 +374,14 @@ class NodesAdapter (private val context: Context, holder.path?.visibility = View.GONE } + // Assign icon colors + var iconColor = if (holder.container.isSelected) + mColorOnAccentColor + else when (subNode.type) { + Type.GROUP -> mTextColorPrimary + Type.ENTRY -> mTextColor + } + // Specific elements for entry if (subNode.type == Type.ENTRY) { val entry = subNode as Entry @@ -457,13 +443,7 @@ class NodesAdapter (private val context: Context, holder.otpProgress?.setIndicatorColor(foregroundColor) holder.attachmentIcon?.setColorFilter(foregroundColor) holder.meta.setTextColor(foregroundColor) - holder.icon.apply { - database.iconDrawableFactory.assignDatabaseIcon( - this, - subNode.icon, - foregroundColor - ) - } + iconColor = foregroundColor } else { holder.text.setTextColor(mTextColor) holder.subText?.setTextColor(mTextColorSecondary) @@ -473,12 +453,12 @@ class NodesAdapter (private val context: Context, holder.meta.setTextColor(mTextColor) } } else { - holder.text.setTextColor(mTextColorSelected) - holder.subText?.setTextColor(mTextColorSelected) - holder.otpToken?.setTextColor(mTextColorSelected) - holder.otpProgress?.setIndicatorColor(mTextColorSelected) - holder.attachmentIcon?.setColorFilter(mTextColorSelected) - holder.meta.setTextColor(mTextColorSelected) + holder.text.setTextColor(mColorOnAccentColor) + holder.subText?.setTextColor(mColorOnAccentColor) + holder.otpToken?.setTextColor(mColorOnAccentColor) + holder.otpProgress?.setIndicatorColor(mColorOnAccentColor) + holder.attachmentIcon?.setColorFilter(mColorOnAccentColor) + holder.meta.setTextColor(mColorOnAccentColor) } database.stopManageEntry(entry) @@ -499,6 +479,17 @@ class NodesAdapter (private val context: Context, } } + // Assign image + holder.imageIdentifier?.setColorFilter(iconColor) + holder.icon.apply { + database.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor) + // Relative size of the icon + layoutParams?.apply { + height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt() + width = (mIconDefaultDimension * mPrefSizeMultiplier).toInt() + } + } + // Assign click holder.container.setOnClickListener { mNodeClickCallback?.onNodeClick(database, subNode) diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt deleted file mode 100644 index 06d576f21..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePassDX. - * - * KeePassDX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePassDX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePassDX. If not, see . - * - */ -package com.kunzisoft.keepass.adapters - -import android.content.Context -import android.database.Cursor -import android.database.MatrixCursor -import android.graphics.Color -import android.provider.BaseColumns -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.cursoradapter.widget.CursorAdapter -import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.element.Entry -import com.kunzisoft.keepass.database.element.Group -import com.kunzisoft.keepass.database.element.node.NodeId -import com.kunzisoft.keepass.database.element.node.NodeIdUUID -import com.kunzisoft.keepass.database.search.SearchHelper -import com.kunzisoft.keepass.settings.PreferencesUtil -import com.kunzisoft.keepass.view.strikeOut -import java.util.* - -class SearchEntryCursorAdapter(private val context: Context, - private val database: Database) - : CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) { - - private val cursorInflater: LayoutInflater? = context.getSystemService( - Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater? - private var mDisplayUsername: Boolean = false - private var mOmitBackup: Boolean = true - private val iconColor: Int - - init { - // Get the icon color - val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse)) - this.iconColor = taTextColor.getColor(0, Color.WHITE) - taTextColor.recycle() - - reInit(context) - } - - fun reInit(context: Context) { - this.mDisplayUsername = PreferencesUtil.showUsernamesListEntries(context) - this.mOmitBackup = PreferencesUtil.omitBackup(context) - } - - override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View { - - val view = cursorInflater!!.inflate(R.layout.item_search_entry, parent, false) - val viewHolder = ViewHolder() - viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon) - viewHolder.textViewTitle = view.findViewById(R.id.entry_text) - viewHolder.textViewSubTitle = view.findViewById(R.id.entry_subtext) - viewHolder.textViewPath = view.findViewById(R.id.entry_path) - view.tag = viewHolder - - return view - } - - override fun bindView(view: View, context: Context, cursor: Cursor?) { - getEntryFrom(cursor)?.let { currentEntry -> - val viewHolder = view.tag as ViewHolder - - // Assign image - viewHolder.imageViewIcon?.let { iconView -> - database.iconDrawableFactory.assignDatabaseIcon(iconView, currentEntry.icon, iconColor) - } - - // Assign title - viewHolder.textViewTitle?.apply { - text = currentEntry.getVisualTitle() - strikeOut(currentEntry.isCurrentlyExpires) - } - - // Assign subtitle - viewHolder.textViewSubTitle?.apply { - val entryUsername = currentEntry.username - text = if (mDisplayUsername && entryUsername.isNotEmpty()) { - String.format("(%s)", entryUsername) - } else { - "" - } - visibility = if (text.isEmpty()) View.GONE else View.VISIBLE - strikeOut(currentEntry.isCurrentlyExpires) - } - - viewHolder.textViewPath?.apply { - text = currentEntry.getPathString() - } - } - } - - private fun getEntryFrom(cursor: Cursor?): Entry? { - val entryCursor = cursor as? EntryCursor? - entryCursor?.getNodeId()?.let { - return database.getEntryById(it) - } - return null - } - - override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? { - return searchEntries(context, constraint.toString()) - } - - private fun searchEntries(context: Context, query: String): Cursor { - val cursor = EntryCursor() - val searchGroup = database.createVirtualGroupFromSearch(query, - mOmitBackup, - SearchHelper.MAX_SEARCH_ENTRY) - if (searchGroup != null) { - // Search in hide entries but not meta-stream - for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) { - database.startManageEntry(entry) - cursor.addEntry(entry) - database.stopManageEntry(entry) - } - } - return cursor - } - - fun getEntryFromPosition(position: Int): Entry? { - var pwEntry: Entry? = null - - val cursor = this.cursor - if (cursor.moveToFirst() && cursor.move(position)) { - pwEntry = getEntryFrom(cursor) - } - return pwEntry - } - - private class ViewHolder { - var imageViewIcon: ImageView? = null - var textViewTitle: TextView? = null - var textViewSubTitle: TextView? = null - var textViewPath: TextView? = null - } - - private class EntryCursor : MatrixCursor(arrayOf( - ID, - COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS, - COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS - )) { - - private var entryId: Long = 0 - - fun addEntry(entry: Entry) { - addRow(arrayOf( - entryId, - entry.nodeId.id.mostSignificantBits, - entry.nodeId.id.leastSignificantBits - )) - entryId++ - } - - fun getNodeId(): NodeId { - return NodeIdUUID( - UUID(getLong(getColumnIndex(COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)), - getLong(getColumnIndex(COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS))) - ) - } - - companion object { - const val ID = BaseColumns._ID - const val COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS = "UUID_most_significant_bits" - const val COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS = "UUID_least_significant_bits" - } - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseAction.kt b/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseAction.kt index 4ede01649..d39f8c6bc 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseAction.kt +++ b/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseAction.kt @@ -22,7 +22,9 @@ package com.kunzisoft.keepass.app.database import android.content.* import android.net.Uri import android.os.IBinder +import android.util.Base64 import android.util.Log +import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.SingletonHolderParameter @@ -125,15 +127,40 @@ class CipherDatabaseAction(context: Context) { } fun getCipherDatabase(databaseUri: Uri, - cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) { + cipherDatabaseResultListener: (CipherEncryptDatabase?) -> Unit) { if (useTempDao) { serviceActionTask { - cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri)) + mBinder?.getCipherDatabase(databaseUri)?.let { cipherDatabaseEntity -> + val cipherDatabase = CipherEncryptDatabase().apply { + this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri) + this.encryptedValue = Base64.decode( + cipherDatabaseEntity.encryptedValue, + Base64.NO_WRAP + ) + this.specParameters = Base64.decode( + cipherDatabaseEntity.specParameters, + Base64.NO_WRAP + ) + } + cipherDatabaseResultListener.invoke(cipherDatabase) + } } } else { IOActionTask( { - cipherDatabaseDao.getByDatabaseUri(databaseUri.toString()) + cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())?.let { cipherDatabaseEntity -> + CipherEncryptDatabase().apply { + this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri) + this.encryptedValue = Base64.decode( + cipherDatabaseEntity.encryptedValue, + Base64.NO_WRAP + ) + this.specParameters = Base64.decode( + cipherDatabaseEntity.specParameters, + Base64.NO_WRAP + ) + } + } }, { cipherDatabaseResultListener.invoke(it) @@ -149,18 +176,27 @@ class CipherDatabaseAction(context: Context) { } } - fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity, + fun addOrUpdateCipherDatabase(cipherEncryptDatabase: CipherEncryptDatabase, cipherDatabaseResultListener: (() -> Unit)? = null) { - if (useTempDao) { - // The only case to create service (not needed to get an info) - serviceActionTask(true) { - mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity) - cipherDatabaseResultListener?.invoke() - } - } else { - IOActionTask( + cipherEncryptDatabase.databaseUri?.let { databaseUri -> + + val cipherDatabaseEntity = CipherDatabaseEntity( + databaseUri.toString(), + Base64.encodeToString(cipherEncryptDatabase.encryptedValue, Base64.NO_WRAP), + Base64.encodeToString(cipherEncryptDatabase.specParameters, Base64.NO_WRAP), + ) + + if (useTempDao) { + // The only case to create service (not needed to get an info) + serviceActionTask(true) { + mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity) + cipherDatabaseResultListener?.invoke() + } + } else { + IOActionTask( { - val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri) + val cipherDatabaseRetrieve = + cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri) // Update values if element not yet in the database if (cipherDatabaseRetrieve == null) { cipherDatabaseDao.add(cipherDatabaseEntity) @@ -171,7 +207,8 @@ class CipherDatabaseAction(context: Context) { { cipherDatabaseResultListener?.invoke() } - ).execute() + ).execute() + } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt index c933ca3da..bc8b93599 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt @@ -40,6 +40,9 @@ import com.kunzisoft.keepass.activities.stylish.StylishFragment import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.database.exception.IODatabaseException import com.kunzisoft.keepass.education.PasswordActivityEducation +import com.kunzisoft.keepass.model.CipherDecryptDatabase +import com.kunzisoft.keepass.model.CipherEncryptDatabase +import com.kunzisoft.keepass.model.CredentialStorage import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.view.AdvancedUnlockInfoView import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel @@ -60,6 +63,9 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU var databaseFileUri: Uri? = null private set + // TODO Retrieve credential storage from app database + var credentialDatabaseStorage: CredentialStorage = CredentialStorage.DEFAULT + /** * Manage setting to auto open biometric prompt */ @@ -477,6 +483,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU } ?: checkUnlockAvailability() } + @RequiresApi(Build.VERSION_CODES.M) override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { lifecycleScope.launch(Dispatchers.Main) { Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString") @@ -528,16 +535,29 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU } } - override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) { + override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) { databaseFileUri?.let { databaseUri -> - mBuilderListener?.onCredentialEncrypted(databaseUri, encryptedValue, ivSpec) + mBuilderListener?.onCredentialEncrypted( + CipherEncryptDatabase().apply { + this.databaseUri = databaseUri + this.credentialStorage = credentialDatabaseStorage + this.encryptedValue = encryptedValue + this.specParameters = ivSpec + } + ) } } - override fun handleDecryptedResult(decryptedValue: String) { + override fun handleDecryptedResult(decryptedValue: ByteArray) { // Load database directly with password retrieve - databaseFileUri?.let { - mBuilderListener?.onCredentialDecrypted(it, decryptedValue) + databaseFileUri?.let { databaseUri -> + mBuilderListener?.onCredentialDecrypted( + CipherDecryptDatabase().apply { + this.databaseUri = databaseUri + this.credentialStorage = credentialDatabaseStorage + this.decryptedValue = decryptedValue + } + ) } } @@ -551,6 +571,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key) } + @RequiresApi(Build.VERSION_CODES.M) override fun onGenericException(e: Exception) { val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: "" setAdvancedUnlockedMessageView(errorMessage) @@ -580,6 +601,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU } } + @RequiresApi(Build.VERSION_CODES.M) private fun setAdvancedUnlockedMessageView(text: CharSequence) { lifecycleScope.launch(Dispatchers.Main) { mAdvancedUnlockInfoView?.message = text @@ -617,10 +639,10 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU } interface BuilderListener { - fun retrieveCredentialForEncryption(): String + fun retrieveCredentialForEncryption(): ByteArray fun conditionToStoreCredential(): Boolean - fun onCredentialEncrypted(databaseUri: Uri, encryptedCredential: String, ivSpec: String) - fun onCredentialDecrypted(databaseUri: Uri, decryptedCredential: String) + fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) + fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) } override fun onPause() { diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockManager.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockManager.kt index a771c39de..0439da147 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockManager.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockManager.kt @@ -27,7 +27,6 @@ import android.os.Build import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyProperties -import android.util.Base64 import android.util.Log import android.widget.Toast import androidx.activity.result.ActivityResultLauncher @@ -214,18 +213,15 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) } } - fun encryptData(value: String) { + fun encryptData(value: ByteArray) { if (!isKeyManagerInitialized) { return } try { - val encrypted = cipher?.doFinal(value.toByteArray()) - val encryptedBase64 = Base64.encodeToString(encrypted, Base64.NO_WRAP) - + val encrypted = cipher?.doFinal(value) ?: byteArrayOf() // passes updated iv spec on to callback so this can be stored for decryption cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec -> - val ivSpecValue = Base64.encodeToString(spec.iv, Base64.NO_WRAP) - advancedUnlockCallback?.handleEncryptedResult(encryptedBase64, ivSpecValue) + advancedUnlockCallback?.handleEncryptedResult(encrypted, spec.iv) } } catch (e: Exception) { Log.e(TAG, "Unable to encrypt data", e) @@ -233,12 +229,12 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) } } - fun initDecryptData(ivSpecValue: String, + fun initDecryptData(ivSpecValue: ByteArray, actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) { initDecryptData(ivSpecValue, actionIfCypherInit, true) } - private fun initDecryptData(ivSpecValue: String, + private fun initDecryptData(ivSpecValue: ByteArray, actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit, firstLaunch: Boolean = true) { if (!isKeyManagerInitialized) { @@ -246,9 +242,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) } try { // important to restore spec here that was used for decryption - val iv = Base64.decode(ivSpecValue, Base64.NO_WRAP) - val spec = IvParameterSpec(iv) - + val spec = IvParameterSpec(ivSpecValue) getSecretKey()?.let { secretKey -> cipher?.let { cipher -> cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) @@ -284,15 +278,14 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) } } - fun decryptData(encryptedValue: String) { + fun decryptData(encryptedValue: ByteArray) { if (!isKeyManagerInitialized) { return } try { // actual decryption here - val encrypted = Base64.decode(encryptedValue, Base64.NO_WRAP) - cipher?.doFinal(encrypted)?.let { decrypted -> - advancedUnlockCallback?.handleDecryptedResult(String(decrypted)) + cipher?.doFinal(encryptedValue)?.let { decrypted -> + advancedUnlockCallback?.handleDecryptedResult(decrypted) } } catch (badPaddingException: BadPaddingException) { Log.e(TAG, "Unable to decrypt data", badPaddingException) @@ -367,8 +360,8 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) fun onAuthenticationSucceeded() fun onAuthenticationFailed() fun onAuthenticationError(errorCode: Int, errString: CharSequence) - fun handleEncryptedResult(encryptedValue: String, ivSpec: String) - fun handleDecryptedResult(decryptedValue: String) + fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) + fun handleDecryptedResult(decryptedValue: ByteArray) } companion object { @@ -469,9 +462,9 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {} - override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {} + override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {} - override fun handleDecryptedResult(decryptedValue: String) {} + override fun handleDecryptedResult(decryptedValue: ByteArray) {} override fun onUnrecoverableKeyException(e: Exception) { advancedCallback.onUnrecoverableKeyException(e) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignMainCredentialInDatabaseRunnable.kt similarity index 98% rename from app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt rename to app/src/main/java/com/kunzisoft/keepass/database/action/AssignMainCredentialInDatabaseRunnable.kt index a49096425..f405faeb6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignMainCredentialInDatabaseRunnable.kt @@ -27,7 +27,7 @@ import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.utils.UriUtil -open class AssignPasswordInDatabaseRunnable ( +open class AssignMainCredentialInDatabaseRunnable ( context: Context, database: Database, protected val mDatabaseUri: Uri, diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt index d1f792c8e..e0c11577c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt @@ -35,7 +35,7 @@ class CreateDatabaseRunnable(context: Context, private val templateGroupName: String?, mainCredential: MainCredential, private val createDatabaseResult: ((Result) -> Unit)?) - : AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) { + : AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) { override fun onStartRun() { try { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/DatabaseTaskProvider.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/DatabaseTaskProvider.kt index c98a44eeb..18468a863 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/DatabaseTaskProvider.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/DatabaseTaskProvider.kt @@ -33,7 +33,6 @@ import androidx.lifecycle.lifecycleScope import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG -import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.element.Database @@ -43,6 +42,7 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.Type +import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.services.DatabaseTaskNotificationService @@ -84,7 +84,6 @@ import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION import kotlinx.coroutines.launch import java.util.* -import kotlin.collections.ArrayList /** * Utility class to connect an activity or a service to the DatabaseTaskNotificationService, @@ -344,21 +343,23 @@ class DatabaseTaskProvider { fun startDatabaseLoad(databaseUri: Uri, mainCredential: MainCredential, readOnly: Boolean, - cipherEntity: CipherDatabaseEntity?, + cipherEncryptDatabase: CipherEncryptDatabase?, fixDuplicateUuid: Boolean) { start(Bundle().apply { putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly) - putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity) + putParcelable(DatabaseTaskNotificationService.CIPHER_DATABASE_KEY, cipherEncryptDatabase) putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid) } , ACTION_DATABASE_LOAD_TASK) } - fun startDatabaseMerge(fixDuplicateUuid: Boolean) { + fun startDatabaseMerge(fromDatabaseUri: Uri? = null, + mainCredential: MainCredential? = null) { start(Bundle().apply { - putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid) + putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri) + putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) } , ACTION_DATABASE_MERGE_TASK) } @@ -693,9 +694,10 @@ class DatabaseTaskProvider { /** * Save Database without parameter */ - fun startDatabaseSave(save: Boolean) { + fun startDatabaseSave(save: Boolean, saveToUri: Uri? = null) { start(Bundle().apply { putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) + putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri) } , ACTION_DATABASE_SAVE) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt index ef1c2be70..2329ff7f7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt @@ -22,12 +22,11 @@ package com.kunzisoft.keepass.database.action import android.content.Context import android.net.Uri import com.kunzisoft.keepass.app.database.CipherDatabaseAction -import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.binary.BinaryData -import com.kunzisoft.keepass.database.element.binary.LoadedKey import com.kunzisoft.keepass.database.exception.LoadDatabaseException +import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable @@ -39,7 +38,7 @@ class LoadDatabaseRunnable(private val context: Context, private val mUri: Uri, private val mMainCredential: MainCredential, private val mReadonly: Boolean, - private val mCipherEntity: CipherDatabaseEntity?, + private val mCipherEncryptDatabase: CipherEncryptDatabase?, private val mFixDuplicateUUID: Boolean, private val progressTaskUpdater: ProgressTaskUpdater?, private val mLoadDatabaseResult: ((Result) -> Unit)?) @@ -76,9 +75,9 @@ class LoadDatabaseRunnable(private val context: Context, } // Register the biometric - mCipherEntity?.let { cipherDatabaseEntity -> + mCipherEncryptDatabase?.let { cipherDatabase -> CipherDatabaseAction.getInstance(context) - .addOrUpdateCipherDatabase(cipherDatabaseEntity) // return value not called + .addOrUpdateCipherDatabase(cipherDatabase) // return value not called } // Register the current time to init the lock timer diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/MergeDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/MergeDatabaseRunnable.kt index 1a6a09c95..a0ee19616 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/MergeDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/MergeDatabaseRunnable.kt @@ -20,17 +20,19 @@ package com.kunzisoft.keepass.database.action import android.content.Context +import android.net.Uri import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.binary.BinaryData -import com.kunzisoft.keepass.database.element.binary.LoadedKey import com.kunzisoft.keepass.database.exception.LoadDatabaseException +import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ProgressTaskUpdater -import com.kunzisoft.keepass.utils.UriUtil class MergeDatabaseRunnable(private val context: Context, private val mDatabase: Database, + private val mDatabaseToMergeUri: Uri?, + private val mDatabaseToMergeMainCredential: MainCredential?, private val progressTaskUpdater: ProgressTaskUpdater?, private val mLoadDatabaseResult: ((Result) -> Unit)?) : ActionRunnable() { @@ -41,11 +43,14 @@ class MergeDatabaseRunnable(private val context: Context, override fun onActionRun() { try { - mDatabase.mergeData(context.contentResolver, - { memoryWanted -> - BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) - }, - progressTaskUpdater) + mDatabase.mergeData(mDatabaseToMergeUri, + mDatabaseToMergeMainCredential, + context.contentResolver, + { memoryWanted -> + BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) + }, + progressTaskUpdater + ) } catch (e: LoadDatabaseException) { setError(e) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/SaveDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/SaveDatabaseRunnable.kt index 29f71c742..3b88d51ed 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/SaveDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/SaveDatabaseRunnable.kt @@ -20,13 +20,15 @@ package com.kunzisoft.keepass.database.action import android.content.Context +import android.net.Uri import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.exception.DatabaseException import com.kunzisoft.keepass.tasks.ActionRunnable open class SaveDatabaseRunnable(protected var context: Context, protected var database: Database, - private var saveDatabase: Boolean) + private var saveDatabase: Boolean, + private var databaseCopyUri: Uri? = null) : ActionRunnable() { var mAfterSaveDatabase: ((Result) -> Unit)? = null @@ -37,7 +39,7 @@ open class SaveDatabaseRunnable(protected var context: Context, database.checkVersion() if (saveDatabase && result.isSuccess) { try { - database.saveData(context.contentResolver) + database.saveData(databaseCopyUri, context.contentResolver) } catch (e: DatabaseException) { setError(e) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/CustomData.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/CustomData.kt index 4264e682f..ff47530f9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/CustomData.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/CustomData.kt @@ -1,3 +1,22 @@ +/* + * Copyright 2020 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePassDX. + * + * KeePassDX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePassDX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePassDX. If not, see . + * + */ package com.kunzisoft.keepass.database.element import android.os.Parcel @@ -17,7 +36,10 @@ class CustomData : Parcelable { } constructor(parcel: Parcel) { - ParcelableUtil.readStringParcelableMap(parcel, CustomDataItem::class.java) + mCustomDataItems.clear() + mCustomDataItems.putAll(ParcelableUtil + .readStringParcelableMap(parcel, CustomDataItem::class.java) + ) } fun get(key: String): CustomDataItem? { @@ -46,6 +68,10 @@ class CustomData : Parcelable { } } + override fun toString(): String { + return mCustomDataItems.toString() + } + override fun writeToParcel(parcel: Parcel, flags: Int) { ParcelableUtil.writeStringParcelableMap(parcel, flags, mCustomDataItems) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/CustomDataItem.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/CustomDataItem.kt index b59ad0607..9d67b3d4b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/CustomDataItem.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/CustomDataItem.kt @@ -21,6 +21,10 @@ class CustomDataItem : Parcelable { this.lastModificationTime = lastModificationTime } + override fun toString(): String { + return value + } + override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeString(key) parcel.writeString(value) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt index c28438ea6..004906cdf 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt @@ -62,7 +62,6 @@ import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.readBytes4ToUInt import java.io.* import java.util.* -import kotlin.collections.ArrayList class Database { @@ -275,6 +274,9 @@ class Database { } } + val defaultFileExtension: String + get() = mDatabaseKDB?.defaultFileExtension ?: mDatabaseKDBX?.defaultFileExtension ?: ".bin" + val type: Class<*>? get() = mDatabaseKDB?.javaClass ?: mDatabaseKDBX?.javaClass @@ -496,7 +498,7 @@ class Database { * Determine if a configurable templates group is available or not for this version of database * @return true if a configurable templates group available */ - val allowConfigurableTemplatesGroup: Boolean + val allowTemplatesGroup: Boolean get() = mDatabaseKDBX != null // Maybe another templates method with KDBX5 @@ -635,9 +637,13 @@ class Database { } DatabaseInputKDB(databaseKDB) .openDatabase(databaseInputStream, - mainCredential.masterPassword, - keyFileInputStream, - progressTaskUpdater) + progressTaskUpdater + ) { + databaseKDB.retrieveMasterKey( + mainCredential.masterPassword, + keyFileInputStream + ) + } databaseKDB }, { databaseInputStream -> @@ -648,9 +654,12 @@ class Database { DatabaseInputKDBX(databaseKDBX).apply { setMethodToCheckIfRAMIsSufficient(isRAMSufficient) openDatabase(databaseInputStream, - mainCredential.masterPassword, - keyFileInputStream, - progressTaskUpdater) + progressTaskUpdater) { + databaseKDBX.retrieveMasterKey( + mainCredential.masterPassword, + keyFileInputStream, + ) + } } databaseKDBX } @@ -672,7 +681,9 @@ class Database { } @Throws(LoadDatabaseException::class) - fun mergeData(contentResolver: ContentResolver, + fun mergeData(databaseToMergeUri: Uri?, + databaseToMergeMainCredential: MainCredential?, + contentResolver: ContentResolver, isRAMSufficient: (memoryWanted: Long) -> Boolean, progressTaskUpdater: ProgressTaskUpdater?) { @@ -682,30 +693,52 @@ class Database { // New database instance to get new changes val databaseToMerge = Database() - databaseToMerge.fileUri = this.fileUri + databaseToMerge.fileUri = databaseToMergeUri ?: this.fileUri + // Pass KeyFile Uri as InputStreams + var keyFileInputStream: InputStream? = null try { - databaseToMerge.fileUri?.let { databaseUri -> - - val databaseKDB = DatabaseKDB() - val databaseKDBX = DatabaseKDBX() + val databaseUri = databaseToMerge.fileUri + if (databaseUri != null) { + if (databaseToMergeMainCredential != null) { + // Get keyFile inputStream + databaseToMergeMainCredential.keyFileUri?.let { keyFile -> + keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile) + } + } databaseToMerge.readDatabaseStream(contentResolver, databaseUri, { databaseInputStream -> - DatabaseInputKDB(databaseKDB) - .openDatabase(databaseInputStream, - masterKey, - progressTaskUpdater) - databaseKDB + val databaseToMergeKDB = DatabaseKDB() + DatabaseInputKDB(databaseToMergeKDB) + .openDatabase(databaseInputStream, progressTaskUpdater) { + if (databaseToMergeMainCredential != null) { + databaseToMergeKDB.retrieveMasterKey( + databaseToMergeMainCredential.masterPassword, + keyFileInputStream, + ) + } else { + databaseToMergeKDB.masterKey = masterKey + } + } + databaseToMergeKDB }, { databaseInputStream -> - DatabaseInputKDBX(databaseKDBX).apply { + val databaseToMergeKDBX = DatabaseKDBX() + DatabaseInputKDBX(databaseToMergeKDBX).apply { setMethodToCheckIfRAMIsSufficient(isRAMSufficient) - openDatabase(databaseInputStream, - masterKey, - progressTaskUpdater) + openDatabase(databaseInputStream, progressTaskUpdater) { + if (databaseToMergeMainCredential != null) { + databaseToMergeKDBX.retrieveMasterKey( + databaseToMergeMainCredential.masterPassword, + keyFileInputStream, + ) + } else { + databaseToMergeKDBX.masterKey = masterKey + } + } } - databaseKDBX + databaseToMergeKDBX } ) @@ -715,13 +748,19 @@ class Database { } databaseToMerge.mDatabaseKDB?.let { databaseKDBToMerge -> databaseMerger.merge(databaseKDBToMerge) + if (databaseToMergeUri != null) { + this.dataModifiedSinceLastLoading = true + } } databaseToMerge.mDatabaseKDBX?.let { databaseKDBXToMerge -> databaseMerger.merge(databaseKDBXToMerge) + if (databaseToMergeUri != null) { + this.dataModifiedSinceLastLoading = true + } } } - } ?: run { - throw IODatabaseException("Database URI is null, database cannot be reloaded") + } else { + throw IODatabaseException("Database URI is null, database cannot be merged") } } catch (e: FileNotFoundException) { throw FileNotFoundDatabaseException("Unable to load the keyfile") @@ -730,6 +769,7 @@ class Database { } catch (e: Exception) { throw LoadDatabaseException(e) } finally { + keyFileInputStream?.close() databaseToMerge.clearAndClose() } } @@ -741,7 +781,8 @@ class Database { // Retrieve the stream from the old database URI try { - fileUri?.let { oldDatabaseUri -> + val oldDatabaseUri = fileUri + if (oldDatabaseUri != null) { readDatabaseStream(contentResolver, oldDatabaseUri, { databaseInputStream -> val databaseKDB = DatabaseKDB() @@ -749,9 +790,9 @@ class Database { databaseKDB.binaryCache = it.binaryCache } DatabaseInputKDB(databaseKDB) - .openDatabase(databaseInputStream, - masterKey, - progressTaskUpdater) + .openDatabase(databaseInputStream, progressTaskUpdater) { + databaseKDB.masterKey = masterKey + } databaseKDB }, { databaseInputStream -> @@ -761,14 +802,14 @@ class Database { } DatabaseInputKDBX(databaseKDBX).apply { setMethodToCheckIfRAMIsSufficient(isRAMSufficient) - openDatabase(databaseInputStream, - masterKey, - progressTaskUpdater) + openDatabase(databaseInputStream, progressTaskUpdater) { + databaseKDBX.masterKey = masterKey + } } databaseKDBX } ) - } ?: run { + } else { throw IODatabaseException("Database URI is null, database cannot be reloaded") } } catch (e: FileNotFoundException) { @@ -782,29 +823,39 @@ class Database { } } - fun isGroupSearchable(group: Group, omitBackup: Boolean): Boolean { - return mDatabaseKDB?.isGroupSearchable(group.groupKDB, omitBackup) ?: - mDatabaseKDBX?.isGroupSearchable(group.groupKDBX, omitBackup) ?: - false + fun groupIsInRecycleBin(group: Group): Boolean { + val groupKDB = group.groupKDB + val groupKDBX = group.groupKDBX + if (groupKDB != null) { + return mDatabaseKDB?.isInRecycleBin(groupKDB) ?: false + } else if (groupKDBX != null) { + return mDatabaseKDBX?.isInRecycleBin(groupKDBX) ?: false + } + return false } - fun createVirtualGroupFromSearch(searchQuery: String, - omitBackup: Boolean, + fun groupIsInTemplates(group: Group): Boolean { + val groupKDBX = group.groupKDBX + if (groupKDBX != null) { + return mDatabaseKDBX?.getTemplatesGroup() == groupKDBX + } + return false + } + + fun createVirtualGroupFromSearch(searchParameters: SearchParameters, + fromGroup: NodeId<*>? = null, max: Int = Integer.MAX_VALUE): Group? { return mSearchHelper?.createVirtualGroupWithSearchResult(this, - SearchParameters().apply { - this.searchQuery = searchQuery - }, omitBackup, max) + searchParameters, fromGroup, max) } fun createVirtualGroupFromSearchInfo(searchInfoString: String, - omitBackup: Boolean, max: Int = Integer.MAX_VALUE): Group? { return mSearchHelper?.createVirtualGroupWithSearchResult(this, SearchParameters().apply { searchQuery = searchInfoString searchInTitles = true - searchInUserNames = false + searchInUsernames = false searchInPasswords = false searchInUrls = true searchInNotes = true @@ -812,8 +863,11 @@ class Database { searchInOther = true searchInUUIDs = false searchInTags = false + searchInCurrentGroup = false + searchInSearchableGroup = true + searchInRecycleBin = false searchInTemplates = false - }, omitBackup, max) + }, null, max) } val tagPool: Tags @@ -855,10 +909,61 @@ class Database { } @Throws(DatabaseOutputException::class) - fun saveData(contentResolver: ContentResolver) { + fun saveData(databaseCopyUri: Uri?, contentResolver: ContentResolver) { try { - this.fileUri?.let { - saveData(contentResolver, it) + val saveUri = databaseCopyUri ?: this.fileUri + if (saveUri != null) { + if (saveUri.scheme == "file") { + saveUri.path?.let { filename -> + val tempFile = File("$filename.tmp") + + var fileOutputStream: FileOutputStream? = null + try { + fileOutputStream = FileOutputStream(tempFile) + val pmo = mDatabaseKDB?.let { DatabaseOutputKDB(it, fileOutputStream) } + ?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, fileOutputStream) } + pmo?.output() + } catch (e: Exception) { + throw IOException(e) + } finally { + fileOutputStream?.close() + } + + // Force data to disk before continuing + try { + fileOutputStream?.fd?.sync() + } catch (e: SyncFailedException) { + // Ignore if fsync fails. We tried. + } + + if (!tempFile.renameTo(File(filename))) { + throw IOException() + } + } + } else { + var outputStream: OutputStream? = null + try { + outputStream = contentResolver.openOutputStream(saveUri, "rwt") + outputStream?.let { definedOutputStream -> + val databaseOutput = + mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) } + ?: mDatabaseKDBX?.let { + DatabaseOutputKDBX( + it, + definedOutputStream + ) + } + databaseOutput?.output() + } + } catch (e: Exception) { + throw IOException(e) + } finally { + outputStream?.close() + } + } + if (databaseCopyUri == null) { + this.dataModifiedSinceLastLoading = false + } } } catch (e: Exception) { Log.e(TAG, "Unable to save database", e) @@ -866,55 +971,6 @@ class Database { } } - @Throws(IOException::class, DatabaseOutputException::class) - private fun saveData(contentResolver: ContentResolver, uri: Uri) { - - if (uri.scheme == "file") { - uri.path?.let { filename -> - val tempFile = File("$filename.tmp") - - var fileOutputStream: FileOutputStream? = null - try { - fileOutputStream = FileOutputStream(tempFile) - val pmo = mDatabaseKDB?.let { DatabaseOutputKDB(it, fileOutputStream) } - ?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, fileOutputStream) } - pmo?.output() - } catch (e: Exception) { - throw IOException(e) - } finally { - fileOutputStream?.close() - } - - // Force data to disk before continuing - try { - fileOutputStream?.fd?.sync() - } catch (e: SyncFailedException) { - // Ignore if fsync fails. We tried. - } - - if (!tempFile.renameTo(File(filename))) { - throw IOException() - } - } - } else { - var outputStream: OutputStream? = null - try { - outputStream = contentResolver.openOutputStream(uri, "rwt") - outputStream?.let { definedOutputStream -> - val databaseOutput = mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) } - ?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, definedOutputStream) } - databaseOutput?.output() - } - } catch (e: Exception) { - throw IOException(e) - } finally { - outputStream?.close() - } - } - this.fileUri = uri - this.dataModifiedSinceLastLoading = false - } - fun clearIndexesAndBinaries(filesDirectory: File? = null) { this.mDatabaseKDB?.clearIndexes() this.mDatabaseKDBX?.clearIndexes() @@ -1245,6 +1301,18 @@ class Database { return mDatabaseKDBX != null } + fun allowCustomSearchableGroup(): Boolean { + return mDatabaseKDBX != null + } + + fun allowAutoType(): Boolean { + return mDatabaseKDBX != null + } + + fun allowTags(): Boolean { + return mDatabaseKDBX != null + } + /** * Remove oldest history for each entry if more than max items or max memory */ diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt index 72f2182ee..d7c6d9211 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt @@ -26,6 +26,7 @@ import com.kunzisoft.androidclearchroma.ChromaUtil import com.kunzisoft.keepass.database.element.binary.AttachmentPool import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseVersioned +import com.kunzisoft.keepass.database.element.entry.AutoType import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface @@ -276,6 +277,18 @@ class Entry : Node, EntryVersionedInterface { } } + var customData: CustomData + get() = entryKDBX?.customData ?: CustomData() + set(value) { + entryKDBX?.customData = value + } + + var autoType: AutoType + get() = entryKDBX?.autoType ?: AutoType() + set(value) { + entryKDBX?.autoType = value + } + private fun isTan(): Boolean { return title == PMS_TAN_ENTRY && username.isNotEmpty() } @@ -460,6 +473,8 @@ class Entry : Node, EntryVersionedInterface { entryInfo.tags = tags entryInfo.backgroundColor = backgroundColor entryInfo.foregroundColor = foregroundColor + entryInfo.customData = customData + entryInfo.autoType = autoType entryInfo.customFields = getExtraFields().toMutableList() // Add otpElement to generate token entryInfo.otpModel = getOtpElement()?.otpModel @@ -497,6 +512,8 @@ class Entry : Node, EntryVersionedInterface { tags = newEntryInfo.tags backgroundColor = newEntryInfo.backgroundColor foregroundColor = newEntryInfo.foregroundColor + customData = newEntryInfo.customData + autoType = newEntryInfo.autoType addExtraFields(newEntryInfo.customFields) database?.attachmentPool?.let { binaryPool -> newEntryInfo.attachments.forEach { attachment -> diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Group.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Group.kt index c1d001462..3f4e94155 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/Group.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Group.kt @@ -262,6 +262,12 @@ class Group : Node, GroupVersionedInterface { } } + var customData: CustomData + get() = groupKDBX?.customData ?: CustomData() + set(value) { + groupKDBX?.customData = value + } + override fun getChildGroups(): List { return groupKDB?.getChildGroups()?.map { Group(it) @@ -434,13 +440,36 @@ class Group : Node, GroupVersionedInterface { groupKDBX?.nodeId = id } - fun setEnableAutoType(enableAutoType: Boolean?) { - groupKDBX?.enableAutoType = enableAutoType + var searchable: Boolean? + get() = groupKDBX?.enableSearching + set(value) { + groupKDBX?.enableSearching = value + } + + fun isSearchable(): Boolean { + val searchableGroup = searchable + if (searchableGroup == null) { + val parenGroup = parent + if (parenGroup == null) + return true + else + return parenGroup.isSearchable() + } else { + return searchableGroup + } } - fun setEnableSearching(enableSearching: Boolean?) { - groupKDBX?.enableSearching = enableSearching - } + var enableAutoType: Boolean? + get() = groupKDBX?.enableAutoType + set(value) { + groupKDBX?.enableAutoType = value + } + + var defaultAutoTypeSequence: String + get() = groupKDBX?.defaultAutoTypeSequence ?: "" + set(value) { + groupKDBX?.defaultAutoTypeSequence = value + } fun setExpanded(expanded: Boolean) { groupKDBX?.isExpanded = expanded @@ -462,7 +491,11 @@ class Group : Node, GroupVersionedInterface { groupInfo.expires = expires groupInfo.expiryTime = expiryTime groupInfo.notes = notes + groupInfo.searchable = searchable + groupInfo.enableAutoType = enableAutoType + groupInfo.defaultAutoTypeSequence = defaultAutoTypeSequence groupInfo.tags = tags + groupInfo.customData = customData return groupInfo } @@ -475,7 +508,11 @@ class Group : Node, GroupVersionedInterface { expires = groupInfo.expires expiryTime = groupInfo.expiryTime notes = groupInfo.notes + searchable = groupInfo.searchable + enableAutoType = groupInfo.enableAutoType + defaultAutoTypeSequence = groupInfo.defaultAutoTypeSequence tags = groupInfo.tags + customData = groupInfo.customData } override fun equals(other: Any?): Boolean { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Tags.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Tags.kt index f9bc4f1f8..608a2f34b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/Tags.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Tags.kt @@ -53,6 +53,10 @@ class Tags: Parcelable { return mTags.isEmpty() } + fun isNotEmpty(): Boolean { + return !isEmpty() + } + fun size(): Int { return mTags.size } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt index c5cf1ce59..f10901014 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt @@ -64,6 +64,9 @@ class DatabaseKDB : DatabaseVersioned() { override val version: String get() = "V1" + override val defaultFileExtension: String + get() = ".kdb" + init { // New manual root because KDB contains multiple root groups (here available with getRootGroups()) rootGroup = createGroup().apply { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt index 3a1fa3c8b..adc05d675 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt @@ -201,6 +201,9 @@ class DatabaseKDBX : DatabaseVersioned { return "V2 - KDBX$kdbxStringVersion" } + override val defaultFileExtension: String + get() = ".kdbx" + private open class NodeOperationHandler : NodeHandler() { var containsCustomData = false override fun operate(node: T): Boolean { @@ -224,7 +227,7 @@ class DatabaseKDBX : DatabaseVersioned { private inner class GroupOperationHandler: NodeOperationHandler() { var containsTags = false override fun operate(node: GroupKDBX): Boolean { - if (!node.tags.isEmpty()) + if (node.tags.isNotEmpty()) containsTags = true return super.operate(node) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt index 260a828a2..3ffae91b2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt @@ -62,6 +62,7 @@ abstract class DatabaseVersioned< protected set abstract val version: String + abstract val defaultFileExtension: String /** * To manage binaries in faster way @@ -325,14 +326,6 @@ abstract class DatabaseVersioned< abstract fun isInRecycleBin(group: Group): Boolean - fun isGroupSearchable(group: Group?, omitBackup: Boolean): Boolean { - if (group == null) - return false - if (omitBackup && isInRecycleBin(group)) - return false - return true - } - fun clearIconsCache() { iconsManager.doForEachCustomIcon { _, binary -> try { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDBX.kt index 172091432..14dca1de5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDBX.kt @@ -41,9 +41,9 @@ class GroupKDBX : GroupVersioned, NodeKDBXInte override var customData = CustomData() var notes = "" var isExpanded = true - var defaultAutoTypeSequence = "" - var enableAutoType: Boolean? = null var enableSearching: Boolean? = null + var enableAutoType: Boolean? = null + var defaultAutoTypeSequence: String = "" var lastTopVisibleEntry: UUID = DatabaseVersioned.UUID_ZERO override var tags = Tags() override var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO @@ -69,11 +69,11 @@ class GroupKDBX : GroupVersioned, NodeKDBXInte customData = parcel.readParcelable(CustomData::class.java.classLoader) ?: CustomData() notes = parcel.readString() ?: notes isExpanded = parcel.readByte().toInt() != 0 - defaultAutoTypeSequence = parcel.readString() ?: defaultAutoTypeSequence - val isAutoTypeEnabled = parcel.readInt() - enableAutoType = if (isAutoTypeEnabled == -1) null else isAutoTypeEnabled == 1 val isSearchingEnabled = parcel.readInt() enableSearching = if (isSearchingEnabled == -1) null else isSearchingEnabled == 1 + val isAutoTypeEnabled = parcel.readInt() + enableAutoType = if (isAutoTypeEnabled == -1) null else isAutoTypeEnabled == 1 + defaultAutoTypeSequence = parcel.readString() ?: defaultAutoTypeSequence lastTopVisibleEntry = parcel.readSerializable() as UUID tags = parcel.readParcelable(Tags::class.java.classLoader) ?: tags previousParentGroup = parcel.readParcelable(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO @@ -94,9 +94,9 @@ class GroupKDBX : GroupVersioned, NodeKDBXInte dest.writeParcelable(customData, flags) dest.writeString(notes) dest.writeByte((if (isExpanded) 1 else 0).toByte()) - dest.writeString(defaultAutoTypeSequence) - dest.writeInt(if (enableAutoType == null) -1 else if (enableAutoType!!) 1 else 0) dest.writeInt(if (enableSearching == null) -1 else if (enableSearching!!) 1 else 0) + dest.writeInt(if (enableAutoType == null) -1 else if (enableAutoType!!) 1 else 0) + dest.writeString(defaultAutoTypeSequence) dest.writeSerializable(lastTopVisibleEntry) dest.writeParcelable(tags, flags) dest.writeParcelable(ParcelUuid(previousParentGroup), flags) @@ -111,9 +111,9 @@ class GroupKDBX : GroupVersioned, NodeKDBXInte customData = CustomData(source.customData) notes = source.notes isExpanded = source.isExpanded - defaultAutoTypeSequence = source.defaultAutoTypeSequence - enableAutoType = source.enableAutoType enableSearching = source.enableSearching + enableAutoType = source.enableAutoType + defaultAutoTypeSequence = source.defaultAutoTypeSequence lastTopVisibleEntry = source.lastTopVisibleEntry tags = source.tags previousParentGroup = source.previousParentGroup diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/template/TemplateField.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/template/TemplateField.kt index 19a2c23b7..07665a8b4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/template/TemplateField.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/template/TemplateField.kt @@ -103,7 +103,7 @@ object TemplateField { LABEL_SSID.equals(name, true) -> context.getString(R.string.ssid) LABEL_TYPE.equals(name, true) -> context.getString(R.string.type) LABEL_CRYPTOCURRENCY.equals(name, true) -> context.getString(R.string.cryptocurrency) - LABEL_TOKEN.equals(name, true) -> context.getString(R.string.token) + LABEL_TOKEN.equals(name, false) -> context.getString(R.string.token) LABEL_PUBLIC_KEY.equals(name, true) -> context.getString(R.string.public_key) LABEL_PRIVATE_KEY.equals(name, true) -> context.getString(R.string.private_key) LABEL_SEED.equals(name, true) -> context.getString(R.string.seed) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt index 7ff7e1294..321876af4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt @@ -43,15 +43,8 @@ abstract class DatabaseInput> (protected var m @Throws(LoadDatabaseException::class) abstract fun openDatabase(databaseInputStream: InputStream, - password: String?, - keyfileInputStream: InputStream?, - progressTaskUpdater: ProgressTaskUpdater?): D - - - @Throws(LoadDatabaseException::class) - abstract fun openDatabase(databaseInputStream: InputStream, - masterKey: ByteArray, - progressTaskUpdater: ProgressTaskUpdater?): D + progressTaskUpdater: ProgressTaskUpdater?, + assignMasterKey: (() -> Unit)): D protected fun startKeyTimer(progressTaskUpdater: ProgressTaskUpdater?) { progressTaskUpdater?.updateMessage(R.string.retrieving_db_key) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt index 7a019bbca..8d9373ecd 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt @@ -39,7 +39,6 @@ import java.security.MessageDigest import java.util.* import javax.crypto.Cipher import javax.crypto.CipherInputStream -import kotlin.collections.HashMap /** @@ -50,27 +49,8 @@ class DatabaseInputKDB(database: DatabaseKDB) @Throws(LoadDatabaseException::class) override fun openDatabase(databaseInputStream: InputStream, - password: String?, - keyfileInputStream: InputStream?, - progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDB { - return openDatabase(databaseInputStream, progressTaskUpdater) { - mDatabase.retrieveMasterKey(password, keyfileInputStream) - } - } - - @Throws(LoadDatabaseException::class) - override fun openDatabase(databaseInputStream: InputStream, - masterKey: ByteArray, - progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDB { - return openDatabase(databaseInputStream, progressTaskUpdater) { - mDatabase.masterKey = masterKey - } - } - - @Throws(LoadDatabaseException::class) - private fun openDatabase(databaseInputStream: InputStream, - progressTaskUpdater: ProgressTaskUpdater?, - assignMasterKey: (() -> Unit)? = null): DatabaseKDB { + progressTaskUpdater: ProgressTaskUpdater?, + assignMasterKey: (() -> Unit)): DatabaseKDB { try { startKeyTimer(progressTaskUpdater) @@ -96,7 +76,7 @@ class DatabaseInputKDB(database: DatabaseKDB) throw VersionDatabaseException() } - assignMasterKey?.invoke() + assignMasterKey.invoke() // Select algorithm when { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt index 35d3a7661..ed6e748c7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt @@ -101,27 +101,8 @@ class DatabaseInputKDBX(database: DatabaseKDBX) @Throws(LoadDatabaseException::class) override fun openDatabase(databaseInputStream: InputStream, - password: String?, - keyfileInputStream: InputStream?, - progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDBX { - return openDatabase(databaseInputStream, progressTaskUpdater) { - mDatabase.retrieveMasterKey(password, keyfileInputStream) - } - } - - @Throws(LoadDatabaseException::class) - override fun openDatabase(databaseInputStream: InputStream, - masterKey: ByteArray, - progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDBX { - return openDatabase(databaseInputStream, progressTaskUpdater) { - mDatabase.masterKey = masterKey - } - } - - @Throws(LoadDatabaseException::class) - private fun openDatabase(databaseInputStream: InputStream, - progressTaskUpdater: ProgressTaskUpdater?, - assignMasterKey: (() -> Unit)? = null): DatabaseKDBX { + progressTaskUpdater: ProgressTaskUpdater?, + assignMasterKey: (() -> Unit)): DatabaseKDBX { try { startKeyTimer(progressTaskUpdater) @@ -133,7 +114,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX) hashOfHeader = headerAndHash.hash val pbHeader = headerAndHash.header - assignMasterKey?.invoke() + assignMasterKey.invoke() mDatabase.makeFinalKey(header.masterSeed) stopKeyTimer() diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt index 6677e6fb8..7228efeb5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt @@ -663,7 +663,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) private fun writeTags(tags: Tags) { - if (!tags.isEmpty()) { + if (tags.isNotEmpty()) { writeString(DatabaseKDBXXML.ElemTags, tags.toString()) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/merge/DatabaseKDBXMerger.kt b/app/src/main/java/com/kunzisoft/keepass/database/merge/DatabaseKDBXMerger.kt index 1b2310d81..25c3847ce 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/merge/DatabaseKDBXMerger.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/merge/DatabaseKDBXMerger.kt @@ -30,6 +30,7 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.node.NodeId +import com.kunzisoft.keepass.database.element.node.NodeIdInt import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.utils.readAllBytes import java.io.IOException @@ -43,7 +44,6 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) { * Merge a KDB database in a KDBX database, by default all data are copied from the KDB */ fun merge(databaseToMerge: DatabaseKDB) { - // TODO Test KDB merge val rootGroup = database.rootGroup val rootGroupId = rootGroup?.nodeId val rootGroupToMerge = databaseToMerge.rootGroup @@ -53,6 +53,11 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) { throw IOException("Database is not open") } + // Replace the UUID of the KDB root group to init seed + databaseToMerge.removeGroupIndex(rootGroupToMerge) + rootGroupToMerge.nodeId = NodeIdInt(0) + databaseToMerge.addGroupIndex(rootGroupToMerge) + // Merge children rootGroupToMerge.doForEachChild( object : NodeHandler() { @@ -87,31 +92,57 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) { val entry = database.getEntryById(entryId) databaseToMerge.getEntryById(entryId)?.let { srcEntryToMerge -> - // Retrieve parent in current database - var parentEntryToMerge: GroupKDBX? = null - srcEntryToMerge.parent?.nodeId?.let { - val parentGroupIdToMerge = getNodeIdUUIDFrom(seed, it) - parentEntryToMerge = database.getGroupById(parentGroupIdToMerge) - } - val entryToMerge = EntryKDBX().apply { - this.nodeId = srcEntryToMerge.nodeId - this.icon = srcEntryToMerge.icon - this.creationTime = DateInstant(srcEntryToMerge.creationTime) - this.lastModificationTime = DateInstant(srcEntryToMerge.lastModificationTime) - this.lastAccessTime = DateInstant(srcEntryToMerge.lastAccessTime) - this.expiryTime = DateInstant(srcEntryToMerge.expiryTime) - this.expires = srcEntryToMerge.expires - this.title = srcEntryToMerge.title - this.username = srcEntryToMerge.username - this.password = srcEntryToMerge.password - this.url = srcEntryToMerge.url - this.notes = srcEntryToMerge.notes - // TODO attachment - } - if (entry != null) { - entry.updateWith(entryToMerge, false) - } else if (parentEntryToMerge != null) { - database.addEntryTo(entryToMerge, parentEntryToMerge) + // Do not merge meta stream elements + if (!srcEntryToMerge.isMetaStream()) { + // Retrieve parent in current database + var parentEntryToMerge: GroupKDBX? = null + srcEntryToMerge.parent?.nodeId?.let { + val parentGroupIdToMerge = getNodeIdUUIDFrom(seed, it) + parentEntryToMerge = database.getGroupById(parentGroupIdToMerge) + } + // Copy attachment + var newAttachment: Attachment? = null + srcEntryToMerge.getAttachment(databaseToMerge.attachmentPool)?.let { attachment -> + val binarySize = attachment.binaryData.getSize() + val binaryData = database.buildNewBinaryAttachment( + isRAMSufficient.invoke(binarySize), + attachment.binaryData.isCompressed, + attachment.binaryData.isProtected + ) + attachment.binaryData.getInputDataStream(databaseToMerge.binaryCache) + .use { inputStream -> + binaryData.getOutputDataStream(database.binaryCache) + .use { outputStream -> + inputStream.readAllBytes { buffer -> + outputStream.write(buffer) + } + } + } + newAttachment = Attachment(attachment.name, binaryData) + } + // Create new entry format + val entryToMerge = EntryKDBX().apply { + this.nodeId = srcEntryToMerge.nodeId + this.icon = srcEntryToMerge.icon + this.creationTime = DateInstant(srcEntryToMerge.creationTime) + this.lastModificationTime = DateInstant(srcEntryToMerge.lastModificationTime) + this.lastAccessTime = DateInstant(srcEntryToMerge.lastAccessTime) + this.expiryTime = DateInstant(srcEntryToMerge.expiryTime) + this.expires = srcEntryToMerge.expires + this.title = srcEntryToMerge.title + this.username = srcEntryToMerge.username + this.password = srcEntryToMerge.password + this.url = srcEntryToMerge.url + this.notes = srcEntryToMerge.notes + newAttachment?.let { + this.putAttachment(it, database.attachmentPool) + } + } + if (entry != null) { + entry.updateWith(entryToMerge, false) + } else if (parentEntryToMerge != null) { + database.addEntryTo(entryToMerge, parentEntryToMerge) + } } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/search/SearchHelper.kt b/app/src/main/java/com/kunzisoft/keepass/database/search/SearchHelper.kt index 594f077e4..55b5d58b2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/search/SearchHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/search/SearchHelper.kt @@ -24,6 +24,7 @@ import com.kunzisoft.keepass.database.action.node.NodeHandler import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Group +import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_FIELD @@ -37,7 +38,7 @@ class SearchHelper { fun createVirtualGroupWithSearchResult(database: Database, searchParameters: SearchParameters, - omitBackup: Boolean, + fromGroup: NodeId<*>? = null, max: Int): Group? { val searchGroup = database.createGroup(virtual = true) @@ -45,7 +46,15 @@ class SearchHelper { // Search all entries incrementEntry = 0 - database.rootGroup?.doForEachChild( + + val allowCustomSearchable = database.allowCustomSearchableGroup() + val startGroup = if (searchParameters.searchInCurrentGroup && fromGroup != null) { + database.getGroupById(fromGroup) ?: database.rootGroup + } else { + database.rootGroup + } + if (groupConditions(database, startGroup, searchParameters, allowCustomSearchable, max)) { + startGroup?.doForEachChild( object : NodeHandler() { override fun operate(node: Entry): Boolean { if (incrementEntry >= max) @@ -62,19 +71,43 @@ class SearchHelper { }, object : NodeHandler() { override fun operate(node: Group): Boolean { - return when { - incrementEntry >= max -> false - database.isGroupSearchable(node, omitBackup) -> true - else -> false - } + return groupConditions(database, + node, + searchParameters, + allowCustomSearchable, + max + ) } }, - false) + false + ) + } searchGroup?.refreshNumberOfChildEntries() return searchGroup } + private fun groupConditions(database: Database, + group: Group?, + searchParameters: SearchParameters, + allowCustomSearchable: Boolean, + max: Int): Boolean { + return if (group == null) + false + else if (incrementEntry >= max) + false + else if (database.groupIsInRecycleBin(group)) + searchParameters.searchInRecycleBin + else if (database.groupIsInTemplates(group)) + searchParameters.searchInTemplates + else if (!allowCustomSearchable) + true + else if (searchParameters.searchInSearchableGroup) + group.isSearchable() + else + true + } + private fun entryContainsString(database: Database, entry: Entry, searchParameters: SearchParameters): Boolean { @@ -88,7 +121,18 @@ class SearchHelper { } companion object { - const val MAX_SEARCH_ENTRY = 10 + const val MAX_SEARCH_ENTRY = 1000 + + /** + * Method to show the number of search results with max results + */ + fun showNumberOfSearchResults(number: Int): String { + return if (number >= MAX_SEARCH_ENTRY) { + (MAX_SEARCH_ENTRY-1).toString() + "+" + } else { + number.toString() + } + } /** * Utility method to perform actions if item is found or not after an auto search in [database] @@ -110,7 +154,6 @@ class SearchHelper { // If search provide results database.createVirtualGroupFromSearchInfo( searchInfo.toString(), - PreferencesUtil.omitBackup(context), MAX_SEARCH_ENTRY )?.let { searchGroup -> if (searchGroup.numberOfChildEntries > 0) { @@ -132,16 +175,23 @@ class SearchHelper { fun searchInEntry(entry: Entry, searchParameters: SearchParameters): Boolean { val searchQuery = searchParameters.searchQuery - // Entry don't contains string if the search string is empty + + // Not found if the search string is empty if (searchQuery.isEmpty()) return false + // Exclude entry expired + if (searchParameters.excludeExpired) { + if (entry.isCurrentlyExpires) + return false + } + // Search all strings in the KDBX entry if (searchParameters.searchInTitles) { if (checkSearchQuery(entry.title, searchParameters)) return true } - if (searchParameters.searchInUserNames) { + if (searchParameters.searchInUsernames) { if (checkSearchQuery(entry.username, searchParameters)) return true } @@ -158,8 +208,8 @@ class SearchHelper { return true } if (searchParameters.searchInUUIDs) { - val hexString = UuidUtil.toHexString(entry.nodeId.id) - if (hexString != null && hexString.contains(searchQuery, true)) + val hexString = UuidUtil.toHexString(entry.nodeId.id) ?: "" + if (checkSearchQuery(hexString, searchParameters)) return true } if (searchParameters.searchInOther) { @@ -171,21 +221,31 @@ class SearchHelper { } } } + if (searchParameters.searchInTags) { + if (checkSearchQuery(entry.tags.toString(), searchParameters)) + return true + } return false } private fun checkSearchQuery(stringToCheck: String, searchParameters: SearchParameters): Boolean { /* // TODO Search settings - var regularExpression = false - var ignoreCase = true var removeAccents = true <- Too much time, to study - var excludeExpired = false - var searchOnlyInCurrentGroup = false */ - return stringToCheck.isNotEmpty() - && stringToCheck.contains( - searchParameters.searchQuery, true) + if (stringToCheck.isEmpty()) + return false + return if (searchParameters.isRegex) { + val regex = if (searchParameters.caseSensitive) { + searchParameters.searchQuery.toRegex(RegexOption.DOT_MATCHES_ALL) + } else { + searchParameters.searchQuery + .toRegex(setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.IGNORE_CASE)) + } + regex.matches(stringToCheck) + } else { + stringToCheck.contains(searchParameters.searchQuery, !searchParameters.caseSensitive) + } } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/search/SearchParameters.kt b/app/src/main/java/com/kunzisoft/keepass/database/search/SearchParameters.kt index 7d08a0be5..34201a607 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/search/SearchParameters.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/search/SearchParameters.kt @@ -19,21 +19,82 @@ */ package com.kunzisoft.keepass.database.search +import android.os.Parcel +import android.os.Parcelable + /** * Parameters for searching strings in the database. */ -class SearchParameters { +class SearchParameters() : Parcelable{ var searchQuery: String = "" + var caseSensitive = false + var isRegex = false var searchInTitles = true - var searchInUserNames = true + var searchInUsernames = true var searchInPasswords = false var searchInUrls = true + var excludeExpired = false var searchInNotes = true var searchInOTP = false var searchInOther = true var searchInUUIDs = false - var searchInTags = true + var searchInTags = false + var searchInCurrentGroup = false + var searchInSearchableGroup = true + var searchInRecycleBin = false var searchInTemplates = false + + constructor(parcel: Parcel) : this() { + searchQuery = parcel.readString() ?: searchQuery + caseSensitive = parcel.readByte() != 0.toByte() + searchInTitles = parcel.readByte() != 0.toByte() + searchInUsernames = parcel.readByte() != 0.toByte() + searchInPasswords = parcel.readByte() != 0.toByte() + searchInUrls = parcel.readByte() != 0.toByte() + excludeExpired = parcel.readByte() != 0.toByte() + searchInNotes = parcel.readByte() != 0.toByte() + searchInOTP = parcel.readByte() != 0.toByte() + searchInOther = parcel.readByte() != 0.toByte() + searchInUUIDs = parcel.readByte() != 0.toByte() + searchInTags = parcel.readByte() != 0.toByte() + searchInCurrentGroup = parcel.readByte() != 0.toByte() + searchInSearchableGroup = parcel.readByte() != 0.toByte() + searchInRecycleBin = parcel.readByte() != 0.toByte() + searchInTemplates = parcel.readByte() != 0.toByte() + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(searchQuery) + parcel.writeByte(if (caseSensitive) 1 else 0) + parcel.writeByte(if (searchInTitles) 1 else 0) + parcel.writeByte(if (searchInUsernames) 1 else 0) + parcel.writeByte(if (searchInPasswords) 1 else 0) + parcel.writeByte(if (searchInUrls) 1 else 0) + parcel.writeByte(if (excludeExpired) 1 else 0) + parcel.writeByte(if (searchInNotes) 1 else 0) + parcel.writeByte(if (searchInOTP) 1 else 0) + parcel.writeByte(if (searchInOther) 1 else 0) + parcel.writeByte(if (searchInUUIDs) 1 else 0) + parcel.writeByte(if (searchInTags) 1 else 0) + parcel.writeByte(if (searchInCurrentGroup) 1 else 0) + parcel.writeByte(if (searchInSearchableGroup) 1 else 0) + parcel.writeByte(if (searchInRecycleBin) 1 else 0) + parcel.writeByte(if (searchInTemplates) 1 else 0) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): SearchParameters { + return SearchParameters(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/education/Education.kt b/app/src/main/java/com/kunzisoft/keepass/education/Education.kt index 8b7d52e71..e40ebbf58 100644 --- a/app/src/main/java/com/kunzisoft/keepass/education/Education.kt +++ b/app/src/main/java/com/kunzisoft/keepass/education/Education.kt @@ -31,23 +31,45 @@ import com.kunzisoft.keepass.R open class Education(val activity: Activity) { + private var mOneEducationHintOpen = false /** * Utility method to save preference after an education action */ protected fun checkAndPerformedEducation(isEducationAlreadyPerformed: Boolean, - tapTarget: TapTarget, - listener: TapTargetView.Listener, - saveEducationStringId: Int): Boolean { + tapTarget: TapTarget, + listener: TapTargetView.Listener, + saveEducationStringId: Int): Boolean { var doEducation = false - if (isEducationScreensEnabled()) { - if (isEducationAlreadyPerformed) { - try { - TapTargetView.showFor(activity, tapTarget, listener) - saveEducationPreference(activity, saveEducationStringId) - doEducation = true - } catch (e: Exception) { - Log.w(Education::class.java.name, "Can't performed education " + e.message) - } + if (isEducationScreensEnabled() + && !mOneEducationHintOpen + && !isEducationAlreadyPerformed) { + try { + TapTargetView.showFor(activity, tapTarget, object : TapTargetView.Listener() { + override fun onTargetClick(view: TapTargetView) { + mOneEducationHintOpen = false + saveEducationPreference(activity, saveEducationStringId) + super.onTargetClick(view) + listener.onTargetClick(view) + } + + override fun onOuterCircleClick(view: TapTargetView?) { + mOneEducationHintOpen = false + saveEducationPreference(activity, saveEducationStringId) + super.onOuterCircleClick(view) + listener.onOuterCircleClick(view) + view?.dismiss(false) + } + + override fun onTargetCancel(view: TapTargetView?) { + mOneEducationHintOpen = false + saveEducationPreference(activity, saveEducationStringId) + super.onTargetCancel(view) + } + }) + mOneEducationHintOpen = true + doEducation = true + } catch (e: Exception) { + Log.w(Education::class.java.name, "Can't performed education " + e.message) } } return doEducation @@ -117,6 +139,29 @@ open class Education(val activity: Activity) { R.string.education_add_attachment_key, R.string.education_setup_OTP_key) + fun putPropertiesInEducationPreferences(context: Context, + editor: SharedPreferences.Editor, + name: String, + value: String) { + when (name) { + context.getString(R.string.education_create_db_key) -> editor.putBoolean(name, value.toBoolean()) + context.getString(R.string.education_select_db_key) -> editor.putBoolean(name, value.toBoolean()) + context.getString(R.string.education_unlock_key) -> editor.putBoolean(name, value.toBoolean()) + context.getString(R.string.education_read_only_key) -> editor.putBoolean(name, value.toBoolean()) + context.getString(R.string.education_biometric_key) -> editor.putBoolean(name, value.toBoolean()) + context.getString(R.string.education_search_key) -> editor.putBoolean(name, value.toBoolean()) + context.getString(R.string.education_new_node_key) -> editor.putBoolean(name, value.toBoolean()) + context.getString(R.string.education_sort_key) -> editor.putBoolean(name, value.toBoolean()) + context.getString(R.string.education_lock_key) -> editor.putBoolean(name, value.toBoolean()) + context.getString(R.string.education_copy_username_key) -> editor.putBoolean(name, value.toBoolean()) + context.getString(R.string.education_entry_edit_key) -> editor.putBoolean(name, value.toBoolean()) + context.getString(R.string.education_password_generator_key) -> editor.putBoolean(name, value.toBoolean()) + context.getString(R.string.education_entry_new_field_key) -> editor.putBoolean(name, value.toBoolean()) + context.getString(R.string.education_add_attachment_key) -> editor.putBoolean(name, value.toBoolean()) + context.getString(R.string.education_setup_OTP_key) -> editor.putBoolean(name, value.toBoolean()) + } + } + /** * Get preferences bundle for education */ diff --git a/app/src/main/java/com/kunzisoft/keepass/education/EntryActivityEducation.kt b/app/src/main/java/com/kunzisoft/keepass/education/EntryActivityEducation.kt index 5f45d85f7..e4792855c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/education/EntryActivityEducation.kt +++ b/app/src/main/java/com/kunzisoft/keepass/education/EntryActivityEducation.kt @@ -32,8 +32,7 @@ class EntryActivityEducation(activity: Activity) fun checkAndPerformedEntryCopyEducation(educationView: View, onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { - return checkAndPerformedEducation( - !isEducationCopyUsernamePerformed(activity), + return checkAndPerformedEducation(isEducationCopyUsernamePerformed(activity), TapTarget.forView(educationView, activity.getString(R.string.education_field_copy_title), activity.getString(R.string.education_field_copy_summary)) @@ -64,8 +63,7 @@ class EntryActivityEducation(activity: Activity) fun checkAndPerformedEntryEditEducation(educationView: View, onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { - return checkAndPerformedEducation( - !isEducationEntryEditPerformed(activity), + return checkAndPerformedEducation(isEducationEntryEditPerformed(activity), TapTarget.forView(educationView, activity.getString(R.string.education_entry_edit_title), activity.getString(R.string.education_entry_edit_summary)) diff --git a/app/src/main/java/com/kunzisoft/keepass/education/EntryEditActivityEducation.kt b/app/src/main/java/com/kunzisoft/keepass/education/EntryEditActivityEducation.kt index b4aa83ba5..39092833c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/education/EntryEditActivityEducation.kt +++ b/app/src/main/java/com/kunzisoft/keepass/education/EntryEditActivityEducation.kt @@ -35,7 +35,7 @@ class EntryEditActivityEducation(activity: Activity) fun checkAndPerformedGeneratePasswordEducation(educationView: View, onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { - return checkAndPerformedEducation(!isEducationPasswordGeneratorPerformed(activity), + return checkAndPerformedEducation(isEducationPasswordGeneratorPerformed(activity), TapTarget.forView(educationView, activity.getString(R.string.education_generate_password_title), activity.getString(R.string.education_generate_password_summary)) @@ -66,7 +66,7 @@ class EntryEditActivityEducation(activity: Activity) fun checkAndPerformedEntryNewFieldEducation(educationView: View, onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { - return checkAndPerformedEducation(!isEducationEntryNewFieldPerformed(activity), + return checkAndPerformedEducation(isEducationEntryNewFieldPerformed(activity), TapTarget.forView(educationView, activity.getString(R.string.education_entry_new_field_title), activity.getString(R.string.education_entry_new_field_summary)) @@ -97,7 +97,7 @@ class EntryEditActivityEducation(activity: Activity) fun checkAndPerformedAttachmentEducation(educationView: View, onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { - return checkAndPerformedEducation(!isEducationAddAttachmentPerformed(activity), + return checkAndPerformedEducation(isEducationAddAttachmentPerformed(activity), TapTarget.forView(educationView, activity.getString(R.string.education_add_attachment_title), activity.getString(R.string.education_add_attachment_summary)) @@ -128,7 +128,7 @@ class EntryEditActivityEducation(activity: Activity) fun checkAndPerformedSetUpOTPEducation(educationView: View, onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { - return checkAndPerformedEducation(!isEducationSetupOTPPerformed(activity), + return checkAndPerformedEducation(isEducationSetupOTPPerformed(activity), TapTarget.forView(educationView, activity.getString(R.string.education_setup_OTP_title), activity.getString(R.string.education_setup_OTP_summary)) diff --git a/app/src/main/java/com/kunzisoft/keepass/education/FileDatabaseSelectActivityEducation.kt b/app/src/main/java/com/kunzisoft/keepass/education/FileDatabaseSelectActivityEducation.kt index 7a58b943c..86ea81200 100644 --- a/app/src/main/java/com/kunzisoft/keepass/education/FileDatabaseSelectActivityEducation.kt +++ b/app/src/main/java/com/kunzisoft/keepass/education/FileDatabaseSelectActivityEducation.kt @@ -39,7 +39,7 @@ class FileDatabaseSelectActivityEducation(activity: Activity) onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { // Try to open the creation base education - return checkAndPerformedEducation(!isEducationCreateDatabasePerformed(activity), + return checkAndPerformedEducation(isEducationCreateDatabasePerformed(activity), TapTarget.forView(educationView, activity.getString(R.string.education_create_database_title), activity.getString(R.string.education_create_database_summary)) @@ -71,7 +71,7 @@ class FileDatabaseSelectActivityEducation(activity: Activity) fun checkAndPerformedSelectDatabaseEducation(educationView: View, onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { - return checkAndPerformedEducation(!isEducationSelectDatabasePerformed(activity), + return checkAndPerformedEducation(isEducationSelectDatabasePerformed(activity), TapTarget.forView(educationView, activity.getString(R.string.education_select_database_title), activity.getString(R.string.education_select_database_summary)) diff --git a/app/src/main/java/com/kunzisoft/keepass/education/GroupActivityEducation.kt b/app/src/main/java/com/kunzisoft/keepass/education/GroupActivityEducation.kt index 2293bd47c..23e67b948 100644 --- a/app/src/main/java/com/kunzisoft/keepass/education/GroupActivityEducation.kt +++ b/app/src/main/java/com/kunzisoft/keepass/education/GroupActivityEducation.kt @@ -31,7 +31,7 @@ class GroupActivityEducation(activity: Activity) fun checkAndPerformedAddNodeButtonEducation(educationView: View, onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { - return checkAndPerformedEducation(!isEducationNewNodePerformed(activity), + return checkAndPerformedEducation(isEducationNewNodePerformed(activity), TapTarget.forView(educationView, activity.getString(R.string.education_new_node_title), activity.getString(R.string.education_new_node_summary)) @@ -58,7 +58,7 @@ class GroupActivityEducation(activity: Activity) fun checkAndPerformedSearchMenuEducation(educationView: View, onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { - return checkAndPerformedEducation(!isEducationSearchPerformed(activity), + return checkAndPerformedEducation(isEducationSearchPerformed(activity), TapTarget.forView(educationView, activity.getString(R.string.education_search_title), activity.getString(R.string.education_search_summary)) @@ -85,7 +85,7 @@ class GroupActivityEducation(activity: Activity) fun checkAndPerformedSortMenuEducation(educationView: View, onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { - return checkAndPerformedEducation(!isEducationSortPerformed(activity), + return checkAndPerformedEducation(isEducationSortPerformed(activity), TapTarget.forView(educationView, activity.getString(R.string.education_sort_title), activity.getString(R.string.education_sort_summary)) @@ -112,7 +112,7 @@ class GroupActivityEducation(activity: Activity) fun checkAndPerformedLockMenuEducation(educationView: View, onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { - return checkAndPerformedEducation(!isEducationLockPerformed(activity), + return checkAndPerformedEducation(isEducationLockPerformed(activity), TapTarget.forView(educationView, activity.getString(R.string.education_lock_title), activity.getString(R.string.education_lock_summary)) diff --git a/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt b/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt index c3f58180d..dc4afd1d9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt +++ b/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt @@ -32,7 +32,7 @@ class PasswordActivityEducation(activity: Activity) fun checkAndPerformedUnlockEducation(educationView: View, onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { - return checkAndPerformedEducation(!isEducationUnlockPerformed(activity), + return checkAndPerformedEducation(isEducationUnlockPerformed(activity), TapTarget.forView(educationView, activity.getString(R.string.education_unlock_title), activity.getString(R.string.education_unlock_summary)) @@ -60,7 +60,7 @@ class PasswordActivityEducation(activity: Activity) fun checkAndPerformedReadOnlyEducation(educationView: View, onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { - return checkAndPerformedEducation(!isEducationReadOnlyPerformed(activity), + return checkAndPerformedEducation(isEducationReadOnlyPerformed(activity), TapTarget.forView(educationView, activity.getString(R.string.education_read_only_title), activity.getString(R.string.education_read_only_summary)) @@ -87,7 +87,7 @@ class PasswordActivityEducation(activity: Activity) fun checkAndPerformedBiometricEducation(educationView: View, onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { - return checkAndPerformedEducation(!isEducationBiometricPerformed(activity), + return checkAndPerformedEducation(isEducationBiometricPerformed(activity), TapTarget.forView(educationView, activity.getString(R.string.education_advanced_unlock_title), activity.getString(R.string.education_advanced_unlock_summary)) diff --git a/app/src/main/java/com/kunzisoft/keepass/model/CipherDecryptDatabase.kt b/app/src/main/java/com/kunzisoft/keepass/model/CipherDecryptDatabase.kt new file mode 100644 index 000000000..0b3503468 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/model/CipherDecryptDatabase.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2022 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePassDX. + * + * KeePassDX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePassDX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePassDX. If not, see . + * + */ +package com.kunzisoft.keepass.model + +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable +import com.kunzisoft.keepass.utils.readEnum +import com.kunzisoft.keepass.utils.writeEnum + +class CipherDecryptDatabase(): Parcelable { + + var databaseUri: Uri? = null + var credentialStorage: CredentialStorage = CredentialStorage.DEFAULT + var decryptedValue: ByteArray = byteArrayOf() + + constructor(parcel: Parcel): this() { + databaseUri = parcel.readParcelable(Uri::class.java.classLoader) + credentialStorage = parcel.readEnum() ?: credentialStorage + decryptedValue = ByteArray(parcel.readInt()) + parcel.readByteArray(decryptedValue) + } + + fun replaceContent(copy: CipherDecryptDatabase) { + this.decryptedValue = copy.decryptedValue + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeParcelable(databaseUri, flags) + parcel.writeEnum(credentialStorage) + parcel.writeInt(decryptedValue.size) + parcel.writeByteArray(decryptedValue) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): CipherDecryptDatabase { + return CipherDecryptDatabase(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CipherDecryptDatabase + + if (databaseUri != other.databaseUri) return false + + return true + } + + override fun hashCode(): Int { + return databaseUri.hashCode() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/model/CipherEncryptDatabase.kt b/app/src/main/java/com/kunzisoft/keepass/model/CipherEncryptDatabase.kt new file mode 100644 index 000000000..f3ed3dade --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/model/CipherEncryptDatabase.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2022 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePassDX. + * + * KeePassDX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePassDX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePassDX. If not, see . + * + */ +package com.kunzisoft.keepass.model + +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable +import com.kunzisoft.keepass.utils.readEnum +import com.kunzisoft.keepass.utils.writeEnum + +class CipherEncryptDatabase(): Parcelable { + + var databaseUri: Uri? = null + var credentialStorage: CredentialStorage = CredentialStorage.DEFAULT + var encryptedValue: ByteArray = byteArrayOf() + var specParameters: ByteArray = byteArrayOf() + + constructor(parcel: Parcel): this() { + databaseUri = parcel.readParcelable(Uri::class.java.classLoader) + credentialStorage = parcel.readEnum() ?: credentialStorage + encryptedValue = ByteArray(parcel.readInt()) + parcel.readByteArray(encryptedValue) + specParameters = ByteArray(parcel.readInt()) + parcel.readByteArray(specParameters) + } + + fun replaceContent(copy: CipherEncryptDatabase) { + this.encryptedValue = copy.encryptedValue + this.specParameters = copy.specParameters + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeParcelable(databaseUri, flags) + parcel.writeEnum(credentialStorage) + parcel.writeInt(encryptedValue.size) + parcel.writeByteArray(encryptedValue) + parcel.writeInt(specParameters.size) + parcel.writeByteArray(specParameters) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): CipherEncryptDatabase { + return CipherEncryptDatabase(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CipherEncryptDatabase + + if (databaseUri != other.databaseUri) return false + + return true + } + + override fun hashCode(): Int { + return databaseUri.hashCode() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/model/CredentialStorage.kt b/app/src/main/java/com/kunzisoft/keepass/model/CredentialStorage.kt new file mode 100644 index 000000000..b82d583b0 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/model/CredentialStorage.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2022 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePassDX. + * + * KeePassDX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePassDX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePassDX. If not, see . + * + */ +package com.kunzisoft.keepass.model + +enum class CredentialStorage { + PASSWORD, KEY_FILE, HARDWARE_KEY; + + companion object { + fun getFromOrdinal(ordinal: Int): CredentialStorage { + return when (ordinal) { + 0 -> PASSWORD + 1 -> KEY_FILE + 2 -> HARDWARE_KEY + else -> DEFAULT + } + } + + val DEFAULT: CredentialStorage + get() = PASSWORD + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt b/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt index 12d1b4b0a..bab761f92 100644 --- a/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt +++ b/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt @@ -23,6 +23,7 @@ import android.os.Parcel import android.os.ParcelUuid import android.os.Parcelable import com.kunzisoft.keepass.database.element.* +import com.kunzisoft.keepass.database.element.entry.AutoType import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.template.TemplateField import com.kunzisoft.keepass.otp.OtpElement @@ -42,6 +43,7 @@ class EntryInfo : NodeInfo { var foregroundColor: Int? = null var customFields: MutableList = mutableListOf() var attachments: MutableList = mutableListOf() + var autoType: AutoType = AutoType() var otpModel: OtpModel? = null var isTemplate: Boolean = false @@ -60,6 +62,7 @@ class EntryInfo : NodeInfo { foregroundColor = if (readFgColor == -1) null else readFgColor parcel.readList(customFields, Field::class.java.classLoader) parcel.readList(attachments, Attachment::class.java.classLoader) + autoType = parcel.readParcelable(AutoType::class.java.classLoader) ?: autoType otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel isTemplate = parcel.readByte().toInt() != 0 } @@ -80,6 +83,7 @@ class EntryInfo : NodeInfo { parcel.writeInt(foregroundColor ?: -1) parcel.writeList(customFields) parcel.writeList(attachments) + parcel.writeParcelable(autoType, flags) parcel.writeParcelable(otpModel, flags) parcel.writeByte((if (isTemplate) 1 else 0).toByte()) } @@ -105,6 +109,7 @@ class EntryInfo : NodeInfo { return customFields.lastOrNull { it.name == label }?.protectedValue?.toString() ?: "" } + // Return true if modified private fun addUniqueField(field: Field, number: Int = 0) { var sameName = false var sameValue = false @@ -126,7 +131,19 @@ class EntryInfo : NodeInfo { (customFields as ArrayList).add(Field(field.name + suffix, field.protectedValue)) } - fun saveSearchInfo(database: Database?, searchInfo: SearchInfo) { + private fun containsDomainOrApplicationId(search: String): Boolean { + if (url.contains(search)) + return true + return customFields.find { + it.protectedValue.stringValue.contains(search) + } != null + } + + /** + * Add searchInfo to current EntryInfo, return true if new data, false if no modification + */ + fun saveSearchInfo(database: Database?, searchInfo: SearchInfo): Boolean { + var modification = false searchInfo.otpString?.let { otpString -> // Replace the OTP field OtpEntryFields.parseOTPUri(otpString)?.let { otpElement -> @@ -141,31 +158,45 @@ class EntryInfo : NodeInfo { mutableCustomFields.remove(otpField) } mutableCustomFields.add(otpField) + modification = true } } ?: searchInfo.webDomain?.let { webDomain -> // If unable to save web domain in custom field or URL not populated, save in URL val scheme = searchInfo.webScheme - val webScheme = if (scheme.isNullOrEmpty()) "http" else scheme + val webScheme = if (scheme.isNullOrEmpty()) "https" else scheme val webDomainToStore = "$webScheme://$webDomain" - if (database?.allowEntryCustomFields() != true || url.isEmpty()) { - url = webDomainToStore - } else if (url != webDomainToStore) { - // Save web domain in custom field - addUniqueField(Field(WEB_DOMAIN_FIELD_NAME, - ProtectedString(false, webDomainToStore)), + if (!containsDomainOrApplicationId(webDomain)) { + if (database?.allowEntryCustomFields() != true || url.isEmpty()) { + url = webDomainToStore + } else { + // Save web domain in custom field + addUniqueField( + Field( + WEB_DOMAIN_FIELD_NAME, + ProtectedString(false, webDomainToStore) + ), 1 // Start to one because URL is a standard field name - ) + ) + } + modification = true } } ?: run { // Save application id in custom field if (database?.allowEntryCustomFields() == true) { searchInfo.applicationId?.let { applicationId -> - addUniqueField(Field(APPLICATION_ID_FIELD_NAME, - ProtectedString(false, applicationId)) - ) + if (!containsDomainOrApplicationId(applicationId)) { + addUniqueField( + Field( + APPLICATION_ID_FIELD_NAME, + ProtectedString(false, applicationId) + ) + ) + modification = true + } } } } + return modification } fun saveRegisterInfo(database: Database?, registerInfo: RegisterInfo) { @@ -209,6 +240,7 @@ class EntryInfo : NodeInfo { if (foregroundColor != other.foregroundColor) return false if (customFields != other.customFields) return false if (attachments != other.attachments) return false + if (autoType != other.autoType) return false if (otpModel != other.otpModel) return false if (isTemplate != other.isTemplate) return false @@ -227,6 +259,7 @@ class EntryInfo : NodeInfo { result = 31 * result + foregroundColor.hashCode() result = 31 * result + customFields.hashCode() result = 31 * result + attachments.hashCode() + result = 31 * result + autoType.hashCode() result = 31 * result + (otpModel?.hashCode() ?: 0) result = 31 * result + isTemplate.hashCode() return result diff --git a/app/src/main/java/com/kunzisoft/keepass/model/GroupInfo.kt b/app/src/main/java/com/kunzisoft/keepass/model/GroupInfo.kt index c7e146d29..fcd5df0d5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/model/GroupInfo.kt +++ b/app/src/main/java/com/kunzisoft/keepass/model/GroupInfo.kt @@ -12,6 +12,9 @@ class GroupInfo : NodeInfo { var id: UUID? = null var notes: String? = null + var searchable: Boolean? = null + var enableAutoType: Boolean? = null + var defaultAutoTypeSequence: String = "" var tags: Tags = Tags() init { @@ -23,6 +26,11 @@ class GroupInfo : NodeInfo { constructor(parcel: Parcel): super(parcel) { id = parcel.readParcelable(ParcelUuid::class.java.classLoader)?.uuid ?: id notes = parcel.readString() + val isSearchingEnabled = parcel.readInt() + searchable = if (isSearchingEnabled == -1) null else isSearchingEnabled == 1 + val isAutoTypeEnabled = parcel.readInt() + enableAutoType = if (isAutoTypeEnabled == -1) null else isAutoTypeEnabled == 1 + defaultAutoTypeSequence = parcel.readString() ?: defaultAutoTypeSequence tags = parcel.readParcelable(Tags::class.java.classLoader) ?: tags } @@ -31,6 +39,9 @@ class GroupInfo : NodeInfo { val uuid = if (id != null) ParcelUuid(id) else null parcel.writeParcelable(uuid, flags) parcel.writeString(notes) + parcel.writeInt(if (searchable == null) -1 else if (searchable!!) 1 else 0) + parcel.writeInt(if (enableAutoType == null) -1 else if (enableAutoType!!) 1 else 0) + parcel.writeString(defaultAutoTypeSequence) parcel.writeParcelable(tags, flags) } @@ -41,6 +52,9 @@ class GroupInfo : NodeInfo { if (id != other.id) return false if (notes != other.notes) return false + if (searchable != other.searchable) return false + if (enableAutoType != other.enableAutoType) return false + if (defaultAutoTypeSequence != other.defaultAutoTypeSequence) return false if (tags != other.tags) return false return true @@ -50,6 +64,9 @@ class GroupInfo : NodeInfo { var result = super.hashCode() result = 31 * result + (id?.hashCode() ?: 0) result = 31 * result + (notes?.hashCode() ?: 0) + result = 31 * result + searchable.hashCode() + result = 31 * result + enableAutoType.hashCode() + result = 31 * result + defaultAutoTypeSequence.hashCode() result = 31 * result + tags.hashCode() return result } diff --git a/app/src/main/java/com/kunzisoft/keepass/model/NodeInfo.kt b/app/src/main/java/com/kunzisoft/keepass/model/NodeInfo.kt index 95de2b790..5becba7b5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/model/NodeInfo.kt +++ b/app/src/main/java/com/kunzisoft/keepass/model/NodeInfo.kt @@ -2,6 +2,7 @@ package com.kunzisoft.keepass.model import android.os.Parcel import android.os.Parcelable +import com.kunzisoft.keepass.database.element.CustomData import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.utils.UuidUtil @@ -15,6 +16,7 @@ open class NodeInfo() : Parcelable { var lastModificationTime: DateInstant = DateInstant() var expires: Boolean = false var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH_DATE_TIME + var customData: CustomData = CustomData() constructor(parcel: Parcel) : this() { title = parcel.readString() ?: title @@ -23,6 +25,7 @@ open class NodeInfo() : Parcelable { lastModificationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: lastModificationTime expires = parcel.readInt() != 0 expiryTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: expiryTime + customData = parcel.readParcelable(CustomData::class.java.classLoader) ?: customData } override fun writeToParcel(parcel: Parcel, flags: Int) { @@ -32,6 +35,7 @@ open class NodeInfo() : Parcelable { parcel.writeParcelable(lastModificationTime, flags) parcel.writeInt(if (expires) 1 else 0) parcel.writeParcelable(expiryTime, flags) + parcel.writeParcelable(customData, flags) } override fun describeContents(): Int { @@ -48,6 +52,7 @@ open class NodeInfo() : Parcelable { if (lastModificationTime != other.lastModificationTime) return false if (expires != other.expires) return false if (expiryTime != other.expiryTime) return false + if (customData != other.customData) return false return true } @@ -59,6 +64,7 @@ open class NodeInfo() : Parcelable { result = 31 * result + lastModificationTime.hashCode() result = 31 * result + expires.hashCode() result = 31 * result + expiryTime.hashCode() + result = 31 * result + customData.hashCode() return result } diff --git a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt index 81e41cc27..fb04544a7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt @@ -22,12 +22,14 @@ package com.kunzisoft.keepass.services import android.app.PendingIntent import android.content.Intent import android.net.Uri -import android.os.* +import android.os.Binder +import android.os.Build +import android.os.Bundle +import android.os.IBinder import android.util.Log import androidx.media.app.NotificationCompat import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.GroupActivity -import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.database.action.* import com.kunzisoft.keepass.database.action.history.DeleteEntryHistoryDatabaseRunnable import com.kunzisoft.keepass.database.action.history.RestoreEntryHistoryDatabaseRunnable @@ -39,16 +41,19 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.Type +import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.timeout.TimeoutHelper -import com.kunzisoft.keepass.utils.* +import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION +import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION +import com.kunzisoft.keepass.utils.LOCK_ACTION +import com.kunzisoft.keepass.utils.closeDatabase import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo import kotlinx.coroutines.* import java.util.* -import kotlin.collections.ArrayList open class DatabaseTaskNotificationService : LockNotificationService(), ProgressTaskUpdater { @@ -226,7 +231,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress val actionRunnable: ActionRunnable? = when (intentAction) { ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent, database) ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent, database) - ACTION_DATABASE_MERGE_TASK -> buildDatabaseMergeActionTask(database) + ACTION_DATABASE_MERGE_TASK -> buildDatabaseMergeActionTask(intent, database) ACTION_DATABASE_RELOAD_TASK -> buildDatabaseReloadActionTask(database) ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent, database) ACTION_DATABASE_CREATE_GROUP_TASK -> buildDatabaseCreateGroupActionTask(intent, database) @@ -469,7 +474,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress intent?.removeExtra(DATABASE_URI_KEY) intent?.removeExtra(MAIN_CREDENTIAL_KEY) intent?.removeExtra(READ_ONLY_KEY) - intent?.removeExtra(CIPHER_ENTITY_KEY) + intent?.removeExtra(CIPHER_DATABASE_KEY) intent?.removeExtra(FIX_DUPLICATE_UUID_KEY) intent?.removeExtra(GROUP_KEY) intent?.removeExtra(ENTRY_KEY) @@ -570,13 +575,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress if (intent.hasExtra(DATABASE_URI_KEY) && intent.hasExtra(MAIN_CREDENTIAL_KEY) && intent.hasExtra(READ_ONLY_KEY) - && intent.hasExtra(CIPHER_ENTITY_KEY) + && intent.hasExtra(CIPHER_DATABASE_KEY) && intent.hasExtra(FIX_DUPLICATE_UUID_KEY) ) { val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY) val mainCredential: MainCredential = intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential() val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true) - val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY) + val cipherEncryptDatabase: CipherEncryptDatabase? = intent.getParcelableExtra(CIPHER_DATABASE_KEY) if (databaseUri == null) return null @@ -589,7 +594,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress databaseUri, mainCredential, readOnly, - cipherEntity, + cipherEncryptDatabase, intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false), this ) { result -> @@ -598,7 +603,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress putParcelable(DATABASE_URI_KEY, databaseUri) putParcelable(MAIN_CREDENTIAL_KEY, mainCredential) putBoolean(READ_ONLY_KEY, readOnly) - putParcelable(CIPHER_ENTITY_KEY, cipherEntity) + putParcelable(CIPHER_DATABASE_KEY, cipherEncryptDatabase) } } } else { @@ -606,10 +611,21 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress } } - private fun buildDatabaseMergeActionTask(database: Database): ActionRunnable { + private fun buildDatabaseMergeActionTask(intent: Intent, database: Database): ActionRunnable { + var databaseToMergeUri: Uri? = null + var databaseToMergeMainCredential: MainCredential? = null + if (intent.hasExtra(DATABASE_URI_KEY)) { + databaseToMergeUri = intent.getParcelableExtra(DATABASE_URI_KEY) + } + if (intent.hasExtra(MAIN_CREDENTIAL_KEY)) { + databaseToMergeMainCredential = intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) + } + return MergeDatabaseRunnable( this, database, + databaseToMergeUri, + databaseToMergeMainCredential, this ) { result -> // No need to add each info to reload database @@ -633,7 +649,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress && intent.hasExtra(MAIN_CREDENTIAL_KEY) ) { val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) ?: return null - AssignPasswordInDatabaseRunnable(this, + AssignMainCredentialInDatabaseRunnable(this, database, databaseUri, intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential() @@ -911,9 +927,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress */ private fun buildDatabaseSave(intent: Intent, database: Database): ActionRunnable? { return if (intent.hasExtra(SAVE_DATABASE_KEY)) { + + var databaseCopyUri: Uri? = null + if (intent.hasExtra(DATABASE_URI_KEY)) { + databaseCopyUri = intent.getParcelableExtra(DATABASE_URI_KEY) + } + SaveDatabaseRunnable(this, database, - !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)) + !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false), + databaseCopyUri) } else { null } @@ -963,7 +986,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress const val DATABASE_URI_KEY = "DATABASE_URI_KEY" const val MAIN_CREDENTIAL_KEY = "MAIN_CREDENTIAL_KEY" const val READ_ONLY_KEY = "READ_ONLY_KEY" - const val CIPHER_ENTITY_KEY = "CIPHER_ENTITY_KEY" + const val CIPHER_DATABASE_KEY = "CIPHER_DATABASE_KEY" const val FIX_DUPLICATE_UUID_KEY = "FIX_DUPLICATE_UUID_KEY" const val GROUP_KEY = "GROUP_KEY" const val ENTRY_KEY = "ENTRY_KEY" diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsFragment.kt index 35dab43ae..1e9fa538a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsFragment.kt @@ -41,12 +41,12 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() { } } - override fun onDisplayPreferenceDialog(preference: Preference?) { + override fun onDisplayPreferenceDialog(preference: Preference) { var otherDialogFragment = false var dialogFragment: DialogFragment? = null - when (preference?.key) { + when (preference.key) { getString(R.string.autofill_application_id_blocklist_key) -> { dialogFragment = AutofillBlocklistAppIdPreferenceDialogFragmentCompat.newInstance(preference.key) } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/MagikeyboardSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/MagikeyboardSettingsFragment.kt index c1af92d98..9616fd7a7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/MagikeyboardSettingsFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/MagikeyboardSettingsFragment.kt @@ -34,13 +34,13 @@ class MagikeyboardSettingsFragment : PreferenceFragmentCompat() { setPreferencesFromResource(R.xml.preferences_keyboard, rootKey) } - override fun onDisplayPreferenceDialog(preference: Preference?) { + override fun onDisplayPreferenceDialog(preference: Preference) { var otherDialogFragment = false var dialogFragment: DialogFragment? = null // Main Preferences - when (preference?.key) { + when (preference.key) { getString(R.string.keyboard_entry_timeout_key) -> { dialogFragment = DurationDialogFragmentCompat.newInstance(preference.key) } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt index 588410864..1885202e1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt @@ -365,7 +365,9 @@ class NestedAppSettingsFragment : NestedSettingsFragment() { ) { _, _ -> validate?.invoke() deleteKeysAlertDialog?.setOnDismissListener(null) - AdvancedUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + AdvancedUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity) + } } .setNegativeButton(resources.getString(android.R.string.cancel) ) { _, _ ->} @@ -437,9 +439,9 @@ class NestedAppSettingsFragment : NestedSettingsFragment() { } } - override fun onPreferenceTreeClick(preference: Preference?): Boolean { + override fun onPreferenceTreeClick(preference: Preference): Boolean { // To reload group when appearance settings are modified - when (preference?.key) { + when (preference.key) { getString(R.string.setting_style_key), getString(R.string.setting_style_brightness_key), getString(R.string.setting_icon_pack_choose_key), @@ -459,13 +461,13 @@ class NestedAppSettingsFragment : NestedSettingsFragment() { return super.onPreferenceTreeClick(preference) } - override fun onDisplayPreferenceDialog(preference: Preference?) { + override fun onDisplayPreferenceDialog(preference: Preference) { var otherDialogFragment = false var dialogFragment: DialogFragment? = null // Main Preferences - when (preference?.key) { + when (preference.key) { getString(R.string.app_timeout_key), getString(R.string.clipboard_timeout_key), getString(R.string.temp_advanced_unlock_timeout_key) -> { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt index c55a727f3..3c2c7c0d8 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt @@ -30,7 +30,7 @@ import androidx.preference.PreferenceCategory import androidx.preference.SwitchPreference import com.kunzisoft.androidclearchroma.ChromaUtil import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment +import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm @@ -43,7 +43,6 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.settings.preference.* import com.kunzisoft.keepass.settings.preferencedialogfragment.* import com.kunzisoft.keepass.tasks.ActionRunnable -import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.viewmodels.DatabaseViewModel class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval { @@ -155,18 +154,22 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev // Database name dbNamePref = findPreference(getString(R.string.database_name_key)) - if (database.allowName) { - dbNamePref?.summary = database.name - } else { - dbGeneralPrefCategory?.removePreference(dbNamePref) + dbNamePref?.let { namePreference -> + if (database.allowName) { + namePreference.summary = database.name + } else { + dbGeneralPrefCategory?.removePreference(namePreference) + } } // Database description dbDescriptionPref = findPreference(getString(R.string.database_description_key)) - if (database.allowDescription) { - dbDescriptionPref?.summary = database.description - } else { - dbGeneralPrefCategory?.removePreference(dbDescriptionPref) + dbDescriptionPref?.let { descriptionPreference -> + if (database.allowDescription) { + dbDescriptionPref?.summary = database.description + } else { + dbGeneralPrefCategory?.removePreference(descriptionPreference) + } } // Database default username @@ -238,7 +241,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev // Templates val templatesGroupPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_category_templates_key)) templatesGroupPref = findPreference(getString(R.string.templates_group_uuid_key)) - if (database.allowConfigurableTemplatesGroup) { + if (database.allowTemplatesGroup) { val templatesEnablePref: SwitchPreference? = findPreference(getString(R.string.templates_group_enable_key)) templatesEnablePref?.apply { isChecked = database.isTemplatesEnabled @@ -334,7 +337,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev findPreference(getString(R.string.settings_database_change_credentials_key))?.apply { isEnabled = if (!mDatabaseReadOnly) { onPreferenceClickListener = Preference.OnPreferenceClickListener { - AssignMasterKeyDialogFragment.getInstance(database.allowNoMasterKey) + SetMainCredentialDialogFragment.getInstance(database.allowNoMasterKey) .show(parentFragmentManager, "passwordDialog") false } @@ -355,7 +358,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { val view = super.onCreateView(inflater, container, savedInstanceState) try { @@ -565,13 +568,13 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev } } - override fun onDisplayPreferenceDialog(preference: Preference?) { + override fun onDisplayPreferenceDialog(preference: Preference) { var otherDialogFragment = false var dialogFragment: DialogFragment? = null // Main Preferences - when (preference?.key) { + when (preference.key) { getString(R.string.database_name_key) -> { dialogFragment = DatabaseNamePreferenceDialogFragmentCompat.newInstance(preference.key) } @@ -669,27 +672,29 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev } R.id.menu_merge_database -> { mergeDatabase() - return true + true } R.id.menu_reload_database -> { reloadDatabase() - return true + true } - - else -> { + R.id.menu_app_settings -> { // Check the time lock before launching settings // TODO activity menu (activity as SettingsActivity?)?.let { - MenuUtil.onDefaultMenuOptionsItemSelected(it, item, true) + SettingsActivity.launch(it, true) } + true + } + else -> { super.onOptionsItemSelected(item) } } } - override fun onPreferenceTreeClick(preference: Preference?): Boolean { + override fun onPreferenceTreeClick(preference: Preference): Boolean { // To reload group when database settings are modified - when (preference?.key) { + when (preference.key) { getString(R.string.database_name_key), getString(R.string.database_description_key), getString(R.string.database_default_username_key), diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt index 822101ee3..0089d8ab9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt @@ -92,12 +92,6 @@ object PreferencesUtil { context.resources.getBoolean(R.bool.remember_keyfile_locations_default)) } - fun omitBackup(context: Context): Boolean { - val prefs = PreferenceManager.getDefaultSharedPreferences(context) - return prefs.getBoolean(context.getString(R.string.omit_backup_search_key), - context.resources.getBoolean(R.bool.omit_backup_search_default)) - } - fun automaticallyFocusSearch(context: Context): Boolean { val prefs = PreferenceManager.getDefaultSharedPreferences(context) return prefs.getBoolean(context.getString(R.string.auto_focus_search_key), @@ -608,7 +602,6 @@ object PreferencesUtil { context.getString(R.string.enable_read_only_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.enable_auto_save_database_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.enable_keep_screen_on_key) -> editor.putBoolean(name, value.toBoolean()) - context.getString(R.string.omit_backup_search_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.auto_focus_search_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.subdomain_search_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.app_timeout_key) -> editor.putString(name, value.toLong().toString()) @@ -678,23 +671,7 @@ object PreferencesUtil { putPropertiesInPreferences(properties, Education.getEducationSharedPreferences(context)) { editor, name, value -> - when (name) { - context.getString(R.string.education_create_db_key) -> editor.putBoolean(name, value.toBoolean()) - context.getString(R.string.education_select_db_key) -> editor.putBoolean(name, value.toBoolean()) - context.getString(R.string.education_unlock_key) -> editor.putBoolean(name, value.toBoolean()) - context.getString(R.string.education_read_only_key) -> editor.putBoolean(name, value.toBoolean()) - context.getString(R.string.education_biometric_key) -> editor.putBoolean(name, value.toBoolean()) - context.getString(R.string.education_search_key) -> editor.putBoolean(name, value.toBoolean()) - context.getString(R.string.education_new_node_key) -> editor.putBoolean(name, value.toBoolean()) - context.getString(R.string.education_sort_key) -> editor.putBoolean(name, value.toBoolean()) - context.getString(R.string.education_lock_key) -> editor.putBoolean(name, value.toBoolean()) - context.getString(R.string.education_copy_username_key) -> editor.putBoolean(name, value.toBoolean()) - context.getString(R.string.education_entry_edit_key) -> editor.putBoolean(name, value.toBoolean()) - context.getString(R.string.education_password_generator_key) -> editor.putBoolean(name, value.toBoolean()) - context.getString(R.string.education_entry_new_field_key) -> editor.putBoolean(name, value.toBoolean()) - context.getString(R.string.education_add_attachment_key) -> editor.putBoolean(name, value.toBoolean()) - context.getString(R.string.education_setup_OTP_key) -> editor.putBoolean(name, value.toBoolean()) - } + Education.putPropertiesInEducationPreferences(context, editor, name, value) } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt index 04f19014c..eb68fd2b5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt @@ -32,7 +32,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.fragment.app.Fragment import com.google.android.material.floatingactionbutton.FloatingActionButton import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment +import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.database.element.Database @@ -46,7 +46,7 @@ import java.util.* open class SettingsActivity : DatabaseLockActivity(), MainPreferenceFragment.Callback, - AssignMasterKeyDialogFragment.AssignPasswordDialogListener { + SetMainCredentialDialogFragment.AssignMainCredentialDialogListener { private var backupManager: BackupManager? = null private var mExternalFileHelper: ExternalFileHelper? = null diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/DurationDialogPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/DurationDialogPreference.kt index 4d1f203d8..46f7a4e92 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/DurationDialogPreference.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/DurationDialogPreference.kt @@ -61,9 +61,9 @@ class DurationDialogPreference @JvmOverloads constructor(context: Context, } } - override fun onGetDefaultValue(a: TypedArray?, index: Int): Any { + override fun onGetDefaultValue(a: TypedArray, index: Int): Any { return try { - a?.getString(index)?.toLongOrNull() ?: mDuration + a.getString(index)?.toLongOrNull() ?: mDuration } catch (e: Exception) { mDuration } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfNumberPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfNumberPreference.kt index 42e1bbbbd..367f73489 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfNumberPreference.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfNumberPreference.kt @@ -35,7 +35,7 @@ open class InputKdfNumberPreference @JvmOverloads constructor(context: Context, return R.layout.pref_dialog_input_numbers } - override fun setSummary(summary: CharSequence) { + override fun setSummary(summary: CharSequence?) { if (summary == UNKNOWN_VALUE_STRING) { isEnabled = false super.setSummary("") diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfSizePreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfSizePreference.kt index 168efe2aa..ff07d973d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfSizePreference.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfSizePreference.kt @@ -30,7 +30,7 @@ class InputKdfSizePreference @JvmOverloads constructor(context: Context, defStyleRes: Int = defStyleAttr) : InputKdfNumberPreference(context, attrs, defStyleAttr, defStyleRes) { - override fun setSummary(summary: CharSequence) { + override fun setSummary(summary: CharSequence?) { if (summary == UNKNOWN_VALUE_STRING) { super.setSummary(summary) } else { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputNumberPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputNumberPreference.kt index e0fe8da97..116eaedbe 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputNumberPreference.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputNumberPreference.kt @@ -34,7 +34,7 @@ open class InputNumberPreference @JvmOverloads constructor(context: Context, return R.layout.pref_dialog_input_numbers } - override fun setSummary(summary: CharSequence) { + override fun setSummary(summary: CharSequence?) { if (summary == INFINITE_VALUE_STRING) { super.setSummary("∞") } else { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputSizePreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputSizePreference.kt index 90b1e42d1..39dee92e6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputSizePreference.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputSizePreference.kt @@ -30,7 +30,7 @@ open class InputSizePreference @JvmOverloads constructor(context: Context, defStyleRes: Int = defStyleAttr) : InputNumberPreference(context, attrs, defStyleAttr, defStyleRes) { - override fun setSummary(summary: CharSequence) { + override fun setSummary(summary: CharSequence?) { var summaryString = summary try { val memorySize = summary.toString().toLong() diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseTemplatesGroupPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseTemplatesGroupPreferenceDialogFragmentCompat.kt index fa4200fea..4349e92d3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseTemplatesGroupPreferenceDialogFragmentCompat.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseTemplatesGroupPreferenceDialogFragmentCompat.kt @@ -64,7 +64,7 @@ class DatabaseTemplatesGroupPreferenceDialogFragmentCompat super.onDialogClosed(database, positiveResult) if (positiveResult) { database?.let { - if (database.allowConfigurableTemplatesGroup) { + if (database.allowTemplatesGroup) { val oldGroup = database.templatesGroup val newGroup = mGroupTemplates database.setTemplatesGroup(newGroup) diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.kt index 73013862e..c7233968d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.kt @@ -32,18 +32,11 @@ import com.kunzisoft.keepass.settings.SettingsActivity object MenuUtil { - fun contributionMenuInflater(inflater: MenuInflater, menu: Menu) { - if (!(BuildConfig.FULL_VERSION && BuildConfig.CLOSED_STORE)) - inflater.inflate(R.menu.contribution, menu) - } - fun defaultMenuInflater(inflater: MenuInflater, menu: Menu) { - contributionMenuInflater(inflater, menu) - inflater.inflate(R.menu.default_menu, menu) - } - - fun onContributionItemSelected(context: Context) { - UriUtil.gotoUrl(context, R.string.contribution_url) + inflater.inflate(R.menu.settings, menu) + inflater.inflate(R.menu.about, menu) + if (!(BuildConfig.FULL_VERSION && BuildConfig.CLOSED_STORE)) + menu.findItem(R.id.menu_contribute)?.isVisible = false } /* @@ -54,7 +47,7 @@ object MenuUtil { timeoutEnable: Boolean = false) { when (item.itemId) { R.id.menu_contribute -> { - onContributionItemSelected(activity) + UriUtil.gotoUrl(activity, R.string.contribution_url) } R.id.menu_app_settings -> { // To avoid flickering when launch settings in a LockingActivity diff --git a/app/src/main/java/com/kunzisoft/keepass/view/InheritedCompletionView.kt b/app/src/main/java/com/kunzisoft/keepass/view/InheritedCompletionView.kt new file mode 100644 index 000000000..cde0638cf --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/view/InheritedCompletionView.kt @@ -0,0 +1,63 @@ +package com.kunzisoft.keepass.view + +import android.content.Context +import android.text.InputType +import android.util.AttributeSet +import android.widget.ArrayAdapter +import androidx.appcompat.widget.AppCompatAutoCompleteTextView +import com.kunzisoft.keepass.R + +class InheritedCompletionView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : AppCompatAutoCompleteTextView(context, attrs) { + + val adapter = ArrayAdapter( + context, + android.R.layout.simple_list_item_1, + InheritedStatus.listOfStrings(context)) + + init { + setAdapter(adapter) + inputType = InputType.TYPE_NULL + adapter.filter.filter(null) + } + + fun getValue(): Boolean? { + return InheritedStatus.getStatusFromString(context, text.toString()).value + } + + fun setValue(inherited: Boolean?) { + setText(context.getString(InheritedStatus.getStatusFromValue(inherited).stringId)) + adapter.filter.filter(null) + } + + private enum class InheritedStatus(val stringId: Int, val value: Boolean?) { + INHERITED(R.string.inherited, null), + ENABLE(R.string.enable, true), + DISABLE(R.string.disable, false); + + companion object { + fun listOfStrings(context: Context): List { + return listOf( + context.getString(INHERITED.stringId), + context.getString(ENABLE.stringId), + context.getString(DISABLE.stringId) + ) + } + + fun getStatusFromValue(value: Boolean?): InheritedStatus { + values().find { it.value == value }?.let { + return it + } + return INHERITED + } + + fun getStatusFromString(context: Context, text: String): InheritedStatus { + values().find { context.getString(it.stringId) == text }?.let { + return it + } + return INHERITED + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt b/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt new file mode 100644 index 000000000..41f701fd9 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt @@ -0,0 +1,231 @@ +/* + * Copyright 2022 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePassDX. + * + * KeePassDX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePassDX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePassDX. If not, see . + * + */ +package com.kunzisoft.keepass.view + +import android.content.Context +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable +import android.text.Editable +import android.text.TextWatcher +import android.util.AttributeSet +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.CompoundButton +import android.widget.EditText +import android.widget.FrameLayout +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper +import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener +import com.kunzisoft.keepass.model.CredentialStorage +import com.kunzisoft.keepass.model.MainCredential + +class MainCredentialView @JvmOverloads constructor(context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0) + : FrameLayout(context, attrs, defStyle) { + + private var passwordView: EditText + private var keyFileSelectionView: KeyFileSelectionView + private var checkboxPasswordView: CompoundButton + private var checkboxKeyFileView: CompoundButton + + var onPasswordChecked: (CompoundButton.OnCheckedChangeListener)? = null + var onValidateListener: (() -> Unit)? = null + + private var mCredentialStorage: CredentialStorage = CredentialStorage.PASSWORD + + init { + val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater? + inflater?.inflate(R.layout.view_main_credentials, this) + + passwordView = findViewById(R.id.password) + keyFileSelectionView = findViewById(R.id.keyfile_selection) + checkboxPasswordView = findViewById(R.id.password_checkbox) + checkboxKeyFileView = findViewById(R.id.keyfile_checkox) + + val onEditorActionListener = object : TextView.OnEditorActionListener { + override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { + if (actionId == EditorInfo.IME_ACTION_DONE) { + onValidateListener?.invoke() + return true + } + return false + } + } + + passwordView.setOnEditorActionListener(onEditorActionListener) + passwordView.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} + + override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} + + override fun afterTextChanged(editable: Editable) { + if (editable.toString().isNotEmpty() && !checkboxPasswordView.isChecked) + checkboxPasswordView.isChecked = true + } + }) + passwordView.setOnKeyListener { _, _, keyEvent -> + var handled = false + if (keyEvent.action == KeyEvent.ACTION_DOWN + && keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER + ) { + onValidateListener?.invoke() + handled = true + } + handled + } + + checkboxPasswordView.setOnCheckedChangeListener { view, checked -> + onPasswordChecked?.onCheckedChanged(view, checked) + } + } + + fun setOpenKeyfileClickListener(externalFileHelper: ExternalFileHelper?) { + keyFileSelectionView.setOpenDocumentClickListener(externalFileHelper) + } + + fun populatePasswordTextView(text: String?) { + if (text == null || text.isEmpty()) { + passwordView.setText("") + if (checkboxPasswordView.isChecked) + checkboxPasswordView.isChecked = false + } else { + passwordView.setText(text) + if (checkboxPasswordView.isChecked) + checkboxPasswordView.isChecked = true + } + } + + fun populateKeyFileTextView(uri: Uri?) { + if (uri == null || uri.toString().isEmpty()) { + keyFileSelectionView.uri = null + if (checkboxKeyFileView.isChecked) + checkboxKeyFileView.isChecked = false + } else { + keyFileSelectionView.uri = uri + if (!checkboxKeyFileView.isChecked) + checkboxKeyFileView.isChecked = true + } + } + + fun isFill(): Boolean { + return checkboxPasswordView.isChecked || checkboxKeyFileView.isChecked + } + + fun getMainCredential(): MainCredential { + return MainCredential().apply { + this.masterPassword = if (checkboxPasswordView.isChecked) + passwordView.text?.toString() else null + this.keyFileUri = if (checkboxKeyFileView.isChecked) + keyFileSelectionView.uri else null + } + } + + fun changeConditionToStoreCredential(credentialStorage: CredentialStorage) { + this.mCredentialStorage = credentialStorage + } + + fun conditionToStoreCredential(): Boolean { + // TODO HARDWARE_KEY + return when (mCredentialStorage) { + CredentialStorage.PASSWORD -> checkboxPasswordView.isChecked + CredentialStorage.KEY_FILE -> checkboxPasswordView.isChecked + CredentialStorage.HARDWARE_KEY -> false + } + } + + /** + * Return content of the store credential view allowed, + * String? for password + * + */ + fun retrieveCredentialForStorage(listener: CredentialStorageListener): ByteArray? { + return when (mCredentialStorage) { + CredentialStorage.PASSWORD -> listener.passwordToStore(passwordView.text?.toString()) + CredentialStorage.KEY_FILE -> listener.keyfileToStore(keyFileSelectionView.uri) + CredentialStorage.HARDWARE_KEY -> listener.hardwareKeyToStore() + } + } + + interface CredentialStorageListener { + fun passwordToStore(password: String?): ByteArray? + fun keyfileToStore(keyfile: Uri?): ByteArray? + fun hardwareKeyToStore(): ByteArray? + } + + fun requestPasswordFocus() { + passwordView.requestFocusFromTouch() + } + + // Auto select the password field and open keyboard + fun focusPasswordFieldAndOpenKeyboard() { + passwordView.postDelayed({ + passwordView.requestFocusFromTouch() + val inputMethodManager = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as? InputMethodManager? + inputMethodManager?.showSoftInput(passwordView, InputMethodManager.SHOW_IMPLICIT) + }, 100) + } + + override fun onSaveInstanceState(): Parcelable { + val superState = super.onSaveInstanceState() + val saveState = SavedState(superState) + saveState.mCredentialStorage = this.mCredentialStorage + return saveState + } + + override fun onRestoreInstanceState(state: Parcelable?) { + if (state !is SavedState) { + super.onRestoreInstanceState(state) + return + } + super.onRestoreInstanceState(state.superState) + this.mCredentialStorage = state.mCredentialStorage ?: CredentialStorage.DEFAULT + } + + internal class SavedState : BaseSavedState { + var mCredentialStorage: CredentialStorage? = null + + constructor(superState: Parcelable?) : super(superState) {} + + private constructor(parcel: Parcel) : super(parcel) { + mCredentialStorage = CredentialStorage.getFromOrdinal(parcel.readInt()) + } + + override fun writeToParcel(out: Parcel, flags: Int) { + super.writeToParcel(out, flags) + out.writeInt(mCredentialStorage?.ordinal ?: 0) + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): SavedState { + return SavedState(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/view/NavigationDatabaseView.kt b/app/src/main/java/com/kunzisoft/keepass/view/NavigationDatabaseView.kt new file mode 100644 index 000000000..63475e7e5 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/view/NavigationDatabaseView.kt @@ -0,0 +1,73 @@ +package com.kunzisoft.keepass.view + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.core.graphics.BlendModeColorFilterCompat +import androidx.core.graphics.BlendModeCompat +import androidx.core.view.isVisible +import com.google.android.material.navigation.NavigationView +import com.kunzisoft.keepass.R + +class NavigationDatabaseView @JvmOverloads constructor(context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0) + : NavigationView(context, attrs, defStyle) { + + private var databaseNavContainerView: View? = null + private var databaseNavIconView: ImageView? = null + private var databaseNavModifiedView: ImageView? = null + private var databaseNavColorView: ImageView? = null + private var databaseNavNameView: TextView? = null + private var databaseNavPathView: TextView? = null + private var databaseNavVersionView: TextView? = null + + init { + inflateHeaderView(R.layout.nav_header_database) + databaseNavIconView = databaseNavContainerView?.findViewById(R.id.nav_database_icon) + databaseNavModifiedView = databaseNavContainerView?.findViewById(R.id.nav_database_modified) + databaseNavColorView = databaseNavContainerView?.findViewById(R.id.nav_database_color) + databaseNavNameView = databaseNavContainerView?.findViewById(R.id.nav_database_name) + databaseNavPathView = databaseNavContainerView?.findViewById(R.id.nav_database_path) + databaseNavVersionView = databaseNavContainerView?.findViewById(R.id.nav_database_version) + } + + override fun inflateHeaderView(res: Int): View { + val headerView = super.inflateHeaderView(res) + databaseNavContainerView = headerView + return headerView + } + + fun setDatabaseName(name: String) { + databaseNavNameView?.text = name + } + + fun setDatabasePath(path: String?) { + if (path != null) { + databaseNavPathView?.text = path + databaseNavPathView?.visibility = View.VISIBLE + } else { + databaseNavPathView?.visibility = View.GONE + } + } + + fun setDatabaseVersion(version: String) { + databaseNavVersionView?.text = version + } + + fun setDatabaseModifiedSinceLastLoading(modified: Boolean) { + databaseNavModifiedView?.isVisible = modified + } + + fun setDatabaseColor(color: Int?) { + if (color != null) { + databaseNavColorView?.drawable?.colorFilter = BlendModeColorFilterCompat + .createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_IN) + databaseNavColorView?.visibility = View.VISIBLE + } else { + databaseNavColorView?.visibility = View.GONE + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/view/SearchFiltersView.kt b/app/src/main/java/com/kunzisoft/keepass/view/SearchFiltersView.kt new file mode 100644 index 000000000..271971d35 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/view/SearchFiltersView.kt @@ -0,0 +1,252 @@ +package com.kunzisoft.keepass.view + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.CompoundButton +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.view.isVisible +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.database.search.SearchHelper +import com.kunzisoft.keepass.database.search.SearchParameters + +class SearchFiltersView @JvmOverloads constructor(context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0) + : LinearLayout(context, attrs, defStyle) { + + private var searchContainer: ViewGroup + private var searchAdvanceFiltersContainer: ViewGroup? = null + private var searchExpandButton: ImageView + private var searchNumbers: TextView + private var searchCurrentGroup: CompoundButton + private var searchCaseSensitive: CompoundButton + private var searchRegex: CompoundButton + private var searchTitle: CompoundButton + private var searchUsername: CompoundButton + private var searchPassword: CompoundButton + private var searchURL: CompoundButton + private var searchExpires: CompoundButton + private var searchNotes: CompoundButton + private var searchOther: CompoundButton + private var searchUUID: CompoundButton + private var searchTag: CompoundButton + private var searchGroupSearchable: CompoundButton + private var searchRecycleBin: CompoundButton + private var searchTemplate: CompoundButton + + var searchParameters = SearchParameters() + get() { + return field.apply { + this.searchInCurrentGroup = searchCurrentGroup.isChecked + this.caseSensitive = searchCaseSensitive.isChecked + this.isRegex = searchRegex.isChecked + this.searchInTitles = searchTitle.isChecked + this.searchInUsernames = searchUsername.isChecked + this.searchInPasswords = searchPassword.isChecked + this.searchInUrls = searchURL.isChecked + this.excludeExpired = !(searchExpires.isChecked) + this.searchInNotes = searchNotes.isChecked + this.searchInOther = searchOther.isChecked + this.searchInUUIDs = searchUUID.isChecked + this.searchInTags = searchTag.isChecked + this.searchInRecycleBin = searchRecycleBin.isChecked + this.searchInTemplates = searchTemplate.isChecked + } + } + set(value) { + field = value + val tempListener = mOnParametersChangeListener + mOnParametersChangeListener = null + searchCurrentGroup.isChecked = value.searchInCurrentGroup + searchCaseSensitive.isChecked = value.caseSensitive + searchRegex.isChecked = value.isRegex + searchTitle.isChecked = value.searchInTitles + searchUsername.isChecked = value.searchInUsernames + searchPassword.isChecked = value.searchInPasswords + searchURL.isChecked = value.searchInUrls + searchExpires.isChecked = !value.excludeExpired + searchNotes.isChecked = value.searchInNotes + searchOther.isChecked = value.searchInOther + searchUUID.isChecked = value.searchInUUIDs + searchTag.isChecked = value.searchInTags + searchGroupSearchable.isChecked = value.searchInRecycleBin + searchRecycleBin.isChecked = value.searchInRecycleBin + searchTemplate.isChecked = value.searchInTemplates + mOnParametersChangeListener = tempListener + } + + var onParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = null + private var mOnParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = { + // To recalculate height + if (searchAdvanceFiltersContainer?.visibility == View.VISIBLE) { + searchAdvanceFiltersContainer?.expand( + false, + searchAdvanceFiltersContainer?.getFullHeight() + ) + } + onParametersChangeListener?.invoke(searchParameters) + } + + init { + val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater? + inflater?.inflate(R.layout.view_search_filters, this) + + searchContainer = findViewById(R.id.search_container) + searchAdvanceFiltersContainer = findViewById(R.id.search_advance_filters) + searchExpandButton = findViewById(R.id.search_expand) + searchNumbers = findViewById(R.id.search_numbers) + searchCurrentGroup = findViewById(R.id.search_chip_current_group) + searchCaseSensitive = findViewById(R.id.search_chip_case_sensitive) + searchRegex = findViewById(R.id.search_chip_regex) + searchTitle = findViewById(R.id.search_chip_title) + searchUsername = findViewById(R.id.search_chip_username) + searchPassword = findViewById(R.id.search_chip_password) + searchURL = findViewById(R.id.search_chip_url) + searchExpires = findViewById(R.id.search_chip_expires) + searchNotes = findViewById(R.id.search_chip_note) + searchUUID = findViewById(R.id.search_chip_uuid) + searchOther = findViewById(R.id.search_chip_other) + searchTag = findViewById(R.id.search_chip_tag) + searchGroupSearchable = findViewById(R.id.search_chip_group_searchable) + searchRecycleBin = findViewById(R.id.search_chip_recycle_bin) + searchTemplate = findViewById(R.id.search_chip_template) + + // Expand menu with button + searchExpandButton.setOnClickListener { + val isVisible = searchAdvanceFiltersContainer?.visibility == View.VISIBLE + if (isVisible) + closeAdvancedFilters() + else + openAdvancedFilters() + } + + searchCurrentGroup.setOnCheckedChangeListener { _, isChecked -> + searchParameters.searchInCurrentGroup = isChecked + mOnParametersChangeListener?.invoke(searchParameters) + } + searchCaseSensitive.setOnCheckedChangeListener { _, isChecked -> + searchParameters.caseSensitive = isChecked + mOnParametersChangeListener?.invoke(searchParameters) + } + searchRegex.setOnCheckedChangeListener { _, isChecked -> + searchParameters.isRegex = isChecked + mOnParametersChangeListener?.invoke(searchParameters) + } + searchTitle.setOnCheckedChangeListener { _, isChecked -> + searchParameters.searchInTitles = isChecked + mOnParametersChangeListener?.invoke(searchParameters) + } + searchUsername.setOnCheckedChangeListener { _, isChecked -> + searchParameters.searchInUsernames = isChecked + mOnParametersChangeListener?.invoke(searchParameters) + } + searchPassword.setOnCheckedChangeListener { _, isChecked -> + searchParameters.searchInPasswords = isChecked + mOnParametersChangeListener?.invoke(searchParameters) + } + searchURL.setOnCheckedChangeListener { _, isChecked -> + searchParameters.searchInUrls = isChecked + mOnParametersChangeListener?.invoke(searchParameters) + } + searchExpires.setOnCheckedChangeListener { _, isChecked -> + searchParameters.excludeExpired = !isChecked + mOnParametersChangeListener?.invoke(searchParameters) + } + searchNotes.setOnCheckedChangeListener { _, isChecked -> + searchParameters.searchInNotes = isChecked + mOnParametersChangeListener?.invoke(searchParameters) + } + searchUUID.setOnCheckedChangeListener { _, isChecked -> + searchParameters.searchInUUIDs = isChecked + mOnParametersChangeListener?.invoke(searchParameters) + } + searchOther.setOnCheckedChangeListener { _, isChecked -> + searchParameters.searchInOther = isChecked + mOnParametersChangeListener?.invoke(searchParameters) + } + searchTag.setOnCheckedChangeListener { _, isChecked -> + searchParameters.searchInTags = isChecked + mOnParametersChangeListener?.invoke(searchParameters) + } + searchGroupSearchable.setOnCheckedChangeListener { _, isChecked -> + searchParameters.searchInSearchableGroup = isChecked + mOnParametersChangeListener?.invoke(searchParameters) + } + searchRecycleBin.setOnCheckedChangeListener { _, isChecked -> + searchParameters.searchInRecycleBin = isChecked + mOnParametersChangeListener?.invoke(searchParameters) + } + searchTemplate.setOnCheckedChangeListener { _, isChecked -> + searchParameters.searchInTemplates = isChecked + mOnParametersChangeListener?.invoke(searchParameters) + } + } + + fun setNumbers(numbers: Int) { + searchNumbers.text = SearchHelper.showNumberOfSearchResults(numbers) + } + + fun setCurrentGroupText(text: String) { + val maxChars = 12 + searchCurrentGroup.text = when { + text.isEmpty() -> context.getString(R.string.current_group) + text.length > maxChars -> text.substring(0, maxChars) + "…" + else -> text + } + } + + fun availableOther(available: Boolean) { + searchOther.isVisible = available + } + + fun availableTags(available: Boolean) { + searchTag.isVisible = available + } + + fun enableTags(enable: Boolean) { + searchTag.isEnabled = enable + } + + fun availableSearchableGroup(available: Boolean) { + searchGroupSearchable.isVisible = available + } + + fun availableTemplates(available: Boolean) { + searchTemplate.isVisible = available + } + + fun enableTemplates(enable: Boolean) { + searchTemplate.isEnabled = enable + } + + fun closeAdvancedFilters() { + searchAdvanceFiltersContainer?.collapse() + } + + private fun openAdvancedFilters() { + searchAdvanceFiltersContainer?.expand(true, + searchAdvanceFiltersContainer?.getFullHeight() + ) + } + + override fun setVisibility(visibility: Int) { + when (visibility) { + View.VISIBLE -> { + searchAdvanceFiltersContainer?.visibility = View.GONE + searchContainer.showByFading() + } + else -> { + searchContainer.hideByFading() + if (searchAdvanceFiltersContainer?.visibility == View.VISIBLE) { + searchAdvanceFiltersContainer?.visibility = View.INVISIBLE + searchAdvanceFiltersContainer?.collapse() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt index 119ec2732..44182c9ee 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt @@ -51,6 +51,8 @@ import androidx.appcompat.widget.ActionMenuView import androidx.core.graphics.drawable.DrawableCompat import android.graphics.drawable.Drawable +import android.view.ViewGroup +import android.widget.LinearLayout import com.google.android.material.appbar.CollapsingToolbarLayout @@ -113,8 +115,7 @@ fun View.collapse(animate: Boolean = true, onCollapseFinished: (() -> Unit)? = null) { val recordViewHeight = layoutParams.height val slideAnimator = ValueAnimator.ofInt(height, 0) - if (animate) - slideAnimator.duration = 300L + slideAnimator.duration = if (animate) 300L else 0L slideAnimator.addUpdateListener { animation -> layoutParams.height = animation.animatedValue as Int requestLayout() @@ -143,8 +144,7 @@ fun View.expand(animate: Boolean = true, layoutParams.height = 0 val slideAnimator = ValueAnimator .ofInt(0, viewHeight) - if (animate) - slideAnimator.duration = 300L + slideAnimator.duration = if (animate) 300L else 0L var alreadyVisible = false slideAnimator.addUpdateListener { animation -> layoutParams.height = animation.animatedValue as Int @@ -168,12 +168,38 @@ fun View.expand(animate: Boolean = true, }.start() } +/*** + * This function returns the actual height the layout. + * The getHeight() function returns the current height which might be zero if + * the layout's visibility is GONE + */ +fun ViewGroup.getFullHeight(): Int { + measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + val initialVisibility = visibility + visibility = LinearLayout.VISIBLE + val desiredWidth = View.MeasureSpec.makeMeasureSpec( + width, + View.MeasureSpec.AT_MOST + ) + measure(desiredWidth, View.MeasureSpec.UNSPECIFIED) + val totalHeight = measuredHeight + visibility = initialVisibility + return totalHeight +} + fun View.hideByFading() { alpha = 1f animate() .alpha(0f) .setDuration(140) - .setListener(null) + .setListener(object: Animator.AnimatorListener { + override fun onAnimationStart(p0: Animator?) {} + override fun onAnimationEnd(p0: Animator?) { + isVisible = false + } + override fun onAnimationCancel(p0: Animator?) {} + override fun onAnimationRepeat(p0: Animator?) {} + }) } fun View.showByFading() { diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/GroupViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/GroupViewModel.kt index f47c6d1ec..a442929a4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/GroupViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/GroupViewModel.kt @@ -22,26 +22,30 @@ package com.kunzisoft.keepass.viewmodels import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.app.database.IOActionTask import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Group -import com.kunzisoft.keepass.database.element.node.Node +import com.kunzisoft.keepass.database.element.node.NodeId +import com.kunzisoft.keepass.database.search.SearchHelper +import com.kunzisoft.keepass.database.search.SearchParameters class GroupViewModel: ViewModel() { + val mainGroup : LiveData get() = _mainGroup + private val _mainGroup = MutableLiveData() + val group : LiveData get() = _group private val _group = MutableLiveData() val firstPositionVisible : LiveData get() = _firstPositionVisible private val _firstPositionVisible = MutableLiveData() - fun loadGroup(database: Database?, - groupState: GroupActivity.GroupState?) { + fun loadMainGroup(database: Database?, + groupId: NodeId<*>?, + showFromPosition: Int?) { IOActionTask( { - val groupId = groupState?.groupId if (groupId != null) { database?.getGroupById(groupId) } else { @@ -50,44 +54,46 @@ class GroupViewModel: ViewModel() { }, { group -> if (group != null) { - _group.value = SuperGroup(group, + _mainGroup.value = SuperGroup(group, database?.recycleBin == group, - groupState?.firstVisibleItem) + showFromPosition) + _group.value = _mainGroup.value } } ).execute() } - fun loadGroup(database: Database?, - group: Group, - showFromPosition: Int?) { - _group.value = SuperGroup(group, - database?.recycleBin == group, - showFromPosition) + fun loadSearchGroup(database: Database?, + searchParameters: SearchParameters, + fromGroup: NodeId<*>?, + showFromPosition: Int?) { + IOActionTask( + { + database?.createVirtualGroupFromSearch( + searchParameters, + fromGroup, + SearchHelper.MAX_SEARCH_ENTRY + ) + }, + { group -> + if (group != null) { + _group.value = SuperGroup(group, + database?.recycleBin == group, + showFromPosition, + searchParameters) + } + } + ).execute() } fun assignPosition(position: Int) { _firstPositionVisible.value = position } - fun loadGroupFromSearch(database: Database?, - searchQuery: String, - omitBackup: Boolean) { - IOActionTask( - { - database?.createVirtualGroupFromSearch(searchQuery, omitBackup) - }, - { group -> - if (group != null) { - _group.value = SuperGroup(group, - database?.recycleBin == group, - 0) - } - } - ).execute() - } - - data class SuperGroup(val group: Group, val isRecycleBin: Boolean, var showFromPosition: Int?) + data class SuperGroup(val group: Group, + val isRecycleBin: Boolean, + var showFromPosition: Int?, + var searchParameters: SearchParameters = SearchParameters()) companion object { private val TAG = GroupViewModel::class.java.name diff --git a/app/src/main/res/color/background_button_color_secondary.xml b/app/src/main/res/color/background_button_color_secondary.xml index 6824d6795..0a69948d9 100644 --- a/app/src/main/res/color/background_button_color_secondary.xml +++ b/app/src/main/res/color/background_button_color_secondary.xml @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/color/background_color_chip.xml b/app/src/main/res/color/background_color_chip.xml new file mode 100644 index 000000000..18061d697 --- /dev/null +++ b/app/src/main/res/color/background_color_chip.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/list_primary_color.xml b/app/src/main/res/color/list_primary_color.xml index d5cc012a7..06694e43a 100644 --- a/app/src/main/res/color/list_primary_color.xml +++ b/app/src/main/res/color/list_primary_color.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/color/text_color_button.xml b/app/src/main/res/color/text_color_button.xml index 9203a4a5d..13c44bba4 100644 --- a/app/src/main/res/color/text_color_button.xml +++ b/app/src/main/res/color/text_color_button.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_button.xml b/app/src/main/res/drawable/background_button.xml index addbbc56d..a61486df1 100644 --- a/app/src/main/res/drawable/background_button.xml +++ b/app/src/main/res/drawable/background_button.xml @@ -9,7 +9,7 @@ android:right="8dp" android:top="12dp" android:bottom="12dp"/> - + diff --git a/app/src/main/res/drawable/background_button_small.xml b/app/src/main/res/drawable/background_button_small.xml index fbcba852e..3516e7b18 100644 --- a/app/src/main/res/drawable/background_button_small.xml +++ b/app/src/main/res/drawable/background_button_small.xml @@ -11,7 +11,7 @@ android:right="14dp" android:top="4dp" android:bottom="8dp"/> - + diff --git a/app/src/main/res/drawable/background_image_button.xml b/app/src/main/res/drawable/background_image_button.xml index b7a501a66..794856688 100644 --- a/app/src/main/res/drawable/background_image_button.xml +++ b/app/src/main/res/drawable/background_image_button.xml @@ -3,7 +3,7 @@ - + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_check_white_24dp.xml b/app/src/main/res/drawable/ic_check_white_24dp.xml index 17aca2af1..7ef4361ff 100644 --- a/app/src/main/res/drawable/ic_check_white_24dp.xml +++ b/app/src/main/res/drawable/ic_check_white_24dp.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_current_folder_white_24dp.xml b/app/src/main/res/drawable/ic_current_folder_white_24dp.xml new file mode 100644 index 000000000..a155c1081 --- /dev/null +++ b/app/src/main/res/drawable/ic_current_folder_white_24dp.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_database_white_36dp.xml b/app/src/main/res/drawable/ic_database_white_36dp.xml new file mode 100644 index 000000000..3d0e57c7c --- /dev/null +++ b/app/src/main/res/drawable/ic_database_white_36dp.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_merge_from_white_24dp.xml b/app/src/main/res/drawable/ic_merge_from_white_24dp.xml new file mode 100644 index 000000000..b70af1987 --- /dev/null +++ b/app/src/main/res/drawable/ic_merge_from_white_24dp.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_modified_white_12dp.xml b/app/src/main/res/drawable/ic_modified_white_12dp.xml new file mode 100644 index 000000000..eba1bd509 --- /dev/null +++ b/app/src/main/res/drawable/ic_modified_white_12dp.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_regex_white_24dp.xml b/app/src/main/res/drawable/ic_regex_white_24dp.xml new file mode 100644 index 000000000..ffe6c14a0 --- /dev/null +++ b/app/src/main/res/drawable/ic_regex_white_24dp.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_save_copy_to_white_24dp.xml b/app/src/main/res/drawable/ic_save_copy_to_white_24dp.xml new file mode 100644 index 000000000..32e5fc593 --- /dev/null +++ b/app/src/main/res/drawable/ic_save_copy_to_white_24dp.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_templates_white_24dp.xml b/app/src/main/res/drawable/ic_templates_white_24dp.xml new file mode 100644 index 000000000..01a04086d --- /dev/null +++ b/app/src/main/res/drawable/ic_templates_white_24dp.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/key_background_normal.xml b/app/src/main/res/drawable/key_background_normal.xml index c6cdf17b6..b93917116 100644 --- a/app/src/main/res/drawable/key_background_normal.xml +++ b/app/src/main/res/drawable/key_background_normal.xml @@ -25,6 +25,6 @@ android:right="0dp" android:top="12dp" android:bottom="12dp"/> - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/key_background_pressed.xml b/app/src/main/res/drawable/key_background_pressed.xml index 18d70899e..39bb882f6 100644 --- a/app/src/main/res/drawable/key_background_pressed.xml +++ b/app/src/main/res/drawable/key_background_pressed.xml @@ -25,6 +25,6 @@ android:right="0dp" android:top="12dp" android:bottom="12dp"/> - - + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_entry.xml b/app/src/main/res/layout/activity_entry.xml index 7c264f82d..ae3bcd13d 100644 --- a/app/src/main/res/layout/activity_entry.xml +++ b/app/src/main/res/layout/activity_entry.xml @@ -104,7 +104,7 @@ android:visibility="gone" android:background="?attr/colorAccent" android:padding="12dp" - android:textColor="?attr/textColorInverse" + android:textColor="?attr/colorOnAccentColor" android:text="@string/entry_history"/> . --> - + android:layout_height="match_parent" + android:fitsSystemWindows="true"> - + android:layout_height="match_parent"> - - + android:theme="?attr/toolbarSpecialAppearance" /> + + + + + + + + - - - - - - - - + + - - - - - - - - - - - + android:layout_below="@+id/toolbar" + android:layout_above="@+id/toolbar_action"> - + + + + + + + + + + + + + - + android:orientation="vertical" + app:layout_behavior="@string/appbar_scrolling_view_behavior" + android:layout_below="@+id/toolbar"> - + + + + + + + + + + + + + - + android:layout_alignParentBottom="true"/> - + - - - - - - + android:layout_height="match_parent" + android:layout_gravity="start" + app:itemTextColor="?android:attr/textColor" + app:subheaderColor="?attr/colorAccent" + android:fitsSystemWindows="true" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_password.xml b/app/src/main/res/layout/activity_main_credential.xml similarity index 61% rename from app/src/main/res/layout/activity_password.xml rename to app/src/main/res/layout/activity_main_credential.xml index e562b9381..975c66c5e 100644 --- a/app/src/main/res/layout/activity_password.xml +++ b/app/src/main/res/layout/activity_main_credential.xml @@ -107,102 +107,21 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="wrap_content" + android:background="?android:attr/windowBackground" /> + diff --git a/app/src/main/res/layout/activity_search_results.xml b/app/src/main/res/layout/activity_search_results.xml deleted file mode 100644 index 0526f5d2b..000000000 --- a/app/src/main/res/layout/activity_search_results.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_color_picker.xml b/app/src/main/res/layout/fragment_color_picker.xml index 01a98be2a..091c092ec 100644 --- a/app/src/main/res/layout/fragment_color_picker.xml +++ b/app/src/main/res/layout/fragment_color_picker.xml @@ -43,7 +43,7 @@ android:layout_margin="20dp" android:text="@string/enable" android:background="@drawable/background_button_small" - android:textColor="@color/white" + android:textColor="?attr/colorOnAccentColor" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" android:minHeight="48dp"/> diff --git a/app/src/main/res/layout/fragment_entry.xml b/app/src/main/res/layout/fragment_entry.xml index b72f4b88c..813f11970 100644 --- a/app/src/main/res/layout/fragment_entry.xml +++ b/app/src/main/res/layout/fragment_entry.xml @@ -109,6 +109,32 @@ android:layout_width="match_parent" android:layout_height="wrap_content" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_set_password.xml b/app/src/main/res/layout/fragment_set_main_credential.xml similarity index 100% rename from app/src/main/res/layout/fragment_set_password.xml rename to app/src/main/res/layout/fragment_set_main_credential.xml diff --git a/app/src/main/res/layout/item_file_info.xml b/app/src/main/res/layout/item_file_info.xml index 1acc01ddf..bd1b66b38 100644 --- a/app/src/main/res/layout/item_file_info.xml +++ b/app/src/main/res/layout/item_file_info.xml @@ -71,8 +71,8 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/file_information_button" android:button="@drawable/checkbox_star" - android:buttonTint="?android:attr/textColorHintInverse" - app:buttonTint="?android:attr/textColorHintInverse" + android:buttonTint="?android:attr/textColorSecondaryInverse" + app:buttonTint="?android:attr/textColorSecondaryInverse" android:gravity="center" android:contentDescription="@string/default_checkbox" /> @@ -189,7 +189,7 @@ android:paddingTop="8dp" android:layout_marginRight="@dimen/default_margin" android:layout_marginEnd="@dimen/default_margin" - android:textColor="?android:attr/textColorHintInverse" + android:textColor="?android:attr/textColorSecondaryInverse" app:layout_constraintBottom_toBottomOf="@+id/file_modification_container" app:layout_constraintEnd_toEndOf="parent" tools:text="8 bytes" /> @@ -202,7 +202,7 @@ android:src="@drawable/ic_content_delete_white_24dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" - android:tint="?android:attr/textColorHintInverse" + android:tint="?android:attr/textColorSecondaryInverse" style="@style/KeepassDXStyle.ImageButton.Simple.Secondary"/> diff --git a/app/src/main/res/layout/item_search_entry.xml b/app/src/main/res/layout/item_search_entry.xml deleted file mode 100644 index 6918589d2..000000000 --- a/app/src/main/res/layout/item_search_entry.xml +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/keyboard_container.xml b/app/src/main/res/layout/keyboard_container.xml index 573fbd89a..7d7330c03 100644 --- a/app/src/main/res/layout/keyboard_container.xml +++ b/app/src/main/res/layout/keyboard_container.xml @@ -23,13 +23,13 @@ android:layout_alignParentBottom="true" android:paddingTop="4dp" android:paddingBottom="8dp" - android:background="@color/grey_blue_dark" + android:background="@color/grey_blue_deep" android:orientation="vertical"> . --> diff --git a/app/src/main/res/layout/nav_header_database.xml b/app/src/main/res/layout/nav_header_database.xml new file mode 100644 index 000000000..cfaa045a4 --- /dev/null +++ b/app/src/main/res/layout/nav_header_database.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/toolbar_default.xml b/app/src/main/res/layout/toolbar_default.xml index 043546dc8..d3bd36aa1 100644 --- a/app/src/main/res/layout/toolbar_default.xml +++ b/app/src/main/res/layout/toolbar_default.xml @@ -20,7 +20,6 @@ - @@ -66,7 +67,7 @@ android:layout_marginBottom="-12dp" android:descendantFocusability="blocksDescendants" android:visibility="gone"> - @@ -88,15 +90,16 @@ + app:useCompatPadding="true" /> \ No newline at end of file diff --git a/app/src/main/res/layout/view_button_lock.xml b/app/src/main/res/layout/view_button_lock.xml index fb2e02aa1..8607d6097 100644 --- a/app/src/main/res/layout/view_button_lock.xml +++ b/app/src/main/res/layout/view_button_lock.xml @@ -9,8 +9,10 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" app:fabSize="mini" + android:layout_gravity="center" android:layout_margin="8dp" android:contentDescription="@string/lock" + android:visibility="gone" android:src="@drawable/ic_lock_white_padding_24dp" xmlns:app="http://schemas.android.com/apk/res-auto" /> \ No newline at end of file diff --git a/app/src/main/res/layout/view_edit_date_time.xml b/app/src/main/res/layout/view_edit_date_time.xml index 3b944dce7..bcc218e9a 100644 --- a/app/src/main/res/layout/view_edit_date_time.xml +++ b/app/src/main/res/layout/view_edit_date_time.xml @@ -20,6 +20,10 @@ android:focusable="false" android:cursorVisible="false" android:focusableInTouchMode="false" + android:paddingLeft="16dp" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:paddingRight="16dp" style="@style/KeepassDXStyle.TextAppearance.TextNode" tools:text="2020-03-04 05:00" /> diff --git a/app/src/main/res/layout/view_main_credentials.xml b/app/src/main/res/layout/view_main_credentials.xml new file mode 100644 index 000000000..254b53e01 --- /dev/null +++ b/app/src/main/res/layout/view_main_credentials.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_search_filters.xml b/app/src/main/res/layout/view_search_filters.xml new file mode 100644 index 000000000..e5a91003b --- /dev/null +++ b/app/src/main/res/layout/view_search_filters.xml @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/default_menu.xml b/app/src/main/res/menu/about.xml similarity index 60% rename from app/src/main/res/menu/default_menu.xml rename to app/src/main/res/menu/about.xml index 6e2628794..319474231 100644 --- a/app/src/main/res/menu/default_menu.xml +++ b/app/src/main/res/menu/about.xml @@ -19,16 +19,18 @@ --> - - + + + + diff --git a/app/src/main/res/menu/database_extra.xml b/app/src/main/res/menu/database_extra.xml new file mode 100644 index 000000000..fecd0e923 --- /dev/null +++ b/app/src/main/res/menu/database_extra.xml @@ -0,0 +1,42 @@ + + + + + + + + + diff --git a/app/src/main/res/menu/open_file.xml b/app/src/main/res/menu/open_file.xml index 11f3ee674..695b37dae 100644 --- a/app/src/main/res/menu/open_file.xml +++ b/app/src/main/res/menu/open_file.xml @@ -22,7 +22,7 @@ \ No newline at end of file diff --git a/app/src/main/res/menu/contribution.xml b/app/src/main/res/menu/settings.xml similarity index 70% rename from app/src/main/res/menu/contribution.xml rename to app/src/main/res/menu/settings.xml index 535de0089..c37ad7367 100644 --- a/app/src/main/res/menu/contribution.xml +++ b/app/src/main/res/menu/settings.xml @@ -1,7 +1,7 @@ - - \ No newline at end of file + + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index f5c907c10..f439a3e6b 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -20,10 +20,10 @@ الصفحة الرئيسة قبول إضافة مجموعة - التعمية - خوارزمية التعمية + التشفير + خوارزمية التشفير التطبيق - أقواس + الأقواس تمديد ASCII السماح مُسِحت الحافظة @@ -86,7 +86,6 @@ التَّوازِي جار حفظ قاعدة البيانات … المساحة - البحث الفرز تصاعدي ↓ العنوان @@ -95,7 +94,6 @@ تاريخ آخر تعديل خاصّ البحث - نتائج البحث تسطير حروف كبيرة تحذير @@ -106,13 +104,13 @@ إضافة حقول مخصصة نسخ حقل تأمين قاعدة البيانات - التغذية الراجعة + ردود الفعل التنفيذ لمُدير كلمات المرور «كي‌ باس» على نظام أندرويد - إضافة مدخلة - تحرير مدخلة + إضافة إدخال + تحرير الإدخال وظيفة اشتقاق المفتاح - انتهت المهلة - مدة الانتظار قبل إقفال قاعدة البيانات + المهلة + مدة الخمول قبل قفل قاعدة البيانات المحرر الذي يمتلك صلاحتي ACTION_CREATE_DOCUMENT و ACTION_OPEN_DOCUMENT ضروري لانشاء, وفتح وحفض قواعد البيانات. بعض الأجهزة لا تسمح للتطبيقات باستعمال الحافظة. مهلة الحافظة @@ -159,7 +157,6 @@ قابل للتعديل فتح قاعدة بيانات موجودة انشاء قاعدة بيانات - لا تبحثفي مدخلات النسخ الاحتياطي قيد العمل… KeePassDX يحتاج صلاحية الكتابة من اجل تعديل قاعدة البيانات. خوارزمية تشفير جميع البيانات. @@ -302,7 +299,6 @@ للمتابعة هل تريد حل المشكلة بتوليد UUID للعناصر المكررة ؟ تحتوي قاعدة البيانات على UUID مكرر. البحث السريع - أزِل مجموعتي \"سلة المحذوفات\" و\" النسخ الاحتياطي\" من نتائج البحث احذف السجل استعادة السجل أفرغ سلة المحذوفات @@ -316,7 +312,7 @@ نوع كلمة المرور لمرة واحدة عين اعتماد واحد على الأقل. ساهم - ‮تواصل معنا + الإتصال بنا البصمة يجب ألا تغير محتوى ملف المفتاح، في أحسن الحالات يجب أن يحتوي بيانات مولدة عشوائيا. من غير المستحسن اضافة ملف مفتاح فارغ. @@ -430,7 +426,7 @@ بيانات الملف موجودة سلفًا. حدث خطأ أثناء إزالة بيانات الملف. حدث خطأ أثناء تنفيذ إجراء على قاعدة البيانات. - معلومات السر لمرة واحدة + معلومات كلمة المرور لمرة واحدة العضوية الاسم البريد الإلكتروني @@ -489,4 +485,5 @@ هل تريد حذف جميع العقد نهائيًا من سلة المهملات؟ احفظ معلومات البحث اسأل لحفظ البيانات + لون قاعدة البيانات \ No newline at end of file diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 41c42248c..8bfde1140 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -102,8 +102,6 @@ Mai Cap resultat de cerca Instal·leu un navegador web per a obri aquesta URL - No cerquis entrades a còpia de seguretat ni paperera - Omet els grups \'Còpia de seguretat\' i paperera dels resultats de cerca Creant nova base de dades… Treballant… Elimina @@ -112,11 +110,9 @@ Més passades d\'encriptació donen protecció addicional contra atacs de força bruta, però poden alentir molt la càrrega i el desat de la base de dades. Guardant la base de dades… Espai - Cerca Ordre natural Especial Cerca - Resultats de la cerca Subratllat Versió de la base de dades no suportada. Majúscules diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 0a58b1bfe..349129cd8 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -109,8 +109,6 @@ Nikdy Žádné výsledky hledání Pro otevření tohoto URL nainstalujte webový prohlížeč. - Neprohledávat položky v záloze - Vynechat skupiny „Záloha“ a \"Koš\" z výsledků vyhledávání Vytváření nové databáze… Pracuji… Ochrana @@ -121,11 +119,9 @@ Vyšší počet šifrovacích průchodů zvýší odolnost proti útoku zkoušením všech možných hesel, ale může výrazně zpomalit načítání a ukládání. Ukládání databáze… Mezera - Hledat Přirozené řazení Speciální Hledat - Výsledky hledání Podtržítko Nepodporovaná verze databáze. Velká písmena diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 2ebc6bddf..764a60622 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -108,8 +108,6 @@ Aldrig Ingen søgeresultater Installer en web-browser til at åbne URL. - Gennemsøg ikke backup poster - Udelader \"Sikkerhedskopiering\" og \"Papirkurv\" - grupper fra søgeresultater Opretter ny database… Arbejder… Beskyttelse @@ -120,11 +118,9 @@ Yderligere krypteringsrunder giver højere beskyttelse mod brute-force angreb, men kan virkelig forsinke læsnings- og skrivehastigheden. Gemmer database… Mellemrum - Søg Naturlig rækkefølge Speciel Søg - Søgeresultater Understregning Database-versionen er ikke understøttet. Store bogstaver diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 269573361..9a53dddc3 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -118,8 +118,6 @@ Nie Keine Suchergebnisse Bitte einen Webbrowser installieren, um diese URL zu öffnen. - Papierkorb und Backup nicht durchsuchen - Die Gruppen „Backup“ und „Papierkorb“ werden bei der Suche nicht berücksichtigt Schnellsuche Beim Öffnen einer Datenbank eine Suche veranlassen Neue Datenbank anlegen … @@ -133,11 +131,9 @@ Zusätzliche Schlüsseltransformationen bieten einen besseren Schutz gegen Wörterbuch- oder Brute-Force-Angriffe. Allerdings dauert dann auch das Laden und Speichern der Datenbank entsprechend länger. Datenbank wird gespeichert … Leerzeichen - Suchen Natürliche Sortierung Spezialsymbole Suchen - Suchergebnisse Unterstriche Datenbankversion wird nicht unterstützt. Großbuchstaben @@ -238,10 +234,10 @@ Einträge durchsuchen Titel, Benutzernamen oder Inhalte anderer Feldern eingeben, um die Passwörter wiederzufinden. Eintrag bearbeiten - Bearbeiten Sie Ihren Eintrag mit persönlichen Feldern. Persönliche Felder können mit Querverweisen aus anderen Einträgen ergänzt werden. + Bearbeiten Sie Ihren Eintrag mit eigenen Feldern. Eigene Felder können mit Querverweisen aus anderen Einträgen ergänzt werden. Ein starkes Passwort erstellen Generieren Sie ein starkes Passwort, um es mit Ihrem Eintrag zu verknüpfen, definieren Sie es einfach nach den Kriterien des Formulars und denken Sie an die Passwortsicherheit. - Benutzerdefinierte Felder hinzufügen + Eigene Felder hinzufügen Tragen Sie ein zusätzliches Feld ein, fügen Sie einen Wert hinzu und schützen Sie es optional. Die Datenbank entsperren Ein Feld kopieren @@ -435,8 +431,10 @@ Wald Göttlich Klassisch + Einfach Mond Sonne + Antwort Kunzite Datei Schreibrechte gewähren, um Datenbankänderungen zu speichern @@ -532,7 +530,7 @@ Rücktaste Eintrag auswählen Zurück zur vorherigen Tastatur - Benutzerdefinierte Felder + Eigene Felder Alle Verschlüsselungsschlüssel, die mit der modernen Entsperrerkennung zusammenhängen, löschen\? Geräteanmeldedaten entsperren Geräteanmeldedaten @@ -563,7 +561,7 @@ Datei zum Exportieren von App-Eigenschaften erstellen App-Eigenschaften exportieren Datei zum Importieren von App-Eigenschaften auswählen - Sie können hier keine Gruppe verschieben. + Hierher kann keine Gruppe verschoben werden. Ausfüllvorschläge Dieses Wort ist reserviert und kann nicht verwendet werden. Benutzerdefiniert @@ -621,4 +619,20 @@ Hintergrundfarbe des Eintrags Daten zusammenführen Erneutes Laden der Datenbank wird die lokal durchgeführten Datenänderungen löschen. + Schlagwörter + Farbe des Eintrags + Bildschirm eingeschaltet lassen + Der Hash der Datei ist nicht garantiert, da Android die Daten im laufenden Betrieb ändern kann. Ändern Sie die Dateierweiterung in .bin, um die Integrität zu gewährleisten. + Den Bildschirm beim Ansehen des Eintrags eingeschaltet lassen + Vorder- und Hintergrundfarbe in einem Eintrag anzeigen + Auto-Type-Sequenz + Regulärer Ausdruck + Durchsuchbar + Vererbt + Eigene Daten + Suchfilter + Aktuelle Gruppe + Groß-/Kleinschreibung beachten + Zusammenführen von … + Kopie speichern unter … \ No newline at end of file diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 0f513aeb2..625e2d0fc 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -111,8 +111,6 @@ Ποτέ Δεν βρέθηκαν αποτελέσματα αναζήτησης Εγκαταστήστε ένα πρόγραμμα περιήγησης για να ανοίξετε αυτήν τη διεύθυνση URL. - Να μην γίνει αναζήτηση μέσα από τις καταχωρήσεις αντιγραφών ασφαλείας - Παράληψη ομάδας \"Αντίγραφο Ασφαλείας\" και \"Κάδος Ανακύκλωσης\" από τα αποτελέσματα αναζήτησης Δημιουργία νέας βάσης δεδομένων… Επεξεργασία… Προστασία @@ -123,11 +121,9 @@ Επιπλέον κύκλοι κρυπτογράφησης παρέχουν πρόσθετη προστασία ενάντια σε επιθέσεις brute force, αλλά μπορεί να επιβραδύνει πολύ την φόρτωση και την αποθήκευση. Αποθήκευση βάσης δεδομένων… Κενό - Αναζήτηση Κανονική ταξινόμηση Ειδικοί Αναζήτηση - Αποτελέσματα αναζήτησης Υπογράμμιση Μη υποστηριζόμενη έκδοση βάσης δεδομένων. Κεφαλαία @@ -476,7 +472,7 @@ Εμφανίζει το UUID που είναι συνδεδεμένο σε μια καταχώρηση ή σε μια ομάδα Εμφάνιση UUID Δεν επιτρέπεται η αποθήκευση δεδομένων για μια βάση δεδομένων που ανοίγει ως μόνο για ανάγνωση. - Ζητήστε να αποθηκεύσετε δεδομένα όταν επικυρώνεται μια φόρμα + Ζητήστε αποθήκευση δεδομένων όταν ολοκληρωθεί η συμπλήρωση μιας φόρμας Ζητήστε να αποθηκεύσετε δεδομένα Προσπαθήστε να αποθηκεύσετε πληροφορίες αναζήτησης κατά την επιλογή χειροκίνητης καταχώρισης Αποθήκευση πληροφοριών αναζήτησης @@ -616,4 +612,17 @@ Διατηρήστε την οθόνη ενεργή Χρώματα καταχώρησης Εμφανίζει τα χρώματα του προσκηνίου και του φόντου σε μια καταχώρηση + Αναζητήσιμο + Διάκριση πεζών-κεφαλαίων + Συγχώνευση από … + Κληρονόμησε + Αναζήτηση φίλτρων + Κεφαλίδα πλοήγησης + Κλείσιμο συρταριού πλοήγησης + Άνοιγμα συρταριού πλοήγησης + Προσαρμοσμένα δεδομένα + Αλληλουχία Αυτόματης-Πληκτρολόγησης + Τρέχουσα ομάδα + Κοινή έκφραση + Αποθήκευση αντιγράφου στο … \ No newline at end of file diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index b470d8700..d08b35e18 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -5,13 +5,11 @@ Versio %1$s Averto Majuskloj - Serĉrezultoj Serĉi Uzantnomo Titolo Naturala ordo Ordigi - Serĉi Ne ĉesigu la aplikaĵon… Datumbaza konservado… Paralelado diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 868dbbe6e..30f52ef37 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -104,8 +104,6 @@ Nunca Sin resultado de búsqueda Instale un navegador web para abrir esta URL. - No buscar en las entradas de respaldo - Omite los grupos «Respaldo» y «Papelera de reciclaje» de los resultados de búsqueda Creando nueva base de datos… Trabajando… Quitar @@ -114,7 +112,6 @@ Un alto número de pasadas de cifrado proporciona protección adicional contra ataques de fuerza bruta, pero puede ralentizar mucho el cargado y el guardado. Guardando base de datos… Espacio - Buscar Orden natural Especial Búsqueda @@ -164,7 +161,6 @@ Creación Modificación Acceso - Resultados de búsqueda Atención No se pudieron encontrar los datos de entrada. Evite los caracteres de la contraseña fuera del formato de codificación de texto en el archivo de la base de datos (los caracteres no reconocidos se convierten a la misma letra). @@ -268,7 +264,7 @@ No se pudo cargar la base de datos. No se pudo cargar la clave. Intente disminuir el uso de memoria de KDF. Enseña nombres de usuario - Enseña nombres de usuador en las listras de entradas + Muestra los nombres de usuario en las listas de entrada Copiar Mover Pegar @@ -327,10 +323,10 @@ Añadir campo Eliminar el campo UUID - No puede mover una entrada aquí. + No se puede mover una entrada aquí. No puede copiar una entrada aquí. Mostrar el número de entradas - Mostrar el número de entradas en un grupo + Muestra el número de entradas de un grupo Fondo Actualizar Cerrar campos @@ -400,7 +396,7 @@ La base de datos contiene UUIDs duplicados. Restaurar historial Vaciar papelera de reciclaje - Guardar base de datos + Guardar datos Creando base de datos… %1$s con la misma UUID %2$s ya existe. Esta etiqueta ya existe. @@ -462,7 +458,7 @@ No se pudo inicializar el indicador de desbloqueo avanzado. Error de desbloqueo avanzado: %1$s No se pudo reconocer la impresión de desbloqueo avanzado - No se pudo leer la llave de desbloqueo avanzada. Por favor, bórrela y repita el procedimiento de reconocimiento de desbloqueo. + No se puede leer la clave de desbloqueo avanzada. Por favor, bórrela y repita el procedimiento de reconocimiento del desbloqueo. Extraer la credencial de la base de datos con datos de desbloqueo avanzado Abrir la base de datos con reconocimiento de desbloqueo avanzado Advertencia: Aún debes recordar tu contraseña maestra si usas el reconocimiento de desbloqueo avanzado. @@ -537,9 +533,9 @@ KiB B Sugerencias en línea - Sobrescribir las modificaciones externas guardando la base de datos o recargándola con los últimos cambios. + Fusionar los datos, sobrescribir las modificaciones externas guardando la base de datos o recargarla con los últimos cambios. La información contenida en su archivo de base de datos ha sido modificada fuera de la aplicación. - Recargar la base de datos + Recargar datos El tipo de OTP existente no es reconocido por este formulario, su validación ya no puede generar correctamente el token. ¡Cancelado! Los datos del archivo ya existen. @@ -610,4 +606,25 @@ Color de la base de datos Color de primer plano de la entrada Color de fondo de la entrada + Etiquetas + La recarga de la base de datos borrará los datos modificados localmente. + El hash del archivo no está garantizado porque Android puede cambiar sus datos sobre la marcha. Cambia la extensión del archivo a .bin para una correcta integridad. + Mantener la pantalla encendida + Mantenga la pantalla encendida cuando vea la entrada + Muestra los colores de primer y segundo plano en una entrada + Colores de entrada + Fusionar datos + Cajón de navegación cerrado + Consultable + Expresión regular + Cabecera de navegación + Cajón de navegación abierto + Sensible a las mayúsculas y minúsculas + Secuencia de autotipado + Datos personalizados + Grupo actual + Filtros de búsqueda + Fusionar desde … + Guardar una copia en … + Heredado \ No newline at end of file diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index e501ae3fe..1c740c6a1 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -111,8 +111,6 @@ Inoiz ez Emaitzarik gabeko bilaketa URL kudatzeko euskarririk ez. - Ez bilatu segurtasun kopiaren sarreretan - Kendu segurtasun kopien taldea bilaketen emaitzetatik (.kdb fitxategie dagokie bakarrik) Datubase berria sortzen… Lanean… Babesa @@ -122,11 +120,9 @@ Enkriptatzeko ronda gehiago indar gordineko atakeen kontrako babes gehiago ematen dute, baina kargatzea eta gordetzea moteldu dezakete modu nabarmenean. Datubasea gordetzen… Lekua - Bilatu DB-a antolatzeko ordena Berezia Sarreraren Izena / Deskribapena - Bilaketaren emaitzak Azpimarratu Euskarririk gabeko datubase bertsioa. Maiuskulak diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index a4bb3195a..8e01e02c7 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -17,7 +17,6 @@ بزرگ نسخه پایگاه داده پشتیبانی نمی شود. زیر خط بزنید - نتایج جستجو جستجو ویژه دسترسی @@ -30,7 +29,6 @@ گروه های قبل اول کمترین مرتب سازی - جستجو فاصله برنامه را نکشید اجرای فرمان… @@ -64,8 +62,6 @@ جستجوی زیردریایی درخواست جستجو در هنگام باز کردن یک پایگاه داده جستجوی سریع - Omits \"پشتیبان گیری\" و \"سطل بازیافت\" گروه از نتایج جستجو - از طریق ورودی های پشتیبان جستجو نکنید ایجاد پایگاه داده جدید باز کردن پایگاه داده موجود برای باز کردن این URL یک مرورگر وب نصب کنید. diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 6dd0373f1..edfa408bd 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -111,8 +111,6 @@ Ei koskaan Ei hakutuloksia Asenna web-selain avataksesi URL:n. - Älä etsi varmuuskopioista eikä roskakorista - Poista \'Varmuuskopiot\' ja roskakori hakutuloksista Luodaan uutta tietokantaa… Työskennellään… Suojaus @@ -122,11 +120,9 @@ Lisätty kierrosten määrä parantaa suojausta raa\'alla voimalla tehdyiltä murtoyrityksiltä, mutta voi todella hidastaa lataamista ja tallentamista. Tallennetaan tietokantaa… Tila - Etsi Luonnollinen järjestys Erityistä Tietueen otsikko/kuvaus - Hakutulokset Alleviivattu Ei-tuettu salasanatietokannan versio. Isot kirjaimet diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 76b1f5872..5c01efbec 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -119,8 +119,6 @@ Aucun résultat Installer un navigateur Web pour ouvrir cette URL. Ouvrir une base de données existante - Ne pas rechercher dans les entrées sauvegardées - Omet les groupes \"Sauvegarde\" et \"Corbeille\" des résultats de recherche Création d’une nouvelle base de données… Traitement en cours… Protection @@ -138,7 +136,6 @@ Degré de parallélisme (nombre de fils d’exécution) utilisé par la fonction de dérivation de clé. Enregistrement de la base de données… Espace - Rechercher Trier Ascendant ↓ Groupes avant @@ -151,7 +148,6 @@ Accèsen Spéciaux Rechercher - Résultats de recherche Souligné Version de la base de données non prise en charge. Majuscules @@ -270,8 +266,10 @@ Forêt Divin Classique + Simple Lune Soleil + Réponse Kunzite Collection d’icônes @@ -490,7 +488,7 @@ Affiche l’UUID lié à une entrée ou un groupe Afficher l’UUID L’enregistrement des données n’est pas autorisé pour une base de données ouverte en lecture seule. - Demande à enregistrer des données quand un formulaire est validé + Demande de sauvegarde des données à la fin du remplissage d\'un formulaire Demander à enregistrer des données Essayer d’enregistrer les informations de recherche lors de la sélection manuelle d’une entrée Enregistrer les infos de recherche @@ -623,4 +621,18 @@ Affiche les couleurs de premier plan et d\'arrière-plan dans une entrée Le rechargement de la base de données supprimera les données modifiées localement. Couleurs d\'entrée + Groupe actuel + En-tête de navigation + Tiroir de navigation ouvert + Expression régulière + Tiroir de navigation fermé + Filtres de recherche + Sauvegarder une copie vers … + Fusionner depuis … + Wi-Fi + Recherchable + Hérité + Séquence Auto-Type + Données personnalisées + Sensible à la casse \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index ecc1e652b..1ef25b899 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -108,9 +108,9 @@ Datoteka ključa je prazna. Duljina Prikaži korisnička imena - Prikaži korisnička imena u popisima unosa + Prikazuje korisnička imena u popisima unosa Prikaži broj unosa - Prikaži broj unosa u grupi + Prikazuje broj unosa u grupi Stvaranje baze podataka … Učitavanje baze podataka … Sakrij lozinke @@ -129,7 +129,7 @@ Izbriši Sakrij lozinku Zaključaj bazu podataka - Spremi bazu podataka + Spremi podatke Otvori Traži Prikaži lozinku @@ -144,7 +144,6 @@ Instalirajte web preglednik da bi ste otvorili ovaj URL. Otvori postojeću bazu podataka Stvori novu bazu podataka - Ne pretražuj kopije unosa Stvaranje nove baze podataka … Zaštita Baza podataka sadrži duplicirane UUID-ove. @@ -156,7 +155,6 @@ Spremanje baze podataka … Izvršavanje naredbe … Razmak - Pretraga Sortiraj po Počevši s najnižim ↓ Grupe prije @@ -167,7 +165,6 @@ Vremenu kreiranja Zadnja promjena Pretraga - Rezultati pretrage Nepodržana verzija baze podataka. Upozorenje Nastaviti bez zaštitite otključavanja lozinkom\? @@ -242,7 +239,7 @@ Barem jedan skup podataka za prijavu mora biti postavljen. Svaki niz mora imati ime polja. Nije moguće aktivirati uslugu automatskog ispunjavanja. - Unos se ne može ovdje premijestiti. + Unos se ne može ovdje premjestiti. Unos se ne može ovdje kopirati. Grupa se ne može ovjde kopirati. Nije moguće stvoriti bazu podataka s ovom lozinkom i datotekom ključa. @@ -259,7 +256,6 @@ Premjesti Otkaži Zaštićeno od pisanja - Izostavi grupe \"Backup\" i \"Recycle bin\" iz rezultata pretraživanja Rad u tijeku … Zaštićeno od pisanja Ovisno o upravitelju datoteka, KeePassDX možda neće moći pisati u tvoje spremište. @@ -503,7 +499,7 @@ Otvori prozor naprednog otključavanja za spremanje podataka za prijavu Otvori prozor naprednog otključavanja za otključavanje baze podataka Nije moguće prepoznati digitanlni otisak za napredno otključavanje - Nije moguće pročitati ključ naprednog otključavanja. Izbriši ga i ponovi postupak prepoznavanja. + Nije moguće pročitati ključ naprednog otključavanja. Izbriši ga i ponovi postupak prepoznavanja otključavanja. Tipka Enter Tipka Backspace Odaberi unos @@ -526,9 +522,9 @@ Prijedlozi za automatsko popunjavanje su dodani. Pokušaj prikazivanja prijedloga za automatsko popunjavanje izravno s kompatibilne tipkovnice Pristup datoteci koju je opozvao upravljač datoteka. Zatvori bazu podataka i ponovo je otvori s njezinog mjesta. - Prepiši vanjske promjene spremanjem baze podataka ili je ponovo učitaj s najnovijim promjenama. + Sjedini podatke, prepiši vanjske promjene spremanjem baze podataka ili je ponovo učitaj s najnovijim promjenama. Podaci u datoteci tvoje baze podataka izmijenjeni su izvan programa. - Ponovo učitaj bazu podataka + Ponovo učitaj podatke Ovaj obrazac ne prepoznaje postojeću vrstu jednokratne lozinke. Provjera valjanosti možda više neće pravilno generirati token. GiB MiB @@ -600,4 +596,15 @@ Ime ikone Nisi dopustio/la programu da koristi točan alarm. Kao rezultat toga, značajke koje zahtijevaju tajmer neće biti obavljene s točnim vremenom. Dozvola + Oznake + Sjedini podatke + Ponovnim učitavanjem baze podataka izbrisat će se lokalno izmijenjeni podaci. + Hash-šifra datoteke nije zajamčena jer Android svoje podatke može promijeniti za vrijeme rada. Promijeni datotečni nastavak u .bin radi ispravnog integriteta. + Ostavi ekran uključen + Ostavi ekran uključen tijekom gledanja unosa + Boje unosa + Prikazuje prednje boje i boje pozadine u unosu + Prednja boja unosa + Boja baze podataka + Pozadinska boja unosa \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index c0ed36ef0..bd7d6f668 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -111,8 +111,6 @@ Soha Nincs találat Telepítsen egy webböngészőt az URL megnyitásához. - Ne keressen a biztonsági mentésekben - A „Biztonsági mentés” csoport kihagyása a keresésből (csak a .kdb fájlokra érvényes) Új adatbázis létrehozása… Feldolgozás… Védelem @@ -124,11 +122,9 @@ A további titkosítási körök magasabb védelmet biztosítanak a nyers erőt használó támadások ellen, ugyanakkor jelentősen lassíthatják az adatbázis betöltését vagy mentését. Adatbázis mentése… Szóköz - Keresés Természetes rendezés Speciális Keresés - Találatok Aláhúzás Nem támogatott adatbázis-verzió. Nagybetűs diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index a9e3b15d4..ca073d97b 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -12,7 +12,7 @@ Tampilkan Kata Sandi Cari Buka - Simpan Basisdata + Simpan Database Basisdata Terkunci Sembunyikan Kata Sandi Batal @@ -41,7 +41,7 @@ Ukuran daftar item Tampilkan jumlah entri dalam sebuah grup Tampilkan jumlah entri - Tampilkan nama pengguna dalam daftar entri + Tampilkan nama pengguna di daftar entri Tampilkan nama pengguna Panjangnya File Kunci kosong. @@ -177,8 +177,6 @@ Kontak Minta pencarian saat membuka database Pencarian cepat - Menghilangkan grup \"Cadangan\" dan \"Tempat sampah\" dari hasil penelusuran - Jangan mencari melalui entri cadangan Buat basisdata baru Buka basisdata yang sudah ada Pasang browser web untuk membuka URL ini. @@ -222,13 +220,11 @@ Hapus data ini\? Peringatan Versi database tidak di dukung. - Hasil pencarian Cari Akses Modifikasi Nama pengguna Judul - Cari Jangan tutup aplikasi… Menjalankan perintah… Menyimpan basisdata… @@ -300,7 +296,7 @@ MiB KiB B - Menampilkan UUID yang ditautkan ke entri + Tampilkan UUID yang ditautkan ke entri atau grup Hindari karakter kata sandi di luar format pengkodean teks dalam file database (karakter yang tidak dikenal dikonversi ke huruf yang sama). Huruf besar Penciptaan @@ -315,7 +311,7 @@ Mode simpan Mode pencarian Hapus pembuka kunci lanjutan - Muat ulang database + Muat ulang data URI database tidak dapat diambil. Nama bidang sudah ada. Menyimpan item baru tidak diperbolehkan dalam database read-only @@ -401,7 +397,7 @@ Buka basisdata dengan pengenalan pembuka kunci tingkat lanjut Peringatan: Anda tetap harus mengingat kata sandi induk Anda jika Anda menggunakan pengenalan pembukaan kunci tingkat lanjut. Akses ke berkas dicabut oleh pengelola berkas, tutup basisdata dan buka kembali dari lokasi tempatnya. - Timpa modifikasi eksternal dengan cara menyimpan basisdata atau muat balik dengan perubahan terakhir. + Gabungkan data, timpa perubahan dengan cara menyimpan database atau muat ulang dengan perubahan terbaru. Informasi yang tersimpan di berkas basisdata Anda sudah diubah di luar aplikasi. Lanjut tanpa kunci enkripsi\? Lanjut tanpa proteksi buka kunci dengan kata sandi\? @@ -500,7 +496,7 @@ Peringatan: Papan klip dipakai oleh semua aplikasi. Jika data sensitif disalin, perangkat lunak lain mungkin dapat mendapatkannya. Kunci basis data Anda dengan cepat, Anda bisa mengatur aplikasi untuk mengunci sendiri setelah beberapa saat dan ketika layar berubah mati. Pilih bagaimana entri dan grup diurut. - Anda belum mengizinkan aplikasi untuk menggunakan alarm yang tepat. Sebagai hasilnya, fitur yang membutuhkan pewaktu tidak akan dilakukan dengan waktu yang tepat. + Anda belum mengizinkan aplikasi untuk menggunakan alarm. Sebagai hasilnya, fitur yang membutuhkan timer tidak bisa dilakukan tepat waktu. Izin Rekomendasi pembaruan Rekomendasi mengubah kunci utama (hari) @@ -534,4 +530,12 @@ Dengan membeli versi pro, Warna basis data Gabung data + Memuat ulang database akan menghapus data yang diubah secara lokal. + Hash file tidak terjamin karena Android dapat mengubah datanya saat itu juga. Ubah ekstensi file menjadi .bin agar integritas bisa dihasilkan secara tepat. + Warna latar depan entri + Warna latar belakang entri + Wi-Fi + Label + Buka penguncian lanjutan untuk menyimpan kredensial + Hapus permanen node yang dipilih\? \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 9ce63ff1b..1a9e0f263 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -113,8 +113,6 @@ Mai Nessun risultato di ricerca Installa un browser web per aprire questo URL. - Non cercare nelle voci di backup - Ometti i gruppi «Backup» e «Cestino» dai risultati di ricerca Creazione di un nuovo database… In corso… Protezione @@ -126,11 +124,9 @@ Livelli di cifratura aggiuntivi forniscono una maggiore protezione contro attacchi di tipo forza bruta, ma può rallentare il caricamento e il salvataggio. Salvataggio del database… Spazio - Cerca Ordine naturale Speciali Cerca - Risultati della ricerca Trattino basso Versione del database non supportata. Maiuscole @@ -333,8 +329,8 @@ Lunghezza della password Aggiungi un campo Rimuovi un campo - Non è possibile spostare una voce qui. - Non è possibile copiare una voce qui. + Non puoi spostare una voce qui. + Non puoi copiare una voce qui. Mostra il numero di voci Mostra il numero di voci in un gruppo Aggiorna @@ -365,7 +361,7 @@ Impostazioni di sicurezza Il database contiene Identificativi Univoci Universali (UUID) duplicati. Non è possibile salvare il database. - Salva database + Salva dati Svuota il cestino Esecuzione del comando… Vuoi eliminare definitivamente i nodi selezionati\? @@ -517,7 +513,7 @@ Contenuto Non è possibile inizializzare lo sblocco avanzato. Non è possibile riconoscere lo sblocco avanzato - Non è possibile leggere la chiave di sblocco avanzato. Eliminala e ripeti la prodecura dello sblocco. + Non è possibile leggere la chiave di sblocco avanzato. Eliminala e ripeti la procedura di riconoscimento dello sblocco. Estrai le credenziali del database con i dati dallo sblocco avanzato Attenzione: dovrai sempre ricordare la password principale anche se usi lo sblocco avanzato. Riconoscimento con sblocco avanzato @@ -534,9 +530,9 @@ Mostra i suggerimenti di riempimento campi in una tastiera compatibile Suggerimenti in linea L\'accesso al file è stato revocato dal file manager, chiudi il database e riaprilo dalla sua posizione originale. - Sovrascrivi le modifiche esterne salvando il database o ricaricalo con gli ultimi cambiamenti. + Unisci i dati, sovrascrivi le modifiche esterne salvando il database o ricaricalo con le ultime modifiche. I dati nel tuo database sono stati modificati al di fuori di questa app. - Ricarica database + Ricarica dati Il tipo di OTP esistente non è riconosciuto da questo modulo, la sua convalida potrebbe non generare più correttamente il token. Annullato! GiB @@ -564,7 +560,7 @@ Seleziona un file da cui importare le proprietà dell\'app Importa le proprietà dell\'app Questa parola è riservata e non può essere usata. - Non è possibile spostare un gruppo qui. + Non puoi spostare un gruppo qui. Modelli Gruppo di modelli Usa modelli dinamici per riempire i campi di una voce @@ -611,4 +607,24 @@ Colore database Non hai consentito all\'app di utilizzare un allarme preciso. Di conseguenza, le funzionalità che richiedono un timer non verranno eseguite con un tempo preciso. Autorizzazione + Etichette + Unisci dati + Ricaricare il database eliminerà i dati modificati in locale. + L\'hash del file non è garantito perché Android può cambiare i suoi dati al volo. Cambia l\'estensione del file in .bin per una corretta integrità. + Tieni lo schermo acceso + Tieni lo schermo acceso mentre si vede la voce + Colori della voce + Mostra colori in evidenza e in secondo piano in una voce + Dati personalizzati + Gruppo attuale + Apertura del cassetto di navigazione + Chiusura del cassetto di navigazione + Ricercabile + Intestazione di navigazione + Ereditato + Filtri di ricerca + Espressione regolare + Unisci da … + Salva una copia in … + Distinzione tra maiuscole e minuscole \ No newline at end of file diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index f1b2a045d..291fda3db 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -109,8 +109,6 @@ אף פעם אין תוצאות חיפוש אין מטפל לכתובת url זו. - אל תחפש ערכי גיבוי - הורד את קבוצת \"גיבוי\" מתוצאות חיפוש (תואם רק לקבצי kdb) צור מסד נתונים חדש… עובד… הגנה @@ -120,10 +118,8 @@ מספר סיבובי הצפנה גבוה יותר מספר הגה טובה יותר נגד התקפות, אבל יכול להעלות בהרבה את זמן הטעינה והשמירה. שומר מסד נתונים… גודל - חיפוש סדר מיון מסד נתונים מיוחד - תוצאות חיפוש קו תחתון גרסת מסד נתונים לא נתמכת. רישית diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index e489a6bf0..46e26b444 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -196,8 +196,6 @@ この URL を開くにはウェブブラウザをインストールしてください。 既存のデータベースを開く 新しいデータベースを作成 - バックアップ エントリーを検索しない - 検索結果から [バックアップ] と [ゴミ箱] のグループを省きます クイック検索 データベースを開いたとき、検索を促します サブドメイン検索 @@ -232,7 +230,6 @@ コマンドを実行しています… アプリを終了しないでください… スペース - 検索 フィルタ 並べ替え 昇順 ↓ @@ -246,7 +243,6 @@ 最終アクセス 特殊文字 検索 - 検索結果 アンダースコア 対応していないバージョンのデータベースです。 大文字 @@ -480,8 +476,10 @@ Forest Divine Classic + Simple Moon Sun + Reply Kunzite アイコンパック @@ -601,4 +599,18 @@ 外部アイコン テンプレート アイコン名 + タグ + マージする + Android は実行中にデータを変更しうるため、ファイルのハッシュ値は保証されません。整合性を正しく保つにはファイルの拡張子を.binに変更してください。 + 権限 + 画面の表示を保つ + エントリーを開いているときは画面をオンのままにします + データベース カラー + エントリーのフォアグラウンド カラー + エントリーのバックグラウンド カラー + Wi-Fi + データベースを再度読み込むとローカルで変更されたデータが削除されます。 + アプリに正確なアラームの使用を許可していません。その結果、タイマーを必要とする機能が正確な時間で行われなくなります。 + エントリー カラー + フォアグラウンド・バックグラウンド カラーをエントリー内に表示します \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 3f56cd507..506162433 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -131,7 +131,6 @@ 이 URL을 열기 위해 웹 브라우저를 설치하십시오. 가지고 있는 데이터베이스 열기 새 데이터베이스 생성 - 백업 항목 검색하지 않기 새 데이터베이스 생성 중… 작업 중… 보호 @@ -144,7 +143,6 @@ 바이너리 바이트 단위의 메모리 용량이 키 파생 기능에 사용됩니다. 데이터베이스 저장 중… 공간 - 검색 정렬 오름차순 ↓ 파일 열기 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index f1fefc4ba..526422c40 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -68,8 +68,6 @@ Eiti į URL Niekada Apsauga - Paieška - Paieškos rezultatai Versija %1$s Įspėjimas Atidaryti esamą duomenų bazę @@ -87,7 +85,6 @@ Programėlės neveiklumas Nepavyko išvalyti iškarpinės Teksto dydis grupės sąraše - Neiškoti atkūrimo įrašuose KeePassDX negali apdoroti šio URI. Įrašo duomenys nerasti. Rakto failas yra tuščias. diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index f3100b69e..65ffa0446 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -108,8 +108,6 @@ Nekad Nav meklēšanas rezultātu Neizdevās atvērt saiti. - Nemeklēt kopijās un atkritnē - Izlaist kopijas un atkritni no meklēšanas rezultātiem Izveido jaunu datu bāzi… Darbojas… Aizsardzība @@ -119,11 +117,9 @@ Augstākā līmeņa šifrēšana sniedz lielāku aizsardzību, bet palēnina darbības ar datu bāzēm. Datu bāzes saglabāšana… Atstarpe - Meklēt Izejas Speciālie Ieraksta nosaukums/apraksts - Meklēšanas rezultāti Pasvītrojums Neatbalstīta datu bāzes versija. Lielie burti diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index d7a51b58c..81da1f6c5 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -227,7 +227,6 @@ Autofill service could not be activated. ക്ലിപ്പ്ബോർഡ് ടൈംഔട്ട് ആപ്പ് ടൈംഔട്ട് - തിരയൽ ഫലങ്ങൾ ഹിസ്റ്ററി വീണ്ടെടുക്കുക കാലഹരണപ്പെടുന്നു വൈബ്രേറ്ററി കീപ്രസ്സ് @@ -254,7 +253,6 @@ സ്ക്രീൻ ലോക്ക് എൻ‌ട്രി തിരുത്തുക ഫയലിന്റെ പേര് - തിരയുക എൻ‌ട്രി ചേർക്കുക ലോക്കുചെയ്യുന്നതിന് \'തിരികെ\' അമർത്തുക ക്ലിപ്പ്ബോർഡ് തകരാർ @@ -307,8 +305,6 @@ ഡാറ്റാബേസിൽ തനിപ്പകർപ്പ് UUID-കൾ അടങ്ങിയിരിക്കുന്നു. നിങ്ങളുടെ ഫയൽ മാനേജരെ ആശ്രയിച്ച്, നിങ്ങളുടെ സ്റ്റോറേജിൽ എഴുതാൻ KeePassDX-നെ അനുവദിച്ചേക്കില്ല. ഡാറ്റാബേസ് തുറക്കുമ്പോൾ തിരയൽ അഭ്യർത്ഥിക്കുക - തിരയൽ ഫലങ്ങളിൽ നിന്ന് \"ബാക്കപ്പ്\", \"റീസൈക്കിൾ ബിൻ\" ഗ്രൂപ്പുകൾ ഒഴിവാക്കുക - ബാക്കപ്പ് എൻ‌ട്രികളിലൂടെ തിരയരുത് പരിഷ്‌ക്കരിക്കാവുന്ന റൈറ്റ്-പരിരക്ഷിതമാണ് ടോക്കണിൽ %1$d മുതൽ %2$d അക്കങ്ങൾ വരെ ഉണ്ടായിരിക്കണം. diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 951b23ce5..adffe80d1 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -130,8 +130,6 @@ Ingen søkeresultater Kan ikke håndtere denne nettadressen. Velg en eksisterende database - Ikke søk gjennom sikkerhetskopioppføringer - Utelat \"Sikkerhetskopi\"-gruppen fra søkeresultater (har kunn innvirkning på .kdb-filer) Oppretter ny database… Arbeider… Beskyttelse @@ -149,7 +147,6 @@ Graden av parallellitet (dvs. antallet tråder) brukt av nøkkelutledingsfunksjonen. Lagrer database… Mellomrom - Søk Sorter Laveste først ↓ Grupper før @@ -162,7 +159,6 @@ Sist åpnet Spesialtegn Søk - Søkeresultater Understrek Ustøttet databaseversjon. Store bokstaver @@ -600,4 +596,16 @@ Anbefal endring av hovednøkkel (dager) Nøkkellageret er ikke igangsatt på riktig vis. Bytt tilbake til forrige tastatur automatisk etter kjøring av «Automatisk tastehandling» + Etiketter + Flett data + Sjekksummen for filen er ikke garantert, siden Android kan endre dens data. Endre filutvidelsen til .bin for å beskytte den. + Behold skjermen påslått + (når oppføringen er oppe) + Oppføringsfarger + Forgrunnsfarge for oppføringer + Databasefarge + Bakgrunnsfarge for oppføringer + Wi-Fi + Viser forgrunn- og bakgrunsfarger for en oppføring + Gjeninnlasting av databasen vil slette lokalt endret data. \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index b608161b8..3adeaadd7 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -34,7 +34,7 @@ Bestandsbeheer dat de Intent-actie ACTION_CREATE_DOCUMENT en ACTION_OPEN_DOCUMENT accepteert, is nodig om databasebestanden aan te maken, te openen en op te slaan. Klembord gewist Time-out van het klembord - Tijd van opslag op het klembord (indien ondersteund op dit apparaat) + Duur van opslag op het klembord (indien ondersteund op dit apparaat) Selecteer om %1$s naar klembord te kopiëren Databasesleutel ophalen… Database @@ -104,8 +104,6 @@ Nooit Geen zoekresultaten Installeer een webbrowser om deze URL te openen. - Back-upitems niet doorzoeken - Hiermee worden groepen \"Back-up\" en \"Prullenbak\" uit de zoekresultaten weggelaten Nieuwe database aanmaken… Bezig met verwerken… Verwijderen @@ -114,7 +112,6 @@ Een hoger aantal encryptie-cycli geeft bijkomende bescherming tegen brute-force aanvallen, maar kan het laden en opslaan sterk vertragen. Database opslaan… Ruimte - Zoeken Natuurlijke volgorde Speciaal Zoeken @@ -149,7 +146,7 @@ Ongeldig algoritme. Het sleutelbestand is leeg. Gebruikersnamen tonen - Gebruikersnamen tonen in lijsten + Toont gebruikersnamen in itemlijsten Kopie van %1$s Formulierinvulling Kopiëren @@ -178,7 +175,6 @@ Gecreëerd op Aangepast om Geopend om - Zoekresultaten Waarschuwing Vermijd wachtwoordtekens buiten het tekstcoderingsformaat in het databasebestand (niet-herkende tekens worden geconverteerd naar dezelfde letter). Doorgaan zonder beveiliging voor wachtwoordontgrendeling\? @@ -255,7 +251,7 @@ Bewerk het item met aangepaste velden. Referenties kunnen worden toegevoegd tussen velden van verschillende items. Genereer een sterk wachtwoord Genereer een sterk wachtwoord om aan het item te koppelen, definieer het eenvoudig volgens de criteria van het formulier en vergeet het veilige wachtwoord niet. - Voeg aangepaste velden toe + Aangepaste velden toevoegen Registreer een extra veld, voeg een waarde toe en bescherm dit desgewenst. Ontgrendel je database Database alleen-lezen @@ -288,8 +284,8 @@ Bijdragen App-thema Thema gebruikt in de app - Icon pack - Gebruikt Icon Pack + Pictogrammenpakket + Gebruikt pictogrammenpakket Build %1$s Magikeyboard Magikeyboard (KeePassDX) @@ -299,7 +295,7 @@ Time-out om de toetsenbordinvoer te wissen Kennisgevingsinfo Toon een melding wanneer een item beschikbaar is - Invoer + Item %1$s beschikbaar op Magikeyboard %1$s Wissen bij het sluiten @@ -338,7 +334,7 @@ Je kan hier geen item plaatsen. Je kan hier geen item kopiëren. Aantal items tonen - Toon het aantal items in een groep + Toont het aantal items in een groep Achtergrond Update Velden sluiten @@ -374,7 +370,7 @@ De database bevat dubbele UUID\'s. Probleem oplossen door nieuwe UUID\'s te genereren voor de duplicaten\? Database geopend - Kopieer invoervelden met behulp van het klembord van dit apparaat + Kopieer velden met behulp van het klembord van dit apparaat Geavanceerde ontgrendeling gebruiken om een database gemakkelijker te openen Gegevenscompressie Gegevenscompressie verkleint de omvang van de database @@ -410,8 +406,8 @@ Automatisch zoekresultaten voorstellen vanuit het webdomein of de toepassings-ID Automatisch zoeken Prullenbak - Geef de vergrendelknop weer in de gebruikersinterface - Vergrendelknop weergeven + Toont de vergrendelknop in de gebruikersinterface + Vergrendelknop tonen Instellingen voor automatisch aanvullen De sleutelopslag is niet correct geïnitialiseerd. Geselecteerde knooppunten definitief verwijderen\? @@ -475,12 +471,12 @@ Gegevens Niet-gekoppelde gegevens verwijderen Verwijdert bijlagen die in de database staan, maar niet aan een item zijn gekoppeld - Geeft de UUID weer die is gekoppeld aan een item of een groep + Toont de UUID die is gekoppeld aan een item of een groep UUID tonen Het opslaan van gegevens is niet toegestaan voor een database die is geopend als alleen-lezen. Vraag om gegevens op te slaan wanneer een formulier is gevalideerd Vragen om gegevens op te slaan - Probeer zoekinformatie op te slaan bij u een handmatige invoerselectie + Probeer zoekinformatie op te slaan bij het maken van een handmatige selectie Zoekinformatie opslaan Sluit de database na een selectie voor automatisch aanvullen Database sluiten @@ -536,7 +532,7 @@ Probeer suggesties voor automatisch aanvullen rechtstreeks vanaf een compatibel toetsenbord weer te geven Inline suggesties Toegang tot het bestand is ingetrokken door de bestandsbeheerder, sluit de database en open deze opnieuw vanaf de locatie. - Voeg de gegevens samen, overschrijf de externe aanpassingen door de database op te slaan of laad de database met de jongste wijzigingen opnieuw. + Voeg de gegevens samen, overschrijf de externe wijzigingen door de database op te slaan of herlaad deze met de laatste wijzigingen. De informatie in het databasebestand is buiten de app gewijzigd. Gegevens opnieuw laden Kan de lijst niet correct opnieuw opbouwen. @@ -567,15 +563,15 @@ Identiteitskaart Pincode Kaarthouder - Geeft OTP-tokens weer in de lijst met items - OTP-token weergeven - Toon optie om de gebruiker database-invoer te laten selecteren + Toont OTP-tokens in de lijst met items + OTP-token tonen + Toon optie om de gebruiker een database-item te laten selecteren Handmatige selectie Sjablonen Sjabloongroep Gebruik dynamische sjablonen om de velden van een item in te vullen Sjabloongebruik - Selecteer invoer… + Selecteer item… Extern pictogram Versie Sjabloon @@ -608,7 +604,14 @@ Toestemming Kleur van de database Door de database opnieuw te laden, worden de lokaal gewijzigde gegevens gewist. - Voorgrondkleur invoer + Voorgrondkleur item Gegevens samenvoegen - Achtergrondkleur invoer + Achtergrondkleur item + Labels + Het controlegetal van het bestand is niet gegarandeerd omdat Android zijn gegevens direct kan wijzigen. Wijzig de bestandsextensie in .bin voor de juiste integriteit. + Houd het scherm aan + Houd het scherm aan bij het bekijken van een item + Itemkleuren + Toont voorgrond- en achtergrondkleuren in een item + Wi-Fi \ No newline at end of file diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index 71da3ef73..d028066f1 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -25,7 +25,6 @@ Legg til gruppe Encryption algorithm Programtidsavbrot - App Programinnstillingar Parentesar @@ -101,8 +100,6 @@ Aldri Ingen søkjeresultat Ingen behandlar for denne adressa. - Søk ikkje i kopipostane eller søppelbøtta - Søkjeresultatet inneheld ikkje oppføringar frå \'Backup\' eller søppelbøtta Lager ny database … Arbeider … Ta vekk @@ -111,7 +108,6 @@ Fleire krypteringsomgangar gjev tilleggsvern mot rå makt-åtak, men kan òg gjera lasting og lagring mykje tregare. Lagrar databasen … Mellomrom - Søk DB-sortering Spesial Oppføringa sin tittel/skildring diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 4aa8bd8ce..47e590a98 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -39,7 +39,6 @@ ਵੱਡੇ ਅੱਖਰ (ਅੰਗਰੇਜ਼ੀ) ਗ਼ੈਰ-ਸਹਾਇਕ ਡਾਟਾਬੇਸ ਵਰਜ਼ਨ ਹੈ। ਹੇਠਾਂ ਲਾਈਨ - ਖੋਜ ਨਤੀਜੇ ਖੋਜ ਖਾਸ ਪਹੁੰਚ @@ -50,7 +49,6 @@ ਸਭ ਤੋਂ ਘੱਟ ਪਹਿਲਾਂ ↓ ਲੜੀਬੱਧ ਫਿਲਟਰ - ਖੋਜੋ ਥਾਂ …ਡਾਟਾਬੇਸ ਸੰਭਾਲਿਆ ਜਾ ਰਿਹਾ ਹੈ ਮੈਮੋਰੀ ਵਰਤੋਂ @@ -72,7 +70,6 @@ ਅਧੀਨ-ਡੋਮੇਨ ਖੋਜ ਜਦੋਂ ਡਾਟਾਬੇਸ ਖੋਲ੍ਹਣਾ ਹੋਵੇ ਤਾਂ ਖੋਜ ਦੀ ਮੰਗ ਕਰੋ ਫ਼ੌਰੀ ਖੋਜ - ਬੈਕਐਪ ਐਂਟਰੀਆਂ ਰਾਹੀਂ ਨਾ ਖੋਜੋ ਨਵਾਂ ਡਾਟਾਬੇਸ ਬਣਾਓ ਮੌਜੂਦਾ ਡਾਟਾਬੇਸ ਨੂੰ ਖੋਲ੍ਹੋ ਇਹ URL ਖੋਲ੍ਹਣ ਲਈ ਵੈੱਬ ਬਰਾਊਜ਼ਰ ਇੰਸਟਾਲ ਕਰੋ। diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 8b76a87a6..fc7c35e5f 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -100,8 +100,6 @@ Nigdy Brak wyników wyszukiwania Zainstaluj przeglądarkę internetową, aby otworzyć ten adres URL. - Nie wyszukuj wpisów kopii zapasowej - Pomija grupy \"Kopia zapasowa\" i \"Kosz\" z wyników wyszukiwania Tworzenie nowej bazy danych… Pracuję… Usuń @@ -110,7 +108,6 @@ Dodatkowe rundy szyfrowania zapewniają lepszą ochronę przed atakami typu brute force, ale mogą znacznie spowolnić ładowanie i zapisywanie. Zapisywanie bazy danych… Spacja - Szukaj Porządek naturalny Znaki specjalne Szukaj @@ -146,7 +143,7 @@ Błędny algorytm. Plik klucza jest pusty. Pokaż nazwy użytkowników - Pokaż nazwy użytkowników na listach wpisów + Wyświetla nazwy użytkowników na listach wpisów Skopiowano %1$s Wypełnianie formularzy Kopiuj @@ -170,7 +167,6 @@ Utwórz Modyfikacja Dostęp - Wyniki wyszukiwania Ostrzeżenie Unikaj znaków hasła spoza formatu kodowania tekstu w pliku bazy danych (nierozpoznane znaki są konwertowane na tę samą literę). Kosz na dole @@ -326,12 +322,12 @@ Usuń pole Nie możesz tutaj skopiować wpisu. Pokaż liczbę wpisów - Pokaż liczbę wpisów w grupie + Wyświetla liczbę wpisów w grupie Tryb wyboru Dodaj węzeł Powtórz przełączanie widoczności hasła UUID - W to miejsce nie można przenieść wpisu. + Nie możesz tutaj przenieść wpisu. Pole hasła Pole pliku klucza Tło @@ -356,7 +352,7 @@ Cyfry Algorytm OTP - Tutaj nie można skopiować grupy. + Nie możesz tutaj skopiować grupy. Tworzenie bazy danych… Ustawienia zabezpieczeń Ustawienia klucza głównego @@ -476,8 +472,8 @@ Wyświetla identyfikator UUID powiązany z wpisem lub grupą Pokaż UUID Zapisywanie danych nie jest dozwolone dla bazy danych otwartej jako tylko do odczytu. - Poproś o zapisanie danych po zatwierdzeniu formularza - Poproś o zapisanie danych + Pytaj o zapisanie danych po zakończeniu wypełniania formularza + Pytaj o zapisanie danych Spróbuj zapisać informacje wyszukiwania podczas dokonywania ręcznego wyboru pozycji Zapisz informacje wyszukiwania Zamknij bazę danych po wybraniu autouzupełniania @@ -506,7 +502,7 @@ Uwierzytelnienie urządzenia Wpisz hasło, a następnie kliknij ten przycisk. Nie można rozpoznać zaawansowanego wydruku odblokowującego - Nie można odczytać zaawansowanego klucza odblokowuj. Usuń go i powtórz procedurę rozpoznawania odblokowania. + Nie można odczytać zaawansowanego klucza odblokowującego. Usuń go i powtórz procedurę rozpoznawania odblokowania. Wyodrębnij poświadczenia bazy danych z zaawansowanymi danymi odblokowującymi Ostrzeżenie: Jeśli używasz zaawansowanego rozpoznawania odblokowania, nadal musisz zapamiętać hasło główne. Zaawansowane rozpoznawanie odblokowania @@ -531,7 +527,7 @@ Otwórz monit odblokowania zaawansowanego, aby odblokować bazę danych Otwórz monit odblokowania zaawansowanego, aby odblokować bazę danych Dostęp do pliku odwołany przez menedżera plików, zamknij bazę danych i otwórz ją ponownie z jej lokalizacji. - Scal dane, zastąp modyfikacje zewnętrzne poprzez zapisanie bazy danych lub ponownie załaduj bazę danych z najnowszymi zmianam. + Scal dane, zastąp modyfikacje zewnętrzne poprzez zapisanie bazy danych lub ponownie załaduj ją z najnowszymi zmianam. Informacje zawarte w pliku bazy danych zostały zmodyfikowane poza aplikacją. Ponownie załaduj dane GiB @@ -560,7 +556,7 @@ Wybierz plik, aby zaimportować właściwości aplikacji Importuj właściwości aplikacji Wystąpił błąd podczas wykonywania akcji w bazie danych. - Nie możesz przenieść tutaj grupy. + Nie możesz tutaj przenieść grupy. To słowo jest zastrzeżone i nie może być używane. Imię i nazwisko Standardowe @@ -610,4 +606,24 @@ Kolor bazy danych Ponowne załadowanie bazy danych spowoduje usunięcie lokalnie zmodyfikowanych danych. Scal dane + Kolory wpisu + Hash pliku nie jest gwarantowany, ponieważ system Android może zmieniać swoje dane w locie. Zmień rozszerzenie pliku na .bin, aby uzyskać prawidłową integralność. + Nie wyłączaj ekranu podczas oglądania wpisu + Znaczniki + Nie wyłączaj ekranu + Wyświetla kolory pierwszego planu i tła we wpisie + Nagłówek nawigacyjny + Szuflada nawigacyjna otwarta + Szuflada nawigacyjna zamknięta + Wielkość liter + Wyrażenie regularne + Dziedziczone + Sekwencja automatycznego wpisywania + Obecna grupa + Dane niestandardowe + Scal z… + Filtry wyszukiwania + Automatyczne wpisywanie + Przeszukiwalne + Zapisz kopię w… \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 10f6824a7..b1cbbe1a9 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -76,7 +76,7 @@ Comprimento Senha Senha - Não foi possível ler credenciais. + Não foi possível ler as credenciais. Não foi possível reconhecer o formato do banco de dados. Comprimento Tamanho da lista de grupos @@ -102,8 +102,6 @@ Nunca Sem resultados na busca Instale um navegador para abrir esta URL. - Não buscar por entradas no backup ou na lixeira - Omite os grupos \"Backup\" e \"Lixeira\" dos resultados da busca Criando novo banco de dados… Trabalhando… Remover @@ -112,7 +110,6 @@ Rodadas adicionais de criptografia adicionam mais proteção contra ataques de força bruta, mas podem tornar o processo de carregar e salvar mais lentos. Salvando banco de dados… Espaço - Busca Ordenação natural Especial Buscar @@ -171,7 +168,6 @@ Data de criação Última modificação Último acesso - Resultados da busca Aviso Evite caracteres fora do formato de codificação do arquivo do banco (todos os caracteres não reconhecidos são convertidos para a mesma letra). Continuar sem proteção de desbloqueio por senha\? @@ -286,7 +282,7 @@ Não foi possível carregar seu banco de dados. Não pôde carregar a chave. Tente diminuir o \"Uso de Memória\" do KDF. Mostrar nomes de usuário - Mostrar nomes de usuário em listas de entrada + Exibe nomes de usuário em listas de entrada Área de transferência Build %1$s Magikeyboard @@ -329,10 +325,10 @@ Adicionar campo Excluir campo UUID - Você não pode mover uma entrada para cá. - Você não pode copiar uma entrada daqui. + Você não pode mover uma entrada aqui. + Você não pode copiar uma entrada aqui. Mostrar número de entradas - Mostrar o número de entradas dentro de um grupo + Exibe o número de entradas em um grupo Nó filho Caixa de seleção da senha Caixa de seleção do arquivo-chave @@ -522,12 +518,12 @@ Incapaz de inicializar o prompt de desbloqueio avançado. Erro de desbloqueio avançado: %1$s Não foi possível reconhecer a impressão de desbloqueio avançado - Não é possível ler a chave de desbloqueio avançado. Exclua-o e repita o procedimento de reconhecimento de desbloqueio. + Não é possível ler a chave de desbloqueio avançada. Por favor, apague-a e repita o procedimento de reconhecimento de desbloqueio. Extraia credencial de banco de dados com dados de desbloqueio avançado Banco de dados aberto com reconhecimento avançado de desbloqueio Aviso: você ainda precisa lembrar sua senha mestra se usar o reconhecimento de desbloqueio avançado. Reconhecimento de desbloqueio avançado - Abra o prompt de desbloqueio avançado para armazenar credenciais + Abra o prompt de desbloqueio avançado para armazenar as credenciais Abra o prompt de desbloqueio avançado para desbloquear o banco de dados Atualização de segurança biométrica necessária. Nenhuma credencial biométrica ou de dispositivo está registrada. @@ -608,4 +604,12 @@ Cor de plano de fundo da entrada Recarregar o banco de dados excluirá os dados modificados localmente. Mesclar dados + Etiquetas + Mantenha a tela ligada + Mantenha a tela ligada enquanto estiver vendo a entrada + Cores de entrada + Exibe as cores de primeiro plano e de fundo em uma entrada + Você não pode mover um grupo aqui. + Wi-Fi + O hash do arquivo não é garantido porque o Android pode alterar seus dados em tempo real. Altere a extensão do arquivo para .bin para obter a integridade correta. \ No newline at end of file diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 80f25c909..bbeac9952 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -112,8 +112,6 @@ Nunca A pesquisa não obteve resultados Instale um navegador para abrir esta URL. - Não procurar por entradas na cópia de segurança e lixo - Omite os grupos \"Cópia de segurança\" e \"Caixote da reciclagem\" dos resultados da busca A criar nova base de dados… Em funcionamento… Proteção @@ -125,11 +123,9 @@ As rodadas adicionais de encriptação fornecem mais proteção contra ataques de força bruta, mas podem tornar o processo de carregar e guardar mais lentos. A guardar a base de dados… Espaço - Pesquisar Ordenação natural Especiais Pesquisar - Resultados da pesquisa Sublinhado Versão da base de dados não suportada. Maiúsculas diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 127e6e370..07cc07b23 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -107,7 +107,6 @@ Maiúsculas Versão da base de dados não suportada. Sublinhado - Resultados da pesquisa Pesquisar Especiais Último acesso @@ -120,7 +119,6 @@ Grupos primeiro Mais baixo primeiro ↓ Ordenar - Pesquisar Espaço A guardar a base de dados… Grau de paralelismo (ou seja, número de threads) usado pela função de derivação de chave. @@ -254,8 +252,6 @@ Mostrar ficheiros recentes Solicitar uma pesquisa quando abrir a base de dados Pesquisa rápida - Omite os grupos \"Cópia de segurança\" e \"Caixote da reciclagem\" dos resultados da busca - Não procurar por entradas na cópia de segurança e lixo Sobre Mascarar palavras-passe (***) por predefinição Ocultar palavras-passe @@ -281,7 +277,7 @@ Eliminar permanentemente os nós selecionados\? A executar o comando… Esvaziar caixote da reciclagem - Guardar base de dados + Guardar dados Não foi possível guardar a base de dados. Ligue a sua palavra-passe às suas credenciais biométricas ou do dispositivo para desbloquear rapidamente a sua base de dados. Configurações do teclado do dispositivo @@ -351,7 +347,7 @@ Atualizar Plano de fundo Um gestor de ficheiros que aceita a ação intencional ACTION_CREATE_DOCUMENT e ACTION_OPEN_DOCUMENT é necessário para criar, abrir e gravar ficheiros da base de dados. - Mostrar o número de entradas dentro de um grupo + Mostra o número de entradas dentro de um grupo Mostrar número de entradas Não se pode copiar uma entrada aqui. Não se pode mover uma entrada para aqui. @@ -400,7 +396,7 @@ Compilação %1$s A criar a chave da base de dados… Área de transferência - Mostrar nomes de utilizador na lista entradas + Mostra nomes de utilizador nas listas de entradas Mostrar nomes de utilizador Não foi possível carregar a chave. Tente diminuir o \"Uso de memória\" do KDF. Não foi possível abrir a sua base de dados. @@ -466,14 +462,14 @@ É necessária uma atualização de segurança biométrica. Não está registada nenhuma credencial biométrica ou de dispositivo. Acesso ao ficheiro revogado pelo gestor de ficheiros. Feche a base de dados e reabra-a a partir da sua localização. - Substitua as alterações externas, guardando a base de dados ou recarregando-a com as últimas alterações. + Unir os dados, substituir as alterações externas guardado a base de dados ou recarregá-los com as últimas alterações. A informação contida no seu ficheiro da base de dados foi alterada fora da aplicação. Eliminar permanentemente todos os nós do caixote da reciclagem\? Modo de registo Modo de guardar Modo de pesquisa Eliminar chave de desbloqueio avançada - Recarregar base de dados + Recarregar dados Não foi possível reconstruir adequadamente a lista. Não foi possível recuperar o URI da base de dados. O nome do campo já existe. @@ -580,5 +576,18 @@ Informação de palavra-passe de uso único Nome do ícone Permissão - Não permitiu que a app usasse um alarme exato. Como resultado, as funcionalidades que requerem um temporizador não serão feitas com um tempo exato. + Não permitiu que a aplicação usasse um alarme exato. Como resultado, as funcionalidades que requerem um temporizador não serão feitas com um tempo exato. + O hash do ficheiro não é garantido porque o Android pode alterar os seus dados em tempo real. Altere a extensão do ficheiro para .bin para obter a integridade correta. + Mostra as cores de fundo e de primeiro plano numa entrada + Etiquetas + Wi-Fi + Unir dados + Recarregar a base de dados irá eliminar os dados alterados localmente. + Manter o ecrã ligado + Manter o ecrã ligado ao ver uma entrada + Cores da entrada + Não se pode mover um grupo para aqui. + Cor da base de dados + Cor do primeiro plano da página inicial + Cor do fundo da página inicial \ No newline at end of file diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index d91d5ecc6..075333673 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -208,7 +208,6 @@ Executarea comenzii … Nu omori aplicația … Spatiu - Cauta Sorteaza Prima cea mai mică ↓ Grupuri înainte @@ -221,7 +220,6 @@ Acces Special Căutare - Rezultatele căutării Subliniere Versiunea bazei de date neacceptată. Cu majuscule @@ -407,8 +405,6 @@ Pachet de pictograme folosit în aplicație Ascundeți intrările expirate Înscrierile expirate vor fi ascunse - Nu căutați prin intrări de rezervă - Omite grupurile „Backup” și „Recycle bin” din rezultatele căutării Căutare rapidă Solicitați o căutare atunci când deschideți o bază de date Salvați locația bazelor de date diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 3fbfdf81c..8711c725e 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -112,8 +112,6 @@ Никогда Совпадения не найдены Установите браузер, чтобы открыть этот URL. - Не искать в резервных копиях - Не искать в группах \"Резервирование\" и \"Корзина\" Создание новой базы… Обработка… Защита @@ -125,11 +123,9 @@ Дополнительные раунды шифрования – выше стойкость базы к подбору пароля, но медленнее открытие и сохранение. Сохранение базы… П р о б е л - Поиск Естественный порядок $пеци@льные Поиск - Результаты поиска _Подчёркивание_ Неподдерживаемая версия базы. ЗАГЛАВНЫЕ @@ -483,7 +479,7 @@ Показывать UUID, связанный с записью или группой Показывать UUID Сохранение данных невозможно для базы, открытой только для чтения. - Запрашивать сохранение данных после проверки формы + Запрашивать сохранение данных после завершения заполнения формы Запрос сохранения данных Закрывать базу после выбора автозаполнения Закрыть базу @@ -610,4 +606,23 @@ Цвет текста записи Объединить данные Перезагрузка базы удалит локально изменённые данные. + Метки + Хеш файла не гарантируется, поскольку Android может изменять свои данные на лету. Измените расширение файла на .bin для сохранения целостности. + Держать экран включённым + Не отключать экран при просмотре записи + Цвета записей + Показывать цвета текста и фона записи + Открыть панель навигации + Заголовок навигации + Искомое + Наследуемое + С учётом регистра + Сохранить копию в… + Объединить с… + Фильтры поиска + Регулярное выражение + Закрыть панель навигации + Последовательность автонабора + Текущая группа + Пользовательские данные \ No newline at end of file diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 7edb79f9e..374bf96ee 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -101,8 +101,6 @@ Nikdy Žiadne výsledky hľadania Žiaden manažér pre url. - Neprehľadávať položky - Vynechať skupinu \'Backup\' a Recycle Bin z výsledkov hľadania Vytváram novú databázu… Pracujem… Odstrániť @@ -111,7 +109,6 @@ Vyššie opakovania šifrovania dávajú vyššiu ochranu proti útokom hrubou silou, ale môžu spomaliť načítavanie a ukladanie. Ukladám databázu… Miesto - Hľadať DB zoradenie poradia Špeciálne Záznam názov/popis diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 37a822a4c..401164e70 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -110,8 +110,6 @@ Aldrig Inget sökresultat Installera en webbläsare för att öppna denna URL. - Sök inte efter backup-poster - Utelämnar poster i grupperna \"Backup\" och \"Papperskorg\" Skapar ny databas… Arbetar… Skydd @@ -121,11 +119,9 @@ Högre antal krypteringsrundor ger ytterligare skydd mot bruteforce-attacker, men kan göra det betydligt långsammare att ladda och spara. Sparar databas… Mellanslag - Sök Naturlig ordning Specialtecken Postens titel/beskrivning - Sökresultat Understreck Databasversionen stöds ej. Versaler diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index b314ef83d..fb14a88b7 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -96,7 +96,7 @@ Anahtar dosya boş. Uzunluk Kullanıcı adlarını göster - Girdi listelerinde kullanıcı adlarını göster + Girdi listelerinde kullanıcı adlarını görüntüler Liste ögelerinin boyutu Öge listesindeki metin boyutu Veri tabanı yükleniyor… @@ -130,8 +130,6 @@ Bu URL\'u açmak için bir web tarayıcısı yükleyin. Mevcut veri tabanını aç Yeni veri tabanı oluştur - Yedek girdilerde arama - Arama sonuçlarından \"Yedekleme\" ve \"Geri dönüşüm kutusu\" gruplarını atlar Yeni veri tabanı oluşturuluyor… Çalışıyor… Koruma @@ -149,7 +147,6 @@ Anahtar türev fonksiyonu tarafından kullanılan paralellik derecesi (yani iplik sayısı). Veri tabanı kaydediliyor… Boşluk - Ara Sırala Önce en düşük ↓ Grup önce @@ -162,7 +159,6 @@ Erişim Özel Ara - Arama Sonuçları Eksi Altı çizili Desteklenmeyen veri tabanı sürümü. @@ -320,7 +316,7 @@ Bir girdiyi buraya taşıyamazsınız. Bir girdiyi buraya kopyalayamazsınız. Girdi sayısını göster - Bir gruptaki girdilerin sayısını göster + Bir gruptaki girdilerin sayısını görüntüler Kilitlemek için \'Geri\'ye basın Geri dönüşüm kutusu Girdi seçimi @@ -470,7 +466,7 @@ Bir girdiye veya gruba bağlı UUID\'yi görüntüler UUID\'yi göster Salt okunur olarak açılan bir veri tabanı için veri kaydına izin verilmiyor. - Bir form doğrulandığında verileri kaydetmek için sor + Form doldurma işlemi tamamlandığında verileri kaydetmek için sor Verileri kaydetmek için sor Elle girdi seçimi yaparken arama bilgilerini kaydetmeyi dene Arama bilgilerini kaydet @@ -526,7 +522,7 @@ Doğrudan uyumlu bir klavyeden otomatik doldurma önerileri görüntülemeye çalış Satır içi öneriler Dosyaya erişim dosya yöneticisi tarafından iptal edildi, veri tabanını kapatın ve bulunduğu yerden yeniden açın. - Verileri birleştirin, veri tabanını kaydederek harici değişikliklerin üzerine yazın veya en son değişikliklerle veri tabanını yeniden yükleyin. + Verileri birleştirin, veri tabanını kaydederek harici değişikliklerin üzerine yazın veya en son değişikliklerle yeniden yükleyin. Veri tabanı dosyanızda bulunan bilgiler, uygulamanın dışında değiştirildi. Verileri yeniden yükle GiB @@ -605,4 +601,23 @@ Girdi arka plan rengi Verileri birleştir Veri tabanını yeniden yüklemek, yerel olarak değiştirilen verileri silecektir. + Etiketler + Android, verilerini anında değiştirebildiğinden dosyanın sağlama toplamı garanti edilmez. Doğru bütünlük için dosya uzantısını .bin olarak değiştirin. + Ekranı açık tut + Girdi renkleri + Bir girdide ön plan ve arka plan renklerini görüntüler + Girdiyi izlerken ekranı açık tutun + Gezinme başlığı + Gezinme çekmecesi açık + Gezinme çekmecesi kapalı + Aranabilir + Devralınan + Otomatik yazma sırası + Şuradan birleştir… + Düzenli ifade + Arama filtreleri + Büyük/küçük harf duyarlı + Geçerli grup + Özel veri + Bir kopyasını şuraya kaydet… \ No newline at end of file diff --git a/app/src/main/res/values-tt/strings.xml b/app/src/main/res/values-tt/strings.xml index c1438ab36..57d224e61 100644 --- a/app/src/main/res/values-tt/strings.xml +++ b/app/src/main/res/values-tt/strings.xml @@ -13,7 +13,6 @@ Кулланучы исеме Исем Иң түбән беренче ↓ - Эзләү Тиз эзләү Беркайчан да Серсүзне күрсәтү diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 548267ad0..72ff83020 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -102,8 +102,6 @@ Ніколи Нічого не знайдено Щоб відкрити цю URL-адресу, встановіть переглядач Інтернету. - Не шукати у записах резервних копій - Не шукати у групах «Резервна копія» та «Кошик» Створення нової бази даних… Працює… Вилучити @@ -112,7 +110,6 @@ Додаткова кількість циклів шифрування забезпечує кращий захист від грубих атак, але може дещо уповільнити завантаження та збереження. Збереження бази даних… Пробіл - Пошук Звичайний порядок Спеціальний Введіть заголовок/опис @@ -199,7 +196,7 @@ Створення бази даних… Показувати кількість записів у групі Показувати кількість записів - Показувати імена користувачів у переліках записів + Показувати імена користувачів у списках записів Показувати імена користувачів Файл ключа порожній. %1$s з таким самим UUID %2$s вже існує. @@ -215,7 +212,7 @@ Неможливо створити базу даних із цим паролем та файлом ключа. Не вдалося створити файл бази даних. Ви не можете копіювати групу сюди. - Ви не можете копіювати записи сюди. + Ви не можете копіювати запис сюди. Ви не можете перемістити запис сюди. Не вдалось ввімкнути службу автозаповнення. Ця мітка вже існує. @@ -421,7 +418,6 @@ Надайте дозвіл змінювати файл для збереження змін у базі даних У паролі, уникайте символів поза форматом кодування тексту у файлі бази даних (нерозпізнані символи будуть перетворені на ту ж літеру). Попередження - Результати пошуку Доступ Востаннє змінено Дата створення @@ -476,7 +472,7 @@ Показ пов\'язаного з записом чи групою UUID Показувати UUID Збереження даних заборонено для бази даних, відкритої лише для читання. - Запитувати зберігати дані після перевірки форми + Запитувати зберігати дані після заповнення форми Запит збереження даних Намагатися зберегти подробиці пошуку під час вибору запису власноруч Збереження відомостей пошуку @@ -531,7 +527,7 @@ Спробувати показ пропозицій автозаповнення безпосередньо з сумісної клавіатури Вбудовані пропозиції Доступ до файлу скасовано менеджером файлів, закрийте базу даних і знову відкрийте її з її розташування. - Об\'єднати дані, перезаписати зовнішні зміни, зберігши базу даних або перезавантажити базу даних з найновішими змінами. + Об\'єднати дані, перезаписати зовнішні зміни, зберігши базу даних або перезавантажити її з найновішими змінами. Відомості, що містяться у файлі бази даних, змінено за межами застосунку. Перезавантажити дані ГіБ @@ -610,4 +606,23 @@ Колір тексту запису Об\'єднати дані Перезавантаження бази даних видалить локально змінені дані. + Не вимикати екран + Не вимикати екран під час перегляду запису + Кольори записів + Показує кольори шрифту й тла запису + Мітки + Хеш файлу не гарантується, оскільки Android може змінювати свої дані на льоту. Змініть розширення файлу на .bin для збереження цілісності. + Панель навігації + Відкрити панель навігації + Закрити панель навігації + Шукане + Успадковане + Поточна група + З урахуванням регістру + Регулярний вираз + Об\'єднати з … + Користувацькі дані + Фільтри пошуку + Послідовність автовведення + Зберегти копію в … \ No newline at end of file diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml index bf17bcbd9..c7be9d679 100644 --- a/app/src/main/res/values-v21/styles.xml +++ b/app/src/main/res/values-v21/styles.xml @@ -48,7 +48,11 @@ - + @@ -58,7 +62,7 @@ - diff --git a/app/src/main/res/values-v21/styles_divine.xml b/app/src/main/res/values-v21/styles_divine.xml index 632408253..450665471 100644 --- a/app/src/main/res/values-v21/styles_divine.xml +++ b/app/src/main/res/values-v21/styles_divine.xml @@ -22,14 +22,14 @@ @style/KeepassDXStyle.Preference.v21 @color/grey_dark @color/grey_dark - @style/KeepassDXStyle.Clear.DateTime.Dialog - @style/KeepassDXStyle.Clear.DateTime.Dialog + @style/KeepassDXStyle.White.DateTime.Dialog + @style/KeepassDXStyle.White.DateTime.Dialog diff --git a/app/src/main/res/values-v21/styles_reply.xml b/app/src/main/res/values-v21/styles_reply.xml new file mode 100644 index 000000000..2bccd5769 --- /dev/null +++ b/app/src/main/res/values-v21/styles_reply.xml @@ -0,0 +1,35 @@ + + + + + + diff --git a/app/src/main/res/values-v21/styles_simple.xml b/app/src/main/res/values-v21/styles_simple.xml new file mode 100644 index 000000000..d5a0f4ea5 --- /dev/null +++ b/app/src/main/res/values-v21/styles_simple.xml @@ -0,0 +1,35 @@ + + + + + + diff --git a/app/src/main/res/values-v23/fingerprint.xml b/app/src/main/res/values-v23/fingerprint.xml index 9ca38763b..aef4ccdf0 100644 --- a/app/src/main/res/values-v23/fingerprint.xml +++ b/app/src/main/res/values-v23/fingerprint.xml @@ -42,9 +42,7 @@ M0 0 L160 0 L160 40 L0 40 Z M0 120 L160 120 L160 160 L0 160 Z - #deffffff - #ffffffff - - #ff607d8b + ?attr/colorOnAccentColor + ?attr/colorOnAccentColor diff --git a/app/src/main/res/values-v23/styles.xml b/app/src/main/res/values-v23/styles.xml index a45d29099..4aac1f9e7 100644 --- a/app/src/main/res/values-v23/styles.xml +++ b/app/src/main/res/values-v23/styles.xml @@ -21,4 +21,11 @@ + + + diff --git a/app/src/main/res/values-v23/styles_simple.xml b/app/src/main/res/values-v23/styles_simple.xml new file mode 100644 index 000000000..8bd4e1137 --- /dev/null +++ b/app/src/main/res/values-v23/styles_simple.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/values-v27/styles_classic.xml b/app/src/main/res/values-v27/styles_classic.xml new file mode 100644 index 000000000..003e545c2 --- /dev/null +++ b/app/src/main/res/values-v27/styles_classic.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/values-v27/styles_divine.xml b/app/src/main/res/values-v27/styles_divine.xml new file mode 100644 index 000000000..251346395 --- /dev/null +++ b/app/src/main/res/values-v27/styles_divine.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/values-v27/styles_simple.xml b/app/src/main/res/values-v27/styles_simple.xml new file mode 100644 index 000000000..bf756fc2b --- /dev/null +++ b/app/src/main/res/values-v27/styles_simple.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 46bbcecf0..20d6e02fe 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -109,7 +109,6 @@ 更多的迭代次数能更好地抵抗暴力破解攻击,但也会增加读取和保存的时间。 正在保存数据库… 空格 - 搜索 自然排序 特殊符号 搜索 @@ -152,8 +151,6 @@ 显示密码 只读 可修改 - 搜索时忽略备份条目 - 从搜索结果中忽略“备份”和“回收站”群组 保护 只读 根据您的文件管理器,KeePassDX 可能不允许在您的存储中写入数据。 @@ -171,7 +168,6 @@ 新建时间 修改时间 访问时间 - 搜索结果 警告 版本 %1$s 历史 @@ -333,7 +329,7 @@ 添加字段 删除字段 UUID - 无法移动条目到此处。 + 你不能将条目移到此处。 您不能在此处复制条目。 显示条目数量 显示群组中的条目数 @@ -476,7 +472,7 @@ 显示与一个条目或群组相链接的 UUID 显示 UUID 以只读方式打开的数据库不允许保存数据。 - 表单经过验证时,询问是否保存数据 + 填写完表单后,询问是否保存数据 询问是否保存数据 手动选择条目时,尝试保存搜索信息 保存搜索信息 @@ -610,4 +606,24 @@ 条目背景色 合并数据 重新加载数据库将删除本地修改的数据。 + 文件的哈希值不能保证,因为 Android 可以在运行中更改数据。将文件扩展名更改为 .bin 以确保完整性。 + 保持屏幕亮起 + 查看条目时保持屏幕亮起 + 条目颜色 + 显示条目的前景色和背景色 + 标签 + 可搜索 + 自动输入序列 + 自定义数据 + 当前组 + 正则表达式 + 合并自… + 保存副本到… + 继承的 + 导航标题 + 导航抽屉关闭 + 导航抽屉开启 + 搜索过滤器 + 区分大小写 + 自动输入 \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index f2a994c16..761897635 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -448,8 +448,6 @@ 沒有這個連結的處理程式。 通知 卡號 - 從搜尋結果中省略\"備份\"和\"回收桶\"組 - 不要搜尋備份的項目 點擊以開啟高級解鎖提示來存儲憑證 點擊以使用高級解鎖 其他 @@ -495,9 +493,7 @@ 保存模式 正在保存資料庫… 條目名稱/說明 - 搜尋 搜尋模式 - 搜尋結果 安全註記 安全 種子 diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index b97eb3bc7..11c9ca46a 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -23,6 +23,10 @@ + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 3f27c7b43..dc19f7eb3 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -41,11 +41,9 @@ #000001 #E0000000 - #3c474c - #263238 - - #90CAF9 - #5DB2F6 + #E6F3FF + #90CAF9 + #5DB2F6 #2196F3 #1E88E5 #1976D2 @@ -56,20 +54,27 @@ #2486AB #2c7a96 - #a5d6a7 + #a5d6a7 + #66BB6A #4caf50 #43a047 #388e3c #2e7d32 + #1B5E20 - #ef9a9a + #ef9a9a + #EF5350 #f44336 #e53935 #d32f2f - #ffa726 + #FFCC80 + #ffa726 + #FF9800 #fb8c00 - #ef6c00 + #F57C00 + #ef6c00 + #E65100 #F2EDFA #F7F3FD @@ -82,4 +87,26 @@ #451D5D #E0451D5D #361748 + + #FCFCFF + #E0FCFCFF + #202124 + #E0202124 + #3E4247 + #3c474c + #263238 + #5E97F6 + #1A73E8 + + #FFBA52 + #F9AA33 + #EDF0F2 + #D1DBE0 + #4A6572 + #344955 + #E0232F34 + #232F34 + #27343A + #414445 + diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 0e146ac90..446636576 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -46,6 +46,7 @@ keepass + _copy .kdbx KeePassDX Database keepassdx_%1$s.properties @@ -69,8 +70,6 @@ true enable_keep_screen_on_key false - omit_backup_search_key - true auto_focus_search_key false subdomain_search_key @@ -325,26 +324,34 @@ KeepassDXStyle_Black KeepassDXStyle_Clear KeepassDXStyle_Dark + KeepassDXStyle_Simple + KeepassDXStyle_Simple_Night KeepassDXStyle_Blue KeepassDXStyle_Blue_Night KeepassDXStyle_Red KeepassDXStyle_Red_Night + KeepassDXStyle_Reply + KeepassDXStyle_Reply_Night KeepassDXStyle_Purple KeepassDXStyle_Purple_Dark @string/list_style_name_light @string/list_style_name_white @string/list_style_name_clear + @string/list_style_name_simple @string/list_style_name_blue @string/list_style_name_red + @string/list_style_name_reply @string/list_style_name_purple @string/list_style_name_night @string/list_style_name_black @string/list_style_name_dark + @string/list_style_name_simple_night @string/list_style_name_blue_night @string/list_style_name_red_night + @string/list_style_name_reply_night @string/list_style_name_purple_dark KeepassDXStyle_Brightness_Light diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 536aff4d5..76e4370c8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -61,6 +61,9 @@ Database color Entry foreground color Entry background color + Navigation header + Navigation drawer open + Navigation drawer close Validate Discard changes? Discard @@ -92,9 +95,14 @@ Attachments Keyfile Modified + Searchable + Inherit + Auto-Type + Auto-Type sequence Could not find entry data. Password Tags + Custom data Save Title Set up one-time password @@ -103,7 +111,11 @@ Period (seconds) Counter Digits + Search filters Algorithm + Current group + Case sensitive + Regular expression Debit / Credit Card Holder Number @@ -234,6 +246,8 @@ Save data Merge data Reload data + Merge from … + Save a copy to … Open Search Show password @@ -251,8 +265,6 @@ Install a web browser to open this URL. Open existing database Create new database - Don\'t search through backup entries - Omits \"Backup\" and \"Recycle bin\" groups from search results Quick search Request a search when opening a database Subdomain search @@ -298,7 +310,6 @@ Executing the command… Do not kill the app… Space - Search Filter Sort Lowest first ↓ @@ -312,7 +323,6 @@ Access Special Search - Search results Underline Unsupported database version. Upper-case @@ -507,7 +517,7 @@ Save search info Try to save search information when making a manual entry selection Ask to save data - Ask to save data when a form is validated + Ask for saving data when filling of a form is completed Application blocklist Blocklist that prevents auto filling of apps Web domain blocklist @@ -626,8 +636,10 @@ Forest Divine Classic + Simple Moon Sun + Reply Kunzite Theme brightness diff --git a/app/src/main/res/values/style_classic.xml b/app/src/main/res/values/style_classic.xml index d13fe72a8..f2973c4ae 100644 --- a/app/src/main/res/values/style_classic.xml +++ b/app/src/main/res/values/style_classic.xml @@ -19,7 +19,8 @@ --> - @@ -89,10 +94,15 @@ + @@ -155,11 +170,16 @@ + @@ -69,7 +74,7 @@ @@ -129,7 +139,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/style_simple.xml b/app/src/main/res/values/style_simple.xml new file mode 100644 index 000000000..5db9eaf2b --- /dev/null +++ b/app/src/main/res/values/style_simple.xml @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/style_sun.xml b/app/src/main/res/values/style_sun.xml index d48239d38..212d2ca60 100644 --- a/app/src/main/res/values/style_sun.xml +++ b/app/src/main/res/values/style_sun.xml @@ -24,10 +24,11 @@ @color/red @color/red_dark @color/orange - @color/orange_light + @color/orange_lighter @color/orange @color/red - @color/red_lighter + @color/red_lightest + @color/red_lightest @color/white_dark @color/white @style/KeepassDXStyle.Toolbar.Red @@ -38,15 +39,19 @@ @style/KeepassDXStyle.Red.Dialog @color/red_dark @color/white + + @color/white + @color/red_lighter + @color/red @@ -84,10 +89,11 @@ @color/red @color/red_dark @color/orange - @color/orange_light + @color/orange_lighter @color/orange @color/red - @color/red_lighter + @color/red_lightest + @color/red_lightest @color/grey_dark @style/KeepassDXStyle.Toolbar.Red.Night @style/KeepassDXStyle.Toolbar.Home.Red.Night @@ -97,15 +103,19 @@ @style/KeepassDXStyle.Red.Night.Dialog @color/red_dark @color/white + + @color/white + @color/red_lighter + @color/red diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index cf3910235..179dda3b8 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -30,7 +30,6 @@ - + + + + @@ -318,7 +342,7 @@ 8dp @@ -372,7 +396,7 @@ @@ -418,7 +442,12 @@ - + - - @@ -535,7 +566,7 @@