Compare commits

..

62 Commits

Author SHA1 Message Date
J-Jamet
7750843b04 Merge branch 'release/3.0.2' 2021-09-24 19:34:58 +02:00
J-Jamet
d7da1ce333 Upgrade to 3.0.2 and update CHANGELOG 2021-09-24 12:55:08 +02:00
J-Jamet
dd9ee8c3f8 Merge branch 'chenxiaolong-samsung_dex' into develop 2021-09-24 12:50:40 +02:00
Andrew Gunnerson
c0ac01a34a Add workaround to support Samsung DeX
This commit changes the Magikeyboard service behavior so that KeePassDX
is able to run in Samsung DeX mode. Currently, the app cannot run in
DeX mode because apps which have services using `BIND_INPUT_METHOD` are
blocked.

A new broadcast receiver has been added to listen for DeX's enter/leave
events [1] and disable/enable the `Magikeyboard` service appropriately.
The enabled state of a service lives in the Android framework's
`PackageManager` and survives app crashes and device reboots (though it
does get reset when app data is cleared).

Additionally, an extra check is added to `FileDatabaseSelectActivity` to
ensure the service's enabled state is correct. This is necessary if the
app crashes or is force quit within DeX mode and then the user exits DeX
mode. Otherwise, the service would stay disabled until the user entered
and exited DeX again.

With the new behavior, KeePassDX will generally just work with DeX,
though there's one caveat: after the initial installation, the user must
open the app once outside of DeX. Otherwise, Android will not trigger
the broadcast receiver. This could be fixed by making the service
intially disabled in the manifest with `android:enabled="false"`, but
Android's Settings app in SDK 15 through 25 does not correctly refresh
the keyboard list when changing the service from disabled to enabled.
I opted *not* to introduce different behavior based on the API version.

[1] https://developer.samsung.com/sdp/blog/en-us/2017/07/27/samsung-dex-how-to-detect-the-samsung-dex-mode

Fixes: #245
Signed-off-by: Andrew Gunnerson <chillermillerlong@hotmail.com>
2021-09-18 23:30:37 -04:00
J-Jamet
1b88f2ddf0 Merge tag '3.0.1' into develop
3.0.1
2021-09-15 13:25:03 +02:00
J-Jamet
b4f2a1eb89 Merge branch 'release/3.0.1' 2021-09-15 13:24:53 +02:00
J-Jamet
e9fc9cbc2a Capture cast exception 2021-09-15 12:21:44 +02:00
J-Jamet
b809180a1b Small changes 2021-09-15 11:25:15 +02:00
J-Jamet
ecc75df3a1 Fix search actions #1091 #1092 2021-09-15 11:16:32 +02:00
J-Jamet
d1b6863143 Update CHANGELOG 2021-09-15 10:36:41 +02:00
J-Jamet
faf27143aa Fix timeout reset #1107 2021-09-15 10:33:37 +02:00
J-Jamet
aee58a4475 Fix exception after group name change and save #1112 2021-09-15 09:48:48 +02:00
J-Jamet
c5e07f643f Fix Magikeyboard URL auto action #1100 2021-09-14 20:40:22 +02:00
J-Jamet
818f5820d5 Update CHANGELOG 2021-09-14 20:13:35 +02:00
J-Jamet
77c6e28876 Fix max lines #1073 2021-09-14 20:06:49 +02:00
J-Jamet
fb7f66012d Upgrade to 3.0.1 2021-09-14 19:35:30 +02:00
J-Jamet
0f7f7bbe6c Min height to 48dp 2021-09-14 19:30:50 +02:00
J-Jamet
8dedb8deb4 Fix text dimension 2021-09-14 19:10:48 +02:00
J-Jamet
d284db4d3c Merge tag '3.0.0' into develop
3.0.0
2021-09-07 18:43:40 +02:00
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
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
62 changed files with 1012 additions and 445 deletions

View File

@@ -1,10 +1,23 @@
KeePassDX(3.0.2)
* Samsung DeX mode #1114 #245 (Thx @chenxiaolong)
KeePassDX(3.0.1)
* Fix text size and smallest margin #1085
* Fix number of lines during an edition #1073
* Fix Magikeyboard URL auto action #1100
* Fix exception after group name change and save #1112
* Fix timeout reset #1107
* Fix search actions #1091 #1092
* Small changes #1106 #1085
KeePassDX(3.0.0)
* 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
* 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 = 85
versionName = "3.0.0_beta02"
versionCode = 89
versionName = "3.0.2"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"

