Compare commits

..

1 Commits

Author SHA1 Message Date
J-Jamet
2b8427750b Add Go lib 2021-03-26 20:05:28 +01:00
365 changed files with 8626 additions and 17138 deletions

View File

@@ -1,66 +1,6 @@
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
KeePassDX(2.10.5)
* Increase the saving speed of database #1028
* Fix advanced unlocking by device credential #1029
KeePassDX(2.10.4)
* Hot fix to increase the opening speed of database #1028
KeePassDX(2.10.3)
* Improve Magikeyboard options description #1022 #1023 (Thx @djibux)
* Fix database opened without notification (database is now closed when screen is killed in background #1025)
* Fix biometric prompt #1018
KeePassDX(2.10.2)
* Fix search fields references #987
* Fix Auto-Types with same key #997
KeePassDX(2.10.1)
* Fix parcelable with custom data #986
KeePassDX(2.10.0)
* Manage new database format 4.1 #956
* Fix show button consistency #980
* Fix persistent notification #979
KeePassDX(2.9.20)
* Fix search with non-latin chars #971
* Fix action mode with search #972 (rollback ignore accents #945)
* Fix timeout with 0s #974
KeePassDX(2.9.19)
* Fix search slowdown #964
* Fix closing notification after lock request #965
* Better temp advanced unlocking code implementation #965
* Fix OTP token generation #967
KeePassDX(2.9.18)
* Move groups #658
* Improve autofill recognition #960
* Remove diacritical marks in search string #945
* Fix search in references #962
* Fix themes in Libre version
KeePassDX(2.9.17)
* Import / Export app properties #839
* Force twofish padding compatibility #955
* Better timeout preference #579
KeePassDX(2.9.16)
* Fix small bugs #948
KeePassDX(2.9.15)
* Fix themes #935 #926
* Fix themes #935
* Decrease default clipboard time #934
* Better opening performance #929 #933
* Fix memory usage setting #941
KeePassDX(2.9.14)
* Add custom icons #96

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass"
minSdkVersion 15
targetSdkVersion 30
versionCode = 85
versionName = "3.0.0_beta02"
versionCode = 66
versionName = "2.9.15"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
@@ -109,7 +109,7 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.1.0'
implementation 'androidx.biometric:biometric:1.1.0-rc01'
// Lifecycle - LiveData - ViewModel - Coroutines
implementation "androidx.core:core-ktx:1.3.2"
implementation 'androidx.fragment:fragment-ktx:1.2.5'
@@ -131,6 +131,7 @@ dependencies {
implementation 'commons-codec:commons-codec:1.15'
// Encrypt lib
implementation project(path: ':crypto')
implementation fileTree(include: ['encrypt.aar'], dir: 'libs')
// Icon pack
implementation project(path: ':icon-pack-classic')
implementation project(path: ':icon-pack-material')

BIN
app/libs/encrypt.aar Normal file

Binary file not shown.

View File

@@ -1,24 +0,0 @@
package com.kunzisoft.keepass.tests.template
import com.kunzisoft.keepass.database.element.template.TemplateAttributeOption
import junit.framework.TestCase
import org.junit.Assert
class TemplateAttributeOptionTest: TestCase() {
fun testSerializeOptions() {
val options = TemplateAttributeOption().apply {
put("TestA", "TestB")
put("{D", "}C")
put("E,gyu", "15,jk")
put("ù*:**", "78:96?545")
}
val strings = TemplateAttributeOption.getStringFromOptions(options)
val optionsAfterSerialization = TemplateAttributeOption.getOptionsFromString(strings)
val otherString = TemplateAttributeOption.getStringFromOptions(optionsAfterSerialization)
Assert.assertEquals("Output not equal to input", strings, otherString)
}
}

View File

@@ -1,15 +0,0 @@
package com.kunzisoft.keepass.tests.utils
import com.kunzisoft.keepass.utils.UuidUtil
import junit.framework.TestCase
import java.util.*
class UUIDTest: TestCase() {
fun testUUID() {
val randomUUID = UUID.randomUUID()
val hexStringUUID = UuidUtil.toHexString(randomUUID)
val retrievedUUID = UuidUtil.fromHexString(hexStringUUID)
assertEquals(randomUUID, retrievedUUID)
}
}

View File

@@ -45,7 +45,7 @@
android:label="@string/app_name"
android:launchMode="singleTop"
android:configChanges="keyboardHidden"
android:windowSoftInputMode="stateHidden|stateAlwaysHidden" >
android:windowSoftInputMode="stateHidden" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -112,7 +112,8 @@
<activity
android:name="com.kunzisoft.keepass.activities.GroupActivity"
android:configChanges="keyboardHidden"
android:windowSoftInputMode="adjustPan">
android:windowSoftInputMode="adjustPan"
android:launchMode="singleTask">
<meta-data
android:name="android.app.default_searchable"
android:value="com.kunzisoft.keepass.search.SearchResults"
@@ -208,7 +209,7 @@
</intent-filter>
</service>
<service
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
android:name="com.kunzisoft.keepass.magikeyboard.MagikIME"
android:label="@string/keyboard_label"
android:permission="android.permission.BIND_INPUT_METHOD" >
<meta-data android:name="android.view.im"

View File

@@ -30,7 +30,6 @@ package com.igreenwood.loupe
import android.animation.Animator
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.graphics.Matrix
import android.graphics.PointF
import android.graphics.Rect
@@ -109,8 +108,6 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
var viewDragFriction = DEFAULT_VIEW_DRAG_FRICTION
// drag distance threshold in dp for swipe to dismiss
var dragDismissDistanceInDp = DEFAULT_DRAG_DISMISS_DISTANCE_IN_DP
// on view touched
var onViewTouchedListener: View.OnTouchListener? = null
// on view translate listener
var onViewTranslateListener: OnViewTranslateListener? = null
// on scale changed
@@ -275,10 +272,7 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
private var imageViewRef: WeakReference<ImageView> = WeakReference(imageView)
private var containerRef: WeakReference<ViewGroup> = WeakReference(container)
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
onViewTouchedListener?.onTouch(view, event)
event ?: return false
val imageView = imageViewRef.get() ?: return false
val container = containerRef.get() ?: return false

View File

@@ -25,13 +25,14 @@ import android.content.Context
import android.content.Intent
import android.content.IntentSender
import android.os.Build
import android.os.Bundle
import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST
import com.kunzisoft.keepass.autofill.KeeAutofillService
@@ -43,18 +44,9 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION
@RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : DatabaseModeActivity() {
class AutofillLauncherActivity : AppCompatActivity() {
override fun applyCustomStyle(): Boolean {
return false
}
override fun finishActivityIfReloadRequested(): Boolean {
return true
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
override fun onCreate(savedInstanceState: Bundle?) {
// Retrieve selection mode
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
@@ -68,7 +60,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
}
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launchSelection(database, searchInfo)
launchSelection(searchInfo)
}
}
SpecialMode.REGISTRATION -> {
@@ -77,7 +69,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
val searchInfo = SearchInfo(registerInfo?.searchInfo)
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launchRegistration(database, searchInfo, registerInfo)
launchRegistration(searchInfo, registerInfo)
}
}
else -> {
@@ -87,10 +79,11 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
}
}
}
super.onCreate(savedInstanceState)
}
private fun launchSelection(database: Database?,
searchInfo: SearchInfo) {
private fun launchSelection(searchInfo: SearchInfo) {
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
@@ -105,22 +98,24 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
setResult(Activity.RESULT_CANCELED)
finish()
} else {
val database = Database.getInstance()
val readOnly = database.isReadOnly
// If database is open
SearchHelper.checkAutoSearchInfo(this,
database,
Database.getInstance(),
searchInfo,
{ openedDatabase, items ->
{ items ->
// Items found
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
AutofillHelper.buildResponseAndSetResult(this, items)
finish()
},
{ openedDatabase ->
{
// Show the database UI to select the entry
GroupActivity.launchForAutofillResult(this,
openedDatabase,
autofillComponent,
searchInfo,
false)
readOnly,
autofillComponent,
searchInfo,
false)
},
{
// If database not open
@@ -132,9 +127,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
}
}
private fun launchRegistration(database: Database?,
searchInfo: SearchInfo,
registerInfo: RegisterInfo?) {
private fun launchRegistration(searchInfo: SearchInfo, registerInfo: RegisterInfo?) {
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
PreferencesUtil.applicationIdBlocklist(this))
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
@@ -142,26 +135,25 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
showBlockRestartMessage()
setResult(Activity.RESULT_CANCELED)
} else {
val readOnly = database?.isReadOnly != false
val database = Database.getInstance()
val readOnly = database.isReadOnly
SearchHelper.checkAutoSearchInfo(this,
database,
searchInfo,
{ openedDatabase, _ ->
{ _ ->
if (!readOnly) {
// Show the database UI to select the entry
GroupActivity.launchForRegistration(this,
openedDatabase,
registerInfo)
registerInfo)
} else {
showReadOnlySaveMessage()
}
},
{ openedDatabase ->
{
if (!readOnly) {
// Show the database UI to select the entry
GroupActivity.launchForRegistration(this,
openedDatabase,
registerInfo)
registerInfo)
} else {
showReadOnlySaveMessage()
}

View File

@@ -32,62 +32,68 @@ import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import android.widget.ProgressBar
import androidx.activity.viewModels
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.fragments.EntryFragment
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryActivityEducation
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.EntryContentsView
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.EntryViewModel
import java.util.*
import kotlin.collections.HashMap
class EntryActivity : DatabaseLockActivity() {
class EntryActivity : LockingActivity() {
private var coordinatorLayout: CoordinatorLayout? = null
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var titleIconView: ImageView? = null
private var historyView: View? = null
private var entryContentsView: EntryContentsView? = null
private var entryProgress: ProgressBar? = null
private var lockView: View? = null
private var toolbar: Toolbar? = null
private var loadingView: ProgressBar? = null
private val mEntryViewModel: EntryViewModel by viewModels()
private var mDatabase: Database? = null
private var mMainEntryId: NodeId<UUID>? = null
private var mHistoryPosition: Int = -1
private var mEntryIsHistory: Boolean = false
private var mUrl: String? = null
private var mEntry: Entry? = null
private var mIsHistory: Boolean = false
private var mEntryLastVersion: Entry? = null
private var mEntryHistoryPosition: Int = -1
private var mShowPassword: Boolean = false
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
private var mExternalFileHelper: ExternalFileHelper? = null
private var mIcon: IconImage? = null
private var mIconColor: Int = 0
private var clipboardHelper: ClipboardHelper? = null
private var mFirstLaunchOfActivity: Boolean = false
private var iconColor: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -99,167 +105,60 @@ class EntryActivity : DatabaseLockActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
mDatabase = Database.getInstance()
mReadOnly = mDatabase!!.isReadOnly || mReadOnly
mShowPassword = !PreferencesUtil.isPasswordMask(this)
// Retrieve the textColor to tint the icon
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
iconColor = taIconColor.getColor(0, Color.BLACK)
taIconColor.recycle()
// Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set
invalidateOptionsMenu()
// Get views
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
titleIconView = findViewById(R.id.entry_icon)
historyView = findViewById(R.id.history_container)
entryContentsView = findViewById(R.id.entry_contents)
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
entryContentsView?.setAttachmentCipherKey(mDatabase)
entryProgress = findViewById(R.id.entry_progress)
lockView = findViewById(R.id.lock_button)
loadingView = findViewById(R.id.loading)
// Empty title
collapsingToolbarLayout?.title = " "
toolbar?.title = " "
// Retrieve the textColor to tint the icon
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
mIconColor = taIconColor.getColor(0, Color.BLACK)
taIconColor.recycle()
// Get Entry from UUID
try {
intent.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { entryId ->
mMainEntryId = entryId
intent.removeExtra(KEY_ENTRY)
mHistoryPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
intent.removeExtra(KEY_ENTRY_HISTORY_POSITION)
}
} catch (e: ClassCastException) {
Log.e(TAG, "Unable to retrieve the entry key")
}
// Init SAF manager
mExternalFileHelper = ExternalFileHelper(this)
// Init attachment service binder manager
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
lockView?.setOnClickListener {
lockAndExit()
}
mEntryViewModel.mainEntryId.observe(this) { mainEntryId ->
this.mMainEntryId = mainEntryId
invalidateOptionsMenu()
}
// Focus view to reinitialize timeout
coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
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()
}
// Init the clipboard helper
clipboardHelper = ClipboardHelper(this)
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
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)
// Init attachment service binder manager
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
when (actionTask) {
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
// Close the current activity after an history action
if (result.isSuccess)
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 ->
if (otpElement == null)
entryProgress?.visibility = View.GONE
when (otpElement?.type) {
// Only add token if HOTP
OtpType.HOTP -> {
entryProgress?.visibility = View.GONE
}
// Refresh view if TOTP
OtpType.TOTP -> {
entryProgress?.apply {
max = otpElement.period
progress = otpElement.secondsRemaining
visibility = View.VISIBLE
}
}
}
}
mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected ->
mExternalFileHelper?.createDocument(attachmentSelected.name)?.let { requestCode ->
mAttachmentsToDownload[requestCode] = attachmentSelected
}
}
mEntryViewModel.historySelected.observe(this) { historySelected ->
mDatabase?.let { database ->
launch(
this,
database,
historySelected.nodeId,
historySelected.historyPosition
)
}
}
}
override fun finishActivityIfReloadRequested(): Boolean {
return true
}
override fun viewToInvalidateTimeout(): View? {
return coordinatorLayout
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
mEntryViewModel.loadEntry(mDatabase, mMainEntryId, mHistoryPosition)
// Assign title icon
mIcon?.let { icon ->
titleIconView?.let { iconView ->
mIconDrawableFactory?.assignDatabaseIcon(iconView, icon, mIconColor)
}
}
}
override fun onDatabaseActionFinished(
database: Database,
actionTask: String,
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) {
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
// Close the current activity after an history action
if (result.isSuccess)
ACTION_DATABASE_RELOAD_TASK -> {
// Close the current activity
this.showActionErrorIfNeeded(result)
finish()
}
}
coordinatorLayout?.showActionErrorIfNeeded(result)
}
coordinatorLayout?.showActionErrorIfNeeded(result)
}
override fun onResume() {
@@ -272,14 +171,63 @@ class EntryActivity : DatabaseLockActivity() {
View.GONE
}
// Get Entry from UUID
try {
val keyEntry: NodeId<UUID>? = intent.getParcelableExtra(KEY_ENTRY)
if (keyEntry != null) {
mEntry = mDatabase?.getEntryById(keyEntry)
mEntryLastVersion = mEntry
}
} catch (e: ClassCastException) {
Log.e(TAG, "Unable to retrieve the entry key")
}
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, mEntryHistoryPosition)
mEntryHistoryPosition = historyPosition
if (historyPosition >= 0) {
mIsHistory = true
mEntry = mEntry?.getHistory()?.get(historyPosition)
}
if (mEntry == null) {
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show()
finish()
return
}
// Update last access time.
mEntry?.touch(modified = false, touchParents = false)
mEntry?.let { entry ->
// Fill data in resume to update from EntryEditActivity
fillEntryDataInContentsView(entry)
// Refresh Menu
invalidateOptionsMenu()
val entryInfo = entry.getEntryInfo(mDatabase)
// Manage entry copy to start notification if allowed
if (mFirstLaunchOfActivity) {
// 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)) {
MagikIME.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
}
}
}
mAttachmentFileBinderManager?.apply {
registerProgressTask()
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
mEntryViewModel.onAttachmentAction(entryAttachmentState)
if (entryAttachmentState.streamDirection != StreamDirection.UPLOAD) {
entryContentsView?.putAttachment(entryAttachmentState)
}
}
}
}
mFirstLaunchOfActivity = false
}
override fun onPause() {
@@ -288,17 +236,151 @@ class EntryActivity : DatabaseLockActivity() {
super.onPause()
}
private fun fillEntryDataInContentsView(entry: Entry) {
val entryInfo = entry.getEntryInfo(mDatabase)
// Assign title icon
titleIconView?.let { iconView ->
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, iconColor)
}
// Assign title text
val entryTitle = entryInfo.title
collapsingToolbarLayout?.title = entryTitle
toolbar?.title = entryTitle
// Assign basic fields
entryContentsView?.assignUserName(entryInfo.username) {
clipboardHelper?.timeoutCopyToClipboard(entryInfo.username,
getString(R.string.copy_field,
getString(R.string.entry_user_name)))
}
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
val allowCopyPasswordAndProtectedFields =
PreferencesUtil.allowCopyPasswordAndProtectedFields(this)
val showWarningClipboardDialogOnClickListener = View.OnClickListener {
AlertDialog.Builder(this@EntryActivity)
.setMessage(getString(R.string.allow_copy_password_warning) +
"\n\n" +
getString(R.string.clipboard_warning))
.create().apply {
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ ->
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true)
dialog.dismiss()
fillEntryDataInContentsView(entry)
}
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false)
dialog.dismiss()
fillEntryDataInContentsView(entry)
}
show()
}
}
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
View.OnClickListener {
clipboardHelper?.timeoutCopyToClipboard(entryInfo.password,
getString(R.string.copy_field,
getString(R.string.entry_password)))
}
} else {
// If dialog not already shown
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
showWarningClipboardDialogOnClickListener
} else {
null
}
}
entryContentsView?.assignPassword(entryInfo.password,
allowCopyPasswordAndProtectedFields,
onPasswordCopyClickListener)
//Assign OTP field
entry.getOtpElement()?.let { otpElement ->
entryContentsView?.assignOtp(otpElement, entryProgress) {
clipboardHelper?.timeoutCopyToClipboard(
otpElement.token,
getString(R.string.copy_field, getString(R.string.entry_otp))
)
}
}
entryContentsView?.assignURL(entryInfo.url)
entryContentsView?.assignNotes(entryInfo.notes)
// Assign custom fields
if (mDatabase?.allowEntryCustomFields() == true) {
entryContentsView?.clearExtraFields()
entryInfo.customFields.forEach { field ->
val label = field.name
// OTP field is already managed in dedicated view
if (label != OtpEntryFields.OTP_TOKEN_FIELD) {
val value = field.protectedValue
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
if (allowCopyProtectedField) {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
clipboardHelper?.timeoutCopyToClipboard(
value.toString(),
getString(R.string.copy_field, label)
)
}
} else {
// If dialog not already shown
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
} else {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
}
}
}
}
}
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
// Manage attachments
entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
createDocument(this, attachmentItem.name)?.let { requestCode ->
mAttachmentsToDownload[requestCode] = attachmentItem
}
}
// Assign dates
entryContentsView?.assignCreationDate(entryInfo.creationTime)
entryContentsView?.assignModificationDate(entryInfo.lastModificationTime)
entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime)
// Manage history
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
if (mIsHistory) {
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
taColorAccent.recycle()
}
entryContentsView?.assignHistory(entry.getHistory()) { historyItem, position ->
launch(this, historyItem, mReadOnly, position)
}
// Assign special data
entryContentsView?.assignUUID(entry.nodeId.id)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
// Reload the current id from database
mEntryViewModel.updateEntry(mDatabase)
}
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE ->
// Not directly get the entry from intent data but from database
mEntry?.let {
fillEntryDataInContentsView(it)
}
}
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
if (createdFileUri != null) {
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
mAttachmentFileBinderManager
@@ -313,18 +395,12 @@ class EntryActivity : DatabaseLockActivity() {
val inflater = menuInflater
MenuUtil.contributionMenuInflater(inflater, menu)
inflater.inflate(R.menu.entry, menu)
inflater.inflate(R.menu.database, menu)
if (mUrl?.isEmpty() != false) {
menu.findItem(R.id.menu_goto_url)?.isVisible = false
}
if (mEntryIsHistory && !mDatabaseReadOnly) {
if (mIsHistory && !mReadOnly) {
inflater.inflate(R.menu.entry_history, menu)
}
if (mEntryIsHistory || mDatabaseReadOnly) {
if (mIsHistory || mReadOnly) {
menu.findItem(R.id.menu_save_database)?.isVisible = false
menu.findItem(R.id.menu_edit)?.isVisible = false
}
@@ -332,6 +408,20 @@ class EntryActivity : DatabaseLockActivity() {
menu.findItem(R.id.menu_reload_database)?.isVisible = false
}
val gotoUrl = menu.findItem(R.id.menu_goto_url)
gotoUrl?.apply {
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
// so mEntry may not be set
if (mEntry == null) {
isVisible = false
} else {
if (mEntry?.url?.isEmpty() != false) {
// disable button if url is not available
isVisible = false
}
}
}
// Show education views
Handler(Looper.getMainLooper()).post { performedNextEducation(EntryActivityEducation(this), menu) }
@@ -340,18 +430,18 @@ class EntryActivity : DatabaseLockActivity() {
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
menu: Menu) {
val entryFragment = supportFragmentManager.findFragmentByTag(ENTRY_FRAGMENT_TAG)
as? EntryFragment?
val entryFieldCopyView: View? = entryFragment?.firstEntryFieldCopyView()
val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView()
val entryCopyEducationPerformed = entryFieldCopyView != null
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
entryFieldCopyView,
{
entryFragment.launchEntryCopyEducationAction()
},
{
performedNextEducation(entryActivityEducation, menu)
})
entryFieldCopyView,
{
val appNameString = getString(R.string.app_name)
clipboardHelper?.timeoutCopyToClipboard(appNameString,
getString(R.string.copy_field, appNameString))
},
{
performedNextEducation(entryActivityEducation, menu)
})
if (!entryCopyEducationPerformed) {
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
@@ -375,53 +465,60 @@ class EntryActivity : DatabaseLockActivity() {
return true
}
R.id.menu_edit -> {
mDatabase?.let { database ->
mMainEntryId?.let { entryId ->
EntryEditActivity.launchToUpdate(
this,
database,
entryId
)
}
mEntry?.let {
EntryEditActivity.launch(this@EntryActivity, it)
}
return true
}
R.id.menu_goto_url -> {
mUrl?.let { url ->
UriUtil.gotoUrl(this, url)
var url: String = mEntry?.url ?: ""
// Default http:// if no protocol specified
if (!url.contains("://")) {
url = "http://$url"
}
UriUtil.gotoUrl(this, url)
return true
}
R.id.menu_restore_entry_history -> {
mMainEntryId?.let { mainEntryId ->
restoreEntryHistory(
mainEntryId,
mHistoryPosition)
mEntryLastVersion?.let { mainEntry ->
mProgressDatabaseTaskProvider?.startDatabaseRestoreEntryHistory(
mainEntry,
mEntryHistoryPosition,
!mReadOnly && mAutoSaveEnable)
}
}
R.id.menu_delete_entry_history -> {
mMainEntryId?.let { mainEntryId ->
deleteEntryHistory(
mainEntryId,
mHistoryPosition)
mEntryLastVersion?.let { mainEntry ->
mProgressDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(
mainEntry,
mEntryHistoryPosition,
!mReadOnly && mAutoSaveEnable)
}
}
R.id.menu_save_database -> {
saveDatabase()
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
}
R.id.menu_reload_database -> {
reloadDatabase()
mProgressDatabaseTaskProvider?.startDatabaseReload(false)
}
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
}
return super.onOptionsItemSelected(item)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(KEY_FIRST_LAUNCH_ACTIVITY, mFirstLaunchOfActivity)
}
override fun finish() {
// Transit data in previous Activity after an update
Intent().apply {
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
setResult(EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE, this)
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry)
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, this)
}
super.finish()
}
@@ -429,46 +526,19 @@ class EntryActivity : DatabaseLockActivity() {
companion object {
private val TAG = EntryActivity::class.java.name
private const val KEY_FIRST_LAUNCH_ACTIVITY = "KEY_FIRST_LAUNCH_ACTIVITY"
const val KEY_ENTRY = "KEY_ENTRY"
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
/**
* Open standard Entry activity
*/
fun launch(activity: Activity,
database: Database,
entryId: NodeId<UUID>) {
if (database.loaded) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
activity.startActivityForResult(
intent,
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
)
}
}
}
/**
* Open history Entry activity
*/
fun launch(activity: Activity,
database: Database,
entryId: NodeId<UUID>,
historyPosition: Int) {
if (database.loaded) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
fun launch(activity: Activity, entry: Entry, readOnly: Boolean, historyPosition: Int? = null) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entry.nodeId)
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
if (historyPosition != null)
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
activity.startActivityForResult(
intent,
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
)
}
activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
}
}
}

View File

