Compare commits

...

46 Commits

Author SHA1 Message Date
J-Jamet
82450c0ae8 Merge branch 'release/3.0.0' 2021-09-07 18:43:31 +02:00
J-Jamet
8dd6c33901 Fix add entry education hint with default templates 2021-09-07 14:00:02 +02:00
J-Jamet
f920d40db5 Fix autofill popup window application id #1046 2021-09-07 13:45:35 +02:00
J-Jamet
19be6c1acc Upgrade to 3.0.0 2021-09-07 13:17:34 +02:00
J-Jamet
7d9d8ad0e4 Manage magikeyboard in landscape 2021-09-07 13:09:24 +02:00
J-Jamet
85f8237d5f Merge branch 'fullscreen' of git://github.com/chenxiaolong/KeePassDX into chenxiaolong-fullscreen 2021-09-07 12:41:24 +02:00
J-Jamet
c542894734 Small change in progress dialog 2021-09-07 12:18:21 +02:00
J-Jamet
d348987077 Remove unused code 2021-09-07 11:21:28 +02:00
J-Jamet
3718610595 Copy OTP Token from list to provide suitable alternative to #553 2021-09-07 10:44:34 +02:00
J-Jamet
9c36ec0623 Fix datetime font size 2021-09-07 10:29:08 +02:00
J-Jamet
c6917b5d74 Update CHANGELOG 2021-09-07 10:16:38 +02:00
Andrew Gunnerson
4eaa179789 magikeyboard: Don't force full screen EditTexts on large screen devices
Per [1], Android defaults to showing EditTexts in full screen mode when
the device is in landscape orientation. This makes sense for phones,
but not so much for larger screen devices, like tablets.

This commit updates MagiKeyboard to not use full screen mode on large
screen devices. The condition for disabling full screen mode is the same
as in the AOSP keyboard and should match what other OEM keyboards do as
well.

[1] https://developer.android.com/reference/android/inputmethodservice/InputMethodService#fullscreen-mode
2021-09-06 13:01:56 -04:00
J-Jamet
9008cd4549 Remove max lines in TextFieldView #1073 #1076 2021-09-06 12:18:17 +02:00
J-Jamet
cc3204453e Upgrade to 3.0.0_beta03 2021-09-03 15:31:17 +02:00
J-Jamet
5ef8d3b7b9 Update CHANGELOG 2021-09-03 15:25:30 +02:00
J-Jamet
2a9de97a19 Default manual selection to true 2021-09-03 15:14:39 +02:00
J-Jamet
9cecfed417 Add dots 2021-09-03 15:10:18 +02:00
J-Jamet
319715918a Small change to merge views 2021-09-03 15:02:40 +02:00
J-Jamet
a3bf6e8b6d Small change for consistency 2021-09-03 14:41:45 +02:00
J-Jamet
c4062658ce Fix search info parcelable 2021-09-03 14:39:19 +02:00
J-Jamet
01a5de413e Merge branch 'develop' of git://github.com/uduerholz/KeePassDX into uduerholz-develop 2021-09-03 14:18:51 +02:00
J-Jamet
e4c22b1f29 Change remote views when the database is open 2021-09-03 12:44:01 +02:00
J-Jamet
b10e60126f Fix UUID view 2021-09-02 17:39:08 +02:00
J-Jamet
ef1f27f421 Check null view model callback 2021-09-02 17:31:51 +02:00
J-Jamet
0ed208675c Fix reloading from history 2021-09-02 17:18:01 +02:00
J-Jamet
00f7a0a194 Better entry activity view model to fix reloading 2021-09-02 17:05:07 +02:00
J-Jamet
935d4f4a64 Unused throw 2021-09-02 16:13:25 +02:00
J-Jamet
dc4d88260d Fix database reload 2021-09-02 16:13:08 +02:00
J-Jamet
18934601da Fix education 2021-09-02 14:26:08 +02:00
J-Jamet
4ea811aeda Fix menu in template creation 2021-09-02 13:48:48 +02:00
J-Jamet
f8fdecdc8f Fix multiple loading by move variables in entry edit view model 2021-09-02 11:12:19 +02:00
J-Jamet
5467c61137 Add equals in node info 2021-09-02 11:10:51 +02:00
J-Jamet
9c72b4cc56 Fix timeout switch 2021-09-01 18:51:18 +02:00
J-Jamet
9102217bc3 Fix template lost after orientation change #1069 2021-09-01 17:38:29 +02:00
Uli
0e8fd7b2c4 Merge branch 'Kunzisoft:develop' into develop 2021-09-01 14:41:07 +02:00
J-Jamet
a06ea8fe55 Fix warning 2021-08-30 11:13:38 +02:00
J-Jamet
31eb0fb48a Upgrade to 3.0.0_beta02 2021-08-30 11:05:38 +02:00
J-Jamet
d6a012e85f Fix Permissions #1066 2021-08-30 11:05:08 +02:00
Uli
c6e2342ab4 Merge branch 'Kunzisoft:develop' into develop 2021-08-29 17:04:31 +02:00
Ulrich Dürholz
b977792168 Autofill manual selection for all form fields 2021-08-29 11:23:22 +02:00
Uli
2595cf87d8 Merge branch 'Kunzisoft:develop' into develop 2021-08-29 10:50:08 +02:00
Ulrich Dürholz
f4342f1448 Manual selection for inline suggestions 2021-08-29 10:16:15 +02:00
Ulrich Dürholz
c71ef24052 Add icon for manual autofill selection 2021-08-28 12:48:03 +02:00
Ulrich Dürholz
39b817bc69 Let user select entry for autofill 2021-08-27 18:23:32 +02:00
Uli
e3adaba3b3 Merge branch 'Kunzisoft:develop' into develop 2021-08-24 16:39:45 +02:00
Ulrich Dürholz
c62064002f Fix small issue with credit card autofill 2021-07-27 17:36:56 +02:00
57 changed files with 856 additions and 406 deletions