View File

@@ -221,6 +221,14 @@
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
android:enabled="true"
android:exported="false" />
<receiver
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.app.action.ENTER_KNOX_DESKTOP_MODE" />
<action android:name="android.app.action.EXIT_KNOX_DESKTOP_MODE" />
</intent-filter>
</receiver>
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
</application>

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

@@ -88,6 +88,12 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Enabling/disabling MagikeyboardService is normally done by DexModeReceiver, but this
// additional check will allow the keyboard to be reenabled more easily if the app crashes
// or is force quit within DeX mode and then the user leaves DeX mode. Without this, the
// user would need to enter and exit DeX mode once to reenable the service.
MagikeyboardUtil.setEnabled(this, !DexUtil.isDexMode(resources.configuration))
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
setContentView(R.layout.activity_file_selection)

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
@@ -651,6 +653,8 @@ class GroupActivity : DatabaseLockActivity(),
Log.e(TAG, "Node can't be cast in Entry")
}
}
reloadGroupIfSearch()
}
private fun entrySelectedForSave(database: Database, entry: Entry, searchInfo: SearchInfo) {
@@ -736,6 +740,12 @@ class GroupActivity : DatabaseLockActivity(),
actionNodeMode?.finish()
}
private fun reloadGroupIfSearch() {
if (Intent.ACTION_SEARCH == intent.action) {
reloadCurrentGroup()
}
}
override fun onNodeSelected(
database: Database,
nodes: List<Node>
@@ -791,6 +801,7 @@ class GroupActivity : DatabaseLockActivity(),
(node as Entry).nodeId
)
}
reloadGroupIfSearch()
return true
}
@@ -845,6 +856,7 @@ class GroupActivity : DatabaseLockActivity(),
): Boolean {
deleteNodes(nodes)
finishNodeAction()
reloadGroupIfSearch()
return true
}
@@ -932,9 +944,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(
@@ -1093,7 +1103,7 @@ class GroupActivity : DatabaseLockActivity(),
try {
mGroupViewModel.loadGroup(mDatabase, mCurrentGroupState)
} catch (e: Exception) {
Log.e(TAG, "Unable to rebuild the list after deletion", e)
Log.e(TAG, "Unable to rebuild the group", e)
}
}

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.*
@@ -64,8 +67,10 @@ class NodeAdapter (private val context: Context,
private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
private var mPrefSizeMultiplier: Float = 0F
private var mSubtextDefaultDimension: Float = 0F
private var mInfoTextDefaultDimension: Float = 0F
private var mTextDefaultDimension: Float = 0F
private var mSubTextDefaultDimension: Float = 0F
private var mMetaTextDefaultDimension: Float = 0F
private var mOtpTokenTextDefaultDimension: Float = 0F
private var mNumberChildrenTextDefaultDimension: Float = 0F
private var mIconDefaultDimension: Float = 0F
@@ -77,6 +82,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
@@ -299,8 +305,10 @@ class NodeAdapter (private val context: Context,
mInflater.inflate(R.layout.item_list_nodes_entry, parent, false)
}
val nodeViewHolder = NodeViewHolder(view)
mInfoTextDefaultDimension = nodeViewHolder.text.textSize
mSubtextDefaultDimension = nodeViewHolder.subText.textSize
mTextDefaultDimension = nodeViewHolder.text.textSize
mSubTextDefaultDimension = nodeViewHolder.subText?.textSize ?: mSubTextDefaultDimension
mMetaTextDefaultDimension = nodeViewHolder.meta.textSize
mOtpTokenTextDefaultDimension = nodeViewHolder.otpToken?.textSize ?: mOtpTokenTextDefaultDimension
nodeViewHolder.numberChildren?.let {
mNumberChildrenTextDefaultDimension = it.textSize
}
@@ -311,7 +319,9 @@ class NodeAdapter (private val context: Context,
val subNode = mNodeSortedList.get(position)
// Node selection
holder.container.isSelected = mActionNodesList.contains(subNode)
holder.container.apply {
isSelected = mActionNodesList.contains(subNode)
}
// Assign image
val iconColor = if (holder.container.isSelected)
@@ -333,19 +343,18 @@ class NodeAdapter (private val context: Context,
// Assign text
holder.text.apply {
text = subNode.title
setTextSize(mTextSizeUnit, mInfoTextDefaultDimension, mPrefSizeMultiplier)
setTextSize(mTextSizeUnit, mTextDefaultDimension, mPrefSizeMultiplier)
strikeOut(subNode.isCurrentlyExpires)
}
// Add subText with username
holder.subText.apply {
text = ""
strikeOut(subNode.isCurrentlyExpires)
visibility = View.GONE
}
// Add meta text to show UUID
holder.meta.apply {
text = subNode.nodeId.toString()
visibility = if (mShowUUID) View.VISIBLE else View.GONE
if (mShowUUID) {
text = subNode.nodeId.toString()
setTextSize(mTextSizeUnit, mMetaTextDefaultDimension, mPrefSizeMultiplier)
visibility = View.VISIBLE
} else {
visibility = View.GONE
}
}
// Specific elements for entry
@@ -354,12 +363,16 @@ class NodeAdapter (private val context: Context,
database.startManageEntry(entry)
holder.text.text = entry.getVisualTitle()
holder.subText.apply {
// Add subText with username
holder.subText?.apply {
val username = entry.username
if (mShowUserNames && username.isNotEmpty()) {
visibility = View.VISIBLE
text = username
setTextSize(mTextSizeUnit, mSubtextDefaultDimension, mPrefSizeMultiplier)
setTextSize(mTextSizeUnit, mSubTextDefaultDimension, mPrefSizeMultiplier)
strikeOut(subNode.isCurrentlyExpires)
} else {
visibility = View.GONE
}
}
@@ -427,7 +440,21 @@ class NodeAdapter (private val context: Context,
}
}
}
holder?.otpToken?.text = otpElement?.token
holder?.otpToken?.apply {
text = otpElement?.token
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
}
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 {
@@ -468,7 +495,7 @@ class NodeAdapter (private val context: Context,
var imageIdentifier: ImageView? = itemView.findViewById(R.id.node_image_identifier)
var icon: ImageView = itemView.findViewById(R.id.node_icon)
var text: TextView = itemView.findViewById(R.id.node_text)
var subText: TextView = itemView.findViewById(R.id.node_subtext)
var subText: TextView? = itemView.findViewById(R.id.node_subtext)
var meta: TextView = itemView.findViewById(R.id.node_meta)
var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container)
var otpProgress: ProgressBar? = itemView.findViewById(R.id.node_otp_progress)

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

@@ -177,16 +177,20 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
fun addChildrenFrom(group: Group) {
group.groupKDB?.getChildEntries()?.forEach { entryToAdd ->
groupKDB?.addChildEntry(entryToAdd)
entryToAdd.parent = groupKDB
}
group.groupKDB?.getChildGroups()?.forEach { groupToAdd ->
groupKDB?.addChildGroup(groupToAdd)
groupToAdd.parent = groupKDB
}
group.groupKDBX?.getChildEntries()?.forEach { entryToAdd ->
groupKDBX?.addChildEntry(entryToAdd)
entryToAdd.parent = groupKDBX
}
group.groupKDBX?.getChildGroups()?.forEach { groupToAdd ->
groupKDBX?.addChildGroup(groupToAdd)
groupToAdd.parent = groupKDBX
}
}

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 -> {}
@@ -267,7 +272,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
if (entryInfoKey != null) {
currentInputConnection.commitText(entryInfoKey!!.url, 1)
}
actionTabAutomatically()
actionGoAutomatically()
}
KEY_FIELDS -> {
if (entryInfoKey != null) {

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

@@ -0,0 +1,33 @@
package com.kunzisoft.keepass.receivers
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.util.Log
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.utils.DexUtil
import com.kunzisoft.keepass.utils.MagikeyboardUtil
class DexModeReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val enabled = when (intent?.action) {
"android.app.action.ENTER_KNOX_DESKTOP_MODE" -> {
Log.i(TAG, "Entered DeX mode")
false
}
"android.app.action.EXIT_KNOX_DESKTOP_MODE" -> {
Log.i(TAG, "Left DeX mode")
true
}
else -> return
}
MagikeyboardUtil.setEnabled(context!!, enabled)
}
companion object {
private val TAG = DexModeReceiver::class.java.name
}
}

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

@@ -64,11 +64,13 @@ class DurationDialogFragmentCompat : InputPreferenceDialogFragmentCompat() {
private fun durationToDaysHoursMinutesSeconds(duration: Long) {
if (duration < 0) {
mEnabled = false
mDays = 0
mHours = 0
mMinutes = 0
mSeconds = 0
} else {
mEnabled = true
mDays = (duration / (24L * 60L * 60L * 1000L)).toInt()
val daysMilliseconds = mDays * 24L * 60L * 60L * 1000L
mHours = ((duration - daysMilliseconds) / (60L * 60L * 1000L)).toInt()
@@ -127,7 +129,7 @@ class DurationDialogFragmentCompat : InputPreferenceDialogFragmentCompat() {
setSwitchAction({ isChecked ->
mEnabled = isChecked
}, mDays + mHours + mMinutes + mSeconds > 0)
}, mEnabled)
assignValuesInViews()
}

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

@@ -0,0 +1,27 @@
package com.kunzisoft.keepass.utils
import android.content.res.Configuration
import android.util.Log
object DexUtil {
private val TAG = DexUtil::class.java.name
// Determine if the current environment is in DeX mode. Always returns false on non-Samsung
// devices.
fun isDexMode(config: Configuration): Boolean {
// This is the documented way to check this: https://developer.samsung.com/samsung-dex/modify-optimizing.html
return try {
val configClass = config.javaClass
val enabledConstant = configClass.getField("SEM_DESKTOP_MODE_ENABLED").getInt(configClass)
val enabledField = configClass.getField("semDesktopModeEnabled").getInt(config)
val isEnabled = enabledConstant == enabledField
Log.d(TAG, "DeX currently enabled: $isEnabled")
isEnabled
} catch (e: Exception) {
Log.d(TAG, "Failed to check for DeX mode; likely not Samsung device: $e")
false
}
}
}

View File

@@ -0,0 +1,27 @@
package com.kunzisoft.keepass.utils
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.util.Log
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
object MagikeyboardUtil {
private val TAG = MagikeyboardUtil::class.java.name
// Set whether MagikeyboardService is enabled. This change is persistent and survives app
// crashes and device restarts. The state is changed immediately and does not require an app
// restart.
fun setEnabled(context: Context, enabled: Boolean) {
val componentState = if (enabled) {
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
} else {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
}
Log.d(TAG, "Setting service state: $enabled")
val component = ComponentName(context, MagikeyboardService::class.java)
context.packageManager.setComponentEnabledSetting(component, componentState, PackageManager.DONT_KILL_APP)
}
}

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

@@ -275,22 +275,26 @@ abstract class TemplateAbstractView<
templateAttribute: TemplateAttribute,
entryInfoValue: String,
showEmptyFields: Boolean) {
var fieldView: TEntryFieldView? = findViewWithTag(fieldTag)
if (!showEmptyFields && entryInfoValue.isEmpty()) {
fieldView?.isFieldVisible = false
} else if (fieldView == null && entryInfoValue.isNotEmpty()) {
// Add new not referenced view if standard field not in template
fieldView = buildViewForNotReferencedField(
Field(templateAttribute.label,
ProtectedString(templateAttribute.protected, "")),
templateAttribute
) as? TEntryFieldView?
fieldView?.let {
addNotReferencedView(it as View)
try {
var fieldView: TEntryFieldView? = findViewWithTag(fieldTag)
if (!showEmptyFields && entryInfoValue.isEmpty()) {
fieldView?.isFieldVisible = false
} else if (fieldView == null && entryInfoValue.isNotEmpty()) {
// Add new not referenced view if standard field not in template
fieldView = buildViewForNotReferencedField(
Field(templateAttribute.label,
ProtectedString(templateAttribute.protected, "")),
templateAttribute
) as? TEntryFieldView?
fieldView?.let {
addNotReferencedView(it as View)
}
}
fieldView?.value = entryInfoValue
fieldView?.applyFontVisibility(mFontInVisibility)
} catch(e: Exception) {
Log.e(TAG, "Unable to populate entry field view", e)
}
fieldView?.value = entryInfoValue
fieldView?.applyFontVisibility(mFontInVisibility)
}
@Suppress("UNCHECKED_CAST")
@@ -299,22 +303,25 @@ abstract class TemplateAbstractView<
expires: Boolean,
expiryTime: DateInstant,
showEmptyFields: Boolean) {
var fieldView: TDateTimeView? = findViewWithTag(fieldTag)
if (!showEmptyFields && !expires) {
fieldView?.isFieldVisible = false
} else if (fieldView == null && expires) {
fieldView = buildViewForNotReferencedField(
Field(templateAttribute.label,
ProtectedString(templateAttribute.protected, "")),
templateAttribute
) as? TDateTimeView?
fieldView?.let {
addNotReferencedView(it as View)
try {
var fieldView: TDateTimeView? = findViewWithTag(fieldTag)
if (!showEmptyFields && !expires) {
fieldView?.isFieldVisible = false
} else if (fieldView == null && expires) {
fieldView = buildViewForNotReferencedField(
Field(templateAttribute.label,
ProtectedString(templateAttribute.protected, "")),
templateAttribute
) as? TDateTimeView?
fieldView?.let {
addNotReferencedView(it as View)
}
}
fieldView?.activation = expires
fieldView?.dateTime = expiryTime
} catch(e: Exception) {
Log.e(TAG, "Unable to populate date time view", e)
}
fieldView?.activation = expires
fieldView?.dateTime = expiryTime
}
/**

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

@@ -187,6 +187,6 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
companion object {
const val MAX_CHARS_LIMIT = Integer.MAX_VALUE
const val MAX_LINES_LIMIT = 40
const val MAX_LINES_LIMIT = Integer.MAX_VALUE
}
}

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

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

@@ -30,8 +30,7 @@
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:minHeight="56dp"
android:maxHeight="72dp"
android:minHeight="48dp"
app:layout_constraintWidth_percent="@dimen/content_percent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
@@ -104,10 +103,12 @@
tools:text="7543A7EAB2EA7CFD1394F1615EBEB08C" />
</LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
android:id="@+id/node_options"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="end"
android:layout_marginStart="12dp"
android:layout_marginLeft="12dp"
app:layout_constraintTop_toTopOf="parent"
@@ -118,9 +119,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">
@@ -162,7 +165,7 @@
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/node_otp_container" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<View
android:layout_width="match_parent"

View File

@@ -30,8 +30,7 @@
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:minHeight="56dp"
android:maxHeight="72dp"
android:minHeight="48dp"
app:layout_constraintWidth_percent="@dimen/content_percent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
@@ -89,18 +88,6 @@
android:maxLines="2"
tools:text="Node Title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/node_subtext"
style="@style/KeepassDXStyle.TextAppearance.Group.SubTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-4dp"
android:gravity="center_vertical"
android:lines="1"
android:singleLine="true"
android:visibility="gone"
tools:text="Node SubTitle" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/node_meta"
style="@style/KeepassDXStyle.TextAppearance.Group.Meta"

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>
@@ -366,7 +367,7 @@
<string name="clipboard_warning">If automatic deletion of clipboard fails, delete its history manually.</string>
<string name="lock">Lock</string>
<string name="lock_database_screen_off_title">Screen lock</string>
<string name="lock_database_screen_off_summary">Lock the database when the screen is off</string>
<string name="lock_database_screen_off_summary">Lock the database after a few seconds once the screen is off</string>
<string name="lock_database_back_root_title">Press \'Back\' to lock</string>
<string name="lock_database_back_root_summary">Lock the database when the user clicks the back button on the root screen</string>
<string name="lock_database_show_button_title">Show lock button</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

@@ -0,0 +1,7 @@
* Fix text size and smallest margin #1085
* Fix number of lines during an edition #1073
* Fix Magikeyboard URL auto action #1100
* Fix exception after group name change and save #1112
* Fix timeout reset #1107
* Fix search actions #1091 #1092
* Small changes #1106 #1085

View File

@@ -0,0 +1 @@
* Samsung DeX mode #1114 #245 (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)

View File

@@ -0,0 +1,7 @@
* Correction de la taille de texte et plus petites marges #1085
* Correction du nombre de lignes d'édition #1073
* Correction de l'action auto de l'URL Magiclavier #1100
* Correction de l'exception après un changement de nom de groupe et sauvegarde #1112
* Correction de la réinitialisation du délai d'expiration #1107
* Correction des actions de recherche #1091 #1092
* Petits changements #1106 #1085

View File

@@ -0,0 +1 @@
* Mode Samsung DeX #1114 #245 (Thx @chenxiaolong)