@@ -22,13 +22,14 @@ package com.kunzisoft.keepass.activities
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields
@@ -38,18 +39,10 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
* Activity to search or select entry in database,
* Commonly used with Magikeyboard
*/
class EntrySelectionLauncherActivity : DatabaseModeActivity() {
class EntrySelectionLauncherActivity : AppCompatActivity() {
override fun applyCustomStyle(): Boolean {
return false
}
override fun onCreate(savedInstanceState: Bundle?) {
override fun finishActivityIfReloadRequested(): Boolean {
return true
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
var sharedWebDomain: String? = null
var otpString: String? = null
@@ -75,39 +68,39 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
else -> {}
}
// Build domain search param
val searchInfo = SearchInfo().apply {
this.webDomain = sharedWebDomain
this.otpString = otpString
}
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launch(database, searchInfo)
launch(searchInfo)
}
super.onCreate(savedInstanceState)
}
private fun launch(database: Database?,
searchInfo: SearchInfo) {
private fun launch(searchInfo: SearchInfo) {
if (!searchInfo.containsOnlyNullValues()) {
// Setting to integrate Magikeyboard
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
// If database is open
val readOnly = database?.isReadOnly != false
val database = Database.getInstance()
val readOnly = database.isReadOnly
SearchHelper.checkAutoSearchInfo(this,
database,
searchInfo,
{ openedDatabase, items ->
{ items ->
// Items found
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForSaveResult(
this,
openedDatabase,
searchInfo,
false)
GroupActivity.launchForSaveResult(this,
searchInfo,
false)
} else {
Toast.makeText(applicationContext,
R.string.autofill_read_only_save,
@@ -118,32 +111,30 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
if (items.size == 1) {
// Automatically populate keyboard
val entryPopulate = items[0]
populateKeyboardAndMoveAppToBackground(
this,
populateKeyboardAndMoveAppToBackground(this,
entryPopulate,
intent)
} else {
// Select the one we want
GroupActivity.launchForKeyboardSelectionResult(this,
openedDatabase,
searchInfo,
true)
readOnly,
searchInfo,
true)
}
} else {
GroupActivity.launchForSearchResult(this,
openedDatabase,
searchInfo,
true)
readOnly,
searchInfo,
true)
}
},
{ openedDatabase ->
{
// Show the database UI to select the entry
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForSaveResult(this,
openedDatabase,
searchInfo,
false)
searchInfo,
false)
} else {
Toast.makeText(applicationContext,
R.string.autofill_read_only_save,
@@ -152,14 +143,13 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
}
} else if (readOnly || searchShareForMagikeyboard) {
GroupActivity.launchForKeyboardSelectionResult(this,
openedDatabase,
searchInfo,
false)
readOnly,
searchInfo,
false)
} else {
GroupActivity.launchForSaveResult(this,
openedDatabase,
searchInfo,
false)
searchInfo,
false)
}
},
{
@@ -193,7 +183,7 @@ fun populateKeyboardAndMoveAppToBackground(activity: Activity,
intent: Intent,
toast: Boolean = true) {
// Populate Magikeyboard with entry
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
MagikIME.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
// Consume the selection mode
EntrySelectionHelper.removeModesFromIntent(intent)
activity.moveTaskToBack(true)

View File

@@ -42,14 +42,14 @@ import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.model.MainCredential
@@ -60,13 +60,12 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
import java.io.FileNotFoundException
class FileDatabaseSelectActivity : DatabaseModeActivity(),
class FileDatabaseSelectActivity : SpecialModeActivity(),
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
// Views
@@ -83,7 +82,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
private var mDatabaseFileUri: Uri? = null
private var mExternalFileHelper: ExternalFileHelper? = null
private var mSelectFileHelper: SelectFileHelper? = null
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -102,9 +103,14 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
createDatabaseButtonView?.setOnClickListener { createNewFile() }
// Open database button
mExternalFileHelper = ExternalFileHelper(this)
mSelectFileHelper = SelectFileHelper(this)
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
openDatabaseButtonView?.apply {
mSelectFileHelper?.selectFileOnClickViewListener?.let {
setOnClickListener(it)
setOnLongClickListener(it)
}
}
// History list
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
@@ -125,6 +131,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
}
}
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
// Remove from app database
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
true
}
@@ -155,31 +162,29 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
// Observe list of databases
databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles ->
try {
when (databaseFiles.databaseFileAction) {
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
when (databaseFiles.databaseFileAction) {
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
}
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
}
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
}
}
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
}
}
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
}
GroupActivity.launch(this@FileDatabaseSelectActivity,
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity))
}
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
}
}
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
}
}
databaseFilesViewModel.consumeAction()
} catch (e: Exception) {
Log.e(TAG, "Unable to observe database action", e)
}
databaseFilesViewModel.consumeAction()
}
// Observe default database
@@ -187,62 +192,37 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
// Retrieve settings for default database
mAdapterDatabaseHistory?.setDefaultDatabase(it)
}
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
if (database != null) {
launchGroupActivityIfLoaded(database)
}
}
override fun onDatabaseActionFinished(
database: Database,
actionTask: String,
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
if (result.isSuccess) {
// Update list
when (actionTask) {
ACTION_DATABASE_CREATE_TASK,
ACTION_DATABASE_LOAD_TASK -> {
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
val mainCredential =
result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY)
?: MainCredential()
databaseFilesViewModel.addDatabaseFile(
databaseUri,
mainCredential.keyFileUri
)
// Attach the dialog thread to this activity
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
onActionFinish = { actionTask, result ->
when (actionTask) {
ACTION_DATABASE_CREATE_TASK -> {
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential()
databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri)
}
}
ACTION_DATABASE_LOAD_TASK -> {
val database = Database.getInstance()
if (result.isSuccess
&& database.loaded) {
launchGroupActivity(database)
} else {
var resultError = ""
val resultMessage = result.message
// Show error message
if (resultMessage != null && resultMessage.isNotEmpty()) {
resultError = "$resultError $resultMessage"
}
Log.e(TAG, resultError)
Snackbar.make(coordinatorLayout,
resultError,
Snackbar.LENGTH_LONG).asError().show()
}
}
}
}
// Launch activity
when (actionTask) {
ACTION_DATABASE_CREATE_TASK -> {
GroupActivity.launch(
this@FileDatabaseSelectActivity,
database,
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity)
)
}
ACTION_DATABASE_LOAD_TASK -> {
launchGroupActivityIfLoaded(database)
}
}
} else {
var resultError = ""
val resultMessage = result.message
// Show error message
if (resultMessage != null && resultMessage.isNotEmpty()) {
resultError = "$resultError $resultMessage"
}
Log.e(TAG, resultError)
Snackbar.make(coordinatorLayout,
resultError,
Snackbar.LENGTH_LONG).asError().show()
}
}
@@ -250,7 +230,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
* Create a new file by calling the content provider
*/
private fun createNewFile() {
mExternalFileHelper?.createDocument( getString(R.string.database_file_name_default) +
createDocument(this, getString(R.string.database_file_name_default) +
getString(R.string.database_file_extension_default), "application/x-keepass")
}
@@ -271,14 +251,12 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
{ onLaunchActivitySpecialMode() })
}
private fun launchGroupActivityIfLoaded(database: Database) {
if (database.loaded) {
GroupActivity.launch(this,
database,
private fun launchGroupActivity(database: Database) {
GroupActivity.launch(this,
database.isReadOnly,
{ onValidateSpecialMode() },
{ onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() })
}
}
override fun onValidateSpecialMode() {
@@ -304,7 +282,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
// Show open and create button or special mode
when (mSpecialMode) {
SpecialMode.DEFAULT -> {
if (ExternalFileHelper.allowCreateDocumentByStorageAccessFramework(packageManager)) {
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
// There is an activity which can handle this intent.
createDatabaseButtonView?.visibility = View.VISIBLE
} else{
@@ -318,18 +296,30 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
}
}
mDatabase?.let { database ->
launchGroupActivityIfLoaded(database)
}
// Show recent files if allowed
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
databaseFilesViewModel.loadListOfDatabases()
val database = Database.getInstance()
if (database.loaded) {
launchGroupActivity(database)
} else {
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
// Construct adapter with listeners
if (PreferencesUtil.showRecentFiles(this)) {
databaseFilesViewModel.loadListOfDatabases()
} else {
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
mAdapterDatabaseHistory?.notifyDataSetChanged()
}
// Register progress task
mProgressDatabaseTaskProvider?.registerProgressTask()
}
}
override fun onPause() {
// Unregister progress task
mProgressDatabaseTaskProvider?.unregisterProgressTask()
super.onPause()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// only to keep the current activity
@@ -339,10 +329,15 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
}
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
try {
mDatabaseFileUri?.let { databaseUri ->
// Create the new database
createDatabase(databaseUri, mainCredential)
mProgressDatabaseTaskProvider?.startDatabaseCreate(
databaseUri,
mainCredential
)
}
} catch (e: Exception) {
val error = getString(R.string.error_create_database_file)
@@ -360,14 +355,14 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
if (uri != null) {
launchPasswordActivityWithPath(uri)
}
}
// Retrieve the created URI from the file manager
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
mDatabaseFileUri = databaseFileCreatedUri
if (mDatabaseFileUri != null) {
AssignMasterKeyDialogFragment.getInstance(true)
@@ -413,9 +408,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
openDatabaseButtonView != null
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
openDatabaseButtonView!!,
{ tapTargetView ->
{tapTargetView ->
tapTargetView?.let {
mExternalFileHelper?.openDocument()
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
}
},
{}

View File

@@ -34,9 +34,9 @@ import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
@@ -49,7 +49,7 @@ import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
import kotlinx.coroutines.*
class IconPickerActivity : DatabaseLockActivity() {
class IconPickerActivity : LockingActivity() {
private lateinit var toolbar: Toolbar
private lateinit var coordinatorLayout: CoordinatorLayout
@@ -64,13 +64,17 @@ class IconPickerActivity : DatabaseLockActivity() {
private var mCustomIconsSelectionMode = false
private var mIconsSelected: List<IconImageCustom> = ArrayList()
private var mExternalFileHelper: ExternalFileHelper? = null
private var mDatabase: Database? = null
private var mSelectFileHelper: SelectFileHelper? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_icon_picker)
mDatabase = Database.getInstance()
toolbar = findViewById(R.id.toolbar)
toolbar.title = " "
setSupportActionBar(toolbar)
@@ -80,9 +84,18 @@ class IconPickerActivity : DatabaseLockActivity() {
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
mExternalFileHelper = ExternalFileHelper(this)
uploadButton = findViewById(R.id.icon_picker_upload)
if (mDatabase?.allowCustomIcons == true) {
uploadButton.setOnClickListener {
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
}
uploadButton.setOnLongClickListener {
mSelectFileHelper?.selectFileOnClickViewListener?.onLongClick(it)
true
}
} else {
uploadButton.visibility = View.GONE
}
lockView = findViewById(R.id.lock_button)
lockView?.setOnClickListener {
@@ -108,6 +121,11 @@ class IconPickerActivity : DatabaseLockActivity() {
mIconImage = savedInstanceState.getParcelable(EXTRA_ICON) ?: mIconImage
}
// Focus view to reinitialize timeout
findViewById<ViewGroup>(R.id.icon_picker_container)?.resetAppTimeoutWhenViewFocusedOrChanged(this)
mSelectFileHelper = SelectFileHelper(this)
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
mIconImage.standard = iconStandard
// Remove the custom icon if a standard one is selected
@@ -141,24 +159,6 @@ class IconPickerActivity : DatabaseLockActivity() {
}
}
override fun viewToInvalidateTimeout(): View? {
return findViewById<ViewGroup>(R.id.icon_picker_container)
}
override fun finishActivityIfReloadRequested(): Boolean {
return true
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
if (database?.allowCustomIcons == true) {
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
} else {
uploadButton.visibility = View.GONE
}
}
private fun updateIconsSelectedViews() {
if (mIconsSelected.isEmpty()) {
mCustomIconsSelectionMode = false
@@ -192,16 +192,11 @@ class IconPickerActivity : DatabaseLockActivity() {
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.icon, menu)
return true
}
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
menu?.findItem(R.id.menu_delete)?.apply {
isEnabled = mCustomIconsSelectionMode
isVisible = isEnabled
if (mCustomIconsSelectionMode) {
menuInflater.inflate(R.menu.icon, menu)
}
return super.onPrepareOptionsMenu(menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@@ -218,9 +213,6 @@ class IconPickerActivity : DatabaseLockActivity() {
removeCustomIcon(iconToRemove)
}
}
R.id.menu_external_icon -> {
UriUtil.gotoUrl(this, R.string.external_icon_url)
}
}
return super.onOptionsItemSelected(item)
@@ -289,7 +281,7 @@ class IconPickerActivity : DatabaseLockActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
addCustomIcon(uri)
}
}

View File

@@ -19,7 +19,6 @@
*/
package com.kunzisoft.keepass.activities
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
@@ -32,19 +31,16 @@ import android.widget.ImageView
import androidx.appcompat.widget.Toolbar
import com.igreenwood.loupe.Loupe
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
import kotlin.math.max
class ImageViewerActivity : DatabaseLockActivity() {
class ImageViewerActivity : LockingActivity() {
private var imageContainerView: ViewGroup? = null
private lateinit var imageView: ImageView
private lateinit var progressView: View
private var mDatabase: Database? = null
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -54,21 +50,49 @@ class ImageViewerActivity : DatabaseLockActivity() {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
toolbar.setOnTouchListener { _, _ ->
resetAppTimeout()
false
val imageContainerView: ViewGroup = findViewById(R.id.image_viewer_container)
val imageView: ImageView = findViewById(R.id.image_viewer_image)
val progressView: View = findViewById(R.id.image_viewer_progress)
// Approximately, to not OOM and allow a zoom
val mImagePreviewMaxWidth = max(
resources.displayMetrics.widthPixels * 2,
resources.displayMetrics.heightPixels * 2
)
mDatabase = Database.getInstance()
try {
progressView.visibility = View.VISIBLE
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
supportActionBar?.title = attachment.name
val size = attachment.binaryData.getSize()
supportActionBar?.subtitle = Formatter.formatFileSize(this, size)
mDatabase?.let { database ->
BinaryDatabaseManager.loadBitmap(
database,
attachment.binaryData,
mImagePreviewMaxWidth
) { bitmapLoaded ->
if (bitmapLoaded == null) {
finish()
} else {
progressView.visibility = View.GONE
imageView.setImageBitmap(bitmapLoaded)
}
}
}
} ?: finish()
} catch (e: Exception) {
Log.e(TAG, "Unable to view the binary", e)
finish()
}
imageContainerView = findViewById(R.id.image_viewer_container)
imageView = findViewById(R.id.image_viewer_image)
progressView = findViewById(R.id.image_viewer_progress)
Loupe.create(imageView, imageContainerView!!) {
onViewTouchedListener = View.OnTouchListener { _, _ ->
// to reset timeout when Loupe image view touched
resetAppTimeout()
false
}
Loupe.create(imageView, imageContainerView) {
onViewTranslateListener = object : Loupe.OnViewTranslateListener {
override fun onStart(view: ImageView) {
@@ -91,54 +115,6 @@ class ImageViewerActivity : DatabaseLockActivity() {
}
}
override fun viewToInvalidateTimeout(): View? {
// Null to manually manage events
return null
}
override fun finishActivityIfReloadRequested(): Boolean {
return true
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
try {
progressView.visibility = View.VISIBLE
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
supportActionBar?.title = attachment.name
val size = attachment.binaryData.getSize()
supportActionBar?.subtitle = Formatter.formatFileSize(this, size)
// Approximately, to not OOM and allow a zoom
val mImagePreviewMaxWidth = max(
resources.displayMetrics.widthPixels * 2,
resources.displayMetrics.heightPixels * 2
)
database?.let { database ->
BinaryDatabaseManager.loadBitmap(
database,
attachment.binaryData,
mImagePreviewMaxWidth
) { bitmapLoaded ->
if (bitmapLoaded == null) {
finish()
} else {
progressView.visibility = View.GONE
imageView.setImageBitmap(bitmapLoaded)
}
}
}
} ?: finish()
} catch (e: Exception) {
Log.e(TAG, "Unable to view the binary", e)
finish()
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> finish()

View File

@@ -19,41 +19,36 @@
*/
package com.kunzisoft.keepass.activities
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper
/**
* Activity to select entry in database and populate it in Magikeyboard
*/
class MagikeyboardLauncherActivity : DatabaseModeActivity() {
class MagikeyboardLauncherActivity : AppCompatActivity() {
override fun applyCustomStyle(): Boolean {
return false
}
override fun finishActivityIfReloadRequested(): Boolean {
return true
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
override fun onCreate(savedInstanceState: Bundle?) {
val database = Database.getInstance()
val readOnly = database.isReadOnly
SearchHelper.checkAutoSearchInfo(this,
database,
null,
{ _, _ ->
// Not called
// if items found directly returns before calling this activity
},
{ openedDatabase ->
// Select if not found
GroupActivity.launchForKeyboardSelectionResult(this, openedDatabase)
},
{
// Pass extra to get entry
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this)
}
database,
null,
{
// Not called
// if items found directly returns before calling this activity
},
{
// Select if not found
GroupActivity.launchForKeyboardSelectionResult(this, readOnly)
},
{
// Pass extra to get entry
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this)
}
)
finish()
super.onCreate(savedInstanceState)
}
}

View File

@@ -31,11 +31,8 @@ import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.*
import android.view.KeyEvent.KEYCODE_ENTER
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
import android.view.inputmethod.InputMethodManager
import android.widget.*
import android.widget.TextView.OnEditorActionListener
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar
@@ -45,13 +42,17 @@ import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
import com.kunzisoft.keepass.activities.helpers.*
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
@@ -65,7 +66,6 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
@@ -74,8 +74,7 @@ import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
import java.io.FileNotFoundException
open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener {
// Views
private var toolbar: Toolbar? = null
@@ -96,14 +95,14 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
private var mDatabaseKeyFileUri: Uri? = null
private var mRememberKeyFile: Boolean = false
private var mExternalFileHelper: ExternalFileHelper? = null
private var mSelectFileHelper: SelectFileHelper? = null
private var mPermissionAsked = false
private var mReadOnly: Boolean = false
private var readOnly: Boolean = false
private var mForceReadOnly: Boolean = false
set(value) {
infoContainerView?.visibility = if (value) {
mReadOnly = true
readOnly = true
View.VISIBLE
} else {
View.GONE
@@ -111,6 +110,8 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
field = value
}
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
private var mAllowAutoOpenBiometricPrompt: Boolean = true
override fun onCreate(savedInstanceState: Bundle?) {
@@ -134,15 +135,16 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
savedInstanceState.getBoolean(KEY_READ_ONLY)
} else {
PreferencesUtil.enableReadOnlyDatabase(this)
}
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity)
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
mSelectFileHelper = SelectFileHelper(this@PasswordActivity)
keyFileSelectionView?.apply {
mSelectFileHelper?.selectFileOnClickViewListener?.let {
setOnClickListener(it)
setOnLongClickListener(it)
}
}
passwordView?.setOnEditorActionListener(onEditorActionListener)
passwordView?.addTextChangedListener(object : TextWatcher {
@@ -155,15 +157,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
checkboxPasswordView?.isChecked = true
}
})
passwordView?.setOnKeyListener { _, _, keyEvent ->
var handled = false
if (keyEvent.action == KeyEvent.ACTION_DOWN
&& keyEvent?.keyCode == KEYCODE_ENTER) {
verifyCheckboxesAndLoadDatabase()
handled = true
}
handled
}
// If is a view intent
getUriFromIntent(intent)
@@ -219,114 +212,72 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
}
}
override fun onResume() {
super.onResume()
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
onActionFinish = { actionTask, result ->
when (actionTask) {
ACTION_DATABASE_LOAD_TASK -> {
// Recheck advanced unlock if error
advancedUnlockFragment?.initAdvancedUnlockMode()
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@PasswordActivity)
if (result.isSuccess) {
mDatabaseKeyFileUri = null
clearCredentialsViews(true)
launchGroupActivity()
} else {
var resultError = ""
val resultException = result.exception
val resultMessage = result.message
// Back to previous keyboard is setting activated
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@PasswordActivity)) {
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
}
if (resultException != null) {
resultError = resultException.getLocalizedMessage(resources)
// Don't allow auto open prompt if lock become when UI visible
mAllowAutoOpenBiometricPrompt = if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
false
else
mAllowAutoOpenBiometricPrompt
mDatabaseFileUri?.let { databaseFileUri ->
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
}
when (resultException) {
is DuplicateUuidDatabaseException -> {
// Relaunch loading if we need to fix UUID
showLoadDatabaseDuplicateUuidMessage {
checkPermission()
var databaseUri: Uri? = null
var mainCredential: MainCredential = MainCredential()
var readOnly = true
var cipherEntity: CipherDatabaseEntity? = null
mDatabase?.let { database ->
launchGroupActivityIfLoaded(database)
}
}
result.data?.let { resultData ->
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
mainCredential = resultData.getParcelable(MAIN_CREDENTIAL_KEY) ?: mainCredential
readOnly = resultData.getBoolean(READ_ONLY_KEY)
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
if (database != null) {
launchGroupActivityIfLoaded(database)
}
}
override fun onDatabaseActionFinished(
database: Database,
actionTask: String,
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) {
ACTION_DATABASE_LOAD_TASK -> {
// Recheck advanced unlock if error
advancedUnlockFragment?.initAdvancedUnlockMode()
if (result.isSuccess) {
launchGroupActivityIfLoaded(database)
} else {
passwordView?.requestFocusFromTouch()
var resultError = ""
val resultException = result.exception
val resultMessage = result.message
if (resultException != null) {
resultError = resultException.getLocalizedMessage(resources)
when (resultException) {
is DuplicateUuidDatabaseException -> {
// Relaunch loading if we need to fix UUID
showLoadDatabaseDuplicateUuidMessage {
var databaseUri: Uri? = null
var mainCredential = MainCredential()
var readOnly = true
var cipherEntity: CipherDatabaseEntity? = null
result.data?.let { resultData ->
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
mainCredential =
resultData.getParcelable(MAIN_CREDENTIAL_KEY)
?: mainCredential
readOnly = resultData.getBoolean(READ_ONLY_KEY)
cipherEntity =
resultData.getParcelable(CIPHER_ENTITY_KEY)
databaseUri?.let { databaseFileUri ->
showProgressDialogAndLoadDatabase(
databaseFileUri,
mainCredential,
readOnly,
cipherEntity,
true)
}
}
}
databaseUri?.let { databaseFileUri ->
showProgressDialogAndLoadDatabase(
databaseFileUri,
mainCredential,
readOnly,
cipherEntity,
true
)
is FileNotFoundDatabaseException -> {
// Remove this default database inaccessible
if (mDefaultDatabase) {
databaseFileViewModel.removeDefaultDatabase()
}
}
}
}
is FileNotFoundDatabaseException -> {
// Remove this default database inaccessible
if (mDefaultDatabase) {
databaseFileViewModel.removeDefaultDatabase()
}
// Show error message
if (resultMessage != null && resultMessage.isNotEmpty()) {
resultError = "$resultError $resultMessage"
}
Log.e(TAG, resultError)
Snackbar.make(coordinatorLayout,
resultError,
Snackbar.LENGTH_LONG).asError().show()
}
}
// Show error message
if (resultMessage != null && resultMessage.isNotEmpty()) {
resultError = "$resultError $resultMessage"
}
Log.e(TAG, resultError)
Snackbar.make(
coordinatorLayout,
resultError,
Snackbar.LENGTH_LONG
).asError().show()
}
}
}
@@ -353,17 +304,13 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
getUriFromIntent(intent)
}
private fun launchGroupActivityIfLoaded(database: Database) {
// Check if database really loaded
if (database.loaded) {
clearCredentialsViews(true)
GroupActivity.launch(this,
database,
private fun launchGroupActivity() {
GroupActivity.launch(this,
readOnly,
{ onValidateSpecialMode() },
{ onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() }
)
}
)
}
override fun onValidateSpecialMode() {
@@ -413,6 +360,40 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
}
}
override fun onResume() {
super.onResume()
if (Database.getInstance().loaded) {
launchGroupActivity()
} else {
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
// If the database isn't accessible make sure to clear the password field, if it
// was saved in the instance state
if (Database.getInstance().loaded) {
clearCredentialsViews()
}
mProgressDatabaseTaskProvider?.registerProgressTask()
// Back to previous keyboard is setting activated
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this)) {
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
}
// Don't allow auto open prompt if lock become when UI visible
mAllowAutoOpenBiometricPrompt = if (LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
false
else
mAllowAutoOpenBiometricPrompt
mDatabaseFileUri?.let { databaseFileUri ->
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
}
checkPermission()
}
}
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
// Define Key File text
if (mRememberKeyFile) {
@@ -436,17 +417,11 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
} else {
// Init Biometric elements
advancedUnlockFragment?.loadDatabase(databaseFileUri,
mAllowAutoOpenBiometricPrompt)
mAllowAutoOpenBiometricPrompt
&& mProgressDatabaseTaskProvider?.isBinded() != true)
}
enableOrNotTheConfirmationButton()
// Auto select the password field and open keyboard
passwordView?.postDelayed({
passwordView?.requestFocusFromTouch()
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager?
inputMethodManager?.showSoftInput(passwordView, InputMethodManager.SHOW_IMPLICIT)
}, 100)
}
private fun enableOrNotTheConfirmationButton() {
@@ -464,7 +439,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
populatePasswordTextView(null)
if (clearKeyFile) {
mDatabaseKeyFileUri = null
populateKeyFileTextView(null)
}
}
@@ -494,8 +468,10 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
}
override fun onPause() {
mProgressDatabaseTaskProvider?.unregisterProgressTask()
// Reinit locking activity UI variable
DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
mAllowAutoOpenBiometricPrompt = true
super.onPause()
@@ -506,7 +482,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
mDatabaseKeyFileUri?.let {
outState.putString(KEY_KEYFILE, it.toString())
}
outState.putBoolean(KEY_READ_ONLY, mReadOnly)
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false)
super.onSaveInstanceState(outState)
}
@@ -544,7 +520,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
clearCredentialsViews()
}
if (mReadOnly && (
if (readOnly && (
mSpecialMode == SpecialMode.SAVE
|| mSpecialMode == SpecialMode.REGISTRATION)
) {
@@ -558,7 +534,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
showProgressDialogAndLoadDatabase(
databaseUri,
MainCredential(password, keyFileUri),
mReadOnly,
readOnly,
cipherDatabaseEntity,
false)
}
@@ -570,7 +546,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
readOnly: Boolean,
cipherDatabaseEntity: CipherDatabaseEntity?,
fixDuplicateUUID: Boolean) {
loadDatabase(
mProgressDatabaseTaskProvider?.startDatabaseLoad(
databaseUri,
mainCredential,
readOnly,
@@ -609,7 +585,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
// Check permission
private fun checkPermission() {
if (Build.VERSION.SDK_INT in 23..28
&& !mReadOnly
&& !readOnly
&& !mPermissionAsked) {
mPermissionAsked = true
// Check self permission to show or not the dialog
@@ -686,7 +662,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
}
private fun changeOpenFileReadIcon(togglePassword: MenuItem) {
if (mReadOnly) {
if (readOnly) {
togglePassword.setTitle(R.string.menu_file_selection_read_only)
togglePassword.setIcon(R.drawable.ic_read_only_white_24dp)
} else {
@@ -700,7 +676,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
when (item.itemId) {
android.R.id.home -> finish()
R.id.menu_open_file_read_mode_key -> {
mReadOnly = !mReadOnly
readOnly = !readOnly
changeOpenFileReadIcon(item)
}
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
@@ -726,8 +702,9 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
}
var keyFileResult = false
mExternalFileHelper?.let {
keyFileResult = it.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
mSelectFileHelper?.let {
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
if (uri != null) {
mDatabaseKeyFileUri = uri
populateKeyFileTextView(uri)
@@ -737,9 +714,9 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
if (!keyFileResult) {
// this block if not a key file response
when (resultCode) {
DatabaseLockActivity.RESULT_EXIT_LOCK -> {
LockingActivity.RESULT_EXIT_LOCK -> {
clearCredentialsViews()
closeDatabase()
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
}
Activity.RESULT_CANCELED -> {
clearCredentialsViews()
@@ -758,7 +735,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
private const val KEY_KEYFILE = "keyFile"
private const val VIEW_INTENT = "android.intent.action.VIEW"
private const val KEY_READ_ONLY = "KEY_READ_ONLY"
private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"

View File

@@ -30,17 +30,18 @@ import android.text.SpannableStringBuilder
import android.text.TextWatcher
import android.view.View
import android.widget.CompoundButton
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView
class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
class AssignMasterKeyDialogFragment : DialogFragment() {
private var mMasterPassword: String? = null
private var mKeyFile: Uri? = null
@@ -59,7 +60,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
private var mListener: AssignPasswordDialogListener? = null
private var mExternalFileHelper: ExternalFileHelper? = null
private var mSelectFileHelper: SelectFileHelper? = null
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
private var mNoKeyConfirmationDialog: AlertDialog? = null
@@ -132,8 +133,11 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
mExternalFileHelper = ExternalFileHelper(this)
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
mSelectFileHelper = SelectFileHelper(this)
keyFileSelectionView?.apply {
setOnClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
setOnLongClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
}
val dialog = builder.create()
@@ -285,7 +289,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
uri?.let { pathUri ->
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
keyFileSelectionView?.error = null

View File

@@ -23,11 +23,12 @@ import android.app.Dialog
import android.os.Bundle
import android.text.SpannableStringBuilder
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
class DatabaseChangedDialogFragment : DialogFragment() {
var actionDatabaseListener: ActionDatabaseChangedListener? = null

View File

@@ -1,71 +0,0 @@
package com.kunzisoft.keepass.activities.dialogs
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
private var mDatabase: Database? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDatabaseViewModel.database.observe(this) { database ->
this.mDatabase = database
resetAppTimeoutOnTouchOrFocus()
onDatabaseRetrieved(database)
}
mDatabaseViewModel.actionFinished.observe(this) { result ->
onDatabaseActionFinished(result.database, result.actionTask, result.result)
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
resetAppTimeoutOnTouchOrFocus()
}
override fun onDatabaseRetrieved(database: Database?) {
// Can be overridden by a subclass
}
override fun onDatabaseActionFinished(
database: Database,
actionTask: String,
result: ActionRunnable.Result
) {
// Can be overridden by a subclass
}
fun resetAppTimeout() {
context?.let {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(it,
mDatabase?.loaded ?: false)
}
}
open fun overrideTimeoutTouchAndFocusEvents(): Boolean {
return false
}
private fun resetAppTimeoutOnTouchOrFocus() {
if (!overrideTimeoutTouchAndFocusEvents()) {
context?.let {
dialog?.window?.decorView?.resetAppTimeoutWhenViewTouchedOrFocused(
it,
mDatabase?.loaded
)
}
}
}
}

View File

@@ -6,7 +6,6 @@ import android.content.Context
import android.os.Bundle
import androidx.fragment.app.DialogFragment
// Not as DatabaseDialogFragment because crash on KitKat
class DatePickerFragment : DialogFragment() {
private var mDefaultYear: Int = 2000

View File

@@ -20,38 +20,61 @@
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.viewmodels.NodesViewModel
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
class DeleteNodesDialogFragment : DatabaseDialogFragment() {
open class DeleteNodesDialogFragment : DialogFragment() {
private var mNodesToDelete: List<Node> = listOf()
private val mNodesViewModel: NodesViewModel by activityViewModels()
private var mNodesToDelete: List<Node> = ArrayList()
private var mListener: DeleteNodeListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
mListener = context as DeleteNodeListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + DeleteNodeListener::class.java.name)
}
}
override fun onDetach() {
mListener = null
super.onDetach()
}
protected open fun retrieveMessage(): String {
return getString(R.string.warning_permanently_delete_nodes)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
mNodesViewModel.nodesToDelete.observe(this) { nodes ->
this.mNodesToDelete = nodes
}
var recycleBin = false
arguments?.apply {
if (containsKey(RECYCLE_BIN_TAG)) {
recycleBin = this.getBoolean(RECYCLE_BIN_TAG)
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), this)
}
} ?: savedInstanceState?.apply {
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), savedInstanceState)
}
}
activity?.let { activity ->
// Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(activity)
builder.setMessage(if (recycleBin)
getString(R.string.warning_empty_recycle_bin)
else
getString(R.string.warning_permanently_delete_nodes))
builder.setMessage(retrieveMessage())
builder.setPositiveButton(android.R.string.ok) { _, _ ->
mNodesViewModel.permanentlyDeleteNodes(mNodesToDelete)
mListener?.permanentlyDeleteNodes(mNodesToDelete)
}
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
// Create the AlertDialog object and return it
@@ -60,14 +83,19 @@ class DeleteNodesDialogFragment : DatabaseDialogFragment() {
return super.onCreateDialog(savedInstanceState)
}
companion object {
private const val RECYCLE_BIN_TAG = "RECYCLE_BIN_TAG"
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putAll(getBundleFromListNodes(mNodesToDelete))
}
fun getInstance(recycleBin: Boolean): DeleteNodesDialogFragment {
interface DeleteNodeListener {
fun permanentlyDeleteNodes(nodes: List<Node>)
}
companion object {
fun getInstance(nodesToDelete: List<Node>): DeleteNodesDialogFragment {
return DeleteNodesDialogFragment().apply {
arguments = Bundle().apply {
putBoolean(RECYCLE_BIN_TAG, recycleBin)
}
arguments = getBundleFromListNodes(nodesToDelete)
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
@@ -15,24 +15,25 @@
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element.template
package com.kunzisoft.keepass.activities.dialogs
enum class TemplateAttributeType(val typeString: String) {
TEXT("text"),
LIST("list"),
DATETIME("datetime"),
DIVIDER("divider");
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
class EmptyRecycleBinDialogFragment : DeleteNodesDialogFragment() {
override fun retrieveMessage(): String {
return getString(R.string.warning_empty_recycle_bin)
}
companion object {
fun getFromString(label: String): TemplateAttributeType {
return when {
label.contains(TEXT.typeString, true) -> TEXT
label.contains(LIST.typeString, true) -> LIST
label.contains(DATETIME.typeString, true) -> DATETIME
label.contains(DIVIDER.typeString, true) -> DIVIDER
else -> TEXT
fun getInstance(nodesToDelete: List<Node>): EmptyRecycleBinDialogFragment {
return EmptyRecycleBinDialogFragment().apply {
arguments = getBundleFromListNodes(nodesToDelete)
}
}
}
}
}

View File

@@ -31,13 +31,14 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.model.Field
class EntryCustomFieldDialogFragment: DatabaseDialogFragment() {
class EntryCustomFieldDialogFragment: DialogFragment() {
private var oldField: Field? = null

View File

@@ -22,18 +22,18 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import com.google.android.material.textfield.TextInputLayout
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.view.View
import android.widget.*
import androidx.appcompat.app.AlertDialog
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.applyFontVisibility
class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
class GeneratePasswordDialogFragment : DialogFragment() {
private var mListener: GeneratePasswordListener? = null
@@ -42,8 +42,6 @@ class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
private var passwordInputLayoutView: TextInputLayout? = null
private var passwordView: EditText? = null
private var mPasswordField: Field? = null
private var uppercaseBox: CompoundButton? = null
private var lowercaseBox: CompoundButton? = null
private var digitsBox: CompoundButton? = null
@@ -79,7 +77,7 @@ class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
passwordView = root?.findViewById(R.id.password)
passwordView?.applyFontVisibility()
val passwordCopyView: ImageView? = root?.findViewById(R.id.password_copy_button)
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(activity))
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyPasswordAndProtectedFields(activity))
View.VISIBLE else View.GONE
val clipboardHelper = ClipboardHelper(activity)
passwordCopyView?.setOnClickListener {
@@ -100,8 +98,6 @@ class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
bracketsBox = root?.findViewById(R.id.cb_brackets)
extendedBox = root?.findViewById(R.id.cb_extended)
mPasswordField = arguments?.getParcelable(KEY_PASSWORD_FIELD)
assignDefaultCharacters()
val seekBar = root?.findViewById<SeekBar>(R.id.seekbar_length)
@@ -124,18 +120,16 @@ class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
builder.setView(root)
.setPositiveButton(R.string.accept) { _, _ ->
mPasswordField?.let { passwordField ->
passwordView?.text?.toString()?.let { passwordValue ->
passwordField.protectedValue.stringValue = passwordValue
}
mListener?.acceptPassword(passwordField)
}
val bundle = Bundle()
bundle.putString(KEY_PASSWORD_ID, passwordView!!.text.toString())
mListener?.acceptPassword(bundle)
dismiss()
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
mPasswordField?.let { passwordField ->
mListener?.cancelPassword(passwordField)
}
val bundle = Bundle()
mListener?.cancelPassword(bundle)
dismiss()
}
@@ -206,19 +200,11 @@ class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
}
interface GeneratePasswordListener {
fun acceptPassword(passwordField: Field)
fun cancelPassword(passwordField: Field)
fun acceptPassword(bundle: Bundle)
fun cancelPassword(bundle: Bundle)
}
companion object {
private const val KEY_PASSWORD_FIELD = "KEY_PASSWORD_FIELD"
fun getInstance(field: Field): GeneratePasswordDialogFragment {
return GeneratePasswordDialogFragment().apply {
arguments = Bundle().apply {
putParcelable(KEY_PASSWORD_FIELD, field)
}
}
}
const val KEY_PASSWORD_ID = "KEY_PASSWORD_ID"
}
}

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.graphics.Color
import android.os.Bundle
import android.view.View
@@ -27,34 +28,35 @@ import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.*
import com.kunzisoft.keepass.activities.IconPickerActivity
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.view.DateTimeEditFieldView
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
import com.kunzisoft.keepass.view.ExpirationView
import org.joda.time.DateTime
class GroupEditDialogFragment : DatabaseDialogFragment() {
class GroupEditDialogFragment : DialogFragment() {
private val mGroupEditViewModel: GroupEditViewModel by activityViewModels()
private var mDatabase: Database? = null
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
private var mEditGroupDialogAction = NONE
private var mEditGroupListener: EditGroupListener? = null
private var mEditGroupDialogAction = EditGroupDialogAction.NONE
private var mGroupInfo = GroupInfo()
private var mGroupNamesNotAllowed: List<String>? = null
private lateinit var iconButtonView: ImageView
private var mIconColor: Int = 0
private var iconColor: Int = 0
private lateinit var nameTextLayoutView: TextInputLayout
private lateinit var nameTextView: TextView
private lateinit var notesTextLayoutView: TextInputLayout
private lateinit var notesTextView: TextView
private lateinit var expirationView: DateTimeEditFieldView
private lateinit var expirationView: ExpirationView
enum class EditGroupDialogAction {
CREATION, UPDATE, NONE;
@@ -66,51 +68,22 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mGroupEditViewModel.onIconSelected.observe(this) { iconImage ->
mGroupInfo.icon = iconImage
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
}
mGroupEditViewModel.onDateSelected.observe(this) { viewModelDate ->
// Save the date
mGroupInfo.expiryTime = DateInstant(
DateTime(mGroupInfo.expiryTime.date)
.withYear(viewModelDate.year)
.withMonthOfYear(viewModelDate.month + 1)
.withDayOfMonth(viewModelDate.day)
.toDate())
expirationView.dateTime = mGroupInfo.expiryTime
if (expirationView.dateTime.type == DateInstant.Type.DATE_TIME) {
val instantTime = DateInstant(mGroupInfo.expiryTime.date, DateInstant.Type.TIME)
// Trick to recall selection with time
mGroupEditViewModel.requestDateTimeSelection(instantTime)
}
}
mGroupEditViewModel.onTimeSelected.observe(this) { viewModelTime ->
// Save the time
mGroupInfo.expiryTime = DateInstant(
DateTime(mGroupInfo.expiryTime.date)
.withHourOfDay(viewModelTime.hours)
.withMinuteOfHour(viewModelTime.minutes)
.toDate(), mGroupInfo.expiryTime.type)
expirationView.dateTime = mGroupInfo.expiryTime
}
mGroupEditViewModel.groupNamesNotAllowed.observe(this) { namesNotAllowed ->
this.mGroupNamesNotAllowed = namesNotAllowed
override fun onAttach(context: Context) {
super.onAttach(context)
// Verify that the host activity implements the callback interface
try {
// Instantiate the NoticeDialogListener so we can send events to the host
mEditGroupListener = context as EditGroupListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context.toString()
+ " must implement " + GroupEditDialogFragment::class.java.name)
}
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
}
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
override fun onDetach() {
mEditGroupListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@@ -125,9 +98,12 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
// Retrieve the textColor to tint the icon
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
mIconColor = ta.getColor(0, Color.WHITE)
iconColor = ta.getColor(0, Color.WHITE)
ta.recycle()
// Init elements
mDatabase = Database.getInstance()
if (savedInstanceState != null
&& savedInstanceState.containsKey(KEY_ACTION_ID)
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
@@ -144,22 +120,32 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
}
// populate info in views
populateInfoToViews(mGroupInfo)
iconButtonView.setOnClickListener { _ ->
mGroupEditViewModel.requestIconSelection(mGroupInfo.icon)
}
expirationView.setOnDateClickListener = { dateInstant ->
mGroupEditViewModel.requestDateTimeSelection(dateInstant)
populateInfoToViews()
expirationView.setOnDateClickListener = {
expirationView.expiryTime.date.let { expiresDate ->
val dateTime = DateTime(expiresDate)
val defaultYear = dateTime.year
val defaultMonth = dateTime.monthOfYear-1
val defaultDay = dateTime.dayOfMonth
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
.show(parentFragmentManager, "DatePickerFragment")
}
}
val builder = AlertDialog.Builder(activity)
builder.setView(root)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel) { _, _ ->
// Do nothing
retrieveGroupInfoFromViews()
mEditGroupListener?.cancelEditGroup(
mEditGroupDialogAction,
mGroupInfo)
}
iconButtonView.setOnClickListener { _ ->
IconPickerActivity.launch(activity, mGroupInfo.icon)
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
@@ -169,34 +155,40 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
super.onResume()
// To prevent auto dismiss
val alertDialog = dialog as AlertDialog?
if (alertDialog != null) {
val positiveButton = alertDialog.getButton(Dialog.BUTTON_POSITIVE) as Button
val d = dialog as AlertDialog?
if (d != null) {
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
positiveButton.setOnClickListener {
retrieveGroupInfoFromViews()
if (isValid()) {
when (mEditGroupDialogAction) {
CREATION ->
mGroupEditViewModel.approveGroupCreation(mGroupInfo)
UPDATE ->
mGroupEditViewModel.approveGroupUpdate(mGroupInfo)
NONE -> {}
}
alertDialog.dismiss()
mEditGroupListener?.approveEditGroup(
mEditGroupDialogAction,
mGroupInfo)
d.dismiss()
}
}
}
}
private fun populateInfoToViews(groupInfo: GroupInfo) {
mGroupEditViewModel.selectIcon(groupInfo.icon)
nameTextView.text = groupInfo.title
notesTextLayoutView.visibility = if (groupInfo.notes == null) View.GONE else View.VISIBLE
groupInfo.notes?.let {
fun getExpiryTime(): DateInstant {
retrieveGroupInfoFromViews()
return mGroupInfo.expiryTime
}
fun setExpiryTime(expiryTime: DateInstant) {
mGroupInfo.expiryTime = expiryTime
populateInfoToViews()
}
private fun populateInfoToViews() {
assignIconView()
nameTextView.text = mGroupInfo.title
notesTextLayoutView.visibility = if (mGroupInfo.notes == null) View.GONE else View.VISIBLE
mGroupInfo.notes?.let {
notesTextView.text = it
}
expirationView.activation = groupInfo.expires
expirationView.dateTime = groupInfo.expiryTime
expirationView.expires = mGroupInfo.expires
expirationView.expiryTime = mGroupInfo.expiryTime
}
private fun retrieveGroupInfoFromViews() {
@@ -206,8 +198,17 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
if (newNotes.isNotEmpty()) {
mGroupInfo.notes = newNotes
}
mGroupInfo.expires = expirationView.activation
mGroupInfo.expiryTime = expirationView.dateTime
mGroupInfo.expires = expirationView.expires
mGroupInfo.expiryTime = expirationView.expiryTime
}
private fun assignIconView() {
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconButtonView, mGroupInfo.icon, iconColor)
}
fun setIcon(icon: IconImage) {
mGroupInfo.icon = icon
assignIconView()
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -218,30 +219,19 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
}
private fun isValid(): Boolean {
val name = nameTextView.text.toString()
val error = when {
name.isEmpty() -> {
Error(true, R.string.error_no_name)
}
mGroupNamesNotAllowed == null -> {
Error(true, R.string.error_word_reserved)
}
mGroupNamesNotAllowed?.find { it.equals(name, ignoreCase = true) } != null -> {
Error(true, R.string.error_word_reserved)
}
else -> {
Error(false, null)
}
if (nameTextView.text.toString().isEmpty()) {
nameTextLayoutView.error = getString(R.string.error_no_name)
return false
}
error.messageId?.let { messageId ->
nameTextLayoutView.error = getString(messageId)
} ?: kotlin.run {
nameTextLayoutView.error = null
}
return !error.isError
return true
}
data class Error(val isError: Boolean, val messageId: Int?)
interface EditGroupListener {
fun approveEditGroup(action: EditGroupDialogAction,
groupInfo: GroupInfo)
fun cancelEditGroup(action: EditGroupDialogAction,
groupInfo: GroupInfo)
}
companion object {

View File

@@ -19,11 +19,11 @@
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.MainCredential

View File

@@ -25,13 +25,14 @@ import android.net.Uri
import android.os.Bundle
import android.text.SpannableStringBuilder
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Attachment
/**
* Custom Dialog to confirm big file to upload
*/
class ReplaceFileDialogFragment : DatabaseDialogFragment() {
class ReplaceFileDialogFragment : DialogFragment() {
private var mActionChooseListener: ActionChooseListener? = null

View File

@@ -31,6 +31,7 @@ import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
@@ -48,7 +49,7 @@ import com.kunzisoft.keepass.otp.TokenCalculator
import com.kunzisoft.keepass.utils.UriUtil
import java.util.*
class SetOTPDialogFragment : DatabaseDialogFragment() {
class SetOTPDialogFragment : DialogFragment() {
private var mCreateOTPElementListener: CreateOtpListener? = null
@@ -79,15 +80,11 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
if (!isFocus)
mManualEvent = true
else
resetAppTimeout()
}
@SuppressLint("ClickableViewAccessibility")
private var mOnTouchListener = View.OnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
mManualEvent = true
resetAppTimeout()
}
}
false
@@ -98,10 +95,6 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
private var mPeriodWellFormed = false
private var mDigitsWellFormed = false
override fun overrideTimeoutTouchAndFocusEvents(): Boolean {
return true
}
override fun onAttach(context: Context) {
super.onAttach(context)
// Verify that the host activity implements the callback interface
@@ -232,11 +225,8 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
val builder = AlertDialog.Builder(activity)
builder.apply {
setView(root)
.setPositiveButton(android.R.string.ok) { _, _ ->
resetAppTimeout()
}
.setPositiveButton(android.R.string.ok) {_, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ ->
resetAppTimeout()
}
}

View File

@@ -22,15 +22,16 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import androidx.annotation.IdRes
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.view.View
import android.widget.CompoundButton
import android.widget.RadioGroup
import androidx.annotation.IdRes
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.SortNodeEnum
class SortDialogFragment : DatabaseDialogFragment() {
class SortDialogFragment : DialogFragment() {
private var mListener: SortSelectionListener? = null

View File

@@ -8,7 +8,6 @@ import android.os.Bundle
import android.text.format.DateFormat
import androidx.fragment.app.DialogFragment
// Not as DatabaseDialogFragment because crash on KitKat
class TimePickerFragment : DialogFragment() {
private var defaultHour: Int = 0

View File

@@ -1,51 +0,0 @@
package com.kunzisoft.keepass.activities.fragments
import android.os.Bundle
import android.view.View
import androidx.fragment.app.activityViewModels
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
abstract class DatabaseFragment : StylishFragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
protected var mDatabase: Database? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
if (mDatabase == null || mDatabase != database) {
this.mDatabase = database
onDatabaseRetrieved(database)
}
}
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) { result ->
onDatabaseActionFinished(result.database, result.actionTask, result.result)
}
}
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
context?.let {
view?.resetAppTimeoutWhenViewTouchedOrFocused(it, mDatabase?.loaded)
}
}
override fun onDatabaseActionFinished(
database: Database,
actionTask: String,
result: ActionRunnable.Result
) {
// Can be overridden by a subclass
}
protected fun buildNewBinaryAttachment(): BinaryData? {
return mDatabase?.buildNewBinaryAttachment()
}
}

View File

@@ -19,255 +19,431 @@
*/
package com.kunzisoft.keepass.activities.fragments
import android.content.Context
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.ImageView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
import com.kunzisoft.keepass.activities.EntryEditActivity
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.view.TemplateEditView
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.ExpirationView
import com.kunzisoft.keepass.view.applyFontVisibility
import com.kunzisoft.keepass.view.collapse
import com.kunzisoft.keepass.view.expand
import com.kunzisoft.keepass.view.showByFading
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
class EntryEditFragment: DatabaseFragment() {
class EntryEditFragment: StylishFragment() {
private val mEntryEditViewModel: EntryEditViewModel by activityViewModels()
private lateinit var rootView: View
private lateinit var templateView: TemplateEditView
private lateinit var attachmentsContainerView: ViewGroup
private lateinit var entryTitleLayoutView: TextInputLayout
private lateinit var entryTitleView: EditText
private lateinit var entryIconView: ImageView
private lateinit var entryUserNameView: EditText
private lateinit var entryUrlView: EditText
private lateinit var entryPasswordLayoutView: TextInputLayout
private lateinit var entryPasswordView: EditText
private lateinit var entryPasswordGeneratorView: View
private lateinit var entryExpirationView: ExpirationView
private lateinit var entryNotesView: EditText
private lateinit var extraFieldsContainerView: View
private lateinit var extraFieldsListView: ViewGroup
private lateinit var attachmentsContainerView: View
private lateinit var attachmentsListView: RecyclerView
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
private var mAllowMultipleAttachments: Boolean = false
private lateinit var attachmentsAdapter: EntryAttachmentsItemsAdapter
private var mIconColor: Int = 0
private var fontInVisibility: Boolean = false
private var iconColor: Int = 0
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
var drawFactory: IconDrawableFactory? = null
var setOnDateClickListener: (() -> Unit)? = null
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
var setOnIconViewClickListener: ((IconImage) -> Unit)? = null
var setOnEditCustomField: ((Field) -> Unit)? = null
var setOnRemoveAttachment: ((Attachment) -> Unit)? = null
// Elements to modify the current entry
private var mEntryInfo = EntryInfo()
private var mLastFocusedEditField: FocusedEditField? = null
private var mExtraViewToRequestFocus: EditText? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
// Retrieve the textColor to tint the icon
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
mIconColor = taIconColor?.getColor(0, Color.BLACK) ?: Color.BLACK
taIconColor?.recycle()
val rootView = inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_entry_edit_contents, container, false)
return inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_entry_edit, container, false)
}
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext())
override fun onViewCreated(view: View,
savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
rootView = view
// Hide only the first time
if (savedInstanceState == null) {
view.isVisible = false
entryTitleLayoutView = rootView.findViewById(R.id.entry_edit_container_title)
entryTitleView = rootView.findViewById(R.id.entry_edit_title)
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
entryIconView.setOnClickListener {
setOnIconViewClickListener?.invoke(mEntryInfo.icon)
}
templateView = view.findViewById(R.id.template_view)
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
attachmentsListView = view.findViewById(R.id.entry_attachments_list)
entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name)
entryUrlView = rootView.findViewById(R.id.entry_edit_url)
entryPasswordLayoutView = rootView.findViewById(R.id.entry_edit_container_password)
entryPasswordView = rootView.findViewById(R.id.entry_edit_password)
entryPasswordGeneratorView = rootView.findViewById(R.id.entry_edit_password_generator_button)
entryPasswordGeneratorView.setOnClickListener {
setOnPasswordGeneratorClickListener?.onClick(it)
}
entryExpirationView = rootView.findViewById(R.id.entry_edit_expiration)
entryExpirationView.setOnDateClickListener = setOnDateClickListener
entryNotesView = rootView.findViewById(R.id.entry_edit_notes)
extraFieldsContainerView = rootView.findViewById(R.id.extra_fields_container)
extraFieldsListView = rootView.findViewById(R.id.extra_fields_list)
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
attachmentsListView.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
adapter = attachmentsAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
templateView.apply {
setOnIconClickListener {
mEntryEditViewModel.requestIconSelection(templateView.getIcon())
}
setOnCustomEditionActionClickListener { field ->
mEntryEditViewModel.requestCustomFieldEdition(field)
}
setOnPasswordGenerationActionClickListener { field ->
mEntryEditViewModel.requestPasswordSelection(field)
}
setOnDateInstantClickListener { dateInstant ->
mEntryEditViewModel.requestDateTimeSelection(dateInstant)
}
}
if (savedInstanceState != null) {
val attachments: List<Attachment> =
savedInstanceState.getParcelableArrayList(ATTACHMENTS_TAG) ?: listOf()
setAttachments(attachments)
}
mEntryEditViewModel.onTemplateChanged.observe(viewLifecycleOwner) { 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)
}
// To prevent flickering
rootView.showByFading()
// Apply timeout reset
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
}
mEntryEditViewModel.requestEntryInfoUpdate.observe(viewLifecycleOwner) {
mEntryEditViewModel.saveEntryInfo(it.database, it.entry, it.parent, retrieveEntryInfo())
}
mEntryEditViewModel.onIconSelected.observe(viewLifecycleOwner) { iconImage ->
templateView.setIcon(iconImage)
}
mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { passwordField ->
templateView.setPasswordField(passwordField)
}
mEntryEditViewModel.onDateSelected.observe(viewLifecycleOwner) { viewModelDate ->
// Save the date
templateView.setCurrentDateTimeValue(viewModelDate)
}
mEntryEditViewModel.onTimeSelected.observe(viewLifecycleOwner) { viewModelTime ->
// Save the time
templateView.setCurrentTimeValue(viewModelTime)
}
mEntryEditViewModel.onCustomFieldEdited.observe(viewLifecycleOwner) { fieldAction ->
val oldField = fieldAction.oldField
val newField = fieldAction.newField
// Field to add
if (oldField == null) {
newField?.let {
if (!templateView.putCustomField(it)) {
mEntryEditViewModel.showCustomFieldEditionError()
}
}
}
// Field to replace
oldField?.let {
newField?.let {
if (!templateView.replaceCustomField(oldField, newField)) {
mEntryEditViewModel.showCustomFieldEditionError()
}
}
}
// Field to remove
if (newField == null) {
oldField?.let {
templateView.removeCustomField(it)
}
}
}
mEntryEditViewModel.requestSetupOtp.observe(viewLifecycleOwner) {
// Retrieve the current otpElement if exists
// and open the dialog to set up the OTP
SetOTPDialogFragment.build(templateView.getEntryInfo().otpModel)
.show(parentFragmentManager, "addOTPDialog")
}
mEntryEditViewModel.onOtpCreated.observe(viewLifecycleOwner) {
// Update the otp field with otpauth:// url
templateView.putOtpElement(it)
}
mEntryEditViewModel.onBuildNewAttachment.observe(viewLifecycleOwner) {
val attachmentToUploadUri = it.attachmentToUploadUri
val fileName = it.fileName
buildNewBinaryAttachment()?.let { binaryAttachment ->
val entryAttachment = Attachment(fileName, binaryAttachment)
// Ask to replace the current attachment
if ((!mAllowMultipleAttachments
&& containsAttachment()) ||
containsAttachment(EntryAttachmentState(entryAttachment, StreamDirection.UPLOAD))) {
ReplaceFileDialogFragment.build(attachmentToUploadUri, entryAttachment)
.show(parentFragmentManager, "replacementFileFragment")
} else {
mEntryEditViewModel.startUploadAttachment(attachmentToUploadUri, entryAttachment)
}
}
}
mEntryEditViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState ->
when (entryAttachmentState?.downloadState) {
AttachmentState.START -> {
putAttachment(entryAttachmentState)
getAttachmentViewPosition(entryAttachmentState) { attachment, position ->
mEntryEditViewModel.binaryPreviewLoaded(attachment, position)
}
}
AttachmentState.IN_PROGRESS -> {
putAttachment(entryAttachmentState)
}
AttachmentState.COMPLETE -> {
putAttachment(entryAttachmentState) { entryAttachment ->
getAttachmentViewPosition(entryAttachment) { attachment, position ->
mEntryEditViewModel.binaryPreviewLoaded(attachment, position)
}
}
mEntryEditViewModel.onAttachmentAction(null)
}
AttachmentState.CANCELED,
AttachmentState.ERROR -> {
removeAttachment(entryAttachmentState)
mEntryEditViewModel.onAttachmentAction(null)
}
else -> {}
}
}
}
override fun onDatabaseRetrieved(database: Database?) {
templateView.populateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
}
mAllowMultipleAttachments = database?.allowMultipleAttachments == true
attachmentsAdapter?.database = database
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
// TODO retrieve current database with its unique key
attachmentsAdapter.database = Database.getInstance()
//attachmentsAdapter.database = arguments?.getInt(KEY_DATABASE)
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
if (previousSize > 0 && newSize == 0) {
attachmentsContainerView.collapse(true)
} else if (previousSize == 0 && newSize == 1) {
attachmentsContainerView.expand(true)
}
}
attachmentsListView.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
adapter = attachmentsAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
// Retrieve the textColor to tint the icon
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
taIconColor?.recycle()
rootView?.resetAppTimeoutWhenViewFocusedOrChanged(requireContext())
// Retrieve the new entry after an orientation change
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true)
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
else if (savedInstanceState?.containsKey(KEY_TEMP_ENTRY_INFO) == true) {
mEntryInfo = savedInstanceState.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
}
if (savedInstanceState?.containsKey(KEY_LAST_FOCUSED_FIELD) == true) {
mLastFocusedEditField = savedInstanceState.getParcelable(KEY_LAST_FOCUSED_FIELD) ?: mLastFocusedEditField
}
populateViewsWithEntry()
return rootView
}
private fun assignEntryInfo(entryInfo: EntryInfo?) {
// Populate entry views
templateView.setEntryInfo(entryInfo)
override fun onDetach() {
super.onDetach()
// Manage attachments
setAttachments(entryInfo?.attachments ?: listOf())
drawFactory = null
setOnDateClickListener = null
setOnPasswordGeneratorClickListener = null
setOnIconViewClickListener = null
setOnRemoveAttachment = null
setOnEditCustomField = null
}
private fun retrieveEntryInfo(): EntryInfo {
val entryInfo = templateView.getEntryInfo()
entryInfo.attachments = getAttachments().toMutableList()
return entryInfo
fun getEntryInfo(): EntryInfo {
populateEntryWithViews()
return mEntryInfo
}
fun generatePasswordEducationPerformed(entryEditActivityEducation: EntryEditActivityEducation): Boolean {
return entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
entryPasswordGeneratorView,
{
GeneratePasswordDialogFragment().show(parentFragmentManager, "PasswordGeneratorFragment")
},
{
try {
(activity as? EntryEditActivity?)?.performedNextEducation(entryEditActivityEducation)
} catch (ignore: Exception) {}
}
)
}
private fun populateViewsWithEntry() {
// Set info in view
icon = mEntryInfo.icon
title = mEntryInfo.title
username = mEntryInfo.username
url = mEntryInfo.url
password = mEntryInfo.password
expires = mEntryInfo.expires
expiryTime = mEntryInfo.expiryTime
notes = mEntryInfo.notes
assignExtraFields(mEntryInfo.customFields) { fields ->
setOnEditCustomField?.invoke(fields)
}
assignAttachments(mEntryInfo.attachments, StreamDirection.UPLOAD) { attachment ->
setOnRemoveAttachment?.invoke(attachment)
}
}
private fun populateEntryWithViews() {
// Icon already populate
mEntryInfo.title = title
mEntryInfo.username = username
mEntryInfo.url = url
mEntryInfo.password = password
mEntryInfo.expires = expires
mEntryInfo.expiryTime = expiryTime
mEntryInfo.notes = notes
mEntryInfo.customFields = getExtraFields()
mEntryInfo.otpModel = OtpEntryFields.parseFields { key ->
getExtraFields().firstOrNull { it.name == key }?.protectedValue?.toString()
}?.otpModel
mEntryInfo.attachments = getAttachments()
}
var title: String
get() {
return entryTitleView.text.toString()
}
set(value) {
entryTitleView.setText(value)
if (fontInVisibility)
entryTitleView.applyFontVisibility()
}
var icon: IconImage
get() {
return mEntryInfo.icon
}
set(value) {
mEntryInfo.icon = value
drawFactory?.assignDatabaseIcon(entryIconView, value, iconColor)
}
var username: String
get() {
return entryUserNameView.text.toString()
}
set(value) {
entryUserNameView.setText(value)
if (fontInVisibility)
entryUserNameView.applyFontVisibility()
}
var url: String
get() {
return entryUrlView.text.toString()
}
set(value) {
entryUrlView.setText(value)
if (fontInVisibility)
entryUrlView.applyFontVisibility()
}
var password: String
get() {
return entryPasswordView.text.toString()
}
set(value) {
entryPasswordView.setText(value)
if (fontInVisibility) {
entryPasswordView.applyFontVisibility()
}
}
var expires: Boolean
get() {
return entryExpirationView.expires
}
set(value) {
entryExpirationView.expires = value
}
var expiryTime: DateInstant
get() {
return entryExpirationView.expiryTime
}
set(value) {
entryExpirationView.expiryTime = value
}
var notes: String
get() {
return entryNotesView.text.toString()
}
set(value) {
entryNotesView.setText(value)
if (fontInVisibility)
entryNotesView.applyFontVisibility()
}
/* -------------
* Extra Fields
* -------------
*/
private var mExtraFieldsList: MutableList<Field> = ArrayList()
private var mOnEditButtonClickListener: ((item: Field)->Unit)? = null
private fun buildViewFromField(extraField: Field): View? {
val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
val itemView: View? = inflater?.inflate(R.layout.item_entry_edit_extra_field, extraFieldsListView, false)
itemView?.id = View.NO_ID
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
extraFieldValueContainer?.endIconMode = if (extraField.protectedValue.isProtected)
TextInputLayout.END_ICON_PASSWORD_TOGGLE else TextInputLayout.END_ICON_NONE
extraFieldValueContainer?.hint = extraField.name
extraFieldValueContainer?.id = View.NO_ID
val extraFieldValue: TextInputEditText? = itemView?.findViewById(R.id.entry_extra_field_value)
extraFieldValue?.apply {
if (extraField.protectedValue.isProtected) {
inputType = extraFieldValue.inputType or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
}
setText(extraField.protectedValue.toString())
if (fontInVisibility)
applyFontVisibility()
}
extraFieldValue?.id = View.NO_ID
extraFieldValue?.tag = "FIELD_VALUE_TAG"
if (mLastFocusedEditField?.field == extraField) {
mExtraViewToRequestFocus = extraFieldValue
}
val extraFieldEditButton: View? = itemView?.findViewById(R.id.entry_extra_field_edit)
extraFieldEditButton?.setOnClickListener {
mOnEditButtonClickListener?.invoke(extraField)
}
extraFieldEditButton?.id = View.NO_ID
return itemView
}
fun getExtraFields(): List<Field> {
mLastFocusedEditField = null
for (index in 0 until extraFieldsListView.childCount) {
val extraFieldValue: EditText = extraFieldsListView.getChildAt(index)
.findViewWithTag("FIELD_VALUE_TAG")
val extraField = mExtraFieldsList[index]
extraField.protectedValue.stringValue = extraFieldValue.text?.toString() ?: ""
if (extraFieldValue.isFocused) {
mLastFocusedEditField = FocusedEditField().apply {
field = extraField
cursorSelectionStart = extraFieldValue.selectionStart
cursorSelectionEnd = extraFieldValue.selectionEnd
}
}
}
return mExtraFieldsList
}
/**
* Remove all children and add new views for each field
*/
fun assignExtraFields(fields: List<Field>,
onEditButtonClickListener: ((item: Field)->Unit)?) {
extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE
// Reinit focused field
mExtraFieldsList.clear()
mExtraFieldsList.addAll(fields)
extraFieldsListView.removeAllViews()
fields.forEach {
extraFieldsListView.addView(buildViewFromField(it))
}
// Request last focus
mLastFocusedEditField?.let { focusField ->
mExtraViewToRequestFocus?.apply {
requestFocus()
setSelection(focusField.cursorSelectionStart,
focusField.cursorSelectionEnd)
}
}
mLastFocusedEditField = null
mOnEditButtonClickListener = onEditButtonClickListener
}
/**
* Update an extra field or create a new one if doesn't exists, the old value is lost
*/
fun putExtraField(extraField: Field) {
extraFieldsContainerView.visibility = View.VISIBLE
val oldField = mExtraFieldsList.firstOrNull { it.name == extraField.name }
oldField?.let {
val index = mExtraFieldsList.indexOf(oldField)
mExtraFieldsList.removeAt(index)
mExtraFieldsList.add(index, extraField)
extraFieldsListView.removeViewAt(index)
val newView = buildViewFromField(extraField)
extraFieldsListView.addView(newView, index)
newView?.requestFocus()
} ?: kotlin.run {
mExtraFieldsList.add(extraField)
val newView = buildViewFromField(extraField)
extraFieldsListView.addView(newView)
newView?.requestFocus()
}
}
/**
* Update an extra field and keep the old value
*/
fun replaceExtraField(oldExtraField: Field, newExtraField: Field) {
extraFieldsContainerView.visibility = View.VISIBLE
val index = mExtraFieldsList.indexOf(oldExtraField)
val oldValueEditText: EditText = extraFieldsListView.getChildAt(index)
.findViewWithTag("FIELD_VALUE_TAG")
val oldValue = oldValueEditText.text.toString()
val newExtraFieldWithOldValue = Field(newExtraField).apply {
this.protectedValue.stringValue = oldValue
}
mExtraFieldsList.removeAt(index)
mExtraFieldsList.add(index, newExtraFieldWithOldValue)
extraFieldsListView.removeViewAt(index)
val newView = buildViewFromField(newExtraFieldWithOldValue)
extraFieldsListView.addView(newView, index)
newView?.requestFocus()
}
fun removeExtraField(oldExtraField: Field) {
val previousSize = mExtraFieldsList.size
val index = mExtraFieldsList.indexOf(oldExtraField)
extraFieldsListView.getChildAt(index)?.let {
it.collapse(true) {
mExtraFieldsList.removeAt(index)
extraFieldsListView.removeViewAt(index)
val newSize = mExtraFieldsList.size
if (previousSize > 0 && newSize == 0) {
extraFieldsContainerView.collapse(true)
} else if (previousSize == 0 && newSize == 1) {
extraFieldsContainerView.expand(true)
}
}
}
}
/* -------------
@@ -275,84 +451,78 @@ class EntryEditFragment: DatabaseFragment() {
* -------------
*/
private fun getAttachments(): List<Attachment> {
return attachmentsAdapter?.itemsList?.map { it.attachment } ?: listOf()
fun getAttachments(): List<Attachment> {
return attachmentsAdapter.itemsList.map { it.attachment }
}
private fun setAttachments(attachments: List<Attachment>) {
fun assignAttachments(attachments: List<Attachment>,
streamDirection: StreamDirection,
onDeleteItem: (attachment: Attachment)->Unit) {
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
attachmentsAdapter?.assignItems(attachments.map {
EntryAttachmentState(it, StreamDirection.UPLOAD)
})
attachmentsAdapter?.onDeleteButtonClickListener = { item ->
val attachment = item.attachment
removeAttachment(EntryAttachmentState(attachment, StreamDirection.DOWNLOAD))
mEntryEditViewModel.deleteAttachment(attachment)
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
attachmentsAdapter.onDeleteButtonClickListener = { item ->
onDeleteItem.invoke(item.attachment)
}
}
private fun containsAttachment(): Boolean {
return attachmentsAdapter?.isEmpty() != true
fun containsAttachment(): Boolean {
return !attachmentsAdapter.isEmpty()
}
private fun containsAttachment(attachment: EntryAttachmentState): Boolean {
return attachmentsAdapter?.contains(attachment) ?: false
fun containsAttachment(attachment: EntryAttachmentState): Boolean {
return attachmentsAdapter.contains(attachment)
}
private fun putAttachment(attachment: EntryAttachmentState,
onPreviewLoaded: ((attachment: EntryAttachmentState) -> Unit)? = null) {
// When only one attachment is allowed
if (!mAllowMultipleAttachments
&& attachment.downloadState == AttachmentState.START) {
attachmentsAdapter?.clear()
}
fun putAttachment(attachment: EntryAttachmentState,
onPreviewLoaded: (()-> Unit)? = null) {
attachmentsContainerView.visibility = View.VISIBLE
attachmentsAdapter?.putItem(attachment)
attachmentsAdapter?.onBinaryPreviewLoaded = {
onPreviewLoaded?.invoke(attachment)
attachmentsAdapter.putItem(attachment)
attachmentsAdapter.onBinaryPreviewLoaded = {
onPreviewLoaded?.invoke()
}
}
private fun removeAttachment(attachment: EntryAttachmentState) {
attachmentsAdapter?.removeItem(attachment)
fun removeAttachment(attachment: EntryAttachmentState) {
attachmentsAdapter.removeItem(attachment)
}
private fun getAttachmentViewPosition(attachment: EntryAttachmentState,
position: (attachment: EntryAttachmentState, Float) -> Unit) {
fun clearAttachments() {
attachmentsAdapter.clear()
}
fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) {
attachmentsListView.postDelayed({
attachmentsAdapter?.indexOf(attachment)?.let { index ->
position.invoke(attachment,
attachmentsContainerView.y
+ attachmentsListView.y
+ (attachmentsListView.getChildAt(index)?.y
?: 0F)
)
}
position.invoke(attachmentsContainerView.y
+ attachmentsListView.y
+ (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y
?: 0F)
)
}, 250)
}
override fun onSaveInstanceState(outState: Bundle) {
populateEntryWithViews()
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
super.onSaveInstanceState(outState)
outState.putParcelableArrayList(ATTACHMENTS_TAG, ArrayList(getAttachments()))
}
/* -------------
* Education
* -------------
*/
fun getActionImageView(): View? {
return templateView.getActionImageView()
}
fun launchGeneratePasswordEductionAction() {
mEntryEditViewModel.requestPasswordSelection(templateView.getPasswordField())
}
companion object {
private val TAG = EntryEditFragment::class.java.name
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
const val KEY_DATABASE = "KEY_DATABASE"
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
private const val ATTACHMENTS_TAG = "ATTACHMENTS_TAG"
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment {
//database: Database?): EntryEditFragment {
return EntryEditFragment().apply {
arguments = Bundle().apply {
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
// TODO Unique database key database.key
putInt(KEY_DATABASE, 0)
}
}
}
}
}

View File

@@ -1,254 +0,0 @@
package com.kunzisoft.keepass.activities.fragments
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.kunzisoft.keepass.R
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.DateInstant
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.utils.UuidUtil
import com.kunzisoft.keepass.view.TemplateView
import com.kunzisoft.keepass.view.showByFading
import com.kunzisoft.keepass.viewmodels.EntryViewModel
import java.util.*
class EntryFragment: DatabaseFragment() {
private lateinit var rootView: View
private lateinit var templateView: TemplateView
private lateinit var creationDateView: TextView
private lateinit var modificationDateView: TextView
private lateinit var attachmentsContainerView: View
private lateinit var attachmentsListView: RecyclerView
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
private lateinit var uuidContainerView: View
private lateinit var uuidView: TextView
private lateinit var uuidReferenceView: TextView
private var mClipboardHelper: ClipboardHelper? = null
private val mEntryViewModel: EntryViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_entry, container, false)
}
override fun onViewCreated(view: View,
savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.let { context ->
mClipboardHelper = ClipboardHelper(context)
}
rootView = view
// Hide only the first time
if (savedInstanceState == null) {
view.isVisible = false
}
templateView = view.findViewById(R.id.entry_template)
loadTemplateSettings()
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
attachmentsListView = view.findViewById(R.id.entry_attachments_list)
attachmentsListView.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
creationDateView = view.findViewById(R.id.entry_created)
modificationDateView = view.findViewById(R.id.entry_modified)
uuidContainerView = view.findViewById(R.id.entry_UUID_container)
uuidContainerView.apply {
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
}
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.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState ->
entryAttachmentState?.let {
if (it.streamDirection != StreamDirection.UPLOAD) {
putAttachment(it)
}
}
}
}
override fun onDatabaseRetrieved(database: Database?) {
context?.let { context ->
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
attachmentsAdapter?.database = database
}
attachmentsListView.adapter = attachmentsAdapter
}
private fun loadTemplateSettings() {
context?.let { context ->
templateView.setFirstTimeAskAllowCopyProtectedFields(PreferencesUtil.isFirstTimeAskAllowCopyProtectedFields(context))
templateView.setAllowCopyProtectedFields(PreferencesUtil.allowCopyProtectedFields(context))
}
}
private fun assignEntryInfo(entryInfo: EntryInfo?) {
// Set copy buttons
templateView.apply {
setOnAskCopySafeClickListener {
showClipboardDialog()
}
setOnCopyActionClickListener { field ->
mClipboardHelper?.timeoutCopyToClipboard(
field.protectedValue.stringValue,
getString(
R.string.copy_field,
TemplateField.getLocalizedName(context, field.name)
)
)
}
}
// Populate entry views
templateView.setEntryInfo(entryInfo)
// OTP timer updated
templateView.setOnOtpElementUpdated { otpElementUpdated ->
mEntryViewModel.onOtpElementUpdated(otpElementUpdated)
}
// Manage attachments
assignAttachments(entryInfo?.attachments ?: listOf())
// Assign dates
assignCreationDate(entryInfo?.creationTime)
assignModificationDate(entryInfo?.lastModificationTime)
// Assign special data
assignUUID(entryInfo?.id)
}
private fun showClipboardDialog() {
context?.let {
AlertDialog.Builder(it)
.setMessage(
getString(R.string.allow_copy_password_warning) +
"\n\n" +
getString(R.string.clipboard_warning)
)
.create().apply {
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ ->
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, true)
finishDialog(dialog)
}
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, false)
finishDialog(dialog)
}
show()
}
}
}
private fun finishDialog(dialog: DialogInterface) {
dialog.dismiss()
loadTemplateSettings()
templateView.reload()
}
private fun assignCreationDate(date: DateInstant?) {
creationDateView.text = date?.getDateTimeString(resources)
}
private fun assignModificationDate(date: DateInstant?) {
modificationDateView.text = date?.getDateTimeString(resources)
}
private fun assignUUID(uuid: UUID?) {
uuidView.text = uuid?.toString()
uuidReferenceView.text = UuidUtil.toHexString(uuid)
}
/* -------------
* Attachments
* -------------
*/
private fun assignAttachments(attachments: List<Attachment>) {
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
attachmentsAdapter?.assignItems(attachments.map {
EntryAttachmentState(it, StreamDirection.DOWNLOAD)
})
attachmentsAdapter?.onItemClickListener = { item ->
mEntryViewModel.onAttachmentSelected(item.attachment)
}
}
fun putAttachment(attachmentToDownload: EntryAttachmentState) {
attachmentsAdapter?.putItem(attachmentToDownload)
}
/* -------------
* Education
* -------------
*/
fun firstEntryFieldCopyView(): View? {
return try {
templateView.getActionImageView()
} catch (e: Exception) {
null
}
}
fun launchEntryCopyEducationAction() {
val appNameString = getString(R.string.app_name)
mClipboardHelper?.timeoutCopyToClipboard(appNameString,
getString(R.string.copy_field, appNameString))
}
companion object {
fun getInstance(): EntryFragment {
return EntryFragment().apply {
arguments = Bundle()
}
}
}
}

View File

@@ -1,70 +0,0 @@
package com.kunzisoft.keepass.activities.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.viewmodels.EntryViewModel
class EntryHistoryFragment: StylishFragment() {
private lateinit var historyContainerView: View
private lateinit var historyListView: RecyclerView
private var historyAdapter: EntryHistoryAdapter? = null
private val mEntryViewModel: EntryViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_entry_history, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.let { context ->
historyAdapter = EntryHistoryAdapter(context)
}
historyContainerView = view.findViewById(R.id.entry_history_container)
historyListView = view.findViewById(R.id.entry_history_list)
historyListView.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
adapter = historyAdapter
}
mEntryViewModel.entryHistory.observe(viewLifecycleOwner) {
assignHistory(it)
}
}
/* -------------
* History
* -------------
*/
private fun assignHistory(history: List<EntryInfo>) {
historyAdapter?.clear()
historyAdapter?.entryHistoryList?.addAll(history)
historyAdapter?.onItemClickListener = { item, position ->
mEntryViewModel.onHistorySelected(item, position)
}
historyContainerView.visibility = if (historyAdapter?.entryHistoryList?.isEmpty() != false)
View.GONE
else
View.VISIBLE
historyAdapter?.notifyDataSetChanged()
}
}

View File

@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.activities.fragments
import android.os.Bundle
import android.view.View
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
@@ -32,8 +31,8 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
return R.layout.fragment_icon_grid
}
override fun defineIconList(database: Database?) {
database?.doForEachCustomIcons { customIcon, _ ->
override fun defineIconList() {
mDatabase?.doForEachCustomIcons { customIcon, _ ->
iconPickerAdapter.addIcon(customIcon, false)
}
}

View File

@@ -19,6 +19,7 @@
*/
package com.kunzisoft.keepass.activities.fragments
import android.content.Context
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
@@ -27,6 +28,7 @@ import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.IconPickerAdapter
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
@@ -36,48 +38,39 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
abstract class IconFragment<T: IconImageDraw> : StylishFragment(),
IconPickerAdapter.IconPickerListener<T> {
protected lateinit var iconsGridView: RecyclerView
protected lateinit var iconPickerAdapter: IconPickerAdapter<T>
protected var iconActionSelectionMode = false
protected var mDatabase: Database? = null
protected val iconPickerViewModel: IconPickerViewModel by activityViewModels()
abstract fun retrieveMainLayoutId(): Int
abstract fun defineIconList(database: Database?)
abstract fun defineIconList()
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View {
return inflater.inflate(retrieveMainLayoutId(), container, false)
}
override fun onAttach(context: Context) {
super.onAttach(context)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mDatabase = Database.getInstance()
// Retrieve the textColor to tint the icon
val ta = contextThemed?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
val tintColor = ta?.getColor(0, Color.BLACK) ?: Color.BLACK
ta?.recycle()
iconsGridView = view.findViewById(R.id.icons_grid_view)
iconPickerAdapter = IconPickerAdapter(requireContext(), tintColor)
iconPickerAdapter.iconPickerListener = this
iconsGridView.adapter = iconPickerAdapter
resetAppTimeoutWhenViewFocusedOrChanged(view)
}
override fun onDatabaseRetrieved(database: Database?) {
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory
iconPickerAdapter = IconPickerAdapter<T>(context, tintColor).apply {
iconDrawableFactory = mDatabase?.iconDrawableFactory
}
CoroutineScope(Dispatchers.IO).launch {
val populateList = launch {
iconPickerAdapter.clear()
defineIconList(database)
defineIconList()
}
withContext(Dispatchers.Main) {
populateList.join()
@@ -86,6 +79,21 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
}
}
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View {
val root = inflater.inflate(retrieveMainLayoutId(), container, false)
iconsGridView = root.findViewById(R.id.icons_grid_view)
iconsGridView.adapter = iconPickerAdapter
return root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
iconPickerAdapter.iconPickerListener = this
}
fun onIconDeleteClicked() {
iconActionSelectionMode = false
}

View File

@@ -9,18 +9,20 @@ import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.IconPickerPagerAdapter
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
class IconPickerFragment : DatabaseFragment() {
class IconPickerFragment : StylishFragment() {
private var iconPickerPagerAdapter: IconPickerPagerAdapter? = null
private lateinit var viewPager: ViewPager2
private lateinit var tabLayout: TabLayout
private val iconPickerViewModel: IconPickerViewModel by activityViewModels()
private var mDatabase: Database? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -30,11 +32,19 @@ class IconPickerFragment : DatabaseFragment() {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mDatabase = Database.getInstance()
viewPager = view.findViewById(R.id.icon_picker_pager)
tabLayout = view.findViewById(R.id.icon_picker_tabs)
resetAppTimeoutWhenViewFocusedOrChanged(view)
val tabLayout = view.findViewById<TabLayout>(R.id.icon_picker_tabs)
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
if (mDatabase?.allowCustomIcons == true) 2 else 1)
viewPager.adapter = iconPickerPagerAdapter
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = when (position) {
1 -> getString(R.string.icon_section_custom)
else -> getString(R.string.icon_section_standard)
}
}.attach()
arguments?.apply {
if (containsKey(ICON_TAB_ARG)) {
@@ -48,18 +58,6 @@ class IconPickerFragment : DatabaseFragment() {
}
}
override fun onDatabaseRetrieved(database: Database?) {
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
if (database?.allowCustomIcons == true) 2 else 1)
viewPager.adapter = iconPickerPagerAdapter
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = when (position) {
1 -> getString(R.string.icon_section_custom)
else -> getString(R.string.icon_section_standard)
}
}.attach()
}
enum class IconTab {
STANDARD, CUSTOM
}

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.activities.fragments
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
@@ -30,8 +29,8 @@ class IconStandardFragment : IconFragment<IconImageStandard>() {
return R.layout.fragment_icon_grid
}
override fun defineIconList(database: Database?) {
database?.doForEachStandardIcons { standardIcon ->
override fun defineIconList() {
mDatabase?.doForEachStandardIcons { standardIcon ->
iconPickerAdapter.addIcon(standardIcon, false)
}
}

View File

@@ -25,40 +25,34 @@ import android.os.Bundle
import android.util.Log
import android.view.*
import androidx.appcompat.view.ActionMode
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.EntryEditActivity
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.NodeAdapter
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.GroupViewModel
import java.util.*
class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListener {
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
private var nodeClickListener: NodeClickListener? = null
private var onScrollListener: OnScrollListener? = null
private var mNodesRecyclerView: RecyclerView? = null
private var mLayoutManager: LinearLayoutManager? = null
var mainGroup: Group? = null
private set
private var mAdapter: NodeAdapter? = null
private val mGroupViewModel: GroupViewModel by activityViewModels()
private var mCurrentGroup: Group? = null
var nodeActionSelectionMode = false
private set
var nodeActionPasteMode: PasteMode = PasteMode.UNDEFINED
@@ -69,27 +63,13 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
private var notFoundView: View? = null
private var isASearchResult: Boolean = false
private var specialMode: SpecialMode = SpecialMode.DEFAULT
private var mRecycleBinEnable: Boolean = false
private var mRecycleBin: Group? = null
private var readOnly: Boolean = false
private var specialMode: SpecialMode = SpecialMode.DEFAULT
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)
if (newState == SCROLL_STATE_IDLE) {
mGroupViewModel.assignPosition(getFirstVisiblePosition())
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
onScrollListener?.onScrolled(dy)
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
try {
@@ -120,136 +100,128 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onDatabaseRetrieved(database: Database?) {
mRecycleBinEnable = database?.isRecycleBinEnabled == true
mRecycleBin = database?.recycleBin
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments)
arguments?.let { args ->
// Contains all the group in element
if (args.containsKey(GROUP_KEY)) {
mainGroup = args.getParcelable(GROUP_KEY)
}
if (args.containsKey(IS_SEARCH)) {
isASearchResult = args.getBoolean(IS_SEARCH)
}
}
contextThemed?.let { context ->
database?.let { database ->
mAdapter = NodeAdapter(context, database).apply {
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
override fun onNodeClick(database: Database, node: Node) {
if (nodeActionSelectionMode) {
if (listActionNodes.contains(node)) {
// Remove selected item if already selected
listActionNodes.remove(node)
} else {
// Add selected item if not already selected
listActionNodes.add(node)
}
nodeClickListener?.onNodeSelected(database, listActionNodes)
setActionNodes(listActionNodes)
notifyNodeChanged(node)
mAdapter = NodeAdapter(context)
mAdapter?.apply {
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
override fun onNodeClick(node: Node) {
if (nodeActionSelectionMode) {
if (listActionNodes.contains(node)) {
// Remove selected item if already selected
listActionNodes.remove(node)
} else {
nodeClickListener?.onNodeClick(database, node)
// Add selected item if not already selected
listActionNodes.add(node)
}
nodeClickListener?.onNodeSelected(listActionNodes)
setActionNodes(listActionNodes)
notifyNodeChanged(node)
} else {
nodeClickListener?.onNodeClick(node)
}
}
override fun onNodeLongClick(database: Database, node: Node): Boolean {
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
// Select the first item after a long click
if (!listActionNodes.contains(node))
listActionNodes.add(node)
override fun onNodeLongClick(node: Node): Boolean {
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
// Select the first item after a long click
if (!listActionNodes.contains(node))
listActionNodes.add(node)
nodeClickListener?.onNodeSelected(database, listActionNodes)
nodeClickListener?.onNodeSelected(listActionNodes)
setActionNodes(listActionNodes)
notifyNodeChanged(node)
}
return true
setActionNodes(listActionNodes)
notifyNodeChanged(node)
}
})
}
mNodesRecyclerView?.adapter = mAdapter
return true
}
})
}
}
}
override fun onDatabaseActionFinished(
database: Database,
actionTask: String,
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
// Too many special cases to make specific additions or deletions,
// rebuilt the list works well.
if (result.isSuccess) {
rebuildList()
}
override fun onSaveInstanceState(outState: Bundle) {
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
super.onSaveInstanceState(outState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
// To apply theme
return inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_group, container, false)
}
val rootView = inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_list_nodes, container, false)
mNodesRecyclerView = rootView.findViewById(R.id.nodes_list)
notFoundView = rootView.findViewById(R.id.not_found_container)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mNodesRecyclerView = view.findViewById(R.id.nodes_list)
notFoundView = view.findViewById(R.id.not_found_container)
mLayoutManager = LinearLayoutManager(context)
mNodesRecyclerView?.apply {
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
layoutManager = mLayoutManager
layoutManager = LinearLayoutManager(context)
adapter = mAdapter
}
resetAppTimeoutWhenViewFocusedOrChanged(view)
mGroupViewModel.group.observe(viewLifecycleOwner) {
mCurrentGroup = it.group
isASearchResult = it.group.isVirtual
rebuildList()
it.showFromPosition?.let { position ->
mNodesRecyclerView?.scrollToPosition(position)
}
onScrollListener?.let { onScrollListener ->
mNodesRecyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
onScrollListener.onScrolled(dy)
}
})
}
return rootView
}
override fun onResume() {
super.onResume()
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
activity?.intent?.let {
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
}
rebuildList()
}
// Refresh data
try {
rebuildList()
} catch (e: Exception) {
Log.e(TAG, "Unable to rebuild the list during resume")
e.printStackTrace()
}
override fun onPause() {
mNodesRecyclerView?.removeOnScrollListener(mRecycleViewScrollListener)
super.onPause()
}
fun getFirstVisiblePosition(): Int {
return mLayoutManager?.findFirstVisibleItemPosition() ?: 0
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
// To show the " no search entry found "
mNodesRecyclerView?.visibility = View.GONE
notFoundView?.visibility = View.VISIBLE
} else {
mNodesRecyclerView?.visibility = View.VISIBLE
notFoundView?.visibility = View.GONE
}
}
@Throws(IllegalArgumentException::class)
private fun rebuildList() {
try {
// Add elements to the list
mCurrentGroup?.let { mainGroup ->
fun rebuildList() {
// Add elements to the list
mainGroup?.let { mainGroup ->
mAdapter?.apply {
// Thrown an exception when sort cannot be performed
mAdapter?.rebuildList(mainGroup)
rebuildList(mainGroup)
// To visually change the elements
if (PreferencesUtil.APPEARANCE_CHANGED) {
notifyDataSetChanged()
PreferencesUtil.APPEARANCE_CHANGED = false
}
}
} catch (e:Exception) {
Log.e(TAG, "Unable to rebuild the list", e)
}
if (isASearchResult && mAdapter != null && mAdapter!!.isEmpty) {
// To show the " no search entry found "
notFoundView?.visibility = View.VISIBLE
} else {
notFoundView?.visibility = View.GONE
}
}
@@ -265,7 +237,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
rebuildList()
} catch (e:Exception) {
Log.e(TAG, "Unable to sort the list", e)
Log.e(TAG, "Unable to rebuild the list with the sort")
e.printStackTrace()
}
}
@@ -281,7 +254,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
R.id.menu_sort -> {
context?.let { context ->
val sortDialogFragment: SortDialogFragment =
if (mRecycleBinEnable) {
if (Database.getInstance().isRecycleBinEnabled) {
SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
@@ -303,32 +276,34 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
}
}
fun actionNodesCallback(database: Database,
nodes: List<Node>,
fun actionNodesCallback(nodes: List<Node>,
menuListener: NodesActionMenuListener?,
onDestroyActionMode: (mode: ActionMode?) -> Unit) : ActionMode.Callback {
actionModeCallback: ActionMode.Callback) : ActionMode.Callback {
return object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
nodeActionSelectionMode = false
nodeActionPasteMode = PasteMode.UNDEFINED
return true
return actionModeCallback.onCreateActionMode(mode, menu)
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
menu?.clear()
if (nodeActionPasteMode != PasteMode.UNDEFINED) {
mode?.menuInflater?.inflate(R.menu.node_paste_menu, menu)
} else {
nodeActionSelectionMode = true
mode?.menuInflater?.inflate(R.menu.node_menu, menu)
val database = Database.getInstance()
// Open and Edit for a single item
if (nodes.size == 1) {
// Edition
if (database.isReadOnly
|| (mRecycleBinEnable && nodes[0] == mRecycleBin)) {
if (readOnly
|| (database.isRecycleBinEnabled && nodes[0] == database.recycleBin)) {
menu?.removeItem(R.id.menu_edit)
}
} else {
@@ -336,59 +311,56 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
menu?.removeItem(R.id.menu_edit)
}
// Move
if (database.isReadOnly
|| isASearchResult) {
// Copy and Move (not for groups)
if (readOnly
|| isASearchResult
|| nodes.any { it.type == Type.GROUP }) {
// TODO Copy For Group
menu?.removeItem(R.id.menu_copy)
menu?.removeItem(R.id.menu_move)
}
// Copy (not allowed for group)
if (database.isReadOnly
|| isASearchResult
|| nodes.any { it.type == Type.GROUP }) {
menu?.removeItem(R.id.menu_copy)
}
// Deletion
if (database.isReadOnly
|| (mRecycleBinEnable && nodes.any { it == mRecycleBin })) {
if (readOnly
|| (database.isRecycleBinEnabled && nodes.any { it == database.recycleBin })) {
menu?.removeItem(R.id.menu_delete)
}
}
// Add the number of items selected in title
mode?.title = nodes.size.toString()
return true
return actionModeCallback.onPrepareActionMode(mode, menu)
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
if (menuListener == null)
return false
return when (item?.itemId) {
R.id.menu_open -> menuListener.onOpenMenuClick(database, nodes[0])
R.id.menu_edit -> menuListener.onEditMenuClick(database, nodes[0])
R.id.menu_open -> menuListener.onOpenMenuClick(nodes[0])
R.id.menu_edit -> menuListener.onEditMenuClick(nodes[0])
R.id.menu_copy -> {
nodeActionPasteMode = PasteMode.PASTE_FROM_COPY
mAdapter?.unselectActionNodes()
val returnValue = menuListener.onCopyMenuClick(database, nodes)
val returnValue = menuListener.onCopyMenuClick(nodes)
nodeActionSelectionMode = false
returnValue
}
R.id.menu_move -> {
nodeActionPasteMode = PasteMode.PASTE_FROM_MOVE
mAdapter?.unselectActionNodes()
val returnValue = menuListener.onMoveMenuClick(database, nodes)
val returnValue = menuListener.onMoveMenuClick(nodes)
nodeActionSelectionMode = false
returnValue
}
R.id.menu_delete -> menuListener.onDeleteMenuClick(database, nodes)
R.id.menu_delete -> menuListener.onDeleteMenuClick(nodes)
R.id.menu_paste -> {
val returnValue = menuListener.onPasteMenuClick(database, nodeActionPasteMode, nodes)
val returnValue = menuListener.onPasteMenuClick(nodeActionPasteMode, nodes)
nodeActionPasteMode = PasteMode.UNDEFINED
nodeActionSelectionMode = false
returnValue
}
else -> false
else -> actionModeCallback.onActionItemClicked(mode, item)
}
}
@@ -398,7 +370,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
mAdapter?.unselectActionNodes()
nodeActionPasteMode = PasteMode.UNDEFINED
nodeActionSelectionMode = false
onDestroyActionMode(mode)
actionModeCallback.onDestroyActionMode(mode)
}
}
}
@@ -408,40 +380,73 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
when (requestCode) {
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
if (resultCode == EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE) {
data?.getParcelableExtra<NodeId<UUID>>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let {
// Simply refresh the list
rebuildList()
// Scroll to the new entry
mDatabase?.getEntryById(it)?.let { entry ->
mAdapter?.indexOf(entry)?.let { position ->
mNodesRecyclerView?.scrollToPosition(position)
}
}
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { changedNode ->
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
addNode(changedNode)
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE)
mAdapter?.notifyDataSetChanged()
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
}
}
}
}
fun contains(node: Node): Boolean {
return mAdapter?.contains(node) ?: false
}
fun addNode(newNode: Node) {
mAdapter?.addNode(newNode)
}
fun addNodes(newNodes: List<Node>) {
mAdapter?.addNodes(newNodes)
}
fun updateNode(oldNode: Node, newNode: Node? = null) {
mAdapter?.updateNode(oldNode, newNode ?: oldNode)
}
fun updateNodes(oldNodes: List<Node>, newNodes: List<Node>) {
mAdapter?.updateNodes(oldNodes, newNodes)
}
fun removeNode(pwNode: Node) {
mAdapter?.removeNode(pwNode)
}
fun removeNodes(nodes: List<Node>) {
mAdapter?.removeNodes(nodes)
}
fun removeNodeAt(position: Int) {
mAdapter?.removeNodeAt(position)
}
fun removeNodesAt(positions: IntArray) {
mAdapter?.removeNodesAt(positions)
}
/**
* Callback listener to redefine to do an action when a node is click
*/
interface NodeClickListener {
fun onNodeClick(database: Database, node: Node)
fun onNodeSelected(database: Database, nodes: List<Node>): Boolean
fun onNodeClick(node: Node)
fun onNodeSelected(nodes: List<Node>): Boolean
}
/**
* Menu listener to redefine to do an action in menu
*/
interface NodesActionMenuListener {
fun onOpenMenuClick(database: Database, node: Node): Boolean
fun onEditMenuClick(database: Database, node: Node): Boolean
fun onCopyMenuClick(database: Database, nodes: List<Node>): Boolean
fun onMoveMenuClick(database: Database, nodes: List<Node>): Boolean
fun onDeleteMenuClick(database: Database, nodes: List<Node>): Boolean
fun onPasteMenuClick(database: Database, pasteMode: PasteMode?, nodes: List<Node>): Boolean
fun onOpenMenuClick(node: Node): Boolean
fun onEditMenuClick(node: Node): Boolean
fun onCopyMenuClick(nodes: List<Node>): Boolean
fun onMoveMenuClick(nodes: List<Node>): Boolean
fun onDeleteMenuClick(nodes: List<Node>): Boolean
fun onPasteMenuClick(pasteMode: PasteMode?, nodes: List<Node>): Boolean
}
enum class PasteMode {
@@ -460,6 +465,22 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
}
companion object {
private val TAG = GroupFragment::class.java.name
private val TAG = ListNodesFragment::class.java.name
private const val GROUP_KEY = "GROUP_KEY"
private const val IS_SEARCH = "IS_SEARCH"
fun newInstance(group: Group?, readOnly: Boolean, isASearch: Boolean): ListNodesFragment {
val bundle = Bundle()
if (group != null) {
bundle.putParcelable(GROUP_KEY, group)
}
bundle.putBoolean(IS_SEARCH, isASearch)
ReadOnlyHelper.putReadOnlyInBundle(bundle, readOnly)
val listNodesFragment = ListNodesFragment()
listNodesFragment.arguments = bundle
return listNodesFragment
}
}
}

View File

@@ -1,244 +0,0 @@
/*
* Copyright 2019 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/>.
*
*/
package com.kunzisoft.keepass.activities.helpers
import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.util.Log
import android.view.View
import androidx.annotation.RequiresApi
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
import com.kunzisoft.keepass.utils.UriUtil
class ExternalFileHelper {
private var activity: FragmentActivity? = null
private var fragment: Fragment? = null
constructor(context: FragmentActivity) {
this.activity = context
this.fragment = null
}
constructor(context: Fragment) {
this.activity = context.activity
this.fragment = context
}
fun openDocument(getContent: Boolean = false,
typeString: String = "*/*") {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
if (getContent) {
openActivityWithActionGetContent(typeString)
} else {
openActivityWithActionOpenDocument(typeString)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to open document", e)
showFileManagerDialogFragment()
}
} else {
showFileManagerDialogFragment()
}
}
@RequiresApi(Build.VERSION_CODES.KITKAT)
private fun openActivityWithActionOpenDocument(typeString: String) {
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
}
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
if (fragment != null)
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
else
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
}
@RequiresApi(Build.VERSION_CODES.KITKAT)
private fun openActivityWithActionGetContent(typeString: String) {
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
}
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
if (fragment != null)
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
else
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
}
/**
* To use in onActivityResultCallback in Fragment or Activity
* @param onFileSelected Callback retrieve from data
* @return true if requestCode was captured, false elsewhere
*/
fun onOpenDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
onFileSelected: ((uri: Uri?) -> Unit)?): Boolean {
when (requestCode) {
FILE_BROWSE -> {
if (resultCode == RESULT_OK) {
val filename = data?.dataString
var keyUri: Uri? = null
if (filename != null) {
keyUri = UriUtil.parse(filename)
}
onFileSelected?.invoke(keyUri)
}
return true
}
GET_CONTENT, OPEN_DOC -> {
if (resultCode == RESULT_OK) {
if (data != null) {
val uri = data.data
if (uri != null) {
UriUtil.takeUriPermission(activity?.contentResolver, uri)
onFileSelected?.invoke(uri)
}
}
}
return true
}
}
return false
}
/**
* Show Browser dialog to select file picker app
*/
private fun showFileManagerDialogFragment() {
try {
if (fragment != null) {
fragment?.parentFragmentManager
} else {
activity?.supportFragmentManager
}?.let { fragmentManager ->
FileManagerDialogFragment().show(fragmentManager, "browserDialog")
}
} catch (e: Exception) {
Log.e(TAG, "Can't open BrowserDialog", e)
}
}
fun createDocument(titleString: String,
typeString: String = "application/octet-stream"): Int? {
val idCode = getUnusedCreateFileRequestCode()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
putExtra(Intent.EXTRA_TITLE, titleString)
}
if (fragment != null)
fragment?.startActivityForResult(intent, idCode)
else
activity?.startActivityForResult(intent, idCode)
return idCode
} catch (e: Exception) {
Log.e(TAG, "Unable to create document", e)
showFileManagerDialogFragment()
}
} else {
showFileManagerDialogFragment()
}
return null
}
/**
* To use in onActivityResultCallback in Fragment or Activity
* @param onFileCreated Callback retrieve from data
* @return true if requestCode was captured, false elsewhere
*/
fun onCreateDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
onFileCreated: (fileCreated: Uri?)->Unit) {
// Retrieve the created URI from the file manager
if (fileRequestCodes.contains(requestCode) && resultCode == RESULT_OK) {
onFileCreated.invoke(data?.data)
fileRequestCodes.remove(requestCode)
}
}
companion object {
private const val TAG = "OpenFileHelper"
private const val GET_CONTENT = 25745
private const val OPEN_DOC = 25845
private const val FILE_BROWSE = 25645
private var CREATE_FILE_REQUEST_CODE_DEFAULT = 3853
private var fileRequestCodes = ArrayList<Int>()
private fun getUnusedCreateFileRequestCode(): Int {
val newCreateFileRequestCode = CREATE_FILE_REQUEST_CODE_DEFAULT++
fileRequestCodes.add(newCreateFileRequestCode)
return newCreateFileRequestCode
}
@SuppressLint("InlinedApi")
fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager,
typeString: String = "application/octet-stream"): Boolean {
return when {
// To check if a custom file manager can manage the ACTION_CREATE_DOCUMENT
Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT -> {
packageManager.queryIntentActivities(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
}, PackageManager.MATCH_DEFAULT_ONLY).isNotEmpty()
}
else -> true
}
}
}
}
fun View.setOpenDocumentClickListener(externalFileHelper: ExternalFileHelper?) {
externalFileHelper?.let { fileHelper ->
setOnClickListener {
fileHelper.openDocument()
}
setOnLongClickListener {
fileHelper.openDocument(true)
true
}
} ?: kotlin.run {
setOnClickListener(null)
setOnLongClickListener(null)
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2019 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/>.
*
*/
package com.kunzisoft.keepass.activities.helpers
import android.content.Context
import android.content.Intent
import android.os.Bundle
import com.kunzisoft.keepass.settings.PreferencesUtil
object ReadOnlyHelper {
private const val READ_ONLY_KEY = "READ_ONLY_KEY"
const val READ_ONLY_DEFAULT = false
fun retrieveReadOnlyFromIntent(intent: Intent): Boolean {
return intent.getBooleanExtra(READ_ONLY_KEY, READ_ONLY_DEFAULT)
}
fun retrieveReadOnlyFromInstanceStateOrPreference(context: Context, savedInstanceState: Bundle?): Boolean {
return if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
savedInstanceState.getBoolean(READ_ONLY_KEY)
} else {
PreferencesUtil.enableReadOnlyDatabase(context)
}
}
fun retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState: Bundle?, arguments: Bundle?): Boolean {
var readOnly = READ_ONLY_DEFAULT
if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY)
} else if (arguments != null && arguments.containsKey(READ_ONLY_KEY)) {
readOnly = arguments.getBoolean(READ_ONLY_KEY)
}
return readOnly
}
fun retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState: Bundle?, intent: Intent?): Boolean {
var readOnly = READ_ONLY_DEFAULT
if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY)
} else {
if (intent != null)
readOnly = intent.getBooleanExtra(READ_ONLY_KEY, READ_ONLY_DEFAULT)
}
return readOnly
}
fun putReadOnlyInIntent(intent: Intent, readOnly: Boolean) {
intent.putExtra(READ_ONLY_KEY, readOnly)
}
fun putReadOnlyInBundle(bundle: Bundle, readOnly: Boolean) {
bundle.putBoolean(READ_ONLY_KEY, readOnly)
}
fun onSaveInstanceState(outState: Bundle, readOnly: Boolean) {
outState.putBoolean(READ_ONLY_KEY, readOnly)
}
}

View File

@@ -0,0 +1,244 @@
/*
* Copyright 2019 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/>.
*
*/
package com.kunzisoft.keepass.activities.helpers
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.util.Log
import android.view.MenuItem
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
import com.kunzisoft.keepass.utils.UriUtil
class SelectFileHelper {
private var activity: Activity? = null
private var fragment: Fragment? = null
val selectFileOnClickViewListener: SelectFileOnClickViewListener
get() = SelectFileOnClickViewListener()
constructor(context: Activity) {
this.activity = context
this.fragment = null
}
constructor(context: Fragment) {
this.activity = context.activity
this.fragment = context
}
inner class SelectFileOnClickViewListener :
View.OnClickListener,
View.OnLongClickListener,
MenuItem.OnMenuItemClickListener {
private fun onAbstractClick(longClick: Boolean = false) {
try {
if (longClick) {
try {
openActivityWithActionGetContent()
} catch (e: Exception) {
openActivityWithActionOpenDocument()
}
} else {
try {
openActivityWithActionOpenDocument()
} catch (e: Exception) {
openActivityWithActionGetContent()
}
}
} catch (e: Exception) {
Log.e(TAG, "Enable to start the file picker activity", e)
// Open browser dialog
if (lookForOpenIntentsFilePicker())
showBrowserDialog()
}
}
override fun onClick(v: View) {
onAbstractClick()
}
override fun onLongClick(v: View?): Boolean {
onAbstractClick(true)
return true
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
onAbstractClick()
return true
}
}
@SuppressLint("InlinedApi")
private fun openActivityWithActionOpenDocument() {
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
if (fragment != null)
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
else
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
}
@SuppressLint("InlinedApi")
private fun openActivityWithActionGetContent() {
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
if (fragment != null)
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
else
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
}
private fun lookForOpenIntentsFilePicker(): Boolean {
var showBrowser = false
try {
if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
val intent = Intent(OPEN_INTENTS_FILE_BROWSE)
if (fragment != null)
fragment?.startActivityForResult(intent, FILE_BROWSE)
else
activity?.startActivityForResult(intent, FILE_BROWSE)
} else {
showBrowser = true
}
} catch (e: Exception) {
Log.w(TAG, "Enable to start OPEN_INTENTS_FILE_BROWSE", e)
showBrowser = true
}
return showBrowser
}
/**
* Indicates whether the specified action can be used as an intent. This
* method queries the package manager for installed packages that can
* respond to an intent with the specified action. If no suitable package is
* found, this method returns false.
*
* @param context The application's environment.
* @param action The Intent action to check for availability.
*
* @return True if an Intent with the specified action can be sent and
* responded to, false otherwise.
*/
private fun isIntentAvailable(context: Context, action: String): Boolean {
val packageManager = context.packageManager
val intent = Intent(action)
val list = packageManager.queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY)
return list.size > 0
}
/**
* Show Browser dialog to select file picker app
*/
private fun showBrowserDialog() {
try {
val fileManagerDialogFragment = FileManagerDialogFragment()
fragment?.let {
fileManagerDialogFragment.show(it.parentFragmentManager, "browserDialog")
} ?: fileManagerDialogFragment.show((activity as FragmentActivity).supportFragmentManager, "browserDialog")
} catch (e: Exception) {
Log.e(TAG, "Can't open BrowserDialog", e)
}
}
/**
* To use in onActivityResultCallback in Fragment or Activity
* @param keyFileCallback Callback retrieve from data
* @return true if requestCode was captured, false elsechere
*/
fun onActivityResultCallback(
requestCode: Int,
resultCode: Int,
data: Intent?,
keyFileCallback: ((uri: Uri?) -> Unit)?): Boolean {
when (requestCode) {
FILE_BROWSE -> {
if (resultCode == RESULT_OK) {
val filename = data?.dataString
var keyUri: Uri? = null
if (filename != null) {
keyUri = UriUtil.parse(filename)
}
keyFileCallback?.invoke(keyUri)
}
return true
}
GET_CONTENT, OPEN_DOC -> {
if (resultCode == RESULT_OK) {
if (data != null) {
val uri = data.data
if (uri != null) {
try {
// try to persist read and write permissions
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
activity?.contentResolver?.apply {
takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
}
} catch (e: Exception) {
// nop
}
keyFileCallback?.invoke(uri)
}
}
}
return true
}
}
return false
}
companion object {
private const val TAG = "OpenFileHelper"
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"
private const val GET_CONTENT = 25745
private const val OPEN_DOC = 25845
private const val FILE_BROWSE = 25645
}
}

View File

@@ -1,90 +0,0 @@
package com.kunzisoft.keepass.activities.legacy
import android.net.Uri
import android.os.Bundle
import androidx.activity.viewModels
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 {
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
protected var mDatabase: Database? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDatabaseTaskProvider = DatabaseTaskProvider(this)
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
if (mDatabase == null || mDatabase != database) {
onDatabaseRetrieved(database)
}
}
mDatabaseTaskProvider?.onActionFinish = { database, actionTask, result ->
onDatabaseActionFinished(database, actionTask, result)
}
}
override fun onDatabaseRetrieved(database: Database?) {
mDatabase = database
mDatabaseViewModel.defineDatabase(database)
// optional method implementation
}
override fun onDatabaseActionFinished(
database: Database,
actionTask: String,
result: ActionRunnable.Result
) {
mDatabaseViewModel.onActionFinished(database, actionTask, result)
// optional method implementation
}
fun createDatabase(databaseUri: Uri,
mainCredential: MainCredential) {
mDatabaseTaskProvider?.startDatabaseCreate(databaseUri, mainCredential)
}
fun loadDatabase(databaseUri: Uri,
mainCredential: MainCredential,
readOnly: Boolean,
cipherEntity: CipherDatabaseEntity?,
fixDuplicateUuid: Boolean) {
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEntity, fixDuplicateUuid)
}
protected fun closeDatabase() {
mDatabase?.clearAndClose(this)
}
override fun reloadActivity() {
super.reloadActivity()
mDatabase?.wasReloaded = false
}
override fun onResume() {
super.onResume()
if (mDatabase?.wasReloaded == true) {
reloadActivity()
}
mDatabaseTaskProvider?.registerProgressTask()
}
override fun onPause() {
mDatabaseTaskProvider?.unregisterProgressTask()
super.onPause()
}
}

View File

@@ -1,479 +0,0 @@
/*
* Copyright 2019 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/>.
*
*/
package com.kunzisoft.keepass.activities.legacy
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.viewModels
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
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.icons.IconDrawableFactory
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.NodesViewModel
import java.util.*
abstract class DatabaseLockActivity : DatabaseModeActivity(),
PasswordEncodingDialogFragment.Listener {
private val mNodesViewModel: NodesViewModel by viewModels()
protected var mTimeoutEnable: Boolean = true
private var mLockReceiver: LockReceiver? = null
private var mExitLock: Boolean = false
protected var mDatabaseReadOnly: Boolean = true
private var mAutoSaveEnable: Boolean = true
protected var mIconDrawableFactory: IconDrawableFactory? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null
&& savedInstanceState.containsKey(TIMEOUT_ENABLE_KEY)
) {
mTimeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY)
} else {
if (intent != null)
mTimeoutEnable =
intent.getBooleanExtra(TIMEOUT_ENABLE_KEY, TIMEOUT_ENABLE_KEY_DEFAULT)
}
mNodesViewModel.nodesToPermanentlyDelete.observe(this) { nodes ->
deleteDatabaseNodes(nodes)
}
mDatabaseViewModel.saveDatabase.observe(this) { save ->
mDatabaseTaskProvider?.startDatabaseSave(save)
}
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
}
mDatabaseViewModel.saveName.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveDescription.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveDescription(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveDefaultUsername.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveColor.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveColor(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveCompression.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveCompression(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.removeUnlinkData.observe(this) {
mDatabaseTaskProvider?.startDatabaseRemoveUnlinkedData(it)
}
mDatabaseViewModel.saveRecycleBin.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveRecycleBin(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveTemplatesGroup.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveTemplatesGroup(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveMaxHistoryItems.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveMaxHistoryItems(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveMaxHistorySize.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveEncryption.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveEncryption(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveKeyDerivation.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveKeyDerivation(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveIterations.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveIterations(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveMemoryUsage.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveParallelism.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveParallelism(it.oldValue, it.newValue, it.save)
}
mExitLock = false
}
open fun finishActivityIfDatabaseNotLoaded(): Boolean {
return true
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
// End activity if database not loaded
if (finishActivityIfDatabaseNotLoaded() && (database == null || !database.loaded)) {
finish()
}
// Focus view to reinitialize timeout,
// view is not necessary loaded so retry later in resume
viewToInvalidateTimeout()
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database?.loaded)
database?.let {
// check timeout
if (mTimeoutEnable) {
if (mLockReceiver == null) {
mLockReceiver = LockReceiver {
mDatabase = null
closeDatabase(database)
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
// Add onActivityForResult response
setResult(RESULT_EXIT_LOCK)
closeOptionsMenu()
finish()
}
registerLockReceiver(mLockReceiver)
}
// After the first creation
// or If simply swipe with another application
// If the time is out -> close the Activity
TimeoutHelper.checkTimeAndLockIfTimeout(this)
// If onCreate already record time
if (!mExitLock)
TimeoutHelper.recordTime(this, database.loaded)
}
mDatabaseReadOnly = database.isReadOnly
mIconDrawableFactory = database.iconDrawableFactory
checkRegister()
}
}
abstract fun viewToInvalidateTimeout(): View?
override fun onDatabaseActionFinished(
database: Database,
actionTask: String,
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) {
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
// Reload the current activity
if (result.isSuccess) {
reloadActivity()
} else {
this.showActionErrorIfNeeded(result)
finish()
}
}
}
}
override fun onPasswordEncodingValidateListener(databaseUri: Uri?,
mainCredential: MainCredential) {
assignDatabasePassword(databaseUri, mainCredential)
}
private fun assignDatabasePassword(databaseUri: Uri?,
mainCredential: MainCredential) {
if (databaseUri != null) {
mDatabaseTaskProvider?.startDatabaseAssignPassword(databaseUri, mainCredential)
}
}
fun assignPassword(mainCredential: MainCredential) {
mDatabase?.let { database ->
database.fileUri?.let { databaseUri ->
// Show the progress dialog now or after dialog confirmation
if (database.validatePasswordEncoding(mainCredential)) {
assignDatabasePassword(databaseUri, mainCredential)
} else {
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
.show(supportFragmentManager, "passwordEncodingTag")
}
}
}
}
fun saveDatabase() {
mDatabaseTaskProvider?.startDatabaseSave(true)
}
fun reloadDatabase() {
mDatabaseTaskProvider?.startDatabaseReload(false)
}
fun createEntry(newEntry: Entry,
parent: Group) {
mDatabaseTaskProvider?.startDatabaseCreateEntry(newEntry, parent, mAutoSaveEnable)
}
fun updateEntry(oldEntry: Entry,
entryToUpdate: Entry) {
mDatabaseTaskProvider?.startDatabaseUpdateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
}
fun copyNodes(nodesToCopy: List<Node>,
newParent: Group) {
mDatabaseTaskProvider?.startDatabaseCopyNodes(nodesToCopy, newParent, mAutoSaveEnable)
}
fun moveNodes(nodesToMove: List<Node>,
newParent: Group) {
mDatabaseTaskProvider?.startDatabaseMoveNodes(nodesToMove, newParent, mAutoSaveEnable)
}
private fun eachNodeRecyclable(database: Database, nodes: List<Node>): Boolean {
return nodes.find { node ->
var cannotRecycle = true
if (node is Entry) {
cannotRecycle = !database.canRecycle(node)
} else if (node is Group) {
cannotRecycle = !database.canRecycle(node)
}
cannotRecycle
} == null
}
fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false) {
mDatabase?.let { database ->
// If recycle bin enabled, ensure it exists
if (database.isRecycleBinEnabled) {
database.ensureRecycleBinExists(resources)
}
// If recycle bin enabled and not in recycle bin, move in recycle bin
if (eachNodeRecyclable(database, nodes)) {
deleteDatabaseNodes(nodes)
}
// else open the dialog to confirm deletion
else {
DeleteNodesDialogFragment.getInstance(recycleBin)
.show(supportFragmentManager, "deleteNodesDialogFragment")
mNodesViewModel.deleteNodes(nodes)
}
}
}
private fun deleteDatabaseNodes(nodes: List<Node>) {
mDatabaseTaskProvider?.startDatabaseDeleteNodes(nodes, mAutoSaveEnable)
}
fun createGroup(parent: Group,
groupInfo: GroupInfo?) {
// Build the group
mDatabase?.createGroup()?.let { newGroup ->
groupInfo?.let { info ->
newGroup.setGroupInfo(info)
}
// Not really needed here because added in runnable but safe
newGroup.parent = parent
mDatabaseTaskProvider?.startDatabaseCreateGroup(newGroup, parent, mAutoSaveEnable)
}
}
fun updateGroup(oldGroup: Group,
groupInfo: GroupInfo) {
// If group updated save it in the database
val updateGroup = Group(oldGroup).let { updateGroup ->
updateGroup.apply {
// WARNING remove parent and children to keep memory
removeParent()
removeChildren()
this.setGroupInfo(groupInfo)
}
}
mDatabaseTaskProvider?.startDatabaseUpdateGroup(oldGroup, updateGroup, mAutoSaveEnable)
}
fun restoreEntryHistory(mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int) {
mDatabaseTaskProvider
?.startDatabaseRestoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
}
fun deleteEntryHistory(mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int) {
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_EXIT_LOCK) {
mExitLock = true
lockAndExit()
}
}
private fun checkRegister() {
// If in ave or registration mode, don't allow read only
if ((mSpecialMode == SpecialMode.SAVE
|| mSpecialMode == SpecialMode.REGISTRATION)
&& mDatabaseReadOnly) {
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
EntrySelectionHelper.removeModesFromIntent(intent)
finish()
}
}
override fun onResume() {
super.onResume()
// To refresh when back to normal workflow from selection workflow
mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this)
// Invalidate timeout by touch
mDatabase?.let { database ->
viewToInvalidateTimeout()
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database.loaded)
}
invalidateOptionsMenu()
LOCKING_ACTIVITY_UI_VISIBLE = true
}
protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
mDatabase?.loaded == true,
action)
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
super.onSaveInstanceState(outState)
}
override fun onPause() {
LOCKING_ACTIVITY_UI_VISIBLE = false
super.onPause()
if (mTimeoutEnable) {
// If the time is out during our navigation in activity -> close the Activity
TimeoutHelper.checkTimeAndLockIfTimeout(this)
}
}
override fun onDestroy() {
unregisterLockReceiver(mLockReceiver)
super.onDestroy()
}
protected fun lockAndExit() {
sendBroadcast(Intent(LOCK_ACTION))
}
fun resetAppTimeout() {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
mDatabase?.loaded ?: false)
}
override fun onBackPressed() {
if (mTimeoutEnable) {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
mDatabase?.loaded == true) {
super.onBackPressed()
}
} else {
super.onBackPressed()
}
}
companion object {
const val TAG = "LockingActivity"
const val RESULT_EXIT_LOCK = 1450
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
private var LOCKING_ACTIVITY_UI_VISIBLE = false
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
}
}
/**
* To reset the app timeout when a view is focused or changed
*/
@SuppressLint("ClickableViewAccessibility")
fun View.resetAppTimeoutWhenViewTouchedOrFocused(context: Context, databaseLoaded: Boolean?) {
// Log.d(DatabaseLockActivity.TAG, "View prepared to reset app timeout")
setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
databaseLoaded ?: false)
}
}
false
}
setOnFocusChangeListener { _, _ ->
// Log.d(DatabaseLockActivity.TAG, "View focused, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
databaseLoaded ?: false)
}
if (this is ViewGroup) {
for (i in 0..childCount) {
getChildAt(i)?.resetAppTimeoutWhenViewTouchedOrFocused(context, databaseLoaded)
}
}
}