View File

@@ -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 #1046 #1082 #1083 (Thx @chenxiaolong)
KeePassDX(2.10.5)
* Increase the saving speed of database #1028

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass"
minSdkVersion 15
targetSdkVersion 30
versionCode = 84
versionName = "3.0.0_beta01"
versionCode = 87
versionName = "3.0.0"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"

View File

@@ -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,

View File

@@ -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,

View File

@@ -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?

View File

@@ -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
}

View File

@@ -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
@@ -932,9 +934,7 @@ class GroupActivity : DatabaseLockActivity(),
) {
// If no node, show education to add new one
val addNodeButtonEducationPerformed = mGroupFragment != null
&& mGroupFragment!!.isEmpty
&& actionNodeMode == null
val addNodeButtonEducationPerformed = actionNodeMode == null
&& addNodeButtonView?.addButtonView != null
&& addNodeButtonView!!.isEnable
&& groupActivityEducation.checkAndPerformedAddNodeButtonEducation(

View File

@@ -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) {

View File

@@ -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 ->

View File

@@ -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)
}

View File

@@ -74,9 +74,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
private var mRecycleBinEnable: Boolean = false
private var mRecycleBin: Group? = null
val isEmpty: Boolean
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
@@ -233,7 +230,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
return mLayoutManager?.findFirstVisibleItemPosition() ?: 0
}
@Throws(IllegalArgumentException::class)
private fun rebuildList() {
try {
// Add elements to the list
@@ -418,7 +414,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")
}
}
}

View File

@@ -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()
}

View File

@@ -28,6 +28,7 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
@@ -41,9 +42,11 @@ import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.setTextSize
import com.kunzisoft.keepass.view.strikeOut
import java.util.*
@@ -77,6 +80,7 @@ class NodeAdapter (private val context: Context,
private var mActionNodesList = LinkedList<Node>()
private var mNodeClickCallback: NodeClickCallback? = null
private var mClipboardHelper = ClipboardHelper(context)
@ColorInt
private val mContentSelectionColor: Int
@@ -428,6 +432,17 @@ class NodeAdapter (private val context: Context,
}
}
holder?.otpToken?.text = otpElement?.token
holder?.otpContainer?.setOnClickListener {
otpElement?.token?.let { token ->
Toast.makeText(
context,
context.getString(R.string.copy_field,
TemplateField.getLocalizedName(context, TemplateField.LABEL_TOKEN)),
Toast.LENGTH_LONG
).show()
mClipboardHelper.copyToClipboard(token)
}
}
}
class OtpRunnable(val view: View?): Runnable {

View File

@@ -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()
}

View File

@@ -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())
}
}
}

View File

@@ -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

View File

@@ -53,8 +53,10 @@ class StructureParser(private val structure: AssistStructure) {
applicationId = windowNode.title.toString().split("/")[0]
Log.d(TAG, "Autofill applicationId: $applicationId")
if (parseViewNode(windowNode.rootViewNode))
break@mainLoop
if (applicationId?.contains("PopupWindow:") == false) {
if (parseViewNode(windowNode.rootViewNode))
break@mainLoop
}
}
// If not explicit username field found, add the field just before password field.
if (usernameId == null && passwordId != null && usernameIdCandidate != null) {
@@ -143,19 +145,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 +184,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 +206,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 +229,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 +401,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)

View File

