Compare commits

..

65 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
J-Jamet
a06ea8fe55 Fix warning 2021-08-30 11:13:38 +02:00
J-Jamet
31eb0fb48a Upgrade to 3.0.0_beta02 2021-08-30 11:05:38 +02:00
J-Jamet
d6a012e85f Fix Permissions #1066 2021-08-30 11:05:08 +02:00
Uli
c6e2342ab4 Merge branch 'Kunzisoft:develop' into develop 2021-08-29 17:04:31 +02:00
Ulrich Dürholz
b977792168 Autofill manual selection for all form fields 2021-08-29 11:23:22 +02:00
Uli
2595cf87d8 Merge branch 'Kunzisoft:develop' into develop 2021-08-29 10:50:08 +02:00
Ulrich Dürholz
f4342f1448 Manual selection for inline suggestions 2021-08-29 10:16:15 +02:00
Ulrich Dürholz
c71ef24052 Add icon for manual autofill selection 2021-08-28 12:48:03 +02:00
Ulrich Dürholz
39b817bc69 Let user select entry for autofill 2021-08-27 18:23:32 +02:00
Uli
e3adaba3b3 Merge branch 'Kunzisoft:develop' into develop 2021-08-24 16:39:45 +02:00
Ulrich Dürholz
c62064002f Fix small issue with credit card autofill 2021-07-27 17:36:56 +02:00
68 changed files with 1077 additions and 476 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) KeePassDX(3.0.0)
* Add / Manage dynamic templates #191 * Add / Manage dynamic templates #191
* Manually select RecycleBin group and Templates group #191 * Manually select RecycleBin group and Templates group #191
* Setting to display OTP Token in list #655 * Setting to display OTP Token in list #655
* Fix timeout in dialogs #716 * Fix timeout in dialogs #716
* Check URI permissions #626 * 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) KeePassDX(2.10.5)
* Increase the saving speed of database #1028 * Increase the saving speed of database #1028

View File

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

View File

@@ -221,6 +221,14 @@
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService" android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
android:enabled="true" android:enabled="true"
android:exported="false" /> 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" /> <meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
</application> </application>

View File

@@ -65,6 +65,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID) applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN) webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME) webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME)
manualSelection = intent.getBooleanExtra(KEY_MANUAL_SELECTION, false)
} }
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain -> SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain searchInfo.webDomain = concreteWebDomain
@@ -198,15 +199,16 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
companion object { 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_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN" private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME" private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME"
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO" private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
fun getAuthIntentSenderForSelection(context: Context, fun getPendingIntentForSelection(context: Context,
searchInfo: SearchInfo? = null, searchInfo: SearchInfo? = null,
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): IntentSender { inlineSuggestionsRequest: InlineSuggestionsRequest? = null): PendingIntent {
return PendingIntent.getActivity(context, 0, return PendingIntent.getActivity(context, 0,
// Doesn't work with Parcelable (don't know why?) // Doesn't work with Parcelable (don't know why?)
Intent(context, AutofillLauncherActivity::class.java).apply { Intent(context, AutofillLauncherActivity::class.java).apply {
@@ -214,6 +216,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId) putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
putExtra(KEY_SEARCH_DOMAIN, it.webDomain) putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
putExtra(KEY_SEARCH_SCHEME, it.webScheme) putExtra(KEY_SEARCH_SCHEME, it.webScheme)
putExtra(KEY_MANUAL_SELECTION, it.manualSelection)
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.let { inlineSuggestionsRequest?.let {
@@ -221,17 +224,17 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
} }
} }
}, },
PendingIntent.FLAG_CANCEL_CURRENT).intentSender PendingIntent.FLAG_CANCEL_CURRENT)
} }
fun getAuthIntentSenderForRegistration(context: Context, fun getPendingIntentForRegistration(context: Context,
registerInfo: RegisterInfo): IntentSender { registerInfo: RegisterInfo): PendingIntent {
return PendingIntent.getActivity(context, 0, return PendingIntent.getActivity(context, 0,
Intent(context, AutofillLauncherActivity::class.java).apply { Intent(context, AutofillLauncherActivity::class.java).apply {
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION) EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
putExtra(KEY_REGISTER_INFO, registerInfo) putExtra(KEY_REGISTER_INFO, registerInfo)
}, },
PendingIntent.FLAG_CANCEL_CURRENT).intentSender PendingIntent.FLAG_CANCEL_CURRENT)
} }
fun launchForRegistration(context: Context, fun launchForRegistration(context: Context,

View File

@@ -81,6 +81,7 @@ class EntryActivity : DatabaseLockActivity() {
private var mHistoryPosition: Int = -1 private var mHistoryPosition: Int = -1
private var mEntryIsHistory: Boolean = false private var mEntryIsHistory: Boolean = false
private var mUrl: String? = null private var mUrl: String? = null
private var mEntryLoaded = false
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap() private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
@@ -119,11 +120,12 @@ class EntryActivity : DatabaseLockActivity() {
// Get Entry from UUID // Get Entry from UUID
try { try {
intent.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { entryId -> intent.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { mainEntryId ->
mMainEntryId = entryId
intent.removeExtra(KEY_ENTRY) 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) intent.removeExtra(KEY_ENTRY_HISTORY_POSITION)
mEntryViewModel.loadEntry(mDatabase, mainEntryId, historyPosition)
} }
} catch (e: ClassCastException) { } catch (e: ClassCastException) {
Log.e(TAG, "Unable to retrieve the entry key") Log.e(TAG, "Unable to retrieve the entry key")
@@ -138,53 +140,53 @@ class EntryActivity : DatabaseLockActivity() {
lockAndExit() lockAndExit()
} }
mEntryViewModel.mainEntryId.observe(this) { mainEntryId -> mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory ->
this.mMainEntryId = mainEntryId if (entryInfoHistory != null) {
invalidateOptionsMenu() this.mMainEntryId = entryInfoHistory.mainEntryId
}
mEntryViewModel.historyPosition.observe(this) { historyPosition -> // Manage history position
this.mHistoryPosition = historyPosition val historyPosition = entryInfoHistory.historyPosition
val entryIsHistory = historyPosition > -1 this.mHistoryPosition = historyPosition
this.mEntryIsHistory = entryIsHistory val entryIsHistory = historyPosition > -1
// Assign history dedicated view this.mEntryIsHistory = entryIsHistory
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE // Assign history dedicated view
if (entryIsHistory) { historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent)) if (entryIsHistory) {
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK)) val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
taColorAccent.recycle() collapsingToolbarLayout?.contentScrim =
} ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
invalidateOptionsMenu() taColorAccent.recycle()
}
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)
} }
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 // Refresh Menu
invalidateOptionsMenu() invalidateOptionsMenu()
loadingView?.hideByFading()
} }
mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement -> mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement ->
@@ -235,7 +237,7 @@ class EntryActivity : DatabaseLockActivity() {
override fun onDatabaseRetrieved(database: Database?) { override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
mEntryViewModel.loadEntry(mDatabase, mMainEntryId, mHistoryPosition) mEntryViewModel.loadDatabase(database)
// Assign title icon // Assign title icon
mIcon?.let { icon -> mIcon?.let { icon ->
@@ -294,7 +296,7 @@ class EntryActivity : DatabaseLockActivity() {
when (requestCode) { when (requestCode) {
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> { EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
// Reload the current id from database // 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 { override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
if (mEntryLoaded) {
val inflater = menuInflater
MenuUtil.contributionMenuInflater(inflater, menu)
val inflater = menuInflater inflater.inflate(R.menu.entry, menu)
MenuUtil.contributionMenuInflater(inflater, menu) inflater.inflate(R.menu.database, menu)
inflater.inflate(R.menu.entry, menu) if (mEntryIsHistory && !mDatabaseReadOnly) {
inflater.inflate(R.menu.database, menu) inflater.inflate(R.menu.entry_history, menu)
}
if (mUrl?.isEmpty() != false) { // Show education views
menu.findItem(R.id.menu_goto_url)?.isVisible = false Handler(Looper.getMainLooper()).post {
performedNextEducation(
EntryActivityEducation(
this
), menu
)
}
} }
return true
}
if (mEntryIsHistory && !mDatabaseReadOnly) { override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
inflater.inflate(R.menu.entry_history, menu) if (mUrl?.isEmpty() != false) {
menu?.findItem(R.id.menu_goto_url)?.isVisible = false
} }
if (mEntryIsHistory || mDatabaseReadOnly) { if (mEntryIsHistory || mDatabaseReadOnly) {
menu.findItem(R.id.menu_save_database)?.isVisible = false menu?.findItem(R.id.menu_save_database)?.isVisible = false
menu.findItem(R.id.menu_edit)?.isVisible = false menu?.findItem(R.id.menu_edit)?.isVisible = false
} }
if (mSpecialMode != SpecialMode.DEFAULT) { if (mSpecialMode != SpecialMode.DEFAULT) {
menu.findItem(R.id.menu_reload_database)?.isVisible = false menu?.findItem(R.id.menu_reload_database)?.isVisible = false
} }
return super.onPrepareOptionsMenu(menu)
// Show education views
Handler(Looper.getMainLooper()).post { performedNextEducation(EntryActivityEducation(this), menu) }
return true
} }
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation, 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.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
import com.kunzisoft.keepass.app.database.IOActionTask
import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.element.* 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.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.template.* import com.kunzisoft.keepass.database.element.template.*
@@ -95,15 +92,10 @@ class EntryEditActivity : DatabaseLockActivity(),
private var lockView: View? = null private var lockView: View? = null
private var loadingView: ProgressBar? = 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 val mEntryEditViewModel: EntryEditViewModel by viewModels()
private var mParent: Group? = null private var mTemplate: Template? = null
private var mEntry: Entry? = null
private var mIsTemplate: Boolean = false private var mIsTemplate: Boolean = false
private var mEntryLoaded: Boolean = false
private var mAllowCustomFields = false private var mAllowCustomFields = false
private var mAllowOTP = false private var mAllowOTP = false
@@ -138,22 +130,27 @@ class EntryEditActivity : DatabaseLockActivity(),
stopService(Intent(this, ClipboardEntryNotificationService::class.java)) stopService(Intent(this, ClipboardEntryNotificationService::class.java))
stopService(Intent(this, KeyboardEntryNotificationService::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 // Entry is retrieve, it's an entry to update
var entryId: NodeId<UUID>? = null
intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let { entryToUpdate -> intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let { entryToUpdate ->
//intent.removeExtra(KEY_ENTRY) intent.removeExtra(KEY_ENTRY)
mEntryId = entryToUpdate entryId = entryToUpdate
} }
// Parent is retrieve, it's a new entry to create // Parent is retrieve, it's a new entry to create
var parentId: NodeId<*>? = null
intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let { parent -> intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let { parent ->
//intent.removeExtra(KEY_PARENT) intent.removeExtra(KEY_PARENT)
mParentId = parent parentId = parent
} }
retrieveEntry(mDatabase) mEntryEditViewModel.loadTemplateEntry(
mDatabase,
entryId,
parentId,
EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent),
EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
)
// To retrieve attachment // To retrieve attachment
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
@@ -166,38 +163,52 @@ class EntryEditActivity : DatabaseLockActivity(),
// Save button // Save button
validateButton?.setOnClickListener { saveEntry() } validateButton?.setOnClickListener { saveEntry() }
mEntryEditViewModel.templatesEntry.observe(this) { templatesEntry -> mEntryEditViewModel.onTemplateChanged.observe(this) { template ->
// Change template dynamically this.mTemplate = template
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])
}
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 // View model listeners
@@ -309,78 +320,7 @@ class EntryEditActivity : DatabaseLockActivity(),
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
mAllowCustomFields = database?.allowEntryCustomFields() == true mAllowCustomFields = database?.allowEntryCustomFields() == true
mAllowOTP = database?.allowOTP == true mAllowOTP = database?.allowOTP == true
retrieveEntry(database) mEntryEditViewModel.loadDatabase(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
}
}
} }
override fun onDatabaseActionFinished( override fun onDatabaseActionFinished(
@@ -571,29 +511,33 @@ class EntryEditActivity : DatabaseLockActivity(),
*/ */
private fun saveEntry() { private fun saveEntry() {
mAttachmentFileBinderManager?.stopUploadAllAttachments() mAttachmentFileBinderManager?.stopUploadAllAttachments()
mEntryEditViewModel.requestEntryInfoUpdate(mDatabase, mEntry, mParent) mEntryEditViewModel.requestEntryInfoUpdate(mDatabase)
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu) 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 return true
} }
override fun onPrepareOptionsMenu(menu: Menu?): Boolean { override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
menu?.findItem(R.id.menu_add_field)?.apply { menu?.findItem(R.id.menu_add_field)?.apply {
isEnabled = mAllowCustomFields isEnabled = mAllowCustomFields
isVisible = isEnabled isVisible = isEnabled
} }
menu?.findItem(R.id.menu_add_attachment)?.apply { menu?.findItem(R.id.menu_add_attachment)?.apply {
// Attachment not compatible below KitKat // Attachment not compatible below KitKat
isEnabled = !mIsTemplate isEnabled = !mIsTemplate
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
isVisible = isEnabled isVisible = isEnabled
} }
menu?.findItem(R.id.menu_add_otp)?.apply { menu?.findItem(R.id.menu_add_otp)?.apply {
// OTP not compatible below KitKat // OTP not compatible below KitKat
isEnabled = mAllowOTP isEnabled = mAllowOTP
@@ -601,14 +545,10 @@ class EntryEditActivity : DatabaseLockActivity(),
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
isVisible = isEnabled isVisible = isEnabled
} }
entryEditActivityEducation?.let {
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
}
return super.onPrepareOptionsMenu(menu) return super.onPrepareOptionsMenu(menu)
} }
fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) { private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
val entryEditFragment = supportFragmentManager.findFragmentById(R.id.entry_edit_content) val entryEditFragment = supportFragmentManager.findFragmentById(R.id.entry_edit_content)
as? EntryEditFragment? as? EntryEditFragment?

View File

@@ -88,6 +88,12 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) 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) mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
setContentView(R.layout.activity_file_selection) setContentView(R.layout.activity_file_selection)
@@ -125,13 +131,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
} }
} }
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete -> mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
// Remove from app database
fileDatabaseHistoryToDelete.databaseUri?.let { databaseUri ->
UriUtil.releaseUriPermission(
contentResolver,
databaseUri
)
}
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete) databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
true true
} }

