mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
33 Commits
3.0.0_beta
...
3.0.0_beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc3204453e | ||
|
|
5ef8d3b7b9 | ||
|
|
2a9de97a19 | ||
|
|
9cecfed417 | ||
|
|
319715918a | ||
|
|
a3bf6e8b6d | ||
|
|
c4062658ce | ||
|
|
01a5de413e | ||
|
|
e4c22b1f29 | ||
|
|
b10e60126f | ||
|
|
ef1f27f421 | ||
|
|
0ed208675c | ||
|
|
00f7a0a194 | ||
|
|
935d4f4a64 | ||
|
|
dc4d88260d | ||
|
|
18934601da | ||
|
|
4ea811aeda | ||
|
|
f8fdecdc8f | ||
|
|
5467c61137 | ||
|
|
9c72b4cc56 | ||
|
|
9102217bc3 | ||
|
|
0e8fd7b2c4 | ||
|
|
a06ea8fe55 | ||
|
|
31eb0fb48a | ||
|
|
d6a012e85f | ||
|
|
c6e2342ab4 | ||
|
|
b977792168 | ||
|
|
2595cf87d8 | ||
|
|
f4342f1448 | ||
|
|
c71ef24052 | ||
|
|
39b817bc69 | ||
|
|
e3adaba3b3 | ||
|
|
c62064002f |
@@ -4,7 +4,8 @@ KeePassDX(3.0.0)
|
||||
* Setting to display OTP Token in list #655
|
||||
* Fix timeout in dialogs #716
|
||||
* Check URI permissions #626
|
||||
* Improvements #1035 #1043 #942 #1021 #1027
|
||||
* Better autofill implementation #943 #946 #984 #1070 (Thx @uduerholz)
|
||||
* Improvements #680 #1035 #1043 #942 #1021 #1027
|
||||
|
||||
KeePassDX(2.10.5)
|
||||
* Increase the saving speed of database #1028
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 30
|
||||
versionCode = 84
|
||||
versionName = "3.0.0_beta01"
|
||||
versionCode = 86
|
||||
versionName = "3.0.0_beta03"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
|
||||
@@ -65,6 +65,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
|
||||
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
||||
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME)
|
||||
manualSelection = intent.getBooleanExtra(KEY_MANUAL_SELECTION, false)
|
||||
}
|
||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
@@ -198,15 +199,16 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_MANUAL_SELECTION = "KEY_MANUAL_SELECTION"
|
||||
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
|
||||
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
|
||||
private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME"
|
||||
|
||||
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
||||
|
||||
fun getAuthIntentSenderForSelection(context: Context,
|
||||
searchInfo: SearchInfo? = null,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): IntentSender {
|
||||
fun getPendingIntentForSelection(context: Context,
|
||||
searchInfo: SearchInfo? = null,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): PendingIntent {
|
||||
return PendingIntent.getActivity(context, 0,
|
||||
// Doesn't work with Parcelable (don't know why?)
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
@@ -214,6 +216,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
|
||||
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
|
||||
putExtra(KEY_SEARCH_SCHEME, it.webScheme)
|
||||
putExtra(KEY_MANUAL_SELECTION, it.manualSelection)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
@@ -221,17 +224,17 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
}
|
||||
}
|
||||
},
|
||||
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
}
|
||||
|
||||
fun getAuthIntentSenderForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo): IntentSender {
|
||||
fun getPendingIntentForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo): PendingIntent {
|
||||
return PendingIntent.getActivity(context, 0,
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
||||
putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||
},
|
||||
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
}
|
||||
|
||||
fun launchForRegistration(context: Context,
|
||||
|
||||
@@ -81,6 +81,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
private var mHistoryPosition: Int = -1
|
||||
private var mEntryIsHistory: Boolean = false
|
||||
private var mUrl: String? = null
|
||||
private var mEntryLoaded = false
|
||||
|
||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
|
||||
@@ -119,11 +120,12 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
// Get Entry from UUID
|
||||
try {
|
||||
intent.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { entryId ->
|
||||
mMainEntryId = entryId
|
||||
intent.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { mainEntryId ->
|
||||
intent.removeExtra(KEY_ENTRY)
|
||||
mHistoryPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
|
||||
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
|
||||
intent.removeExtra(KEY_ENTRY_HISTORY_POSITION)
|
||||
|
||||
mEntryViewModel.loadEntry(mDatabase, mainEntryId, historyPosition)
|
||||
}
|
||||
} catch (e: ClassCastException) {
|
||||
Log.e(TAG, "Unable to retrieve the entry key")
|
||||
@@ -138,53 +140,53 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
lockAndExit()
|
||||
}
|
||||
|
||||
mEntryViewModel.mainEntryId.observe(this) { mainEntryId ->
|
||||
this.mMainEntryId = mainEntryId
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory ->
|
||||
if (entryInfoHistory != null) {
|
||||
this.mMainEntryId = entryInfoHistory.mainEntryId
|
||||
|
||||
mEntryViewModel.historyPosition.observe(this) { historyPosition ->
|
||||
this.mHistoryPosition = historyPosition
|
||||
val entryIsHistory = historyPosition > -1
|
||||
this.mEntryIsHistory = entryIsHistory
|
||||
// Assign history dedicated view
|
||||
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
|
||||
if (entryIsHistory) {
|
||||
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
||||
taColorAccent.recycle()
|
||||
}
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
mEntryViewModel.entryInfo.observe(this) { entryInfo ->
|
||||
// Manage entry copy to start notification if allowed (at the first start)
|
||||
if (savedInstanceState == null) {
|
||||
// Manage entry to launch copying notification if allowed
|
||||
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
||||
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
||||
// Manage history position
|
||||
val historyPosition = entryInfoHistory.historyPosition
|
||||
this.mHistoryPosition = historyPosition
|
||||
val entryIsHistory = historyPosition > -1
|
||||
this.mEntryIsHistory = entryIsHistory
|
||||
// Assign history dedicated view
|
||||
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
|
||||
if (entryIsHistory) {
|
||||
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
collapsingToolbarLayout?.contentScrim =
|
||||
ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
||||
taColorAccent.recycle()
|
||||
}
|
||||
|
||||
val entryInfo = entryInfoHistory.entryInfo
|
||||
// Manage entry copy to start notification if allowed (at the first start)
|
||||
if (savedInstanceState == null) {
|
||||
// Manage entry to launch copying notification if allowed
|
||||
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
||||
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
||||
}
|
||||
}
|
||||
// Assign title icon
|
||||
mIcon = entryInfo.icon
|
||||
titleIconView?.let { iconView ->
|
||||
mIconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, mIconColor)
|
||||
}
|
||||
// Assign title text
|
||||
val entryTitle =
|
||||
if (entryInfo.title.isNotEmpty()) entryInfo.title else entryInfo.id.toString()
|
||||
collapsingToolbarLayout?.title = entryTitle
|
||||
toolbar?.title = entryTitle
|
||||
mUrl = entryInfo.url
|
||||
|
||||
loadingView?.hideByFading()
|
||||
mEntryLoaded = true
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
|
||||
// Assign title icon
|
||||
mIcon = entryInfo.icon
|
||||
titleIconView?.let { iconView ->
|
||||
mIconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, mIconColor)
|
||||
}
|
||||
|
||||
// Assign title text
|
||||
val entryTitle = if (entryInfo.title.isNotEmpty()) entryInfo.title else entryInfo.id.toString()
|
||||
collapsingToolbarLayout?.title = entryTitle
|
||||
toolbar?.title = entryTitle
|
||||
|
||||
mUrl = entryInfo.url
|
||||
|
||||
// Refresh Menu
|
||||
invalidateOptionsMenu()
|
||||
|
||||
loadingView?.hideByFading()
|
||||
}
|
||||
|
||||
mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement ->
|
||||
@@ -235,7 +237,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
mEntryViewModel.loadEntry(mDatabase, mMainEntryId, mHistoryPosition)
|
||||
mEntryViewModel.loadDatabase(database)
|
||||
|
||||
// Assign title icon
|
||||
mIcon?.let { icon ->
|
||||
@@ -294,7 +296,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
when (requestCode) {
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
||||
// Reload the current id from database
|
||||
mEntryViewModel.updateEntry(mDatabase)
|
||||
mEntryViewModel.loadDatabase(mDatabase)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,32 +312,41 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
if (mEntryLoaded) {
|
||||
val inflater = menuInflater
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
|
||||
val inflater = menuInflater
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
inflater.inflate(R.menu.entry, menu)
|
||||
inflater.inflate(R.menu.database, menu)
|
||||
|
||||
inflater.inflate(R.menu.entry, menu)
|
||||
inflater.inflate(R.menu.database, menu)
|
||||
if (mEntryIsHistory && !mDatabaseReadOnly) {
|
||||
inflater.inflate(R.menu.entry_history, menu)
|
||||
}
|
||||
|
||||
if (mUrl?.isEmpty() != false) {
|
||||
menu.findItem(R.id.menu_goto_url)?.isVisible = false
|
||||
// Show education views
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
performedNextEducation(
|
||||
EntryActivityEducation(
|
||||
this
|
||||
), menu
|
||||
)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if (mEntryIsHistory && !mDatabaseReadOnly) {
|
||||
inflater.inflate(R.menu.entry_history, menu)
|
||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||
if (mUrl?.isEmpty() != false) {
|
||||
menu?.findItem(R.id.menu_goto_url)?.isVisible = false
|
||||
}
|
||||
if (mEntryIsHistory || mDatabaseReadOnly) {
|
||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
menu.findItem(R.id.menu_edit)?.isVisible = false
|
||||
menu?.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
menu?.findItem(R.id.menu_edit)?.isVisible = false
|
||||
}
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
menu.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||
menu?.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||
}
|
||||
|
||||
// Show education views
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(EntryActivityEducation(this), menu) }
|
||||
|
||||
return true
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
||||
|
||||
@@ -48,12 +48,9 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
|
||||
import com.kunzisoft.keepass.app.database.IOActionTask
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.template.*
|
||||
@@ -95,15 +92,10 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
private var lockView: View? = null
|
||||
private var loadingView: ProgressBar? = null
|
||||
|
||||
private var mEntryId: NodeId<UUID>? = null
|
||||
private var mParentId: NodeId<*>? = null
|
||||
private var mRegisterInfo: RegisterInfo? = null
|
||||
private var mSearchInfo: SearchInfo? = null
|
||||
|
||||
private val mEntryEditViewModel: EntryEditViewModel by viewModels()
|
||||
private var mParent: Group? = null
|
||||
private var mEntry: Entry? = null
|
||||
private var mTemplate: Template? = null
|
||||
private var mIsTemplate: Boolean = false
|
||||
private var mEntryLoaded: Boolean = false
|
||||
|
||||
private var mAllowCustomFields = false
|
||||
private var mAllowOTP = false
|
||||
@@ -138,22 +130,27 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
||||
|
||||
mRegisterInfo = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)
|
||||
mSearchInfo = EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||
|
||||
// Entry is retrieve, it's an entry to update
|
||||
var entryId: NodeId<UUID>? = null
|
||||
intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let { entryToUpdate ->
|
||||
//intent.removeExtra(KEY_ENTRY)
|
||||
mEntryId = entryToUpdate
|
||||
intent.removeExtra(KEY_ENTRY)
|
||||
entryId = entryToUpdate
|
||||
}
|
||||
|
||||
// Parent is retrieve, it's a new entry to create
|
||||
var parentId: NodeId<*>? = null
|
||||
intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let { parent ->
|
||||
//intent.removeExtra(KEY_PARENT)
|
||||
mParentId = parent
|
||||
intent.removeExtra(KEY_PARENT)
|
||||
parentId = parent
|
||||
}
|
||||
|
||||
retrieveEntry(mDatabase)
|
||||
mEntryEditViewModel.loadTemplateEntry(
|
||||
mDatabase,
|
||||
entryId,
|
||||
parentId,
|
||||
EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent),
|
||||
EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||
)
|
||||
|
||||
// To retrieve attachment
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
@@ -166,38 +163,52 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
// Save button
|
||||
validateButton?.setOnClickListener { saveEntry() }
|
||||
|
||||
mEntryEditViewModel.templatesEntry.observe(this) { templatesEntry ->
|
||||
// Change template dynamically
|
||||
templatesEntry?.templates?.let { templates ->
|
||||
val defaultTemplate = templatesEntry.defaultTemplate
|
||||
templateSelectorSpinner?.apply {
|
||||
// Build template selector
|
||||
if (templates.isNotEmpty()) {
|
||||
adapter = TemplatesSelectorAdapter(
|
||||
this@EntryEditActivity,
|
||||
mIconDrawableFactory,
|
||||
templates
|
||||
)
|
||||
setSelection(templates.indexOf(defaultTemplate))
|
||||
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
parent: AdapterView<*>?,
|
||||
view: View?,
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
mEntryEditViewModel.changeTemplate(templates[position])
|
||||
}
|
||||
mEntryEditViewModel.onTemplateChanged.observe(this) { template ->
|
||||
this.mTemplate = template
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
mEntryEditViewModel.templatesEntry.observe(this) { templatesEntry ->
|
||||
if (templatesEntry != null) {
|
||||
// Change template dynamically
|
||||
this.mIsTemplate = templatesEntry.isTemplate
|
||||
templatesEntry.templates.let { templates ->
|
||||
templateSelectorSpinner?.apply {
|
||||
// Build template selector
|
||||
if (templates.isNotEmpty()) {
|
||||
adapter = TemplatesSelectorAdapter(
|
||||
this@EntryEditActivity,
|
||||
mIconDrawableFactory,
|
||||
templates
|
||||
)
|
||||
val selectedTemplate = if (mTemplate != null)
|
||||
mTemplate
|
||||
else
|
||||
templatesEntry.defaultTemplate
|
||||
setSelection(templates.indexOf(selectedTemplate))
|
||||
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
parent: AdapterView<*>?,
|
||||
view: View?,
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
mEntryEditViewModel.changeTemplate(templates[position])
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
}
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadingView?.hideByFading()
|
||||
loadingView?.hideByFading()
|
||||
mEntryLoaded = true
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
// View model listeners
|
||||
@@ -309,78 +320,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
super.onDatabaseRetrieved(database)
|
||||
mAllowCustomFields = database?.allowEntryCustomFields() == true
|
||||
mAllowOTP = database?.allowOTP == true
|
||||
retrieveEntry(database)
|
||||
}
|
||||
|
||||
private fun retrieveEntry(database: Database?) {
|
||||
database?.let {
|
||||
mEntryId?.let {
|
||||
IOActionTask(
|
||||
{
|
||||
// Create an Entry copy to modify from the database entry
|
||||
mEntry = database.getEntryById(it)
|
||||
// Retrieve the parent
|
||||
mEntry?.let { entry ->
|
||||
// If no parent, add root group as parent
|
||||
if (entry.parent == null) {
|
||||
entry.parent = database.rootGroup
|
||||
}
|
||||
}
|
||||
// Define if current entry is a template (in direct template group)
|
||||
mIsTemplate = database.entryIsTemplate(mEntry)
|
||||
},
|
||||
{
|
||||
mEntryEditViewModel.loadTemplateEntry(
|
||||
database,
|
||||
mEntry,
|
||||
mIsTemplate,
|
||||
mRegisterInfo,
|
||||
mSearchInfo
|
||||
)
|
||||
}
|
||||
).execute()
|
||||
mEntryId = null
|
||||
}
|
||||
|
||||
mParentId?.let {
|
||||
IOActionTask(
|
||||
{
|
||||
mParent = database.getGroupById(it)
|
||||
mParent?.let { parentGroup ->
|
||||
mEntry = database.createEntry()?.apply {
|
||||
// Add the default icon from parent if not a folder
|
||||
val parentIcon = parentGroup.icon
|
||||
// Set default icon
|
||||
if (parentIcon.custom.isUnknown
|
||||
&& parentIcon.standard.id != IconImageStandard.FOLDER_ID
|
||||
) {
|
||||
icon = IconImage(parentIcon.standard)
|
||||
}
|
||||
if (!parentIcon.custom.isUnknown) {
|
||||
icon = IconImage(parentIcon.custom)
|
||||
}
|
||||
// Set default username
|
||||
username = database.defaultUsername
|
||||
// Warning only the entry recognize is parent, parent don't yet recognize the new entry
|
||||
// Useful to recognize child state (ie: entry is a template)
|
||||
parent = parentGroup
|
||||
}
|
||||
}
|
||||
mIsTemplate = database.entryIsTemplate(mEntry)
|
||||
},
|
||||
{
|
||||
mEntryEditViewModel.loadTemplateEntry(
|
||||
database,
|
||||
mEntry,
|
||||
mIsTemplate,
|
||||
mRegisterInfo,
|
||||
mSearchInfo
|
||||
)
|
||||
}
|
||||
).execute()
|
||||
mParentId = null
|
||||
}
|
||||
}
|
||||
mEntryEditViewModel.loadDatabase(database)
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
@@ -571,29 +511,33 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
*/
|
||||
private fun saveEntry() {
|
||||
mAttachmentFileBinderManager?.stopUploadAllAttachments()
|
||||
mEntryEditViewModel.requestEntryInfoUpdate(mDatabase, mEntry, mParent)
|
||||
mEntryEditViewModel.requestEntryInfoUpdate(mDatabase)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
menuInflater.inflate(R.menu.entry_edit, menu)
|
||||
if (mEntryLoaded) {
|
||||
menuInflater.inflate(R.menu.entry_edit, menu)
|
||||
entryEditActivityEducation?.let {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
performedNextEducation(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||
|
||||
menu?.findItem(R.id.menu_add_field)?.apply {
|
||||
isEnabled = mAllowCustomFields
|
||||
isVisible = isEnabled
|
||||
}
|
||||
|
||||
menu?.findItem(R.id.menu_add_attachment)?.apply {
|
||||
// Attachment not compatible below KitKat
|
||||
isEnabled = !mIsTemplate
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
||||
isVisible = isEnabled
|
||||
}
|
||||
|
||||
menu?.findItem(R.id.menu_add_otp)?.apply {
|
||||
// OTP not compatible below KitKat
|
||||
isEnabled = mAllowOTP
|
||||
@@ -601,14 +545,10 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
||||
isVisible = isEnabled
|
||||
}
|
||||
|
||||
entryEditActivityEducation?.let {
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
|
||||
}
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
||||
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
||||
|
||||
val entryEditFragment = supportFragmentManager.findFragmentById(R.id.entry_edit_content)
|
||||
as? EntryEditFragment?
|
||||
|
||||
@@ -125,13 +125,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
}
|
||||
}
|
||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
||||
// Remove from app database
|
||||
fileDatabaseHistoryToDelete.databaseUri?.let { databaseUri ->
|
||||
UriUtil.releaseUriPermission(
|
||||
contentResolver,
|
||||
databaseUri
|
||||
)
|
||||
}
|
||||
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -487,7 +487,9 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
if (groupMetaView != null) {
|
||||
val meta = group.nodeId.toString()
|
||||
groupMetaView?.text = meta
|
||||
if (meta.isNotEmpty() && PreferencesUtil.showUUID(this)) {
|
||||
if (meta.isNotEmpty()
|
||||
&& !group.isVirtual
|
||||
&& PreferencesUtil.showUUID(this)) {
|
||||
groupMetaView?.visibility = View.VISIBLE
|
||||
} else {
|
||||
groupMetaView?.visibility = View.GONE
|
||||
|
||||
@@ -35,6 +35,7 @@ import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.template.Template
|
||||
import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
@@ -55,6 +56,7 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
private lateinit var attachmentsListView: RecyclerView
|
||||
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
||||
|
||||
private var mTemplate: Template? = null
|
||||
private var mAllowMultipleAttachments: Boolean = false
|
||||
|
||||
private var mIconColor: Int = 0
|
||||
@@ -115,19 +117,26 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
}
|
||||
|
||||
mEntryEditViewModel.onTemplateChanged.observe(viewLifecycleOwner) { template ->
|
||||
this.mTemplate = template
|
||||
templateView.setTemplate(template)
|
||||
}
|
||||
|
||||
mEntryEditViewModel.templatesEntry.observe(viewLifecycleOwner) { templateEntry ->
|
||||
templateView.setTemplate(templateEntry.defaultTemplate)
|
||||
// Load entry info only the first time to keep change locally
|
||||
if (savedInstanceState == null) {
|
||||
assignEntryInfo(templateEntry.entryInfo)
|
||||
if (templateEntry != null) {
|
||||
val selectedTemplate = if (mTemplate != null)
|
||||
mTemplate
|
||||
else
|
||||
templateEntry.defaultTemplate
|
||||
templateView.setTemplate(selectedTemplate)
|
||||
// Load entry info only the first time to keep change locally
|
||||
if (savedInstanceState == null) {
|
||||
assignEntryInfo(templateEntry.entryInfo)
|
||||
}
|
||||
// To prevent flickering
|
||||
rootView.showByFading()
|
||||
// Apply timeout reset
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
|
||||
}
|
||||
// To prevent flickering
|
||||
rootView.showByFading()
|
||||
// Apply timeout reset
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
|
||||
}
|
||||
|
||||
mEntryEditViewModel.requestEntryInfoUpdate.observe(viewLifecycleOwner) {
|
||||
|
||||
@@ -91,15 +91,14 @@ class EntryFragment: DatabaseFragment() {
|
||||
uuidView = view.findViewById(R.id.entry_UUID)
|
||||
uuidReferenceView = view.findViewById(R.id.entry_UUID_reference)
|
||||
|
||||
mEntryViewModel.template.observe(viewLifecycleOwner) { template ->
|
||||
templateView.setTemplate(template)
|
||||
}
|
||||
|
||||
mEntryViewModel.entryInfo.observe(viewLifecycleOwner) { entryInfo ->
|
||||
assignEntryInfo(entryInfo)
|
||||
// Smooth appearing
|
||||
rootView.showByFading()
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
|
||||
mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory ->
|
||||
if (entryInfoHistory != null) {
|
||||
templateView.setTemplate(entryInfoHistory.template)
|
||||
assignEntryInfo(entryInfoHistory.entryInfo)
|
||||
// Smooth appearing
|
||||
rootView.showByFading()
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
|
||||
}
|
||||
}
|
||||
|
||||
mEntryViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState ->
|
||||
|
||||
@@ -55,9 +55,11 @@ class EntryHistoryFragment: StylishFragment() {
|
||||
* History
|
||||
* -------------
|
||||
*/
|
||||
private fun assignHistory(history: List<EntryInfo>) {
|
||||
private fun assignHistory(history: List<EntryInfo>?) {
|
||||
historyAdapter?.clear()
|
||||
historyAdapter?.entryHistoryList?.addAll(history)
|
||||
history?.let {
|
||||
historyAdapter?.entryHistoryList?.addAll(history)
|
||||
}
|
||||
historyAdapter?.onItemClickListener = { item, position ->
|
||||
mEntryViewModel.onHistorySelected(item, position)
|
||||
}
|
||||
|
||||
@@ -233,7 +233,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
return mLayoutManager?.findFirstVisibleItemPosition() ?: 0
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
private fun rebuildList() {
|
||||
try {
|
||||
// Add elements to the list
|
||||
@@ -418,7 +417,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
mNodesRecyclerView?.scrollToPosition(position)
|
||||
}
|
||||
}
|
||||
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
|
||||
} ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,9 @@ 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.database.element.Entry
|
||||
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.model.MainCredential
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
import java.util.*
|
||||
|
||||
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
||||
|
||||
@@ -28,7 +23,11 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
||||
mDatabaseTaskProvider = DatabaseTaskProvider(this)
|
||||
|
||||
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||
if (mDatabase == null || mDatabase != database) {
|
||||
val databaseWasReloaded = database?.wasReloaded == true
|
||||
if (databaseWasReloaded && finishActivityIfReloadRequested()) {
|
||||
finish()
|
||||
} else if (mDatabase == null || mDatabase != database || databaseWasReloaded) {
|
||||
database?.wasReloaded = false
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
}
|
||||
@@ -69,17 +68,8 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
||||
mDatabase?.clearAndClose(this)
|
||||
}
|
||||
|
||||
override fun reloadActivity() {
|
||||
super.reloadActivity()
|
||||
mDatabase?.wasReloaded = false
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (mDatabase?.wasReloaded == true) {
|
||||
reloadActivity()
|
||||
}
|
||||
mDatabaseTaskProvider?.registerProgressTask()
|
||||
}
|
||||
|
||||
|
||||
@@ -189,26 +189,36 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) {
|
||||
fun deleteKeyFileByDatabaseUri(databaseUri: Uri,
|
||||
result: (() ->Unit)? = null) {
|
||||
IOActionTask(
|
||||
{
|
||||
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
|
||||
},
|
||||
{
|
||||
result?.invoke()
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun deleteAllKeyFiles() {
|
||||
fun deleteAllKeyFiles(result: (() ->Unit)? = null) {
|
||||
IOActionTask(
|
||||
{
|
||||
databaseFileHistoryDao.deleteAllKeyFiles()
|
||||
},
|
||||
{
|
||||
result?.invoke()
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun deleteAll() {
|
||||
fun deleteAll(result: (() ->Unit)? = null) {
|
||||
IOActionTask(
|
||||
{
|
||||
databaseFileHistoryDao.deleteAll()
|
||||
},
|
||||
{
|
||||
result?.invoke()
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.app.PendingIntent
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentSender
|
||||
import android.graphics.BlendMode
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
@@ -37,11 +38,13 @@ import android.view.autofill.AutofillValue
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
import android.widget.RemoteViews
|
||||
import android.widget.Toast
|
||||
import android.widget.inline.InlinePresentationSpec
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.autofill.inline.UiVersions
|
||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
@@ -51,7 +54,6 @@ import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
|
||||
@@ -126,12 +128,14 @@ object AutofillHelper {
|
||||
if (entryInfo.expires) {
|
||||
val year = entryInfo.expiryTime.getYearInt()
|
||||
val month = entryInfo.expiryTime.getMonthInt()
|
||||
val monthString = month.toString().padStart(2, '0')
|
||||
val day = entryInfo.expiryTime.getDay()
|
||||
val dayString = day.toString().padStart(2, '0')
|
||||
|
||||
struct.creditCardExpirationDateId?.let {
|
||||
if (struct.isWebView) {
|
||||
// set date string as defined in https://html.spec.whatwg.org
|
||||
builder.setValue(it, AutofillValue.forText("$year\u002D$month"))
|
||||
builder.setValue(it, AutofillValue.forText("$year\u002D$monthString"))
|
||||
} else {
|
||||
builder.setValue(it, AutofillValue.forDate(entryInfo.expiryTime.date.time))
|
||||
}
|
||||
@@ -157,24 +161,24 @@ object AutofillHelper {
|
||||
}
|
||||
struct.creditCardExpirationMonthId?.let {
|
||||
if (struct.isWebView) {
|
||||
builder.setValue(it, AutofillValue.forText(month.toString()))
|
||||
builder.setValue(it, AutofillValue.forText(monthString))
|
||||
} else {
|
||||
if (struct.creditCardExpirationMonthOptions != null) {
|
||||
// index starts at 0
|
||||
builder.setValue(it, AutofillValue.forList(month - 1))
|
||||
} else {
|
||||
builder.setValue(it, AutofillValue.forText(month.toString()))
|
||||
builder.setValue(it, AutofillValue.forText(monthString))
|
||||
}
|
||||
}
|
||||
}
|
||||
struct.creditCardExpirationDayId?.let {
|
||||
if (struct.isWebView) {
|
||||
builder.setValue(it, AutofillValue.forText(day.toString()))
|
||||
builder.setValue(it, AutofillValue.forText(dayString))
|
||||
} else {
|
||||
if (struct.creditCardExpirationDayOptions != null) {
|
||||
builder.setValue(it, AutofillValue.forList(day - 1))
|
||||
} else {
|
||||
builder.setValue(it, AutofillValue.forText(day.toString()))
|
||||
builder.setValue(it, AutofillValue.forText(dayString))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,6 +274,27 @@ object AutofillHelper {
|
||||
return null
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun buildInlinePresentationForManualSelection(context: Context,
|
||||
inlinePresentationSpec: InlinePresentationSpec,
|
||||
pendingIntent: PendingIntent): InlinePresentation? {
|
||||
// Make sure that the IME spec claims support for v1 UI template.
|
||||
val imeStyle = inlinePresentationSpec.style
|
||||
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
|
||||
return null
|
||||
|
||||
// Build the content for IME UI
|
||||
return InlinePresentation(
|
||||
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
|
||||
setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
|
||||
setTitle(context.getString(R.string.autofill_select_entry))
|
||||
setStartIcon(Icon.createWithResource(context, R.drawable.ic_arrow_right_green_24dp).apply {
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
})
|
||||
}.build().slice, inlinePresentationSpec, false)
|
||||
}
|
||||
|
||||
fun buildResponse(context: Context,
|
||||
database: Database,
|
||||
entriesInfo: List<EntryInfo>,
|
||||
@@ -293,17 +318,58 @@ object AutofillHelper {
|
||||
}
|
||||
|
||||
// Add inline suggestion for new IME and dataset
|
||||
entriesInfo.forEachIndexed { index, entryInfo ->
|
||||
val inlinePresentation = inlineSuggestionsRequest?.let {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
buildInlinePresentationForEntry(context, database, inlineSuggestionsRequest, index, entryInfo)
|
||||
} else {
|
||||
null
|
||||
var numberInlineSuggestions = 0
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
|
||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
|
||||
--numberInlineSuggestions
|
||||
}
|
||||
}
|
||||
}
|
||||
val dataSet = buildDataset(context, database, entryInfo, parseResult, inlinePresentation)
|
||||
dataSet?.let {
|
||||
responseBuilder.addDataset(it)
|
||||
|
||||
}
|
||||
|
||||
entriesInfo.forEachIndexed { _, entry ->
|
||||
val inlinePresentation = if (numberInlineSuggestions > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
buildInlinePresentationForEntry(context, database, inlineSuggestionsRequest, numberInlineSuggestions--, entry)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult, inlinePresentation))
|
||||
}
|
||||
|
||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||
val searchInfo = SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
webDomain = parseResult.webDomain
|
||||
webScheme = parseResult.webScheme
|
||||
manualSelection = true
|
||||
}
|
||||
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
|
||||
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
|
||||
searchInfo, inlineSuggestionsRequest)
|
||||
|
||||
parseResult.allAutofillIds().let { autofillIds ->
|
||||
autofillIds.forEach { id ->
|
||||
val builder = Dataset.Builder(manualSelectionView)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
||||
val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
|
||||
inlinePresentation?.let {
|
||||
builder.setInlinePresentation(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.setValue(id, null)
|
||||
builder.setAuthentication(pendingIntent.intentSender)
|
||||
responseBuilder.addDataset(builder.build())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -138,14 +138,14 @@ class KeeAutofillService : AutofillService() {
|
||||
items, parseResult, inlineSuggestionsRequest)
|
||||
)
|
||||
},
|
||||
{
|
||||
{ openedDatabase ->
|
||||
// Show UI if no search result
|
||||
showUIForEntrySelection(parseResult,
|
||||
showUIForEntrySelection(parseResult, openedDatabase,
|
||||
searchInfo, inlineSuggestionsRequest, callback)
|
||||
},
|
||||
{
|
||||
// Show UI if database not open
|
||||
showUIForEntrySelection(parseResult,
|
||||
showUIForEntrySelection(parseResult, null,
|
||||
searchInfo, inlineSuggestionsRequest, callback)
|
||||
}
|
||||
)
|
||||
@@ -153,6 +153,7 @@ class KeeAutofillService : AutofillService() {
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||
database: Database?,
|
||||
searchInfo: SearchInfo,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
||||
callback: FillCallback) {
|
||||
@@ -160,19 +161,51 @@ class KeeAutofillService : AutofillService() {
|
||||
if (autofillIds.isNotEmpty()) {
|
||||
// If the entire Autofill Response is authenticated, AuthActivity is used
|
||||
// to generate Response.
|
||||
val intentSender = AutofillLauncherActivity.getAuthIntentSenderForSelection(this,
|
||||
searchInfo, inlineSuggestionsRequest)
|
||||
val intentSender = AutofillLauncherActivity.getPendingIntentForSelection(this,
|
||||
searchInfo, inlineSuggestionsRequest).intentSender
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
val remoteViewsUnlock: RemoteViews = if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply {
|
||||
setTextViewText(R.id.autofill_web_domain_text, parseResult.webDomain)
|
||||
}
|
||||
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
|
||||
setTextViewText(R.id.autofill_app_id_text, parseResult.applicationId)
|
||||
val remoteViewsUnlock: RemoteViews = if (database == null) {
|
||||
if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||
RemoteViews(
|
||||
packageName,
|
||||
R.layout.item_autofill_unlock_web_domain
|
||||
).apply {
|
||||
setTextViewText(
|
||||
R.id.autofill_web_domain_text,
|
||||
parseResult.webDomain
|
||||
)
|
||||
}
|
||||
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
|
||||
setTextViewText(
|
||||
R.id.autofill_app_id_text,
|
||||
parseResult.applicationId
|
||||
)
|
||||
}
|
||||
} else {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock)
|
||||
}
|
||||
} else {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock)
|
||||
if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||
RemoteViews(
|
||||
packageName,
|
||||
R.layout.item_autofill_select_entry_web_domain
|
||||
).apply {
|
||||
setTextViewText(
|
||||
R.id.autofill_web_domain_text,
|
||||
parseResult.webDomain
|
||||
)
|
||||
}
|
||||
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
||||
RemoteViews(packageName, R.layout.item_autofill_select_entry_app_id).apply {
|
||||
setTextViewText(
|
||||
R.id.autofill_app_id_text,
|
||||
parseResult.applicationId
|
||||
)
|
||||
}
|
||||
} else {
|
||||
RemoteViews(packageName, R.layout.item_autofill_select_entry)
|
||||
}
|
||||
}
|
||||
|
||||
// Tell the autofill framework the interest to save credentials
|
||||
|
||||
@@ -143,19 +143,19 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
Log.d(TAG, "Autofill password hint")
|
||||
return true
|
||||
}
|
||||
it.contains("cc-name", true) -> {
|
||||
it.equals("cc-name", true) -> {
|
||||
Log.d(TAG, "Autofill credit card name hint")
|
||||
result?.creditCardHolderId = autofillId
|
||||
result?.creditCardHolder = node.autofillValue?.textValue?.toString()
|
||||
}
|
||||
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_NUMBER, true)
|
||||
|| it.contains("cc-number", true) -> {
|
||||
|| it.equals("cc-number", true) -> {
|
||||
Log.d(TAG, "Autofill credit card number hint")
|
||||
result?.creditCardNumberId = autofillId
|
||||
result?.creditCardNumber = node.autofillValue?.textValue?.toString()
|
||||
}
|
||||
// expect date string as defined in https://html.spec.whatwg.org, e.g. 2014-12
|
||||
it.contains("cc-exp", true) -> {
|
||||
it.equals("cc-exp", true) -> {
|
||||
Log.d(TAG, "Autofill credit card expiration date hint")
|
||||
result?.creditCardExpirationDateId = autofillId
|
||||
node.autofillValue?.let { value ->
|
||||
@@ -182,7 +182,7 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
}
|
||||
}
|
||||
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR, true)
|
||||
|| it.contains("cc-exp-year", true) -> {
|
||||
|| it.equals("cc-exp-year", true) -> {
|
||||
Log.d(TAG, "Autofill credit card expiration year hint")
|
||||
result?.creditCardExpirationYearId = autofillId
|
||||
if (node.autofillOptions != null) {
|
||||
@@ -204,7 +204,7 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
}
|
||||
}
|
||||
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH, true)
|
||||
|| it.contains("cc-exp-month", true) -> {
|
||||
|| it.equals("cc-exp-month", true) -> {
|
||||
Log.d(TAG, "Autofill credit card expiration month hint")
|
||||
result?.creditCardExpirationMonthId = autofillId
|
||||
if (node.autofillOptions != null) {
|
||||
@@ -227,7 +227,7 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
}
|
||||
}
|
||||
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY, true)
|
||||
|| it.contains("cc-exp-day", true) -> {
|
||||
|| it.equals("cc-exp-day", true) -> {
|
||||
Log.d(TAG, "Autofill credit card expiration day hint")
|
||||
result?.creditCardExpirationDayId = autofillId
|
||||
if (node.autofillOptions != null) {
|
||||
@@ -399,7 +399,6 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
class Result {
|
||||
var isWebView: Boolean = false
|
||||
var applicationId: String? = null
|
||||
|
||||
var webDomain: String? = null
|
||||
set(value) {
|
||||
if (field == null)
|
||||
|
||||
@@ -106,7 +106,7 @@ class SearchHelper {
|
||||
} else if (TimeoutHelper.checkTime(context)) {
|
||||
var searchWithoutUI = false
|
||||
if (PreferencesUtil.isAutofillAutoSearchEnable(context)
|
||||
&& searchInfo != null
|
||||
&& searchInfo != null && !searchInfo.manualSelection
|
||||
&& !searchInfo.containsOnlyNullValues()) {
|
||||
// If search provide results
|
||||
database.createVirtualGroupFromSearchInfo(
|
||||
|
||||
@@ -186,6 +186,39 @@ class EntryInfo : NodeInfo {
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is EntryInfo) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
if (username != other.username) return false
|
||||
if (password != other.password) return false
|
||||
if (url != other.url) return false
|
||||
if (notes != other.notes) return false
|
||||
if (customFields != other.customFields) return false
|
||||
if (attachments != other.attachments) return false
|
||||
if (otpModel != other.otpModel) return false
|
||||
if (isTemplate != other.isTemplate) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + id.hashCode()
|
||||
result = 31 * result + username.hashCode()
|
||||
result = 31 * result + password.hashCode()
|
||||
result = 31 * result + url.hashCode()
|
||||
result = 31 * result + notes.hashCode()
|
||||
result = 31 * result + customFields.hashCode()
|
||||
result = 31 * result + attachments.hashCode()
|
||||
result = 31 * result + (otpModel?.hashCode() ?: 0)
|
||||
result = 31 * result + isTemplate.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
const val WEB_DOMAIN_FIELD_NAME = "URL"
|
||||
|
||||
@@ -24,6 +24,22 @@ class GroupInfo : NodeInfo {
|
||||
parcel.writeString(notes)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is GroupInfo) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
if (notes != other.notes) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + (notes?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<GroupInfo> {
|
||||
override fun createFromParcel(parcel: Parcel): GroupInfo {
|
||||
return GroupInfo(parcel)
|
||||
|
||||
@@ -36,6 +36,30 @@ open class NodeInfo() : Parcelable {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is NodeInfo) return false
|
||||
|
||||
if (title != other.title) return false
|
||||
if (icon != other.icon) return false
|
||||
if (creationTime != other.creationTime) return false
|
||||
if (lastModificationTime != other.lastModificationTime) return false
|
||||
if (expires != other.expires) return false
|
||||
if (expiryTime != other.expiryTime) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = title.hashCode()
|
||||
result = 31 * result + icon.hashCode()
|
||||
result = 31 * result + creationTime.hashCode()
|
||||
result = 31 * result + lastModificationTime.hashCode()
|
||||
result = 31 * result + expires.hashCode()
|
||||
result = 31 * result + expiryTime.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<NodeInfo> {
|
||||
override fun createFromParcel(parcel: Parcel): NodeInfo {
|
||||
return NodeInfo(parcel)
|
||||
|
||||
@@ -14,7 +14,7 @@ import kotlinx.coroutines.launch
|
||||
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
||||
|
||||
class SearchInfo : ObjectNameResource, Parcelable {
|
||||
|
||||
var manualSelection: Boolean = false
|
||||
var applicationId: String? = null
|
||||
set(value) {
|
||||
field = when {
|
||||
@@ -42,6 +42,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
constructor()
|
||||
|
||||
constructor(toCopy: SearchInfo?) {
|
||||
manualSelection = toCopy?.manualSelection ?: manualSelection
|
||||
applicationId = toCopy?.applicationId
|
||||
webDomain = toCopy?.webDomain
|
||||
webScheme = toCopy?.webScheme
|
||||
@@ -49,6 +50,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
}
|
||||
|
||||
private constructor(parcel: Parcel) {
|
||||
manualSelection = parcel.readByte().toInt() != 0
|
||||
val readAppId = parcel.readString()
|
||||
applicationId = if (readAppId.isNullOrEmpty()) null else readAppId
|
||||
val readDomain = parcel.readString()
|
||||
@@ -64,6 +66,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeByte((if (manualSelection) 1 else 0).toByte())
|
||||
parcel.writeString(applicationId ?: "")
|
||||
parcel.writeString(webDomain ?: "")
|
||||
parcel.writeString(webScheme ?: "")
|
||||
@@ -88,10 +91,9 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as SearchInfo
|
||||
if (other !is SearchInfo) return false
|
||||
|
||||
if (manualSelection != other.manualSelection) return false
|
||||
if (applicationId != other.applicationId) return false
|
||||
if (webDomain != other.webDomain) return false
|
||||
if (webScheme != other.webScheme) return false
|
||||
@@ -101,7 +103,8 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = applicationId?.hashCode() ?: 0
|
||||
var result = manualSelection.hashCode()
|
||||
result = 31 * result + (applicationId?.hashCode() ?: 0)
|
||||
result = 31 * result + (webDomain?.hashCode() ?: 0)
|
||||
result = 31 * result + (webScheme?.hashCode() ?: 0)
|
||||
result = 31 * result + (otpString?.hashCode() ?: 0)
|
||||
|
||||
@@ -81,14 +81,18 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
activity?.let { activity ->
|
||||
findPreference<Preference>(getString(R.string.remember_database_locations_key))?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (!(newValue as Boolean)) {
|
||||
FileDatabaseHistoryAction.getInstance(activity.applicationContext).deleteAll()
|
||||
FileDatabaseHistoryAction.getInstance(activity.applicationContext).deleteAll {
|
||||
UriUtil.releaseAllUnnecessaryPermissionUris(activity.applicationContext)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
findPreference<Preference>(getString(R.string.remember_keyfile_locations_key))?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (!(newValue as Boolean)) {
|
||||
FileDatabaseHistoryAction.getInstance(activity.applicationContext).deleteAllKeyFiles()
|
||||
FileDatabaseHistoryAction.getInstance(activity.applicationContext).deleteAllKeyFiles {
|
||||
UriUtil.releaseAllUnnecessaryPermissionUris(activity.applicationContext)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -493,6 +493,12 @@ object PreferencesUtil {
|
||||
context.resources.getBoolean(R.bool.autofill_inline_suggestions_default))
|
||||
}
|
||||
|
||||
fun isAutofillManualSelectionEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.autofill_manual_selection_key),
|
||||
context.resources.getBoolean(R.bool.autofill_manual_selection_default))
|
||||
}
|
||||
|
||||
fun isAutofillSaveSearchInfoEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.autofill_save_search_info_key),
|
||||
@@ -630,6 +636,7 @@ object PreferencesUtil {
|
||||
context.getString(R.string.autofill_close_database_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.autofill_auto_search_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.autofill_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.autofill_manual_selection_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.autofill_save_search_info_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.autofill_ask_to_save_data_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.autofill_application_id_blocklist_key) -> editor.putStringSet(name, getStringSetFromProperties(value))
|
||||
|
||||
@@ -125,6 +125,7 @@ class DurationDialogFragmentCompat : InputPreferenceDialogFragmentCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
mEnabled = isSwitchActivated()
|
||||
setSwitchAction({ isChecked ->
|
||||
mEnabled = isChecked
|
||||
}, mDays + mHours + mMinutes + mSeconds > 0)
|
||||
|
||||
@@ -155,6 +155,10 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
|
||||
}
|
||||
}
|
||||
|
||||
fun isSwitchActivated(): Boolean {
|
||||
return switchElementView?.isChecked == true
|
||||
}
|
||||
|
||||
fun activateSwitch() {
|
||||
if (switchElementView?.isChecked != true)
|
||||
switchElementView?.isChecked = true
|
||||
|
||||
@@ -30,7 +30,6 @@ import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||
@@ -142,19 +141,5 @@ fun Context.closeDatabase(database: Database?) {
|
||||
database?.clearAndClose(this)
|
||||
|
||||
// Release not useful URI permission
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
applicationContext?.let { appContext ->
|
||||
val fileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(appContext)
|
||||
fileDatabaseHistoryAction.getDatabaseFileList { databaseFileList ->
|
||||
val listToNotRemove = databaseFileList.map { it.databaseUri }
|
||||
// Remove URI permission for not database files
|
||||
val resolver = appContext.contentResolver
|
||||
resolver.persistedUriPermissions.forEach { uriPermission ->
|
||||
val uri = uriPermission.uri
|
||||
if (!listToNotRemove.contains(uri))
|
||||
UriUtil.releaseUriPermission(resolver, uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
UriUtil.releaseAllUnnecessaryPermissionUris(applicationContext)
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
|
||||
@@ -187,6 +188,32 @@ object UriUtil {
|
||||
persistUriPermission(contentResolver, uri, release = true, readOnly = false)
|
||||
}
|
||||
|
||||
fun releaseAllUnnecessaryPermissionUris(applicationContext: Context?) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
applicationContext?.let { appContext ->
|
||||
val fileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(appContext)
|
||||
fileDatabaseHistoryAction.getDatabaseFileList { databaseFileList ->
|
||||
val listToNotRemove = mutableListOf<Uri>()
|
||||
databaseFileList.forEach {
|
||||
it.databaseUri?.let { databaseUri ->
|
||||
listToNotRemove.add(databaseUri)
|
||||
}
|
||||
it.keyFileUri?.let { keyFileUri ->
|
||||
listToNotRemove.add(keyFileUri)
|
||||
}
|
||||
}
|
||||
// Remove URI permission for not database files
|
||||
val resolver = appContext.contentResolver
|
||||
resolver.persistedUriPermissions.forEach { uriPermission ->
|
||||
val uri = uriPermission.uri
|
||||
if (!listToNotRemove.contains(uri))
|
||||
releaseUriPermission(resolver, uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getUriFromIntent(intent: Intent, key: String): Uri? {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
|
||||
@@ -62,7 +62,8 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
|
||||
field: Field): TextEditFieldView? {
|
||||
return context?.let {
|
||||
TextEditFieldView(it).apply {
|
||||
setProtection(field.protectedValue.isProtected, mHideProtectedValue)
|
||||
// hiddenProtectedValue (mHideProtectedValue) don't work with TextInputLayout
|
||||
setProtection(field.protectedValue.isProtected)
|
||||
setMaxChars(templateAttribute.options.getNumberChars())
|
||||
setMaxLines(templateAttribute.options.getNumberLines())
|
||||
setActionClick(templateAttribute, field, this)
|
||||
|
||||
@@ -161,8 +161,7 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
|
||||
}
|
||||
}
|
||||
|
||||
fun setProtection(protection: Boolean, hiddenProtectedValue: Boolean) {
|
||||
// hiddenProtectedValue don't work with TextInputLayout
|
||||
fun setProtection(protection: Boolean) {
|
||||
if (protection) {
|
||||
labelView.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
|
||||
valueView.inputType = valueView.inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
|
||||
@@ -110,6 +110,21 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
|
||||
fun deleteDatabaseFile(databaseFileToDelete: DatabaseFile) {
|
||||
mFileDatabaseHistoryAction?.deleteDatabaseFile(databaseFileToDelete) { databaseFileDeleted ->
|
||||
databaseFileDeleted?.let { _ ->
|
||||
// Release database and keyfile URIs permissions
|
||||
val contentResolver = getApplication<App>().applicationContext.contentResolver
|
||||
databaseFileDeleted.databaseUri?.let { databaseUri ->
|
||||
UriUtil.releaseUriPermission(
|
||||
contentResolver,
|
||||
databaseUri
|
||||
)
|
||||
}
|
||||
databaseFileDeleted.keyFileUri?.let { keyFileUri ->
|
||||
UriUtil.releaseUriPermission(
|
||||
contentResolver,
|
||||
keyFileUri
|
||||
)
|
||||
}
|
||||
// Call the feedback
|
||||
databaseFilesLoaded.value = getDatabaseFilesLoadedValue().apply {
|
||||
databaseFileAction = DatabaseFileAction.DELETE
|
||||
databaseFileToActivate = databaseFileDeleted
|
||||
|
||||
@@ -5,17 +5,28 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.kunzisoft.keepass.app.database.IOActionTask
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.template.Template
|
||||
import com.kunzisoft.keepass.model.*
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import java.util.*
|
||||
|
||||
|
||||
class EntryEditViewModel: NodeEditViewModel() {
|
||||
|
||||
private var mEntryId: NodeId<UUID>? = null
|
||||
private var mParentId: NodeId<*>? = null
|
||||
private var mRegisterInfo: RegisterInfo? = null
|
||||
private var mSearchInfo: SearchInfo? = null
|
||||
private var mParent: Group? = null
|
||||
private var mEntry: Entry? = null
|
||||
private var mIsTemplate: Boolean = false
|
||||
private val mTempAttachments = mutableListOf<EntryAttachmentState>()
|
||||
|
||||
val templatesEntry : LiveData<TemplatesEntry> get() = _templatesEntry
|
||||
private val _templatesEntry = MutableLiveData<TemplatesEntry>()
|
||||
val templatesEntry : LiveData<TemplatesEntry?> get() = _templatesEntry
|
||||
private val _templatesEntry = MutableLiveData<TemplatesEntry?>()
|
||||
|
||||
val requestEntryInfoUpdate : LiveData<EntryUpdate> get() = _requestEntryInfoUpdate
|
||||
private val _requestEntryInfoUpdate = SingleLiveEvent<EntryUpdate>()
|
||||
@@ -23,7 +34,7 @@ class EntryEditViewModel: NodeEditViewModel() {
|
||||
private val _onEntrySaved = SingleLiveEvent<EntrySave>()
|
||||
|
||||
val onTemplateChanged : LiveData<Template> get() = _onTemplateChanged
|
||||
private val _onTemplateChanged = SingleLiveEvent<Template>()
|
||||
private val _onTemplateChanged = MutableLiveData<Template>()
|
||||
|
||||
val requestPasswordSelection : LiveData<Field> get() = _requestPasswordSelection
|
||||
private val _requestPasswordSelection = SingleLiveEvent<Field>()
|
||||
@@ -53,39 +64,118 @@ class EntryEditViewModel: NodeEditViewModel() {
|
||||
val onBinaryPreviewLoaded : LiveData<AttachmentPosition> get() = _onBinaryPreviewLoaded
|
||||
private val _onBinaryPreviewLoaded = SingleLiveEvent<AttachmentPosition>()
|
||||
|
||||
fun loadDatabase(database: Database?) {
|
||||
loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo, mSearchInfo)
|
||||
}
|
||||
|
||||
fun loadTemplateEntry(database: Database,
|
||||
entry: Entry?,
|
||||
isTemplate: Boolean,
|
||||
fun loadTemplateEntry(database: Database?,
|
||||
entryId: NodeId<UUID>?,
|
||||
parentId: NodeId<*>?,
|
||||
registerInfo: RegisterInfo?,
|
||||
searchInfo: SearchInfo?) {
|
||||
IOActionTask(
|
||||
{
|
||||
val templates = database.getTemplates(isTemplate)
|
||||
val entryTemplate = entry?.let { database.getTemplate(it) } ?: Template.STANDARD
|
||||
var entryInfo: EntryInfo? = null
|
||||
// Decode the entry / load entry info
|
||||
entry?.let {
|
||||
database.decodeEntryWithTemplateConfiguration(it).let { entry ->
|
||||
// Load entry info
|
||||
entry.getEntryInfo(database, true).let { tempEntryInfo ->
|
||||
// Retrieve data from registration
|
||||
(registerInfo?.searchInfo ?: searchInfo)?.let { tempSearchInfo ->
|
||||
tempEntryInfo.saveSearchInfo(database, tempSearchInfo)
|
||||
this.mEntryId = entryId
|
||||
this.mParentId = parentId
|
||||
this.mRegisterInfo = registerInfo
|
||||
this.mSearchInfo = searchInfo
|
||||
|
||||
database?.let {
|
||||
mEntryId?.let {
|
||||
IOActionTask(
|
||||
{
|
||||
// Create an Entry copy to modify from the database entry
|
||||
mEntry = database.getEntryById(it)
|
||||
// Retrieve the parent
|
||||
mEntry?.let { entry ->
|
||||
// If no parent, add root group as parent
|
||||
if (entry.parent == null) {
|
||||
entry.parent = database.rootGroup
|
||||
}
|
||||
registerInfo?.let { regInfo ->
|
||||
tempEntryInfo.saveRegisterInfo(database, regInfo)
|
||||
}
|
||||
entryInfo = tempEntryInfo
|
||||
// Define if current entry is a template (in direct template group)
|
||||
mIsTemplate = database.entryIsTemplate(mEntry)
|
||||
decodeTemplateEntry(
|
||||
database,
|
||||
entry,
|
||||
mIsTemplate,
|
||||
registerInfo,
|
||||
searchInfo
|
||||
)
|
||||
}
|
||||
},
|
||||
{ templatesEntry ->
|
||||
mEntryId = null
|
||||
_templatesEntry.value = templatesEntry
|
||||
}
|
||||
}
|
||||
TemplatesEntry(templates, entryTemplate, entryInfo)
|
||||
},
|
||||
{ templatesEntry ->
|
||||
_templatesEntry.value = templatesEntry
|
||||
).execute()
|
||||
}
|
||||
).execute()
|
||||
|
||||
mParentId?.let {
|
||||
IOActionTask(
|
||||
{
|
||||
mParent = database.getGroupById(it)
|
||||
mParent?.let { parentGroup ->
|
||||
mEntry = database.createEntry()?.apply {
|
||||
// Add the default icon from parent if not a folder
|
||||
val parentIcon = parentGroup.icon
|
||||
// Set default icon
|
||||
if (parentIcon.custom.isUnknown
|
||||
&& parentIcon.standard.id != IconImageStandard.FOLDER_ID
|
||||
) {
|
||||
icon = IconImage(parentIcon.standard)
|
||||
}
|
||||
if (!parentIcon.custom.isUnknown) {
|
||||
icon = IconImage(parentIcon.custom)
|
||||
}
|
||||
// Set default username
|
||||
username = database.defaultUsername
|
||||
// Warning only the entry recognize is parent, parent don't yet recognize the new entry
|
||||
// Useful to recognize child state (ie: entry is a template)
|
||||
parent = parentGroup
|
||||
}
|
||||
mIsTemplate = database.entryIsTemplate(mEntry)
|
||||
decodeTemplateEntry(
|
||||
database,
|
||||
mEntry,
|
||||
mIsTemplate,
|
||||
registerInfo,
|
||||
searchInfo
|
||||
)
|
||||
}
|
||||
},
|
||||
{ templatesEntry ->
|
||||
mParentId = null
|
||||
_templatesEntry.value = templatesEntry
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeTemplateEntry(database: Database,
|
||||
entry: Entry?,
|
||||
isTemplate: Boolean,
|
||||
registerInfo: RegisterInfo?,
|
||||
searchInfo: SearchInfo?): TemplatesEntry {
|
||||
val templates = database.getTemplates(isTemplate)
|
||||
val entryTemplate = entry?.let { database.getTemplate(it) }
|
||||
?: Template.STANDARD
|
||||
var entryInfo: EntryInfo? = null
|
||||
// Decode the entry / load entry info
|
||||
entry?.let {
|
||||
database.decodeEntryWithTemplateConfiguration(it).let { entry ->
|
||||
// Load entry info
|
||||
entry.getEntryInfo(database, true).let { tempEntryInfo ->
|
||||
// Retrieve data from registration
|
||||
(registerInfo?.searchInfo ?: searchInfo)?.let { tempSearchInfo ->
|
||||
tempEntryInfo.saveSearchInfo(database, tempSearchInfo)
|
||||
}
|
||||
registerInfo?.let { regInfo ->
|
||||
tempEntryInfo.saveRegisterInfo(database, regInfo)
|
||||
}
|
||||
entryInfo = tempEntryInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
return TemplatesEntry(isTemplate, templates, entryTemplate, entryInfo)
|
||||
}
|
||||
|
||||
fun changeTemplate(template: Template) {
|
||||
@@ -94,8 +184,8 @@ class EntryEditViewModel: NodeEditViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun requestEntryInfoUpdate(database: Database?, entry: Entry?, parent: Group?) {
|
||||
_requestEntryInfoUpdate.value = EntryUpdate(database, entry, parent)
|
||||
fun requestEntryInfoUpdate(database: Database?) {
|
||||
_requestEntryInfoUpdate.value = EntryUpdate(database, mEntry, mParent)
|
||||
}
|
||||
|
||||
fun saveEntryInfo(database: Database?, entry: Entry?, parent: Group?, entryInfo: EntryInfo) {
|
||||
@@ -222,7 +312,10 @@ class EntryEditViewModel: NodeEditViewModel() {
|
||||
_onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition)
|
||||
}
|
||||
|
||||
data class TemplatesEntry(val templates: List<Template>, val defaultTemplate: Template, val entryInfo: EntryInfo?)
|
||||
data class TemplatesEntry(val isTemplate: Boolean,
|
||||
val templates: List<Template>,
|
||||
val defaultTemplate: Template,
|
||||
val entryInfo: EntryInfo?)
|
||||
data class EntryUpdate(val database: Database?, val entry: Entry?, val parent: Group?)
|
||||
data class EntrySave(val oldEntry: Entry, val newEntry: Entry, val parent: Group?)
|
||||
data class FieldEdition(val oldField: Field?, val newField: Field?)
|
||||
|
||||
@@ -36,20 +36,14 @@ import java.util.*
|
||||
|
||||
class EntryViewModel: ViewModel() {
|
||||
|
||||
val template : LiveData<Template> get() = _template
|
||||
private val _template = MutableLiveData<Template>()
|
||||
private var mMainEntryId: NodeId<UUID>? = null
|
||||
private var mHistoryPosition: Int = -1
|
||||
|
||||
val mainEntryId : LiveData<NodeId<UUID>?> get() = _mainEntryId
|
||||
private val _mainEntryId = MutableLiveData<NodeId<UUID>?>()
|
||||
val entryInfoHistory : LiveData<EntryInfoHistory?> get() = _entryInfoHistory
|
||||
private val _entryInfoHistory = MutableLiveData<EntryInfoHistory?>()
|
||||
|
||||
val historyPosition : LiveData<Int> get() = _historyPosition
|
||||
private val _historyPosition = MutableLiveData<Int>()
|
||||
|
||||
val entryInfo : LiveData<EntryInfo> get() = _entryInfo
|
||||
private val _entryInfo = MutableLiveData<EntryInfo>()
|
||||
|
||||
val entryHistory : LiveData<List<EntryInfo>> get() = _entryHistory
|
||||
private val _entryHistory = MutableLiveData<List<EntryInfo>>()
|
||||
val entryHistory : LiveData<List<EntryInfo>?> get() = _entryHistory
|
||||
private val _entryHistory = MutableLiveData<List<EntryInfo>?>()
|
||||
|
||||
val onOtpElementUpdated : LiveData<OtpElement?> get() = _onOtpElementUpdated
|
||||
private val _onOtpElementUpdated = SingleLiveEvent<OtpElement?>()
|
||||
@@ -62,11 +56,18 @@ class EntryViewModel: ViewModel() {
|
||||
val historySelected : LiveData<EntryHistory> get() = _historySelected
|
||||
private val _historySelected = SingleLiveEvent<EntryHistory>()
|
||||
|
||||
fun loadEntry(database: Database?, entryId: NodeId<UUID>?, historyPosition: Int) {
|
||||
if (database != null && entryId != null) {
|
||||
fun loadDatabase(database: Database?) {
|
||||
loadEntry(database, mMainEntryId, mHistoryPosition)
|
||||
}
|
||||
|
||||
fun loadEntry(database: Database?, mainEntryId: NodeId<UUID>?, historyPosition: Int = -1) {
|
||||
this.mMainEntryId = mainEntryId
|
||||
this.mHistoryPosition = historyPosition
|
||||
|
||||
if (database != null && mainEntryId != null) {
|
||||
IOActionTask(
|
||||
{
|
||||
val mainEntry = database.getEntryById(entryId)
|
||||
val mainEntry = database.getEntryById(mainEntryId)
|
||||
val currentEntry = if (historyPosition > -1) {
|
||||
mainEntry?.getHistory()?.get(historyPosition)
|
||||
} else {
|
||||
@@ -91,6 +92,7 @@ class EntryViewModel: ViewModel() {
|
||||
|
||||
EntryInfoHistory(
|
||||
mainEntry!!.nodeId,
|
||||
historyPosition,
|
||||
entryTemplate,
|
||||
it.getEntryInfo(database),
|
||||
entryInfoHistory
|
||||
@@ -99,22 +101,13 @@ class EntryViewModel: ViewModel() {
|
||||
}
|
||||
},
|
||||
{ entryInfoHistory ->
|
||||
if (entryInfoHistory != null) {
|
||||
_mainEntryId.value = entryInfoHistory.mainEntryId
|
||||
_historyPosition.value = historyPosition
|
||||
_template.value = entryInfoHistory.template
|
||||
_entryInfo.value = entryInfoHistory.entryInfo
|
||||
_entryHistory.value = entryInfoHistory.entryHistory
|
||||
}
|
||||
_entryInfoHistory.value = entryInfoHistory
|
||||
_entryHistory.value = entryInfoHistory?.entryHistory
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateEntry(database: Database?) {
|
||||
loadEntry(database, _mainEntryId.value, _historyPosition.value ?: -1)
|
||||
}
|
||||
|
||||
fun onOtpElementUpdated(optElement: OtpElement?) {
|
||||
_onOtpElementUpdated.value = optElement
|
||||
}
|
||||
@@ -132,6 +125,7 @@ class EntryViewModel: ViewModel() {
|
||||
}
|
||||
|
||||
data class EntryInfoHistory(var mainEntryId: NodeId<UUID>,
|
||||
var historyPosition: Int,
|
||||
val template: Template,
|
||||
val entryInfo: EntryInfo,
|
||||
val entryHistory: List<EntryInfo>)
|
||||
|
||||
9
app/src/main/res/drawable/ic_arrow_right_green_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_arrow_right_green_24dp.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="@color/green"
|
||||
android:pathData="M10,17l5,-5 -5,-5v10z"/>
|
||||
</vector>
|
||||
@@ -95,7 +95,7 @@
|
||||
style="@style/KeepassDXStyle.TextAppearance.Info" />
|
||||
</RelativeLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
|
||||
@@ -23,15 +23,15 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/autofill_entry_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginRight="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginLeft="12dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:contentDescription="@string/content_description_entry_icon"
|
||||
android:src="@drawable/ic_key_white_24dp" />
|
||||
android:src="@drawable/ic_arrow_right_green_24dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/autofill_entry_text"
|
||||
|
||||
42
app/src/main/res/layout/item_autofill_select_entry.xml
Normal file
42
app/src/main/res/layout/item_autofill_select_entry.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/windowBackground"
|
||||
android:minHeight="36dp"
|
||||
android:orientation="horizontal">
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginLeft="12dp"
|
||||
android:contentDescription="@string/autofill_select_entry"
|
||||
android:src="@drawable/ic_arrow_right_green_24dp"/>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/autofill_select_entry"
|
||||
android:layout_gravity="center"
|
||||
android:paddingRight="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingStart="12dp"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSmall"/>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
<include
|
||||
layout="@layout/item_autofill_app_id"/>
|
||||
<include
|
||||
layout="@layout/item_autofill_select_entry"/>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
<include
|
||||
layout="@layout/item_autofill_web_domain"/>
|
||||
<include
|
||||
layout="@layout/item_autofill_select_entry"/>
|
||||
</LinearLayout>
|
||||
@@ -203,6 +203,7 @@
|
||||
<string name="autofill_sign_in_prompt">Mit KeePassDX anmelden</string>
|
||||
<string name="set_autofill_service_title">Standarddienst für automatisches Ausfüllen festlegen</string>
|
||||
<string name="autofill_explanation_summary">Automatisches Ausfüllen aktivieren, um Formulare schnell in anderen Apps auszufüllen</string>
|
||||
<string name="autofill_select_entry">Eintrag auswählen…</string>
|
||||
<string name="clipboard">Zwischenablage</string>
|
||||
<string name="biometric_delete_all_key_title">Verschlüsselungsschlüssel löschen</string>
|
||||
<string name="biometric_delete_all_key_summary">Alle Verschlüsselungsschlüssel löschen, die mit der modernen Entsperrerkennung zusammenhängen</string>
|
||||
@@ -448,6 +449,8 @@
|
||||
<string name="validate">Validieren</string>
|
||||
<string name="autofill_auto_search_summary">Suchergebnisse automatisch nach Web-Domain oder Anwendungs-ID vorschlagen</string>
|
||||
<string name="autofill_auto_search_title">Automatische Suche</string>
|
||||
<string name="autofill_manual_selection_title">Manuelle Auswahl</string>
|
||||
<string name="autofill_manual_selection_summary">Manuelle Auswahl des Datenbank-Eintrags ermöglichen</string>
|
||||
<string name="lock_database_show_button_summary">Zeigt die Sperrtaste in der Benutzeroberfläche an</string>
|
||||
<string name="lock_database_show_button_title">Sperrtaste anzeigen</string>
|
||||
<string name="autofill_preference_title">Einstellungen für automatisches Ausfüllen</string>
|
||||
|
||||
@@ -171,6 +171,7 @@
|
||||
<string name="autofill_sign_in_prompt">Se connecter avec KeePassDX</string>
|
||||
<string name="set_autofill_service_title">Définir le service de remplissage automatique par défaut</string>
|
||||
<string name="autofill_explanation_summary">Activer le remplissage automatique pour remplir rapidement des formulaires dans d’autres applications</string>
|
||||
<string name="autofill_select_entry">Sélectionner une entrée…</string>
|
||||
<string name="password_size_title">Taille du mot de passe généré</string>
|
||||
<string name="password_size_summary">Défini la taille par défaut des mots de passe générés</string>
|
||||
<string name="list_password_generator_options_title">Caractères de mot de passe</string>
|
||||
|
||||
@@ -159,6 +159,8 @@
|
||||
<bool name="autofill_auto_search_default" translatable="false">true</bool>
|
||||
<string name="autofill_inline_suggestions_key" translatable="false">autofill_inline_suggestions_key</string>
|
||||
<bool name="autofill_inline_suggestions_default" translatable="false">false</bool>
|
||||
<string name="autofill_manual_selection_key" translatable="false">autofill_manual_selection_key</string>
|
||||
<bool name="autofill_manual_selection_default" translatable="false">true</bool>
|
||||
<string name="autofill_save_search_info_key" translatable="false">autofill_save_search_info_key</string>
|
||||
<bool name="autofill_save_search_info_default" translatable="false">true</bool>
|
||||
<string name="autofill_ask_to_save_data_key" translatable="false">autofill_ask_to_save_data_key</string>
|
||||
|
||||
@@ -352,6 +352,7 @@
|
||||
<string name="autofill_service_name">KeePassDX form autofilling</string>
|
||||
<string name="autofill_sign_in_prompt">Sign in with KeePassDX</string>
|
||||
<string name="autofill_explanation_summary">Enable autofilling to quickly fill out forms in other apps</string>
|
||||
<string name="autofill_select_entry">Select entry…</string>
|
||||
<string name="set_autofill_service_title">Set default autofill service</string>
|
||||
<string name="autofill_preference_title">Autofill settings</string>
|
||||
<string name="password_size_title">Generated password size</string>
|
||||
@@ -487,6 +488,8 @@
|
||||
<string name="autofill_auto_search_summary">Automatically suggest search results from the web domain or application ID</string>
|
||||
<string name="autofill_inline_suggestions_title">Inline suggestions</string>
|
||||
<string name="autofill_inline_suggestions_summary">Attempt to display autofill suggestions directly from a compatible keyboard</string>
|
||||
<string name="autofill_manual_selection_title">Manual selection</string>
|
||||
<string name="autofill_manual_selection_summary">Display option to let the user select database entry</string>
|
||||
<string name="autofill_save_search_info_title">Save search info</string>
|
||||
<string name="autofill_save_search_info_summary">Try to save search information when making a manual entry selection</string>
|
||||
<string name="autofill_ask_to_save_data_title">Ask to save data</string>
|
||||
|
||||
@@ -35,6 +35,11 @@
|
||||
android:title="@string/autofill_inline_suggestions_title"
|
||||
android:summary="@string/autofill_inline_suggestions_summary"
|
||||
android:defaultValue="@bool/autofill_inline_suggestions_default"/>
|
||||
<SwitchPreference
|
||||
android:key="@string/autofill_manual_selection_key"
|
||||
android:title="@string/autofill_manual_selection_title"
|
||||
android:summary="@string/autofill_manual_selection_summary"
|
||||
android:defaultValue="@bool/autofill_manual_selection_default"/>
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory
|
||||
android:title="@string/save">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
* Add / Manage dynamic templates #191
|
||||
* Allow to manually select RecycleBin group and Templates group #191
|
||||
* Manually select RecycleBin group and Templates group #191
|
||||
* Setting to display OTP Token in list #655
|
||||
* Fix timeout in dialogs #716
|
||||
* Check URI permissions #626
|
||||
* Improvements #1035 #1043 #942 #1021 #1027
|
||||
* Better autofill implementation #943 #946 #984 #1070 (Thx @uduerholz)
|
||||
* Improvements #680 #1035 #1043 #942 #1021 #1027
|
||||
@@ -3,4 +3,5 @@
|
||||
* Paramètres pour afficher les jetons OTP dans la liste #655
|
||||
* Correction du délai d'expiration dans les dialogues #716
|
||||
* Vérification des permissions URI #626
|
||||
* Améliorations #1035 #1043 #942 #1021 #1027
|
||||
* Meilleure implémentation du remplissage auto #943 #946 #984 #1070 (Thx @uduerholz)
|
||||
* Améliorations #680 #1035 #1043 #942 #1021 #1027
|
||||
Reference in New Issue
Block a user