View File

@@ -1,11 +0,0 @@
package com.kunzisoft.keepass.activities.legacy
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.tasks.ActionRunnable
interface DatabaseRetrieval {
fun onDatabaseRetrieved(database: Database?)
fun onDatabaseActionFinished(database: Database,
actionTask: String,
result: ActionRunnable.Result)
}

View File

@@ -0,0 +1,215 @@
/*
* Copyright 2019 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/>.
*
*/
package com.kunzisoft.keepass.activities.lock
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.*
abstract class LockingActivity : SpecialModeActivity() {
protected var mTimeoutEnable: Boolean = true
private var mLockReceiver: LockReceiver? = null
private var mExitLock: Boolean = false
// Force readOnly if Entry Selection mode
protected var mReadOnly: Boolean
get() {
return mReadOnlyToSave
}
set(value) {
mReadOnlyToSave = value
}
private var mReadOnlyToSave: Boolean = false
protected var mAutoSaveEnable: Boolean = true
var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
private set
override fun onCreate(savedInstanceState: Bundle?) {
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
super.onCreate(savedInstanceState)
if (savedInstanceState != null
&& savedInstanceState.containsKey(TIMEOUT_ENABLE_KEY)) {
mTimeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY)
} else {
if (intent != null)
mTimeoutEnable = intent.getBooleanExtra(TIMEOUT_ENABLE_KEY, TIMEOUT_ENABLE_KEY_DEFAULT)
}
if (mTimeoutEnable) {
mLockReceiver = LockReceiver {
closeDatabase()
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
// Add onActivityForResult response
setResult(RESULT_EXIT_LOCK)
closeOptionsMenu()
finish()
}
registerLockReceiver(mLockReceiver)
}
mExitLock = false
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_EXIT_LOCK) {
mExitLock = true
if (Database.getInstance().loaded) {
lockAndExit()
}
}
}
override fun onResume() {
super.onResume()
// If in ave or registration mode, don't allow read only
if ((mSpecialMode == SpecialMode.SAVE
|| mSpecialMode == SpecialMode.REGISTRATION)
&& mReadOnly) {
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
EntrySelectionHelper.removeModesFromIntent(intent)
finish()
}
mProgressDatabaseTaskProvider?.registerProgressTask()
// To refresh when back to normal workflow from selection workflow
mReadOnlyToSave = ReadOnlyHelper.retrieveReadOnlyFromIntent(intent)
mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this)
invalidateOptionsMenu()
if (mTimeoutEnable) {
// End activity if database not loaded
if (!Database.getInstance().loaded) {
finish()
return
}
// After the first creation
// or If simply swipe with another application
// If the time is out -> close the Activity
TimeoutHelper.checkTimeAndLockIfTimeout(this)
// If onCreate already record time
if (!mExitLock)
TimeoutHelper.recordTime(this)
}
LOCKING_ACTIVITY_UI_VISIBLE = true
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
super.onSaveInstanceState(outState)
}
override fun onPause() {
LOCKING_ACTIVITY_UI_VISIBLE = false
mProgressDatabaseTaskProvider?.unregisterProgressTask()
super.onPause()
if (mTimeoutEnable) {
// If the time is out during our navigation in activity -> close the Activity
TimeoutHelper.checkTimeAndLockIfTimeout(this)
}
}
override fun onDestroy() {
unregisterLockReceiver(mLockReceiver)
super.onDestroy()
}
protected fun lockAndExit() {
sendBroadcast(Intent(LOCK_ACTION))
}
override fun onBackPressed() {
if (mTimeoutEnable) {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
super.onBackPressed()
}
} else {
super.onBackPressed()
}
}
companion object {
const val TAG = "LockingActivity"
const val RESULT_EXIT_LOCK = 1450
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
private var LOCKING_ACTIVITY_UI_VISIBLE = false
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
}
}
/**
* To reset the app timeout when a view is focused or changed
*/
@SuppressLint("ClickableViewAccessibility")
fun View.resetAppTimeoutWhenViewFocusedOrChanged(context: Context) {
setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
//Log.d(LockingActivity.TAG, "View touched, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context)
}
}
false
}
setOnFocusChangeListener { _, _ ->
//Log.d(LockingActivity.TAG, "View focused, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context)
}
if (this is ViewGroup) {
for (i in 0..childCount) {
getChildAt(i)?.resetAppTimeoutWhenViewFocusedOrChanged(context)
}
}
}