@@ -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(

View File

@@ -178,6 +178,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
assignKeyboardView()
}
override fun onEvaluateFullscreenMode(): Boolean {
return resources.getBoolean(R.bool.magikeyboard_allow_fullscreen_mode)
&& super.onEvaluateFullscreenMode()
}
private fun playVibration(keyCode: Int) {
when (keyCode) {
Keyboard.KEYCODE_DELETE -> {}

View File

@@ -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"

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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))

View File

@@ -125,6 +125,7 @@ class DurationDialogFragmentCompat : InputPreferenceDialogFragmentCompat() {
}
}
mEnabled = isSwitchActivated()
setSwitchAction({ isChecked ->
mEnabled = isChecked
}, mDays + mHours + mMinutes + mSeconds > 0)

View File

@@ -155,6 +155,10 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
}
}
fun isSwitchActivated(): Boolean {
return switchElementView?.isChecked == true
}
fun activateSwitch() {
if (switchElementView?.isChecked != true)
switchElementView?.isChecked = true

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -46,8 +46,6 @@ class DateTimeFieldView @JvmOverloads constructor(context: Context,
private var mDefault: DateInstant = DateInstant.NEVER_EXPIRES
var setOnDateClickListener: ((DateInstant) -> Unit)? = null
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_date_time, this)

View File

@@ -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)

View File

@@ -53,7 +53,6 @@ class TemplateView @JvmOverloads constructor(context: Context,
label = templateAttribute.alias
?: TemplateField.getLocalizedName(context, field.name)
setMaxChars(templateAttribute.options.getNumberChars())
setMaxLines(templateAttribute.options.getNumberLines())
// TODO Linkify
value = field.protectedValue.stringValue
// Here the value is often empty

View File

@@ -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

View File

@@ -214,18 +214,6 @@ class TextFieldView @JvmOverloads constructor(context: Context,
}
}
fun setMaxLines(numberLines: Int) {
when {
numberLines <= 0 -> {
valueView.maxLines = MAX_LINES_LIMIT
}
else -> {
val lines = if (numberLines > MAX_LINES_LIMIT) MAX_LINES_LIMIT else numberLines
valueView.maxLines = lines
}
}
}
fun setProtection(protection: Boolean, hiddenProtectedValue: Boolean = false) {
showButton.isVisible = protection
showButton.isSelected = hiddenProtectedValue
@@ -343,6 +331,5 @@ class TextFieldView @JvmOverloads constructor(context: Context,
companion object {
const val MAX_CHARS_LIMIT = Integer.MAX_VALUE
const val MAX_LINES_LIMIT = 40
}
}

View File

@@ -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

View File

@@ -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?)

View File

@@ -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>)

View 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>

View File

@@ -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"

View File

@@ -33,6 +33,7 @@
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
style="@style/KeepassDXStyle.TextAppearance.Title"
android:textStyle="bold"
android:textColor="?android:attr/textColor"/>
<TextView
@@ -62,7 +63,7 @@
app:indicatorColor="?attr/colorAccent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginTop="20dp"
android:indeterminate="true"
android:max="100"/>

View File

@@ -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"

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -118,9 +118,11 @@
android:id="@+id/node_otp_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginEnd="12dp"
android:layout_marginRight="12dp"
android:padding="4dp"
android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/node_attachment_icon"
app:layout_constraintEnd_toEndOf="parent">

View File

@@ -20,7 +20,7 @@
android:focusable="false"
android:cursorVisible="false"
android:focusableInTouchMode="false"
style="@style/KeepassDXStyle.TextAppearance.Large"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem"
tools:text="2020-03-04 05:00" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.appcompat.widget.SwitchCompat

View File

@@ -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>

View File

@@ -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 dautres 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>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2021 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 <http://www.gnu.org/licenses/>.
-->
<resources>
<bool name="magikeyboard_allow_fullscreen_mode">true</bool>
</resources>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2021 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 <http://www.gnu.org/licenses/>.
-->
<resources>
<bool name="magikeyboard_allow_fullscreen_mode">false</bool>
</resources>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2021 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 <http://www.gnu.org/licenses/>.
-->
<resources>
<bool name="magikeyboard_allow_fullscreen_mode">false</bool>
</resources>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -1,6 +0,0 @@
* Add / Manage dynamic templates #191
* Allow to 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

View File

@@ -0,0 +1,7 @@
* Add / Manage dynamic templates #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
* Better autofill implementation #943 #946 #984 #1070 (Thx @uduerholz)
* Improvements #680 #1035 #1043 #942 #1021 #1027 #1046 #1082 #1083 (Thx @chenxiaolong)

View File

@@ -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 #1046 #1082 #1083 (Thx @chenxiaolong)