View File

@@ -487,7 +487,9 @@ class GroupActivity : DatabaseLockActivity(),
if (groupMetaView != null) { if (groupMetaView != null) {
val meta = group.nodeId.toString() val meta = group.nodeId.toString()
groupMetaView?.text = meta groupMetaView?.text = meta
if (meta.isNotEmpty() && PreferencesUtil.showUUID(this)) { if (meta.isNotEmpty()
&& !group.isVirtual
&& PreferencesUtil.showUUID(this)) {
groupMetaView?.visibility = View.VISIBLE groupMetaView?.visibility = View.VISIBLE
} else { } else {
groupMetaView?.visibility = View.GONE groupMetaView?.visibility = View.GONE
@@ -651,6 +653,8 @@ class GroupActivity : DatabaseLockActivity(),
Log.e(TAG, "Node can't be cast in Entry") Log.e(TAG, "Node can't be cast in Entry")
} }
} }
reloadGroupIfSearch()
} }
private fun entrySelectedForSave(database: Database, entry: Entry, searchInfo: SearchInfo) { private fun entrySelectedForSave(database: Database, entry: Entry, searchInfo: SearchInfo) {
@@ -736,6 +740,12 @@ class GroupActivity : DatabaseLockActivity(),
actionNodeMode?.finish() actionNodeMode?.finish()
} }
private fun reloadGroupIfSearch() {
if (Intent.ACTION_SEARCH == intent.action) {
reloadCurrentGroup()
}
}
override fun onNodeSelected( override fun onNodeSelected(
database: Database, database: Database,
nodes: List<Node> nodes: List<Node>
@@ -791,6 +801,7 @@ class GroupActivity : DatabaseLockActivity(),
(node as Entry).nodeId (node as Entry).nodeId
) )
} }
reloadGroupIfSearch()
return true return true
} }
@@ -845,6 +856,7 @@ class GroupActivity : DatabaseLockActivity(),
): Boolean { ): Boolean {
deleteNodes(nodes) deleteNodes(nodes)
finishNodeAction() finishNodeAction()
reloadGroupIfSearch()
return true return true
} }
@@ -932,9 +944,7 @@ class GroupActivity : DatabaseLockActivity(),
) { ) {
// If no node, show education to add new one // If no node, show education to add new one
val addNodeButtonEducationPerformed = mGroupFragment != null val addNodeButtonEducationPerformed = actionNodeMode == null
&& mGroupFragment!!.isEmpty
&& actionNodeMode == null
&& addNodeButtonView?.addButtonView != null && addNodeButtonView?.addButtonView != null
&& addNodeButtonView!!.isEnable && addNodeButtonView!!.isEnable
&& groupActivityEducation.checkAndPerformedAddNodeButtonEducation( && groupActivityEducation.checkAndPerformedAddNodeButtonEducation(
@@ -1093,7 +1103,7 @@ class GroupActivity : DatabaseLockActivity(),
try { try {
mGroupViewModel.loadGroup(mDatabase, mCurrentGroupState) mGroupViewModel.loadGroup(mDatabase, mCurrentGroupState)
} catch (e: Exception) { } 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.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database 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.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
@@ -55,6 +56,7 @@ class EntryEditFragment: DatabaseFragment() {
private lateinit var attachmentsListView: RecyclerView private lateinit var attachmentsListView: RecyclerView
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
private var mTemplate: Template? = null
private var mAllowMultipleAttachments: Boolean = false private var mAllowMultipleAttachments: Boolean = false
private var mIconColor: Int = 0 private var mIconColor: Int = 0
@@ -115,19 +117,26 @@ class EntryEditFragment: DatabaseFragment() {
} }
mEntryEditViewModel.onTemplateChanged.observe(viewLifecycleOwner) { template -> mEntryEditViewModel.onTemplateChanged.observe(viewLifecycleOwner) { template ->
this.mTemplate = template
templateView.setTemplate(template) templateView.setTemplate(template)
} }
mEntryEditViewModel.templatesEntry.observe(viewLifecycleOwner) { templateEntry -> mEntryEditViewModel.templatesEntry.observe(viewLifecycleOwner) { templateEntry ->
templateView.setTemplate(templateEntry.defaultTemplate) if (templateEntry != null) {
// Load entry info only the first time to keep change locally val selectedTemplate = if (mTemplate != null)
if (savedInstanceState == null) { mTemplate
assignEntryInfo(templateEntry.entryInfo) 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) { mEntryEditViewModel.requestEntryInfoUpdate.observe(viewLifecycleOwner) {

View File

@@ -91,15 +91,14 @@ class EntryFragment: DatabaseFragment() {
uuidView = view.findViewById(R.id.entry_UUID) uuidView = view.findViewById(R.id.entry_UUID)
uuidReferenceView = view.findViewById(R.id.entry_UUID_reference) uuidReferenceView = view.findViewById(R.id.entry_UUID_reference)
mEntryViewModel.template.observe(viewLifecycleOwner) { template -> mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory ->
templateView.setTemplate(template) if (entryInfoHistory != null) {
} templateView.setTemplate(entryInfoHistory.template)
assignEntryInfo(entryInfoHistory.entryInfo)
mEntryViewModel.entryInfo.observe(viewLifecycleOwner) { entryInfo -> // Smooth appearing
assignEntryInfo(entryInfo) rootView.showByFading()
// Smooth appearing resetAppTimeoutWhenViewFocusedOrChanged(rootView)
rootView.showByFading() }
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
} }
mEntryViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState -> mEntryViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState ->

View File

@@ -55,9 +55,11 @@ class EntryHistoryFragment: StylishFragment() {
* History * History
* ------------- * -------------
*/ */
private fun assignHistory(history: List<EntryInfo>) { private fun assignHistory(history: List<EntryInfo>?) {
historyAdapter?.clear() historyAdapter?.clear()
historyAdapter?.entryHistoryList?.addAll(history) history?.let {
historyAdapter?.entryHistoryList?.addAll(history)
}
historyAdapter?.onItemClickListener = { item, position -> historyAdapter?.onItemClickListener = { item, position ->
mEntryViewModel.onHistorySelected(item, position) mEntryViewModel.onHistorySelected(item, position)
} }

View File

@@ -74,9 +74,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
private var mRecycleBinEnable: Boolean = false private var mRecycleBinEnable: Boolean = false
private var mRecycleBin: Group? = null private var mRecycleBin: Group? = null
val isEmpty: Boolean
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() { private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState) super.onScrollStateChanged(recyclerView, newState)
@@ -233,7 +230,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
return mLayoutManager?.findFirstVisibleItemPosition() ?: 0 return mLayoutManager?.findFirstVisibleItemPosition() ?: 0
} }
@Throws(IllegalArgumentException::class)
private fun rebuildList() { private fun rebuildList() {
try { try {
// Add elements to the list // Add elements to the list
@@ -418,7 +414,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
mNodesRecyclerView?.scrollToPosition(position) 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.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database 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.model.MainCredential
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import java.util.*
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval { abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
@@ -28,7 +23,11 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
mDatabaseTaskProvider = DatabaseTaskProvider(this) mDatabaseTaskProvider = DatabaseTaskProvider(this)
mDatabaseTaskProvider?.onDatabaseRetrieved = { database -> 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) onDatabaseRetrieved(database)
} }
} }
@@ -69,17 +68,8 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
mDatabase?.clearAndClose(this) mDatabase?.clearAndClose(this)
} }
override fun reloadActivity() {
super.reloadActivity()
mDatabase?.wasReloaded = false
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (mDatabase?.wasReloaded == true) {
reloadActivity()
}
mDatabaseTaskProvider?.registerProgressTask() mDatabaseTaskProvider?.registerProgressTask()
} }

View File

@@ -28,6 +28,7 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView 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.Node
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
import com.kunzisoft.keepass.database.element.node.Type 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.OtpElement
import com.kunzisoft.keepass.otp.OtpType import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.setTextSize import com.kunzisoft.keepass.view.setTextSize
import com.kunzisoft.keepass.view.strikeOut import com.kunzisoft.keepass.view.strikeOut
import java.util.* 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 mCalculateViewTypeTextSize = Array(2) { true } // number of view type
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
private var mPrefSizeMultiplier: Float = 0F private var mPrefSizeMultiplier: Float = 0F
private var mSubtextDefaultDimension: Float = 0F private var mTextDefaultDimension: Float = 0F
private var mInfoTextDefaultDimension: Float = 0F private var mSubTextDefaultDimension: Float = 0F
private var mMetaTextDefaultDimension: Float = 0F
private var mOtpTokenTextDefaultDimension: Float = 0F
private var mNumberChildrenTextDefaultDimension: Float = 0F private var mNumberChildrenTextDefaultDimension: Float = 0F
private var mIconDefaultDimension: Float = 0F private var mIconDefaultDimension: Float = 0F
@@ -77,6 +82,7 @@ class NodeAdapter (private val context: Context,
private var mActionNodesList = LinkedList<Node>() private var mActionNodesList = LinkedList<Node>()
private var mNodeClickCallback: NodeClickCallback? = null private var mNodeClickCallback: NodeClickCallback? = null
private var mClipboardHelper = ClipboardHelper(context)
@ColorInt @ColorInt
private val mContentSelectionColor: Int private val mContentSelectionColor: Int
@@ -299,8 +305,10 @@ class NodeAdapter (private val context: Context,
mInflater.inflate(R.layout.item_list_nodes_entry, parent, false) mInflater.inflate(R.layout.item_list_nodes_entry, parent, false)
} }
val nodeViewHolder = NodeViewHolder(view) val nodeViewHolder = NodeViewHolder(view)
mInfoTextDefaultDimension = nodeViewHolder.text.textSize mTextDefaultDimension = nodeViewHolder.text.textSize
mSubtextDefaultDimension = nodeViewHolder.subText.textSize mSubTextDefaultDimension = nodeViewHolder.subText?.textSize ?: mSubTextDefaultDimension
mMetaTextDefaultDimension = nodeViewHolder.meta.textSize
mOtpTokenTextDefaultDimension = nodeViewHolder.otpToken?.textSize ?: mOtpTokenTextDefaultDimension
nodeViewHolder.numberChildren?.let { nodeViewHolder.numberChildren?.let {
mNumberChildrenTextDefaultDimension = it.textSize mNumberChildrenTextDefaultDimension = it.textSize
} }
@@ -311,7 +319,9 @@ class NodeAdapter (private val context: Context,
val subNode = mNodeSortedList.get(position) val subNode = mNodeSortedList.get(position)
// Node selection // Node selection
holder.container.isSelected = mActionNodesList.contains(subNode) holder.container.apply {
isSelected = mActionNodesList.contains(subNode)
}
// Assign image // Assign image
val iconColor = if (holder.container.isSelected) val iconColor = if (holder.container.isSelected)
@@ -333,19 +343,18 @@ class NodeAdapter (private val context: Context,
// Assign text // Assign text
holder.text.apply { holder.text.apply {
text = subNode.title text = subNode.title
setTextSize(mTextSizeUnit, mInfoTextDefaultDimension, mPrefSizeMultiplier) setTextSize(mTextSizeUnit, mTextDefaultDimension, mPrefSizeMultiplier)
strikeOut(subNode.isCurrentlyExpires) strikeOut(subNode.isCurrentlyExpires)
} }
// Add subText with username
holder.subText.apply {
text = ""
strikeOut(subNode.isCurrentlyExpires)
visibility = View.GONE
}
// Add meta text to show UUID // Add meta text to show UUID
holder.meta.apply { holder.meta.apply {
text = subNode.nodeId.toString() if (mShowUUID) {
visibility = if (mShowUUID) View.VISIBLE else View.GONE text = subNode.nodeId.toString()
setTextSize(mTextSizeUnit, mMetaTextDefaultDimension, mPrefSizeMultiplier)
visibility = View.VISIBLE
} else {
visibility = View.GONE
}
} }
// Specific elements for entry // Specific elements for entry
@@ -354,12 +363,16 @@ class NodeAdapter (private val context: Context,
database.startManageEntry(entry) database.startManageEntry(entry)
holder.text.text = entry.getVisualTitle() holder.text.text = entry.getVisualTitle()
holder.subText.apply { // Add subText with username
holder.subText?.apply {
val username = entry.username val username = entry.username
if (mShowUserNames && username.isNotEmpty()) { if (mShowUserNames && username.isNotEmpty()) {
visibility = View.VISIBLE visibility = View.VISIBLE
text = username 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 { 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 imageIdentifier: ImageView? = itemView.findViewById(R.id.node_image_identifier)
var icon: ImageView = itemView.findViewById(R.id.node_icon) var icon: ImageView = itemView.findViewById(R.id.node_icon)
var text: TextView = itemView.findViewById(R.id.node_text) 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 meta: TextView = itemView.findViewById(R.id.node_meta)
var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container) var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container)
var otpProgress: ProgressBar? = itemView.findViewById(R.id.node_otp_progress) var otpProgress: ProgressBar? = itemView.findViewById(R.id.node_otp_progress)

View File

@@ -189,26 +189,36 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
).execute() ).execute()
} }
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) { fun deleteKeyFileByDatabaseUri(databaseUri: Uri,
result: (() ->Unit)? = null) {
IOActionTask( IOActionTask(
{ {
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString()) databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
},
{
result?.invoke()
} }
).execute() ).execute()
} }
fun deleteAllKeyFiles() { fun deleteAllKeyFiles(result: (() ->Unit)? = null) {
IOActionTask( IOActionTask(
{ {
databaseFileHistoryDao.deleteAllKeyFiles() databaseFileHistoryDao.deleteAllKeyFiles()
},
{
result?.invoke()
} }
).execute() ).execute()
} }
fun deleteAll() { fun deleteAll(result: (() ->Unit)? = null) {
IOActionTask( IOActionTask(
{ {
databaseFileHistoryDao.deleteAll() databaseFileHistoryDao.deleteAll()
},
{
result?.invoke()
} }
).execute() ).execute()
} }

View File

@@ -25,6 +25,7 @@ import android.app.PendingIntent
import android.app.assist.AssistStructure import android.app.assist.AssistStructure
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentSender
import android.graphics.BlendMode import android.graphics.BlendMode
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.os.Build import android.os.Build
@@ -37,11 +38,13 @@ import android.view.autofill.AutofillValue
import android.view.inputmethod.InlineSuggestionsRequest import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.RemoteViews import android.widget.RemoteViews
import android.widget.Toast import android.widget.Toast
import android.widget.inline.InlinePresentationSpec
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.autofill.inline.UiVersions import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi import androidx.autofill.inline.v1.InlineSuggestionUi
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.database.element.Database 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.database.element.template.TemplateField
import com.kunzisoft.keepass.settings.AutofillSettingsActivity import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@@ -126,12 +128,14 @@ object AutofillHelper {
if (entryInfo.expires) { if (entryInfo.expires) {
val year = entryInfo.expiryTime.getYearInt() val year = entryInfo.expiryTime.getYearInt()
val month = entryInfo.expiryTime.getMonthInt() val month = entryInfo.expiryTime.getMonthInt()
val monthString = month.toString().padStart(2, '0')
val day = entryInfo.expiryTime.getDay() val day = entryInfo.expiryTime.getDay()
val dayString = day.toString().padStart(2, '0')
struct.creditCardExpirationDateId?.let { struct.creditCardExpirationDateId?.let {
if (struct.isWebView) { if (struct.isWebView) {
// set date string as defined in https://html.spec.whatwg.org // 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 { } else {
builder.setValue(it, AutofillValue.forDate(entryInfo.expiryTime.date.time)) builder.setValue(it, AutofillValue.forDate(entryInfo.expiryTime.date.time))
} }
@@ -157,24 +161,24 @@ object AutofillHelper {
} }
struct.creditCardExpirationMonthId?.let { struct.creditCardExpirationMonthId?.let {
if (struct.isWebView) { if (struct.isWebView) {
builder.setValue(it, AutofillValue.forText(month.toString())) builder.setValue(it, AutofillValue.forText(monthString))
} else { } else {
if (struct.creditCardExpirationMonthOptions != null) { if (struct.creditCardExpirationMonthOptions != null) {
// index starts at 0 // index starts at 0
builder.setValue(it, AutofillValue.forList(month - 1)) builder.setValue(it, AutofillValue.forList(month - 1))
} else { } else {
builder.setValue(it, AutofillValue.forText(month.toString())) builder.setValue(it, AutofillValue.forText(monthString))
} }
} }
} }
struct.creditCardExpirationDayId?.let { struct.creditCardExpirationDayId?.let {
if (struct.isWebView) { if (struct.isWebView) {
builder.setValue(it, AutofillValue.forText(day.toString())) builder.setValue(it, AutofillValue.forText(dayString))
} else { } else {
if (struct.creditCardExpirationDayOptions != null) { if (struct.creditCardExpirationDayOptions != null) {
builder.setValue(it, AutofillValue.forList(day - 1)) builder.setValue(it, AutofillValue.forList(day - 1))
} else { } else {
builder.setValue(it, AutofillValue.forText(day.toString())) builder.setValue(it, AutofillValue.forText(dayString))
} }
} }
} }
@@ -270,6 +274,27 @@ object AutofillHelper {
return null 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, fun buildResponse(context: Context,
database: Database, database: Database,
entriesInfo: List<EntryInfo>, entriesInfo: List<EntryInfo>,
@@ -293,17 +318,58 @@ object AutofillHelper {
} }
// Add inline suggestion for new IME and dataset // Add inline suggestion for new IME and dataset
entriesInfo.forEachIndexed { index, entryInfo -> var numberInlineSuggestions = 0
val inlinePresentation = inlineSuggestionsRequest?.let { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { inlineSuggestionsRequest?.let {
buildInlinePresentationForEntry(context, database, inlineSuggestionsRequest, index, entryInfo) numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
} else { if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
null 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) items, parseResult, inlineSuggestionsRequest)
) )
}, },
{ { openedDatabase ->
// Show UI if no search result // Show UI if no search result
showUIForEntrySelection(parseResult, showUIForEntrySelection(parseResult, openedDatabase,
searchInfo, inlineSuggestionsRequest, callback) searchInfo, inlineSuggestionsRequest, callback)
}, },
{ {
// Show UI if database not open // Show UI if database not open
showUIForEntrySelection(parseResult, showUIForEntrySelection(parseResult, null,
searchInfo, inlineSuggestionsRequest, callback) searchInfo, inlineSuggestionsRequest, callback)
} }
) )
@@ -153,6 +153,7 @@ class KeeAutofillService : AutofillService() {
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private fun showUIForEntrySelection(parseResult: StructureParser.Result, private fun showUIForEntrySelection(parseResult: StructureParser.Result,
database: Database?,
searchInfo: SearchInfo, searchInfo: SearchInfo,
inlineSuggestionsRequest: InlineSuggestionsRequest?, inlineSuggestionsRequest: InlineSuggestionsRequest?,
callback: FillCallback) { callback: FillCallback) {
@@ -160,19 +161,51 @@ class KeeAutofillService : AutofillService() {
if (autofillIds.isNotEmpty()) { if (autofillIds.isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used // If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response. // to generate Response.
val intentSender = AutofillLauncherActivity.getAuthIntentSenderForSelection(this, val intentSender = AutofillLauncherActivity.getPendingIntentForSelection(this,
searchInfo, inlineSuggestionsRequest) searchInfo, inlineSuggestionsRequest).intentSender
val responseBuilder = FillResponse.Builder() val responseBuilder = FillResponse.Builder()
val remoteViewsUnlock: RemoteViews = if (!parseResult.webDomain.isNullOrEmpty()) { val remoteViewsUnlock: RemoteViews = if (database == null) {
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply { if (!parseResult.webDomain.isNullOrEmpty()) {
setTextViewText(R.id.autofill_web_domain_text, parseResult.webDomain) RemoteViews(
} packageName,
} else if (!parseResult.applicationId.isNullOrEmpty()) { R.layout.item_autofill_unlock_web_domain
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply { ).apply {
setTextViewText(R.id.autofill_app_id_text, parseResult.applicationId) 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 { } 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 // 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] applicationId = windowNode.title.toString().split("/")[0]
Log.d(TAG, "Autofill applicationId: $applicationId") Log.d(TAG, "Autofill applicationId: $applicationId")
if (parseViewNode(windowNode.rootViewNode)) if (applicationId?.contains("PopupWindow:") == false) {
break@mainLoop if (parseViewNode(windowNode.rootViewNode))
break@mainLoop
}
} }
// If not explicit username field found, add the field just before password field. // If not explicit username field found, add the field just before password field.
if (usernameId == null && passwordId != null && usernameIdCandidate != null) { if (usernameId == null && passwordId != null && usernameIdCandidate != null) {
@@ -143,19 +145,19 @@ class StructureParser(private val structure: AssistStructure) {
Log.d(TAG, "Autofill password hint") Log.d(TAG, "Autofill password hint")
return true return true
} }
it.contains("cc-name", true) -> { it.equals("cc-name", true) -> {
Log.d(TAG, "Autofill credit card name hint") Log.d(TAG, "Autofill credit card name hint")
result?.creditCardHolderId = autofillId result?.creditCardHolderId = autofillId
result?.creditCardHolder = node.autofillValue?.textValue?.toString() result?.creditCardHolder = node.autofillValue?.textValue?.toString()
} }
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_NUMBER, true) 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") Log.d(TAG, "Autofill credit card number hint")
result?.creditCardNumberId = autofillId result?.creditCardNumberId = autofillId
result?.creditCardNumber = node.autofillValue?.textValue?.toString() result?.creditCardNumber = node.autofillValue?.textValue?.toString()
} }
// expect date string as defined in https://html.spec.whatwg.org, e.g. 2014-12 // 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") Log.d(TAG, "Autofill credit card expiration date hint")
result?.creditCardExpirationDateId = autofillId result?.creditCardExpirationDateId = autofillId
node.autofillValue?.let { value -> 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(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") Log.d(TAG, "Autofill credit card expiration year hint")
result?.creditCardExpirationYearId = autofillId result?.creditCardExpirationYearId = autofillId
if (node.autofillOptions != null) { 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(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") Log.d(TAG, "Autofill credit card expiration month hint")
result?.creditCardExpirationMonthId = autofillId result?.creditCardExpirationMonthId = autofillId
if (node.autofillOptions != null) { 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(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") Log.d(TAG, "Autofill credit card expiration day hint")
result?.creditCardExpirationDayId = autofillId result?.creditCardExpirationDayId = autofillId
if (node.autofillOptions != null) { if (node.autofillOptions != null) {
@@ -399,7 +401,6 @@ class StructureParser(private val structure: AssistStructure) {
class Result { class Result {
var isWebView: Boolean = false var isWebView: Boolean = false
var applicationId: String? = null var applicationId: String? = null
var webDomain: String? = null var webDomain: String? = null
set(value) { set(value) {
if (field == null) if (field == null)

View File

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

View File

@@ -106,7 +106,7 @@ class SearchHelper {
} else if (TimeoutHelper.checkTime(context)) { } else if (TimeoutHelper.checkTime(context)) {
var searchWithoutUI = false var searchWithoutUI = false
if (PreferencesUtil.isAutofillAutoSearchEnable(context) if (PreferencesUtil.isAutofillAutoSearchEnable(context)
&& searchInfo != null && searchInfo != null && !searchInfo.manualSelection
&& !searchInfo.containsOnlyNullValues()) { && !searchInfo.containsOnlyNullValues()) {
// If search provide results // If search provide results
database.createVirtualGroupFromSearchInfo( database.createVirtualGroupFromSearchInfo(

View File

@@ -178,6 +178,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
assignKeyboardView() assignKeyboardView()
} }
override fun onEvaluateFullscreenMode(): Boolean {
return resources.getBoolean(R.bool.magikeyboard_allow_fullscreen_mode)
&& super.onEvaluateFullscreenMode()
}
private fun playVibration(keyCode: Int) { private fun playVibration(keyCode: Int) {
when (keyCode) { when (keyCode) {
Keyboard.KEYCODE_DELETE -> {} Keyboard.KEYCODE_DELETE -> {}
@@ -267,7 +272,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
if (entryInfoKey != null) { if (entryInfoKey != null) {
currentInputConnection.commitText(entryInfoKey!!.url, 1) currentInputConnection.commitText(entryInfoKey!!.url, 1)
} }
actionTabAutomatically() actionGoAutomatically()
} }
KEY_FIELDS -> { KEY_FIELDS -> {
if (entryInfoKey != null) { 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 { companion object {
const val WEB_DOMAIN_FIELD_NAME = "URL" const val WEB_DOMAIN_FIELD_NAME = "URL"

View File

@@ -24,6 +24,22 @@ class GroupInfo : NodeInfo {
parcel.writeString(notes) 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> { companion object CREATOR : Parcelable.Creator<GroupInfo> {
override fun createFromParcel(parcel: Parcel): GroupInfo { override fun createFromParcel(parcel: Parcel): GroupInfo {
return GroupInfo(parcel) return GroupInfo(parcel)

View File

@@ -36,6 +36,30 @@ open class NodeInfo() : Parcelable {
return 0 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> { companion object CREATOR : Parcelable.Creator<NodeInfo> {
override fun createFromParcel(parcel: Parcel): NodeInfo { override fun createFromParcel(parcel: Parcel): NodeInfo {
return NodeInfo(parcel) return NodeInfo(parcel)

View File

@@ -14,7 +14,7 @@ import kotlinx.coroutines.launch
import mozilla.components.lib.publicsuffixlist.PublicSuffixList import mozilla.components.lib.publicsuffixlist.PublicSuffixList
class SearchInfo : ObjectNameResource, Parcelable { class SearchInfo : ObjectNameResource, Parcelable {
var manualSelection: Boolean = false
var applicationId: String? = null var applicationId: String? = null
set(value) { set(value) {
field = when { field = when {
@@ -42,6 +42,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
constructor() constructor()
constructor(toCopy: SearchInfo?) { constructor(toCopy: SearchInfo?) {
manualSelection = toCopy?.manualSelection ?: manualSelection
applicationId = toCopy?.applicationId applicationId = toCopy?.applicationId
webDomain = toCopy?.webDomain webDomain = toCopy?.webDomain
webScheme = toCopy?.webScheme webScheme = toCopy?.webScheme
@@ -49,6 +50,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
} }
private constructor(parcel: Parcel) { private constructor(parcel: Parcel) {
manualSelection = parcel.readByte().toInt() != 0
val readAppId = parcel.readString() val readAppId = parcel.readString()
applicationId = if (readAppId.isNullOrEmpty()) null else readAppId applicationId = if (readAppId.isNullOrEmpty()) null else readAppId
val readDomain = parcel.readString() val readDomain = parcel.readString()
@@ -64,6 +66,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
} }
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeByte((if (manualSelection) 1 else 0).toByte())
parcel.writeString(applicationId ?: "") parcel.writeString(applicationId ?: "")
parcel.writeString(webDomain ?: "") parcel.writeString(webDomain ?: "")
parcel.writeString(webScheme ?: "") parcel.writeString(webScheme ?: "")
@@ -88,10 +91,9 @@ class SearchInfo : ObjectNameResource, Parcelable {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (other !is SearchInfo) return false
other as SearchInfo
if (manualSelection != other.manualSelection) return false
if (applicationId != other.applicationId) return false if (applicationId != other.applicationId) return false
if (webDomain != other.webDomain) return false if (webDomain != other.webDomain) return false
if (webScheme != other.webScheme) return false if (webScheme != other.webScheme) return false
@@ -101,7 +103,8 @@ class SearchInfo : ObjectNameResource, Parcelable {
} }
override fun hashCode(): Int { 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 + (webDomain?.hashCode() ?: 0)
result = 31 * result + (webScheme?.hashCode() ?: 0) result = 31 * result + (webScheme?.hashCode() ?: 0)
result = 31 * result + (otpString?.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

@@ -81,14 +81,18 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
activity?.let { activity -> activity?.let { activity ->
findPreference<Preference>(getString(R.string.remember_database_locations_key))?.setOnPreferenceChangeListener { _, newValue -> findPreference<Preference>(getString(R.string.remember_database_locations_key))?.setOnPreferenceChangeListener { _, newValue ->
if (!(newValue as Boolean)) { if (!(newValue as Boolean)) {
FileDatabaseHistoryAction.getInstance(activity.applicationContext).deleteAll() FileDatabaseHistoryAction.getInstance(activity.applicationContext).deleteAll {
UriUtil.releaseAllUnnecessaryPermissionUris(activity.applicationContext)
}
} }
true true
} }
findPreference<Preference>(getString(R.string.remember_keyfile_locations_key))?.setOnPreferenceChangeListener { _, newValue -> findPreference<Preference>(getString(R.string.remember_keyfile_locations_key))?.setOnPreferenceChangeListener { _, newValue ->
if (!(newValue as Boolean)) { if (!(newValue as Boolean)) {
FileDatabaseHistoryAction.getInstance(activity.applicationContext).deleteAllKeyFiles() FileDatabaseHistoryAction.getInstance(activity.applicationContext).deleteAllKeyFiles {
UriUtil.releaseAllUnnecessaryPermissionUris(activity.applicationContext)
}
} }
true true
} }

View File

@@ -493,6 +493,12 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.autofill_inline_suggestions_default)) 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 { fun isAutofillSaveSearchInfoEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.autofill_save_search_info_key), 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_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_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_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_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_ask_to_save_data_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_application_id_blocklist_key) -> editor.putStringSet(name, getStringSetFromProperties(value)) 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) { private fun durationToDaysHoursMinutesSeconds(duration: Long) {
if (duration < 0) { if (duration < 0) {
mEnabled = false
mDays = 0 mDays = 0
mHours = 0 mHours = 0
mMinutes = 0 mMinutes = 0
mSeconds = 0 mSeconds = 0
} else { } else {
mEnabled = true
mDays = (duration / (24L * 60L * 60L * 1000L)).toInt() mDays = (duration / (24L * 60L * 60L * 1000L)).toInt()
val daysMilliseconds = mDays * 24L * 60L * 60L * 1000L val daysMilliseconds = mDays * 24L * 60L * 60L * 1000L
mHours = ((duration - daysMilliseconds) / (60L * 60L * 1000L)).toInt() mHours = ((duration - daysMilliseconds) / (60L * 60L * 1000L)).toInt()
@@ -127,7 +129,7 @@ class DurationDialogFragmentCompat : InputPreferenceDialogFragmentCompat() {
setSwitchAction({ isChecked -> setSwitchAction({ isChecked ->
mEnabled = isChecked mEnabled = isChecked
}, mDays + mHours + mMinutes + mSeconds > 0) }, mEnabled)
assignValuesInViews() assignValuesInViews()
} }

View File

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

View File

@@ -30,7 +30,6 @@ import android.content.IntentFilter
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
@@ -142,19 +141,5 @@ fun Context.closeDatabase(database: Database?) {
database?.clearAndClose(this) database?.clearAndClose(this)
// Release not useful URI permission // Release not useful URI permission
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { UriUtil.releaseAllUnnecessaryPermissionUris(applicationContext)
applicationContext?.let { appContext ->
val fileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(appContext)
fileDatabaseHistoryAction.getDatabaseFileList { databaseFileList ->
val listToNotRemove = databaseFileList.map { it.databaseUri }
// Remove URI permission for not database files
val resolver = appContext.contentResolver
resolver.persistedUriPermissions.forEach { uriPermission ->
val uri = uriPermission.uri
if (!listToNotRemove.contains(uri))
UriUtil.releaseUriPermission(resolver, uri)
}
}
}
}
} }

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

@@ -29,6 +29,7 @@ import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import java.io.* import java.io.*
import java.util.* import java.util.*
@@ -187,6 +188,32 @@ object UriUtil {
persistUriPermission(contentResolver, uri, release = true, readOnly = false) persistUriPermission(contentResolver, uri, release = true, readOnly = false)
} }
fun releaseAllUnnecessaryPermissionUris(applicationContext: Context?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
applicationContext?.let { appContext ->
val fileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(appContext)
fileDatabaseHistoryAction.getDatabaseFileList { databaseFileList ->
val listToNotRemove = mutableListOf<Uri>()
databaseFileList.forEach {
it.databaseUri?.let { databaseUri ->
listToNotRemove.add(databaseUri)
}
it.keyFileUri?.let { keyFileUri ->
listToNotRemove.add(keyFileUri)
}
}
// Remove URI permission for not database files
val resolver = appContext.contentResolver
resolver.persistedUriPermissions.forEach { uriPermission ->
val uri = uriPermission.uri
if (!listToNotRemove.contains(uri))
releaseUriPermission(resolver, uri)
}
}
}
}
}
fun getUriFromIntent(intent: Intent, key: String): Uri? { fun getUriFromIntent(intent: Intent, key: String): Uri? {
try { try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {

View File

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

View File

@@ -275,22 +275,26 @@ abstract class TemplateAbstractView<
templateAttribute: TemplateAttribute, templateAttribute: TemplateAttribute,
entryInfoValue: String, entryInfoValue: String,
showEmptyFields: Boolean) { showEmptyFields: Boolean) {
var fieldView: TEntryFieldView? = findViewWithTag(fieldTag) try {
if (!showEmptyFields && entryInfoValue.isEmpty()) { var fieldView: TEntryFieldView? = findViewWithTag(fieldTag)
fieldView?.isFieldVisible = false if (!showEmptyFields && entryInfoValue.isEmpty()) {
} else if (fieldView == null && entryInfoValue.isNotEmpty()) { fieldView?.isFieldVisible = false
// Add new not referenced view if standard field not in template } else if (fieldView == null && entryInfoValue.isNotEmpty()) {
fieldView = buildViewForNotReferencedField( // Add new not referenced view if standard field not in template
Field(templateAttribute.label, fieldView = buildViewForNotReferencedField(
ProtectedString(templateAttribute.protected, "")), Field(templateAttribute.label,
templateAttribute ProtectedString(templateAttribute.protected, "")),
) as? TEntryFieldView? templateAttribute
fieldView?.let { ) as? TEntryFieldView?
addNotReferencedView(it as View) 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") @Suppress("UNCHECKED_CAST")
@@ -299,22 +303,25 @@ abstract class TemplateAbstractView<
expires: Boolean, expires: Boolean,
expiryTime: DateInstant, expiryTime: DateInstant,
showEmptyFields: Boolean) { showEmptyFields: Boolean) {
try {
var fieldView: TDateTimeView? = findViewWithTag(fieldTag) var fieldView: TDateTimeView? = findViewWithTag(fieldTag)
if (!showEmptyFields && !expires) { if (!showEmptyFields && !expires) {
fieldView?.isFieldVisible = false fieldView?.isFieldVisible = false
} else if (fieldView == null && expires) { } else if (fieldView == null && expires) {
fieldView = buildViewForNotReferencedField( fieldView = buildViewForNotReferencedField(
Field(templateAttribute.label, Field(templateAttribute.label,
ProtectedString(templateAttribute.protected, "")), ProtectedString(templateAttribute.protected, "")),
templateAttribute templateAttribute
) as? TDateTimeView? ) as? TDateTimeView?
fieldView?.let { fieldView?.let {
addNotReferencedView(it as View) 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

@@ -62,7 +62,8 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
field: Field): TextEditFieldView? { field: Field): TextEditFieldView? {
return context?.let { return context?.let {
TextEditFieldView(it).apply { TextEditFieldView(it).apply {
setProtection(field.protectedValue.isProtected, mHideProtectedValue) // hiddenProtectedValue (mHideProtectedValue) don't work with TextInputLayout
setProtection(field.protectedValue.isProtected)
setMaxChars(templateAttribute.options.getNumberChars()) setMaxChars(templateAttribute.options.getNumberChars())
setMaxLines(templateAttribute.options.getNumberLines()) setMaxLines(templateAttribute.options.getNumberLines())
setActionClick(templateAttribute, field, this) setActionClick(templateAttribute, field, this)

View File

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

View File

@@ -161,8 +161,7 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
} }
} }
fun setProtection(protection: Boolean, hiddenProtectedValue: Boolean) { fun setProtection(protection: Boolean) {
// hiddenProtectedValue don't work with TextInputLayout
if (protection) { if (protection) {
labelView.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE labelView.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
valueView.inputType = valueView.inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD valueView.inputType = valueView.inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD
@@ -188,6 +187,6 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
companion object { companion object {
const val MAX_CHARS_LIMIT = Integer.MAX_VALUE 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) { fun setProtection(protection: Boolean, hiddenProtectedValue: Boolean = false) {
showButton.isVisible = protection showButton.isVisible = protection
showButton.isSelected = hiddenProtectedValue showButton.isSelected = hiddenProtectedValue
@@ -343,6 +331,5 @@ class TextFieldView @JvmOverloads constructor(context: Context,
companion object { companion object {
const val MAX_CHARS_LIMIT = Integer.MAX_VALUE const val MAX_CHARS_LIMIT = Integer.MAX_VALUE
const val MAX_LINES_LIMIT = 40
} }
} }

View File

@@ -110,6 +110,21 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
fun deleteDatabaseFile(databaseFileToDelete: DatabaseFile) { fun deleteDatabaseFile(databaseFileToDelete: DatabaseFile) {
mFileDatabaseHistoryAction?.deleteDatabaseFile(databaseFileToDelete) { databaseFileDeleted -> mFileDatabaseHistoryAction?.deleteDatabaseFile(databaseFileToDelete) { databaseFileDeleted ->
databaseFileDeleted?.let { _ -> databaseFileDeleted?.let { _ ->
// Release database and keyfile URIs permissions
val contentResolver = getApplication<App>().applicationContext.contentResolver
databaseFileDeleted.databaseUri?.let { databaseUri ->
UriUtil.releaseUriPermission(
contentResolver,
databaseUri
)
}
databaseFileDeleted.keyFileUri?.let { keyFileUri ->
UriUtil.releaseUriPermission(
contentResolver,
keyFileUri
)
}
// Call the feedback
databaseFilesLoaded.value = getDatabaseFilesLoadedValue().apply { databaseFilesLoaded.value = getDatabaseFilesLoadedValue().apply {
databaseFileAction = DatabaseFileAction.DELETE databaseFileAction = DatabaseFileAction.DELETE
databaseFileToActivate = databaseFileDeleted databaseFileToActivate = databaseFileDeleted

View File

@@ -5,17 +5,28 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.kunzisoft.keepass.app.database.IOActionTask import com.kunzisoft.keepass.app.database.IOActionTask
import com.kunzisoft.keepass.database.element.* 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.database.element.template.Template
import com.kunzisoft.keepass.model.* import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
import java.util.*
class EntryEditViewModel: NodeEditViewModel() { 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>() private val mTempAttachments = mutableListOf<EntryAttachmentState>()
val templatesEntry : LiveData<TemplatesEntry> get() = _templatesEntry val templatesEntry : LiveData<TemplatesEntry?> get() = _templatesEntry
private val _templatesEntry = MutableLiveData<TemplatesEntry>() private val _templatesEntry = MutableLiveData<TemplatesEntry?>()
val requestEntryInfoUpdate : LiveData<EntryUpdate> get() = _requestEntryInfoUpdate val requestEntryInfoUpdate : LiveData<EntryUpdate> get() = _requestEntryInfoUpdate
private val _requestEntryInfoUpdate = SingleLiveEvent<EntryUpdate>() private val _requestEntryInfoUpdate = SingleLiveEvent<EntryUpdate>()
@@ -23,7 +34,7 @@ class EntryEditViewModel: NodeEditViewModel() {
private val _onEntrySaved = SingleLiveEvent<EntrySave>() private val _onEntrySaved = SingleLiveEvent<EntrySave>()
val onTemplateChanged : LiveData<Template> get() = _onTemplateChanged val onTemplateChanged : LiveData<Template> get() = _onTemplateChanged
private val _onTemplateChanged = SingleLiveEvent<Template>() private val _onTemplateChanged = MutableLiveData<Template>()
val requestPasswordSelection : LiveData<Field> get() = _requestPasswordSelection val requestPasswordSelection : LiveData<Field> get() = _requestPasswordSelection
private val _requestPasswordSelection = SingleLiveEvent<Field>() private val _requestPasswordSelection = SingleLiveEvent<Field>()
@@ -53,39 +64,118 @@ class EntryEditViewModel: NodeEditViewModel() {
val onBinaryPreviewLoaded : LiveData<AttachmentPosition> get() = _onBinaryPreviewLoaded val onBinaryPreviewLoaded : LiveData<AttachmentPosition> get() = _onBinaryPreviewLoaded
private val _onBinaryPreviewLoaded = SingleLiveEvent<AttachmentPosition>() private val _onBinaryPreviewLoaded = SingleLiveEvent<AttachmentPosition>()
fun loadDatabase(database: Database?) {
loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo, mSearchInfo)
}
fun loadTemplateEntry(database: Database, fun loadTemplateEntry(database: Database?,
entry: Entry?, entryId: NodeId<UUID>?,
isTemplate: Boolean, parentId: NodeId<*>?,
registerInfo: RegisterInfo?, registerInfo: RegisterInfo?,
searchInfo: SearchInfo?) { searchInfo: SearchInfo?) {
IOActionTask( this.mEntryId = entryId
{ this.mParentId = parentId
val templates = database.getTemplates(isTemplate) this.mRegisterInfo = registerInfo
val entryTemplate = entry?.let { database.getTemplate(it) } ?: Template.STANDARD this.mSearchInfo = searchInfo
var entryInfo: EntryInfo? = null
// Decode the entry / load entry info database?.let {
entry?.let { mEntryId?.let {
database.decodeEntryWithTemplateConfiguration(it).let { entry -> IOActionTask(
// Load entry info {
entry.getEntryInfo(database, true).let { tempEntryInfo -> // Create an Entry copy to modify from the database entry
// Retrieve data from registration mEntry = database.getEntryById(it)
(registerInfo?.searchInfo ?: searchInfo)?.let { tempSearchInfo -> // Retrieve the parent
tempEntryInfo.saveSearchInfo(database, tempSearchInfo) mEntry?.let { entry ->
// If no parent, add root group as parent
if (entry.parent == null) {
entry.parent = database.rootGroup
} }
registerInfo?.let { regInfo -> // Define if current entry is a template (in direct template group)
tempEntryInfo.saveRegisterInfo(database, regInfo) mIsTemplate = database.entryIsTemplate(mEntry)
} decodeTemplateEntry(
entryInfo = tempEntryInfo database,
entry,
mIsTemplate,
registerInfo,
searchInfo
)
} }
},
{ templatesEntry ->
mEntryId = null
_templatesEntry.value = templatesEntry
} }
} ).execute()
TemplatesEntry(templates, entryTemplate, entryInfo)
},
{ templatesEntry ->
_templatesEntry.value = templatesEntry
} }
).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) { fun changeTemplate(template: Template) {
@@ -94,8 +184,8 @@ class EntryEditViewModel: NodeEditViewModel() {
} }
} }
fun requestEntryInfoUpdate(database: Database?, entry: Entry?, parent: Group?) { fun requestEntryInfoUpdate(database: Database?) {
_requestEntryInfoUpdate.value = EntryUpdate(database, entry, parent) _requestEntryInfoUpdate.value = EntryUpdate(database, mEntry, mParent)
} }
fun saveEntryInfo(database: Database?, entry: Entry?, parent: Group?, entryInfo: EntryInfo) { fun saveEntryInfo(database: Database?, entry: Entry?, parent: Group?, entryInfo: EntryInfo) {
@@ -222,7 +312,10 @@ class EntryEditViewModel: NodeEditViewModel() {
_onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition) _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 EntryUpdate(val database: Database?, val entry: Entry?, val parent: Group?)
data class EntrySave(val oldEntry: Entry, val newEntry: 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?) data class FieldEdition(val oldField: Field?, val newField: Field?)

View File

@@ -36,20 +36,14 @@ import java.util.*
class EntryViewModel: ViewModel() { class EntryViewModel: ViewModel() {
val template : LiveData<Template> get() = _template private var mMainEntryId: NodeId<UUID>? = null
private val _template = MutableLiveData<Template>() private var mHistoryPosition: Int = -1
val mainEntryId : LiveData<NodeId<UUID>?> get() = _mainEntryId val entryInfoHistory : LiveData<EntryInfoHistory?> get() = _entryInfoHistory
private val _mainEntryId = MutableLiveData<NodeId<UUID>?>() private val _entryInfoHistory = MutableLiveData<EntryInfoHistory?>()
val historyPosition : LiveData<Int> get() = _historyPosition val entryHistory : LiveData<List<EntryInfo>?> get() = _entryHistory
private val _historyPosition = MutableLiveData<Int>() private val _entryHistory = MutableLiveData<List<EntryInfo>?>()
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 onOtpElementUpdated : LiveData<OtpElement?> get() = _onOtpElementUpdated val onOtpElementUpdated : LiveData<OtpElement?> get() = _onOtpElementUpdated
private val _onOtpElementUpdated = SingleLiveEvent<OtpElement?>() private val _onOtpElementUpdated = SingleLiveEvent<OtpElement?>()
@@ -62,11 +56,18 @@ class EntryViewModel: ViewModel() {
val historySelected : LiveData<EntryHistory> get() = _historySelected val historySelected : LiveData<EntryHistory> get() = _historySelected
private val _historySelected = SingleLiveEvent<EntryHistory>() private val _historySelected = SingleLiveEvent<EntryHistory>()
fun loadEntry(database: Database?, entryId: NodeId<UUID>?, historyPosition: Int) { fun loadDatabase(database: Database?) {
if (database != null && entryId != null) { 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( IOActionTask(
{ {
val mainEntry = database.getEntryById(entryId) val mainEntry = database.getEntryById(mainEntryId)
val currentEntry = if (historyPosition > -1) { val currentEntry = if (historyPosition > -1) {
mainEntry?.getHistory()?.get(historyPosition) mainEntry?.getHistory()?.get(historyPosition)
} else { } else {
@@ -91,6 +92,7 @@ class EntryViewModel: ViewModel() {
EntryInfoHistory( EntryInfoHistory(
mainEntry!!.nodeId, mainEntry!!.nodeId,
historyPosition,
entryTemplate, entryTemplate,
it.getEntryInfo(database), it.getEntryInfo(database),
entryInfoHistory entryInfoHistory
@@ -99,22 +101,13 @@ class EntryViewModel: ViewModel() {
} }
}, },
{ entryInfoHistory -> { entryInfoHistory ->
if (entryInfoHistory != null) { _entryInfoHistory.value = entryInfoHistory
_mainEntryId.value = entryInfoHistory.mainEntryId _entryHistory.value = entryInfoHistory?.entryHistory
_historyPosition.value = historyPosition
_template.value = entryInfoHistory.template
_entryInfo.value = entryInfoHistory.entryInfo
_entryHistory.value = entryInfoHistory.entryHistory
}
} }
).execute() ).execute()
} }
} }
fun updateEntry(database: Database?) {
loadEntry(database, _mainEntryId.value, _historyPosition.value ?: -1)
}
fun onOtpElementUpdated(optElement: OtpElement?) { fun onOtpElementUpdated(optElement: OtpElement?) {
_onOtpElementUpdated.value = optElement _onOtpElementUpdated.value = optElement
} }
@@ -132,6 +125,7 @@ class EntryViewModel: ViewModel() {
} }
data class EntryInfoHistory(var mainEntryId: NodeId<UUID>, data class EntryInfoHistory(var mainEntryId: NodeId<UUID>,
var historyPosition: Int,
val template: Template, val template: Template,
val entryInfo: EntryInfo, val entryInfo: EntryInfo,
val entryHistory: List<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" /> style="@style/KeepassDXStyle.TextAppearance.Info" />
</RelativeLayout> </RelativeLayout>
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:layout_gravity="start|center_vertical" android:layout_gravity="start|center_vertical"

View File

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

View File

@@ -23,15 +23,15 @@
<ImageView <ImageView
android:id="@+id/autofill_entry_icon" android:id="@+id/autofill_entry_icon"
android:layout_width="wrap_content" android:layout_width="24dp"
android:layout_height="wrap_content" android:layout_height="24dp"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginRight="12dp" android:layout_marginRight="12dp"
android:layout_marginEnd="12dp" android:layout_marginEnd="12dp"
android:layout_marginLeft="12dp" android:layout_marginLeft="12dp"
android:layout_marginStart="12dp" android:layout_marginStart="12dp"
android:contentDescription="@string/content_description_entry_icon" android:contentDescription="@string/content_description_entry_icon"
android:src="@drawable/ic_key_white_24dp" /> android:src="@drawable/ic_arrow_right_green_24dp" />
<TextView <TextView
android:id="@+id/autofill_entry_text" 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_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:minHeight="56dp" android:minHeight="48dp"
android:maxHeight="72dp"
app:layout_constraintWidth_percent="@dimen/content_percent" app:layout_constraintWidth_percent="@dimen/content_percent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
@@ -104,10 +103,12 @@
tools:text="7543A7EAB2EA7CFD1394F1615EBEB08C" /> tools:text="7543A7EAB2EA7CFD1394F1615EBEB08C" />
</LinearLayout> </LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout <LinearLayout
android:id="@+id/node_options" android:id="@+id/node_options"
android:layout_width="wrap_content" 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_marginStart="12dp"
android:layout_marginLeft="12dp" android:layout_marginLeft="12dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@@ -118,9 +119,11 @@
android:id="@+id/node_otp_container" android:id="@+id/node_otp_container"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="16dp" android:layout_marginEnd="12dp"
android:layout_marginRight="16dp" android:layout_marginRight="12dp"
android:padding="4dp"
android:orientation="horizontal" android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/node_attachment_icon" app:layout_constraintBottom_toTopOf="@+id/node_attachment_icon"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
@@ -162,7 +165,7 @@
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/node_otp_container" /> app:layout_constraintTop_toBottomOf="@+id/node_otp_container" />
</androidx.constraintlayout.widget.ConstraintLayout> </LinearLayout>
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -30,8 +30,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:minHeight="56dp" android:minHeight="48dp"
android:maxHeight="72dp"
app:layout_constraintWidth_percent="@dimen/content_percent" app:layout_constraintWidth_percent="@dimen/content_percent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
@@ -89,18 +88,6 @@
android:maxLines="2" android:maxLines="2"
tools:text="Node Title" /> 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 <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/node_meta" android:id="@+id/node_meta"
style="@style/KeepassDXStyle.TextAppearance.Group.Meta" style="@style/KeepassDXStyle.TextAppearance.Group.Meta"

View File

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

View File

@@ -203,6 +203,7 @@
<string name="autofill_sign_in_prompt">Mit KeePassDX anmelden</string> <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="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_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="clipboard">Zwischenablage</string>
<string name="biometric_delete_all_key_title">Verschlüsselungsschlüssel löschen</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> <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="validate">Validieren</string>
<string name="autofill_auto_search_summary">Suchergebnisse automatisch nach Web-Domain oder Anwendungs-ID vorschlagen</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_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_summary">Zeigt die Sperrtaste in der Benutzeroberfläche an</string>
<string name="lock_database_show_button_title">Sperrtaste anzeigen</string> <string name="lock_database_show_button_title">Sperrtaste anzeigen</string>
<string name="autofill_preference_title">Einstellungen für automatisches Ausfüllen</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="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="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_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_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="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> <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> <bool name="autofill_auto_search_default" translatable="false">true</bool>
<string name="autofill_inline_suggestions_key" translatable="false">autofill_inline_suggestions_key</string> <string name="autofill_inline_suggestions_key" translatable="false">autofill_inline_suggestions_key</string>
<bool name="autofill_inline_suggestions_default" translatable="false">false</bool> <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> <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> <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> <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_service_name">KeePassDX form autofilling</string>
<string name="autofill_sign_in_prompt">Sign in with KeePassDX</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_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="set_autofill_service_title">Set default autofill service</string>
<string name="autofill_preference_title">Autofill settings</string> <string name="autofill_preference_title">Autofill settings</string>
<string name="password_size_title">Generated password size</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="clipboard_warning">If automatic deletion of clipboard fails, delete its history manually.</string>
<string name="lock">Lock</string> <string name="lock">Lock</string>
<string name="lock_database_screen_off_title">Screen 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_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_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> <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_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_title">Inline suggestions</string>
<string name="autofill_inline_suggestions_summary">Attempt to display autofill suggestions directly from a compatible keyboard</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_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_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> <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:title="@string/autofill_inline_suggestions_title"
android:summary="@string/autofill_inline_suggestions_summary" android:summary="@string/autofill_inline_suggestions_summary"
android:defaultValue="@bool/autofill_inline_suggestions_default"/> 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>
<PreferenceCategory <PreferenceCategory
android:title="@string/save"> 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 * Paramètres pour afficher les jetons OTP dans la liste #655
* Correction du délai d'expiration dans les dialogues #716 * Correction du délai d'expiration dans les dialogues #716
* Vérification des permissions URI #626 * 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)