View File

@@ -1,4 +1,4 @@
package com.kunzisoft.keepass.activities.legacy
package com.kunzisoft.keepass.activities.selection
import android.os.Bundle
import android.view.View
@@ -7,14 +7,15 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.helpers.TypeMode
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.SpecialModeView
/**
* Activity to manage database special mode (ie: selection mode)
* Activity to manage special mode (ie: selection mode)
*/
abstract class DatabaseModeActivity : DatabaseActivity() {
abstract class SpecialModeActivity : StylishActivity() {
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
private var mTypeMode: TypeMode = TypeMode.DEFAULT

View File

@@ -37,12 +37,12 @@ object Stylish {
* Initialize the class with a theme preference
* @param context Context to retrieve the theme preference
*/
fun load(context: Context) {
fun init(context: Context) {
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
themeString = PreferencesUtil.getStyle(context)
}
fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
private fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
val systemNightMode = when (PreferencesUtil.getStyleBrightness(context)) {
context.getString(R.string.list_style_brightness_light) -> false
context.getString(R.string.list_style_brightness_night) -> true
@@ -84,16 +84,12 @@ object Stylish {
}
}
fun defaultStyle(context: Context): String {
return context.getString(R.string.list_style_name_light)
}
/**
* Assign the style to the class attribute
* @param styleString Style id String
*/
fun assignStyle(context: Context, styleString: String) {
PreferencesUtil.setStyle(context, styleString)
themeString = retrieveEquivalentSystemStyle(context, styleString)
}
/**

View File

@@ -22,13 +22,10 @@ package com.kunzisoft.keepass.activities.stylish
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.WindowManager
import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_APPEARANCE_PREFERENCE_CHANGED
import android.util.Log
import android.view.WindowManager
/**
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
@@ -38,7 +35,6 @@ abstract class StylishActivity : AppCompatActivity() {
@StyleRes
private var themeId: Int = 0
private var customStyle = true
/* (non-Javadoc) Workaround for HTC Linkify issues
* @see android.app.Activity#startActivity(android.content.Intent)
@@ -56,30 +52,10 @@ abstract class StylishActivity : AppCompatActivity() {
}
}
open fun applyCustomStyle(): Boolean {
return true
}
open fun finishActivityIfReloadRequested(): Boolean {
return false
}
open fun reloadActivity() {
if (!finishActivityIfReloadRequested()) {
startActivity(intent)
}
finish()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
customStyle = applyCustomStyle()
if (customStyle) {
this.themeId = Stylish.getThemeId(this)
setTheme(themeId)
}
this.themeId = Stylish.getThemeId(this)
setTheme(themeId)
// Several gingerbread devices have problems with FLAG_SECURE
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
@@ -87,17 +63,9 @@ abstract class StylishActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
if ((customStyle && Stylish.getThemeId(this) != this.themeId)
|| DATABASE_APPEARANCE_PREFERENCE_CHANGED) {
DATABASE_APPEARANCE_PREFERENCE_CHANGED = false
if (Stylish.getThemeId(this) != this.themeId) {
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
recreateActivity()
this.recreate()
}
}
private fun recreateActivity() {
// To prevent KitKat bugs
Handler(Looper.getMainLooper()).post { recreate() }
}
}

View File

@@ -26,13 +26,13 @@ import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.database.element.Entry
class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHistoryAdapter.EntryHistoryViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
var entryHistoryList: MutableList<EntryInfo> = ArrayList()
var onItemClickListener: ((item: EntryInfo, position: Int)->Unit)? = null
var entryHistoryList: MutableList<Entry> = ArrayList()
var onItemClickListener: ((item: Entry, position: Int)->Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryHistoryViewHolder {
return EntryHistoryViewHolder(inflater.inflate(R.layout.item_list_entry_history, parent, false))
@@ -44,6 +44,7 @@ class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHist
holder.lastModifiedView.text = entryHistory.lastModificationTime.getDateTimeString(context.resources)
holder.titleView.text = entryHistory.title
holder.usernameView.text = entryHistory.username
holder.urlView.text = entryHistory.url
holder.itemView.setOnClickListener {
onItemClickListener?.invoke(entryHistory, position)
@@ -63,5 +64,6 @@ class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHist
var lastModifiedView: TextView = itemView.findViewById(R.id.entry_history_last_modified)
var titleView: TextView = itemView.findViewById(R.id.entry_history_title)
var usernameView: TextView = itemView.findViewById(R.id.entry_history_username)
var urlView: TextView = itemView.findViewById(R.id.entry_history_url)
}
}

View File

@@ -27,7 +27,7 @@ import android.view.ViewGroup
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.model.Field
import java.util.ArrayList

View File

@@ -30,8 +30,6 @@ import android.view.ViewGroup
import android.widget.*
import androidx.annotation.ColorInt
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.view.collapse
@@ -46,43 +44,11 @@ class FileDatabaseHistoryAdapter(context: Context)
private var fileSelectClearListener: ((DatabaseFile)->Boolean)? = null
private var saveAliasListener: ((DatabaseFile)->Unit)? = null
private var mDefaultDatabase: DatabaseFile? = null
private var mExpandedDatabaseFile: SuperDatabaseFile? = null
private var mPreviousExpandedDatabaseFile: SuperDatabaseFile? = null
private val listDatabaseFiles = ArrayList<DatabaseFile>()
private val mListPosition = mutableListOf<SuperDatabaseFile>()
private val mSortedListDatabaseFiles = SortedList(SuperDatabaseFile::class.java,
object: SortedListAdapterCallback<SuperDatabaseFile>(this) {
override fun compare(item1: SuperDatabaseFile, item2: SuperDatabaseFile): Int {
val indexItem1 = mListPosition.indexOf(item1)
val indexItem2 = mListPosition.indexOf(item2)
return if (indexItem1 == -1 && indexItem2 == -1)
-1
else if (indexItem1 < indexItem2)
-1
else if (indexItem1 > indexItem2)
1
else
0
}
override fun areContentsTheSame(oldItem: SuperDatabaseFile, newItem: SuperDatabaseFile): Boolean {
val oldDatabaseFile = oldItem.databaseFile
val newDatabaseFile = newItem.databaseFile
return oldDatabaseFile.databaseUri == newDatabaseFile.databaseUri
&& oldDatabaseFile.databaseDecodedPath == newDatabaseFile.databaseDecodedPath
&& oldDatabaseFile.databaseAlias == newDatabaseFile.databaseAlias
&& oldDatabaseFile.databaseFileExists == newDatabaseFile.databaseFileExists
&& oldDatabaseFile.databaseLastModified == newDatabaseFile.databaseLastModified
&& oldDatabaseFile.databaseSize == newDatabaseFile.databaseSize
&& oldItem.default == newItem.default
}
override fun areItemsTheSame(item1: SuperDatabaseFile, item2: SuperDatabaseFile): Boolean {
return item1.databaseFile == item2.databaseFile
}
}
)
private var mDefaultDatabaseFile: DatabaseFile? = null
private var mExpandedDatabaseFile: DatabaseFile? = null
private var mPreviousExpandedDatabaseFile: DatabaseFile? = null
@ColorInt
private val defaultColor: Int
@@ -105,8 +71,7 @@ class FileDatabaseHistoryAdapter(context: Context)
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
// Get info from position
val superDatabaseFile = mSortedListDatabaseFiles[position]
val databaseFile = superDatabaseFile.databaseFile
val databaseFile = listDatabaseFiles[position]
// Click item to open file
holder.fileContainer.setOnClickListener {
@@ -115,7 +80,7 @@ class FileDatabaseHistoryAdapter(context: Context)
// Default database
holder.defaultFileButton.apply {
this.isChecked = superDatabaseFile.default
this.isChecked = mDefaultDatabaseFile == databaseFile
setOnClickListener {
defaultDatabaseListener?.invoke(if (isChecked) databaseFile else null)
}
@@ -150,7 +115,7 @@ class FileDatabaseHistoryAdapter(context: Context)
}
// Click on information
val isExpanded = superDatabaseFile == mExpandedDatabaseFile
val isExpanded = databaseFile == mExpandedDatabaseFile
// Hides or shows info
holder.fileExpandContainer.apply {
if (isExpanded) {
@@ -186,16 +151,16 @@ class FileDatabaseHistoryAdapter(context: Context)
}
if (isExpanded) {
mPreviousExpandedDatabaseFile = superDatabaseFile
mPreviousExpandedDatabaseFile = databaseFile
}
holder.fileInformationButton.apply {
animate().rotation(if (isExpanded) 180F else 0F).start()
setOnClickListener {
mExpandedDatabaseFile = if (isExpanded) null else superDatabaseFile
mExpandedDatabaseFile = if (isExpanded) null else databaseFile
// Notify change
val previousExpandedPosition = mListPosition.indexOf(mPreviousExpandedDatabaseFile)
val previousExpandedPosition = listDatabaseFiles.indexOf(mPreviousExpandedDatabaseFile)
notifyItemChanged(previousExpandedPosition)
val expandedPosition = mListPosition.indexOf(mExpandedDatabaseFile)
val expandedPosition = listDatabaseFiles.indexOf(mExpandedDatabaseFile)
notifyItemChanged(expandedPosition)
}
}
@@ -207,67 +172,50 @@ class FileDatabaseHistoryAdapter(context: Context)
}
override fun getItemCount(): Int {
return mSortedListDatabaseFiles.size()
return listDatabaseFiles.size
}
fun clearDatabaseFileHistoryList() {
mListPosition.clear()
mSortedListDatabaseFiles.clear()
listDatabaseFiles.clear()
}
fun addDatabaseFileHistory(fileDatabaseHistoryToAdd: DatabaseFile) {
val superToAdd = SuperDatabaseFile(fileDatabaseHistoryToAdd)
mListPosition.add(0, superToAdd)
mSortedListDatabaseFiles.add(superToAdd)
listDatabaseFiles.add(0, fileDatabaseHistoryToAdd)
notifyItemInserted(0)
}
fun updateDatabaseFileHistory(fileDatabaseHistoryToUpdate: DatabaseFile) {
val superToUpdate = SuperDatabaseFile(fileDatabaseHistoryToUpdate)
val index = mListPosition.indexOf(superToUpdate)
if (mListPosition.remove(superToUpdate)) {
mListPosition.add(index, superToUpdate)
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToUpdate)
if (listDatabaseFiles.remove(fileDatabaseHistoryToUpdate)) {
listDatabaseFiles.add(index, fileDatabaseHistoryToUpdate)
notifyItemChanged(index)
}
mSortedListDatabaseFiles.updateItemAt(index, superToUpdate)
}
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: DatabaseFile) {
val superToDelete = SuperDatabaseFile(fileDatabaseHistoryToDelete)
val index = mListPosition.indexOf(superToDelete)
mListPosition.remove(superToDelete)
mSortedListDatabaseFiles.removeItemAt(index)
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToDelete)
if (listDatabaseFiles.remove(fileDatabaseHistoryToDelete)) {
notifyItemRemoved(index)
}
}
fun replaceAllDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<DatabaseFile>) {
val superMapToReplace = listFileDatabaseHistoryToAdd.map {
SuperDatabaseFile(it)
if (listDatabaseFiles.isEmpty()) {
listFileDatabaseHistoryToAdd.forEach {
listDatabaseFiles.add(it)
notifyItemInserted(listDatabaseFiles.size)
}
} else {
listDatabaseFiles.clear()
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
notifyDataSetChanged()
}
mListPosition.clear()
mListPosition.addAll(superMapToReplace)
mSortedListDatabaseFiles.replaceAll(superMapToReplace)
}
fun setDefaultDatabase(databaseUri: Uri?) {
// Remove default from last item
val oldDefaultDatabasePosition = mListPosition.indexOfFirst {
it.default
}
if (oldDefaultDatabasePosition >= 0) {
val oldDefaultDatabase = mListPosition[oldDefaultDatabasePosition].apply {
default = false
}
mSortedListDatabaseFiles.updateItemAt(oldDefaultDatabasePosition, oldDefaultDatabase)
}
// Add default to new item
val newDefaultDatabaseFilePosition = mListPosition.indexOfFirst {
it.databaseFile.databaseUri == databaseUri
}
if (newDefaultDatabaseFilePosition >= 0) {
val newDefaultDatabase = mListPosition[newDefaultDatabaseFilePosition].apply {
default = true
}
mDefaultDatabase = newDefaultDatabase.databaseFile
mSortedListDatabaseFiles.updateItemAt(newDefaultDatabaseFilePosition, newDefaultDatabase)
}
val defaultDatabaseFile = listDatabaseFiles.firstOrNull { it.databaseUri == databaseUri }
mDefaultDatabaseFile = defaultDatabaseFile
notifyDataSetChanged()
}
fun setOnDefaultDatabaseListener(listener: ((DatabaseFile?) -> Unit)?) {
@@ -286,30 +234,6 @@ class FileDatabaseHistoryAdapter(context: Context)
this.saveAliasListener = listener
}
private inner class SuperDatabaseFile(
var databaseFile: DatabaseFile,
var default: Boolean = false
) {
init {
if (mDefaultDatabase == databaseFile)
this.default = true
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SuperDatabaseFile) return false
if (databaseFile != other.databaseFile) return false
return true
}
override fun hashCode(): Int {
return databaseFile.hashCode()
}
}
class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info)

View File

@@ -5,7 +5,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
@@ -96,12 +95,6 @@ class IconPickerAdapter<I: IconImageDraw>(val context: Context, private val tint
override fun onBindViewHolder(holder: CustomIconViewHolder, position: Int) {
val icon = iconList[position]
iconDrawableFactory?.assignDatabaseIcon(holder.iconImageView, icon, tintIcon)
icon.getIconImageToDraw().custom.name.let { iconName ->
holder.iconTextView.apply {
text = iconName
visibility = if (iconName.isNotEmpty()) View.VISIBLE else View.GONE
}
}
holder.iconContainerView.isSelected = icon.selected
holder.itemView.setOnClickListener {
iconPickerListener?.onIconClickListener(icon)
@@ -124,6 +117,5 @@ class IconPickerAdapter<I: IconImageDraw>(val context: Context, private val tint
inner class CustomIconViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var iconContainerView: ViewGroup = itemView.findViewById(R.id.icon_container)
var iconImageView: ImageView = itemView.findViewById(R.id.icon_image)
var iconTextView: TextView = itemView.findViewById(R.id.icon_name)
}
}

View File

@@ -26,7 +26,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
@@ -41,8 +40,6 @@ 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.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.setTextSize
import com.kunzisoft.keepass.view.strikeOut
@@ -52,8 +49,7 @@ import java.util.*
* Create node list adapter with contextMenu or not
* @param context Context to use
*/
class NodeAdapter (private val context: Context,
private val database: Database)
class NodeAdapter (private val context: Context)
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
@@ -71,13 +67,13 @@ class NodeAdapter (private val context: Context,
private var mShowUserNames: Boolean = true
private var mShowNumberEntries: Boolean = true
private var mShowOTP: Boolean = false
private var mShowUUID: Boolean = false
private var mEntryFilters = arrayOf<Group.ChildFilter>()
private var mActionNodesList = LinkedList<Node>()
private var mNodeClickCallback: NodeClickCallback? = null
private val mDatabase: Database
@ColorInt
private val mContentSelectionColor: Int
@ColorInt
@@ -100,6 +96,9 @@ class NodeAdapter (private val context: Context,
this.mNodeSortedListCallback = NodeSortedListCallback()
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback)
// Database
this.mDatabase = Database.getInstance()
// Color of content selection
this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white)
// Retrieve the color to tint the icon
@@ -112,7 +111,7 @@ class NodeAdapter (private val context: Context,
taTextColor.recycle()
}
private fun assignPreferences() {
fun assignPreferences() {
this.mPrefSizeMultiplier = PreferencesUtil.getListTextSize(context)
notifyChangeSort(
@@ -126,8 +125,6 @@ class NodeAdapter (private val context: Context,
this.mShowUserNames = PreferencesUtil.showUsernamesListEntries(context)
this.mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
this.mShowOTP = PreferencesUtil.showOTPToken(context)
this.mShowUUID = PreferencesUtil.showUUID(context)
this.mEntryFilters = Group.ChildFilter.getDefaults(context)
@@ -149,21 +146,9 @@ class NodeAdapter (private val context: Context,
}
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
var typeContentTheSame = true
if (oldItem is Entry && newItem is Entry) {
typeContentTheSame = oldItem.getVisualTitle() == newItem.getVisualTitle()
&& oldItem.username == newItem.username
&& oldItem.getOtpElement() == newItem.getOtpElement()
&& oldItem.containsAttachment() == newItem.containsAttachment()
} else if (oldItem is Group && newItem is Group) {
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
}
return typeContentTheSame
&& oldItem.nodeId == newItem.nodeId
&& oldItem.type == newItem.type
return oldItem.type == newItem.type
&& oldItem.title == newItem.title
&& oldItem.icon == newItem.icon
&& oldItem.isCurrentlyExpires == newItem.isCurrentlyExpires
}
override fun areItemsTheSame(item1: Node, item2: Node): Boolean {
@@ -256,10 +241,6 @@ class NodeAdapter (private val context: Context,
mNodeSortedList.endBatchedUpdates()
}
fun indexOf(node: Node): Int {
return mNodeSortedList.indexOf(node)
}
fun notifyNodeChanged(node: Node) {
notifyItemChanged(mNodeSortedList.indexOf(node))
}
@@ -285,7 +266,7 @@ class NodeAdapter (private val context: Context,
*/
fun notifyChangeSort(sortNodeEnum: SortNodeEnum,
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
this.mNodeComparator = sortNodeEnum.getNodeComparator(database, sortNodeParameters)
this.mNodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters)
}
override fun getItemViewType(position: Int): Int {
@@ -322,7 +303,7 @@ class NodeAdapter (private val context: Context,
}
holder.imageIdentifier?.setColorFilter(iconColor)
holder.icon.apply {
database.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
mDatabase.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
// Relative size of the icon
layoutParams?.apply {
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
@@ -342,16 +323,11 @@ class NodeAdapter (private val context: Context,
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
}
// Specific elements for entry
if (subNode.type == Type.ENTRY) {
val entry = subNode as Entry
database.startManageEntry(entry)
mDatabase.startManageEntry(entry)
holder.text.text = entry.getVisualTitle()
holder.subText.apply {
@@ -363,29 +339,10 @@ class NodeAdapter (private val context: Context,
}
}
val otpElement = entry.getOtpElement()
holder.otpContainer?.removeCallbacks(holder.otpRunnable)
if (otpElement != null
&& mShowOTP
&& otpElement.token.isNotEmpty()) {
// Execute runnable to show progress
holder.otpRunnable.action = {
populateOtpView(holder, otpElement)
}
if (otpElement.type == OtpType.TOTP) {
holder.otpRunnable.postDelayed()
}
populateOtpView(holder, otpElement)
holder.otpContainer?.visibility = View.VISIBLE
} else {
holder.otpContainer?.visibility = View.GONE
}
holder.attachmentIcon?.visibility =
if (entry.containsAttachment()) View.VISIBLE else View.GONE
database.stopManageEntry(entry)
mDatabase.stopManageEntry(entry)
}
// Add number of entries in groups
@@ -393,7 +350,7 @@ class NodeAdapter (private val context: Context,
if (mShowNumberEntries) {
holder.numberChildren?.apply {
text = (subNode as Group)
.numberOfChildEntries
.getNumberOfChildEntries(mEntryFilters)
.toString()
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
visibility = View.VISIBLE
@@ -405,45 +362,13 @@ class NodeAdapter (private val context: Context,
// Assign click
holder.container.setOnClickListener {
mNodeClickCallback?.onNodeClick(database, subNode)
mNodeClickCallback?.onNodeClick(subNode)
}
holder.container.setOnLongClickListener {
mNodeClickCallback?.onNodeLongClick(database, subNode) ?: false
mNodeClickCallback?.onNodeLongClick(subNode) ?: false
}
}
private fun populateOtpView(holder: NodeViewHolder?, otpElement: OtpElement?) {
when (otpElement?.type) {
OtpType.HOTP -> {
holder?.otpProgress?.apply {
max = 100
progress = 100
}
}
OtpType.TOTP -> {
holder?.otpProgress?.apply {
max = otpElement.period
progress = otpElement.secondsRemaining
}
}
}
holder?.otpToken?.text = otpElement?.token
}
class OtpRunnable(val view: View?): Runnable {
var action: (() -> Unit)? = null
override fun run() {
action?.invoke()
postDelayed()
}
fun postDelayed() {
view?.postDelayed(this, 1000)
}
}
override fun getItemCount(): Int {
return mNodeSortedList.size()
}
@@ -459,8 +384,8 @@ class NodeAdapter (private val context: Context,
* Callback listener to redefine to do an action when a node is click
*/
interface NodeClickCallback {
fun onNodeClick(database: Database, node: Node)
fun onNodeLongClick(database: Database, node: Node): Boolean
fun onNodeClick(node: Node)
fun onNodeLongClick(node: Node): Boolean
}
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
@@ -469,11 +394,6 @@ class NodeAdapter (private val context: Context,
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 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)
var otpToken: TextView? = itemView.findViewById(R.id.node_otp_token)
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
}

View File

@@ -106,6 +106,7 @@ class SearchEntryCursorAdapter(private val context: Context,
private fun getEntryFrom(cursor: Cursor): Entry? {
return database.createEntry()?.apply {
database.startManageEntry(this)
entryKDB?.let { entryKDB ->
(cursor as EntryCursorKDB).populateEntry(entryKDB,
{ standardIconId ->
@@ -126,6 +127,7 @@ class SearchEntryCursorAdapter(private val context: Context,
}
)
}
database.stopManageEntry(this)
}
}
@@ -148,14 +150,12 @@ class SearchEntryCursorAdapter(private val context: Context,
if (searchGroup != null) {
// Search in hide entries but not meta-stream
for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) {
database.startManageEntry(entry)
entry.entryKDB?.let {
cursorKDB?.addEntry(it)
}
entry.entryKDBX?.let {
cursorKDBX?.addEntry(it)
}
database.stopManageEntry(entry)
}
}

View File

@@ -1,70 +0,0 @@
package com.kunzisoft.keepass.adapters
import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ImageView
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.icons.IconDrawableFactory
class TemplatesSelectorAdapter(private val context: Context,
private val iconDrawableFactory: IconDrawableFactory?,
private var templates: List<Template>): BaseAdapter() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var mIconColor = Color.BLACK
init {
val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
mIconColor = taIconColor.getColor(0, Color.BLACK)
taIconColor.recycle()
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val template: Template = getItem(position)
val holder: TemplateSelectorViewHolder
var templateView = convertView
if (templateView == null) {
holder = TemplateSelectorViewHolder()
templateView = inflater.inflate(R.layout.item_template, parent, false)
holder.icon = templateView?.findViewById(R.id.template_image)
holder.name = templateView?.findViewById(R.id.template_name)
templateView?.tag = holder
} else {
holder = templateView.tag as TemplateSelectorViewHolder
}
holder.icon?.let { icon ->
iconDrawableFactory?.assignDatabaseIcon(icon, template.icon, mIconColor)
}
holder.name?.text = TemplateField.getLocalizedName(context, template.title)
return templateView!!
}
override fun getCount(): Int {
return templates.size
}
override fun getItem(position: Int): Template {
return templates[position]
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
inner class TemplateSelectorViewHolder {
var icon: ImageView? = null
var name: TextView? = null
}
}

View File

@@ -21,13 +21,20 @@ package com.kunzisoft.keepass.app
import androidx.multidex.MultiDexApplication
import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.utils.UriUtil
class App : MultiDexApplication() {
override fun onCreate() {
super.onCreate()
Stylish.load(this)
Stylish.init(this)
PRNGFixes.apply()
}
override fun onTerminate() {
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
super.onTerminate()
}
}

View File

@@ -19,10 +19,12 @@
*/
package com.kunzisoft.keepass.app.database
import android.content.*
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.net.Uri
import android.os.IBinder
import android.util.Log
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.SingletonHolderParameter
@@ -39,95 +41,62 @@ class CipherDatabaseAction(context: Context) {
// Temp DAO to easily remove content if object no longer in memory
private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
private val mIntentAdvancedUnlockService = Intent(applicationContext,
AdvancedUnlockNotificationService::class.java)
private var mBinder: AdvancedUnlockNotificationService.AdvancedUnlockBinder? = null
private var mServiceConnection: ServiceConnection? = null
private var mDatabaseListeners = LinkedList<CipherDatabaseListener>()
private var mAdvancedUnlockBroadcastReceiver = AdvancedUnlockNotificationService.AdvancedUnlockReceiver {
deleteAll()
removeAllDataAndDetach()
}
private var mDatabaseListeners = LinkedList<DatabaseListener>()
fun reloadPreferences() {
useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
}
@Synchronized
private fun serviceActionTask(startService: Boolean = false, performedAction: () -> Unit) {
// Check if a service is currently running else call action without info
if (startService && mServiceConnection == null) {
attachService(performedAction)
} else {
performedAction.invoke()
}
}
@Synchronized
private fun attachService(performedAction: () -> Unit) {
applicationContext.registerReceiver(mAdvancedUnlockBroadcastReceiver, IntentFilter().apply {
addAction(AdvancedUnlockNotificationService.REMOVE_ADVANCED_UNLOCK_KEY_ACTION)
})
mServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as AdvancedUnlockNotificationService.AdvancedUnlockBinder)
performedAction.invoke()
}
override fun onServiceDisconnected(name: ComponentName?) {
onClear()
}
}
try {
AdvancedUnlockNotificationService.bindService(applicationContext,
mServiceConnection!!,
Context.BIND_AUTO_CREATE)
} catch (e: Exception) {
Log.e(TAG, "Unable to start cipher action", e)
// Check if a service is currently running else do nothing
if (mBinder != null) {
performedAction.invoke()
} else if (mServiceConnection == null) {
mServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as AdvancedUnlockNotificationService.AdvancedUnlockBinder)
performedAction.invoke()
}
override fun onServiceDisconnected(name: ComponentName?) {
mBinder = null
mServiceConnection = null
mDatabaseListeners.forEach {
it.onDatabaseCleared()
}
}
}
applicationContext.bindService(mIntentAdvancedUnlockService,
mServiceConnection!!,
Context.BIND_ABOVE_CLIENT)
if (mBinder == null) {
applicationContext.startService(mIntentAdvancedUnlockService)
}
}
}
@Synchronized
private fun detachService() {
try {
applicationContext.unregisterReceiver(mAdvancedUnlockBroadcastReceiver)
} catch (e: Exception) {}
mServiceConnection?.let {
AdvancedUnlockNotificationService.unbindService(applicationContext, it)
}
fun registerDatabaseListener(listener: DatabaseListener) {
mDatabaseListeners.add(listener)
}
private fun removeAllDataAndDetach() {
detachService()
onClear()
fun unregisterDatabaseListener(listener: DatabaseListener) {
mDatabaseListeners.remove(listener)
}
fun registerDatabaseListener(listenerCipher: CipherDatabaseListener) {
mDatabaseListeners.add(listenerCipher)
}
fun unregisterDatabaseListener(listenerCipher: CipherDatabaseListener) {
mDatabaseListeners.remove(listenerCipher)
}
private fun onClear() {
mBinder = null
mServiceConnection = null
mDatabaseListeners.forEach {
it.onCipherDatabaseCleared()
}
}
interface CipherDatabaseListener {
fun onCipherDatabaseCleared()
interface DatabaseListener {
fun onDatabaseCleared()
}
fun getCipherDatabase(databaseUri: Uri,
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
if (useTempDao) {
serviceActionTask {
attachService {
cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri))
}
} else {
@@ -152,8 +121,7 @@ class CipherDatabaseAction(context: Context) {
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
cipherDatabaseResultListener: (() -> Unit)? = null) {
if (useTempDao) {
// The only case to create service (not needed to get an info)
serviceActionTask(true) {
attachService {
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
cipherDatabaseResultListener?.invoke()
}
@@ -178,7 +146,7 @@ class CipherDatabaseAction(context: Context) {
fun deleteByDatabaseUri(databaseUri: Uri,
cipherDatabaseResultListener: (() -> Unit)? = null) {
if (useTempDao) {
serviceActionTask {
attachService {
mBinder?.deleteByDatabaseUri(databaseUri)
cipherDatabaseResultListener?.invoke()
}
@@ -195,22 +163,15 @@ class CipherDatabaseAction(context: Context) {
}
fun deleteAll() {
if (useTempDao) {
serviceActionTask {
mBinder?.deleteAll()
}
attachService {
mBinder?.deleteAll()
}
// To erase the residues
IOActionTask(
{
cipherDatabaseDao.deleteAll()
}
).execute()
// Unbind
removeAllDataAndDetach()
}
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) {
private val TAG = CipherDatabaseAction::class.java.name
}
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction)
}

View File

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

View File

@@ -48,11 +48,8 @@ import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.model.EntryInfo
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
@RequiresApi(api = Build.VERSION_CODES.O)
@@ -88,14 +85,13 @@ object AutofillHelper {
}
private fun newRemoteViews(context: Context,
database: Database,
remoteViewsText: String,
remoteViewsIcon: IconImage? = null): RemoteViews {
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
if (remoteViewsIcon != null) {
try {
database.iconDrawableFactory.getBitmapFromIcon(context,
Database.getInstance().iconDrawableFactory.getBitmapFromIcon(context,
remoteViewsIcon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
presentation.setImageViewBitmap(R.id.autofill_entry_icon, bitmap)
}
@@ -107,94 +103,19 @@ object AutofillHelper {
}
private fun buildDataset(context: Context,
database: Database,
entryInfo: EntryInfo,
struct: StructureParser.Result,
inlinePresentation: InlinePresentation?): Dataset? {
entryInfo: EntryInfo,
struct: StructureParser.Result,
inlinePresentation: InlinePresentation?): Dataset? {
val title = makeEntryTitle(entryInfo)
val views = newRemoteViews(context, database, title, entryInfo.icon)
val views = newRemoteViews(context, title, entryInfo.icon)
val builder = Dataset.Builder(views)
builder.setId(entryInfo.id.toString())
builder.setId(entryInfo.id)
struct.usernameId?.let { usernameId ->
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
}
struct.passwordId?.let { passwordId ->
builder.setValue(passwordId, AutofillValue.forText(entryInfo.password))
}
if (entryInfo.expires) {
val year = entryInfo.expiryTime.getYearInt()
val month = entryInfo.expiryTime.getMonthInt()
val day = entryInfo.expiryTime.getDay()
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"))
} else {
builder.setValue(it, AutofillValue.forDate(entryInfo.expiryTime.date.time))
}
}
struct.creditCardExpirationYearId?.let {
var autofillValue: AutofillValue? = null
struct.creditCardExpirationYearOptions?.let { options ->
var yearIndex = options.indexOf(year.toString().substring(0, 2))
if (yearIndex == -1) {
yearIndex = options.indexOf(year.toString())
}
if (yearIndex != -1) {
autofillValue = AutofillValue.forList(yearIndex)
builder.setValue(it, autofillValue)
}
}
if (autofillValue == null) {
builder.setValue(it, AutofillValue.forText(year.toString()))
}
}
struct.creditCardExpirationMonthId?.let {
if (struct.isWebView) {
builder.setValue(it, AutofillValue.forText(month.toString()))
} else {
if (struct.creditCardExpirationMonthOptions != null) {
// index starts at 0
builder.setValue(it, AutofillValue.forList(month - 1))
} else {
builder.setValue(it, AutofillValue.forText(month.toString()))
}
}
}
struct.creditCardExpirationDayId?.let {
if (struct.isWebView) {
builder.setValue(it, AutofillValue.forText(day.toString()))
} else {
if (struct.creditCardExpirationDayOptions != null) {
builder.setValue(it, AutofillValue.forList(day - 1))
} else {
builder.setValue(it, AutofillValue.forText(day.toString()))
}
}
}
}
for (field in entryInfo.customFields) {
if (field.name == TemplateField.LABEL_HOLDER) {
struct.creditCardHolderId?.let { ccNameId ->
builder.setValue(ccNameId, AutofillValue.forText(field.protectedValue.stringValue))
}
}
if (field.name == TemplateField.LABEL_NUMBER) {
struct.creditCardNumberId?.let { ccnId ->
builder.setValue(ccnId, AutofillValue.forText(field.protectedValue.stringValue))
}
}
if (field.name == TemplateField.LABEL_CVV) {
struct.cardVerificationValueId?.let { cvvId ->
builder.setValue(cvvId, AutofillValue.forText(field.protectedValue.stringValue))
}
}
struct.passwordId?.let { password ->
builder.setValue(password, AutofillValue.forText(entryInfo.password))
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
@@ -205,8 +126,8 @@ object AutofillHelper {
return try {
builder.build()
} catch (e: Exception) {
// at least one value must be set
} catch (e: IllegalArgumentException) {
// if not value be set
null
}
}
@@ -214,11 +135,9 @@ object AutofillHelper {
/**
* Method to assign a drawable to a new icon from a database icon
*/
private fun buildIconFromEntry(context: Context,
database: Database,
entryInfo: EntryInfo): Icon? {
private fun buildIconFromEntry(context: Context, entryInfo: EntryInfo): Icon? {
try {
database.iconDrawableFactory.getBitmapFromIcon(context,
Database.getInstance().iconDrawableFactory.getBitmapFromIcon(context,
entryInfo.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
return Icon.createWithBitmap(bitmap)
}
@@ -231,14 +150,13 @@ object AutofillHelper {
@RequiresApi(Build.VERSION_CODES.R)
@SuppressLint("RestrictedApi")
private fun buildInlinePresentationForEntry(context: Context,
database: Database,
inlineSuggestionsRequest: InlineSuggestionsRequest,
positionItem: Int,
entryInfo: EntryInfo): InlinePresentation? {
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
if (positionItem <= maxSuggestion - 1
if (positionItem <= maxSuggestion-1
&& inlinePresentationSpecs.size > positionItem) {
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
@@ -260,7 +178,7 @@ object AutofillHelper {
setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
setTintBlendMode(BlendMode.DST)
})
buildIconFromEntry(context, database, entryInfo)?.let { icon ->
buildIconFromEntry(context, entryInfo)?.let { icon ->
setEndIcon(icon.apply {
setTintBlendMode(BlendMode.DST)
})
@@ -271,10 +189,9 @@ object AutofillHelper {
}
fun buildResponse(context: Context,
database: Database,
entriesInfo: List<EntryInfo>,
parseResult: StructureParser.Result,
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse? {
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse {
val responseBuilder = FillResponse.Builder()
// Add Header
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@@ -291,44 +208,31 @@ 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)
buildInlinePresentationForEntry(context, inlineSuggestionsRequest, index, entryInfo)
} else {
null
}
}
val dataSet = buildDataset(context, database, entryInfo, parseResult, inlinePresentation)
dataSet?.let {
responseBuilder.addDataset(it)
}
}
return try {
responseBuilder.build()
} catch (e: Exception) {
null
responseBuilder.addDataset(buildDataset(context, entryInfo, parseResult, inlinePresentation))
}
return responseBuilder.build()
}
/**
* Build the Autofill response for one entry
*/
fun buildResponseAndSetResult(activity: Activity,
database: Database,
entryInfo: EntryInfo) {
buildResponseAndSetResult(activity, database, ArrayList<EntryInfo>().apply { add(entryInfo) })
fun buildResponseAndSetResult(activity: Activity, entryInfo: EntryInfo) {
buildResponseAndSetResult(activity, ArrayList<EntryInfo>().apply { add(entryInfo) })
}
/**
* Build the Autofill response for many entry
*/
fun buildResponseAndSetResult(activity: Activity,
database: Database,
entriesInfo: List<EntryInfo>) {
fun buildResponseAndSetResult(activity: Activity, entriesInfo: List<EntryInfo>) {
if (entriesInfo.isEmpty()) {
activity.setResult(Activity.RESULT_CANCELED)
} else {
@@ -341,9 +245,9 @@ object AutofillHelper {
if (inlineSuggestionsRequest != null) {
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
}
buildResponse(activity, database, entriesInfo, result, inlineSuggestionsRequest)
buildResponse(activity, entriesInfo, result, inlineSuggestionsRequest)
} else {
buildResponse(activity, database, entriesInfo, result, null)
buildResponse(activity, entriesInfo, result, null)
}
val mReplyIntent = Intent()
Log.d(activity.javaClass.name, "Successed Autofill auth.")

View File

@@ -36,47 +36,29 @@ import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.model.CreditCard
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import org.joda.time.DateTime
import java.util.concurrent.atomic.AtomicBoolean
@RequiresApi(api = Build.VERSION_CODES.O)
class KeeAutofillService : AutofillService() {
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
private var mDatabase: Database? = null
private var applicationIdBlocklist: Set<String>? = null
private var webDomainBlocklist: Set<String>? = null
private var askToSaveData: Boolean = false
private var autofillInlineSuggestionsEnabled: Boolean = false
var applicationIdBlocklist: Set<String>? = null
var webDomainBlocklist: Set<String>? = null
var askToSaveData: Boolean = false
var autofillInlineSuggestionsEnabled: Boolean = false
private var mLock = AtomicBoolean()
override fun onCreate() {
super.onCreate()
mDatabaseTaskProvider = DatabaseTaskProvider(this)
mDatabaseTaskProvider?.registerProgressTask()
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
this.mDatabase = database
}
getPreferences()
}
override fun onDestroy() {
mDatabaseTaskProvider?.unregisterProgressTask()
super.onDestroy()
}
private fun getPreferences() {
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
@@ -113,8 +95,7 @@ class KeeAutofillService : AutofillService() {
} else {
null
}
launchSelection(mDatabase,
searchInfo,
launchSelection(searchInfo,
parseResult,
inlineSuggestionsRequest,
callback)
@@ -124,17 +105,16 @@ class KeeAutofillService : AutofillService() {
}
}
private fun launchSelection(database: Database?,
searchInfo: SearchInfo,
private fun launchSelection(searchInfo: SearchInfo,
parseResult: StructureParser.Result,
inlineSuggestionsRequest: InlineSuggestionsRequest?,
callback: FillCallback) {
SearchHelper.checkAutoSearchInfo(this,
database,
Database.getInstance(),
searchInfo,
{ openedDatabase, items ->
{ items ->
callback.onSuccess(
AutofillHelper.buildResponse(this, openedDatabase,
AutofillHelper.buildResponse(this,
items, parseResult, inlineSuggestionsRequest)
)
},
@@ -175,40 +155,23 @@ class KeeAutofillService : AutofillService() {
RemoteViews(packageName, R.layout.item_autofill_unlock)
}
// Tell the autofill framework the interest to save credentials
// Tell to service the interest to save credentials
if (askToSaveData) {
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
val requiredIds = ArrayList<AutofillId>()
val optionalIds = ArrayList<AutofillId>()
val info = ArrayList<AutofillId>()
// Only if at least a password
parseResult.passwordId?.let { passwordInfo ->
parseResult.usernameId?.let { usernameInfo ->
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
requiredIds.add(usernameInfo)
info.add(usernameInfo)
}
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
requiredIds.add(passwordInfo)
info.add(passwordInfo)
}
// or a credit card form
if (requiredIds.isEmpty()) {
parseResult.creditCardNumberId?.let { numberId ->
types = types or SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD
requiredIds.add(numberId)
Log.d(TAG, "Asking to save credit card number")
}
parseResult.creditCardExpirationDateId?.let { id -> optionalIds.add(id) }
parseResult.creditCardExpirationYearId?.let { id -> optionalIds.add(id) }
parseResult.creditCardExpirationMonthId?.let { id -> optionalIds.add(id) }
parseResult.creditCardHolderId?.let { id -> optionalIds.add(id) }
parseResult.cardVerificationValueId?.let { id -> optionalIds.add(id) }
}
if (requiredIds.isNotEmpty()) {
val builder = SaveInfo.Builder(types, requiredIds.toTypedArray())
if (optionalIds.isNotEmpty()) {
builder.setOptionalIds(optionalIds.toTypedArray())
}
responseBuilder.setSaveInfo(builder.build())
if (info.isNotEmpty()) {
responseBuilder.setSaveInfo(
SaveInfo.Builder(types, info.toTypedArray()).build()
)
}
}
@@ -260,35 +223,14 @@ class KeeAutofillService : AutofillService() {
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
Log.d(TAG, "autofill onSaveRequest password")
// Build expiration from date or from year and month
var expiration: DateTime? = parseResult.creditCardExpirationValue
if (parseResult.creditCardExpirationValue == null
&& parseResult.creditCardExpirationYearValue != 0
&& parseResult.creditCardExpirationMonthValue != 0) {
expiration = DateTime()
.withYear(parseResult.creditCardExpirationYearValue)
.withMonthOfYear(parseResult.creditCardExpirationMonthValue)
if (parseResult.creditCardExpirationDayValue != 0) {
expiration = expiration.withDayOfMonth(parseResult.creditCardExpirationDayValue)
}
}
// Show UI to save data
val registerInfo = RegisterInfo(
SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
},
val registerInfo = RegisterInfo(SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
},
parseResult.usernameValue?.textValue?.toString(),
parseResult.passwordValue?.textValue?.toString(),
CreditCard(
parseResult.creditCardHolder,
parseResult.creditCardNumber,
expiration,
parseResult.cardVerificationValue
))
parseResult.passwordValue?.textValue?.toString())
// TODO Callback in each activity #765
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,

View File

@@ -21,14 +21,12 @@ package com.kunzisoft.keepass.autofill
import android.app.assist.AssistStructure
import android.os.Build
import android.text.InputType
import androidx.annotation.RequiresApi
import android.util.Log
import android.view.View
import android.view.autofill.AutofillId
import android.view.autofill.AutofillValue
import androidx.annotation.RequiresApi
import org.joda.time.DateTime
import java.util.*
import kotlin.collections.ArrayList
/**
@@ -37,8 +35,10 @@ import kotlin.collections.ArrayList
@RequiresApi(api = Build.VERSION_CODES.O)
class StructureParser(private val structure: AssistStructure) {
private var result: Result? = null
private var usernameNeeded = true
private var usernameIdCandidate: AutofillId? = null
private var usernameCandidate: AutofillId? = null
private var usernameValueCandidate: AutofillValue? = null
fun parse(saveValue: Boolean = false): Result? {
@@ -46,7 +46,7 @@ class StructureParser(private val structure: AssistStructure) {
result = Result()
result?.apply {
allowSaveValues = saveValue
usernameIdCandidate = null
usernameCandidate = null
usernameValueCandidate = null
mainLoop@ for (i in 0 until structure.windowNodeCount) {
val windowNode = structure.getWindowNodeAt(i)
@@ -57,29 +57,26 @@ class StructureParser(private val structure: AssistStructure) {
break@mainLoop
}
// If not explicit username field found, add the field just before password field.
if (usernameId == null && passwordId != null && usernameIdCandidate != null) {
usernameId = usernameIdCandidate
if (usernameId == null && passwordId != null && usernameCandidate != null) {
usernameId = usernameCandidate
if (allowSaveValues) {
usernameValue = usernameValueCandidate
}
}
}
return if (result?.passwordId != null || result?.creditCardNumberId != null)
result
else
null
// Return the result only if password field is retrieved
return if ((!usernameNeeded || result?.usernameId != null)
&& result?.passwordId != null)
result
else
null
} catch (e: Exception) {
return null
}
}
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
// remember this
if (node.className == "android.webkit.WebView") {
result?.isWebView = true
}
// Get the domain of a web app
node.webDomain?.let { webDomain ->
if (webDomain.isNotEmpty()) {
@@ -100,7 +97,8 @@ class StructureParser(private val structure: AssistStructure) {
var returnValue = false
// Only parse visible nodes
if (node.visibility == View.VISIBLE) {
if (node.autofillId != null) {
if (node.autofillId != null
&& node.autofillType == View.AUTOFILL_TYPE_TEXT) {
// Parse methods
val hints = node.autofillHints
if (hints != null && hints.isNotEmpty()) {
@@ -132,7 +130,7 @@ class StructureParser(private val structure: AssistStructure) {
it.contains(View.AUTOFILL_HINT_USERNAME, true)
|| it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|| it.contains("email", true)
|| it.contains(View.AUTOFILL_HINT_PHONE, true) -> {
|| it.contains(View.AUTOFILL_HINT_PHONE, true)-> {
result?.usernameId = autofillId
result?.usernameValue = node.autofillValue
Log.d(TAG, "Autofill username hint")
@@ -141,123 +139,14 @@ class StructureParser(private val structure: AssistStructure) {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password hint")
// Username not needed in this case
usernameNeeded = false
return true
}
it.contains("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) -> {
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) -> {
Log.d(TAG, "Autofill credit card expiration date hint")
result?.creditCardExpirationDateId = autofillId
node.autofillValue?.let { value ->
if (value.isText && value.textValue.length == 7) {
value.textValue.let { date ->
try {
result?.creditCardExpirationValue = DateTime()
.withYear(date.substring(2, 4).toInt())
.withMonthOfYear(date.substring(5, 7).toInt())
} catch(e: Exception) {
Log.e(TAG, "Unable to retrieve expiration", e)
}
}
}
}
}
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE, true) -> {
Log.d(TAG, "Autofill credit card expiration date hint")
result?.creditCardExpirationDateId = autofillId
node.autofillValue?.let { value ->
if (value.isDate) {
result?.creditCardExpirationValue = DateTime(value.dateValue)
}
}
}
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR, true)
|| it.contains("cc-exp-year", true) -> {
Log.d(TAG, "Autofill credit card expiration year hint")
result?.creditCardExpirationYearId = autofillId
if (node.autofillOptions != null) {
result?.creditCardExpirationYearOptions = node.autofillOptions
}
node.autofillValue?.let { value ->
var year = 0
try {
if (value.isText) {
year = value.textValue.toString().toInt()
}
if (value.isList) {
year = node.autofillOptions?.get(value.listValue).toString().toInt()
}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve expiration year", e)
}
result?.creditCardExpirationYearValue = year % 100
}
}
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH, true)
|| it.contains("cc-exp-month", true) -> {
Log.d(TAG, "Autofill credit card expiration month hint")
result?.creditCardExpirationMonthId = autofillId
if (node.autofillOptions != null) {
result?.creditCardExpirationMonthOptions = node.autofillOptions
}
node.autofillValue?.let { value ->
var month = 0
try {
if (value.isText) {
month = value.textValue.toString().toInt()
}
if (value.isList) {
// assume list starts with January (index 0)
month = value.listValue + 1
}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve expiration month", e)
}
result?.creditCardExpirationMonthValue = month
}
}
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY, true)
|| it.contains("cc-exp-day", true) -> {
Log.d(TAG, "Autofill credit card expiration day hint")
result?.creditCardExpirationDayId = autofillId
if (node.autofillOptions != null) {
result?.creditCardExpirationDayOptions = node.autofillOptions
}
node.autofillValue?.let { value ->
var day = 0
try {
if (value.isText) {
day = value.textValue.toString().toInt()
}
if (value.isList) {
day = node.autofillOptions?.get(value.listValue).toString().toInt()
}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve expiration day", e)
}
result?.creditCardExpirationDayValue = day
}
}
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE, true)
|| it.contains("cc-csc", true) -> {
Log.d(TAG, "Autofill card security code hint")
result?.cardVerificationValueId = autofillId
result?.cardVerificationValue = node.autofillValue?.textValue?.toString()
}
// Ignore autocomplete="off"
// https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
it.equals("off", true) ||
it.equals("on", true) -> {
it.equals("on", true) -> {
Log.d(TAG, "Autofill web hint")
return parseNodeByHtmlAttributes(node)
}
@@ -282,7 +171,7 @@ class StructureParser(private val structure: AssistStructure) {
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
"text" -> {
usernameIdCandidate = autofillId
usernameCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
@@ -330,30 +219,18 @@ class StructureParser(private val structure: AssistStructure) {
InputType.TYPE_TEXT_VARIATION_NORMAL,
InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
usernameIdCandidate = autofillId
usernameCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
}
inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
// Some forms used visible password as username
if (usernameIdCandidate == null && usernameValueCandidate == null) {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill visible password android text type (as username): ${showHexInputType(inputType)}")
} else if (result?.passwordId == null && result?.passwordValue == null) {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill visible password android text type (as password): ${showHexInputType(inputType)}")
usernameNeeded = false
}
}
inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_PASSWORD,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}")
usernameNeeded = false
return true
}
inputIsVariationType(inputType,
@@ -375,15 +252,16 @@ class StructureParser(private val structure: AssistStructure) {
when {
inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
usernameIdCandidate = autofillId
usernameCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate android number type: ${showHexInputType(inputType)}")
Log.d(TAG, "Autofill usernale candidate android number type: ${showHexInputType(inputType)}")
}
inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}")
usernameNeeded = false
return true
}
else -> {
@@ -397,7 +275,6 @@ class StructureParser(private val structure: AssistStructure) {
@RequiresApi(api = Build.VERSION_CODES.O)
class Result {
var isWebView: Boolean = false
var applicationId: String? = null
var webDomain: String? = null
@@ -412,12 +289,6 @@ class StructureParser(private val structure: AssistStructure) {
field = value
}
// if the user selects the credit card expiration date from a list of options
// all options are stored here
var creditCardExpirationYearOptions: Array<CharSequence>? = null
var creditCardExpirationMonthOptions: Array<CharSequence>? = null
var creditCardExpirationDayOptions: Array<CharSequence>? = null
var usernameId: AutofillId? = null
set(value) {
if (field == null)
@@ -430,48 +301,6 @@ class StructureParser(private val structure: AssistStructure) {
field = value
}
var creditCardHolderId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardNumberId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardExpirationDateId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardExpirationYearId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardExpirationMonthId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardExpirationDayId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var cardVerificationValueId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
fun allAutofillIds(): Array<AutofillId> {
val all = ArrayList<AutofillId>()
usernameId?.let {
@@ -480,15 +309,6 @@ class StructureParser(private val structure: AssistStructure) {
passwordId?.let {
all.add(it)
}
creditCardHolderId?.let {
all.add(it)
}
creditCardNumberId?.let {
all.add(it)
}
cardVerificationValueId?.let {
all.add(it)
}
return all.toTypedArray()
}
@@ -506,52 +326,6 @@ class StructureParser(private val structure: AssistStructure) {
if (allowSaveValues && field == null)
field = value
}
var creditCardHolder: String? = null
set(value) {
if (allowSaveValues)
field = value
}
var creditCardNumber: String? = null
set(value) {
if (allowSaveValues)
field = value
}
// format MMYY
var creditCardExpirationValue: DateTime? = null
set(value) {
if (allowSaveValues)
field = value
}
// for year of CC expiration date: YY
var creditCardExpirationYearValue = 0
set(value) {
if (allowSaveValues)
field = value
}
// for month of CC expiration date: MM
var creditCardExpirationMonthValue = 0
set(value) {
if (allowSaveValues)
field = value
}
var creditCardExpirationDayValue = 0
set(value) {
if (allowSaveValues)
field = value
}
// the security code for the credit card (also called CVV)
var cardVerificationValue: String? = null
set(value) {
if (allowSaveValues)
field = value
}
}
companion object {

View File

@@ -30,17 +30,15 @@ import android.view.*
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.lifecycle.lifecycleScope
import com.getkeepsafe.taptargetview.TapTargetView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.database.exception.IODatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedUnlockCallback {
@@ -70,7 +68,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
private lateinit var cipherDatabaseAction : CipherDatabaseAction
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
private var cipherDatabaseListener: CipherDatabaseAction.DatabaseListener? = null
// Only to fix multiple fingerprint menu #332
private var mAllowAdvancedUnlockMenu = false
@@ -127,10 +125,8 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
override fun onResume() {
super.onResume()
context?.let {
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(it)
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(it)
}
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(requireContext())
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(requireContext())
keepConnection = false
}
@@ -180,37 +176,35 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
* Check unlock availability and change the current mode depending of device's state
*/
fun checkUnlockAvailability() {
context?.let { context ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
allowOpenBiometricPrompt = true
if (PreferencesUtil.isBiometricUnlockEnable(context)) {
mAdvancedUnlockInfoView?.setIconResource(R.drawable.fingerprint)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
allowOpenBiometricPrompt = true
if (PreferencesUtil.isBiometricUnlockEnable(requireContext())) {
mAdvancedUnlockInfoView?.setIconResource(R.drawable.fingerprint)
// biometric not supported (by API level or hardware) so keep option hidden
// or manually disable
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(context)
if (!PreferencesUtil.isAdvancedUnlockEnable(context)
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
toggleMode(Mode.BIOMETRIC_UNAVAILABLE)
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) {
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
} else {
// biometric is available but not configured, show icon but in disabled state with some information
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
} else {
selectMode()
}
}
} else if (PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
mAdvancedUnlockInfoView?.setIconResource(R.drawable.bolt)
if (AdvancedUnlockManager.isDeviceSecure(context)) {
selectMode()
} else {
// biometric not supported (by API level or hardware) so keep option hidden
// or manually disable
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(requireContext())
if (!PreferencesUtil.isAdvancedUnlockEnable(requireContext())
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
toggleMode(Mode.BIOMETRIC_UNAVAILABLE)
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) {
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
} else {
// biometric is available but not configured, show icon but in disabled state with some information
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
} else {
selectMode()
}
}
} else if (PreferencesUtil.isDeviceCredentialUnlockEnable(requireContext())) {
mAdvancedUnlockInfoView?.setIconResource(R.drawable.bolt)
if (AdvancedUnlockManager.isDeviceSecure(requireContext())) {
selectMode()
} else {
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
}
}
}
}
@@ -267,7 +261,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
private fun openBiometricSetting() {
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
context?.startActivity(Intent(Settings.ACTION_SETTINGS))
requireContext().startActivity(Intent(Settings.ACTION_SETTINGS))
}
}
@@ -302,17 +296,15 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
setAdvancedUnlockedTitleView(R.string.no_credentials_stored)
setAdvancedUnlockedMessageView("")
context?.let { context ->
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
context.getString(R.string.credential_before_click_advanced_unlock_button))
}
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
requireContext().getString(R.string.credential_before_click_advanced_unlock_button))
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
lifecycleScope.launch(Dispatchers.Main) {
activity?.runOnUiThread {
if (allowOpenBiometricPrompt) {
if (cryptoPrompt.isDeviceCredentialOperation)
keepConnection = true
@@ -410,10 +402,9 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
fun connect(databaseUri: Uri) {
showViews(true)
this.databaseFileUri = databaseUri
cipherDatabaseListener = object: CipherDatabaseAction.CipherDatabaseListener {
override fun onCipherDatabaseCleared() {
advancedUnlockManager?.closeBiometricPrompt()
checkUnlockAvailability()
cipherDatabaseListener = object: CipherDatabaseAction.DatabaseListener {
override fun onDatabaseCleared() {
deleteEncryptedDatabaseKey()
}
}
cipherDatabaseAction.apply {
@@ -444,16 +435,18 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
@RequiresApi(Build.VERSION_CODES.M)
fun deleteEncryptedDatabaseKey() {
allowOpenBiometricPrompt = false
mAdvancedUnlockInfoView?.setIconViewClickListener(false, null)
advancedUnlockManager?.closeBiometricPrompt()
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
checkUnlockAvailability()
}
} ?: checkUnlockAvailability()
}
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
lifecycleScope.launch(Dispatchers.Main) {
activity?.runOnUiThread {
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
setAdvancedUnlockedMessageView(errString.toString())
}
@@ -461,7 +454,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
@RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationFailed() {
lifecycleScope.launch(Dispatchers.Main) {
activity?.runOnUiThread {
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized)
}
@@ -469,7 +462,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
@RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationSucceeded() {
lifecycleScope.launch(Dispatchers.Main) {
activity?.runOnUiThread {
when (biometricMode) {
Mode.BIOMETRIC_UNAVAILABLE -> {
}
@@ -486,6 +479,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
mBuilderListener?.retrieveCredentialForEncryption()?.let { credential ->
advancedUnlockManager?.encryptData(credential)
}
AdvancedUnlockNotificationService.startServiceForTimeout(requireContext())
}
Mode.EXTRACT_CREDENTIAL -> {
// retrieve the encrypted value from preferences
@@ -527,7 +521,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
}
private fun showViews(show: Boolean) {
lifecycleScope.launch(Dispatchers.Main) {
activity?.runOnUiThread {
mAdvancedUnlockInfoView?.visibility = if (show)
View.VISIBLE
else {
@@ -538,20 +532,20 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
@RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedTitleView(textId: Int) {
lifecycleScope.launch(Dispatchers.Main) {
activity?.runOnUiThread {
mAdvancedUnlockInfoView?.setTitle(textId)
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedMessageView(textId: Int) {
lifecycleScope.launch(Dispatchers.Main) {
activity?.runOnUiThread {
mAdvancedUnlockInfoView?.setMessage(textId)
}
}
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
lifecycleScope.launch(Dispatchers.Main) {
activity?.runOnUiThread {
mAdvancedUnlockInfoView?.message = text
}
}

View File

@@ -26,13 +26,13 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil
class CreateDatabaseRunnable(context: Context,
private val mDatabase: Database,
databaseUri: Uri,
private val databaseName: String,
private val rootName: String,
private val templateGroupName: String?,
mainCredential: MainCredential,
private val createDatabaseResult: ((Result) -> Unit)?)
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
@@ -41,10 +41,10 @@ class CreateDatabaseRunnable(context: Context,
try {
// Create new database record
mDatabase.apply {
createData(mDatabaseUri, databaseName, rootName, templateGroupName)
createData(mDatabaseUri, databaseName, rootName)
}
} catch (e: Exception) {
mDatabase.clearAndClose(context)
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
setError(e)
}

View File

@@ -25,8 +25,8 @@ import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil
@@ -47,7 +47,7 @@ class LoadDatabaseRunnable(private val context: Context,
override fun onStartRun() {
// Clear before we load
mDatabase.clearAndClose(context)
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
}
override fun onActionRun() {
@@ -85,7 +85,7 @@ class LoadDatabaseRunnable(private val context: Context,
// Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context)
} else {
mDatabase.clearAndClose(context)
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
}
}

View File

@@ -19,23 +19,18 @@
*/
package com.kunzisoft.keepass.database.action
import android.app.Service
import android.content.*
import android.content.Context.*
import android.content.Context.BIND_ABOVE_CLIENT
import android.content.Context.BIND_NOT_FOREGROUND
import android.net.Uri
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import android.widget.Toast
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
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.database.CompressionAlgorithm
@@ -72,35 +67,21 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import kotlinx.coroutines.launch
import java.util.*
import kotlin.collections.ArrayList
/**
* Utility class to connect an activity or a service to the DatabaseTaskNotificationService,
* Useful to retrieve a database instance and sending tasks commands
*/
class DatabaseTaskProvider {
class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
private var activity: FragmentActivity? = null
private var service: Service? = null
private var context: Context
var onDatabaseRetrieved: ((database: Database?) -> Unit)? = null
var onActionFinish: ((database: Database,
actionTask: String,
var onActionFinish: ((actionTask: String,
result: ActionRunnable.Result) -> Unit)? = null
private var intentDatabaseTask: Intent
private var intentDatabaseTask = Intent(activity.applicationContext, DatabaseTaskNotificationService::class.java)
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
@@ -110,31 +91,17 @@ class DatabaseTaskProvider {
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
constructor(activity: FragmentActivity) {
this.activity = activity
this.context = activity
this.intentDatabaseTask = Intent(activity.applicationContext,
DatabaseTaskNotificationService::class.java)
}
constructor(service: Service) {
this.service = service
this.context = service
this.intentDatabaseTask = Intent(service.applicationContext,
DatabaseTaskNotificationService::class.java)
}
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
override fun onStartAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) {
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
startDialog(titleId, messageId, warningId)
}
override fun onUpdateAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) {
override fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) {
updateDialog(titleId, messageId, warningId)
}
override fun onStopAction(database: Database, actionTask: String, result: ActionRunnable.Result) {
onActionFinish?.invoke(database, actionTask, result)
override fun onStopAction(actionTask: String, result: ActionRunnable.Result) {
onActionFinish?.invoke(actionTask, result)
// Remove the progress task
stopDialog()
}
@@ -149,56 +116,31 @@ class DatabaseTaskProvider {
private var databaseInfoListener = object: DatabaseTaskNotificationService.DatabaseInfoListener {
override fun onDatabaseInfoChanged(previousDatabaseInfo: SnapFileDatabaseInfo,
newDatabaseInfo: SnapFileDatabaseInfo) {
activity?.let { activity ->
activity.lifecycleScope.launch {
if (databaseChangedDialogFragment == null) {
databaseChangedDialogFragment = activity.supportFragmentManager
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
databaseChangedDialogFragment?.actionDatabaseListener =
mActionDatabaseListener
}
if (progressTaskDialogFragment == null) {
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
previousDatabaseInfo,
newDatabaseInfo
)
databaseChangedDialogFragment?.actionDatabaseListener =
mActionDatabaseListener
databaseChangedDialogFragment?.show(
activity.supportFragmentManager,
DATABASE_CHANGED_DIALOG_TAG
)
}
}
if (databaseChangedDialogFragment == null) {
databaseChangedDialogFragment = activity.supportFragmentManager
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
databaseChangedDialogFragment?.actionDatabaseListener = mActionDatabaseListener
}
if (progressTaskDialogFragment == null) {
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(previousDatabaseInfo, newDatabaseInfo)
databaseChangedDialogFragment?.actionDatabaseListener = mActionDatabaseListener
databaseChangedDialogFragment?.show(activity.supportFragmentManager, DATABASE_CHANGED_DIALOG_TAG)
}
}
}
private var databaseListener = object: DatabaseTaskNotificationService.DatabaseListener {
override fun onDatabaseRetrieved(database: Database?) {
onDatabaseRetrieved?.invoke(database)
}
}
private fun startDialog(titleId: Int? = null,
messageId: Int? = null,
warningId: Int? = null) {
activity?.let { activity ->
activity.lifecycleScope.launch {
if (progressTaskDialogFragment == null) {
progressTaskDialogFragment = activity.supportFragmentManager
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
}
if (progressTaskDialogFragment == null) {
progressTaskDialogFragment = ProgressTaskDialogFragment()
progressTaskDialogFragment?.show(
activity.supportFragmentManager,
PROGRESS_TASK_DIALOG_TAG
)
}
updateDialog(titleId, messageId, warningId)
}
if (progressTaskDialogFragment == null) {
progressTaskDialogFragment = activity.supportFragmentManager
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
}
if (progressTaskDialogFragment == null) {
progressTaskDialogFragment = ProgressTaskDialogFragment()
progressTaskDialogFragment?.show(activity.supportFragmentManager, PROGRESS_TASK_DIALOG_TAG)
}
updateDialog(titleId, messageId, warningId)
}
private fun updateDialog(titleId: Int?, messageId: Int?, warningId: Int?) {
@@ -225,19 +167,16 @@ class DatabaseTaskProvider {
serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
addDatabaseListener(databaseListener)
addDatabaseFileInfoListener(databaseInfoListener)
addActionTaskListener(actionTaskListener)
getService().checkDatabase()
getService().checkDatabaseInfo()
addDatabaseFileInfoListener(databaseInfoListener)
getService().checkAction()
getService().checkDatabaseInfo()
}
}
override fun onServiceDisconnected(name: ComponentName?) {
mBinder?.removeActionTaskListener(actionTaskListener)
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
mBinder?.removeDatabaseListener(databaseListener)
mBinder?.removeActionTaskListener(actionTaskListener)
mBinder = null
}
}
@@ -247,7 +186,7 @@ class DatabaseTaskProvider {
private fun bindService() {
initServiceConnection()
serviceConnection?.let {
context.bindService(intentDatabaseTask, it, BIND_AUTO_CREATE or BIND_NOT_FOREGROUND or BIND_ABOVE_CLIENT)
activity.bindService(intentDatabaseTask, it, BIND_NOT_FOREGROUND or BIND_ABOVE_CLIENT)
}
}
@@ -256,7 +195,7 @@ class DatabaseTaskProvider {
*/
private fun unBindService() {
serviceConnection?.let {
context.unbindService(it)
activity.unbindService(it)
}
serviceConnection = null
}
@@ -284,7 +223,7 @@ class DatabaseTaskProvider {
}
}
}
context.registerReceiver(databaseTaskBroadcastReceiver,
activity.registerReceiver(databaseTaskBroadcastReceiver,
IntentFilter().apply {
addAction(DATABASE_START_TASK_ACTION)
addAction(DATABASE_STOP_TASK_ACTION)
@@ -298,30 +237,25 @@ class DatabaseTaskProvider {
fun unregisterProgressTask() {
stopDialog()
mBinder?.removeActionTaskListener(actionTaskListener)
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
mBinder?.removeDatabaseListener(databaseListener)
mBinder?.removeActionTaskListener(actionTaskListener)
mBinder = null
unBindService()
try {
context.unregisterReceiver(databaseTaskBroadcastReceiver)
activity.unregisterReceiver(databaseTaskBroadcastReceiver)
} catch (e: IllegalArgumentException) {
// If receiver not register, do nothing
}
}
private fun start(bundle: Bundle? = null, actionTask: String) {
try {
if (bundle != null)
intentDatabaseTask.putExtras(bundle)
intentDatabaseTask.action = actionTask
context.startService(intentDatabaseTask)
} catch (e: Exception) {
Log.e(TAG, "Unable to perform database action", e)
Toast.makeText(activity, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
}
activity.stopService(intentDatabaseTask)
if (bundle != null)
intentDatabaseTask.putExtras(bundle)
intentDatabaseTask.action = actionTask
activity.startService(intentDatabaseTask)
}
/*
@@ -430,7 +364,9 @@ class DatabaseTaskProvider {
nodesPaste.forEach { nodeVersioned ->
when (nodeVersioned.type) {
Type.GROUP -> {
groupsIdToCopy.add((nodeVersioned as Group).nodeId)
(nodeVersioned as Group).nodeId?.let { groupId ->
groupsIdToCopy.add(groupId)
}
}
Type.ENTRY -> {
entriesIdToCopy.add((nodeVersioned as Entry).nodeId)
@@ -473,22 +409,22 @@ class DatabaseTaskProvider {
-----------------
*/
fun startDatabaseRestoreEntryHistory(mainEntryId: NodeId<UUID>,
fun startDatabaseRestoreEntryHistory(mainEntry: Entry,
entryHistoryPosition: Int,
save: Boolean) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntry.nodeId)
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
}
fun startDatabaseDeleteEntryHistory(mainEntryId: NodeId<UUID>,
fun startDatabaseDeleteEntryHistory(mainEntry: Entry,
entryHistoryPosition: Int,
save: Boolean) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntry.nodeId)
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
@@ -563,28 +499,6 @@ class DatabaseTaskProvider {
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
}
fun startDatabaseSaveRecycleBin(oldRecycleBin: Group?,
newRecycleBin: Group?,
save: Boolean) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldRecycleBin)
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newRecycleBin)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
}
fun startDatabaseSaveTemplatesGroup(oldTemplatesGroup: Group?,
newTemplatesGroup: Group?,
save: Boolean) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldTemplatesGroup)
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newTemplatesGroup)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
}
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
newMaxHistoryItems: Int,
save: Boolean) {
@@ -677,8 +591,4 @@ class DatabaseTaskProvider {
}
, ACTION_DATABASE_SAVE)
}
companion object {
private val TAG = DatabaseTaskProvider::class.java.name
}
}

View File

@@ -21,8 +21,8 @@ package com.kunzisoft.keepass.database.action
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -62,7 +62,7 @@ class ReloadDatabaseRunnable(private val context: Context,
PreferencesUtil.saveCurrentTime(context)
} else {
tempCipherKey = null
mDatabase.clearAndClose(context)
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
}
}

View File

@@ -31,47 +31,41 @@ class DeleteNodesRunnable(context: Context,
afterActionNodesFinish: AfterActionNodesFinish)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
private var mOldParent: Group? = null
private var mParent: Group? = null
private var mCanRecycle: Boolean = false
private var mNodesToDeleteBackup = ArrayList<Node>()
override fun nodeAction() {
foreachNode@ for(nodeToDelete in mNodesToDelete) {
mOldParent = nodeToDelete.parent
mOldParent?.touch(modified = false, touchParents = true)
foreachNode@ for(currentNode in mNodesToDelete) {
mParent = currentNode.parent
mParent?.touch(modified = false, touchParents = true)
when (nodeToDelete.type) {
when (currentNode.type) {
Type.GROUP -> {
val groupToDelete = nodeToDelete as Group
// Create a copy to keep the old ref and remove it visually
mNodesToDeleteBackup.add(Group(groupToDelete))
mNodesToDeleteBackup.add(Group(currentNode as Group))
// Remove Node from parent
mCanRecycle = database.canRecycle(groupToDelete)
mCanRecycle = database.canRecycle(currentNode)
if (mCanRecycle) {
groupToDelete.touch(modified = false, touchParents = true)
database.recycle(groupToDelete, context.resources)
groupToDelete.setPreviousParentGroup(mOldParent)
database.recycle(currentNode, context.resources)
} else {
database.deleteGroup(groupToDelete)
database.deleteGroup(currentNode)
}
}
Type.ENTRY -> {
val entryToDelete = nodeToDelete as Entry
// Create a copy to keep the old ref and remove it visually
mNodesToDeleteBackup.add(Entry(entryToDelete))
mNodesToDeleteBackup.add(Entry(currentNode as Entry))
// Remove Node from parent
mCanRecycle = database.canRecycle(entryToDelete)
mCanRecycle = database.canRecycle(currentNode)
if (mCanRecycle) {
entryToDelete.touch(modified = false, touchParents = true)
database.recycle(entryToDelete, context.resources)
entryToDelete.setPreviousParentGroup(mOldParent)
database.recycle(currentNode, context.resources)
} else {
database.deleteEntry(entryToDelete)
database.deleteEntry(currentNode)
}
// Remove the oldest attachments
entryToDelete.getAttachments(database.attachmentPool).forEach {
currentNode.getAttachments(database.attachmentPool).forEach {
database.removeAttachmentIfNotUsed(it)
}
}
@@ -82,7 +76,7 @@ class DeleteNodesRunnable(context: Context,
override fun nodeFinish(): ActionNodesValues {
if (!result.isSuccess) {
if (mCanRecycle) {
mOldParent?.let {
mParent?.let {
mNodesToDeleteBackup.forEach { backupNode ->
when (backupNode.type) {
Type.GROUP -> {

View File

@@ -24,7 +24,7 @@ import android.util.Log
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.exception.MoveEntryDatabaseException
import com.kunzisoft.keepass.database.exception.EntryDatabaseException
import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
class MoveNodesRunnable constructor(
@@ -47,14 +47,11 @@ class MoveNodesRunnable constructor(
when (nodeToMove.type) {
Type.GROUP -> {
val groupToMove = nodeToMove as Group
// Move group if the parent change
if (mOldParent != mNewParent
// and if not in the current group
&& groupToMove != mNewParent
// Move group in new parent if not in the current group
if (groupToMove != mNewParent
&& !mNewParent.isContainedIn(groupToMove)) {
groupToMove.touch(modified = true, touchParents = true)
nodeToMove.touch(modified = true, touchParents = true)
database.moveGroupTo(groupToMove, mNewParent)
groupToMove.setPreviousParentGroup(mOldParent)
} else {
// Only finish thread
setError(MoveGroupDatabaseException())
@@ -67,12 +64,11 @@ class MoveNodesRunnable constructor(
if (mOldParent != mNewParent
// and root can contains entry
&& (mNewParent != database.rootGroup || database.rootCanContainsEntry())) {
entryToMove.touch(modified = true, touchParents = true)
nodeToMove.touch(modified = true, touchParents = true)
database.moveEntryTo(entryToMove, mNewParent)
entryToMove.setPreviousParentGroup(mOldParent)
} else {
// Only finish thread
setError(MoveEntryDatabaseException())
setError(EntryDatabaseException())
break@foreachNode
}
}

View File

@@ -34,52 +34,54 @@ class UpdateEntryRunnable constructor(
afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
// Keep backup of original values in case save fails
private var mBackupEntryHistory: Entry = Entry(mOldEntry)
override fun nodeAction() {
if (mOldEntry.nodeId == mNewEntry.nodeId) {
// WARNING : Re attribute parent removed in entry edit activity to save memory
mNewEntry.addParentFrom(mOldEntry)
// WARNING : Re attribute parent removed in entry edit activity to save memory
mNewEntry.addParentFrom(mOldEntry)
// Build oldest attachments
val oldEntryAttachments = mOldEntry.getAttachments(database.attachmentPool, true)
val newEntryAttachments = mNewEntry.getAttachments(database.attachmentPool, true)
val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments)
// Not use equals because only check name
newEntryAttachments.forEach { newAttachment ->
oldEntryAttachments.forEach { oldAttachment ->
if (oldAttachment.name == newAttachment.name
&& oldAttachment.binaryData == newAttachment.binaryData
)
attachmentsToRemove.remove(oldAttachment)
}
// Build oldest attachments
val oldEntryAttachments = mOldEntry.getAttachments(database.attachmentPool, true)
val newEntryAttachments = mNewEntry.getAttachments(database.attachmentPool, true)
val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments)
// Not use equals because only check name
newEntryAttachments.forEach { newAttachment ->
oldEntryAttachments.forEach { oldAttachment ->
if (oldAttachment.name == newAttachment.name
&& oldAttachment.binaryData == newAttachment.binaryData)
attachmentsToRemove.remove(oldAttachment)
}
}
// Update entry with new values
mNewEntry.touch(modified = true, touchParents = true)
// Update entry with new values
mOldEntry.updateWith(mNewEntry)
mNewEntry.touch(modified = true, touchParents = true)
// Create an entry history (an entry history don't have history)
mNewEntry.addEntryToHistory(Entry(mOldEntry, copyHistory = false))
database.removeOldestEntryHistory(mNewEntry, database.attachmentPool)
// Create an entry history (an entry history don't have history)
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false))
database.removeOldestEntryHistory(mOldEntry, database.attachmentPool)
// Only change data in index
database.updateEntry(mNewEntry)
// Only change data in index
database.updateEntry(mOldEntry)
// Remove oldest attachments
attachmentsToRemove.forEach {
database.removeAttachmentIfNotUsed(it)
}
// Remove oldest attachments
attachmentsToRemove.forEach {
database.removeAttachmentIfNotUsed(it)
}
}
override fun nodeFinish(): ActionNodesValues {
if (!result.isSuccess) {
mOldEntry.updateWith(mBackupEntryHistory)
// If we fail to save, back out changes to global structure
database.updateEntry(mOldEntry)
}
val oldNodesReturn = ArrayList<Node>()
oldNodesReturn.add(mOldEntry)
oldNodesReturn.add(mBackupEntryHistory)
val newNodesReturn = ArrayList<Node>()
newNodesReturn.add(mNewEntry)
newNodesReturn.add(mOldEntry)
return ActionNodesValues(oldNodesReturn, newNodesReturn)
}
}

View File

@@ -33,30 +33,33 @@ class UpdateGroupRunnable constructor(
afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
// Keep backup of original values in case save fails
private val mBackupGroup: Group = Group(mOldGroup)
override fun nodeAction() {
if (mOldGroup.nodeId == mNewGroup.nodeId) {
// WARNING : Re attribute parent and children removed in group activity to save memory
mNewGroup.addParentFrom(mOldGroup)
mNewGroup.addChildrenFrom(mOldGroup)
// WARNING : Re attribute parent and children removed in group activity to save memory
mNewGroup.addParentFrom(mOldGroup)
mNewGroup.addChildrenFrom(mOldGroup)
// Update group with new values
mNewGroup.touch(modified = true, touchParents = true)
// Update group with new values
mOldGroup.updateWith(mNewGroup)
mOldGroup.touch(modified = true, touchParents = true)
// Only change data in index
database.updateGroup(mNewGroup)
}
// Only change data in index
database.updateGroup(mOldGroup)
}
override fun nodeFinish(): ActionNodesValues {
if (!result.isSuccess) {
// If we fail to save, back out changes to global structure
mOldGroup.updateWith(mBackupGroup)
database.updateGroup(mOldGroup)
}
val oldNodesReturn = ArrayList<Node>()
oldNodesReturn.add(mOldGroup)
oldNodesReturn.add(mBackupGroup)
val newNodesReturn = ArrayList<Node>()
newNodesReturn.add(mNewGroup)
newNodesReturn.add(mOldGroup)
return ActionNodesValues(oldNodesReturn, newNodesReturn)
}
}

View File

@@ -36,9 +36,6 @@ abstract class CipherEngine {
return 16
}
// Used only with padding workaround
var forcePaddingCompatibility = false
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
abstract fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher

View File

@@ -30,7 +30,7 @@ class TwofishEngine : CipherEngine() {
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
return CipherFactory.getTwofish(opmode, key, IV, forcePaddingCompatibility)
return CipherFactory.getTwofish(opmode, key, IV)
}
override fun getEncryptionAlgorithm(): EncryptionAlgorithm {

View File

@@ -21,8 +21,8 @@ package com.kunzisoft.keepass.database.crypto.kdf
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.utils.UnsignedLong
import com.kunzisoft.encrypt.aes.AESTransformer
import com.kunzisoft.keepass.utils.bytes16ToUuid
import encrypt.Encrypt
import java.io.IOException
import java.security.SecureRandom
import java.util.*
@@ -58,7 +58,8 @@ class AesKdf : KdfEngine() {
val rounds = kdfParameters.getUInt64(PARAM_ROUNDS)?.toKotlinLong()
return AESTransformer.transformKey(seed, currentMasterKey, rounds) ?: ByteArray(0)
return Encrypt.transformAESKey(currentMasterKey, seed, rounds!!)
//return AESTransformer.transformKey(seed, currentMasterKey, rounds) ?: ByteArray(0)
}
override fun randomize(kdfParameters: KdfParameters) {

View File

@@ -21,9 +21,8 @@ package com.kunzisoft.keepass.database.crypto.kdf
import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.UnsignedLong
import com.kunzisoft.encrypt.argon2.Argon2Transformer
import com.kunzisoft.encrypt.argon2.Argon2Type
import com.kunzisoft.keepass.utils.bytes16ToUuid
import encrypt.Encrypt
import java.io.IOException
import java.security.SecureRandom
import java.util.*
@@ -63,7 +62,14 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
// val secretKey = kdfParameters.getByteArray(PARAM_SECRET_KEY)
// val assocData = kdfParameters.getByteArray(PARAM_ASSOC_DATA)
val argonType = if (type == Type.ARGON2_ID) Argon2Type.ARGON2_ID else Argon2Type.ARGON2_D
// With Go lib
return when(type) {
Type.ARGON2_D -> Encrypt.transformArgon2DKey(masterKey, salt, iterations, memory, parallelism.toShort(), 32)
else -> Encrypt.transformArgon2IDKey(masterKey, salt, iterations, memory, parallelism.toShort(), 32)
}
/*
val argonType = if (type == Type.ARGON2_ID) Argon2Type.ARGON2_ID else Argon2Type.ARGON2_ID
return Argon2Transformer.transformKey(
argonType,
@@ -73,6 +79,7 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
memory,
iterations,
version)
*/
}
override fun randomize(kdfParameters: KdfParameters) {

View File

@@ -45,8 +45,8 @@ class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
entry.expires
))
entry.doForEachDecodedCustomField { field ->
extraFieldCursor.addExtraField(entryId, field)
for (element in entry.customFields.entries) {
extraFieldCursor.addExtraField(entryId, element.key, element.value)
}
entryId++

View File

@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.database.cursor
import android.database.MatrixCursor
import android.provider.BaseColumns
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.security.ProtectedString
@@ -37,17 +36,13 @@ class ExtraFieldCursor : MatrixCursor(arrayOf(
private var fieldId: Long = 0
@Synchronized
fun addExtraField(entryId: Long, field: Field) {
addRow(arrayOf(fieldId,
entryId,
field.name,
if (field.protectedValue.isProtected) 1 else 0,
field.protectedValue.toString()))
fun addExtraField(entryId: Long, label: String, value: ProtectedString) {
addRow(arrayOf(fieldId, entryId, label, if (value.isProtected) 1 else 0, value.toString()))
fieldId++
}
fun populateExtraFieldInEntry(pwEntry: EntryKDBX) {
pwEntry.putField(getString(getColumnIndex(COLUMN_LABEL)),
pwEntry.putExtraField(getString(getColumnIndex(COLUMN_LABEL)),
ProtectedString(getInt(getColumnIndex(COLUMN_PROTECTION)) > 0,
getString(getColumnIndex(COLUMN_VALUE))))
}

View File

@@ -1,66 +0,0 @@
package com.kunzisoft.keepass.database.element
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.utils.ParcelableUtil
import java.util.*
class CustomData : Parcelable {
private val mCustomDataItems = HashMap<String, CustomDataItem>()
constructor()
constructor(toCopy: CustomData) {
mCustomDataItems.clear()
mCustomDataItems.putAll(toCopy.mCustomDataItems)
}
constructor(parcel: Parcel) {
ParcelableUtil.readStringParcelableMap(parcel, CustomDataItem::class.java)
}
fun get(key: String): CustomDataItem? {
return mCustomDataItems[key]
}
fun put(customDataItem: CustomDataItem) {
mCustomDataItems[customDataItem.key] = customDataItem
}
fun containsItemWithValue(value: String): Boolean {
return mCustomDataItems.any { mapEntry -> mapEntry.value.value.equals(value, true) }
}
fun containsItemWithLastModificationTime(): Boolean {
return mCustomDataItems.any { mapEntry -> mapEntry.value.lastModificationTime != null }
}
fun isNotEmpty(): Boolean {
return mCustomDataItems.isNotEmpty()
}
fun doForEachItems(action: (CustomDataItem) -> Unit) {
for ((_, value) in mCustomDataItems) {
action.invoke(value)
}
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
ParcelableUtil.writeStringParcelableMap(parcel, flags, mCustomDataItems)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<CustomData> {
override fun createFromParcel(parcel: Parcel): CustomData {
return CustomData(parcel)
}
override fun newArray(size: Int): Array<CustomData?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -1,43 +0,0 @@
package com.kunzisoft.keepass.database.element
import android.os.Parcel
import android.os.Parcelable
class CustomDataItem : Parcelable {
val key: String
var value: String
var lastModificationTime: DateInstant? = null
constructor(parcel: Parcel) {
key = parcel.readString() ?: ""
value = parcel.readString() ?: ""
lastModificationTime = parcel.readParcelable(DateInstant::class.java.classLoader)
}
constructor(key: String, value: String, lastModificationTime: DateInstant? = null) {
this.key = key
this.value = value
this.lastModificationTime = lastModificationTime
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(key)
parcel.writeString(value)
parcel.writeParcelable(lastModificationTime, flags)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<CustomDataItem> {
override fun createFromParcel(parcel: Parcel): CustomDataItem {
return CustomDataItem(parcel)
}
override fun newArray(size: Int): Array<CustomDataItem?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -20,12 +20,10 @@
package com.kunzisoft.keepass.database.element
import android.content.ContentResolver
import android.content.Context
import android.content.res.Resources
import android.net.Uri
import android.os.Build
import android.util.Log
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.utils.readBytes4ToUInt
import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
@@ -42,12 +40,10 @@ import com.kunzisoft.keepass.database.element.icon.IconsManager
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.database.element.template.TemplateEngine
import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
@@ -59,7 +55,6 @@ import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.SingletonHolder
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.readBytes4ToUInt
import java.io.*
import java.util.*
import kotlin.collections.ArrayList
@@ -110,6 +105,10 @@ class Database {
return mDatabaseKDB?.binaryCache ?: mDatabaseKDBX?.binaryCache ?: BinaryCache()
}
fun setCacheDirectory(cacheDirectory: File) {
binaryCache.cacheDirectory = cacheDirectory
}
private val iconsManager: IconsManager
get() {
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager(binaryCache)
@@ -147,57 +146,6 @@ class Database {
iconsManager.removeCustomIcon(binaryCache, customIcon.uuid)
}
fun getTemplates(templateCreation: Boolean): List<Template> {
return mDatabaseKDBX?.getTemplates(templateCreation) ?: listOf()
}
fun getTemplate(entry: Entry): Template? {
if (entryIsTemplate(entry))
return TemplateEngine.CREATION
entry.entryKDBX?.let { entryKDBX ->
return mDatabaseKDBX?.getTemplate(entryKDBX)
}
return null
}
fun entryIsTemplate(entry: Entry?): Boolean {
// Define is current entry is a template (in direct template group)
if (entry == null || templatesGroup == null)
return false
return templatesGroup == entry.parent
}
// Not the same as decode, here remove in all cases the template link in the entry data
fun removeTemplateConfiguration(entry: Entry): Entry {
entry.entryKDBX?.let {
mDatabaseKDBX?.decodeEntryWithTemplateConfiguration(it, false)?.let { decode ->
return Entry(decode)
}
}
return entry
}
// Remove the template link in the entry data if it's a basic entry
// or compress the template fields (as pseudo language) if it's a template entry
fun decodeEntryWithTemplateConfiguration(entry: Entry, lastEntryVersion: Entry? = null): Entry {
entry.entryKDBX?.let {
val lastEntry = lastEntryVersion ?: entry
mDatabaseKDBX?.decodeEntryWithTemplateConfiguration(it, entryIsTemplate(lastEntry))?.let { decode ->
return Entry(decode)
}
}
return entry
}
fun encodeEntryWithTemplateConfiguration(entry: Entry, template: Template): Entry {
entry.entryKDBX?.let {
mDatabaseKDBX?.encodeEntryWithTemplateConfiguration(it, entryIsTemplate(entry), template)?.let { encode ->
return Entry(encode)
}
}
return entry
}
val allowName: Boolean
get() = mDatabaseKDBX != null
@@ -278,7 +226,7 @@ class Database {
// Default compression not necessary if stored in header
mDatabaseKDBX?.let {
return it.compressionAlgorithm == CompressionAlgorithm.GZip
&& it.kdbxVersion.isBefore(FILE_VERSION_40)
&& it.kdbxVersion.isBefore(FILE_VERSION_32_4)
}
return false
}
@@ -371,15 +319,6 @@ class Database {
return null
}
/**
* Do not modify groups here, used for read only
*/
fun getAllGroupsWithoutRoot(): List<Group> {
return mDatabaseKDB?.getAllGroupsWithoutRoot()?.map { Group(it) }
?: mDatabaseKDBX?.getAllGroupsWithoutRoot()?.map { Group(it) }
?: listOf()
}
val manageHistory: Boolean
get() = mDatabaseKDBX != null
@@ -406,18 +345,12 @@ class Database {
val allowConfigurableRecycleBin: Boolean
get() = mDatabaseKDBX != null
val isRecycleBinEnabled: Boolean
var isRecycleBinEnabled: Boolean
// Backup is always enabled in KDB database
get() = mDatabaseKDB != null || mDatabaseKDBX?.isRecycleBinEnabled ?: false
fun enableRecycleBin(enable: Boolean, resources: Resources) {
mDatabaseKDBX?.isRecycleBinEnabled = enable
if (enable) {
ensureRecycleBinExists(resources)
} else {
mDatabaseKDBX?.removeRecycleBin()
set(value) {
mDatabaseKDBX?.isRecycleBinEnabled = value
}
}
val recycleBin: Group?
get() {
@@ -430,52 +363,16 @@ class Database {
return null
}
fun setRecycleBin(group: Group?) {
// Only the kdbx recycle bin can be changed
if (group != null) {
mDatabaseKDBX?.recycleBinUUID = group.nodeIdKDBX.id
} else {
mDatabaseKDBX?.removeTemplatesGroup()
}
fun ensureRecycleBinExists(resources: Resources) {
mDatabaseKDB?.ensureBackupExists()
mDatabaseKDBX?.ensureRecycleBinExists(resources)
}
/**
* Determine if a configurable templates group is available or not for this version of database
* @return true if a configurable templates group available
*/
val allowConfigurableTemplatesGroup: Boolean
get() = mDatabaseKDBX != null
// Maybe another templates method with KDBX5
val isTemplatesEnabled: Boolean
get() = mDatabaseKDBX?.isTemplatesGroupEnabled() ?: false
fun enableTemplates(enable: Boolean, templatesGroupName: String) {
mDatabaseKDBX?.enableTemplatesGroup(enable, templatesGroupName)
fun removeRecycleBin() {
// Don't allow remove backup in KDB
mDatabaseKDBX?.removeRecycleBin()
}
val templatesGroup: Group?
get() {
mDatabaseKDBX?.getTemplatesGroup()?.let {
return Group(it)
}
return null
}
fun setTemplatesGroup(group: Group?) {
// Only the kdbx templates group can be changed
if (group != null) {
mDatabaseKDBX?.entryTemplatesGroup = group.nodeIdKDBX.id
} else {
mDatabaseKDBX?.entryTemplatesGroup
}
}
val groupNamesNotAllowed: List<String>
get() {
return mDatabaseKDB?.groupNamesNotAllowed ?: ArrayList()
}
private fun setDatabaseKDB(databaseKDB: DatabaseKDB) {
this.mDatabaseKDB = databaseKDB
this.mDatabaseKDBX = null
@@ -486,11 +383,8 @@ class Database {
this.mDatabaseKDBX = databaseKDBX
}
fun createData(databaseUri: Uri,
databaseName: String,
rootName: String,
templateGroupName: String?) {
val newDatabase = DatabaseKDBX(databaseName, rootName, templateGroupName)
fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
val newDatabase = DatabaseKDBX(databaseName, rootName)
setDatabaseKDBX(newDatabase)
this.fileUri = databaseUri
// Set Database state
@@ -652,28 +546,25 @@ class Database {
omitBackup: Boolean,
max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
SearchParameters().apply {
this.searchQuery = searchQuery
}, omitBackup, max)
searchQuery, SearchParameters(), omitBackup, max)
}
fun createVirtualGroupFromSearchInfo(searchInfoString: String,
omitBackup: Boolean,
max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
SearchParameters().apply {
searchQuery = searchInfoString
searchInTitles = true
searchInUserNames = false
searchInPasswords = false
searchInUrls = true
searchInNotes = true
searchInOTP = false
searchInOther = true
searchInUUIDs = false
searchInTags = false
searchInTemplates = false
}, omitBackup, max)
searchInfoString, SearchParameters().apply {
searchInTitles = true
searchInUserNames = false
searchInPasswords = false
searchInUrls = true
searchInNotes = true
searchInOTP = false
searchInOther = true
searchInUUIDs = false
searchInTags = false
ignoreCase = true
}, omitBackup, max)
}
val attachmentPool: AttachmentPool
@@ -690,11 +581,10 @@ class Database {
return false
}
fun buildNewBinaryAttachment(): BinaryData? {
fun buildNewBinaryAttachment(compressed: Boolean = false,
protected: Boolean = false): BinaryData? {
return mDatabaseKDB?.buildNewAttachment()
?: mDatabaseKDBX?.buildNewAttachment( false,
compressionForNewEntry(),
false)
?: mDatabaseKDBX?.buildNewAttachment( false, compressed, protected)
}
fun removeAttachmentIfNotUsed(attachment: Attachment) {
@@ -785,8 +675,8 @@ class Database {
}
}
fun clearAndClose(context: Context? = null) {
clear(context?.let { UriUtil.getBinaryDir(context) })
fun clearAndClose(filesDirectory: File? = null) {
clear(filesDirectory)
this.mDatabaseKDB = null
this.mDatabaseKDBX = null
this.fileUri = null
@@ -904,11 +794,11 @@ class Database {
}
fun addGroupTo(group: Group, parent: Group) {
group.groupKDB?.let { groupKDB ->
mDatabaseKDB?.addGroupTo(groupKDB, parent.groupKDB)
group.groupKDB?.let { entryKDB ->
mDatabaseKDB?.addGroupTo(entryKDB, parent.groupKDB)
}
group.groupKDBX?.let { groupKDBX ->
mDatabaseKDBX?.addGroupTo(groupKDBX, parent.groupKDBX)
group.groupKDBX?.let { entryKDBX ->
mDatabaseKDBX?.addGroupTo(entryKDBX, parent.groupKDBX)
}
group.afterAssignNewParent()
}
@@ -923,11 +813,11 @@ class Database {
}
fun removeGroupFrom(group: Group, parent: Group) {
group.groupKDB?.let { groupKDB ->
mDatabaseKDB?.removeGroupFrom(groupKDB, parent.groupKDB)
group.groupKDB?.let { entryKDB ->
mDatabaseKDB?.removeGroupFrom(entryKDB, parent.groupKDB)
}
group.groupKDBX?.let { groupKDBX ->
mDatabaseKDBX?.removeGroupFrom(groupKDBX, parent.groupKDBX)
group.groupKDBX?.let { entryKDBX ->
mDatabaseKDBX?.removeGroupFrom(entryKDBX, parent.groupKDBX)
}
group.afterAssignNewParent()
}
@@ -1002,11 +892,6 @@ class Database {
}
}
fun ensureRecycleBinExists(resources: Resources) {
mDatabaseKDB?.ensureBackupExists()
mDatabaseKDBX?.ensureRecycleBinExists(resources)
}
fun canRecycle(entry: Entry): Boolean {
var canRecycle: Boolean? = null
entry.entryKDB?.let {

View File

@@ -23,209 +23,95 @@ import android.content.res.Resources
import android.os.Parcel
import android.os.Parcelable
import androidx.core.os.ConfigurationCompat
import com.kunzisoft.keepass.utils.readEnum
import com.kunzisoft.keepass.utils.writeEnum
import org.joda.time.*
import java.text.DateFormat
import org.joda.time.Duration
import org.joda.time.Instant
import java.text.SimpleDateFormat
import java.util.*
class DateInstant : Parcelable {
private var jDate: Date = Date()
private var mType: Type = Type.DATE_TIME
val date: Date
get() = jDate
var type: Type
get() = mType
set(value) {
mType = value
}
constructor(source: DateInstant) {
this.jDate = Date(source.jDate.time)
this.mType = source.mType
}
constructor(date: Date, type: Type = Type.DATE_TIME) {
constructor(date: Date) {
jDate = Date(date.time)
mType = type
}
constructor(millis: Long, type: Type = Type.DATE_TIME) {
constructor(millis: Long) {
jDate = Date(millis)
mType = type
}
private fun parse(value: String, type: Type): Date {
return when (type) {
Type.DATE -> dateFormat.parse(value) ?: jDate
Type.TIME -> timeFormat.parse(value) ?: jDate
else -> dateTimeFormat.parse(value) ?: jDate
}
}
constructor(string: String, type: Type = Type.DATE_TIME) {
try {
jDate = parse(string, type)
mType = type
} catch (e: Exception) {
// Retry with second format
try {
when (type) {
Type.TIME -> {
jDate = parse(string, Type.DATE)
mType = Type.DATE
}
else -> {
jDate = parse(string, Type.TIME)
mType = Type.TIME
}
}
} catch (e: Exception) {
// Retry with third format
when (type) {
Type.DATE, Type.TIME -> {
jDate = parse(string, Type.DATE_TIME)
mType = Type.DATE_TIME
}
else -> {
jDate = parse(string, Type.DATE)
mType = Type.DATE
}
}
}
}
}
constructor(type: Type) {
mType = type
constructor(string: String) {
jDate = dateFormat.parse(string) ?: jDate
}
constructor() {
jDate = Date()
}
constructor(parcel: Parcel) {
jDate = parcel.readSerializable() as? Date? ?: jDate
mType = parcel.readEnum<Type>() ?: mType
protected constructor(parcel: Parcel) {
jDate = parcel.readSerializable() as Date
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeSerializable(jDate)
dest.writeEnum(mType)
}
fun getDateTimeString(resources: Resources): String {
return when (mType) {
Type.DATE -> DateFormat.getDateInstance(
DateFormat.MEDIUM,
ConfigurationCompat.getLocales(resources.configuration)[0])
.format(jDate)
Type.TIME -> DateFormat.getTimeInstance(
DateFormat.SHORT,
ConfigurationCompat.getLocales(resources.configuration)[0])
.format(jDate)
else -> DateFormat.getDateTimeInstance(
DateFormat.MEDIUM,
DateFormat.SHORT,
ConfigurationCompat.getLocales(resources.configuration)[0])
.format(jDate)
}
return Companion.getDateTimeString(resources, this.date)
}
fun getYearInt(): Int {
val dateFormat = SimpleDateFormat("yyyy", Locale.ENGLISH)
return dateFormat.format(date).toInt()
}
fun getMonthInt(): Int {
val dateFormat = SimpleDateFormat("MM", Locale.ENGLISH)
return dateFormat.format(date).toInt()
}
fun getDay(): Int {
val dateFormat = SimpleDateFormat("dd", Locale.ENGLISH)
return dateFormat.format(date).toInt()
}
// If expireDate is before NEVER_EXPIRE date less 1 month (to be sure)
// it is not expires
fun isNeverExpires(): Boolean {
return LocalDateTime(jDate)
.isBefore(
LocalDateTime.fromDateFields(NEVER_EXPIRES.date)
.minusMonths(1))
}
fun isCurrentlyExpire(): Boolean {
return when (type) {
Type.DATE -> LocalDate.fromDateFields(jDate).isBefore(LocalDate.now())
Type.TIME -> LocalTime.fromDateFields(jDate).isBefore(LocalTime.now())
else -> LocalDateTime.fromDateFields(jDate).isBefore(LocalDateTime.now())
}
}
override fun toString(): String {
return when (type) {
Type.DATE -> dateFormat.format(jDate)
Type.TIME -> timeFormat.format(jDate)
else -> dateTimeFormat.format(jDate)
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeSerializable(date)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is DateInstant) return false
if (this === other) {
return true
}
if (other == null) {
return false
}
if (javaClass != other.javaClass) {
return false
}
if (jDate != other.jDate) return false
if (mType != other.mType) return false
return true
val date = other as DateInstant
return isSameDate(jDate, date.jDate)
}
override fun hashCode(): Int {
var result = jDate.hashCode()
result = 31 * result + mType.hashCode()
return result
return jDate.hashCode()
}
enum class Type {
DATE_TIME, DATE, TIME
override fun toString(): String {
return dateFormat.format(jDate)
}
companion object {
val NEVER_EXPIRES = DateInstant(Calendar.getInstance().apply {
set(Calendar.YEAR, 2999)
set(Calendar.MONTH, 11)
set(Calendar.DAY_OF_MONTH, 28)
set(Calendar.HOUR, 23)
set(Calendar.MINUTE, 59)
set(Calendar.SECOND, 59)
}.time)
val IN_ONE_MONTH_DATE_TIME = DateInstant(
Instant.now().plus(Duration.standardDays(30)).toDate(), Type.DATE_TIME)
val IN_ONE_MONTH_DATE = DateInstant(
Instant.now().plus(Duration.standardDays(30)).toDate(), Type.DATE)
val IN_ONE_HOUR_TIME = DateInstant(
Instant.now().plus(Duration.standardHours(1)).toDate(), Type.TIME)
val NEVER_EXPIRE = neverExpire
val IN_ONE_MONTH = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
private val dateFormat = SimpleDateFormat.getDateTimeInstance()
private val dateTimeFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.ROOT).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'Z'", Locale.ROOT).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
private val timeFormat = SimpleDateFormat("HH:mm'Z'", Locale.ROOT).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
private val neverExpire: DateInstant
get() {
val cal = Calendar.getInstance()
cal.set(Calendar.YEAR, 2999)
cal.set(Calendar.MONTH, 11)
cal.set(Calendar.DAY_OF_MONTH, 28)
cal.set(Calendar.HOUR, 23)
cal.set(Calendar.MINUTE, 59)
cal.set(Calendar.SECOND, 59)
return DateInstant(cal.time)
}
@JvmField
val CREATOR: Parcelable.Creator<DateInstant> = object : Parcelable.Creator<DateInstant> {
@@ -237,5 +123,31 @@ class DateInstant : Parcelable {
return arrayOfNulls(size)
}
}
private fun isSameDate(d1: Date, d2: Date): Boolean {
val cal1 = Calendar.getInstance()
cal1.time = d1
cal1.set(Calendar.MILLISECOND, 0)
val cal2 = Calendar.getInstance()
cal2.time = d2
cal2.set(Calendar.MILLISECOND, 0)
return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
cal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH) &&
cal1.get(Calendar.DAY_OF_MONTH) == cal2.get(Calendar.DAY_OF_MONTH) &&
cal1.get(Calendar.HOUR) == cal2.get(Calendar.HOUR) &&
cal1.get(Calendar.MINUTE) == cal2.get(Calendar.MINUTE) &&
cal1.get(Calendar.SECOND) == cal2.get(Calendar.SECOND)
}
fun getDateTimeString(resources: Resources, date: Date): String {
return java.text.DateFormat.getDateTimeInstance(
java.text.DateFormat.MEDIUM,
java.text.DateFormat.SHORT,
ConfigurationCompat.getLocales(resources.configuration)[0])
.format(date)
}
}
}

View File

@@ -19,37 +19,30 @@
*/
package com.kunzisoft.keepass.database.element
import android.os.Parcel
import android.os.ParcelUuid
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import java.util.*
import java.util.Date
import java.util.UUID
class DeletedObject : Parcelable {
class DeletedObject {
var uuid: UUID = DatabaseVersioned.UUID_ZERO
private var mDeletionTime: DateInstant? = null
private var mDeletionTime: Date? = null
constructor()
constructor(uuid: UUID, deletionTime: DateInstant = DateInstant()) {
this.uuid = uuid
this.mDeletionTime = deletionTime
}
constructor(parcel: Parcel) {
uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
mDeletionTime = parcel.readParcelable(DateInstant::class.java.classLoader)
}
fun getDeletionTime(): DateInstant {
fun getDeletionTime(): Date {
if (mDeletionTime == null) {
mDeletionTime = DateInstant(System.currentTimeMillis())
mDeletionTime = Date(System.currentTimeMillis())
}
return mDeletionTime!!
}
fun setDeletionTime(deletionTime: DateInstant) {
fun setDeletionTime(deletionTime: Date) {
this.mDeletionTime = deletionTime
}
constructor()
constructor(uuid: UUID, deletionTime: Date = Date()) {
this.uuid = uuid
this.mDeletionTime = deletionTime
}
@@ -66,23 +59,4 @@ class DeletedObject : Parcelable {
override fun hashCode(): Int {
return uuid.hashCode()
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(ParcelUuid(uuid), flags)
parcel.writeParcelable(mDeletionTime, flags)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<DeletedObject> {
override fun createFromParcel(parcel: Parcel): DeletedObject {
return DeletedObject(parcel)
}
override fun newArray(size: Int): Array<DeletedObject?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -23,7 +23,6 @@ import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
@@ -33,6 +32,7 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields
import java.util.*
@@ -45,6 +45,15 @@ class Entry : Node, EntryVersionedInterface<Group> {
var entryKDBX: EntryKDBX? = null
private set
fun updateWith(entry: Entry, copyHistory: Boolean = true) {
entry.entryKDB?.let {
this.entryKDB?.updateWith(it)
}
entry.entryKDBX?.let {
this.entryKDBX?.updateWith(it, copyHistory)
}
}
/**
* Use this constructor to copy an Entry with exact same values
*/
@@ -55,12 +64,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
if (entry.entryKDBX != null) {
this.entryKDBX = EntryKDBX()
}
entry.entryKDB?.let {
this.entryKDB?.updateWith(it)
}
entry.entryKDBX?.let {
this.entryKDBX?.updateWith(it, copyHistory)
}
updateWith(entry, copyHistory)
}
constructor(entry: EntryKDB) {
@@ -110,20 +114,6 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryKDBX?.icon = value
}
var tags: Tags
get() = entryKDBX?.tags ?: Tags()
set(value) {
entryKDBX?.tags = value
}
var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO
get() = entryKDBX?.previousParentGroup ?: DatabaseVersioned.UUID_ZERO
private set
fun setPreviousParentGroup(previousParent: Group?) {
entryKDBX?.previousParentGroup = previousParent?.groupKDBX?.id ?: DatabaseVersioned.UUID_ZERO
}
override val type: Type
get() = Type.ENTRY
@@ -278,8 +268,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
fun getExtraFields(): List<Field> {
val extraFields = ArrayList<Field>()
entryKDBX?.let {
it.doForEachDecodedCustomField { field ->
extraFields.add(field)
for (field in it.customFields) {
extraFields.add(Field(field.key, field.value))
}
}
return extraFields
@@ -289,7 +279,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
* Update or add an extra field to the list (standard or custom)
*/
fun putExtraField(field: Field) {
entryKDBX?.putField(field)
entryKDBX?.putExtraField(field.name, field.protectedValue)
}
private fun addExtraFields(fields: List<Field>) {
@@ -305,7 +295,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
fun getOtpElement(): OtpElement? {
entryKDBX?.let {
return OtpEntryFields.parseFields { key ->
it.getFieldValue(key)?.toString()
it.customFields[key]?.toString()
}
}
return null
@@ -383,6 +373,10 @@ class Entry : Node, EntryVersionedInterface<Group> {
return entryKDBX?.getSize(attachmentPool) ?: 0L
}
fun containsCustomData(): Boolean {
return entryKDBX?.containsCustomData() ?: false
}
/*
------------
Converter
@@ -393,46 +387,37 @@ class Entry : Node, EntryVersionedInterface<Group> {
* Retrieve generated entry info.
* If are not [raw] data, remove parameter fields and add auto generated elements in auto custom fields
*/
fun getEntryInfo(database: Database?,
raw: Boolean = false,
removeTemplateConfiguration: Boolean = true): EntryInfo {
fun getEntryInfo(database: Database?, raw: Boolean = false): EntryInfo {
val entryInfo = EntryInfo()
// Remove unwanted template fields
val baseInfo = if (removeTemplateConfiguration)
database?.removeTemplateConfiguration(this) ?: this
if (raw)
database?.stopManageEntry(this)
else
this
baseInfo.apply {
if (raw)
database?.stopManageEntry(this)
else
database?.startManageEntry(this)
database?.startManageEntry(this)
entryInfo.id = nodeId.id
entryInfo.title = title
entryInfo.icon = icon
entryInfo.username = username
entryInfo.password = password
entryInfo.creationTime = creationTime
entryInfo.lastModificationTime = lastModificationTime
entryInfo.expires = expires
entryInfo.expiryTime = expiryTime
entryInfo.url = url
entryInfo.notes = notes
entryInfo.customFields = getExtraFields().toMutableList()
// Add otpElement to generate token
entryInfo.otpModel = getOtpElement()?.otpModel
if (!raw) {
// Replace parameter fields by generated OTP fields
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
}
database?.attachmentPool?.let { binaryPool ->
entryInfo.attachments = getAttachments(binaryPool).toMutableList()
}
if (!raw)
database?.stopManageEntry(this)
entryInfo.id = nodeId.toString()
entryInfo.title = title
entryInfo.icon = icon
entryInfo.username = username
entryInfo.password = password
entryInfo.creationTime = creationTime
entryInfo.lastModificationTime = lastModificationTime
entryInfo.expires = expires
entryInfo.expiryTime = expiryTime
entryInfo.url = url
entryInfo.notes = notes
entryInfo.customFields = getExtraFields()
// Add otpElement to generate token
entryInfo.otpModel = getOtpElement()?.otpModel
if (!raw) {
// Replace parameter fields by generated OTP fields
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
}
database?.attachmentPool?.let { binaryPool ->
entryInfo.attachments = getAttachments(binaryPool)
}
if (!raw)
database?.stopManageEntry(this)
return entryInfo
}
@@ -481,7 +466,16 @@ class Entry : Node, EntryVersionedInterface<Group> {
return result
}
companion object {
companion object CREATOR : Parcelable.Creator<Entry> {
override fun createFromParcel(parcel: Parcel): Entry {
return Entry(parcel)
}
override fun newArray(size: Int): Array<Entry?> {
return arrayOfNulls(size)
}
const val PMS_TAN_ENTRY = "<TAN>"
/**
@@ -490,16 +484,5 @@ class Entry : Node, EntryVersionedInterface<Group> {
fun newExtraFieldNameAllowed(field: Field): Boolean {
return EntryKDBX.newCustomNameAllowed(field.name)
}
@JvmField
val CREATOR: Parcelable.Creator<Entry> = object : Parcelable.Creator<Entry> {
override fun createFromParcel(parcel: Parcel): Entry {
return Entry(parcel)
}
override fun newArray(size: Int): Array<Entry?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.database.element
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
@@ -44,7 +43,14 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
// Virtual group is used to defined a detached database group
var isVirtual = false
var numberOfChildEntries: Int = 0
fun updateWith(group: Group) {
group.groupKDB?.let {
this.groupKDB?.updateWith(it)
}
group.groupKDBX?.let {
this.groupKDBX?.updateWith(it)
}
}
/**
* Use this constructor to copy a Group
@@ -58,12 +64,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
if (this.groupKDBX == null)
this.groupKDBX = GroupKDBX()
}
group.groupKDB?.let {
this.groupKDB?.updateWith(it)
}
group.groupKDBX?.let {
this.groupKDBX?.updateWith(it)
}
updateWith(group)
}
constructor(group: GroupKDB) {
@@ -116,8 +117,8 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
dest.writeByte((if (isVirtual) 1 else 0).toByte())
}
override val nodeId: NodeId<*>
get() = groupKDBX?.nodeId ?: groupKDB?.nodeId ?: NodeIdUUID()
override val nodeId: NodeId<*>?
get() = groupKDBX?.nodeId ?: groupKDB?.nodeId
override var title: String
get() = groupKDB?.title ?: groupKDBX?.title ?: ""
@@ -133,20 +134,6 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
groupKDBX?.icon = value
}
var tags: Tags
get() = groupKDBX?.tags ?: Tags()
set(value) {
groupKDBX?.tags = value
}
var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO
get() = groupKDBX?.previousParentGroup ?: DatabaseVersioned.UUID_ZERO
private set
fun setPreviousParentGroup(previousParent: Group?) {
groupKDBX?.previousParentGroup = previousParent?.groupKDBX?.id ?: DatabaseVersioned.UUID_ZERO
}
override val type: Type
get() = Type.GROUP
@@ -268,20 +255,6 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
ArrayList()
}
fun getFilteredChildGroups(filters: Array<ChildFilter>): List<Group> {
return groupKDB?.getChildGroups()?.map {
Group(it).apply {
this.refreshNumberOfChildEntries(filters)
}
} ?:
groupKDBX?.getChildGroups()?.map {
Group(it).apply {
this.refreshNumberOfChildEntries(filters)
}
} ?:
ArrayList()
}
override fun getChildEntries(): List<Entry> {
return groupKDB?.getChildEntries()?.map {
Entry(it)
@@ -318,8 +291,8 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
ArrayList()
}
fun refreshNumberOfChildEntries(filters: Array<ChildFilter> = emptyArray()) {
this.numberOfChildEntries = getFilteredChildEntries(filters).size
fun getNumberOfChildEntries(filters: Array<ChildFilter> = emptyArray()): Int {
return getFilteredChildEntries(filters).size
}
/**
@@ -331,9 +304,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
}
fun getFilteredChildren(filters: Array<ChildFilter>): List<Node> {
val nodes = getFilteredChildGroups(filters) + getFilteredChildEntries(filters)
refreshNumberOfChildEntries(filters)
return nodes
return getChildGroups() + getFilteredChildEntries(filters)
}
override fun addChildGroup(group: Group) {
@@ -354,24 +325,6 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
}
}
override fun updateChildGroup(group: Group) {
group.groupKDB?.let {
groupKDB?.updateChildGroup(it)
}
group.groupKDBX?.let {
groupKDBX?.updateChildGroup(it)
}
}
override fun updateChildEntry(entry: Entry) {
entry.entryKDB?.let {
groupKDB?.updateChildEntry(it)
}
entry.entryKDBX?.let {
groupKDBX?.updateChildEntry(it)
}
}
override fun removeChildGroup(group: Group) {
group.groupKDB?.let {
groupKDB?.removeChildGroup(it)
@@ -415,6 +368,14 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
groupKDB?.nodeId = id
}
fun getLevel(): Int {
return groupKDB?.level ?: -1
}
fun setLevel(level: Int) {
groupKDB?.level = level
}
/*
------------
KDBX Methods
@@ -441,6 +402,10 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
groupKDBX?.isExpanded = expanded
}
fun containsCustomData(): Boolean {
return groupKDBX?.containsCustomData() ?: false
}
/*
------------
Converter
@@ -487,10 +452,4 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
result = 31 * result + (groupKDBX?.hashCode() ?: 0)
return result
}
override fun toString(): String {
return groupKDB?.toString() ?: groupKDBX?.toString() ?: "Undefined"
}
}

View File

@@ -28,17 +28,15 @@ import java.util.*
enum class SortNodeEnum {
DB, TITLE, USERNAME, CREATION_TIME, LAST_MODIFY_TIME, LAST_ACCESS_TIME;
fun <G: GroupVersionedInterface<G, *>> getNodeComparator(
database: Database,
sortNodeParameters: SortNodeParameters)
fun <G: GroupVersionedInterface<G, *>> getNodeComparator(sortNodeParameters: SortNodeParameters)
: Comparator<NodeVersionedInterface<G>> {
return when (this) {
DB -> NodeNaturalComparator(database, sortNodeParameters) // Force false because natural order contains recycle bin
TITLE -> NodeTitleComparator(database, sortNodeParameters)
USERNAME -> NodeUsernameComparator(database, sortNodeParameters)
CREATION_TIME -> NodeCreationComparator(database, sortNodeParameters)
LAST_MODIFY_TIME -> NodeLastModificationComparator(database, sortNodeParameters)
LAST_ACCESS_TIME -> NodeLastAccessComparator(database, sortNodeParameters)
DB -> NodeNaturalComparator(sortNodeParameters) // Force false because natural order contains recycle bin
TITLE -> NodeTitleComparator(sortNodeParameters)
USERNAME -> NodeUsernameComparator(sortNodeParameters)
CREATION_TIME -> NodeCreationComparator(sortNodeParameters)
LAST_MODIFY_TIME -> NodeLastModificationComparator(sortNodeParameters)
LAST_ACCESS_TIME -> NodeLastAccessComparator(sortNodeParameters)
}
}
@@ -50,9 +48,11 @@ enum class SortNodeEnum {
<
G: GroupVersionedInterface<*, *>,
T: NodeVersionedInterface<G>
>(var database: Database, var sortNodeParameters: SortNodeParameters)
>(var sortNodeParameters: SortNodeParameters)
: Comparator<T> {
val database = Database.getInstance()
abstract fun compareBySpecificOrder(object1: T, object2: T): Int
private fun specificOrderOrHashIfEquals(object1: T, object2: T): Int {
@@ -110,9 +110,8 @@ enum class SortNodeEnum {
* Comparator of node by natural database placement
*/
class NodeNaturalComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
database: Database,
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(database, sortNodeParameters) {
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int {
return object1.nodeIndexInParentForNaturalOrder()
@@ -124,14 +123,13 @@ enum class SortNodeEnum {
* Comparator of Node by Title
*/
class NodeTitleComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
database: Database,
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(database, sortNodeParameters) {
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int {
val titleCompare = object1.title.compareTo(object2.title, ignoreCase = true)
return if (titleCompare == 0)
NodeNaturalComparator<G, T>(database, sortNodeParameters)
NodeNaturalComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
else
titleCompare
@@ -142,9 +140,8 @@ enum class SortNodeEnum {
* Comparator of Node by Username, Groups by title
*/
class NodeUsernameComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
database: Database,
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(database, sortNodeParameters) {
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int {
return if (object1.type == Type.ENTRY && object2.type == Type.ENTRY) {
@@ -153,12 +150,12 @@ enum class SortNodeEnum {
.compareTo((object2 as Entry).getEntryInfo(database).username,
ignoreCase = true)
if (usernameCompare == 0)
NodeTitleComparator<G, T>(database, sortNodeParameters)
NodeTitleComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
else
usernameCompare
} else {
NodeTitleComparator<G, T>(database, sortNodeParameters)
NodeTitleComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
}
}
@@ -168,15 +165,14 @@ enum class SortNodeEnum {
* Comparator of node by creation
*/
class NodeCreationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
database: Database,
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(database, sortNodeParameters) {
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int {
val creationCompare = object1.creationTime.date
.compareTo(object2.creationTime.date)
return if (creationCompare == 0)
NodeNaturalComparator<G, T>(database, sortNodeParameters)
NodeNaturalComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
else
creationCompare
@@ -187,15 +183,14 @@ enum class SortNodeEnum {
* Comparator of node by last modification
*/
class NodeLastModificationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
database: Database,
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(database, sortNodeParameters) {
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int {
val lastModificationCompare = object1.lastModificationTime.date
.compareTo(object2.lastModificationTime.date)
return if (lastModificationCompare == 0)
NodeNaturalComparator<G, T>(database, sortNodeParameters)
NodeNaturalComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
else
lastModificationCompare
@@ -206,15 +201,14 @@ enum class SortNodeEnum {
* Comparator of node by last access
*/
class NodeLastAccessComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
database: Database,
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(database, sortNodeParameters) {
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int {
val lastAccessCompare = object1.lastAccessTime.date
.compareTo(object2.lastAccessTime.date)
return if (lastAccessCompare == 0)
NodeNaturalComparator<G, T>(database, sortNodeParameters)
NodeNaturalComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
else
lastAccessCompare

View File

@@ -1,45 +0,0 @@
package com.kunzisoft.keepass.database.element
import android.os.Parcel
import android.os.Parcelable
class Tags: Parcelable {
private val mTags = ArrayList<String>()
constructor()
constructor(values: String): this() {
mTags.addAll(values.split(';'))
}
constructor(parcel: Parcel) : this() {
parcel.readStringList(mTags)
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeStringList(mTags)
}
override fun describeContents(): Int {
return 0
}
fun isEmpty(): Boolean {
return mTags.isEmpty()
}
override fun toString(): String {
return mTags.joinToString(";")
}
companion object CREATOR : Parcelable.Creator<Tags> {
override fun createFromParcel(parcel: Parcel): Tags {
return Tags(parcel)
}
override fun newArray(size: Int): Array<Tags?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -11,7 +11,7 @@ class BinaryCache {
*/
var loadedCipherKey: LoadedKey = LoadedKey.generateNewCipherKey()
var cacheDirectory: File? = null
lateinit var cacheDirectory: File
private val voidBinary = KeyByteArray(UNKNOWN, ByteArray(0))
@@ -19,16 +19,15 @@ class BinaryCache {
smallSize: Boolean = false,
compression: Boolean = false,
protection: Boolean = false): BinaryData {
val cacheDir = cacheDirectory
return if (smallSize || cacheDir == null) {
return if (smallSize) {
BinaryByte(binaryId, compression, protection)
} else {
val fileInCache = File(cacheDir, binaryId)
BinaryFile(fileInCache, compression, protection)
val fileInCache = File(cacheDirectory, binaryId)
return BinaryFile(fileInCache, compression, protection)
}
}
// Similar to file storage but much faster TODO SparseArray
// Similar to file storage but much faster
private val byteArrayList = HashMap<String, ByteArray>()
fun getByteArray(key: String): KeyByteArray {

View File

@@ -1,27 +1,8 @@
package com.kunzisoft.keepass.database.element.binary
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import java.util.*
class CustomIconPool(private val binaryCache: BinaryCache) : BinaryPool<UUID>(binaryCache) {
private val customIcons = HashMap<UUID, IconImageCustom>()
fun put(key: UUID? = null,
name: String,
lastModificationTime: DateInstant?,
smallSize: Boolean,
result: (IconImageCustom, BinaryData?) -> Unit) {
val keyBinary = super.put(key) { uniqueBinaryId ->
// Create a byte array for better performance with small data
binaryCache.getBinaryData(uniqueBinaryId, smallSize)
}
val uuid = keyBinary.keys.first()
val customIcon = IconImageCustom(uuid, name, lastModificationTime)
customIcons[uuid] = customIcon
result.invoke(customIcon, keyBinary.binary)
}
class CustomIconPool(binaryCache: BinaryCache) : BinaryPool<UUID>(binaryCache) {
override fun findUnusedKey(): UUID {
var newUUID = UUID.randomUUID()
@@ -30,14 +11,4 @@ class CustomIconPool(private val binaryCache: BinaryCache) : BinaryPool<UUID>(bi
}
return newUUID
}
fun any(predicate: (IconImageCustom)-> Boolean): Boolean {
return customIcons.any { predicate(it.value) }
}
fun doForEachCustomIcon(action: (customIcon: IconImageCustom, binary: BinaryData) -> Unit) {
doForEachBinary { key, binary ->
action.invoke(customIcons[key] ?: IconImageCustom(key), binary)
}
}
}

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.database.element.database
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.encrypt.aes.AESTransformer
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
@@ -31,6 +30,7 @@ import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeVersioned
import encrypt.Encrypt
import java.io.IOException
import java.io.InputStream
import java.util.*
@@ -38,6 +38,8 @@ import kotlin.collections.ArrayList
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
private var backupGroupId: Int = BACKUP_FOLDER_UNDEFINED_ID
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
override val version: String
@@ -53,14 +55,13 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return getGroupById(NodeIdInt(groupId))
}
// Retrieve backup group in index
val backupGroup: GroupKDB?
get() {
return retrieveBackup()
}
val groupNamesNotAllowed: List<String>
get() {
return listOf(BACKUP_FOLDER_TITLE)
return if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
null
else
getGroupById(backupGroupId)
}
override val kdfEngine: KdfEngine
@@ -79,7 +80,12 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
val rootGroups: List<GroupKDB>
get() {
return rootGroup?.getChildGroups() ?: ArrayList()
val kids = ArrayList<GroupKDB>()
doForEachGroupInIndex { group ->
if (group.level == 0)
kids.add(group)
}
return kids
}
override val passwordEncoding: String
@@ -136,7 +142,8 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
@Throws(IOException::class)
fun makeFinalKey(masterSeed: ByteArray, transformSeed: ByteArray, numRounds: Long) {
// Encrypt the master key a few times to make brute-force key-search harder
val transformedKey = AESTransformer.transformKey(transformSeed, masterKey, numRounds) ?: ByteArray(0)
val transformedKey = Encrypt.transformAESKey(masterSeed, transformSeed, numRounds) ?: ByteArray(0)
//val transformedKey = AESTransformer.transformKey(transformSeed, masterKey, numRounds) ?: ByteArray(0)
// Write checksum Checksum
finalKey = HashManager.hashSha256(masterSeed, transformedKey)
}
@@ -157,16 +164,27 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return this.iconsManager.getIcon(iconId)
}
override fun containsCustomData(): Boolean {
return false
}
override fun isInRecycleBin(group: GroupKDB): Boolean {
var currentGroup: GroupKDB? = group
val currentBackupGroup = backupGroup ?: return false
if (currentGroup == currentBackupGroup)
// Init backup group variable
if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
findBackupGroupId()
if (backupGroup == null)
return false
if (currentGroup == backupGroup)
return true
val backupGroupId = currentBackupGroup.id
while (currentGroup != null) {
if (backupGroupId == currentGroup.id) {
if (currentGroup.level == 0
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) {
backupGroupId = currentGroup.id
return true
}
currentGroup = currentGroup.parent
@@ -174,12 +192,12 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return false
}
/**
* Retrieve backup group with his name
*/
private fun retrieveBackup(): GroupKDB? {
return rootGroup?.searchChildGroup {
it.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)
private fun findBackupGroupId() {
rootGroups.forEach { currentGroup ->
if (currentGroup.level == 0
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) {
backupGroupId = currentGroup.id
}
}
}
@@ -188,6 +206,8 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
* if it doesn't exist
*/
fun ensureBackupExists() {
findBackupGroupId()
if (backupGroup == null) {
// Create recycle bin
val recycleBinGroup = createGroup().apply {
@@ -195,6 +215,7 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
}
addGroupTo(recycleBinGroup, rootGroup)
backupGroupId = recycleBinGroup.id
}
}
@@ -248,5 +269,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
val TYPE = DatabaseKDB::class.java
const val BACKUP_FOLDER_TITLE = "Backup"
private const val BACKUP_FOLDER_UNDEFINED_ID = -1
}
}

View File

@@ -23,6 +23,8 @@ import android.content.res.Resources
import android.util.Base64
import android.util.Log
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.longTo8Bytes
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.crypto.AesEngine
@@ -32,29 +34,22 @@ import com.kunzisoft.keepass.database.crypto.VariantDictionary
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
import com.kunzisoft.keepass.database.element.CustomData
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.entry.FieldReferencesEngine
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.database.element.template.TemplateEngineCompatible
import com.kunzisoft.keepass.database.exception.UnknownKDF
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_31
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_3
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.longTo8Bytes
import org.apache.commons.codec.binary.Hex
import org.w3c.dom.Node
import java.io.IOException
@@ -80,8 +75,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
private var kdfList: MutableList<KdfEngine> = ArrayList()
private var numKeyEncRounds: Long = 0
var publicCustomData = VariantDictionary()
private val mFieldReferenceEngine = FieldReferencesEngine(this)
private val mTemplateEngine = TemplateEngineCompatible(this)
var kdbxVersion = UnsignedInt(0)
var name = ""
@@ -107,7 +100,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
*/
var isRecycleBinEnabled = true
var recycleBinUUID: UUID = UUID_ZERO
var recycleBinChanged = DateInstant()
var recycleBinChanged = Date()
var entryTemplatesGroup = UUID_ZERO
var entryTemplatesGroupChanged = DateInstant()
var historyMaxItems = DEFAULT_HISTORY_MAX_ITEMS
@@ -116,7 +109,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
var lastTopVisibleGroupUUID = UUID_ZERO
var memoryProtection = MemoryProtectionConfig()
val deletedObjects = ArrayList<DeletedObject>()
val customData = CustomData()
val customData = HashMap<String, String>()
var localizedAppName = "KeePassDX"
@@ -131,29 +124,22 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
/**
* Create a new database with a root group
*/
constructor(databaseName: String,
rootName: String,
templatesGroupName: String? = null) {
constructor(databaseName: String, rootName: String) {
name = databaseName
kdbxVersion = FILE_VERSION_31
kdbxVersion = FILE_VERSION_32_3
val group = createGroup().apply {
title = rootName
icon.standard = getStandardIcon(IconImageStandard.FOLDER_ID)
}
rootGroup = group
if (templatesGroupName != null) {
val templatesGroup = mTemplateEngine.createNewTemplatesGroup(templatesGroupName)
entryTemplatesGroup = templatesGroup.id
entryTemplatesGroupChanged = templatesGroup.lastModificationTime
}
addGroupIndex(group)
}
override val version: String
get() {
val kdbxStringVersion = when(kdbxVersion) {
FILE_VERSION_31 -> "3.1"
FILE_VERSION_40 -> "4.0"
FILE_VERSION_41 -> "4.1"
FILE_VERSION_32_3 -> "3.1"
FILE_VERSION_32_4 -> "4.0"
else -> "UNKNOWN"
}
return "KeePass 2 - KDBX$kdbxStringVersion"
@@ -201,7 +187,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
}
CompressionAlgorithm.GZip -> {
// Only in databaseV3.1, in databaseV4 the header is zipped during the save
if (kdbxVersion.isBefore(FILE_VERSION_40)) {
if (kdbxVersion.isBefore(FILE_VERSION_32_4)) {
compressAllBinaries()
}
}
@@ -209,7 +195,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
}
CompressionAlgorithm.GZip -> {
// In databaseV4 the header is zipped during the save, so not necessary here
if (kdbxVersion.isBefore(FILE_VERSION_40)) {
if (kdbxVersion.isBefore(FILE_VERSION_32_4)) {
when (newCompression) {
CompressionAlgorithm.None -> {
decompressAllBinaries()
@@ -327,11 +313,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
}
fun addCustomIcon(customIconId: UUID? = null,
name: String,
lastModificationTime: DateInstant?,
smallSize: Boolean,
result: (IconImageCustom, BinaryData?) -> Unit) {
iconsManager.addCustomIcon(customIconId, name, lastModificationTime, smallSize, result)
iconsManager.addCustomIcon(customIconId, smallSize, result)
}
fun isCustomIconBinaryDuplicate(binary: BinaryData): Boolean {
@@ -342,118 +326,12 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return this.iconsManager.getIcon(iconUuid)
}
fun isTemplatesGroupEnabled(): Boolean {
return entryTemplatesGroup != UUID_ZERO
fun putCustomData(label: String, value: String) {
this.customData[label] = value
}
fun enableTemplatesGroup(enable: Boolean, templatesGroupName: String) {
// Create templates group only if a group with a valid name don't already exists
val firstGroupWithValidName = getGroupIndexes().firstOrNull {
it.title == templatesGroupName
}
if (enable) {
val templatesGroup = firstGroupWithValidName
?: mTemplateEngine.createNewTemplatesGroup(templatesGroupName)
entryTemplatesGroup = templatesGroup.id
entryTemplatesGroupChanged = templatesGroup.lastModificationTime
} else {
removeTemplatesGroup()
}
}
fun removeTemplatesGroup() {
entryTemplatesGroup = UUID_ZERO
entryTemplatesGroupChanged = DateInstant()
mTemplateEngine.clearCache()
}
fun getTemplatesGroup(): GroupKDBX? {
if (isTemplatesGroupEnabled()) {
return getGroupById(entryTemplatesGroup)
}
return null
}
fun getTemplates(templateCreation: Boolean): List<Template> {
return if (templateCreation)
listOf(mTemplateEngine.getTemplateCreation())
else
mTemplateEngine.getTemplates()
}
fun getTemplate(entry: EntryKDBX): Template? {
return mTemplateEngine.getTemplate(entry)
}
fun decodeEntryWithTemplateConfiguration(entryKDBX: EntryKDBX, entryIsTemplate: Boolean): EntryKDBX {
return if (entryIsTemplate) {
mTemplateEngine.decodeTemplateEntry(entryKDBX)
} else {
mTemplateEngine.removeMetaTemplateRecognitionFromEntry(entryKDBX)
}
}
fun encodeEntryWithTemplateConfiguration(entryKDBX: EntryKDBX, entryIsTemplate: Boolean, template: Template): EntryKDBX {
return if (entryIsTemplate) {
mTemplateEngine.encodeTemplateEntry(entryKDBX)
} else {
mTemplateEngine.addMetaTemplateRecognitionToEntry(template, entryKDBX)
}
}
/*
* Search methods
*/
fun getGroupById(id: UUID): GroupKDBX? {
return this.getGroupById(NodeIdUUID(id))
}
fun getEntryById(id: UUID): EntryKDBX? {
return this.getEntryById(NodeIdUUID(id))
}
fun getEntryByTitle(title: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry ->
entry.decodeTitleKey(recursionLevel).equals(title, true)
}
}
fun getEntryByUsername(username: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry ->
entry.decodeUsernameKey(recursionLevel).equals(username, true)
}
}
fun getEntryByURL(url: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry ->
entry.decodeUrlKey(recursionLevel).equals(url, true)
}
}
fun getEntryByPassword(password: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry ->
entry.decodePasswordKey(recursionLevel).equals(password, true)
}
}
fun getEntryByNotes(notes: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry ->
entry.decodeNotesKey(recursionLevel).equals(notes, true)
}
}
fun getEntryByCustomData(customDataValue: String): EntryKDBX? {
return entryIndexes.values.find { entry ->
entry.customData.containsItemWithValue(customDataValue)
}
}
/**
* Retrieve the value of a field reference
*/
fun getFieldReferenceValue(textReference: String, recursionLevel: Int): String {
return mFieldReferenceEngine.compile(textReference, recursionLevel)
override fun containsCustomData(): Boolean {
return customData.isNotEmpty()
}
@Throws(IOException::class)
@@ -706,32 +584,24 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
*/
fun ensureRecycleBinExists(resources: Resources) {
if (recycleBin == null) {
// Create recycle bin only if a group with a valid name don't already exists
val firstGroupWithValidName = getGroupIndexes().firstOrNull {
it.title == resources.getString(R.string.recycle_bin)
}
val recycleBinGroup = if (firstGroupWithValidName == null) {
val newRecycleBinGroup = createGroup().apply {
title = resources.getString(R.string.recycle_bin)
icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
enableAutoType = false
enableSearching = false
isExpanded = false
}
addGroupTo(newRecycleBinGroup, rootGroup)
newRecycleBinGroup
} else {
firstGroupWithValidName
// Create recycle bin
val recycleBinGroup = createGroup().apply {
title = resources.getString(R.string.recycle_bin)
icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
enableAutoType = false
enableSearching = false
isExpanded = false
}
addGroupTo(recycleBinGroup, rootGroup)
recycleBinUUID = recycleBinGroup.id
recycleBinChanged = recycleBinGroup.lastModificationTime
recycleBinChanged = recycleBinGroup.lastModificationTime.date
}
}
fun removeRecycleBin() {
if (recycleBin != null) {
recycleBinUUID = UUID_ZERO
recycleBinChanged = DateInstant()
recycleBinChanged = DateInstant().date
}
}
@@ -745,9 +615,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return false
if (recycleBin == null)
return false
if (node is GroupKDBX
&& recycleBin!!.isContainedIn(node))
return false
if (!node.isContainedIn(recycleBin!!))
return true
return false
@@ -785,20 +652,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
this.deletedObjects.add(deletedObject)
}
override fun addEntryTo(newEntry: EntryKDBX, parent: GroupKDBX?) {
super.addEntryTo(newEntry, parent)
mFieldReferenceEngine.clear()
}
override fun updateEntry(entry: EntryKDBX) {
super.updateEntry(entry)
mFieldReferenceEngine.clear()
}
override fun removeEntryFrom(entryToRemove: EntryKDBX, parent: GroupKDBX?) {
super.removeEntryFrom(entryToRemove, parent)
deletedObjects.add(DeletedObject(entryToRemove.id))
mFieldReferenceEngine.clear()
}
override fun undoDeleteEntryFrom(entry: EntryKDBX, origParent: GroupKDBX?) {
@@ -869,7 +725,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
override fun clearCache() {
try {
super.clearCache()
mFieldReferenceEngine.clear()
attachmentPool.clear()
} catch (e: Exception) {
Log.e(TAG, "Unable to clear cache", e)

View File

@@ -35,6 +35,7 @@ import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStream
import java.io.UnsupportedEncodingException
import java.security.MessageDigest
import java.util.*
abstract class DatabaseVersioned<
@@ -67,7 +68,7 @@ abstract class DatabaseVersioned<
var changeDuplicateId = false
private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>()
protected var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
private var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
abstract val version: String
@@ -86,16 +87,6 @@ abstract class DatabaseVersioned<
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
var rootGroup: Group? = null
set(value) {
field = value
value?.let {
addGroupIndex(it)
}
}
fun getAllGroupsWithoutRoot(): List<Group> {
return getGroupIndexes().filter { it != rootGroup }
}
@Throws(IOException::class)
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
@@ -242,6 +233,13 @@ abstract class DatabaseVersioned<
}
}
fun updateGroupIndex(group: Group) {
val groupId = group.nodeId
if (groupIndexes.containsKey(groupId)) {
groupIndexes[groupId] = group
}
}
fun removeGroupIndex(group: Group) {
this.groupIndexes.remove(group.nodeId)
}
@@ -284,6 +282,13 @@ abstract class DatabaseVersioned<
}
}
fun updateEntryIndex(entry: Entry) {
val entryId = entry.nodeId
if (entryIndexes.containsKey(entryId)) {
entryIndexes[entryId] = entry
}
}
fun removeEntryIndex(entry: Entry) {
this.entryIndexes.remove(entry.nodeId)
}
@@ -307,6 +312,8 @@ abstract class DatabaseVersioned<
abstract fun getStandardIcon(iconId: Int): IconImageStandard
abstract fun containsCustomData(): Boolean
fun addGroupTo(newGroup: Group, parent: Group?) {
// Add tree to parent tree
parent?.addChildGroup(newGroup)
@@ -315,11 +322,7 @@ abstract class DatabaseVersioned<
}
fun updateGroup(group: Group) {
group.parent?.updateChildGroup(group)
val groupId = group.nodeId
if (groupIndexes.containsKey(groupId)) {
groupIndexes[groupId] = group
}
updateGroupIndex(group)
}
fun removeGroupFrom(groupToRemove: Group, parent: Group?) {
@@ -328,19 +331,15 @@ abstract class DatabaseVersioned<
removeGroupIndex(groupToRemove)
}
open fun addEntryTo(newEntry: Entry, parent: Group?) {
fun addEntryTo(newEntry: Entry, parent: Group?) {
// Add entry to parent
parent?.addChildEntry(newEntry)
newEntry.parent = parent
addEntryIndex(newEntry)
}
open fun updateEntry(entry: Entry) {
entry.parent?.updateChildEntry(entry)
val entryId = entry.nodeId
if (entryIndexes.containsKey(entryId)) {
entryIndexes[entryId] = entry
}
fun updateEntry(entry: Entry) {
updateEntryIndex(entry)
}
open fun removeEntryFrom(entryToRemove: Entry, parent: Group?) {

View File

@@ -21,6 +21,8 @@ package com.kunzisoft.keepass.database.element.entry
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.utils.ParcelableUtil
import com.kunzisoft.keepass.utils.UnsignedInt
class AutoType : Parcelable {
@@ -28,7 +30,7 @@ class AutoType : Parcelable {
var enabled = true
var obfuscationOptions = OBF_OPT_NONE
var defaultSequence = ""
private var windowSeqPairs = ArrayList<AutoTypeItem>()
private var windowSeqPairs = LinkedHashMap<String, String>()
constructor()
@@ -36,15 +38,16 @@ class AutoType : Parcelable {
this.enabled = autoType.enabled
this.obfuscationOptions = autoType.obfuscationOptions
this.defaultSequence = autoType.defaultSequence
this.windowSeqPairs.clear()
this.windowSeqPairs.addAll(autoType.windowSeqPairs)
for ((key, value) in autoType.windowSeqPairs) {
this.windowSeqPairs[key] = value
}
}
constructor(parcel: Parcel) {
this.enabled = parcel.readByte().toInt() != 0
this.obfuscationOptions = UnsignedInt(parcel.readInt())
this.defaultSequence = parcel.readString() ?: defaultSequence
parcel.readTypedList(this.windowSeqPairs, AutoTypeItem.CREATOR)
this.windowSeqPairs = ParcelableUtil.readStringParcelableMap(parcel)
}
override fun describeContents(): Int {
@@ -55,43 +58,15 @@ class AutoType : Parcelable {
dest.writeByte((if (enabled) 1 else 0).toByte())
dest.writeInt(obfuscationOptions.toKotlinInt())
dest.writeString(defaultSequence)
dest.writeTypedList(windowSeqPairs)
ParcelableUtil.writeStringParcelableMap(dest, windowSeqPairs)
}
fun add(key: String, value: String) {
windowSeqPairs.add(AutoTypeItem(key, value))
fun put(key: String, value: String) {
windowSeqPairs[key] = value
}
fun doForEachAutoTypeItem(action: (key: String, value: String) -> Unit) {
windowSeqPairs.forEach {
action.invoke(it.key, it.value)
}
}
private data class AutoTypeItem(var key: String, var value: String): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString() ?: "",
parcel.readString() ?: "") {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(key)
parcel.writeString(value)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<AutoTypeItem> {
override fun createFromParcel(parcel: Parcel): AutoTypeItem {
return AutoTypeItem(parcel)
}
override fun newArray(size: Int): Array<AutoTypeItem?> {
return arrayOfNulls(size)
}
}
fun entrySet(): Set<MutableMap.MutableEntry<String, String>> {
return windowSeqPairs.entries
}
companion object {

View File

@@ -90,8 +90,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
url = parcel.readString() ?: url
notes = parcel.readString() ?: notes
binaryDescription = parcel.readString() ?: binaryDescription
val rawBinaryDataId = parcel.readInt()
binaryDataId = if (rawBinaryDataId == -1) null else rawBinaryDataId
binaryDataId = parcel.readInt()
}
override fun readParentParcelable(parcel: Parcel): GroupKDB? {
@@ -110,7 +109,9 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
dest.writeString(url)
dest.writeString(notes)
dest.writeString(binaryDescription)
dest.writeInt(binaryDataId ?: -1)
binaryDataId?.let {
dest.writeInt(it)
}
}
fun updateWith(source: EntryKDB) {

View File

@@ -20,12 +20,12 @@
package com.kunzisoft.keepass.database.element.entry
import android.os.Parcel
import android.os.ParcelUuid
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.utils.UnsignedLong
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
@@ -33,7 +33,6 @@ import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.utils.ParcelableUtil
import com.kunzisoft.keepass.utils.UnsignedLong
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.LinkedHashMap
@@ -46,20 +45,42 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
@Transient
private var mDecodeRef = false
override var usageCount = UnsignedLong(0)
override var locationChanged = DateInstant()
override var customData = CustomData()
private var fields = LinkedHashMap<String, ProtectedString>()
var customData = LinkedHashMap<String, String>()
var fields = LinkedHashMap<String, ProtectedString>()
var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId>
var foregroundColor = ""
var backgroundColor = ""
var overrideURL = ""
override var tags = Tags()
override var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO
var qualityCheck = true
var autoType = AutoType()
var history = ArrayList<EntryKDBX>()
var additional = ""
var tags = ""
fun getSize(attachmentPool: AttachmentPool): Long {
var size = FIXED_LENGTH_SIZE
for (entry in fields.entries) {
size += entry.key.length.toLong()
size += entry.value.length().toLong()
}
size += getAttachmentsSize(attachmentPool)
size += autoType.defaultSequence.length.toLong()
for ((key, value) in autoType.entrySet()) {
size += key.length.toLong()
size += value.length.toLong()
}
for (entry in history) {
size += entry.getSize(attachmentPool)
}
size += overrideURL.length.toLong()
size += tags.length.toLong()
return size
}
override var expires: Boolean = false
@@ -68,42 +89,34 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
constructor(parcel: Parcel) : super(parcel) {
usageCount = UnsignedLong(parcel.readLong())
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
customData = parcel.readParcelable(CustomData::class.java.classLoader) ?: CustomData()
customData = ParcelableUtil.readStringParcelableMap(parcel)
fields = ParcelableUtil.readStringParcelableMap(parcel, ProtectedString::class.java)
binaries = ParcelableUtil.readStringIntMap(parcel)
foregroundColor = parcel.readString() ?: foregroundColor
backgroundColor = parcel.readString() ?: backgroundColor
overrideURL = parcel.readString() ?: overrideURL
tags = parcel.readParcelable(Tags::class.java.classLoader) ?: tags
previousParentGroup = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
autoType = parcel.readParcelable(AutoType::class.java.classLoader) ?: autoType
parcel.readTypedList(history, CREATOR)
url = parcel.readString() ?: url
additional = parcel.readString() ?: additional
}
override fun readParentParcelable(parcel: Parcel): GroupKDBX? {
return parcel.readParcelable(GroupKDBX::class.java.classLoader)
}
override fun writeParentParcelable(parent: GroupKDBX?, parcel: Parcel, flags: Int) {
parcel.writeParcelable(parent, flags)
tags = parcel.readString() ?: tags
}
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
dest.writeLong(usageCount.toKotlinLong())
dest.writeParcelable(locationChanged, flags)
dest.writeParcelable(customData, flags)
ParcelableUtil.writeStringParcelableMap(dest, customData)
ParcelableUtil.writeStringParcelableMap(dest, flags, fields)
ParcelableUtil.writeStringIntMap(dest, binaries)
dest.writeString(foregroundColor)
dest.writeString(backgroundColor)
dest.writeString(overrideURL)
dest.writeParcelable(tags, flags)
dest.writeParcelable(ParcelUuid(previousParentGroup), flags)
dest.writeParcelable(autoType, flags)
dest.writeTypedList(history)
dest.writeString(url)
dest.writeString(additional)
dest.writeString(tags)
}
/**
@@ -114,7 +127,9 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
super.updateWith(source)
usageCount = source.usageCount
locationChanged = DateInstant(source.locationChanged)
customData = CustomData(source.customData)
// Add all custom elements in map
customData.clear()
customData.putAll(source.customData)
fields.clear()
fields.putAll(source.fields)
binaries.clear()
@@ -122,13 +137,13 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
foregroundColor = source.foregroundColor
backgroundColor = source.backgroundColor
overrideURL = source.overrideURL
tags = source.tags
previousParentGroup = source.previousParentGroup
autoType = AutoType(source.autoType)
history.clear()
if (copyHistory)
history.addAll(source.history)
url = source.url
additional = source.additional
tags = source.tags
}
fun startToManageFieldReferences(database: DatabaseKDBX) {
@@ -149,8 +164,13 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
return NodeIdUUID(nodeId.id)
}
override val type: Type
get() = Type.ENTRY
override fun readParentParcelable(parcel: Parcel): GroupKDBX? {
return parcel.readParcelable(GroupKDBX::class.java.classLoader)
}
override fun writeParentParcelable(parent: GroupKDBX?, parcel: Parcel, flags: Int) {
parcel.writeParcelable(parent, flags)
}
/**
* Decode a reference key with the FieldReferencesEngine
@@ -158,98 +178,55 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
* @param key
* @return
*/
private fun decodeRefKey(decodeRef: Boolean, key: String, recursionLevel: Int): String {
private fun decodeRefKey(decodeRef: Boolean, key: String): String {
return fields[key]?.toString()?.let { text ->
return if (decodeRef) {
mDatabase?.getFieldReferenceValue(text, recursionLevel) ?: text
if (mDatabase == null) text else FieldReferencesEngine().compile(text, this, mDatabase!!)
} else text
} ?: ""
}
fun decodeTitleKey(recursionLevel: Int): String {
return decodeRefKey(mDecodeRef, STR_TITLE, recursionLevel)
}
override var title: String
get() = decodeTitleKey(0)
get() = decodeRefKey(mDecodeRef, STR_TITLE)
set(value) {
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectTitle
fields[STR_TITLE] = ProtectedString(protect, value)
}
fun decodeUsernameKey(recursionLevel: Int): String {
return decodeRefKey(mDecodeRef, STR_USERNAME, recursionLevel)
}
override val type: Type
get() = Type.ENTRY
override var username: String
get() = decodeUsernameKey(0)
get() = decodeRefKey(mDecodeRef, STR_USERNAME)
set(value) {
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectUserName
fields[STR_USERNAME] = ProtectedString(protect, value)
}
fun decodePasswordKey(recursionLevel: Int): String {
return decodeRefKey(mDecodeRef, STR_PASSWORD, recursionLevel)
}
override var password: String
get() = decodePasswordKey(0)
get() = decodeRefKey(mDecodeRef, STR_PASSWORD)
set(value) {
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectPassword
fields[STR_PASSWORD] = ProtectedString(protect, value)
}
fun decodeUrlKey(recursionLevel: Int): String {
return decodeRefKey(mDecodeRef, STR_URL, recursionLevel)
}
override var url
get() = decodeUrlKey(0)
get() = decodeRefKey(mDecodeRef, STR_URL)
set(value) {
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectUrl
fields[STR_URL] = ProtectedString(protect, value)
}
fun decodeNotesKey(recursionLevel: Int): String {
return decodeRefKey(mDecodeRef, STR_NOTES, recursionLevel)
}
override var notes: String
get() = decodeNotesKey(0)
get() = decodeRefKey(mDecodeRef, STR_NOTES)
set(value) {
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectNotes
fields[STR_NOTES] = ProtectedString(protect, value)
}
fun getCustomFieldValue(label: String): String {
return decodeRefKey(mDecodeRef, label, 0)
}
override var usageCount = UnsignedLong(0)
fun getSize(attachmentPool: AttachmentPool): Long {
var size = FIXED_LENGTH_SIZE
for (entry in fields.entries) {
size += entry.key.length.toLong()
size += entry.value.length().toLong()
}
size += getAttachmentsSize(attachmentPool)
size += autoType.defaultSequence.length.toLong()
autoType.doForEachAutoTypeItem { key, value ->
size += key.length.toLong()
size += value.length.toLong()
}
for (entry in history) {
size += entry.getSize(attachmentPool)
}
size += overrideURL.length.toLong()
size += tags.toString().length
return size
}
override var locationChanged = DateInstant()
fun afterChangeParent() {
locationChanged = DateInstant()
@@ -263,45 +240,25 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|| key == STR_NOTES)
}
fun doForEachDecodedCustomField(action: (field: Field) -> Unit) {
val iterator = fields.entries.iterator()
while (iterator.hasNext()) {
val mapEntry = iterator.next()
if (!isStandardField(mapEntry.key)) {
action.invoke(Field(mapEntry.key,
ProtectedString(mapEntry.value.isProtected,
decodeRefKey(mDecodeRef, mapEntry.key, 0)
)
)
)
var customFields = LinkedHashMap<String, ProtectedString>()
get() {
field.clear()
for ((key, value) in fields) {
if (!isStandardField(key)) {
field[key] = ProtectedString(value.isProtected, decodeRefKey(mDecodeRef, key))
}
}
return field
}
}
fun getFieldValue(label: String): ProtectedString? {
return fields[label]
}
fun getFields(): List<Field> {
return fields.map { Field(it.key, it.value) }
}
fun putField(field: Field) {
putField(field.name, field.protectedValue)
}
fun putField(label: String, value: ProtectedString) {
fields[label] = value
}
fun removeField(name: String) {
fields.remove(name)
}
fun removeAllFields() {
fields.clear()
}
fun putExtraField(label: String, value: ProtectedString) {
fields[label] = value
}
/**
* It's a list because history labels can be defined multiple times
*/
@@ -345,6 +302,14 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
return size
}
override fun putCustomData(key: String, value: String) {
customData[key] = value
}
override fun containsCustomData(): Boolean {
return customData.isNotEmpty()
}
fun addEntryToHistory(entry: EntryKDBX) {
history.add(entry)
}
@@ -384,8 +349,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
const val STR_URL = "URL"
const val STR_NOTES = "Notes"
private const val FIXED_LENGTH_SIZE: Long = 128 // Approximate fixed length size
fun newCustomNameAllowed(name: String): Boolean {
return !(name.equals(STR_TITLE, true)
|| name.equals(STR_USERNAME, true)
@@ -404,5 +367,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
return arrayOfNulls(size)
}
}
private const val FIXED_LENGTH_SIZE: Long = 128 // Approximate fixed length size
}
}

View File

@@ -36,10 +36,6 @@ abstract class EntryVersioned
constructor(parcel: Parcel) : super(parcel)
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
}
override fun nodeIndexInParentForNaturalOrder(): Int {
if (nodeIndexInParentForNaturalOrder == -1) {
val numberOfGroups = parent?.getChildGroups()?.size

View File

@@ -19,132 +19,269 @@
*/
package com.kunzisoft.keepass.database.element.entry
import android.util.Log
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.utils.UuidUtil
import java.util.concurrent.ConcurrentHashMap
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.search.EntryKDBXSearchHandler
import com.kunzisoft.keepass.database.search.SearchParameters
import java.util.*
class FieldReferencesEngine(private val mDatabase: DatabaseKDBX) {
class FieldReferencesEngine {
// Key : <WantedField>@<SearchIn>:<Text>
// Value : content
private var refsCache = ConcurrentHashMap<String, String?>()
inner class TargetResult(var entry: EntryKDBX?, var wanted: Char)
fun clear() {
refsCache.clear()
private inner class SprContextV4 {
var databaseV4: DatabaseKDBX? = null
var entry: EntryKDBX
var refsCache: MutableMap<String, String> = HashMap()
internal constructor(db: DatabaseKDBX, entry: EntryKDBX) {
this.databaseV4 = db
this.entry = entry
}
internal constructor(source: SprContextV4) {
this.databaseV4 = source.databaseV4
this.entry = source.entry
this.refsCache = source.refsCache
}
}
fun compile(textReference: String, recursionLevel: Int): String {
fun compile(text: String, entry: EntryKDBX, database: DatabaseKDBX): String {
return compileInternal(text, SprContextV4(database, entry), 0)
}
private fun compileInternal(text: String?, sprContextV4: SprContextV4?, recursionLevel: Int): String {
if (text == null) {
return ""
}
if (sprContextV4 == null) {
return ""
}
return if (recursionLevel >= MAX_RECURSION_DEPTH) {
""
} else
fillReferencesPlaceholders(textReference, recursionLevel)
} else fillRefPlaceholders(text, sprContextV4, recursionLevel)
}
/**
* Manage placeholders with {REF:<WantedField>@<SearchIn>:<Text>}
*/
private fun fillReferencesPlaceholders(textReference: String, recursionLevel: Int): String {
var textValue = textReference
private fun fillRefPlaceholders(textReference: String, contextV4: SprContextV4, recursionLevel: Int): String {
var text = textReference
if (contextV4.databaseV4 == null) {
return text
}
var offset = 0
var numberInlineRef = 0
while (textValue.contains(STR_REF_START)
&& numberInlineRef <= MAX_INLINE_REF) {
numberInlineRef++
for (i in 0..19) {
text = fillRefsUsingCache(text, contextV4)
try {
textValue = fillReferencesUsingCache(textValue)
val start = textValue.indexOf(STR_REF_START, offset, true)
if (start < 0) {
break
}
val end = textValue.indexOf(STR_REF_END, offset, true)
if (end <= start) {
break
}
val reference = textValue.substring(start + STR_REF_START.length, end)
val fullReference = "$STR_REF_START$reference$STR_REF_END"
if (!refsCache.containsKey(fullReference)) {
val newRecursionLevel = recursionLevel + 1
val result = findReferenceTarget(reference, newRecursionLevel)
val entryFound = result.entry
val data: String? = when (result.wanted) {
'T' -> entryFound?.decodeTitleKey(newRecursionLevel)
'U' -> entryFound?.decodeUsernameKey(newRecursionLevel)
'A' -> entryFound?.decodeUrlKey(newRecursionLevel)
'P' -> entryFound?.decodePasswordKey(newRecursionLevel)
'N' -> entryFound?.decodeNotesKey(newRecursionLevel)
'I' -> UuidUtil.toHexString(entryFound?.nodeId?.id)
else -> null
}
refsCache[fullReference] = data
textValue = fillReferencesUsingCache(textValue)
}
offset = end
} catch (e: Exception) {
Log.e(TAG, "Error when fill placeholders by reference", e)
val start = text.indexOf(STR_REF_START, offset, true)
if (start < 0) {
break
}
val end = text.indexOf(STR_REF_END, start + 1, true)
if (end <= start) {
break
}
val fullRef = text.substring(start, end + 1)
val result = findRefTarget(fullRef, contextV4)
if (result != null) {
val found = result.entry
found?.stopToManageFieldReferences()
val wanted = result.wanted
var data: String? = null
when (wanted) {
'T' -> data = found?.title
'U' -> data = found?.username
'A' -> data = found?.url
'P' -> data = found?.password
'N' -> data = found?.notes
'I' -> data = found?.nodeId.toString()
}
if (data != null && found != null) {
val subCtx = SprContextV4(contextV4)
subCtx.entry = found
val innerContent = compileInternal(data, subCtx, recursionLevel + 1)
addRefsToCache(fullRef, innerContent, contextV4)
text = fillRefsUsingCache(text, contextV4)
} else {
offset = start + 1
}
}
}
return textValue
return text
}
private fun fillReferencesUsingCache(text: String): String {
private fun findRefTarget(fullReference: String?, contextV4: SprContextV4): TargetResult? {
var fullRef: String? = fullReference ?: return null
fullRef = fullRef!!.toUpperCase(Locale.ENGLISH)
if (!fullRef.startsWith(STR_REF_START) || !fullRef.endsWith(STR_REF_END)) {
return null
}
val ref = fullRef.substring(STR_REF_START.length, fullRef.length - STR_REF_END.length)
if (ref.length <= 4) {
return null
}
if (ref[1] != '@') {
return null
}
if (ref[3] != ':') {
return null
}
val scan = Character.toUpperCase(ref[2])
val wanted = Character.toUpperCase(ref[0])
val searchParameters = SearchParameters()
searchParameters.setupNone()
searchParameters.searchString = ref.substring(4)
when (scan) {
'T' -> searchParameters.searchInTitles = true
'U' -> searchParameters.searchInUserNames = true
'A' -> searchParameters.searchInUrls = true
'P' -> searchParameters.searchInPasswords = true
'N' -> searchParameters.searchInNotes = true
'I' -> searchParameters.searchInUUIDs = true
'O' -> searchParameters.searchInOther = true
else -> return null
}
val list = ArrayList<EntryKDBX>()
searchEntries(contextV4.databaseV4?.rootGroup, searchParameters, list)
return if (list.size > 0) {
TargetResult(list[0], wanted)
} else null
}
private fun addRefsToCache(ref: String?, value: String?, ctx: SprContextV4?) {
if (ref == null) {
return
}
if (value == null) {
return
}
if (ctx == null) {
return
}
if (!ctx.refsCache.containsKey(ref)) {
ctx.refsCache[ref] = value
}
}
private fun fillRefsUsingCache(text: String, sprContextV4: SprContextV4): String {
var newText = text
refsCache.keys.forEach { key ->
// Replace by key if value not found
newText = newText.replace(key, refsCache[key] ?: key, true)
for ((key, value) in sprContextV4.refsCache) {
newText = text.replace(key, value, true)
}
return newText
}
private fun findReferenceTarget(reference: String, recursionLevel: Int): TargetResult {
val targetResult = TargetResult(null, 'J')
if (reference.length <= 4) {
return targetResult
private fun searchEntries(root: GroupKDBX?, searchParameters: SearchParameters?, listStorage: MutableList<EntryKDBX>?) {
if (searchParameters == null) {
return
}
if (reference[1] != '@') {
return targetResult
}
if (reference[3] != ':') {
return targetResult
if (listStorage == null) {
return
}
targetResult.wanted = Character.toUpperCase(reference[0])
val searchIn = Character.toUpperCase(reference[2])
val searchQuery = reference.substring(4)
targetResult.entry = when (searchIn) {
'T' -> mDatabase.getEntryByTitle(searchQuery, recursionLevel)
'U' -> mDatabase.getEntryByUsername(searchQuery, recursionLevel)
'A' -> mDatabase.getEntryByURL(searchQuery, recursionLevel)
'P' -> mDatabase.getEntryByPassword(searchQuery, recursionLevel)
'N' -> mDatabase.getEntryByNotes(searchQuery, recursionLevel)
'I' -> {
UuidUtil.fromHexString(searchQuery)?.let { uuid ->
mDatabase.getEntryById(NodeIdUUID(uuid))
val terms = splitStringTerms(searchParameters.searchString)
if (terms.size <= 1 || searchParameters.regularExpression) {
root!!.doForEachChild(EntryKDBXSearchHandler(searchParameters, listStorage), null)
return
}
// Search longest term first
val stringLengthComparator = Comparator<String> { lhs, rhs -> lhs.length - rhs.length }
Collections.sort(terms, stringLengthComparator)
val fullSearch = searchParameters.searchString
var childEntries: List<EntryKDBX>? = root!!.getChildEntries()
for (i in terms.indices) {
val pgNew = ArrayList<EntryKDBX>()
searchParameters.searchString = terms[i]
var negate = false
if (searchParameters.searchString.startsWith("-")) {
searchParameters.searchString = searchParameters.searchString.substring(1)
negate = searchParameters.searchString.isNotEmpty()
}
if (!root.doForEachChild(EntryKDBXSearchHandler(searchParameters, pgNew), null)) {
childEntries = null
break
}
childEntries = if (negate) {
val complement = ArrayList<EntryKDBX>()
for (entry in childEntries!!) {
if (!pgNew.contains(entry)) {
complement.add(entry)
}
}
complement
} else {
pgNew
}
}
if (childEntries != null) {
listStorage.addAll(childEntries)
}
searchParameters.searchString = fullSearch
}
/**
* Create a list of String by split text when ' ', '\t', '\r' or '\n' is found
*/
private fun splitStringTerms(text: String?): List<String> {
val list = ArrayList<String>()
if (text == null) {
return list
}
val stringBuilder = StringBuilder()
var quoted = false
for (element in text) {
if ((element == ' ' || element == '\t' || element == '\r' || element == '\n') && !quoted) {
val len = stringBuilder.length
when {
len > 0 -> {
list.add(stringBuilder.toString())
stringBuilder.delete(0, len)
}
element == '\"' -> quoted = !quoted
else -> stringBuilder.append(element)
}
}
'O' -> mDatabase.getEntryByCustomData(searchQuery)
else -> null
}
return targetResult
}
private data class TargetResult(var entry: EntryKDBX?, var wanted: Char)
if (stringBuilder.isNotEmpty()) {
list.add(stringBuilder.toString())
}
return list
}
companion object {
private const val MAX_RECURSION_DEPTH = 10
private const val MAX_INLINE_REF = 10
private const val MAX_RECURSION_DEPTH = 12
private const val STR_REF_START = "{REF:"
private const val STR_REF_END = "}"
private val TAG = FieldReferencesEngine::class.java.name
}
}

View File

@@ -31,12 +31,14 @@ import java.util.*
class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface {
var level = 0 // short
// Used by KeePass internally, don't use
var groupFlags = 0
constructor() : super()
constructor(parcel: Parcel) : super(parcel) {
level = parcel.readInt()
groupFlags = parcel.readInt()
}
@@ -50,11 +52,13 @@ class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
dest.writeInt(level)
dest.writeInt(groupFlags)
}
fun updateWith(source: GroupKDB) {
super.updateWith(source)
level = source.level
groupFlags = source.groupFlags
}
@@ -69,12 +73,15 @@ class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
return NodeIdInt(nodeId.id)
}
override fun afterAssignNewParent() {
if (parent != null)
level = parent!!.level + 1
}
fun setGroupId(groupId: Int) {
this.nodeId = NodeIdInt(groupId)
}
override fun afterAssignNewParent() {}
companion object {
@JvmField

View File

@@ -20,11 +20,8 @@
package com.kunzisoft.keepass.database.element.group
import android.os.Parcel
import android.os.ParcelUuid
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.CustomData
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Tags
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.node.NodeId
@@ -36,17 +33,14 @@ import java.util.*
class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
override var usageCount = UnsignedLong(0)
override var locationChanged = DateInstant()
override var customData = CustomData()
private val customData = HashMap<String, String>()
var notes = ""
var isExpanded = true
var defaultAutoTypeSequence = ""
var enableAutoType: Boolean? = null
var enableSearching: Boolean? = null
var lastTopVisibleEntry: UUID = DatabaseVersioned.UUID_ZERO
override var tags = Tags()
override var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO
override var expires: Boolean = false
@@ -66,7 +60,7 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
constructor(parcel: Parcel) : super(parcel) {
usageCount = UnsignedLong(parcel.readLong())
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
customData = parcel.readParcelable(CustomData::class.java.classLoader) ?: CustomData()
// TODO customData = ParcelableUtil.readStringParcelableMap(parcel);
notes = parcel.readString() ?: notes
isExpanded = parcel.readByte().toInt() != 0
defaultAutoTypeSequence = parcel.readString() ?: defaultAutoTypeSequence
@@ -75,8 +69,6 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
val isSearchingEnabled = parcel.readInt()
enableSearching = if (isSearchingEnabled == -1) null else isSearchingEnabled == 1
lastTopVisibleEntry = parcel.readSerializable() as UUID
tags = parcel.readParcelable(Tags::class.java.classLoader) ?: tags
previousParentGroup = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
}
override fun readParentParcelable(parcel: Parcel): GroupKDBX? {
@@ -91,15 +83,13 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
super.writeToParcel(dest, flags)
dest.writeLong(usageCount.toKotlinLong())
dest.writeParcelable(locationChanged, flags)
dest.writeParcelable(customData, flags)
// TODO ParcelableUtil.writeStringParcelableMap(dest, customData);
dest.writeString(notes)
dest.writeByte((if (isExpanded) 1 else 0).toByte())
dest.writeString(defaultAutoTypeSequence)
dest.writeInt(if (enableAutoType == null) -1 else if (enableAutoType!!) 1 else 0)
dest.writeInt(if (enableSearching == null) -1 else if (enableSearching!!) 1 else 0)
dest.writeSerializable(lastTopVisibleEntry)
dest.writeParcelable(tags, flags)
dest.writeParcelable(ParcelUuid(previousParentGroup), flags)
}
fun updateWith(source: GroupKDBX) {
@@ -107,21 +97,34 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
usageCount = source.usageCount
locationChanged = DateInstant(source.locationChanged)
// Add all custom elements in map
customData = CustomData(source.customData)
customData.clear()
for ((key, value) in source.customData) {
customData[key] = value
}
notes = source.notes
isExpanded = source.isExpanded
defaultAutoTypeSequence = source.defaultAutoTypeSequence
enableAutoType = source.enableAutoType
enableSearching = source.enableSearching
lastTopVisibleEntry = source.lastTopVisibleEntry
tags = source.tags
previousParentGroup = source.previousParentGroup
}
override var usageCount = UnsignedLong(0)
override var locationChanged = DateInstant()
override fun afterAssignNewParent() {
locationChanged = DateInstant()
}
override fun putCustomData(key: String, value: String) {
customData[key] = value
}
override fun containsCustomData(): Boolean {
return customData.isNotEmpty()
}
companion object {
@JvmField

View File

@@ -63,17 +63,6 @@ abstract class GroupVersioned
get() = titleGroup
set(value) { titleGroup = value }
/**
* To determine the level from the root group (root group level is -1)
*/
fun getLevel(): Int {
var level = -1
parent?.let { parent ->
level = parent.getLevel() + 1
}
return level
}
override fun getChildGroups(): List<Group> {
return childGroups
}
@@ -98,24 +87,6 @@ abstract class GroupVersioned
this.childEntries.add(entry)
}
override fun updateChildGroup(group: Group) {
val index = this.childGroups.indexOfFirst { it.nodeId == group.nodeId }
if (index >= 0) {
val oldGroup = this.childGroups.removeAt(index)
group.nodeIndexInParentForNaturalOrder = oldGroup.nodeIndexInParentForNaturalOrder
this.childGroups.add(index, group)
}
}
override fun updateChildEntry(entry: Entry) {
val index = this.childEntries.indexOfFirst { it.nodeId == entry.nodeId }
if (index >= 0) {
val oldEntry = this.childEntries.removeAt(index)
entry.nodeIndexInParentForNaturalOrder = oldEntry.nodeIndexInParentForNaturalOrder
this.childEntries.add(index, entry)
}
}
override fun removeChildGroup(group: Group) {
this.childGroups.remove(group)
}
@@ -135,4 +106,8 @@ abstract class GroupVersioned
else
nodeIndexInParentForNaturalOrder
}
override fun toString(): String {
return titleGroup
}
}

Some files were not shown because too many files have changed in this diff Show More