mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
62 Commits
3.0.0_beta
...
3.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7750843b04 | ||
|
|
d7da1ce333 | ||
|
|
dd9ee8c3f8 | ||
|
|
c0ac01a34a | ||
|
|
1b88f2ddf0 | ||
|
|
b4f2a1eb89 | ||
|
|
e9fc9cbc2a | ||
|
|
b809180a1b | ||
|
|
ecc75df3a1 | ||
|
|
d1b6863143 | ||
|
|
faf27143aa | ||
|
|
aee58a4475 | ||
|
|
c5e07f643f | ||
|
|
818f5820d5 | ||
|
|
77c6e28876 | ||
|
|
fb7f66012d | ||
|
|
0f7f7bbe6c | ||
|
|
8dedb8deb4 | ||
|
|
d284db4d3c | ||
|
|
82450c0ae8 | ||
|
|
8dd6c33901 | ||
|
|
f920d40db5 | ||
|
|
19be6c1acc | ||
|
|
7d9d8ad0e4 | ||
|
|
85f8237d5f | ||
|
|
c542894734 | ||
|
|
d348987077 | ||
|
|
3718610595 | ||
|
|
9c36ec0623 | ||
|
|
c6917b5d74 | ||
|
|
4eaa179789 | ||
|
|
9008cd4549 | ||
|
|
cc3204453e | ||
|
|
5ef8d3b7b9 | ||
|
|
2a9de97a19 | ||
|
|
9cecfed417 | ||
|
|
319715918a | ||
|
|
a3bf6e8b6d | ||
|
|
c4062658ce | ||
|
|
01a5de413e | ||
|
|
e4c22b1f29 | ||
|
|
b10e60126f | ||
|
|
ef1f27f421 | ||
|
|
0ed208675c | ||
|
|
00f7a0a194 | ||
|
|
935d4f4a64 | ||
|
|
dc4d88260d | ||
|
|
18934601da | ||
|
|
4ea811aeda | ||
|
|
f8fdecdc8f | ||
|
|
5467c61137 | ||
|
|
9c72b4cc56 | ||
|
|
9102217bc3 | ||
|
|
0e8fd7b2c4 | ||
|
|
c6e2342ab4 | ||
|
|
b977792168 | ||
|
|
2595cf87d8 | ||
|
|
f4342f1448 | ||
|
|
c71ef24052 | ||
|
|
39b817bc69 | ||
|
|
e3adaba3b3 | ||
|
|
c62064002f |
15
CHANGELOG
15
CHANGELOG
@@ -1,10 +1,23 @@
|
||||
KeePassDX(3.0.2)
|
||||
* Samsung DeX mode #1114 #245 (Thx @chenxiaolong)
|
||||
|
||||
KeePassDX(3.0.1)
|
||||
* Fix text size and smallest margin #1085
|
||||
* Fix number of lines during an edition #1073
|
||||
* Fix Magikeyboard URL auto action #1100
|
||||
* Fix exception after group name change and save #1112
|
||||
* Fix timeout reset #1107
|
||||
* Fix search actions #1091 #1092
|
||||
* Small changes #1106 #1085
|
||||
|
||||
KeePassDX(3.0.0)
|
||||
* Add / Manage dynamic templates #191
|
||||
* Manually select RecycleBin group and Templates group #191
|
||||
* Setting to display OTP Token in list #655
|
||||
* Fix timeout in dialogs #716
|
||||
* Check URI permissions #626
|
||||
* Improvements #1035 #1043 #942 #1021 #1027
|
||||
* Better autofill implementation #943 #946 #984 #1070 (Thx @uduerholz)
|
||||
* Improvements #680 #1035 #1043 #942 #1021 #1027 #1046 #1082 #1083 (Thx @chenxiaolong)
|
||||
|
||||
KeePassDX(2.10.5)
|
||||
* Increase the saving speed of database #1028
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 30
|
||||
versionCode = 85
|
||||
versionName = "3.0.0_beta02"
|
||||
versionCode = 89
|
||||
versionName = "3.0.2"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
|
||||
@@ -221,6 +221,14 @@
|
||||
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<receiver
|
||||
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.app.action.ENTER_KNOX_DESKTOP_MODE" />
|
||||
<action android:name="android.app.action.EXIT_KNOX_DESKTOP_MODE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||
</application>
|
||||
|
||||
@@ -65,6 +65,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
|
||||
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
||||
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME)
|
||||
manualSelection = intent.getBooleanExtra(KEY_MANUAL_SELECTION, false)
|
||||
}
|
||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
@@ -198,15 +199,16 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_MANUAL_SELECTION = "KEY_MANUAL_SELECTION"
|
||||
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
|
||||
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
|
||||
private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME"
|
||||
|
||||
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
||||
|
||||
fun getAuthIntentSenderForSelection(context: Context,
|
||||
searchInfo: SearchInfo? = null,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): IntentSender {
|
||||
fun getPendingIntentForSelection(context: Context,
|
||||
searchInfo: SearchInfo? = null,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): PendingIntent {
|
||||
return PendingIntent.getActivity(context, 0,
|
||||
// Doesn't work with Parcelable (don't know why?)
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
@@ -214,6 +216,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
|
||||
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
|
||||
putExtra(KEY_SEARCH_SCHEME, it.webScheme)
|
||||
putExtra(KEY_MANUAL_SELECTION, it.manualSelection)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
@@ -221,17 +224,17 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
}
|
||||
}
|
||||
},
|
||||
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
}
|
||||
|
||||
fun getAuthIntentSenderForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo): IntentSender {
|
||||
fun getPendingIntentForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo): PendingIntent {
|
||||
return PendingIntent.getActivity(context, 0,
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
||||
putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||
},
|
||||
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
}
|
||||
|
||||
fun launchForRegistration(context: Context,
|
||||
|
||||
@@ -81,6 +81,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
private var mHistoryPosition: Int = -1
|
||||
private var mEntryIsHistory: Boolean = false
|
||||
private var mUrl: String? = null
|
||||
private var mEntryLoaded = false
|
||||
|
||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
|
||||
@@ -119,11 +120,12 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
// Get Entry from UUID
|
||||
try {
|
||||
intent.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { entryId ->
|
||||
mMainEntryId = entryId
|
||||
intent.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { mainEntryId ->
|
||||
intent.removeExtra(KEY_ENTRY)
|
||||
mHistoryPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
|
||||
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
|
||||
intent.removeExtra(KEY_ENTRY_HISTORY_POSITION)
|
||||
|
||||
mEntryViewModel.loadEntry(mDatabase, mainEntryId, historyPosition)
|
||||
}
|
||||
} catch (e: ClassCastException) {
|
||||
Log.e(TAG, "Unable to retrieve the entry key")
|
||||
@@ -138,53 +140,53 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
lockAndExit()
|
||||
}
|
||||
|
||||
mEntryViewModel.mainEntryId.observe(this) { mainEntryId ->
|
||||
this.mMainEntryId = mainEntryId
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory ->
|
||||
if (entryInfoHistory != null) {
|
||||
this.mMainEntryId = entryInfoHistory.mainEntryId
|
||||
|
||||
mEntryViewModel.historyPosition.observe(this) { historyPosition ->
|
||||
this.mHistoryPosition = historyPosition
|
||||
val entryIsHistory = historyPosition > -1
|
||||
this.mEntryIsHistory = entryIsHistory
|
||||
// Assign history dedicated view
|
||||
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
|
||||
if (entryIsHistory) {
|
||||
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
||||
taColorAccent.recycle()
|
||||
}
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
mEntryViewModel.entryInfo.observe(this) { entryInfo ->
|
||||
// Manage entry copy to start notification if allowed (at the first start)
|
||||
if (savedInstanceState == null) {
|
||||
// Manage entry to launch copying notification if allowed
|
||||
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
||||
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
||||
// Manage history position
|
||||
val historyPosition = entryInfoHistory.historyPosition
|
||||
this.mHistoryPosition = historyPosition
|
||||
val entryIsHistory = historyPosition > -1
|
||||
this.mEntryIsHistory = entryIsHistory
|
||||
// Assign history dedicated view
|
||||
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
|
||||
if (entryIsHistory) {
|
||||
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
collapsingToolbarLayout?.contentScrim =
|
||||
ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
||||
taColorAccent.recycle()
|
||||
}
|
||||
|
||||
val entryInfo = entryInfoHistory.entryInfo
|
||||
// Manage entry copy to start notification if allowed (at the first start)
|
||||
if (savedInstanceState == null) {
|
||||
// Manage entry to launch copying notification if allowed
|
||||
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
||||
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
||||
}
|
||||
}
|
||||
// Assign title icon
|
||||
mIcon = entryInfo.icon
|
||||
titleIconView?.let { iconView ->
|
||||
mIconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, mIconColor)
|
||||
}
|
||||
// Assign title text
|
||||
val entryTitle =
|
||||
if (entryInfo.title.isNotEmpty()) entryInfo.title else entryInfo.id.toString()
|
||||
collapsingToolbarLayout?.title = entryTitle
|
||||
toolbar?.title = entryTitle
|
||||
mUrl = entryInfo.url
|
||||
|
||||
loadingView?.hideByFading()
|
||||
mEntryLoaded = true
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
|
||||
// Assign title icon
|
||||
mIcon = entryInfo.icon
|
||||
titleIconView?.let { iconView ->
|
||||
mIconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, mIconColor)
|
||||
}
|
||||
|
||||
// Assign title text
|
||||
val entryTitle = if (entryInfo.title.isNotEmpty()) entryInfo.title else entryInfo.id.toString()
|
||||
collapsingToolbarLayout?.title = entryTitle
|
||||
toolbar?.title = entryTitle
|
||||
|
||||
mUrl = entryInfo.url
|
||||
|
||||
// Refresh Menu
|
||||
invalidateOptionsMenu()
|
||||
|
||||
loadingView?.hideByFading()
|
||||
}
|
||||
|
||||
mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement ->
|
||||
@@ -235,7 +237,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
mEntryViewModel.loadEntry(mDatabase, mMainEntryId, mHistoryPosition)
|
||||
mEntryViewModel.loadDatabase(database)
|
||||
|
||||
// Assign title icon
|
||||
mIcon?.let { icon ->
|
||||
@@ -294,7 +296,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
when (requestCode) {
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
||||
// Reload the current id from database
|
||||
mEntryViewModel.updateEntry(mDatabase)
|
||||
mEntryViewModel.loadDatabase(mDatabase)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,32 +312,41 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
if (mEntryLoaded) {
|
||||
val inflater = menuInflater
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
|
||||
val inflater = menuInflater
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
inflater.inflate(R.menu.entry, menu)
|
||||
inflater.inflate(R.menu.database, menu)
|
||||
|
||||
inflater.inflate(R.menu.entry, menu)
|
||||
inflater.inflate(R.menu.database, menu)
|
||||
if (mEntryIsHistory && !mDatabaseReadOnly) {
|
||||
inflater.inflate(R.menu.entry_history, menu)
|
||||
}
|
||||
|
||||
if (mUrl?.isEmpty() != false) {
|
||||
menu.findItem(R.id.menu_goto_url)?.isVisible = false
|
||||
// Show education views
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
performedNextEducation(
|
||||
EntryActivityEducation(
|
||||
this
|
||||
), menu
|
||||
)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if (mEntryIsHistory && !mDatabaseReadOnly) {
|
||||
inflater.inflate(R.menu.entry_history, menu)
|
||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||
if (mUrl?.isEmpty() != false) {
|
||||
menu?.findItem(R.id.menu_goto_url)?.isVisible = false
|
||||
}
|
||||
if (mEntryIsHistory || mDatabaseReadOnly) {
|
||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
menu.findItem(R.id.menu_edit)?.isVisible = false
|
||||
menu?.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
menu?.findItem(R.id.menu_edit)?.isVisible = false
|
||||
}
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
menu.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||
menu?.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||
}
|
||||
|
||||
// Show education views
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(EntryActivityEducation(this), menu) }
|
||||
|
||||
return true
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
||||
|
||||
@@ -48,12 +48,9 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
|
||||
import com.kunzisoft.keepass.app.database.IOActionTask
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.template.*
|
||||
@@ -95,15 +92,10 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
private var lockView: View? = null
|
||||
private var loadingView: ProgressBar? = null
|
||||
|
||||
private var mEntryId: NodeId<UUID>? = null
|
||||
private var mParentId: NodeId<*>? = null
|
||||
private var mRegisterInfo: RegisterInfo? = null
|
||||
private var mSearchInfo: SearchInfo? = null
|
||||
|
||||
private val mEntryEditViewModel: EntryEditViewModel by viewModels()
|
||||
private var mParent: Group? = null
|
||||
private var mEntry: Entry? = null
|
||||
private var mTemplate: Template? = null
|
||||
private var mIsTemplate: Boolean = false
|
||||
private var mEntryLoaded: Boolean = false
|
||||
|
||||
private var mAllowCustomFields = false
|
||||
private var mAllowOTP = false
|
||||
@@ -138,22 +130,27 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
||||
|
||||
mRegisterInfo = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)
|
||||
mSearchInfo = EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||
|
||||
// Entry is retrieve, it's an entry to update
|
||||
var entryId: NodeId<UUID>? = null
|
||||
intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let { entryToUpdate ->
|
||||
//intent.removeExtra(KEY_ENTRY)
|
||||
mEntryId = entryToUpdate
|
||||
intent.removeExtra(KEY_ENTRY)
|
||||
entryId = entryToUpdate
|
||||
}
|
||||
|
||||
// Parent is retrieve, it's a new entry to create
|
||||
var parentId: NodeId<*>? = null
|
||||
intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let { parent ->
|
||||
//intent.removeExtra(KEY_PARENT)
|
||||
mParentId = parent
|
||||
intent.removeExtra(KEY_PARENT)
|
||||
parentId = parent
|
||||
}
|
||||
|
||||
retrieveEntry(mDatabase)
|
||||
mEntryEditViewModel.loadTemplateEntry(
|
||||
mDatabase,
|
||||
entryId,
|
||||
parentId,
|
||||
EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent),
|
||||
EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||
)
|
||||
|
||||
// To retrieve attachment
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
@@ -166,38 +163,52 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
// Save button
|
||||
validateButton?.setOnClickListener { saveEntry() }
|
||||
|
||||
mEntryEditViewModel.templatesEntry.observe(this) { templatesEntry ->
|
||||
// Change template dynamically
|
||||
templatesEntry?.templates?.let { templates ->
|
||||
val defaultTemplate = templatesEntry.defaultTemplate
|
||||
templateSelectorSpinner?.apply {
|
||||
// Build template selector
|
||||
if (templates.isNotEmpty()) {
|
||||
adapter = TemplatesSelectorAdapter(
|
||||
this@EntryEditActivity,
|
||||
mIconDrawableFactory,
|
||||
templates
|
||||
)
|
||||
setSelection(templates.indexOf(defaultTemplate))
|
||||
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
parent: AdapterView<*>?,
|
||||
view: View?,
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
mEntryEditViewModel.changeTemplate(templates[position])
|
||||
}
|
||||
mEntryEditViewModel.onTemplateChanged.observe(this) { template ->
|
||||
this.mTemplate = template
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
mEntryEditViewModel.templatesEntry.observe(this) { templatesEntry ->
|
||||
if (templatesEntry != null) {
|
||||
// Change template dynamically
|
||||
this.mIsTemplate = templatesEntry.isTemplate
|
||||
templatesEntry.templates.let { templates ->
|
||||
templateSelectorSpinner?.apply {
|
||||
// Build template selector
|
||||
if (templates.isNotEmpty()) {
|
||||
adapter = TemplatesSelectorAdapter(
|
||||
this@EntryEditActivity,
|
||||
mIconDrawableFactory,
|
||||
templates
|
||||
)
|
||||
val selectedTemplate = if (mTemplate != null)
|
||||
mTemplate
|
||||
else
|
||||
templatesEntry.defaultTemplate
|
||||
setSelection(templates.indexOf(selectedTemplate))
|
||||
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
parent: AdapterView<*>?,
|
||||
view: View?,
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
mEntryEditViewModel.changeTemplate(templates[position])
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
}
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadingView?.hideByFading()
|
||||
loadingView?.hideByFading()
|
||||
mEntryLoaded = true
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
// View model listeners
|
||||
@@ -309,78 +320,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
super.onDatabaseRetrieved(database)
|
||||
mAllowCustomFields = database?.allowEntryCustomFields() == true
|
||||
mAllowOTP = database?.allowOTP == true
|
||||
retrieveEntry(database)
|
||||
}
|
||||
|
||||
private fun retrieveEntry(database: Database?) {
|
||||
database?.let {
|
||||
mEntryId?.let {
|
||||
IOActionTask(
|
||||
{
|
||||
// Create an Entry copy to modify from the database entry
|
||||
mEntry = database.getEntryById(it)
|
||||
// Retrieve the parent
|
||||
mEntry?.let { entry ->
|
||||
// If no parent, add root group as parent
|
||||
if (entry.parent == null) {
|
||||
entry.parent = database.rootGroup
|
||||
}
|
||||
}
|
||||
// Define if current entry is a template (in direct template group)
|
||||
mIsTemplate = database.entryIsTemplate(mEntry)
|
||||
},
|
||||
{
|
||||
mEntryEditViewModel.loadTemplateEntry(
|
||||
database,
|
||||
mEntry,
|
||||
mIsTemplate,
|
||||
mRegisterInfo,
|
||||
mSearchInfo
|
||||
)
|
||||
}
|
||||
).execute()
|
||||
mEntryId = null
|
||||
}
|
||||
|
||||
mParentId?.let {
|
||||
IOActionTask(
|
||||
{
|
||||
mParent = database.getGroupById(it)
|
||||
mParent?.let { parentGroup ->
|
||||
mEntry = database.createEntry()?.apply {
|
||||
// Add the default icon from parent if not a folder
|
||||
val parentIcon = parentGroup.icon
|
||||
// Set default icon
|
||||
if (parentIcon.custom.isUnknown
|
||||
&& parentIcon.standard.id != IconImageStandard.FOLDER_ID
|
||||
) {
|
||||
icon = IconImage(parentIcon.standard)
|
||||
}
|
||||
if (!parentIcon.custom.isUnknown) {
|
||||
icon = IconImage(parentIcon.custom)
|
||||
}
|
||||
// Set default username
|
||||
username = database.defaultUsername
|
||||
// Warning only the entry recognize is parent, parent don't yet recognize the new entry
|
||||
// Useful to recognize child state (ie: entry is a template)
|
||||
parent = parentGroup
|
||||
}
|
||||
}
|
||||
mIsTemplate = database.entryIsTemplate(mEntry)
|
||||
},
|
||||
{
|
||||
mEntryEditViewModel.loadTemplateEntry(
|
||||
database,
|
||||
mEntry,
|
||||
mIsTemplate,
|
||||
mRegisterInfo,
|
||||
mSearchInfo
|
||||
)
|
||||
}
|
||||
).execute()
|
||||
mParentId = null
|
||||
}
|
||||
}
|
||||
mEntryEditViewModel.loadDatabase(database)
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
@@ -571,29 +511,33 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
*/
|
||||
private fun saveEntry() {
|
||||
mAttachmentFileBinderManager?.stopUploadAllAttachments()
|
||||
mEntryEditViewModel.requestEntryInfoUpdate(mDatabase, mEntry, mParent)
|
||||
mEntryEditViewModel.requestEntryInfoUpdate(mDatabase)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
menuInflater.inflate(R.menu.entry_edit, menu)
|
||||
if (mEntryLoaded) {
|
||||
menuInflater.inflate(R.menu.entry_edit, menu)
|
||||
entryEditActivityEducation?.let {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
performedNextEducation(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||
|
||||
menu?.findItem(R.id.menu_add_field)?.apply {
|
||||
isEnabled = mAllowCustomFields
|
||||
isVisible = isEnabled
|
||||
}
|
||||
|
||||
menu?.findItem(R.id.menu_add_attachment)?.apply {
|
||||
// Attachment not compatible below KitKat
|
||||
isEnabled = !mIsTemplate
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
||||
isVisible = isEnabled
|
||||
}
|
||||
|
||||
menu?.findItem(R.id.menu_add_otp)?.apply {
|
||||
// OTP not compatible below KitKat
|
||||
isEnabled = mAllowOTP
|
||||
@@ -601,14 +545,10 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
||||
isVisible = isEnabled
|
||||
}
|
||||
|
||||
entryEditActivityEducation?.let {
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
|
||||
}
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
||||
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
||||
|
||||
val entryEditFragment = supportFragmentManager.findFragmentById(R.id.entry_edit_content)
|
||||
as? EntryEditFragment?
|
||||
|
||||
@@ -88,6 +88,12 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Enabling/disabling MagikeyboardService is normally done by DexModeReceiver, but this
|
||||
// additional check will allow the keyboard to be reenabled more easily if the app crashes
|
||||
// or is force quit within DeX mode and then the user leaves DeX mode. Without this, the
|
||||
// user would need to enter and exit DeX mode once to reenable the service.
|
||||
MagikeyboardUtil.setEnabled(this, !DexUtil.isDexMode(resources.configuration))
|
||||
|
||||
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||
|
||||
setContentView(R.layout.activity_file_selection)
|
||||
|
||||
@@ -487,7 +487,9 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
if (groupMetaView != null) {
|
||||
val meta = group.nodeId.toString()
|
||||
groupMetaView?.text = meta
|
||||
if (meta.isNotEmpty() && PreferencesUtil.showUUID(this)) {
|
||||
if (meta.isNotEmpty()
|
||||
&& !group.isVirtual
|
||||
&& PreferencesUtil.showUUID(this)) {
|
||||
groupMetaView?.visibility = View.VISIBLE
|
||||
} else {
|
||||
groupMetaView?.visibility = View.GONE
|
||||
@@ -651,6 +653,8 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
Log.e(TAG, "Node can't be cast in Entry")
|
||||
}
|
||||
}
|
||||
|
||||
reloadGroupIfSearch()
|
||||
}
|
||||
|
||||
private fun entrySelectedForSave(database: Database, entry: Entry, searchInfo: SearchInfo) {
|
||||
@@ -736,6 +740,12 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
actionNodeMode?.finish()
|
||||
}
|
||||
|
||||
private fun reloadGroupIfSearch() {
|
||||
if (Intent.ACTION_SEARCH == intent.action) {
|
||||
reloadCurrentGroup()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNodeSelected(
|
||||
database: Database,
|
||||
nodes: List<Node>
|
||||
@@ -791,6 +801,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
(node as Entry).nodeId
|
||||
)
|
||||
}
|
||||
reloadGroupIfSearch()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -845,6 +856,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
): Boolean {
|
||||
deleteNodes(nodes)
|
||||
finishNodeAction()
|
||||
reloadGroupIfSearch()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -932,9 +944,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
) {
|
||||
|
||||
// If no node, show education to add new one
|
||||
val addNodeButtonEducationPerformed = mGroupFragment != null
|
||||
&& mGroupFragment!!.isEmpty
|
||||
&& actionNodeMode == null
|
||||
val addNodeButtonEducationPerformed = actionNodeMode == null
|
||||
&& addNodeButtonView?.addButtonView != null
|
||||
&& addNodeButtonView!!.isEnable
|
||||
&& groupActivityEducation.checkAndPerformedAddNodeButtonEducation(
|
||||
@@ -1093,7 +1103,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
try {
|
||||
mGroupViewModel.loadGroup(mDatabase, mCurrentGroupState)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to rebuild the list after deletion", e)
|
||||
Log.e(TAG, "Unable to rebuild the group", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.template.Template
|
||||
import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
@@ -55,6 +56,7 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
private lateinit var attachmentsListView: RecyclerView
|
||||
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
||||
|
||||
private var mTemplate: Template? = null
|
||||
private var mAllowMultipleAttachments: Boolean = false
|
||||
|
||||
private var mIconColor: Int = 0
|
||||
@@ -115,19 +117,26 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
}
|
||||
|
||||
mEntryEditViewModel.onTemplateChanged.observe(viewLifecycleOwner) { template ->
|
||||
this.mTemplate = template
|
||||
templateView.setTemplate(template)
|
||||
}
|
||||
|
||||
mEntryEditViewModel.templatesEntry.observe(viewLifecycleOwner) { templateEntry ->
|
||||
templateView.setTemplate(templateEntry.defaultTemplate)
|
||||
// Load entry info only the first time to keep change locally
|
||||
if (savedInstanceState == null) {
|
||||
assignEntryInfo(templateEntry.entryInfo)
|
||||
if (templateEntry != null) {
|
||||
val selectedTemplate = if (mTemplate != null)
|
||||
mTemplate
|
||||
else
|
||||
templateEntry.defaultTemplate
|
||||
templateView.setTemplate(selectedTemplate)
|
||||
// Load entry info only the first time to keep change locally
|
||||
if (savedInstanceState == null) {
|
||||
assignEntryInfo(templateEntry.entryInfo)
|
||||
}
|
||||
// To prevent flickering
|
||||
rootView.showByFading()
|
||||
// Apply timeout reset
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
|
||||
}
|
||||
// To prevent flickering
|
||||
rootView.showByFading()
|
||||
// Apply timeout reset
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
|
||||
}
|
||||
|
||||
mEntryEditViewModel.requestEntryInfoUpdate.observe(viewLifecycleOwner) {
|
||||
|
||||
@@ -91,15 +91,14 @@ class EntryFragment: DatabaseFragment() {
|
||||
uuidView = view.findViewById(R.id.entry_UUID)
|
||||
uuidReferenceView = view.findViewById(R.id.entry_UUID_reference)
|
||||
|
||||
mEntryViewModel.template.observe(viewLifecycleOwner) { template ->
|
||||
templateView.setTemplate(template)
|
||||
}
|
||||
|
||||
mEntryViewModel.entryInfo.observe(viewLifecycleOwner) { entryInfo ->
|
||||
assignEntryInfo(entryInfo)
|
||||
// Smooth appearing
|
||||
rootView.showByFading()
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
|
||||
mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory ->
|
||||
if (entryInfoHistory != null) {
|
||||
templateView.setTemplate(entryInfoHistory.template)
|
||||
assignEntryInfo(entryInfoHistory.entryInfo)
|
||||
// Smooth appearing
|
||||
rootView.showByFading()
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
|
||||
}
|
||||
}
|
||||
|
||||
mEntryViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState ->
|
||||
|
||||
@@ -55,9 +55,11 @@ class EntryHistoryFragment: StylishFragment() {
|
||||
* History
|
||||
* -------------
|
||||
*/
|
||||
private fun assignHistory(history: List<EntryInfo>) {
|
||||
private fun assignHistory(history: List<EntryInfo>?) {
|
||||
historyAdapter?.clear()
|
||||
historyAdapter?.entryHistoryList?.addAll(history)
|
||||
history?.let {
|
||||
historyAdapter?.entryHistoryList?.addAll(history)
|
||||
}
|
||||
historyAdapter?.onItemClickListener = { item, position ->
|
||||
mEntryViewModel.onHistorySelected(item, position)
|
||||
}
|
||||
|
||||
@@ -74,9 +74,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
private var mRecycleBinEnable: Boolean = false
|
||||
private var mRecycleBin: Group? = null
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
|
||||
|
||||
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
super.onScrollStateChanged(recyclerView, newState)
|
||||
@@ -233,7 +230,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
return mLayoutManager?.findFirstVisibleItemPosition() ?: 0
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
private fun rebuildList() {
|
||||
try {
|
||||
// Add elements to the list
|
||||
@@ -418,7 +414,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
mNodesRecyclerView?.scrollToPosition(position)
|
||||
}
|
||||
}
|
||||
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
|
||||
} ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,9 @@ import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
import java.util.*
|
||||
|
||||
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
||||
|
||||
@@ -28,7 +23,11 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
||||
mDatabaseTaskProvider = DatabaseTaskProvider(this)
|
||||
|
||||
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||
if (mDatabase == null || mDatabase != database) {
|
||||
val databaseWasReloaded = database?.wasReloaded == true
|
||||
if (databaseWasReloaded && finishActivityIfReloadRequested()) {
|
||||
finish()
|
||||
} else if (mDatabase == null || mDatabase != database || databaseWasReloaded) {
|
||||
database?.wasReloaded = false
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
}
|
||||
@@ -69,17 +68,8 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
||||
mDatabase?.clearAndClose(this)
|
||||
}
|
||||
|
||||
override fun reloadActivity() {
|
||||
super.reloadActivity()
|
||||
mDatabase?.wasReloaded = false
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (mDatabase?.wasReloaded == true) {
|
||||
reloadActivity()
|
||||
}
|
||||
mDatabaseTaskProvider?.registerProgressTask()
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -41,9 +42,11 @@ import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpType
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.view.setTextSize
|
||||
import com.kunzisoft.keepass.view.strikeOut
|
||||
import java.util.*
|
||||
@@ -64,8 +67,10 @@ class NodeAdapter (private val context: Context,
|
||||
private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type
|
||||
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
||||
private var mPrefSizeMultiplier: Float = 0F
|
||||
private var mSubtextDefaultDimension: Float = 0F
|
||||
private var mInfoTextDefaultDimension: Float = 0F
|
||||
private var mTextDefaultDimension: Float = 0F
|
||||
private var mSubTextDefaultDimension: Float = 0F
|
||||
private var mMetaTextDefaultDimension: Float = 0F
|
||||
private var mOtpTokenTextDefaultDimension: Float = 0F
|
||||
private var mNumberChildrenTextDefaultDimension: Float = 0F
|
||||
private var mIconDefaultDimension: Float = 0F
|
||||
|
||||
@@ -77,6 +82,7 @@ class NodeAdapter (private val context: Context,
|
||||
|
||||
private var mActionNodesList = LinkedList<Node>()
|
||||
private var mNodeClickCallback: NodeClickCallback? = null
|
||||
private var mClipboardHelper = ClipboardHelper(context)
|
||||
|
||||
@ColorInt
|
||||
private val mContentSelectionColor: Int
|
||||
@@ -299,8 +305,10 @@ class NodeAdapter (private val context: Context,
|
||||
mInflater.inflate(R.layout.item_list_nodes_entry, parent, false)
|
||||
}
|
||||
val nodeViewHolder = NodeViewHolder(view)
|
||||
mInfoTextDefaultDimension = nodeViewHolder.text.textSize
|
||||
mSubtextDefaultDimension = nodeViewHolder.subText.textSize
|
||||
mTextDefaultDimension = nodeViewHolder.text.textSize
|
||||
mSubTextDefaultDimension = nodeViewHolder.subText?.textSize ?: mSubTextDefaultDimension
|
||||
mMetaTextDefaultDimension = nodeViewHolder.meta.textSize
|
||||
mOtpTokenTextDefaultDimension = nodeViewHolder.otpToken?.textSize ?: mOtpTokenTextDefaultDimension
|
||||
nodeViewHolder.numberChildren?.let {
|
||||
mNumberChildrenTextDefaultDimension = it.textSize
|
||||
}
|
||||
@@ -311,7 +319,9 @@ class NodeAdapter (private val context: Context,
|
||||
val subNode = mNodeSortedList.get(position)
|
||||
|
||||
// Node selection
|
||||
holder.container.isSelected = mActionNodesList.contains(subNode)
|
||||
holder.container.apply {
|
||||
isSelected = mActionNodesList.contains(subNode)
|
||||
}
|
||||
|
||||
// Assign image
|
||||
val iconColor = if (holder.container.isSelected)
|
||||
@@ -333,19 +343,18 @@ class NodeAdapter (private val context: Context,
|
||||
// Assign text
|
||||
holder.text.apply {
|
||||
text = subNode.title
|
||||
setTextSize(mTextSizeUnit, mInfoTextDefaultDimension, mPrefSizeMultiplier)
|
||||
setTextSize(mTextSizeUnit, mTextDefaultDimension, mPrefSizeMultiplier)
|
||||
strikeOut(subNode.isCurrentlyExpires)
|
||||
}
|
||||
// Add subText with username
|
||||
holder.subText.apply {
|
||||
text = ""
|
||||
strikeOut(subNode.isCurrentlyExpires)
|
||||
visibility = View.GONE
|
||||
}
|
||||
// Add meta text to show UUID
|
||||
holder.meta.apply {
|
||||
text = subNode.nodeId.toString()
|
||||
visibility = if (mShowUUID) View.VISIBLE else View.GONE
|
||||
if (mShowUUID) {
|
||||
text = subNode.nodeId.toString()
|
||||
setTextSize(mTextSizeUnit, mMetaTextDefaultDimension, mPrefSizeMultiplier)
|
||||
visibility = View.VISIBLE
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
// Specific elements for entry
|
||||
@@ -354,12 +363,16 @@ class NodeAdapter (private val context: Context,
|
||||
database.startManageEntry(entry)
|
||||
|
||||
holder.text.text = entry.getVisualTitle()
|
||||
holder.subText.apply {
|
||||
// Add subText with username
|
||||
holder.subText?.apply {
|
||||
val username = entry.username
|
||||
if (mShowUserNames && username.isNotEmpty()) {
|
||||
visibility = View.VISIBLE
|
||||
text = username
|
||||
setTextSize(mTextSizeUnit, mSubtextDefaultDimension, mPrefSizeMultiplier)
|
||||
setTextSize(mTextSizeUnit, mSubTextDefaultDimension, mPrefSizeMultiplier)
|
||||
strikeOut(subNode.isCurrentlyExpires)
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,7 +440,21 @@ class NodeAdapter (private val context: Context,
|
||||
}
|
||||
}
|
||||
}
|
||||
holder?.otpToken?.text = otpElement?.token
|
||||
holder?.otpToken?.apply {
|
||||
text = otpElement?.token
|
||||
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
|
||||
}
|
||||
holder?.otpContainer?.setOnClickListener {
|
||||
otpElement?.token?.let { token ->
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.copy_field,
|
||||
TemplateField.getLocalizedName(context, TemplateField.LABEL_TOKEN)),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
mClipboardHelper.copyToClipboard(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OtpRunnable(val view: View?): Runnable {
|
||||
@@ -468,7 +495,7 @@ class NodeAdapter (private val context: Context,
|
||||
var imageIdentifier: ImageView? = itemView.findViewById(R.id.node_image_identifier)
|
||||
var icon: ImageView = itemView.findViewById(R.id.node_icon)
|
||||
var text: TextView = itemView.findViewById(R.id.node_text)
|
||||
var subText: TextView = itemView.findViewById(R.id.node_subtext)
|
||||
var subText: TextView? = itemView.findViewById(R.id.node_subtext)
|
||||
var meta: TextView = itemView.findViewById(R.id.node_meta)
|
||||
var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container)
|
||||
var otpProgress: ProgressBar? = itemView.findViewById(R.id.node_otp_progress)
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.app.PendingIntent
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentSender
|
||||
import android.graphics.BlendMode
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
@@ -37,11 +38,13 @@ import android.view.autofill.AutofillValue
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
import android.widget.RemoteViews
|
||||
import android.widget.Toast
|
||||
import android.widget.inline.InlinePresentationSpec
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.autofill.inline.UiVersions
|
||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
@@ -51,7 +54,6 @@ import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
|
||||
@@ -126,12 +128,14 @@ object AutofillHelper {
|
||||
if (entryInfo.expires) {
|
||||
val year = entryInfo.expiryTime.getYearInt()
|
||||
val month = entryInfo.expiryTime.getMonthInt()
|
||||
val monthString = month.toString().padStart(2, '0')
|
||||
val day = entryInfo.expiryTime.getDay()
|
||||
val dayString = day.toString().padStart(2, '0')
|
||||
|
||||
struct.creditCardExpirationDateId?.let {
|
||||
if (struct.isWebView) {
|
||||
// set date string as defined in https://html.spec.whatwg.org
|
||||
builder.setValue(it, AutofillValue.forText("$year\u002D$month"))
|
||||
builder.setValue(it, AutofillValue.forText("$year\u002D$monthString"))
|
||||
} else {
|
||||
builder.setValue(it, AutofillValue.forDate(entryInfo.expiryTime.date.time))
|
||||
}
|
||||
@@ -157,24 +161,24 @@ object AutofillHelper {
|
||||
}
|
||||
struct.creditCardExpirationMonthId?.let {
|
||||
if (struct.isWebView) {
|
||||
builder.setValue(it, AutofillValue.forText(month.toString()))
|
||||
builder.setValue(it, AutofillValue.forText(monthString))
|
||||
} else {
|
||||
if (struct.creditCardExpirationMonthOptions != null) {
|
||||
// index starts at 0
|
||||
builder.setValue(it, AutofillValue.forList(month - 1))
|
||||
} else {
|
||||
builder.setValue(it, AutofillValue.forText(month.toString()))
|
||||
builder.setValue(it, AutofillValue.forText(monthString))
|
||||
}
|
||||
}
|
||||
}
|
||||
struct.creditCardExpirationDayId?.let {
|
||||
if (struct.isWebView) {
|
||||
builder.setValue(it, AutofillValue.forText(day.toString()))
|
||||
builder.setValue(it, AutofillValue.forText(dayString))
|
||||
} else {
|
||||
if (struct.creditCardExpirationDayOptions != null) {
|
||||
builder.setValue(it, AutofillValue.forList(day - 1))
|
||||
} else {
|
||||
builder.setValue(it, AutofillValue.forText(day.toString()))
|
||||
builder.setValue(it, AutofillValue.forText(dayString))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,6 +274,27 @@ object AutofillHelper {
|
||||
return null
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun buildInlinePresentationForManualSelection(context: Context,
|
||||
inlinePresentationSpec: InlinePresentationSpec,
|
||||
pendingIntent: PendingIntent): InlinePresentation? {
|
||||
// Make sure that the IME spec claims support for v1 UI template.
|
||||
val imeStyle = inlinePresentationSpec.style
|
||||
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
|
||||
return null
|
||||
|
||||
// Build the content for IME UI
|
||||
return InlinePresentation(
|
||||
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
|
||||
setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
|
||||
setTitle(context.getString(R.string.autofill_select_entry))
|
||||
setStartIcon(Icon.createWithResource(context, R.drawable.ic_arrow_right_green_24dp).apply {
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
})
|
||||
}.build().slice, inlinePresentationSpec, false)
|
||||
}
|
||||
|
||||
fun buildResponse(context: Context,
|
||||
database: Database,
|
||||
entriesInfo: List<EntryInfo>,
|
||||
@@ -293,17 +318,58 @@ object AutofillHelper {
|
||||
}
|
||||
|
||||
// Add inline suggestion for new IME and dataset
|
||||
entriesInfo.forEachIndexed { index, entryInfo ->
|
||||
val inlinePresentation = inlineSuggestionsRequest?.let {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
buildInlinePresentationForEntry(context, database, inlineSuggestionsRequest, index, entryInfo)
|
||||
} else {
|
||||
null
|
||||
var numberInlineSuggestions = 0
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
|
||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
|
||||
--numberInlineSuggestions
|
||||
}
|
||||
}
|
||||
}
|
||||
val dataSet = buildDataset(context, database, entryInfo, parseResult, inlinePresentation)
|
||||
dataSet?.let {
|
||||
responseBuilder.addDataset(it)
|
||||
|
||||
}
|
||||
|
||||
entriesInfo.forEachIndexed { _, entry ->
|
||||
val inlinePresentation = if (numberInlineSuggestions > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
buildInlinePresentationForEntry(context, database, inlineSuggestionsRequest, numberInlineSuggestions--, entry)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult, inlinePresentation))
|
||||
}
|
||||
|
||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||
val searchInfo = SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
webDomain = parseResult.webDomain
|
||||
webScheme = parseResult.webScheme
|
||||
manualSelection = true
|
||||
}
|
||||
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
|
||||
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
|
||||
searchInfo, inlineSuggestionsRequest)
|
||||
|
||||
parseResult.allAutofillIds().let { autofillIds ->
|
||||
autofillIds.forEach { id ->
|
||||
val builder = Dataset.Builder(manualSelectionView)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
||||
val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
|
||||
inlinePresentation?.let {
|
||||
builder.setInlinePresentation(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.setValue(id, null)
|
||||
builder.setAuthentication(pendingIntent.intentSender)
|
||||
responseBuilder.addDataset(builder.build())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -138,14 +138,14 @@ class KeeAutofillService : AutofillService() {
|
||||
items, parseResult, inlineSuggestionsRequest)
|
||||
)
|
||||
},
|
||||
{
|
||||
{ openedDatabase ->
|
||||
// Show UI if no search result
|
||||
showUIForEntrySelection(parseResult,
|
||||
showUIForEntrySelection(parseResult, openedDatabase,
|
||||
searchInfo, inlineSuggestionsRequest, callback)
|
||||
},
|
||||
{
|
||||
// Show UI if database not open
|
||||
showUIForEntrySelection(parseResult,
|
||||
showUIForEntrySelection(parseResult, null,
|
||||
searchInfo, inlineSuggestionsRequest, callback)
|
||||
}
|
||||
)
|
||||
@@ -153,6 +153,7 @@ class KeeAutofillService : AutofillService() {
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||
database: Database?,
|
||||
searchInfo: SearchInfo,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
||||
callback: FillCallback) {
|
||||
@@ -160,19 +161,51 @@ class KeeAutofillService : AutofillService() {
|
||||
if (autofillIds.isNotEmpty()) {
|
||||
// If the entire Autofill Response is authenticated, AuthActivity is used
|
||||
// to generate Response.
|
||||
val intentSender = AutofillLauncherActivity.getAuthIntentSenderForSelection(this,
|
||||
searchInfo, inlineSuggestionsRequest)
|
||||
val intentSender = AutofillLauncherActivity.getPendingIntentForSelection(this,
|
||||
searchInfo, inlineSuggestionsRequest).intentSender
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
val remoteViewsUnlock: RemoteViews = if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply {
|
||||
setTextViewText(R.id.autofill_web_domain_text, parseResult.webDomain)
|
||||
}
|
||||
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
|
||||
setTextViewText(R.id.autofill_app_id_text, parseResult.applicationId)
|
||||
val remoteViewsUnlock: RemoteViews = if (database == null) {
|
||||
if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||
RemoteViews(
|
||||
packageName,
|
||||
R.layout.item_autofill_unlock_web_domain
|
||||
).apply {
|
||||
setTextViewText(
|
||||
R.id.autofill_web_domain_text,
|
||||
parseResult.webDomain
|
||||
)
|
||||
}
|
||||
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
|
||||
setTextViewText(
|
||||
R.id.autofill_app_id_text,
|
||||
parseResult.applicationId
|
||||
)
|
||||
}
|
||||
} else {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock)
|
||||
}
|
||||
} else {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock)
|
||||
if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||
RemoteViews(
|
||||
packageName,
|
||||
R.layout.item_autofill_select_entry_web_domain
|
||||
).apply {
|
||||
setTextViewText(
|
||||
R.id.autofill_web_domain_text,
|
||||
parseResult.webDomain
|
||||
)
|
||||
}
|
||||
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
||||
RemoteViews(packageName, R.layout.item_autofill_select_entry_app_id).apply {
|
||||
setTextViewText(
|
||||
R.id.autofill_app_id_text,
|
||||
parseResult.applicationId
|
||||
)
|
||||
}
|
||||
} else {
|
||||
RemoteViews(packageName, R.layout.item_autofill_select_entry)
|
||||
}
|
||||
}
|
||||
|
||||
// Tell the autofill framework the interest to save credentials
|
||||
|
||||
@@ -53,8 +53,10 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
applicationId = windowNode.title.toString().split("/")[0]
|
||||
Log.d(TAG, "Autofill applicationId: $applicationId")
|
||||
|
||||
if (parseViewNode(windowNode.rootViewNode))
|
||||
break@mainLoop
|
||||
if (applicationId?.contains("PopupWindow:") == false) {
|
||||
if (parseViewNode(windowNode.rootViewNode))
|
||||
break@mainLoop
|
||||
}
|
||||
}
|
||||
// If not explicit username field found, add the field just before password field.
|
||||
if (usernameId == null && passwordId != null && usernameIdCandidate != null) {
|
||||
@@ -143,19 +145,19 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
Log.d(TAG, "Autofill password hint")
|
||||
return true
|
||||
}
|
||||
it.contains("cc-name", true) -> {
|
||||
it.equals("cc-name", true) -> {
|
||||
Log.d(TAG, "Autofill credit card name hint")
|
||||
result?.creditCardHolderId = autofillId
|
||||
result?.creditCardHolder = node.autofillValue?.textValue?.toString()
|
||||
}
|
||||
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_NUMBER, true)
|
||||
|| it.contains("cc-number", true) -> {
|
||||
|| it.equals("cc-number", true) -> {
|
||||
Log.d(TAG, "Autofill credit card number hint")
|
||||
result?.creditCardNumberId = autofillId
|
||||
result?.creditCardNumber = node.autofillValue?.textValue?.toString()
|
||||
}
|
||||
// expect date string as defined in https://html.spec.whatwg.org, e.g. 2014-12
|
||||
it.contains("cc-exp", true) -> {
|
||||
it.equals("cc-exp", true) -> {
|
||||
Log.d(TAG, "Autofill credit card expiration date hint")
|
||||
result?.creditCardExpirationDateId = autofillId
|
||||
node.autofillValue?.let { value ->
|
||||
@@ -182,7 +184,7 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
}
|
||||
}
|
||||
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR, true)
|
||||
|| it.contains("cc-exp-year", true) -> {
|
||||
|| it.equals("cc-exp-year", true) -> {
|
||||
Log.d(TAG, "Autofill credit card expiration year hint")
|
||||
result?.creditCardExpirationYearId = autofillId
|
||||
if (node.autofillOptions != null) {
|
||||
@@ -204,7 +206,7 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
}
|
||||
}
|
||||
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH, true)
|
||||
|| it.contains("cc-exp-month", true) -> {
|
||||
|| it.equals("cc-exp-month", true) -> {
|
||||
Log.d(TAG, "Autofill credit card expiration month hint")
|
||||
result?.creditCardExpirationMonthId = autofillId
|
||||
if (node.autofillOptions != null) {
|
||||
@@ -227,7 +229,7 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
}
|
||||
}
|
||||
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY, true)
|
||||
|| it.contains("cc-exp-day", true) -> {
|
||||
|| it.equals("cc-exp-day", true) -> {
|
||||
Log.d(TAG, "Autofill credit card expiration day hint")
|
||||
result?.creditCardExpirationDayId = autofillId
|
||||
if (node.autofillOptions != null) {
|
||||
@@ -399,7 +401,6 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
class Result {
|
||||
var isWebView: Boolean = false
|
||||
var applicationId: String? = null
|
||||
|
||||
var webDomain: String? = null
|
||||
set(value) {
|
||||
if (field == null)
|
||||
|
||||
@@ -177,16 +177,20 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
fun addChildrenFrom(group: Group) {
|
||||
group.groupKDB?.getChildEntries()?.forEach { entryToAdd ->
|
||||
groupKDB?.addChildEntry(entryToAdd)
|
||||
entryToAdd.parent = groupKDB
|
||||
}
|
||||
group.groupKDB?.getChildGroups()?.forEach { groupToAdd ->
|
||||
groupKDB?.addChildGroup(groupToAdd)
|
||||
groupToAdd.parent = groupKDB
|
||||
}
|
||||
|
||||
group.groupKDBX?.getChildEntries()?.forEach { entryToAdd ->
|
||||
groupKDBX?.addChildEntry(entryToAdd)
|
||||
entryToAdd.parent = groupKDBX
|
||||
}
|
||||
group.groupKDBX?.getChildGroups()?.forEach { groupToAdd ->
|
||||
groupKDBX?.addChildGroup(groupToAdd)
|
||||
groupToAdd.parent = groupKDBX
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ class SearchHelper {
|
||||
} else if (TimeoutHelper.checkTime(context)) {
|
||||
var searchWithoutUI = false
|
||||
if (PreferencesUtil.isAutofillAutoSearchEnable(context)
|
||||
&& searchInfo != null
|
||||
&& searchInfo != null && !searchInfo.manualSelection
|
||||
&& !searchInfo.containsOnlyNullValues()) {
|
||||
// If search provide results
|
||||
database.createVirtualGroupFromSearchInfo(
|
||||
|
||||
@@ -178,6 +178,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
assignKeyboardView()
|
||||
}
|
||||
|
||||
override fun onEvaluateFullscreenMode(): Boolean {
|
||||
return resources.getBoolean(R.bool.magikeyboard_allow_fullscreen_mode)
|
||||
&& super.onEvaluateFullscreenMode()
|
||||
}
|
||||
|
||||
private fun playVibration(keyCode: Int) {
|
||||
when (keyCode) {
|
||||
Keyboard.KEYCODE_DELETE -> {}
|
||||
@@ -267,7 +272,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
if (entryInfoKey != null) {
|
||||
currentInputConnection.commitText(entryInfoKey!!.url, 1)
|
||||
}
|
||||
actionTabAutomatically()
|
||||
actionGoAutomatically()
|
||||
}
|
||||
KEY_FIELDS -> {
|
||||
if (entryInfoKey != null) {
|
||||
|
||||
@@ -186,6 +186,39 @@ class EntryInfo : NodeInfo {
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is EntryInfo) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
if (username != other.username) return false
|
||||
if (password != other.password) return false
|
||||
if (url != other.url) return false
|
||||
if (notes != other.notes) return false
|
||||
if (customFields != other.customFields) return false
|
||||
if (attachments != other.attachments) return false
|
||||
if (otpModel != other.otpModel) return false
|
||||
if (isTemplate != other.isTemplate) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + id.hashCode()
|
||||
result = 31 * result + username.hashCode()
|
||||
result = 31 * result + password.hashCode()
|
||||
result = 31 * result + url.hashCode()
|
||||
result = 31 * result + notes.hashCode()
|
||||
result = 31 * result + customFields.hashCode()
|
||||
result = 31 * result + attachments.hashCode()
|
||||
result = 31 * result + (otpModel?.hashCode() ?: 0)
|
||||
result = 31 * result + isTemplate.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
const val WEB_DOMAIN_FIELD_NAME = "URL"
|
||||
|
||||
@@ -24,6 +24,22 @@ class GroupInfo : NodeInfo {
|
||||
parcel.writeString(notes)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is GroupInfo) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
if (notes != other.notes) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + (notes?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<GroupInfo> {
|
||||
override fun createFromParcel(parcel: Parcel): GroupInfo {
|
||||
return GroupInfo(parcel)
|
||||
|
||||
@@ -36,6 +36,30 @@ open class NodeInfo() : Parcelable {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is NodeInfo) return false
|
||||
|
||||
if (title != other.title) return false
|
||||
if (icon != other.icon) return false
|
||||
if (creationTime != other.creationTime) return false
|
||||
if (lastModificationTime != other.lastModificationTime) return false
|
||||
if (expires != other.expires) return false
|
||||
if (expiryTime != other.expiryTime) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = title.hashCode()
|
||||
result = 31 * result + icon.hashCode()
|
||||
result = 31 * result + creationTime.hashCode()
|
||||
result = 31 * result + lastModificationTime.hashCode()
|
||||
result = 31 * result + expires.hashCode()
|
||||
result = 31 * result + expiryTime.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<NodeInfo> {
|
||||
override fun createFromParcel(parcel: Parcel): NodeInfo {
|
||||
return NodeInfo(parcel)
|
||||
|
||||
@@ -14,7 +14,7 @@ import kotlinx.coroutines.launch
|
||||
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
||||
|
||||
class SearchInfo : ObjectNameResource, Parcelable {
|
||||
|
||||
var manualSelection: Boolean = false
|
||||
var applicationId: String? = null
|
||||
set(value) {
|
||||
field = when {
|
||||
@@ -42,6 +42,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
constructor()
|
||||
|
||||
constructor(toCopy: SearchInfo?) {
|
||||
manualSelection = toCopy?.manualSelection ?: manualSelection
|
||||
applicationId = toCopy?.applicationId
|
||||
webDomain = toCopy?.webDomain
|
||||
webScheme = toCopy?.webScheme
|
||||
@@ -49,6 +50,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
}
|
||||
|
||||
private constructor(parcel: Parcel) {
|
||||
manualSelection = parcel.readByte().toInt() != 0
|
||||
val readAppId = parcel.readString()
|
||||
applicationId = if (readAppId.isNullOrEmpty()) null else readAppId
|
||||
val readDomain = parcel.readString()
|
||||
@@ -64,6 +66,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeByte((if (manualSelection) 1 else 0).toByte())
|
||||
parcel.writeString(applicationId ?: "")
|
||||
parcel.writeString(webDomain ?: "")
|
||||
parcel.writeString(webScheme ?: "")
|
||||
@@ -88,10 +91,9 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as SearchInfo
|
||||
if (other !is SearchInfo) return false
|
||||
|
||||
if (manualSelection != other.manualSelection) return false
|
||||
if (applicationId != other.applicationId) return false
|
||||
if (webDomain != other.webDomain) return false
|
||||
if (webScheme != other.webScheme) return false
|
||||
@@ -101,7 +103,8 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = applicationId?.hashCode() ?: 0
|
||||
var result = manualSelection.hashCode()
|
||||
result = 31 * result + (applicationId?.hashCode() ?: 0)
|
||||
result = 31 * result + (webDomain?.hashCode() ?: 0)
|
||||
result = 31 * result + (webScheme?.hashCode() ?: 0)
|
||||
result = 31 * result + (otpString?.hashCode() ?: 0)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -493,6 +493,12 @@ object PreferencesUtil {
|
||||
context.resources.getBoolean(R.bool.autofill_inline_suggestions_default))
|
||||
}
|
||||
|
||||
fun isAutofillManualSelectionEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.autofill_manual_selection_key),
|
||||
context.resources.getBoolean(R.bool.autofill_manual_selection_default))
|
||||
}
|
||||
|
||||
fun isAutofillSaveSearchInfoEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.autofill_save_search_info_key),
|
||||
@@ -630,6 +636,7 @@ object PreferencesUtil {
|
||||
context.getString(R.string.autofill_close_database_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.autofill_auto_search_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.autofill_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.autofill_manual_selection_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.autofill_save_search_info_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.autofill_ask_to_save_data_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.autofill_application_id_blocklist_key) -> editor.putStringSet(name, getStringSetFromProperties(value))
|
||||
|
||||
@@ -64,11 +64,13 @@ class DurationDialogFragmentCompat : InputPreferenceDialogFragmentCompat() {
|
||||
|
||||
private fun durationToDaysHoursMinutesSeconds(duration: Long) {
|
||||
if (duration < 0) {
|
||||
mEnabled = false
|
||||
mDays = 0
|
||||
mHours = 0
|
||||
mMinutes = 0
|
||||
mSeconds = 0
|
||||
} else {
|
||||
mEnabled = true
|
||||
mDays = (duration / (24L * 60L * 60L * 1000L)).toInt()
|
||||
val daysMilliseconds = mDays * 24L * 60L * 60L * 1000L
|
||||
mHours = ((duration - daysMilliseconds) / (60L * 60L * 1000L)).toInt()
|
||||
@@ -127,7 +129,7 @@ class DurationDialogFragmentCompat : InputPreferenceDialogFragmentCompat() {
|
||||
|
||||
setSwitchAction({ isChecked ->
|
||||
mEnabled = isChecked
|
||||
}, mDays + mHours + mMinutes + mSeconds > 0)
|
||||
}, mEnabled)
|
||||
|
||||
assignValuesInViews()
|
||||
}
|
||||
|
||||
@@ -155,6 +155,10 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
|
||||
}
|
||||
}
|
||||
|
||||
fun isSwitchActivated(): Boolean {
|
||||
return switchElementView?.isChecked == true
|
||||
}
|
||||
|
||||
fun activateSwitch() {
|
||||
if (switchElementView?.isChecked != true)
|
||||
switchElementView?.isChecked = true
|
||||
|
||||
27
app/src/main/java/com/kunzisoft/keepass/utils/DexUtil.kt
Normal file
27
app/src/main/java/com/kunzisoft/keepass/utils/DexUtil.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,6 @@ class DateTimeFieldView @JvmOverloads constructor(context: Context,
|
||||
|
||||
private var mDefault: DateInstant = DateInstant.NEVER_EXPIRES
|
||||
|
||||
var setOnDateClickListener: ((DateInstant) -> Unit)? = null
|
||||
|
||||
init {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
inflater?.inflate(R.layout.view_date_time, this)
|
||||
|
||||
@@ -275,22 +275,26 @@ abstract class TemplateAbstractView<
|
||||
templateAttribute: TemplateAttribute,
|
||||
entryInfoValue: String,
|
||||
showEmptyFields: Boolean) {
|
||||
var fieldView: TEntryFieldView? = findViewWithTag(fieldTag)
|
||||
if (!showEmptyFields && entryInfoValue.isEmpty()) {
|
||||
fieldView?.isFieldVisible = false
|
||||
} else if (fieldView == null && entryInfoValue.isNotEmpty()) {
|
||||
// Add new not referenced view if standard field not in template
|
||||
fieldView = buildViewForNotReferencedField(
|
||||
Field(templateAttribute.label,
|
||||
ProtectedString(templateAttribute.protected, "")),
|
||||
templateAttribute
|
||||
) as? TEntryFieldView?
|
||||
fieldView?.let {
|
||||
addNotReferencedView(it as View)
|
||||
try {
|
||||
var fieldView: TEntryFieldView? = findViewWithTag(fieldTag)
|
||||
if (!showEmptyFields && entryInfoValue.isEmpty()) {
|
||||
fieldView?.isFieldVisible = false
|
||||
} else if (fieldView == null && entryInfoValue.isNotEmpty()) {
|
||||
// Add new not referenced view if standard field not in template
|
||||
fieldView = buildViewForNotReferencedField(
|
||||
Field(templateAttribute.label,
|
||||
ProtectedString(templateAttribute.protected, "")),
|
||||
templateAttribute
|
||||
) as? TEntryFieldView?
|
||||
fieldView?.let {
|
||||
addNotReferencedView(it as View)
|
||||
}
|
||||
}
|
||||
fieldView?.value = entryInfoValue
|
||||
fieldView?.applyFontVisibility(mFontInVisibility)
|
||||
} catch(e: Exception) {
|
||||
Log.e(TAG, "Unable to populate entry field view", e)
|
||||
}
|
||||
fieldView?.value = entryInfoValue
|
||||
fieldView?.applyFontVisibility(mFontInVisibility)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -299,22 +303,25 @@ abstract class TemplateAbstractView<
|
||||
expires: Boolean,
|
||||
expiryTime: DateInstant,
|
||||
showEmptyFields: Boolean) {
|
||||
|
||||
var fieldView: TDateTimeView? = findViewWithTag(fieldTag)
|
||||
if (!showEmptyFields && !expires) {
|
||||
fieldView?.isFieldVisible = false
|
||||
} else if (fieldView == null && expires) {
|
||||
fieldView = buildViewForNotReferencedField(
|
||||
Field(templateAttribute.label,
|
||||
ProtectedString(templateAttribute.protected, "")),
|
||||
templateAttribute
|
||||
) as? TDateTimeView?
|
||||
fieldView?.let {
|
||||
addNotReferencedView(it as View)
|
||||
try {
|
||||
var fieldView: TDateTimeView? = findViewWithTag(fieldTag)
|
||||
if (!showEmptyFields && !expires) {
|
||||
fieldView?.isFieldVisible = false
|
||||
} else if (fieldView == null && expires) {
|
||||
fieldView = buildViewForNotReferencedField(
|
||||
Field(templateAttribute.label,
|
||||
ProtectedString(templateAttribute.protected, "")),
|
||||
templateAttribute
|
||||
) as? TDateTimeView?
|
||||
fieldView?.let {
|
||||
addNotReferencedView(it as View)
|
||||
}
|
||||
}
|
||||
fieldView?.activation = expires
|
||||
fieldView?.dateTime = expiryTime
|
||||
} catch(e: Exception) {
|
||||
Log.e(TAG, "Unable to populate date time view", e)
|
||||
}
|
||||
fieldView?.activation = expires
|
||||
fieldView?.dateTime = expiryTime
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -53,7 +53,6 @@ class TemplateView @JvmOverloads constructor(context: Context,
|
||||
label = templateAttribute.alias
|
||||
?: TemplateField.getLocalizedName(context, field.name)
|
||||
setMaxChars(templateAttribute.options.getNumberChars())
|
||||
setMaxLines(templateAttribute.options.getNumberLines())
|
||||
// TODO Linkify
|
||||
value = field.protectedValue.stringValue
|
||||
// Here the value is often empty
|
||||
|
||||
@@ -187,6 +187,6 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
|
||||
|
||||
companion object {
|
||||
const val MAX_CHARS_LIMIT = Integer.MAX_VALUE
|
||||
const val MAX_LINES_LIMIT = 40
|
||||
const val MAX_LINES_LIMIT = Integer.MAX_VALUE
|
||||
}
|
||||
}
|
||||
@@ -214,18 +214,6 @@ class TextFieldView @JvmOverloads constructor(context: Context,
|
||||
}
|
||||
}
|
||||
|
||||
fun setMaxLines(numberLines: Int) {
|
||||
when {
|
||||
numberLines <= 0 -> {
|
||||
valueView.maxLines = MAX_LINES_LIMIT
|
||||
}
|
||||
else -> {
|
||||
val lines = if (numberLines > MAX_LINES_LIMIT) MAX_LINES_LIMIT else numberLines
|
||||
valueView.maxLines = lines
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setProtection(protection: Boolean, hiddenProtectedValue: Boolean = false) {
|
||||
showButton.isVisible = protection
|
||||
showButton.isSelected = hiddenProtectedValue
|
||||
@@ -343,6 +331,5 @@ class TextFieldView @JvmOverloads constructor(context: Context,
|
||||
|
||||
companion object {
|
||||
const val MAX_CHARS_LIMIT = Integer.MAX_VALUE
|
||||
const val MAX_LINES_LIMIT = 40
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,28 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.kunzisoft.keepass.app.database.IOActionTask
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.template.Template
|
||||
import com.kunzisoft.keepass.model.*
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import java.util.*
|
||||
|
||||
|
||||
class EntryEditViewModel: NodeEditViewModel() {
|
||||
|
||||
private var mEntryId: NodeId<UUID>? = null
|
||||
private var mParentId: NodeId<*>? = null
|
||||
private var mRegisterInfo: RegisterInfo? = null
|
||||
private var mSearchInfo: SearchInfo? = null
|
||||
private var mParent: Group? = null
|
||||
private var mEntry: Entry? = null
|
||||
private var mIsTemplate: Boolean = false
|
||||
private val mTempAttachments = mutableListOf<EntryAttachmentState>()
|
||||
|
||||
val templatesEntry : LiveData<TemplatesEntry> get() = _templatesEntry
|
||||
private val _templatesEntry = MutableLiveData<TemplatesEntry>()
|
||||
val templatesEntry : LiveData<TemplatesEntry?> get() = _templatesEntry
|
||||
private val _templatesEntry = MutableLiveData<TemplatesEntry?>()
|
||||
|
||||
val requestEntryInfoUpdate : LiveData<EntryUpdate> get() = _requestEntryInfoUpdate
|
||||
private val _requestEntryInfoUpdate = SingleLiveEvent<EntryUpdate>()
|
||||
@@ -23,7 +34,7 @@ class EntryEditViewModel: NodeEditViewModel() {
|
||||
private val _onEntrySaved = SingleLiveEvent<EntrySave>()
|
||||
|
||||
val onTemplateChanged : LiveData<Template> get() = _onTemplateChanged
|
||||
private val _onTemplateChanged = SingleLiveEvent<Template>()
|
||||
private val _onTemplateChanged = MutableLiveData<Template>()
|
||||
|
||||
val requestPasswordSelection : LiveData<Field> get() = _requestPasswordSelection
|
||||
private val _requestPasswordSelection = SingleLiveEvent<Field>()
|
||||
@@ -53,39 +64,118 @@ class EntryEditViewModel: NodeEditViewModel() {
|
||||
val onBinaryPreviewLoaded : LiveData<AttachmentPosition> get() = _onBinaryPreviewLoaded
|
||||
private val _onBinaryPreviewLoaded = SingleLiveEvent<AttachmentPosition>()
|
||||
|
||||
fun loadDatabase(database: Database?) {
|
||||
loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo, mSearchInfo)
|
||||
}
|
||||
|
||||
fun loadTemplateEntry(database: Database,
|
||||
entry: Entry?,
|
||||
isTemplate: Boolean,
|
||||
fun loadTemplateEntry(database: Database?,
|
||||
entryId: NodeId<UUID>?,
|
||||
parentId: NodeId<*>?,
|
||||
registerInfo: RegisterInfo?,
|
||||
searchInfo: SearchInfo?) {
|
||||
IOActionTask(
|
||||
{
|
||||
val templates = database.getTemplates(isTemplate)
|
||||
val entryTemplate = entry?.let { database.getTemplate(it) } ?: Template.STANDARD
|
||||
var entryInfo: EntryInfo? = null
|
||||
// Decode the entry / load entry info
|
||||
entry?.let {
|
||||
database.decodeEntryWithTemplateConfiguration(it).let { entry ->
|
||||
// Load entry info
|
||||
entry.getEntryInfo(database, true).let { tempEntryInfo ->
|
||||
// Retrieve data from registration
|
||||
(registerInfo?.searchInfo ?: searchInfo)?.let { tempSearchInfo ->
|
||||
tempEntryInfo.saveSearchInfo(database, tempSearchInfo)
|
||||
this.mEntryId = entryId
|
||||
this.mParentId = parentId
|
||||
this.mRegisterInfo = registerInfo
|
||||
this.mSearchInfo = searchInfo
|
||||
|
||||
database?.let {
|
||||
mEntryId?.let {
|
||||
IOActionTask(
|
||||
{
|
||||
// Create an Entry copy to modify from the database entry
|
||||
mEntry = database.getEntryById(it)
|
||||
// Retrieve the parent
|
||||
mEntry?.let { entry ->
|
||||
// If no parent, add root group as parent
|
||||
if (entry.parent == null) {
|
||||
entry.parent = database.rootGroup
|
||||
}
|
||||
registerInfo?.let { regInfo ->
|
||||
tempEntryInfo.saveRegisterInfo(database, regInfo)
|
||||
}
|
||||
entryInfo = tempEntryInfo
|
||||
// Define if current entry is a template (in direct template group)
|
||||
mIsTemplate = database.entryIsTemplate(mEntry)
|
||||
decodeTemplateEntry(
|
||||
database,
|
||||
entry,
|
||||
mIsTemplate,
|
||||
registerInfo,
|
||||
searchInfo
|
||||
)
|
||||
}
|
||||
},
|
||||
{ templatesEntry ->
|
||||
mEntryId = null
|
||||
_templatesEntry.value = templatesEntry
|
||||
}
|
||||
}
|
||||
TemplatesEntry(templates, entryTemplate, entryInfo)
|
||||
},
|
||||
{ templatesEntry ->
|
||||
_templatesEntry.value = templatesEntry
|
||||
).execute()
|
||||
}
|
||||
).execute()
|
||||
|
||||
mParentId?.let {
|
||||
IOActionTask(
|
||||
{
|
||||
mParent = database.getGroupById(it)
|
||||
mParent?.let { parentGroup ->
|
||||
mEntry = database.createEntry()?.apply {
|
||||
// Add the default icon from parent if not a folder
|
||||
val parentIcon = parentGroup.icon
|
||||
// Set default icon
|
||||
if (parentIcon.custom.isUnknown
|
||||
&& parentIcon.standard.id != IconImageStandard.FOLDER_ID
|
||||
) {
|
||||
icon = IconImage(parentIcon.standard)
|
||||
}
|
||||
if (!parentIcon.custom.isUnknown) {
|
||||
icon = IconImage(parentIcon.custom)
|
||||
}
|
||||
// Set default username
|
||||
username = database.defaultUsername
|
||||
// Warning only the entry recognize is parent, parent don't yet recognize the new entry
|
||||
// Useful to recognize child state (ie: entry is a template)
|
||||
parent = parentGroup
|
||||
}
|
||||
mIsTemplate = database.entryIsTemplate(mEntry)
|
||||
decodeTemplateEntry(
|
||||
database,
|
||||
mEntry,
|
||||
mIsTemplate,
|
||||
registerInfo,
|
||||
searchInfo
|
||||
)
|
||||
}
|
||||
},
|
||||
{ templatesEntry ->
|
||||
mParentId = null
|
||||
_templatesEntry.value = templatesEntry
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeTemplateEntry(database: Database,
|
||||
entry: Entry?,
|
||||
isTemplate: Boolean,
|
||||
registerInfo: RegisterInfo?,
|
||||
searchInfo: SearchInfo?): TemplatesEntry {
|
||||
val templates = database.getTemplates(isTemplate)
|
||||
val entryTemplate = entry?.let { database.getTemplate(it) }
|
||||
?: Template.STANDARD
|
||||
var entryInfo: EntryInfo? = null
|
||||
// Decode the entry / load entry info
|
||||
entry?.let {
|
||||
database.decodeEntryWithTemplateConfiguration(it).let { entry ->
|
||||
// Load entry info
|
||||
entry.getEntryInfo(database, true).let { tempEntryInfo ->
|
||||
// Retrieve data from registration
|
||||
(registerInfo?.searchInfo ?: searchInfo)?.let { tempSearchInfo ->
|
||||
tempEntryInfo.saveSearchInfo(database, tempSearchInfo)
|
||||
}
|
||||
registerInfo?.let { regInfo ->
|
||||
tempEntryInfo.saveRegisterInfo(database, regInfo)
|
||||
}
|
||||
entryInfo = tempEntryInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
return TemplatesEntry(isTemplate, templates, entryTemplate, entryInfo)
|
||||
}
|
||||
|
||||
fun changeTemplate(template: Template) {
|
||||
@@ -94,8 +184,8 @@ class EntryEditViewModel: NodeEditViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun requestEntryInfoUpdate(database: Database?, entry: Entry?, parent: Group?) {
|
||||
_requestEntryInfoUpdate.value = EntryUpdate(database, entry, parent)
|
||||
fun requestEntryInfoUpdate(database: Database?) {
|
||||
_requestEntryInfoUpdate.value = EntryUpdate(database, mEntry, mParent)
|
||||
}
|
||||
|
||||
fun saveEntryInfo(database: Database?, entry: Entry?, parent: Group?, entryInfo: EntryInfo) {
|
||||
@@ -222,7 +312,10 @@ class EntryEditViewModel: NodeEditViewModel() {
|
||||
_onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition)
|
||||
}
|
||||
|
||||
data class TemplatesEntry(val templates: List<Template>, val defaultTemplate: Template, val entryInfo: EntryInfo?)
|
||||
data class TemplatesEntry(val isTemplate: Boolean,
|
||||
val templates: List<Template>,
|
||||
val defaultTemplate: Template,
|
||||
val entryInfo: EntryInfo?)
|
||||
data class EntryUpdate(val database: Database?, val entry: Entry?, val parent: Group?)
|
||||
data class EntrySave(val oldEntry: Entry, val newEntry: Entry, val parent: Group?)
|
||||
data class FieldEdition(val oldField: Field?, val newField: Field?)
|
||||
|
||||
@@ -36,20 +36,14 @@ import java.util.*
|
||||
|
||||
class EntryViewModel: ViewModel() {
|
||||
|
||||
val template : LiveData<Template> get() = _template
|
||||
private val _template = MutableLiveData<Template>()
|
||||
private var mMainEntryId: NodeId<UUID>? = null
|
||||
private var mHistoryPosition: Int = -1
|
||||
|
||||
val mainEntryId : LiveData<NodeId<UUID>?> get() = _mainEntryId
|
||||
private val _mainEntryId = MutableLiveData<NodeId<UUID>?>()
|
||||
val entryInfoHistory : LiveData<EntryInfoHistory?> get() = _entryInfoHistory
|
||||
private val _entryInfoHistory = MutableLiveData<EntryInfoHistory?>()
|
||||
|
||||
val historyPosition : LiveData<Int> get() = _historyPosition
|
||||
private val _historyPosition = MutableLiveData<Int>()
|
||||
|
||||
val entryInfo : LiveData<EntryInfo> get() = _entryInfo
|
||||
private val _entryInfo = MutableLiveData<EntryInfo>()
|
||||
|
||||
val entryHistory : LiveData<List<EntryInfo>> get() = _entryHistory
|
||||
private val _entryHistory = MutableLiveData<List<EntryInfo>>()
|
||||
val entryHistory : LiveData<List<EntryInfo>?> get() = _entryHistory
|
||||
private val _entryHistory = MutableLiveData<List<EntryInfo>?>()
|
||||
|
||||
val onOtpElementUpdated : LiveData<OtpElement?> get() = _onOtpElementUpdated
|
||||
private val _onOtpElementUpdated = SingleLiveEvent<OtpElement?>()
|
||||
@@ -62,11 +56,18 @@ class EntryViewModel: ViewModel() {
|
||||
val historySelected : LiveData<EntryHistory> get() = _historySelected
|
||||
private val _historySelected = SingleLiveEvent<EntryHistory>()
|
||||
|
||||
fun loadEntry(database: Database?, entryId: NodeId<UUID>?, historyPosition: Int) {
|
||||
if (database != null && entryId != null) {
|
||||
fun loadDatabase(database: Database?) {
|
||||
loadEntry(database, mMainEntryId, mHistoryPosition)
|
||||
}
|
||||
|
||||
fun loadEntry(database: Database?, mainEntryId: NodeId<UUID>?, historyPosition: Int = -1) {
|
||||
this.mMainEntryId = mainEntryId
|
||||
this.mHistoryPosition = historyPosition
|
||||
|
||||
if (database != null && mainEntryId != null) {
|
||||
IOActionTask(
|
||||
{
|
||||
val mainEntry = database.getEntryById(entryId)
|
||||
val mainEntry = database.getEntryById(mainEntryId)
|
||||
val currentEntry = if (historyPosition > -1) {
|
||||
mainEntry?.getHistory()?.get(historyPosition)
|
||||
} else {
|
||||
@@ -91,6 +92,7 @@ class EntryViewModel: ViewModel() {
|
||||
|
||||
EntryInfoHistory(
|
||||
mainEntry!!.nodeId,
|
||||
historyPosition,
|
||||
entryTemplate,
|
||||
it.getEntryInfo(database),
|
||||
entryInfoHistory
|
||||
@@ -99,22 +101,13 @@ class EntryViewModel: ViewModel() {
|
||||
}
|
||||
},
|
||||
{ entryInfoHistory ->
|
||||
if (entryInfoHistory != null) {
|
||||
_mainEntryId.value = entryInfoHistory.mainEntryId
|
||||
_historyPosition.value = historyPosition
|
||||
_template.value = entryInfoHistory.template
|
||||
_entryInfo.value = entryInfoHistory.entryInfo
|
||||
_entryHistory.value = entryInfoHistory.entryHistory
|
||||
}
|
||||
_entryInfoHistory.value = entryInfoHistory
|
||||
_entryHistory.value = entryInfoHistory?.entryHistory
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateEntry(database: Database?) {
|
||||
loadEntry(database, _mainEntryId.value, _historyPosition.value ?: -1)
|
||||
}
|
||||
|
||||
fun onOtpElementUpdated(optElement: OtpElement?) {
|
||||
_onOtpElementUpdated.value = optElement
|
||||
}
|
||||
@@ -132,6 +125,7 @@ class EntryViewModel: ViewModel() {
|
||||
}
|
||||
|
||||
data class EntryInfoHistory(var mainEntryId: NodeId<UUID>,
|
||||
var historyPosition: Int,
|
||||
val template: Template,
|
||||
val entryInfo: EntryInfo,
|
||||
val entryHistory: List<EntryInfo>)
|
||||
|
||||
9
app/src/main/res/drawable/ic_arrow_right_green_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_arrow_right_green_24dp.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="@color/green"
|
||||
android:pathData="M10,17l5,-5 -5,-5v10z"/>
|
||||
</vector>
|
||||
@@ -95,7 +95,7 @@
|
||||
style="@style/KeepassDXStyle.TextAppearance.Info" />
|
||||
</RelativeLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
style="@style/KeepassDXStyle.TextAppearance.Title"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?android:attr/textColor"/>
|
||||
|
||||
<TextView
|
||||
@@ -62,7 +63,7 @@
|
||||
app:indicatorColor="?attr/colorAccent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:indeterminate="true"
|
||||
android:max="100"/>
|
||||
|
||||
|
||||
@@ -23,15 +23,15 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/autofill_entry_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginRight="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginLeft="12dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:contentDescription="@string/content_description_entry_icon"
|
||||
android:src="@drawable/ic_key_white_24dp" />
|
||||
android:src="@drawable/ic_arrow_right_green_24dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/autofill_entry_text"
|
||||
|
||||
42
app/src/main/res/layout/item_autofill_select_entry.xml
Normal file
42
app/src/main/res/layout/item_autofill_select_entry.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/windowBackground"
|
||||
android:minHeight="36dp"
|
||||
android:orientation="horizontal">
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginLeft="12dp"
|
||||
android:contentDescription="@string/autofill_select_entry"
|
||||
android:src="@drawable/ic_arrow_right_green_24dp"/>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/autofill_select_entry"
|
||||
android:layout_gravity="center"
|
||||
android:paddingRight="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingStart="12dp"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSmall"/>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
<include
|
||||
layout="@layout/item_autofill_app_id"/>
|
||||
<include
|
||||
layout="@layout/item_autofill_select_entry"/>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
<include
|
||||
layout="@layout/item_autofill_web_domain"/>
|
||||
<include
|
||||
layout="@layout/item_autofill_select_entry"/>
|
||||
</LinearLayout>
|
||||
@@ -30,8 +30,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:minHeight="56dp"
|
||||
android:maxHeight="72dp"
|
||||
android:minHeight="48dp"
|
||||
app:layout_constraintWidth_percent="@dimen/content_percent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
@@ -104,10 +103,12 @@
|
||||
tools:text="7543A7EAB2EA7CFD1394F1615EBEB08C" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<LinearLayout
|
||||
android:id="@+id/node_options"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="end"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginLeft="12dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
@@ -118,9 +119,11 @@
|
||||
android:id="@+id/node_otp_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginRight="12dp"
|
||||
android:padding="4dp"
|
||||
android:orientation="horizontal"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/node_attachment_icon"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
@@ -162,7 +165,7 @@
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/node_otp_container" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -30,8 +30,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:minHeight="56dp"
|
||||
android:maxHeight="72dp"
|
||||
android:minHeight="48dp"
|
||||
app:layout_constraintWidth_percent="@dimen/content_percent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
@@ -89,18 +88,6 @@
|
||||
android:maxLines="2"
|
||||
tools:text="Node Title" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/node_subtext"
|
||||
style="@style/KeepassDXStyle.TextAppearance.Group.SubTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-4dp"
|
||||
android:gravity="center_vertical"
|
||||
android:lines="1"
|
||||
android:singleLine="true"
|
||||
android:visibility="gone"
|
||||
tools:text="Node SubTitle" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/node_meta"
|
||||
style="@style/KeepassDXStyle.TextAppearance.Group.Meta"
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
android:focusable="false"
|
||||
android:cursorVisible="false"
|
||||
android:focusableInTouchMode="false"
|
||||
style="@style/KeepassDXStyle.TextAppearance.Large"
|
||||
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem"
|
||||
tools:text="2020-03-04 05:00" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
|
||||
@@ -203,6 +203,7 @@
|
||||
<string name="autofill_sign_in_prompt">Mit KeePassDX anmelden</string>
|
||||
<string name="set_autofill_service_title">Standarddienst für automatisches Ausfüllen festlegen</string>
|
||||
<string name="autofill_explanation_summary">Automatisches Ausfüllen aktivieren, um Formulare schnell in anderen Apps auszufüllen</string>
|
||||
<string name="autofill_select_entry">Eintrag auswählen…</string>
|
||||
<string name="clipboard">Zwischenablage</string>
|
||||
<string name="biometric_delete_all_key_title">Verschlüsselungsschlüssel löschen</string>
|
||||
<string name="biometric_delete_all_key_summary">Alle Verschlüsselungsschlüssel löschen, die mit der modernen Entsperrerkennung zusammenhängen</string>
|
||||
@@ -448,6 +449,8 @@
|
||||
<string name="validate">Validieren</string>
|
||||
<string name="autofill_auto_search_summary">Suchergebnisse automatisch nach Web-Domain oder Anwendungs-ID vorschlagen</string>
|
||||
<string name="autofill_auto_search_title">Automatische Suche</string>
|
||||
<string name="autofill_manual_selection_title">Manuelle Auswahl</string>
|
||||
<string name="autofill_manual_selection_summary">Manuelle Auswahl des Datenbank-Eintrags ermöglichen</string>
|
||||
<string name="lock_database_show_button_summary">Zeigt die Sperrtaste in der Benutzeroberfläche an</string>
|
||||
<string name="lock_database_show_button_title">Sperrtaste anzeigen</string>
|
||||
<string name="autofill_preference_title">Einstellungen für automatisches Ausfüllen</string>
|
||||
|
||||
@@ -171,6 +171,7 @@
|
||||
<string name="autofill_sign_in_prompt">Se connecter avec KeePassDX</string>
|
||||
<string name="set_autofill_service_title">Définir le service de remplissage automatique par défaut</string>
|
||||
<string name="autofill_explanation_summary">Activer le remplissage automatique pour remplir rapidement des formulaires dans d’autres applications</string>
|
||||
<string name="autofill_select_entry">Sélectionner une entrée…</string>
|
||||
<string name="password_size_title">Taille du mot de passe généré</string>
|
||||
<string name="password_size_summary">Défini la taille par défaut des mots de passe générés</string>
|
||||
<string name="list_password_generator_options_title">Caractères de mot de passe</string>
|
||||
|
||||
22
app/src/main/res/values-land/config.xml
Normal file
22
app/src/main/res/values-land/config.xml
Normal 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>
|
||||
22
app/src/main/res/values-sw430dp/config.xml
Normal file
22
app/src/main/res/values-sw430dp/config.xml
Normal 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>
|
||||
22
app/src/main/res/values/config.xml
Normal file
22
app/src/main/res/values/config.xml
Normal 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>
|
||||
@@ -159,6 +159,8 @@
|
||||
<bool name="autofill_auto_search_default" translatable="false">true</bool>
|
||||
<string name="autofill_inline_suggestions_key" translatable="false">autofill_inline_suggestions_key</string>
|
||||
<bool name="autofill_inline_suggestions_default" translatable="false">false</bool>
|
||||
<string name="autofill_manual_selection_key" translatable="false">autofill_manual_selection_key</string>
|
||||
<bool name="autofill_manual_selection_default" translatable="false">true</bool>
|
||||
<string name="autofill_save_search_info_key" translatable="false">autofill_save_search_info_key</string>
|
||||
<bool name="autofill_save_search_info_default" translatable="false">true</bool>
|
||||
<string name="autofill_ask_to_save_data_key" translatable="false">autofill_ask_to_save_data_key</string>
|
||||
|
||||
@@ -352,6 +352,7 @@
|
||||
<string name="autofill_service_name">KeePassDX form autofilling</string>
|
||||
<string name="autofill_sign_in_prompt">Sign in with KeePassDX</string>
|
||||
<string name="autofill_explanation_summary">Enable autofilling to quickly fill out forms in other apps</string>
|
||||
<string name="autofill_select_entry">Select entry…</string>
|
||||
<string name="set_autofill_service_title">Set default autofill service</string>
|
||||
<string name="autofill_preference_title">Autofill settings</string>
|
||||
<string name="password_size_title">Generated password size</string>
|
||||
@@ -366,7 +367,7 @@
|
||||
<string name="clipboard_warning">If automatic deletion of clipboard fails, delete its history manually.</string>
|
||||
<string name="lock">Lock</string>
|
||||
<string name="lock_database_screen_off_title">Screen lock</string>
|
||||
<string name="lock_database_screen_off_summary">Lock the database when the screen is off</string>
|
||||
<string name="lock_database_screen_off_summary">Lock the database after a few seconds once the screen is off</string>
|
||||
<string name="lock_database_back_root_title">Press \'Back\' to lock</string>
|
||||
<string name="lock_database_back_root_summary">Lock the database when the user clicks the back button on the root screen</string>
|
||||
<string name="lock_database_show_button_title">Show lock button</string>
|
||||
@@ -487,6 +488,8 @@
|
||||
<string name="autofill_auto_search_summary">Automatically suggest search results from the web domain or application ID</string>
|
||||
<string name="autofill_inline_suggestions_title">Inline suggestions</string>
|
||||
<string name="autofill_inline_suggestions_summary">Attempt to display autofill suggestions directly from a compatible keyboard</string>
|
||||
<string name="autofill_manual_selection_title">Manual selection</string>
|
||||
<string name="autofill_manual_selection_summary">Display option to let the user select database entry</string>
|
||||
<string name="autofill_save_search_info_title">Save search info</string>
|
||||
<string name="autofill_save_search_info_summary">Try to save search information when making a manual entry selection</string>
|
||||
<string name="autofill_ask_to_save_data_title">Ask to save data</string>
|
||||
|
||||
@@ -35,6 +35,11 @@
|
||||
android:title="@string/autofill_inline_suggestions_title"
|
||||
android:summary="@string/autofill_inline_suggestions_summary"
|
||||
android:defaultValue="@bool/autofill_inline_suggestions_default"/>
|
||||
<SwitchPreference
|
||||
android:key="@string/autofill_manual_selection_key"
|
||||
android:title="@string/autofill_manual_selection_title"
|
||||
android:summary="@string/autofill_manual_selection_summary"
|
||||
android:defaultValue="@bool/autofill_manual_selection_default"/>
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory
|
||||
android:title="@string/save">
|
||||
|
||||
@@ -1,6 +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
|
||||
7
fastlane/metadata/android/en-US/changelogs/87.txt
Normal file
7
fastlane/metadata/android/en-US/changelogs/87.txt
Normal 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)
|
||||
7
fastlane/metadata/android/en-US/changelogs/88.txt
Normal file
7
fastlane/metadata/android/en-US/changelogs/88.txt
Normal 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
|
||||
1
fastlane/metadata/android/en-US/changelogs/89.txt
Normal file
1
fastlane/metadata/android/en-US/changelogs/89.txt
Normal file
@@ -0,0 +1 @@
|
||||
* Samsung DeX mode #1114 #245 (Thx @chenxiaolong)
|
||||
@@ -3,4 +3,5 @@
|
||||
* Paramètres pour afficher les jetons OTP dans la liste #655
|
||||
* Correction du délai d'expiration dans les dialogues #716
|
||||
* Vérification des permissions URI #626
|
||||
* Améliorations #1035 #1043 #942 #1021 #1027
|
||||
* Meilleure implémentation du remplissage auto #943 #946 #984 #1070 (Thx @uduerholz)
|
||||
* Améliorations #680 #1035 #1043 #942 #1021 #1027 #1046 #1082 #1083 (Thx @chenxiaolong)
|
||||
7
fastlane/metadata/android/fr-FR/changelogs/88.txt
Normal file
7
fastlane/metadata/android/fr-FR/changelogs/88.txt
Normal 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
|
||||
1
fastlane/metadata/android/fr-FR/changelogs/89.txt
Normal file
1
fastlane/metadata/android/fr-FR/changelogs/89.txt
Normal file
@@ -0,0 +1 @@
|
||||
* Mode Samsung DeX #1114 #245 (Thx @chenxiaolong)
|
||||
Reference in New Issue
Block a user