Compare commits

..

1 Commits

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

View File

@@ -24,7 +24,6 @@ A clear and concise description of what you expected to happen.
- Created with: [e.g Windows KeePass 2.42] - Created with: [e.g Windows KeePass 2.42]
- Version: [e.g. 2] - Version: [e.g. 2]
- Location: [e.g. Remote file retrieved with GDrive app] - Location: [e.g. Remote file retrieved with GDrive app]
- File provider (`content://` URI): [e.g. `content://com.google.android.apps.docs.storage/5`]
- Size: [e.g. 150Mo] - Size: [e.g. 150Mo]
- Contains attachment: [e.g. Yes] - Contains attachment: [e.g. Yes]

View File

@@ -1,102 +1,6 @@
KeePassDX(3.1.0)
* Add breadcrumb
* Add path in search results #1148
* Add group info dialog #1177
* Manage colors #64 #913
* Fix UI in Android 8 #509
* Upgrade libs and SDK to 31 #833
* Fix parser of database v1 #1201
* Stop asking WRITE_EXTERNAL_STORAGE permission
KeePassDX(3.0.4)
* Fix autofill inline bugs #1173 #1165
* Small UI change
KeePassDX(3.0.3)
* Change default Argon2 parameters #1098
* Add & edit custom icon name #976
* Fix templates #1128 #1133 #1138
* Update Autofill compatibility list #725 #1154
* Improve fingerprint usage #1137 #1145
* Change backup configuration #1144
* Add lock button in database notification
KeePassDX(3.0.2)
* Samsung DeX mode #1114 #245 (Thx @chenxiaolong)
KeePassDX(3.0.1)
* Fix text size and smallest margin #1085
* Fix number of lines during an edition #1073
* Fix Magikeyboard URL auto action #1100
* Fix exception after group name change and save #1112
* Fix timeout reset #1107
* Fix search actions #1091 #1092
* Small changes #1106 #1085
KeePassDX(3.0.0)
* Add / Manage dynamic templates #191
* Manually select RecycleBin group and Templates group #191
* Setting to display OTP Token in list #655
* Fix timeout in dialogs #716
* Check URI permissions #626
* Better autofill implementation #943 #946 #984 #1070 (Thx @uduerholz)
* Improvements #680 #1035 #1043 #942 #1021 #1027 #1046 #1082 #1083 (Thx @chenxiaolong)
KeePassDX(2.10.5)
* Increase the saving speed of database #1028
* 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) KeePassDX(2.9.15)
* Fix themes #935 #926 * Fix themes #935
* Decrease default clipboard time #934 * Decrease default clipboard time #934
* Better opening performance #929 #933
* Fix memory usage setting #941
KeePassDX(2.9.14) KeePassDX(2.9.14)
* Add custom icons #96 * Add custom icons #96

View File

@@ -15,7 +15,6 @@
- Material design with **themes**. - Material design with **themes**.
- **Auto-Fill** and Integration. - **Auto-Fill** and Integration.
- Field filling **keyboard**. - Field filling **keyboard**.
- Dynamic **templates**
- **History** of each entry. - **History** of each entry.
- Precise management of **settings**. - Precise management of **settings**.
- Code written in **native languages** *(Kotlin / Java / JNI / C)*. - Code written in **native languages** *(Kotlin / Java / JNI / C)*.
@@ -72,7 +71,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
## License ## License
Copyright © 2022 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com). Copyright © 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
This file is part of KeePassDX. This file is part of KeePassDX.

View File

@@ -3,16 +3,16 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
android { android {
compileSdkVersion 31 compileSdkVersion 30
buildToolsVersion "31.0.0" buildToolsVersion "30.0.3"
ndkVersion "21.4.7075529" ndkVersion "21.4.7075529"
defaultConfig { defaultConfig {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 31 targetSdkVersion 30
versionCode = 92 versionCode = 66
versionName = "3.1.0" versionName = "2.9.15"
multiDexEnabled true multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests" testApplicationId = "com.kunzisoft.keepass.tests"
@@ -99,43 +99,44 @@ android {
} }
} }
def room_version = "2.4.1" def room_version = "2.2.6"
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.appcompat:appcompat:$android_appcompat_version" implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3' implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.1.0' implementation 'androidx.biometric:biometric:1.1.0-rc01'
implementation 'androidx.media:media:1.4.3'
// Lifecycle - LiveData - ViewModel - Coroutines // Lifecycle - LiveData - ViewModel - Coroutines
implementation "androidx.core:core-ktx:$android_core_version" implementation "androidx.core:core-ktx:1.3.2"
implementation 'androidx.fragment:fragment-ktx:1.4.0' implementation 'androidx.fragment:fragment-ktx:1.2.5'
implementation "com.google.android.material:material:$android_material_version" // WARNING: Don't upgrade because slowdown https://github.com/Kunzisoft/KeePassDX/issues/923
implementation 'com.google.android.material:material:1.1.0'
// Database // Database
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
// Autofill // Autofill
implementation "androidx.autofill:autofill:1.1.0" implementation "androidx.autofill:autofill:1.1.0"
// Time // Time
implementation 'joda-time:joda-time:2.10.13' implementation 'joda-time:joda-time:2.10.6'
// Color // Color
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.6' implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
// Education // Education
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3' implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
// Apache Commons // Apache Commons
implementation 'commons-io:commons-io:2.8.0' implementation 'commons-io:commons-io:2.8.0'
implementation 'commons-codec:commons-codec:1.15' implementation 'commons-codec:commons-codec:1.15'
// Encrypt lib // Encrypt lib
implementation project(path: ':crypto') implementation project(path: ':crypto')
implementation fileTree(include: ['encrypt.aar'], dir: 'libs')
// Icon pack // Icon pack
implementation project(path: ':icon-pack-classic') implementation project(path: ':icon-pack-classic')
implementation project(path: ':icon-pack-material') implementation project(path: ':icon-pack-material')
// Tests // Tests
androidTestImplementation "androidx.test:runner:$android_test_version" androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation "androidx.test:rules:$android_test_version" androidTestImplementation 'androidx.test:rules:1.3.0'
} }

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

@@ -10,12 +10,15 @@
android:anyDensity="true" /> android:anyDensity="true" />
<uses-permission <uses-permission
android:name="android.permission.FOREGROUND_SERVICE" /> android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission <uses-permission
android:name="android.permission.USE_BIOMETRIC" /> android:name="android.permission.USE_BIOMETRIC" />
<uses-permission <uses-permission
android:name="android.permission.VIBRATE"/> android:name="android.permission.VIBRATE"/>
<!-- Write permission until Android 10 -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"
tools:ignore="ScopedStorage" />
<!-- Open apps from links --> <!-- Open apps from links -->
<uses-permission <uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES" android:name="android.permission.QUERY_ALL_PACKAGES"
@@ -27,13 +30,12 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:name="com.kunzisoft.keepass.app.App" android:name="com.kunzisoft.keepass.app.App"
android:allowBackup="true" android:allowBackup="true"
android:fullBackupContent="@xml/old_backup_rules" android:fullBackupContent="@xml/backup_rules"
android:dataExtractionRules="@xml/backup_rules"
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent" android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
android:largeHeap="true" android:largeHeap="true"
android:resizeableActivity="true" android:resizeableActivity="true"
android:theme="@style/KeepassDXStyle.Night" android:theme="@style/KeepassDXStyle.Night"
tools:targetApi="s"> tools:targetApi="n">
<meta-data <meta-data
android:name="com.google.android.backup.api_key" android:name="com.google.android.backup.api_key"
android:value="${googleAndroidBackupAPIKey}" /> android:value="${googleAndroidBackupAPIKey}" />
@@ -42,9 +44,8 @@
android:theme="@style/KeepassDXStyle.SplashScreen" android:theme="@style/KeepassDXStyle.SplashScreen"
android:label="@string/app_name" android:label="@string/app_name"
android:launchMode="singleTop" android:launchMode="singleTop"
android:exported="true"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:windowSoftInputMode="stateHidden|stateAlwaysHidden" > android:windowSoftInputMode="stateHidden" >
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
@@ -52,7 +53,6 @@
</activity> </activity>
<activity <activity
android:name="com.kunzisoft.keepass.activities.PasswordActivity" android:name="com.kunzisoft.keepass.activities.PasswordActivity"
android:exported="true"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:windowSoftInputMode="adjustResize|stateUnchanged"> android:windowSoftInputMode="adjustResize|stateUnchanged">
<intent-filter> <intent-filter>
@@ -111,9 +111,9 @@
<!-- Main Activity --> <!-- Main Activity -->
<activity <activity
android:name="com.kunzisoft.keepass.activities.GroupActivity" android:name="com.kunzisoft.keepass.activities.GroupActivity"
android:exported="false"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:windowSoftInputMode="adjustPan"> android:windowSoftInputMode="adjustPan"
android:launchMode="singleTask">
<meta-data <meta-data
android:name="android.app.default_searchable" android:name="android.app.default_searchable"
android:value="com.kunzisoft.keepass.search.SearchResults" android:value="com.kunzisoft.keepass.search.SearchResults"
@@ -155,8 +155,7 @@
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" /> android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
<activity <activity
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity" android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
android:theme="@style/Theme.Transparent" android:theme="@style/Theme.Transparent">
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@@ -175,8 +174,7 @@
android:theme="@style/Theme.Transparent" /> android:theme="@style/Theme.Transparent" />
<activity <activity
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity" android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
android:label="@string/keyboard_setting_label" android:label="@string/keyboard_setting_label">
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
</intent-filter> </intent-filter>
@@ -202,7 +200,6 @@
<service <service
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService" android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
android:label="@string/autofill_service_name" android:label="@string/autofill_service_name"
android:exported="true"
android:permission="android.permission.BIND_AUTOFILL_SERVICE"> android:permission="android.permission.BIND_AUTOFILL_SERVICE">
<meta-data <meta-data
android:name="android.autofill" android:name="android.autofill"
@@ -212,9 +209,8 @@
</intent-filter> </intent-filter>
</service> </service>
<service <service
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService" android:name="com.kunzisoft.keepass.magikeyboard.MagikIME"
android:label="@string/keyboard_label" android:label="@string/keyboard_label"
android:exported="true"
android:permission="android.permission.BIND_INPUT_METHOD" > android:permission="android.permission.BIND_INPUT_METHOD" >
<meta-data android:name="android.view.im" <meta-data android:name="android.view.im"
android:resource="@xml/keyboard_method"/> android:resource="@xml/keyboard_method"/>
@@ -226,14 +222,6 @@
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService" android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />
<receiver
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.app.action.ENTER_KNOX_DESKTOP_MODE" />
<action android:name="android.app.action.EXIT_KNOX_DESKTOP_MODE" />
</intent-filter>
</receiver>
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" /> <meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
</application> </application>

View File

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

View File

@@ -23,79 +23,45 @@ import android.app.Activity
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentSender
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST
import com.kunzisoft.keepass.autofill.KeeAutofillService import com.kunzisoft.keepass.autofill.KeeAutofillService
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : DatabaseModeActivity() { class AutofillLauncherActivity : AppCompatActivity() {
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? = override fun onCreate(savedInstanceState: Bundle?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
AutofillHelper.buildActivityResultLauncher(this, true)
else null
override fun applyCustomStyle(): Boolean {
return false
}
override fun finishActivityIfReloadRequested(): Boolean {
return true
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
// Retrieve selection mode // Retrieve selection mode
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode -> EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
when (specialMode) { when (specialMode) {
SpecialMode.SELECTION -> { SpecialMode.SELECTION -> {
intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle -> // Build search param
// To pass extra inline request val searchInfo = SearchInfo().apply {
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
compatInlineSuggestionsRequest = bundle.getParcelable(KEY_INLINE_SUGGESTION) webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME)
} }
// Build search param SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
bundle.getParcelable<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo -> searchInfo.webDomain = concreteWebDomain
SearchInfo.getConcreteWebDomain( launchSelection(searchInfo)
this,
searchInfo.webDomain
) { concreteWebDomain ->
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val assistStructure = AutofillHelper
.retrieveAutofillComponent(intent)
?.assistStructure
val newAutofillComponent = if (assistStructure != null) {
AutofillComponent(
assistStructure,
compatInlineSuggestionsRequest
)
} else {
null
}
searchInfo.webDomain = concreteWebDomain
launchSelection(database, newAutofillComponent, searchInfo)
}
}
} }
// Remove bundle
intent.removeExtra(KEY_SELECTION_BUNDLE)
} }
SpecialMode.REGISTRATION -> { SpecialMode.REGISTRATION -> {
// To register info // To register info
@@ -103,7 +69,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
val searchInfo = SearchInfo(registerInfo?.searchInfo) val searchInfo = SearchInfo(registerInfo?.searchInfo)
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain -> SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain searchInfo.webDomain = concreteWebDomain
launchRegistration(database, searchInfo, registerInfo) launchRegistration(searchInfo, registerInfo)
} }
} }
else -> { else -> {
@@ -113,11 +79,14 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
} }
} }
} }
super.onCreate(savedInstanceState)
} }
private fun launchSelection(database: Database?, private fun launchSelection(searchInfo: SearchInfo) {
autofillComponent: AutofillComponent?, // Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
searchInfo: SearchInfo) { val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
if (autofillComponent == null) { if (autofillComponent == null) {
setResult(Activity.RESULT_CANCELED) setResult(Activity.RESULT_CANCELED)
finish() finish()
@@ -129,28 +98,28 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
setResult(Activity.RESULT_CANCELED) setResult(Activity.RESULT_CANCELED)
finish() finish()
} else { } else {
val database = Database.getInstance()
val readOnly = database.isReadOnly
// If database is open // If database is open
SearchHelper.checkAutoSearchInfo(this, SearchHelper.checkAutoSearchInfo(this,
database, Database.getInstance(),
searchInfo, searchInfo,
{ openedDatabase, items -> { items ->
// Items found // Items found
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items) AutofillHelper.buildResponseAndSetResult(this, items)
finish() finish()
}, },
{ openedDatabase -> {
// Show the database UI to select the entry // Show the database UI to select the entry
GroupActivity.launchForAutofillResult(this, GroupActivity.launchForAutofillResult(this,
openedDatabase, readOnly,
mAutofillActivityResultLauncher, autofillComponent,
autofillComponent, searchInfo,
searchInfo, false)
false)
}, },
{ {
// If database not open // If database not open
FileDatabaseSelectActivity.launchForAutofillResult(this, FileDatabaseSelectActivity.launchForAutofillResult(this,
mAutofillActivityResultLauncher,
autofillComponent, autofillComponent,
searchInfo) searchInfo)
} }
@@ -158,9 +127,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
} }
} }
private fun launchRegistration(database: Database?, private fun launchRegistration(searchInfo: SearchInfo, registerInfo: RegisterInfo?) {
searchInfo: SearchInfo,
registerInfo: RegisterInfo?) {
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId, if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
PreferencesUtil.applicationIdBlocklist(this)) PreferencesUtil.applicationIdBlocklist(this))
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain, || !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
@@ -168,26 +135,25 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
showBlockRestartMessage() showBlockRestartMessage()
setResult(Activity.RESULT_CANCELED) setResult(Activity.RESULT_CANCELED)
} else { } else {
val readOnly = database?.isReadOnly != false val database = Database.getInstance()
val readOnly = database.isReadOnly
SearchHelper.checkAutoSearchInfo(this, SearchHelper.checkAutoSearchInfo(this,
database, database,
searchInfo, searchInfo,
{ openedDatabase, _ -> { _ ->
if (!readOnly) { if (!readOnly) {
// Show the database UI to select the entry // Show the database UI to select the entry
GroupActivity.launchForRegistration(this, GroupActivity.launchForRegistration(this,
openedDatabase, registerInfo)
registerInfo)
} else { } else {
showReadOnlySaveMessage() showReadOnlySaveMessage()
} }
}, },
{ openedDatabase -> {
if (!readOnly) { if (!readOnly) {
// Show the database UI to select the entry // Show the database UI to select the entry
GroupActivity.launchForRegistration(this, GroupActivity.launchForRegistration(this,
openedDatabase, registerInfo)
registerInfo)
} else { } else {
showReadOnlySaveMessage() showReadOnlySaveMessage()
} }
@@ -211,47 +177,53 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show() Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show()
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
if (PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
// Close the database
sendBroadcast(Intent(LOCK_ACTION))
}
super.onActivityResult(requestCode, resultCode, data)
}
companion object { companion object {
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE" private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO" private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION" private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME"
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO" private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
fun getPendingIntentForSelection(context: Context, fun getAuthIntentSenderForSelection(context: Context,
searchInfo: SearchInfo? = null, searchInfo: SearchInfo? = null,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent { inlineSuggestionsRequest: InlineSuggestionsRequest? = null): IntentSender {
return PendingIntent.getActivity(context, 0, return PendingIntent.getActivity(context, 0,
// Doesn't work with direct extra Parcelable (don't know why?) // Doesn't work with Parcelable (don't know why?)
// Wrap into a bundle to bypass the problem Intent(context, AutofillLauncherActivity::class.java).apply {
Intent(context, AutofillLauncherActivity::class.java).apply { searchInfo?.let {
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply { putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
putParcelable(KEY_SEARCH_INFO, searchInfo) putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { putExtra(KEY_SEARCH_SCHEME, it.webScheme)
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
} }
}) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
}, inlineSuggestionsRequest?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT }
} else { }
PendingIntent.FLAG_CANCEL_CURRENT },
}) PendingIntent.FLAG_CANCEL_CURRENT).intentSender
} }
fun getPendingIntentForRegistration(context: Context, fun getAuthIntentSenderForRegistration(context: Context,
registerInfo: RegisterInfo): PendingIntent { registerInfo: RegisterInfo): IntentSender {
return PendingIntent.getActivity(context, 0, return PendingIntent.getActivity(context, 0,
Intent(context, AutofillLauncherActivity::class.java).apply { Intent(context, AutofillLauncherActivity::class.java).apply {
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION) EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
putExtra(KEY_REGISTER_INFO, registerInfo) putExtra(KEY_REGISTER_INFO, registerInfo)
}, },
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { PendingIntent.FLAG_CANCEL_CURRENT).intentSender
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
})
} }
fun launchForRegistration(context: Context, fun launchForRegistration(context: Context,

View File

@@ -32,83 +32,68 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.ProgressBar import android.widget.ProgressBar
import androidx.activity.result.ActivityResultLauncher import android.widget.Toast
import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import androidx.core.graphics.ColorUtils
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.CollapsingToolbarLayout import com.google.android.material.appbar.CollapsingToolbarLayout
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.fragments.EntryFragment import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode 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.Attachment
import com.kunzisoft.keepass.database.element.Database 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.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryActivityEducation 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.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.AttachmentFileNotificationService
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService 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_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.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.view.EntryContentsView
import com.kunzisoft.keepass.utils.UuidUtil
import com.kunzisoft.keepass.view.changeControlColor
import com.kunzisoft.keepass.view.changeTitleColor
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.showActionErrorIfNeeded import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.EntryViewModel
import java.util.* import java.util.*
import kotlin.collections.HashMap
class EntryActivity : DatabaseLockActivity() { class EntryActivity : LockingActivity() {
private var coordinatorLayout: CoordinatorLayout? = null private var coordinatorLayout: CoordinatorLayout? = null
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var appBarLayout: AppBarLayout? = null
private var titleIconView: ImageView? = null private var titleIconView: ImageView? = null
private var historyView: View? = null private var historyView: View? = null
private var entryProgress: LinearProgressIndicator? = null private var entryContentsView: EntryContentsView? = null
private var entryProgress: ProgressBar? = null
private var lockView: View? = null private var lockView: View? = null
private var toolbar: Toolbar? = 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 mEntry: Entry? = null
private var mHistoryPosition: Int = -1
private var mEntryIsHistory: Boolean = false private var mIsHistory: Boolean = false
private var mUrl: String? = null private var mEntryLastVersion: Entry? = null
private var mEntryLoaded = false private var mEntryHistoryPosition: Int = -1
private var mShowPassword: Boolean = false
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mExternalFileHelper: ExternalFileHelper? = null private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
private var mAttachmentSelected: Attachment? = null
private var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) { private var clipboardHelper: ClipboardHelper? = null
// Reload the current id from database private var mFirstLaunchOfActivity: Boolean = false
mEntryViewModel.loadDatabase(mDatabase)
}
private var mIcon: IconImage? = null private var iconColor: Int = 0
private var mColorAccent: Int = 0
private var mControlColor: Int = 0
private var mColorPrimary: Int = 0
private var mColorBackground: Int = 0
private var mBackgroundColor: Int? = null
private var mForegroundColor: Int? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -120,178 +105,60 @@ class EntryActivity : DatabaseLockActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(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 // Get views
coordinatorLayout = findViewById(R.id.toolbar_coordinator) coordinatorLayout = findViewById(R.id.toolbar_coordinator)
collapsingToolbarLayout = findViewById(R.id.toolbar_layout) collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
appBarLayout = findViewById(R.id.app_bar)
titleIconView = findViewById(R.id.entry_icon) titleIconView = findViewById(R.id.entry_icon)
historyView = findViewById(R.id.history_container) 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) entryProgress = findViewById(R.id.entry_progress)
lockView = findViewById(R.id.lock_button) lockView = findViewById(R.id.lock_button)
loadingView = findViewById(R.id.loading)
// Empty title
collapsingToolbarLayout?.title = " "
toolbar?.title = " "
// Retrieve the textColor to tint the toolbar
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
val taControlColor = theme.obtainStyledAttributes(intArrayOf(R.attr.toolbarColorControl))
val taColorPrimary = theme.obtainStyledAttributes(intArrayOf(R.attr.colorPrimary))
val taColorBackground = theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
mColorAccent = taColorAccent.getColor(0, Color.BLACK)
mControlColor = taControlColor.getColor(0, Color.BLACK)
mColorPrimary = taColorPrimary.getColor(0, Color.BLACK)
mColorBackground = taColorBackground.getColor(0, Color.BLACK)
taColorAccent.recycle()
taControlColor.recycle()
taColorPrimary.recycle()
taColorBackground.recycle()
// Get Entry from UUID
try {
intent.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { mainEntryId ->
intent.removeExtra(KEY_ENTRY)
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
intent.removeExtra(KEY_ENTRY_HISTORY_POSITION)
mEntryViewModel.loadEntry(mDatabase, mainEntryId, historyPosition)
}
} catch (e: ClassCastException) {
Log.e(TAG, "Unable to retrieve the entry key")
}
// Init SAF manager
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
mAttachmentSelected?.let { attachment ->
if (createdFileUri != null) {
mAttachmentFileBinderManager
?.startDownloadAttachment(createdFileUri, attachment)
}
mAttachmentSelected = null
}
}
// Init attachment service binder manager
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
lockView?.setOnClickListener { lockView?.setOnClickListener {
lockAndExit() lockAndExit()
} }
mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory -> // Focus view to reinitialize timeout
if (entryInfoHistory != null) { coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
this.mMainEntryId = entryInfoHistory.mainEntryId
// Manage history position // Init the clipboard helper
val historyPosition = entryInfoHistory.historyPosition clipboardHelper = ClipboardHelper(this)
this.mHistoryPosition = historyPosition mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
val entryIsHistory = historyPosition > -1
this.mEntryIsHistory = entryIsHistory // Init attachment service binder manager
// Assign history dedicated view mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
if (entryIsHistory) { mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
collapsingToolbarLayout?.contentScrim = when (actionTask) {
ColorDrawable(mColorAccent) ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
// Close the current activity after an history action
if (result.isSuccess)
finish()
} }
ACTION_DATABASE_RELOAD_TASK -> {
val entryInfo = entryInfoHistory.entryInfo // Close the current activity
// Manage entry copy to start notification if allowed (at the first start) this.showActionErrorIfNeeded(result)
if (savedInstanceState == null) {
// Manage entry to launch copying notification if allowed
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
}
}
// Assign title icon
mIcon = entryInfo.icon
// Assign title text
val entryTitle =
if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id)
collapsingToolbarLayout?.title = entryTitle
toolbar?.title = entryTitle
mUrl = entryInfo.url
// Assign colors
mBackgroundColor = entryInfo.backgroundColor
mForegroundColor = entryInfo.foregroundColor
loadingView?.hideByFading()
mEntryLoaded = true
} else {
finish()
}
// Refresh Menu
invalidateOptionsMenu()
}
mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement ->
if (otpElement == null) {
entryProgress?.visibility = View.GONE
} else 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
setProgressCompat(otpElement.secondsRemaining, true)
visibility = View.VISIBLE
}
}
}
}
mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected ->
mAttachmentSelected = attachmentSelected
mExternalFileHelper?.createDocument(attachmentSelected.name)
}
mEntryViewModel.historySelected.observe(this) { historySelected ->
mDatabase?.let { database ->
launch(
this,
database,
historySelected.nodeId,
historySelected.historyPosition,
mEntryActivityResultLauncher
)
}
}
}
override fun finishActivityIfReloadRequested(): Boolean {
return true
}
override fun viewToInvalidateTimeout(): View? {
return coordinatorLayout
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
mEntryViewModel.loadDatabase(database)
}
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)
finish() finish()
}
} }
coordinatorLayout?.showActionErrorIfNeeded(result)
} }
coordinatorLayout?.showActionErrorIfNeeded(result)
} }
override fun onResume() { override fun onResume() {
@@ -304,14 +171,63 @@ class EntryActivity : DatabaseLockActivity() {
View.GONE 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 { mAttachmentFileBinderManager?.apply {
registerProgressTask() registerProgressTask()
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener { onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) { override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
mEntryViewModel.onAttachmentAction(entryAttachmentState) if (entryAttachmentState.streamDirection != StreamDirection.UPLOAD) {
entryContentsView?.putAttachment(entryAttachmentState)
}
} }
} }
} }
mFirstLaunchOfActivity = false
} }
override fun onPause() { override fun onPause() {
@@ -320,83 +236,212 @@ class EntryActivity : DatabaseLockActivity() {
super.onPause() super.onPause()
} }
private fun applyToolbarColors() { private fun fillEntryDataInContentsView(entry: Entry) {
appBarLayout?.setBackgroundColor(mBackgroundColor ?: mColorPrimary)
collapsingToolbarLayout?.contentScrim = ColorDrawable(mBackgroundColor ?: mColorPrimary) val entryInfo = entry.getEntryInfo(mDatabase)
val backgroundDarker = if (mBackgroundColor != null) {
ColorUtils.blendARGB(mBackgroundColor!!, Color.WHITE, 0.1f) // Assign title icon
} else { titleIconView?.let { iconView ->
mColorBackground mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, iconColor)
} }
titleIconView?.background?.colorFilter = BlendModeColorFilterCompat
.createBlendModeColorFilterCompat(backgroundDarker, BlendModeCompat.SRC_IN) // Assign title text
mIcon?.let { icon -> val entryTitle = entryInfo.title
titleIconView?.let { iconView -> collapsingToolbarLayout?.title = entryTitle
mIconDrawableFactory?.assignDatabaseIcon( toolbar?.title = entryTitle
iconView,
icon, // Assign basic fields
mForegroundColor ?: mColorAccent 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))
) )
} }
} }
toolbar?.changeControlColor(mForegroundColor ?: mControlColor)
collapsingToolbarLayout?.changeTitleColor(mForegroundColor ?: mControlColor) 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 ->
// Not directly get the entry from intent data but from database
mEntry?.let {
fillEntryDataInContentsView(it)
}
}
onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
if (createdFileUri != null) {
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
mAttachmentFileBinderManager
?.startDownloadAttachment(createdFileUri, attachmentToDownload)
}
}
}
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
if (mEntryLoaded) {
val inflater = menuInflater
MenuUtil.contributionMenuInflater(inflater, menu)
inflater.inflate(R.menu.entry, menu) val inflater = menuInflater
inflater.inflate(R.menu.database, menu) MenuUtil.contributionMenuInflater(inflater, menu)
inflater.inflate(R.menu.entry, menu)
if (mEntryIsHistory && !mDatabaseReadOnly) { inflater.inflate(R.menu.database, menu)
inflater.inflate(R.menu.entry_history, menu) if (mIsHistory && !mReadOnly) {
} inflater.inflate(R.menu.entry_history, menu)
// Show education views
Handler(Looper.getMainLooper()).post {
performedNextEducation(
EntryActivityEducation(
this
), menu
)
}
} }
return true if (mIsHistory || mReadOnly) {
} menu.findItem(R.id.menu_save_database)?.isVisible = false
menu.findItem(R.id.menu_edit)?.isVisible = false
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
if (mUrl?.isEmpty() != false) {
menu?.findItem(R.id.menu_goto_url)?.isVisible = false
}
if (mEntryIsHistory || mDatabaseReadOnly) {
menu?.findItem(R.id.menu_save_database)?.isVisible = false
menu?.findItem(R.id.menu_edit)?.isVisible = false
} }
if (mSpecialMode != SpecialMode.DEFAULT) { if (mSpecialMode != SpecialMode.DEFAULT) {
menu?.findItem(R.id.menu_reload_database)?.isVisible = false menu.findItem(R.id.menu_reload_database)?.isVisible = false
} }
applyToolbarColors()
return super.onPrepareOptionsMenu(menu) 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) }
return true
} }
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation, private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
menu: Menu) { menu: Menu) {
val entryFragment = supportFragmentManager.findFragmentByTag(ENTRY_FRAGMENT_TAG) val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView()
as? EntryFragment?
val entryFieldCopyView: View? = entryFragment?.firstEntryFieldCopyView()
val entryCopyEducationPerformed = entryFieldCopyView != null val entryCopyEducationPerformed = entryFieldCopyView != null
&& entryActivityEducation.checkAndPerformedEntryCopyEducation( && entryActivityEducation.checkAndPerformedEntryCopyEducation(
entryFieldCopyView, entryFieldCopyView,
{ {
entryFragment.launchEntryCopyEducationAction() val appNameString = getString(R.string.app_name)
}, clipboardHelper?.timeoutCopyToClipboard(appNameString,
{ getString(R.string.copy_field, appNameString))
performedNextEducation(entryActivityEducation, menu) },
}) {
performedNextEducation(entryActivityEducation, menu)
})
if (!entryCopyEducationPerformed) { if (!entryCopyEducationPerformed) {
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit) val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
@@ -420,54 +465,60 @@ class EntryActivity : DatabaseLockActivity() {
return true return true
} }
R.id.menu_edit -> { R.id.menu_edit -> {
mDatabase?.let { database -> mEntry?.let {
mMainEntryId?.let { entryId -> EntryEditActivity.launch(this@EntryActivity, it)
EntryEditActivity.launchToUpdate(
this,
database,
entryId,
mEntryActivityResultLauncher
)
}
} }
return true return true
} }
R.id.menu_goto_url -> { R.id.menu_goto_url -> {
mUrl?.let { url -> var url: String = mEntry?.url ?: ""
UriUtil.gotoUrl(this, url)
// Default http:// if no protocol specified
if (!url.contains("://")) {
url = "http://$url"
} }
UriUtil.gotoUrl(this, url)
return true return true
} }
R.id.menu_restore_entry_history -> { R.id.menu_restore_entry_history -> {
mMainEntryId?.let { mainEntryId -> mEntryLastVersion?.let { mainEntry ->
restoreEntryHistory( mProgressDatabaseTaskProvider?.startDatabaseRestoreEntryHistory(
mainEntryId, mainEntry,
mHistoryPosition) mEntryHistoryPosition,
!mReadOnly && mAutoSaveEnable)
} }
} }
R.id.menu_delete_entry_history -> { R.id.menu_delete_entry_history -> {
mMainEntryId?.let { mainEntryId -> mEntryLastVersion?.let { mainEntry ->
deleteEntryHistory( mProgressDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(
mainEntryId, mainEntry,
mHistoryPosition) mEntryHistoryPosition,
!mReadOnly && mAutoSaveEnable)
} }
} }
R.id.menu_save_database -> { R.id.menu_save_database -> {
saveDatabase() mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
} }
R.id.menu_reload_database -> { 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) android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(KEY_FIRST_LAUNCH_ACTIVITY, mFirstLaunchOfActivity)
}
override fun finish() { override fun finish() {
// Transit data in previous Activity after an update // Transit data in previous Activity after an update
Intent().apply { Intent().apply {
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId) putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry)
setResult(Activity.RESULT_OK, this) setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, this)
} }
super.finish() super.finish()
} }
@@ -475,42 +526,19 @@ class EntryActivity : DatabaseLockActivity() {
companion object { companion object {
private val TAG = EntryActivity::class.java.name 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 = "KEY_ENTRY"
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION" const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG" fun launch(activity: Activity, entry: Entry, readOnly: Boolean, historyPosition: Int? = null) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
/** val intent = Intent(activity, EntryActivity::class.java)
* Open standard Entry activity intent.putExtra(KEY_ENTRY, entry.nodeId)
*/ ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
fun launch(activity: Activity, if (historyPosition != null)
database: Database,
entryId: NodeId<UUID>,
activityResultLauncher: ActivityResultLauncher<Intent>) {
if (database.loaded) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
activityResultLauncher.launch(intent)
}
}
}
/**
* Open history Entry activity
*/
fun launch(activity: Activity,
database: Database,
entryId: NodeId<UUID>,
historyPosition: Int,
activityResultLauncher: ActivityResultLauncher<Intent>) {
if (database.loaded) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition) intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
activityResultLauncher.launch(intent) 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.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper 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.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper 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.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields
@@ -38,18 +39,10 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
* Activity to search or select entry in database, * Activity to search or select entry in database,
* Commonly used with Magikeyboard * Commonly used with Magikeyboard
*/ */
class EntrySelectionLauncherActivity : DatabaseModeActivity() { class EntrySelectionLauncherActivity : AppCompatActivity() {
override fun applyCustomStyle(): Boolean { override fun onCreate(savedInstanceState: Bundle?) {
return false
}
override fun finishActivityIfReloadRequested(): Boolean {
return true
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
var sharedWebDomain: String? = null var sharedWebDomain: String? = null
var otpString: String? = null var otpString: String? = null
@@ -75,39 +68,39 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
else -> {} else -> {}
} }
// Build domain search param // Build domain search param
val searchInfo = SearchInfo().apply { val searchInfo = SearchInfo().apply {
this.webDomain = sharedWebDomain this.webDomain = sharedWebDomain
this.otpString = otpString this.otpString = otpString
} }
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain -> SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain searchInfo.webDomain = concreteWebDomain
launch(database, searchInfo) launch(searchInfo)
} }
super.onCreate(savedInstanceState)
} }
private fun launch(database: Database?, private fun launch(searchInfo: SearchInfo) {
searchInfo: SearchInfo) {
if (!searchInfo.containsOnlyNullValues()) { if (!searchInfo.containsOnlyNullValues()) {
// Setting to integrate Magikeyboard // Setting to integrate Magikeyboard
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this) val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
// If database is open // If database is open
val readOnly = database?.isReadOnly != false val database = Database.getInstance()
val readOnly = database.isReadOnly
SearchHelper.checkAutoSearchInfo(this, SearchHelper.checkAutoSearchInfo(this,
database, database,
searchInfo, searchInfo,
{ openedDatabase, items -> { items ->
// Items found // Items found
if (searchInfo.otpString != null) { if (searchInfo.otpString != null) {
if (!readOnly) { if (!readOnly) {
GroupActivity.launchForSaveResult( GroupActivity.launchForSaveResult(this,
this, searchInfo,
openedDatabase, false)
searchInfo,
false)
} else { } else {
Toast.makeText(applicationContext, Toast.makeText(applicationContext,
R.string.autofill_read_only_save, R.string.autofill_read_only_save,
@@ -118,32 +111,30 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
if (items.size == 1) { if (items.size == 1) {
// Automatically populate keyboard // Automatically populate keyboard
val entryPopulate = items[0] val entryPopulate = items[0]
populateKeyboardAndMoveAppToBackground( populateKeyboardAndMoveAppToBackground(this,
this,
entryPopulate, entryPopulate,
intent) intent)
} else { } else {
// Select the one we want // Select the one we want
GroupActivity.launchForKeyboardSelectionResult(this, GroupActivity.launchForKeyboardSelectionResult(this,
openedDatabase, readOnly,
searchInfo, searchInfo,
true) true)
} }
} else { } else {
GroupActivity.launchForSearchResult(this, GroupActivity.launchForSearchResult(this,
openedDatabase, readOnly,
searchInfo, searchInfo,
true) true)
} }
}, },
{ openedDatabase -> {
// Show the database UI to select the entry // Show the database UI to select the entry
if (searchInfo.otpString != null) { if (searchInfo.otpString != null) {
if (!readOnly) { if (!readOnly) {
GroupActivity.launchForSaveResult(this, GroupActivity.launchForSaveResult(this,
openedDatabase, searchInfo,
searchInfo, false)
false)
} else { } else {
Toast.makeText(applicationContext, Toast.makeText(applicationContext,
R.string.autofill_read_only_save, R.string.autofill_read_only_save,
@@ -152,14 +143,13 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
} }
} else if (readOnly || searchShareForMagikeyboard) { } else if (readOnly || searchShareForMagikeyboard) {
GroupActivity.launchForKeyboardSelectionResult(this, GroupActivity.launchForKeyboardSelectionResult(this,
openedDatabase, readOnly,
searchInfo, searchInfo,
false) false)
} else { } else {
GroupActivity.launchForSaveResult(this, GroupActivity.launchForSaveResult(this,
openedDatabase, searchInfo,
searchInfo, false)
false)
} }
}, },
{ {
@@ -193,7 +183,7 @@ fun populateKeyboardAndMoveAppToBackground(activity: Activity,
intent: Intent, intent: Intent,
toast: Boolean = true) { toast: Boolean = true) {
// Populate Magikeyboard with entry // Populate Magikeyboard with entry
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast) MagikIME.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
// Consume the selection mode // Consume the selection mode
EntrySelectionHelper.removeModesFromIntent(intent) EntrySelectionHelper.removeModesFromIntent(intent)
activity.moveTaskToBack(true) activity.moveTaskToBack(true)

View File

@@ -31,10 +31,8 @@ import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -44,14 +42,14 @@ import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper 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.SpecialMode
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
@@ -62,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.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
import java.io.FileNotFoundException import java.io.FileNotFoundException
class FileDatabaseSelectActivity : DatabaseModeActivity(), class FileDatabaseSelectActivity : SpecialModeActivity(),
AssignMasterKeyDialogFragment.AssignPasswordDialogListener { AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
// Views // Views
@@ -85,22 +82,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
private var mDatabaseFileUri: Uri? = null private var mDatabaseFileUri: Uri? = null
private var mExternalFileHelper: ExternalFileHelper? = null private var mSelectFileHelper: SelectFileHelper? = null
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? = private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
AutofillHelper.buildActivityResultLauncher(this)
else null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// Enabling/disabling MagikeyboardService is normally done by DexModeReceiver, but this
// additional check will allow the keyboard to be reenabled more easily if the app crashes
// or is force quit within DeX mode and then the user leaves DeX mode. Without this, the
// user would need to enter and exit DeX mode once to reenable the service.
MagikeyboardUtil.setEnabled(this, !DexUtil.isDexMode(resources.configuration))
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext) mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
setContentView(R.layout.activity_file_selection) setContentView(R.layout.activity_file_selection)
@@ -115,25 +103,14 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
createDatabaseButtonView?.setOnClickListener { createNewFile() } createDatabaseButtonView?.setOnClickListener { createNewFile() }
// Open database button // Open database button
mExternalFileHelper = ExternalFileHelper(this) mSelectFileHelper = SelectFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let {
launchPasswordActivityWithPath(uri)
}
}
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
mDatabaseFileUri = databaseFileCreatedUri
if (mDatabaseFileUri != null) {
AssignMasterKeyDialogFragment.getInstance(true)
.show(supportFragmentManager, "passwordDialog")
} else {
val error = getString(R.string.error_create_database)
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
Log.e(TAG, error)
}
}
openDatabaseButtonView = findViewById(R.id.open_keyfile_button) openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper) openDatabaseButtonView?.apply {
mSelectFileHelper?.selectFileOnClickViewListener?.let {
setOnClickListener(it)
setOnLongClickListener(it)
}
}
// History list // History list
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list) val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
@@ -154,6 +131,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
} }
} }
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete -> mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
// Remove from app database
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete) databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
true true
} }
@@ -184,31 +162,29 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
// Observe list of databases // Observe list of databases
databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles -> databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles ->
try { when (databaseFiles.databaseFileAction) {
when (databaseFiles.databaseFileAction) { DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
DatabaseFilesViewModel.DatabaseFileAction.NONE -> { mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList) }
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
} }
DatabaseFilesViewModel.DatabaseFileAction.ADD -> { GroupActivity.launch(this@FileDatabaseSelectActivity,
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd -> PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity))
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd) }
} DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
} databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> { mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate -> }
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate) }
} DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
} databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> { mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
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 // Observe default database
@@ -216,62 +192,37 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
// Retrieve settings for default database // Retrieve settings for default database
mAdapterDatabaseHistory?.setDefaultDatabase(it) mAdapterDatabaseHistory?.setDefaultDatabase(it)
} }
}
override fun onDatabaseRetrieved(database: Database?) { // Attach the dialog thread to this activity
super.onDatabaseRetrieved(database) mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
if (database != null) { onActionFinish = { actionTask, result ->
launchGroupActivityIfLoaded(database) 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()
override fun onDatabaseActionFinished( databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri)
database: Database, }
actionTask: String, }
result: ActionRunnable.Result ACTION_DATABASE_LOAD_TASK -> {
) { val database = Database.getInstance()
super.onDatabaseActionFinished(database, actionTask, result) if (result.isSuccess
&& database.loaded) {
if (result.isSuccess) { launchGroupActivity(database)
// Update list } else {
when (actionTask) { var resultError = ""
ACTION_DATABASE_CREATE_TASK, val resultMessage = result.message
ACTION_DATABASE_LOAD_TASK -> { // Show error message
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri -> if (resultMessage != null && resultMessage.isNotEmpty()) {
val mainCredential = resultError = "$resultError $resultMessage"
result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) }
?: MainCredential() Log.e(TAG, resultError)
databaseFilesViewModel.addDatabaseFile( Snackbar.make(coordinatorLayout,
databaseUri, resultError,
mainCredential.keyFileUri 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()
} }
} }
@@ -279,9 +230,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
* Create a new file by calling the content provider * Create a new file by calling the content provider
*/ */
private fun createNewFile() { private fun createNewFile() {
mExternalFileHelper?.createDocument( createDocument(this, getString(R.string.database_file_name_default) +
getString(R.string.database_file_name_default) + getString(R.string.database_file_extension_default), "application/x-keepass")
getString(R.string.database_file_extension_default))
} }
private fun fileNoFoundAction(e: FileNotFoundException) { private fun fileNoFoundAction(e: FileNotFoundException) {
@@ -298,19 +248,15 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
fileNoFoundAction(exception) fileNoFoundAction(exception)
}, },
{ onCancelSpecialMode() }, { onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() }, { onLaunchActivitySpecialMode() })
mAutofillActivityResultLauncher)
} }
private fun launchGroupActivityIfLoaded(database: Database) { private fun launchGroupActivity(database: Database) {
if (database.loaded) { GroupActivity.launch(this,
GroupActivity.launch(this, database.isReadOnly,
database,
{ onValidateSpecialMode() }, { onValidateSpecialMode() },
{ onCancelSpecialMode() }, { onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() }, { onLaunchActivitySpecialMode() })
mAutofillActivityResultLauncher)
}
} }
override fun onValidateSpecialMode() { override fun onValidateSpecialMode() {
@@ -336,7 +282,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
// Show open and create button or special mode // Show open and create button or special mode
when (mSpecialMode) { when (mSpecialMode) {
SpecialMode.DEFAULT -> { SpecialMode.DEFAULT -> {
if (ExternalFileHelper.allowCreateDocumentByStorageAccessFramework(packageManager)) { if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
// There is an activity which can handle this intent. // There is an activity which can handle this intent.
createDatabaseButtonView?.visibility = View.VISIBLE createDatabaseButtonView?.visibility = View.VISIBLE
} else{ } else{
@@ -350,18 +296,30 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
} }
} }
mDatabase?.let { database -> val database = Database.getInstance()
launchGroupActivityIfLoaded(database) if (database.loaded) {
} launchGroupActivity(database)
// Show recent files if allowed
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
databaseFilesViewModel.loadListOfDatabases()
} else { } 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) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
// only to keep the current activity // only to keep the current activity
@@ -371,10 +329,15 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
} }
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) { override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
try { try {
mDatabaseFileUri?.let { databaseUri -> mDatabaseFileUri?.let { databaseUri ->
// Create the new database // Create the new database
createDatabase(databaseUri, mainCredential) mProgressDatabaseTaskProvider?.startDatabaseCreate(
databaseUri,
mainCredential
)
} }
} catch (e: Exception) { } catch (e: Exception) {
val error = getString(R.string.error_create_database_file) val error = getString(R.string.error_create_database_file)
@@ -385,6 +348,33 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {} override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
if (uri != null) {
launchPasswordActivityWithPath(uri)
}
}
// Retrieve the created URI from the file manager
onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
mDatabaseFileUri = databaseFileCreatedUri
if (mDatabaseFileUri != null) {
AssignMasterKeyDialogFragment.getInstance(true)
.show(supportFragmentManager, "passwordDialog")
} else {
val error = getString(R.string.error_create_database)
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
Log.e(TAG, error)
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
@@ -418,9 +408,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
openDatabaseButtonView != null openDatabaseButtonView != null
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation( && fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
openDatabaseButtonView!!, openDatabaseButtonView!!,
{ tapTargetView -> {tapTargetView ->
tapTargetView?.let { tapTargetView?.let {
mExternalFileHelper?.openDocument() mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
} }
}, },
{} {}
@@ -498,13 +488,11 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
*/ */
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: AppCompatActivity, fun launchForAutofillResult(activity: Activity,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent, autofillComponent: AutofillComponent,
searchInfo: SearchInfo? = null) { searchInfo: SearchInfo? = null) {
AutofillHelper.startActivityForAutofillResult(activity, AutofillHelper.startActivityForAutofillResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java), Intent(activity, FileDatabaseSelectActivity::class.java),
activityResultLauncher,
autofillComponent, autofillComponent,
searchInfo) searchInfo)
} }

View File

@@ -27,20 +27,16 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.commit import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.IconEditDialogFragment
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.icon.IconImageCustom
@@ -53,7 +49,7 @@ import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
import kotlinx.coroutines.* import kotlinx.coroutines.*
class IconPickerActivity : DatabaseLockActivity() { class IconPickerActivity : LockingActivity() {
private lateinit var toolbar: Toolbar private lateinit var toolbar: Toolbar
private lateinit var coordinatorLayout: CoordinatorLayout private lateinit var coordinatorLayout: CoordinatorLayout
@@ -68,13 +64,17 @@ class IconPickerActivity : DatabaseLockActivity() {
private var mCustomIconsSelectionMode = false private var mCustomIconsSelectionMode = false
private var mIconsSelected: List<IconImageCustom> = ArrayList() 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_icon_picker) setContentView(R.layout.activity_icon_picker)
mDatabase = Database.getInstance()
toolbar = findViewById(R.id.toolbar) toolbar = findViewById(R.id.toolbar)
toolbar.title = " " toolbar.title = " "
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
@@ -84,12 +84,18 @@ class IconPickerActivity : DatabaseLockActivity() {
coordinatorLayout = findViewById(R.id.icon_picker_coordinator) coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
addCustomIcon(uri)
}
uploadButton = findViewById(R.id.icon_picker_upload) 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 = findViewById(R.id.lock_button)
lockView?.setOnClickListener { lockView?.setOnClickListener {
@@ -115,6 +121,11 @@ class IconPickerActivity : DatabaseLockActivity() {
mIconImage = savedInstanceState.getParcelable(EXTRA_ICON) ?: mIconImage 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 -> iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
mIconImage.standard = iconStandard mIconImage.standard = iconStandard
// Remove the custom icon if a standard one is selected // Remove the custom icon if a standard one is selected
@@ -146,34 +157,6 @@ class IconPickerActivity : DatabaseLockActivity() {
} }
uploadButton.isEnabled = true uploadButton.isEnabled = true
} }
iconPickerViewModel.customIconUpdated.observe(this) { iconCustomUpdated ->
if (iconCustomUpdated.error && !iconCustomUpdated.errorConsumed) {
Snackbar.make(coordinatorLayout, iconCustomUpdated.errorStringId, Snackbar.LENGTH_LONG).asError().show()
iconCustomUpdated.errorConsumed = true
}
iconCustomUpdated.iconCustom?.let {
mDatabase?.updateCustomIcon(it)
}
iconPickerViewModel.deselectAllCustomIcons()
}
}
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() { private fun updateIconsSelectedViews() {
@@ -209,20 +192,11 @@ class IconPickerActivity : DatabaseLockActivity() {
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.icon, menu)
return true
}
override fun onPrepareOptionsMenu(menu: Menu?): Boolean { if (mCustomIconsSelectionMode) {
menu?.findItem(R.id.menu_edit)?.apply { menuInflater.inflate(R.menu.icon, menu)
isEnabled = mIconsSelected.size == 1
isVisible = isEnabled
} }
menu?.findItem(R.id.menu_delete)?.apply { return true
isEnabled = mCustomIconsSelectionMode
isVisible = isEnabled
}
return super.onPrepareOptionsMenu(menu)
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
@@ -234,17 +208,11 @@ class IconPickerActivity : DatabaseLockActivity() {
onBackPressed() onBackPressed()
} }
} }
R.id.menu_edit -> {
updateCustomIcon(mIconsSelected[0])
}
R.id.menu_delete -> { R.id.menu_delete -> {
mIconsSelected.forEach { iconToRemove -> mIconsSelected.forEach { iconToRemove ->
removeCustomIcon(iconToRemove) removeCustomIcon(iconToRemove)
} }
} }
R.id.menu_external_icon -> {
UriUtil.gotoUrl(this, R.string.external_icon_url)
}
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
@@ -301,11 +269,6 @@ class IconPickerActivity : DatabaseLockActivity() {
} }
} }
private fun updateCustomIcon(iconImageCustom: IconImageCustom) {
IconEditDialogFragment.update(iconImageCustom)
.show(supportFragmentManager, IconEditDialogFragment.TAG_UPDATE_ICON)
}
private fun removeCustomIcon(iconImageCustom: IconImageCustom) { private fun removeCustomIcon(iconImageCustom: IconImageCustom) {
uploadButton.isEnabled = false uploadButton.isEnabled = false
iconPickerViewModel.deselectAllCustomIcons() iconPickerViewModel.deselectAllCustomIcons()
@@ -315,6 +278,14 @@ class IconPickerActivity : DatabaseLockActivity() {
) )
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
addCustomIcon(uri)
}
}
private fun setResult() { private fun setResult() {
setResult(Activity.RESULT_OK, Intent().apply { setResult(Activity.RESULT_OK, Intent().apply {
putExtra(EXTRA_ICON, mIconImage) putExtra(EXTRA_ICON, mIconImage)
@@ -329,28 +300,30 @@ class IconPickerActivity : DatabaseLockActivity() {
companion object { companion object {
private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG" private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG"
private const val ICON_SELECTED_REQUEST = 15861
private const val EXTRA_ICON = "EXTRA_ICON" private const val EXTRA_ICON = "EXTRA_ICON"
private const val MAX_ICON_SIZE = 5242880 private const val MAX_ICON_SIZE = 5242880
fun registerIconSelectionForResult(context: FragmentActivity, fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, listener: (icon: IconImage) -> Unit) {
listener: (icon: IconImage) -> Unit): ActivityResultLauncher<Intent> { if (requestCode == ICON_SELECTED_REQUEST) {
return context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (resultCode == Activity.RESULT_OK) {
if (result.resultCode == Activity.RESULT_OK) { listener.invoke(data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
listener.invoke(result.data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
} }
} }
} }
fun launch(context: FragmentActivity, fun launch(context: Activity,
previousIcon: IconImage?, previousIcon: IconImage?) {
resultLauncher: ActivityResultLauncher<Intent>) {
// Create an instance to return the picker icon // Create an instance to return the picker icon
resultLauncher.launch( context.startActivityForResult(
Intent(context, IconPickerActivity::class.java).apply { Intent(context,
IconPickerActivity::class.java).apply {
if (previousIcon != null) if (previousIcon != null)
putExtra(EXTRA_ICON, previousIcon) putExtra(EXTRA_ICON, previousIcon)
} },
) ICON_SELECTED_REQUEST)
} }
} }
} }

View File

@@ -19,7 +19,6 @@
*/ */
package com.kunzisoft.keepass.activities package com.kunzisoft.keepass.activities
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
@@ -32,19 +31,16 @@ import android.widget.ImageView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import com.igreenwood.loupe.Loupe import com.igreenwood.loupe.Loupe
import com.kunzisoft.keepass.R 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.Attachment
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
import kotlin.math.max import kotlin.math.max
class ImageViewerActivity : DatabaseLockActivity() { class ImageViewerActivity : LockingActivity() {
private var imageContainerView: ViewGroup? = null private var mDatabase: Database? = null
private lateinit var imageView: ImageView
private lateinit var progressView: View
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -54,21 +50,49 @@ class ImageViewerActivity : DatabaseLockActivity() {
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
toolbar.setOnTouchListener { _, _ ->
resetAppTimeout() val imageContainerView: ViewGroup = findViewById(R.id.image_viewer_container)
false 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) Loupe.create(imageView, imageContainerView) {
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
}
onViewTranslateListener = object : Loupe.OnViewTranslateListener { onViewTranslateListener = object : Loupe.OnViewTranslateListener {
override fun onStart(view: ImageView) { 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 { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
android.R.id.home -> finish() android.R.id.home -> finish()

View File

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

View File

@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.activities
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -30,27 +31,28 @@ import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.util.Log import android.util.Log
import android.view.* import android.view.*
import android.view.KeyEvent.KEYCODE_ENTER
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
import android.view.inputmethod.InputMethodManager
import android.widget.* import android.widget.*
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ActivityCompat
import androidx.fragment.app.commit import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
import com.kunzisoft.keepass.activities.helpers.* import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity 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.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment 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.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
@@ -64,18 +66,15 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil 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.BACK_PREVIOUS_KEYBOARD_ACTION
import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
import java.io.FileNotFoundException import java.io.FileNotFoundException
open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener {
class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
// Views // Views
private var toolbar: Toolbar? = null private var toolbar: Toolbar? = null
@@ -89,23 +88,31 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
private lateinit var coordinatorLayout: CoordinatorLayout private lateinit var coordinatorLayout: CoordinatorLayout
private var advancedUnlockFragment: AdvancedUnlockFragment? = null private var advancedUnlockFragment: AdvancedUnlockFragment? = null
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels() private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels()
private var mDefaultDatabase: Boolean = false private var mDefaultDatabase: Boolean = false
private var mDatabaseFileUri: Uri? = null private var mDatabaseFileUri: Uri? = null
private var mDatabaseKeyFileUri: Uri? = null private var mDatabaseKeyFileUri: Uri? = null
private var mRememberKeyFile: Boolean = false private var mRememberKeyFile: Boolean = false
private var mExternalFileHelper: ExternalFileHelper? = null private var mSelectFileHelper: SelectFileHelper? = null
private var mReadOnly: Boolean = false private var mPermissionAsked = false
private var readOnly: Boolean = false
private var mForceReadOnly: Boolean = false private var mForceReadOnly: Boolean = false
set(value) {
infoContainerView?.visibility = if (value) {
readOnly = true
View.VISIBLE
} else {
View.GONE
}
field = value
}
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? = private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
AutofillHelper.buildActivityResultLauncher(this) private var mAllowAutoOpenBiometricPrompt: Boolean = true
else null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -127,21 +134,17 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
infoContainerView = findViewById(R.id.activity_password_info_container) infoContainerView = findViewById(R.id.activity_password_info_container)
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout) coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) { mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
savedInstanceState.getBoolean(KEY_READ_ONLY) readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
} else {
PreferencesUtil.enableReadOnlyDatabase(this)
}
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this) mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity) mSelectFileHelper = SelectFileHelper(this@PasswordActivity)
mExternalFileHelper?.buildOpenDocument { uri -> keyFileSelectionView?.apply {
if (uri != null) { mSelectFileHelper?.selectFileOnClickViewListener?.let {
mDatabaseKeyFileUri = uri setOnClickListener(it)
populateKeyFileTextView(uri) setOnLongClickListener(it)
} }
} }
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
passwordView?.setOnEditorActionListener(onEditorActionListener) passwordView?.setOnEditorActionListener(onEditorActionListener)
passwordView?.addTextChangedListener(object : TextWatcher { passwordView?.addTextChangedListener(object : TextWatcher {
@@ -154,21 +157,15 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
checkboxPasswordView?.isChecked = true 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 // If is a view intent
getUriFromIntent(intent) getUriFromIntent(intent)
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) { if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE)) mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
} }
if (savedInstanceState?.containsKey(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) == true) {
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
}
// Init Biometric elements // Init Biometric elements
advancedUnlockFragment = supportFragmentManager advancedUnlockFragment = supportFragmentManager
@@ -184,30 +181,21 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
// Listen password checkbox to init advanced unlock and confirmation button // Listen password checkbox to init advanced unlock and confirmation button
checkboxPasswordView?.setOnCheckedChangeListener { _, _ -> checkboxPasswordView?.setOnCheckedChangeListener { _, _ ->
mAdvancedUnlockViewModel.checkUnlockAvailability() advancedUnlockFragment?.checkUnlockAvailability()
enableOrNotTheConfirmationButton() enableOrNotTheConfirmationButton()
} }
// Observe if default database // Observe if default database
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase -> databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
mDefaultDatabase = isDefaultDatabase mDefaultDatabase = isDefaultDatabase
} }
// Observe database file change // Observe database file change
mDatabaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile -> databaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
// Force read only if the file does not exists // Force read only if the file does not exists
val databaseFileNotExists = databaseFile?.let { mForceReadOnly = databaseFile?.let {
!it.databaseFileExists !it.databaseFileExists
} ?: true } ?: true
infoContainerView?.visibility = if (databaseFileNotExists) {
mReadOnly = true
View.VISIBLE
} else {
View.GONE
}
mForceReadOnly = databaseFileNotExists
invalidateOptionsMenu() invalidateOptionsMenu()
// Post init uri with KeyFile only if needed // Post init uri with KeyFile only if needed
@@ -224,112 +212,72 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri) onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
} }
}
override fun onResume() { mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
super.onResume() 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 (resultException != null) {
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@PasswordActivity)) { resultError = resultException.getLocalizedMessage(resources)
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
}
// Don't allow auto open prompt if lock become when UI visible when (resultException) {
if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) { is DuplicateUuidDatabaseException -> {
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false // Relaunch loading if we need to fix UUID
} showLoadDatabaseDuplicateUuidMessage {
mDatabaseFileUri?.let { databaseFileUri -> var databaseUri: Uri? = null
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri) var mainCredential: MainCredential = MainCredential()
} var readOnly = true
var cipherEntity: CipherDatabaseEntity? = null
mDatabase?.let { database -> result.data?.let { resultData ->
launchGroupActivityIfLoaded(database) 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?) { databaseUri?.let { databaseFileUri ->
super.onDatabaseRetrieved(database) showProgressDialogAndLoadDatabase(
if (database != null) { databaseFileUri,
launchGroupActivityIfLoaded(database) mainCredential,
} readOnly,
} cipherEntity,
true)
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
mAdvancedUnlockViewModel.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)
} }
is FileNotFoundDatabaseException -> {
databaseUri?.let { databaseFileUri -> // Remove this default database inaccessible
showProgressDialogAndLoadDatabase( if (mDefaultDatabase) {
databaseFileUri, databaseFileViewModel.removeDefaultDatabase()
mainCredential, }
readOnly,
cipherEntity,
true
)
} }
} }
} }
is FileNotFoundDatabaseException -> {
// Remove this default database inaccessible // Show error message
if (mDefaultDatabase) { if (resultMessage != null && resultMessage.isNotEmpty()) {
mDatabaseFileViewModel.removeDefaultDatabase() 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()
} }
} }
} }
@@ -347,7 +295,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE) mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
} }
mDatabaseFileUri?.let { mDatabaseFileUri?.let {
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it) databaseFileViewModel.checkIfIsDefaultDatabase(it)
} }
} }
@@ -356,18 +304,13 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
getUriFromIntent(intent) getUriFromIntent(intent)
} }
private fun launchGroupActivityIfLoaded(database: Database) { private fun launchGroupActivity() {
// Check if database really loaded GroupActivity.launch(this,
if (database.loaded) { readOnly,
clearCredentialsViews(true)
GroupActivity.launch(this,
database,
{ onValidateSpecialMode() }, { onValidateSpecialMode() },
{ onCancelSpecialMode() }, { onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() }, { onLaunchActivitySpecialMode() }
mAutofillActivityResultLauncher )
)
}
} }
override fun onValidateSpecialMode() { override fun onValidateSpecialMode() {
@@ -417,6 +360,40 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
} }
} }
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?) { private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
// Define Key File text // Define Key File text
if (mRememberKeyFile) { if (mRememberKeyFile) {
@@ -439,17 +416,12 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
verifyCheckboxesAndLoadDatabase(password, keyFileUri) verifyCheckboxesAndLoadDatabase(password, keyFileUri)
} else { } else {
// Init Biometric elements // Init Biometric elements
mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri) advancedUnlockFragment?.loadDatabase(databaseFileUri,
mAllowAutoOpenBiometricPrompt
&& mProgressDatabaseTaskProvider?.isBinded() != true)
} }
enableOrNotTheConfirmationButton() 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() { private fun enableOrNotTheConfirmationButton() {
@@ -467,7 +439,6 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) { private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
populatePasswordTextView(null) populatePasswordTextView(null)
if (clearKeyFile) { if (clearKeyFile) {
mDatabaseKeyFileUri = null
populateKeyFileTextView(null) populateKeyFileTextView(null)
} }
} }
@@ -497,17 +468,22 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
} }
override fun onPause() { override fun onPause() {
mProgressDatabaseTaskProvider?.unregisterProgressTask()
// Reinit locking activity UI variable // 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() super.onPause()
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
mDatabaseKeyFileUri?.let { mDatabaseKeyFileUri?.let {
outState.putString(KEY_KEYFILE, it.toString()) 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) super.onSaveInstanceState(outState)
} }
@@ -544,7 +520,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
clearCredentialsViews() clearCredentialsViews()
} }
if (mReadOnly && ( if (readOnly && (
mSpecialMode == SpecialMode.SAVE mSpecialMode == SpecialMode.SAVE
|| mSpecialMode == SpecialMode.REGISTRATION) || mSpecialMode == SpecialMode.REGISTRATION)
) { ) {
@@ -558,7 +534,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
showProgressDialogAndLoadDatabase( showProgressDialogAndLoadDatabase(
databaseUri, databaseUri,
MainCredential(password, keyFileUri), MainCredential(password, keyFileUri),
mReadOnly, readOnly,
cipherDatabaseEntity, cipherDatabaseEntity,
false) false)
} }
@@ -570,7 +546,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
readOnly: Boolean, readOnly: Boolean,
cipherDatabaseEntity: CipherDatabaseEntity?, cipherDatabaseEntity: CipherDatabaseEntity?,
fixDuplicateUUID: Boolean) { fixDuplicateUUID: Boolean) {
loadDatabase( mProgressDatabaseTaskProvider?.startDatabaseLoad(
databaseUri, databaseUri,
mainCredential, mainCredential,
readOnly, readOnly,
@@ -606,6 +582,35 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
return true return true
} }
// Check permission
private fun checkPermission() {
if (Build.VERSION.SDK_INT in 23..28
&& !readOnly
&& !mPermissionAsked) {
mPermissionAsked = true
// Check self permission to show or not the dialog
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
val permissions = arrayOf(writePermission)
if (toolbar != null
&& ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST)
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
WRITE_EXTERNAL_STORAGE_REQUEST -> {
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
Toast.makeText(this, R.string.read_only_warning, Toast.LENGTH_LONG).show()
}
}
}
}
// To fix multiple view education // To fix multiple view education
private var performedEductionInProgress = false private var performedEductionInProgress = false
private fun launchEducation(menu: Menu) { private fun launchEducation(menu: Menu) {
@@ -657,7 +662,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
} }
private fun changeOpenFileReadIcon(togglePassword: MenuItem) { private fun changeOpenFileReadIcon(togglePassword: MenuItem) {
if (mReadOnly) { if (readOnly) {
togglePassword.setTitle(R.string.menu_file_selection_read_only) togglePassword.setTitle(R.string.menu_file_selection_read_only)
togglePassword.setIcon(R.drawable.ic_read_only_white_24dp) togglePassword.setIcon(R.drawable.ic_read_only_white_24dp)
} else { } else {
@@ -671,7 +676,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
when (item.itemId) { when (item.itemId) {
android.R.id.home -> finish() android.R.id.home -> finish()
R.id.menu_open_file_read_mode_key -> { R.id.menu_open_file_read_mode_key -> {
mReadOnly = !mReadOnly readOnly = !readOnly
changeOpenFileReadIcon(item) changeOpenFileReadIcon(item)
} }
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item) else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
@@ -680,6 +685,46 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mAllowAutoOpenBiometricPrompt = false
// To get device credential unlock result
advancedUnlockFragment?.onActivityResult(requestCode, resultCode, data)
// To get entry in result
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
var keyFileResult = false
mSelectFileHelper?.let {
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
if (uri != null) {
mDatabaseKeyFileUri = uri
populateKeyFileTextView(uri)
}
}
}
if (!keyFileResult) {
// this block if not a key file response
when (resultCode) {
LockingActivity.RESULT_EXIT_LOCK -> {
clearCredentialsViews()
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
}
Activity.RESULT_CANCELED -> {
clearCredentialsViews()
}
}
}
}
companion object { companion object {
private val TAG = PasswordActivity::class.java.name private val TAG = PasswordActivity::class.java.name
@@ -690,9 +735,12 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
private const val KEY_KEYFILE = "keyFile" private const val KEY_KEYFILE = "keyFile"
private const val VIEW_INTENT = "android.intent.action.VIEW" 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_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately" private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647
private const val ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT = "ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT"
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?, private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
intentBuildLauncher: (Intent) -> Unit) { intentBuildLauncher: (Intent) -> Unit) {
@@ -783,17 +831,15 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
@Throws(FileNotFoundException::class) @Throws(FileNotFoundException::class)
fun launchForAutofillResult(activity: AppCompatActivity, fun launchForAutofillResult(activity: Activity,
databaseFile: Uri, databaseFile: Uri,
keyFile: Uri?, keyFile: Uri?,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent, autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) { searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
AutofillHelper.startActivityForAutofillResult( AutofillHelper.startActivityForAutofillResult(
activity, activity,
intent, intent,
activityResultLauncher,
autofillComponent, autofillComponent,
searchInfo) searchInfo)
} }
@@ -821,13 +867,12 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
* Global Launch * Global Launch
* ------------------------- * -------------------------
*/ */
fun launch(activity: AppCompatActivity, fun launch(activity: Activity,
databaseUri: Uri, databaseUri: Uri,
keyFile: Uri?, keyFile: Uri?,
fileNoFoundAction: (exception: FileNotFoundException) -> Unit, fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
onCancelSpecialMode: () -> Unit, onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit, onLaunchActivitySpecialMode: () -> Unit) {
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
try { try {
EntrySelectionHelper.doSpecialAction(activity.intent, EntrySelectionHelper.doSpecialAction(activity.intent,
@@ -857,7 +902,6 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PasswordActivity.launchForAutofillResult(activity, PasswordActivity.launchForAutofillResult(activity,
databaseUri, keyFile, databaseUri, keyFile,
autofillActivityResultLauncher,
autofillComponent, autofillComponent,
searchInfo) searchInfo)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
@@ -29,17 +30,18 @@ import android.text.SpannableStringBuilder
import android.text.TextWatcher import android.text.TextWatcher
import android.view.View import android.view.View
import android.widget.CompoundButton import android.widget.CompoundButton
import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.KeyFileSelectionView
class AssignMasterKeyDialogFragment : DatabaseDialogFragment() { class AssignMasterKeyDialogFragment : DialogFragment() {
private var mMasterPassword: String? = null private var mMasterPassword: String? = null
private var mKeyFile: Uri? = null private var mKeyFile: Uri? = null
@@ -58,7 +60,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
private var mListener: AssignPasswordDialogListener? = null private var mListener: AssignPasswordDialogListener? = null
private var mExternalFileHelper: ExternalFileHelper? = null private var mSelectFileHelper: SelectFileHelper? = null
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
private var mNoKeyConfirmationDialog: AlertDialog? = null private var mNoKeyConfirmationDialog: AlertDialog? = null
@@ -131,20 +133,11 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox) keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection) keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
mExternalFileHelper = ExternalFileHelper(this) mSelectFileHelper = SelectFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri -> keyFileSelectionView?.apply {
uri?.let { pathUri -> setOnClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile -> setOnLongClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
keyFileSelectionView?.error = null
keyFileCheckBox?.isChecked = true
keyFileSelectionView?.uri = pathUri
if (lengthFile <= 0L) {
showEmptyKeyFileConfirmationDialog()
}
}
}
} }
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
val dialog = builder.create() val dialog = builder.create()
@@ -219,11 +212,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match) passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
} }
if ((mMasterPassword == null if (mMasterPassword == null || mMasterPassword!!.isEmpty()) {
|| mMasterPassword!!.isEmpty())
&& (keyFileCheckBox == null
|| !keyFileCheckBox!!.isChecked
|| keyFileSelectionView?.uri == null)) {
error = true error = true
showEmptyPasswordConfirmationDialog() showEmptyPasswordConfirmationDialog()
} }
@@ -297,6 +286,23 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
uri?.let { pathUri ->
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
keyFileSelectionView?.error = null
keyFileCheckBox?.isChecked = true
keyFileSelectionView?.uri = pathUri
if (lengthFile <= 0L) {
showEmptyKeyFileConfirmationDialog()
}
}
}
}
}
companion object { companion object {
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG" private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"

View File

@@ -1,95 +0,0 @@
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.graphics.Color
import android.os.Bundle
import android.widget.CompoundButton
import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels
import com.kunzisoft.androidclearchroma.view.ChromaColorView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
class ColorPickerDialogFragment : DatabaseDialogFragment() {
private val mColorPickerViewModel: ColorPickerViewModel by activityViewModels()
private lateinit var enableSwitchView: CompoundButton
private lateinit var chromaColorView: ChromaColorView
private var mDefaultColor = Color.WHITE
private var mActivated = false
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.fragment_color_picker, null)
enableSwitchView = root.findViewById(R.id.switch_element)
chromaColorView = root.findViewById(R.id.chroma_color_view)
if (savedInstanceState != null) {
if (savedInstanceState.containsKey(ARG_INITIAL_COLOR)) {
mDefaultColor = savedInstanceState.getInt(ARG_INITIAL_COLOR)
}
if (savedInstanceState.containsKey(ARG_ACTIVATED)) {
mActivated = savedInstanceState.getBoolean(ARG_ACTIVATED)
}
} else {
arguments?.apply {
if (containsKey(ARG_INITIAL_COLOR)) {
mDefaultColor = getInt(ARG_INITIAL_COLOR)
}
if (containsKey(ARG_ACTIVATED)) {
mActivated = getBoolean(ARG_ACTIVATED)
}
}
}
enableSwitchView.isChecked = mActivated
chromaColorView.currentColor = mDefaultColor
chromaColorView.setOnColorChangedListener {
if (!enableSwitchView.isChecked)
enableSwitchView.isChecked = true
}
val builder = AlertDialog.Builder(activity)
builder.setView(root)
.setPositiveButton(android.R.string.ok) { _, _ ->
val color: Int? = if (enableSwitchView.isChecked)
chromaColorView.currentColor
else
null
mColorPickerViewModel.pickColor(color)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
// Do nothing
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(ARG_INITIAL_COLOR, chromaColorView.currentColor)
outState.putBoolean(ARG_ACTIVATED, mActivated)
}
companion object {
private const val ARG_INITIAL_COLOR = "ARG_INITIAL_COLOR"
private const val ARG_ACTIVATED = "ARG_ACTIVATED"
fun newInstance(
@ColorInt initialColor: Int?,
): ColorPickerDialogFragment {
return ColorPickerDialogFragment().apply {
arguments = Bundle().apply {
putInt(ARG_INITIAL_COLOR, initialColor ?: Color.WHITE)
putBoolean(ARG_ACTIVATED, initialColor != null)
}
}
}
}
}

View File

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

View File

@@ -1,72 +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)
}
}
@Suppress("DEPRECATION")
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 android.os.Bundle
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
// Not as DatabaseDialogFragment because crash on KitKat
class DatePickerFragment : DialogFragment() { class DatePickerFragment : DialogFragment() {
private var mDefaultYear: Int = 2000 private var mDefaultYear: Int = 2000

View File

@@ -20,38 +20,61 @@
package com.kunzisoft.keepass.activities.dialogs package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog import android.app.Dialog
import android.content.Context
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.node.Node 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 var mNodesToDelete: List<Node> = ArrayList()
private val mNodesViewModel: NodesViewModel by activityViewModels() 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 { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
mNodesViewModel.nodesToDelete.observe(this) { nodes ->
this.mNodesToDelete = nodes
}
var recycleBin = false
arguments?.apply { arguments?.apply {
if (containsKey(RECYCLE_BIN_TAG)) { if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
recycleBin = this.getBoolean(RECYCLE_BIN_TAG) && 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 -> activity?.let { activity ->
// Use the Builder class for convenient dialog construction // Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(activity) val builder = AlertDialog.Builder(activity)
builder.setMessage(if (recycleBin) builder.setMessage(retrieveMessage())
getString(R.string.warning_empty_recycle_bin)
else
getString(R.string.warning_permanently_delete_nodes))
builder.setPositiveButton(android.R.string.ok) { _, _ -> builder.setPositiveButton(android.R.string.ok) { _, _ ->
mNodesViewModel.permanentlyDeleteNodes(mNodesToDelete) mListener?.permanentlyDeleteNodes(mNodesToDelete)
} }
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() } builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
// Create the AlertDialog object and return it // Create the AlertDialog object and return it
@@ -60,14 +83,19 @@ class DeleteNodesDialogFragment : DatabaseDialogFragment() {
return super.onCreateDialog(savedInstanceState) return super.onCreateDialog(savedInstanceState)
} }
companion object { override fun onSaveInstanceState(outState: Bundle) {
private const val RECYCLE_BIN_TAG = "RECYCLE_BIN_TAG" 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 { return DeleteNodesDialogFragment().apply {
arguments = Bundle().apply { arguments = getBundleFromListNodes(nodesToDelete)
putBoolean(RECYCLE_BIN_TAG, recycleBin)
}
} }
} }
} }

View File

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

View File

@@ -31,13 +31,14 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.security.ProtectedString 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 private var oldField: Field? = null

View File

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

View File

@@ -1,151 +0,0 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UuidUtil
import com.kunzisoft.keepass.view.DateTimeFieldView
class GroupDialogFragment : DatabaseDialogFragment() {
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
private var mGroupInfo = GroupInfo()
private lateinit var iconView: ImageView
private var mIconColor: Int = 0
private lateinit var nameTextView: TextView
private lateinit var notesTextLabelView: TextView
private lateinit var notesTextView: TextView
private lateinit var expirationView: DateTimeFieldView
private lateinit var creationView: TextView
private lateinit var modificationView: TextView
private lateinit var uuidContainerView: ViewGroup
private lateinit var uuidReferenceView: TextView
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
}
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.fragment_group, null)
iconView = root.findViewById(R.id.group_icon)
nameTextView = root.findViewById(R.id.group_name)
notesTextLabelView = root.findViewById(R.id.group_note_label)
notesTextView = root.findViewById(R.id.group_note)
expirationView = root.findViewById(R.id.group_expiration)
creationView = root.findViewById(R.id.group_created)
modificationView = root.findViewById(R.id.group_modified)
uuidContainerView = root.findViewById(R.id.group_UUID_container)
uuidReferenceView = root.findViewById(R.id.group_UUID_reference)
// Retrieve the textColor to tint the icon
val ta = activity.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
mIconColor = ta.getColor(0, Color.WHITE)
ta.recycle()
if (savedInstanceState != null
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
} else {
arguments?.apply {
if (containsKey(KEY_GROUP_INFO)) {
mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
}
}
}
// populate info in views
val title = mGroupInfo.title
if (title.isEmpty()) {
nameTextView.visibility = View.GONE
} else {
nameTextView.text = title
nameTextView.visibility = View.VISIBLE
}
val notes = mGroupInfo.notes
if (notes == null || notes.isEmpty()) {
notesTextLabelView.visibility = View.GONE
notesTextView.visibility = View.GONE
} else {
notesTextView.text = notes
notesTextLabelView.visibility = View.VISIBLE
notesTextView.visibility = View.VISIBLE
}
expirationView.activation = mGroupInfo.expires
expirationView.dateTime = mGroupInfo.expiryTime
creationView.text = mGroupInfo.creationTime.getDateTimeString(resources)
modificationView.text = mGroupInfo.lastModificationTime.getDateTimeString(resources)
val uuid = UuidUtil.toHexString(mGroupInfo.id)
if (uuid == null || uuid.isEmpty()) {
uuidContainerView.visibility = View.GONE
} else {
uuidReferenceView.text = uuid
uuidContainerView.apply {
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
}
}
val builder = AlertDialog.Builder(activity)
builder.setView(root)
.setPositiveButton(android.R.string.ok){ _, _ ->
// Do nothing
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putParcelable(KEY_GROUP_INFO, mGroupInfo)
super.onSaveInstanceState(outState)
}
data class Error(val isError: Boolean, val messageId: Int?)
companion object {
const val TAG_SHOW_GROUP = "TAG_SHOW_GROUP"
private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
fun launch(groupInfo: GroupInfo): GroupDialogFragment {
val bundle = Bundle()
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
val fragment = GroupDialogFragment()
fragment.arguments = bundle
return fragment
}
}
}

View File

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

View File

@@ -1,126 +0,0 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.os.Bundle
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
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.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
class IconEditDialogFragment : DatabaseDialogFragment() {
private val mIconPickerViewModel: IconPickerViewModel by activityViewModels()
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
private lateinit var iconView: ImageView
private lateinit var nameTextLayoutView: TextInputLayout
private lateinit var nameTextView: TextView
private var mCustomIcon: IconImageCustom? = null
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
}
mCustomIcon?.let { customIcon ->
populateViewsWithCustomIcon(customIcon)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.fragment_icon_edit, null)
iconView = root.findViewById(R.id.icon_edit_image)
nameTextLayoutView = root.findViewById(R.id.icon_edit_name_container)
nameTextView = root.findViewById(R.id.icon_edit_name)
if (savedInstanceState != null
&& savedInstanceState.containsKey(KEY_CUSTOM_ICON_ID)) {
mCustomIcon = savedInstanceState.getParcelable(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
} else {
arguments?.apply {
if (containsKey(KEY_CUSTOM_ICON_ID)) {
mCustomIcon = getParcelable(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
}
}
}
val builder = AlertDialog.Builder(activity)
builder.setView(root)
.setPositiveButton(android.R.string.ok) { _, _ ->
retrieveIconInfoFromViews()
mCustomIcon?.let { customIcon ->
mIconPickerViewModel.updateCustomIcon(
IconPickerViewModel.IconCustomState(customIcon, false)
)
}
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
// Do nothing
mIconPickerViewModel.updateCustomIcon(
IconPickerViewModel.IconCustomState(null, false)
)
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun populateViewsWithCustomIcon(customIcon: IconImageCustom) {
mPopulateIconMethod?.invoke(iconView, customIcon.getIconImageToDraw())
nameTextView.text = customIcon.name
}
private fun retrieveIconInfoFromViews() {
mCustomIcon?.name = nameTextView.text.toString()
mCustomIcon?.lastModificationTime = DateInstant()
}
override fun onSaveInstanceState(outState: Bundle) {
retrieveIconInfoFromViews()
outState.putParcelable(KEY_CUSTOM_ICON_ID, mCustomIcon)
super.onSaveInstanceState(outState)
}
companion object {
const val TAG_UPDATE_ICON = "TAG_UPDATE_ICON"
const val KEY_CUSTOM_ICON_ID = "KEY_CUSTOM_ICON_ID"
fun update(customIcon: IconImageCustom): IconEditDialogFragment {
val bundle = Bundle()
bundle.putParcelable(KEY_CUSTOM_ICON_ID, IconImageCustom(customIcon))
val fragment = IconEditDialogFragment()
fragment.arguments = bundle
return fragment
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,6 @@ import android.os.Bundle
import android.text.format.DateFormat import android.text.format.DateFormat
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
// Not as DatabaseDialogFragment because crash on KitKat
class TimePickerFragment : DialogFragment() { class TimePickerFragment : DialogFragment() {
private var defaultHour: Int = 0 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,278 +19,431 @@
*/ */
package com.kunzisoft.keepass.activities.fragments package com.kunzisoft.keepass.activities.fragments
import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import android.view.inputmethod.EditorInfo
import androidx.fragment.app.activityViewModels import android.widget.EditText
import android.widget.ImageView
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator 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.R
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment import com.kunzisoft.keepass.activities.EntryEditActivity
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment 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.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.template.Template import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.model.AttachmentState import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.model.StreamDirection import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.view.TemplateEditView 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.collapse
import com.kunzisoft.keepass.view.expand 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 entryTitleLayoutView: TextInputLayout
private lateinit var entryTitleView: EditText
private lateinit var rootView: View private lateinit var entryIconView: ImageView
private lateinit var templateView: TemplateEditView private lateinit var entryUserNameView: EditText
private lateinit var attachmentsContainerView: ViewGroup 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 lateinit var attachmentsListView: RecyclerView
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
private var mTemplate: Template? = null private lateinit var attachmentsAdapter: EntryAttachmentsItemsAdapter
private var mAllowMultipleAttachments: Boolean = false
private var mIconColor: Int = 0 private var fontInVisibility: Boolean = false
private var iconColor: Int = 0
override fun onCreateView(inflater: LayoutInflater, var drawFactory: IconDrawableFactory? = null
container: ViewGroup?, var setOnDateClickListener: (() -> Unit)? = null
savedInstanceState: Bundle?): View? { 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) super.onCreateView(inflater, container, savedInstanceState)
// Retrieve the textColor to tint the icon val rootView = inflater.cloneInContext(contextThemed)
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) .inflate(R.layout.fragment_entry_edit_contents, container, false)
mIconColor = taIconColor?.getColor(0, Color.BLACK) ?: Color.BLACK
taIconColor?.recycle()
return inflater.cloneInContext(contextThemed) fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext())
.inflate(R.layout.fragment_entry_edit, container, false)
}
override fun onViewCreated(view: View, entryTitleLayoutView = rootView.findViewById(R.id.entry_edit_container_title)
savedInstanceState: Bundle?) { entryTitleView = rootView.findViewById(R.id.entry_edit_title)
super.onViewCreated(view, savedInstanceState) entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
entryIconView.setOnClickListener {
rootView = view setOnIconViewClickListener?.invoke(mEntryInfo.icon)
// Hide only the first time
if (savedInstanceState == null) {
view.isVisible = false
} }
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()) attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
attachmentsListView.apply { // TODO retrieve current database with its unique key
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) attachmentsAdapter.database = Database.getInstance()
adapter = attachmentsAdapter //attachmentsAdapter.database = arguments?.getInt(KEY_DATABASE)
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
}
templateView.apply {
setOnIconClickListener {
mEntryEditViewModel.requestIconSelection(templateView.getIcon())
}
setOnBackgroundColorClickListener {
mEntryEditViewModel.requestBackgroundColorSelection(templateView.getBackgroundColor())
}
setOnForegroundColorClickListener {
mEntryEditViewModel.requestForegroundColorSelection(templateView.getForegroundColor())
}
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 ->
this.mTemplate = template
templateView.setTemplate(template)
}
mEntryEditViewModel.templatesEntry.observe(viewLifecycleOwner) { templateEntry ->
if (templateEntry != null) {
val selectedTemplate = if (mTemplate != null)
mTemplate
else
templateEntry.defaultTemplate
templateView.setTemplate(selectedTemplate)
// Load entry info only the first time to keep change locally
if (savedInstanceState == null) {
assignEntryInfo(templateEntry.entryInfo)
}
// To prevent flickering
rootView.showByFading()
// Apply timeout reset
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
}
}
mEntryEditViewModel.requestEntryInfoUpdate.observe(viewLifecycleOwner) {
mEntryEditViewModel.saveEntryInfo(it.database, it.entry, it.parent, retrieveEntryInfo())
}
mEntryEditViewModel.onIconSelected.observe(viewLifecycleOwner) { iconImage ->
templateView.setIcon(iconImage)
}
mEntryEditViewModel.onBackgroundColorSelected.observe(this) { color ->
templateView.setBackgroundColor(color)
}
mEntryEditViewModel.onForegroundColorSelected.observe(this) { color ->
templateView.setForegroundColor(color)
}
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 ->
if (previousSize > 0 && newSize == 0) { if (previousSize > 0 && newSize == 0) {
attachmentsContainerView.collapse(true) attachmentsContainerView.collapse(true)
} else if (previousSize == 0 && newSize == 1) { } else if (previousSize == 0 && newSize == 1) {
attachmentsContainerView.expand(true) 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?) { override fun onDetach() {
// Populate entry views super.onDetach()
templateView.setEntryInfo(entryInfo)
// Manage attachments drawFactory = null
setAttachments(entryInfo?.attachments ?: listOf()) setOnDateClickListener = null
setOnPasswordGeneratorClickListener = null
setOnIconViewClickListener = null
setOnRemoveAttachment = null
setOnEditCustomField = null
} }
private fun retrieveEntryInfo(): EntryInfo { fun getEntryInfo(): EntryInfo {
val entryInfo = templateView.getEntryInfo() populateEntryWithViews()
entryInfo.attachments = getAttachments().toMutableList() return mEntryInfo
return entryInfo }
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)
}
}
}
} }
/* ------------- /* -------------
@@ -298,84 +451,78 @@ class EntryEditFragment: DatabaseFragment() {
* ------------- * -------------
*/ */
private fun getAttachments(): List<Attachment> { fun getAttachments(): List<Attachment> {
return attachmentsAdapter?.itemsList?.map { it.attachment } ?: listOf() 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 attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
attachmentsAdapter?.assignItems(attachments.map { attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
EntryAttachmentState(it, StreamDirection.UPLOAD) attachmentsAdapter.onDeleteButtonClickListener = { item ->
}) onDeleteItem.invoke(item.attachment)
attachmentsAdapter?.onDeleteButtonClickListener = { item ->
val attachment = item.attachment
removeAttachment(EntryAttachmentState(attachment, StreamDirection.DOWNLOAD))
mEntryEditViewModel.deleteAttachment(attachment)
} }
} }
private fun containsAttachment(): Boolean { fun containsAttachment(): Boolean {
return attachmentsAdapter?.isEmpty() != true return !attachmentsAdapter.isEmpty()
} }
private fun containsAttachment(attachment: EntryAttachmentState): Boolean { fun containsAttachment(attachment: EntryAttachmentState): Boolean {
return attachmentsAdapter?.contains(attachment) ?: false return attachmentsAdapter.contains(attachment)
} }
private fun putAttachment(attachment: EntryAttachmentState, fun putAttachment(attachment: EntryAttachmentState,
onPreviewLoaded: ((attachment: EntryAttachmentState) -> Unit)? = null) { onPreviewLoaded: (()-> Unit)? = null) {
// When only one attachment is allowed
if (!mAllowMultipleAttachments
&& attachment.downloadState == AttachmentState.START) {
attachmentsAdapter?.clear()
}
attachmentsContainerView.visibility = View.VISIBLE attachmentsContainerView.visibility = View.VISIBLE
attachmentsAdapter?.putItem(attachment) attachmentsAdapter.putItem(attachment)
attachmentsAdapter?.onBinaryPreviewLoaded = { attachmentsAdapter.onBinaryPreviewLoaded = {
onPreviewLoaded?.invoke(attachment) onPreviewLoaded?.invoke()
} }
} }
private fun removeAttachment(attachment: EntryAttachmentState) { fun removeAttachment(attachment: EntryAttachmentState) {
attachmentsAdapter?.removeItem(attachment) attachmentsAdapter.removeItem(attachment)
} }
private fun getAttachmentViewPosition(attachment: EntryAttachmentState, fun clearAttachments() {
position: (attachment: EntryAttachmentState, Float) -> Unit) { attachmentsAdapter.clear()
}
fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) {
attachmentsListView.postDelayed({ attachmentsListView.postDelayed({
attachmentsAdapter?.indexOf(attachment)?.let { index -> position.invoke(attachmentsContainerView.y
position.invoke(attachment, + attachmentsListView.y
attachmentsContainerView.y + (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y
+ attachmentsListView.y ?: 0F)
+ (attachmentsListView.getChildAt(index)?.y )
?: 0F)
)
}
}, 250) }, 250)
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
populateEntryWithViews()
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putParcelableArrayList(ATTACHMENTS_TAG, ArrayList(getAttachments()))
}
/* -------------
* Education
* -------------
*/
fun getActionImageView(): View? {
return templateView.getActionImageView()
}
fun launchGeneratePasswordEductionAction() {
mEntryEditViewModel.requestPasswordSelection(templateView.getPasswordField())
} }
companion object { 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,250 +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 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
}
uuidReferenceView = view.findViewById(R.id.entry_UUID_reference)
mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory ->
if (entryInfoHistory != null) {
templateView.setTemplate(entryInfoHistory.template)
assignEntryInfo(entryInfoHistory.entryInfo)
// Smooth appearing
rootView.showByFading()
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
}
}
mEntryViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState ->
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?) {
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,72 +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()
history?.let {
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.os.Bundle
import android.view.View import android.view.View
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.icon.IconImageCustom
@@ -32,8 +31,8 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
return R.layout.fragment_icon_grid return R.layout.fragment_icon_grid
} }
override fun defineIconList(database: Database?) { override fun defineIconList() {
database?.doForEachCustomIcons { customIcon, _ -> mDatabase?.doForEachCustomIcons { customIcon, _ ->
iconPickerAdapter.addIcon(customIcon, false) iconPickerAdapter.addIcon(customIcon, false)
} }
} }
@@ -55,10 +54,8 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
iconCustomAdded?.iconCustom?.let { icon -> iconCustomAdded?.iconCustom?.let { icon ->
iconPickerAdapter.addIcon(icon) iconPickerAdapter.addIcon(icon)
iconCustomAdded.iconCustom = null iconCustomAdded.iconCustom = null
try {
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
} catch (ignore: Exception) {}
} }
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
} }
} }
iconPickerViewModel.customIconRemoved.observe(viewLifecycleOwner) { iconCustomRemoved -> iconPickerViewModel.customIconRemoved.observe(viewLifecycleOwner) { iconCustomRemoved ->
@@ -69,14 +66,6 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
} }
} }
} }
iconPickerViewModel.customIconUpdated.observe(viewLifecycleOwner) { iconCustomUpdated ->
if (!iconCustomUpdated.error) {
iconCustomUpdated?.iconCustom?.let { icon ->
iconPickerAdapter.updateIcon(icon)
iconCustomUpdated.iconCustom = null
}
}
}
} }
override fun onIconClickListener(icon: IconImageCustom) { override fun onIconClickListener(icon: IconImageCustom) {

View File

@@ -19,6 +19,7 @@
*/ */
package com.kunzisoft.keepass.activities.fragments package com.kunzisoft.keepass.activities.fragments
import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -27,6 +28,7 @@ import android.view.ViewGroup
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.IconPickerAdapter import com.kunzisoft.keepass.adapters.IconPickerAdapter
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImageDraw import com.kunzisoft.keepass.database.element.icon.IconImageDraw
@@ -36,48 +38,39 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(), abstract class IconFragment<T: IconImageDraw> : StylishFragment(),
IconPickerAdapter.IconPickerListener<T> { IconPickerAdapter.IconPickerListener<T> {
protected lateinit var iconsGridView: RecyclerView protected lateinit var iconsGridView: RecyclerView
protected lateinit var iconPickerAdapter: IconPickerAdapter<T> protected lateinit var iconPickerAdapter: IconPickerAdapter<T>
protected var iconActionSelectionMode = false protected var iconActionSelectionMode = false
protected var mDatabase: Database? = null
protected val iconPickerViewModel: IconPickerViewModel by activityViewModels() protected val iconPickerViewModel: IconPickerViewModel by activityViewModels()
abstract fun retrieveMainLayoutId(): Int abstract fun retrieveMainLayoutId(): Int
abstract fun defineIconList(database: Database?) abstract fun defineIconList()
override fun onCreateView(inflater: LayoutInflater, override fun onAttach(context: Context) {
container: ViewGroup?, super.onAttach(context)
savedInstanceState: Bundle?): View {
return inflater.inflate(retrieveMainLayoutId(), container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { mDatabase = Database.getInstance()
super.onViewCreated(view, savedInstanceState)
// Retrieve the textColor to tint the icon // Retrieve the textColor to tint the icon
val ta = contextThemed?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) val ta = contextThemed?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
val tintColor = ta?.getColor(0, Color.BLACK) ?: Color.BLACK val tintColor = ta?.getColor(0, Color.BLACK) ?: Color.BLACK
ta?.recycle() ta?.recycle()
iconsGridView = view.findViewById(R.id.icons_grid_view) iconPickerAdapter = IconPickerAdapter<T>(context, tintColor).apply {
iconPickerAdapter = IconPickerAdapter(requireContext(), tintColor) iconDrawableFactory = mDatabase?.iconDrawableFactory
iconPickerAdapter.iconPickerListener = this }
iconsGridView.adapter = iconPickerAdapter
resetAppTimeoutWhenViewFocusedOrChanged(view)
}
override fun onDatabaseRetrieved(database: Database?) {
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val populateList = launch { val populateList = launch {
iconPickerAdapter.clear() iconPickerAdapter.clear()
defineIconList(database) defineIconList()
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
populateList.join() 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() { fun onIconDeleteClicked() {
iconActionSelectionMode = false 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.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.IconPickerPagerAdapter import com.kunzisoft.keepass.adapters.IconPickerPagerAdapter
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
class IconPickerFragment : DatabaseFragment() { class IconPickerFragment : StylishFragment() {
private var iconPickerPagerAdapter: IconPickerPagerAdapter? = null private var iconPickerPagerAdapter: IconPickerPagerAdapter? = null
private lateinit var viewPager: ViewPager2 private lateinit var viewPager: ViewPager2
private lateinit var tabLayout: TabLayout
private val iconPickerViewModel: IconPickerViewModel by activityViewModels() private val iconPickerViewModel: IconPickerViewModel by activityViewModels()
private var mDatabase: Database? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@@ -30,11 +32,19 @@ class IconPickerFragment : DatabaseFragment() {
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) mDatabase = Database.getInstance()
viewPager = view.findViewById(R.id.icon_picker_pager) viewPager = view.findViewById(R.id.icon_picker_pager)
tabLayout = view.findViewById(R.id.icon_picker_tabs) val tabLayout = view.findViewById<TabLayout>(R.id.icon_picker_tabs)
resetAppTimeoutWhenViewFocusedOrChanged(view) 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 { arguments?.apply {
if (containsKey(ICON_TAB_ARG)) { 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 { enum class IconTab {
STANDARD, CUSTOM STANDARD, CUSTOM
} }

View File

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

View File

@@ -20,43 +20,38 @@
package com.kunzisoft.keepass.activities.fragments package com.kunzisoft.keepass.activities.fragments
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.* import android.view.*
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.EntryEditActivity import com.kunzisoft.keepass.activities.EntryEditActivity
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper 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.helpers.SpecialMode
import com.kunzisoft.keepass.adapters.NodesAdapter 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.Database
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.GroupViewModel
import java.util.* import java.util.*
class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListener { class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
private var nodeClickListener: NodeClickListener? = null private var nodeClickListener: NodeClickListener? = null
private var onScrollListener: OnScrollListener? = null private var onScrollListener: OnScrollListener? = null
private var groupRefreshed: GroupRefreshedListener? = null
private var mNodesRecyclerView: RecyclerView? = null private var mNodesRecyclerView: RecyclerView? = null
private var mLayoutManager: LinearLayoutManager? = null var mainGroup: Group? = null
private var mAdapter: NodesAdapter? = null private set
private var mAdapter: NodeAdapter? = null
private val mGroupViewModel: GroupViewModel by activityViewModels()
private var mCurrentGroup: Group? = null
var nodeActionSelectionMode = false var nodeActionSelectionMode = false
private set private set
@@ -68,47 +63,21 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
private var notFoundView: View? = null private var notFoundView: View? = null
private var isASearchResult: Boolean = false private var isASearchResult: Boolean = false
private var readOnly: Boolean = false
private var specialMode: SpecialMode = SpecialMode.DEFAULT private var specialMode: SpecialMode = SpecialMode.DEFAULT
private var mRecycleBinEnable: Boolean = false val isEmpty: Boolean
private var mRecycleBin: Group? = null get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) { entryId ->
entryId?.let {
// Simply refresh the list
rebuildList()
// Scroll to the new entry
mDatabase?.getEntryById(it)?.let { entry ->
mAdapter?.indexOf(entry)?.let { position ->
mNodesRecyclerView?.scrollToPosition(position)
}
}
} ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result")
}
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) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
// TODO Change to ViewModel
try { try {
nodeClickListener = context as NodeClickListener nodeClickListener = context as NodeClickListener
} catch (e: ClassCastException) { } catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception // The activity doesn't implement the interface, throw exception
throw ClassCastException(context.toString() throw ClassCastException(context.toString()
+ " must implement " + NodesAdapter.NodeClickCallback::class.java.name) + " must implement " + NodeAdapter.NodeClickCallback::class.java.name)
} }
try { try {
@@ -116,24 +85,14 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
} catch (e: ClassCastException) { } catch (e: ClassCastException) {
onScrollListener = null onScrollListener = null
// Context menu can be omit // Context menu can be omit
Log.w( Log.w(TAG, context.toString()
TAG, context.toString()
+ " must implement " + RecyclerView.OnScrollListener::class.java.name) + " must implement " + RecyclerView.OnScrollListener::class.java.name)
} }
try {
groupRefreshed = context as GroupRefreshedListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context.toString()
+ " must implement " + GroupRefreshedListener::class.java.name)
}
} }
override fun onDetach() { override fun onDetach() {
nodeClickListener = null nodeClickListener = null
onScrollListener = null onScrollListener = null
groupRefreshed = null
super.onDetach() super.onDetach()
} }
@@ -141,138 +100,129 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)
}
override fun onDatabaseRetrieved(database: Database?) { readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments)
mRecycleBinEnable = database?.isRecycleBinEnabled == true
mRecycleBin = database?.recycleBin 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 -> contextThemed?.let { context ->
database?.let { database -> mAdapter = NodeAdapter(context)
mAdapter = NodesAdapter(context, database).apply { mAdapter?.apply {
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback { setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
override fun onNodeClick(database: Database, node: Node) { override fun onNodeClick(node: Node) {
if (nodeActionSelectionMode) { if (nodeActionSelectionMode) {
if (listActionNodes.contains(node)) { if (listActionNodes.contains(node)) {
// Remove selected item if already selected // Remove selected item if already selected
listActionNodes.remove(node) listActionNodes.remove(node)
} else {
// Add selected item if not already selected
listActionNodes.add(node)
}
nodeClickListener?.onNodeSelected(database, listActionNodes)
setActionNodes(listActionNodes)
notifyNodeChanged(node)
} else { } 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 { override fun onNodeLongClick(node: Node): Boolean {
if (nodeActionPasteMode == PasteMode.UNDEFINED) { if (nodeActionPasteMode == PasteMode.UNDEFINED) {
// Select the first item after a long click // Select the first item after a long click
if (!listActionNodes.contains(node)) if (!listActionNodes.contains(node))
listActionNodes.add(node) listActionNodes.add(node)
nodeClickListener?.onNodeSelected(database, listActionNodes) nodeClickListener?.onNodeSelected(listActionNodes)
setActionNodes(listActionNodes) setActionNodes(listActionNodes)
notifyNodeChanged(node) notifyNodeChanged(node)
}
return true
} }
}) return true
} }
mNodesRecyclerView?.adapter = mAdapter })
} }
} }
} }
override fun onDatabaseActionFinished( override fun onSaveInstanceState(outState: Bundle) {
database: Database, ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
actionTask: String, super.onSaveInstanceState(outState)
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 onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
// To apply theme // To apply theme
return inflater.cloneInContext(contextThemed) val rootView = inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_nodes, container, false) .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 { mNodesRecyclerView?.apply {
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
layoutManager = mLayoutManager layoutManager = LinearLayoutManager(context)
adapter = mAdapter adapter = mAdapter
} }
resetAppTimeoutWhenViewFocusedOrChanged(view)
mGroupViewModel.group.observe(viewLifecycleOwner) { onScrollListener?.let { onScrollListener ->
mCurrentGroup = it.group mNodesRecyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
isASearchResult = it.group.isVirtual override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
rebuildList() super.onScrolled(recyclerView, dx, dy)
it.showFromPosition?.let { position -> onScrollListener.onScrolled(dy)
mNodesRecyclerView?.scrollToPosition(position) }
} })
} }
return rootView
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
activity?.intent?.let { activity?.intent?.let {
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it) specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
} }
rebuildList() // Refresh data
}
override fun onPause() {
mNodesRecyclerView?.removeOnScrollListener(mRecycleViewScrollListener)
super.onPause()
}
fun getFirstVisiblePosition(): Int {
return mLayoutManager?.findFirstVisibleItemPosition() ?: 0
}
private fun rebuildList() {
try { try {
// Add elements to the list rebuildList()
mCurrentGroup?.let { mainGroup -> } catch (e: Exception) {
// Thrown an exception when sort cannot be performed Log.e(TAG, "Unable to rebuild the list during resume")
mAdapter?.rebuildList(mainGroup) e.printStackTrace()
}
} catch (e:Exception) {
Log.e(TAG, "Unable to rebuild the list", e)
} }
if (isASearchResult && mAdapter != null && mAdapter!!.isEmpty) { if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
// To show the " no search entry found " // To show the " no search entry found "
mNodesRecyclerView?.visibility = View.GONE
notFoundView?.visibility = View.VISIBLE notFoundView?.visibility = View.VISIBLE
} else { } else {
mNodesRecyclerView?.visibility = View.VISIBLE
notFoundView?.visibility = View.GONE notFoundView?.visibility = View.GONE
} }
}
groupRefreshed?.onGroupRefreshed() @Throws(IllegalArgumentException::class)
fun rebuildList() {
// Add elements to the list
mainGroup?.let { mainGroup ->
mAdapter?.apply {
// Thrown an exception when sort cannot be performed
rebuildList(mainGroup)
// To visually change the elements
if (PreferencesUtil.APPEARANCE_CHANGED) {
notifyDataSetChanged()
PreferencesUtil.APPEARANCE_CHANGED = false
}
}
}
} }
override fun onSortSelected(sortNodeEnum: SortNodeEnum, override fun onSortSelected(sortNodeEnum: SortNodeEnum,
@@ -287,7 +237,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters) mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
rebuildList() rebuildList()
} catch (e:Exception) { } 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()
} }
} }
@@ -303,19 +254,17 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
R.id.menu_sort -> { R.id.menu_sort -> {
context?.let { context -> context?.let { context ->
val sortDialogFragment: SortDialogFragment = val sortDialogFragment: SortDialogFragment =
if (mRecycleBinEnable) { if (Database.getInstance().isRecycleBinEnabled) {
SortDialogFragment.getInstance( SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context), PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context), PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context), PreferencesUtil.getGroupsBeforeSort(context),
PreferencesUtil.getRecycleBinBottomSort(context) PreferencesUtil.getRecycleBinBottomSort(context))
)
} else { } else {
SortDialogFragment.getInstance( SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context), PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context), PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context) PreferencesUtil.getGroupsBeforeSort(context))
)
} }
sortDialogFragment.show(childFragmentManager, "sortDialog") sortDialogFragment.show(childFragmentManager, "sortDialog")
@@ -327,32 +276,34 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
} }
} }
fun actionNodesCallback(database: Database, fun actionNodesCallback(nodes: List<Node>,
nodes: List<Node>,
menuListener: NodesActionMenuListener?, menuListener: NodesActionMenuListener?,
onDestroyActionMode: (mode: ActionMode?) -> Unit) : ActionMode.Callback { actionModeCallback: ActionMode.Callback) : ActionMode.Callback {
return object : ActionMode.Callback { return object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
nodeActionSelectionMode = false nodeActionSelectionMode = false
nodeActionPasteMode = PasteMode.UNDEFINED nodeActionPasteMode = PasteMode.UNDEFINED
return true return actionModeCallback.onCreateActionMode(mode, menu)
} }
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
menu?.clear() menu?.clear()
if (nodeActionPasteMode != PasteMode.UNDEFINED) { if (nodeActionPasteMode != PasteMode.UNDEFINED) {
mode?.menuInflater?.inflate(R.menu.node_paste_menu, menu) mode?.menuInflater?.inflate(R.menu.node_paste_menu, menu)
} else { } else {
nodeActionSelectionMode = true nodeActionSelectionMode = true
mode?.menuInflater?.inflate(R.menu.node_menu, menu) mode?.menuInflater?.inflate(R.menu.node_menu, menu)
val database = Database.getInstance()
// Open and Edit for a single item // Open and Edit for a single item
if (nodes.size == 1) { if (nodes.size == 1) {
// Edition // Edition
if (database.isReadOnly if (readOnly
|| (mRecycleBinEnable && nodes[0] == mRecycleBin)) { || (database.isRecycleBinEnabled && nodes[0] == database.recycleBin)) {
menu?.removeItem(R.id.menu_edit) menu?.removeItem(R.id.menu_edit)
} }
} else { } else {
@@ -360,59 +311,56 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
menu?.removeItem(R.id.menu_edit) menu?.removeItem(R.id.menu_edit)
} }
// Move // Copy and Move (not for groups)
if (database.isReadOnly if (readOnly
|| isASearchResult) { || isASearchResult
|| nodes.any { it.type == Type.GROUP }) {
// TODO Copy For Group
menu?.removeItem(R.id.menu_copy)
menu?.removeItem(R.id.menu_move) 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 // Deletion
if (database.isReadOnly if (readOnly
|| (mRecycleBinEnable && nodes.any { it == mRecycleBin })) { || (database.isRecycleBinEnabled && nodes.any { it == database.recycleBin })) {
menu?.removeItem(R.id.menu_delete) menu?.removeItem(R.id.menu_delete)
} }
} }
// Add the number of items selected in title // Add the number of items selected in title
mode?.title = nodes.size.toString() mode?.title = nodes.size.toString()
return true
return actionModeCallback.onPrepareActionMode(mode, menu)
} }
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
if (menuListener == null) if (menuListener == null)
return false return false
return when (item?.itemId) { return when (item?.itemId) {
R.id.menu_open -> menuListener.onOpenMenuClick(database, nodes[0]) R.id.menu_open -> menuListener.onOpenMenuClick(nodes[0])
R.id.menu_edit -> menuListener.onEditMenuClick(database, nodes[0]) R.id.menu_edit -> menuListener.onEditMenuClick(nodes[0])
R.id.menu_copy -> { R.id.menu_copy -> {
nodeActionPasteMode = PasteMode.PASTE_FROM_COPY nodeActionPasteMode = PasteMode.PASTE_FROM_COPY
mAdapter?.unselectActionNodes() mAdapter?.unselectActionNodes()
val returnValue = menuListener.onCopyMenuClick(database, nodes) val returnValue = menuListener.onCopyMenuClick(nodes)
nodeActionSelectionMode = false nodeActionSelectionMode = false
returnValue returnValue
} }
R.id.menu_move -> { R.id.menu_move -> {
nodeActionPasteMode = PasteMode.PASTE_FROM_MOVE nodeActionPasteMode = PasteMode.PASTE_FROM_MOVE
mAdapter?.unselectActionNodes() mAdapter?.unselectActionNodes()
val returnValue = menuListener.onMoveMenuClick(database, nodes) val returnValue = menuListener.onMoveMenuClick(nodes)
nodeActionSelectionMode = false nodeActionSelectionMode = false
returnValue returnValue
} }
R.id.menu_delete -> menuListener.onDeleteMenuClick(database, nodes) R.id.menu_delete -> menuListener.onDeleteMenuClick(nodes)
R.id.menu_paste -> { R.id.menu_paste -> {
val returnValue = menuListener.onPasteMenuClick(database, nodeActionPasteMode, nodes) val returnValue = menuListener.onPasteMenuClick(nodeActionPasteMode, nodes)
nodeActionPasteMode = PasteMode.UNDEFINED nodeActionPasteMode = PasteMode.UNDEFINED
nodeActionSelectionMode = false nodeActionSelectionMode = false
returnValue returnValue
} }
else -> false else -> actionModeCallback.onActionItemClicked(mode, item)
} }
} }
@@ -422,29 +370,83 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
mAdapter?.unselectActionNodes() mAdapter?.unselectActionNodes()
nodeActionPasteMode = PasteMode.UNDEFINED nodeActionPasteMode = PasteMode.UNDEFINED
nodeActionSelectionMode = false nodeActionSelectionMode = false
onDestroyActionMode(mode) actionModeCallback.onDestroyActionMode(mode)
} }
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
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 * Callback listener to redefine to do an action when a node is click
*/ */
interface NodeClickListener { interface NodeClickListener {
fun onNodeClick(database: Database, node: Node) fun onNodeClick(node: Node)
fun onNodeSelected(database: Database, nodes: List<Node>): Boolean fun onNodeSelected(nodes: List<Node>): Boolean
} }
/** /**
* Menu listener to redefine to do an action in menu * Menu listener to redefine to do an action in menu
*/ */
interface NodesActionMenuListener { interface NodesActionMenuListener {
fun onOpenMenuClick(database: Database, node: Node): Boolean fun onOpenMenuClick(node: Node): Boolean
fun onEditMenuClick(database: Database, node: Node): Boolean fun onEditMenuClick(node: Node): Boolean
fun onCopyMenuClick(database: Database, nodes: List<Node>): Boolean fun onCopyMenuClick(nodes: List<Node>): Boolean
fun onMoveMenuClick(database: Database, nodes: List<Node>): Boolean fun onMoveMenuClick(nodes: List<Node>): Boolean
fun onDeleteMenuClick(database: Database, nodes: List<Node>): Boolean fun onDeleteMenuClick(nodes: List<Node>): Boolean
fun onPasteMenuClick(database: Database, pasteMode: PasteMode?, nodes: List<Node>): Boolean fun onPasteMenuClick(pasteMode: PasteMode?, nodes: List<Node>): Boolean
} }
enum class PasteMode { enum class PasteMode {
@@ -462,11 +464,23 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
fun onScrolled(dy: Int) fun onScrolled(dy: Int)
} }
interface GroupRefreshedListener {
fun onGroupRefreshed()
}
companion object { 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,225 +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.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.View
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
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
private var getContentResultLauncher: ActivityResultLauncher<String>? = null
private var openDocumentResultLauncher: ActivityResultLauncher<Array<String>>? = null
private var createDocumentResultLauncher: ActivityResultLauncher<String>? = null
constructor(context: FragmentActivity) {
this.activity = context
this.fragment = null
}
constructor(context: Fragment) {
this.activity = context.activity
this.fragment = context
}
fun buildOpenDocument(onFileSelected: ((uri: Uri?) -> Unit)?) {
val resultCallback = ActivityResultCallback<Uri> { result ->
result?.let { uri ->
UriUtil.takeUriPermission(activity?.contentResolver, uri)
onFileSelected?.invoke(uri)
}
}
getContentResultLauncher = if (fragment != null) {
fragment?.registerForActivityResult(
GetContent(),
resultCallback
)
} else {
activity?.registerForActivityResult(
GetContent(),
resultCallback
)
}
openDocumentResultLauncher = if (fragment != null) {
fragment?.registerForActivityResult(
OpenDocument(),
resultCallback
)
} else {
activity?.registerForActivityResult(
OpenDocument(),
resultCallback
)
}
}
fun buildCreateDocument(typeString: String = "application/octet-stream",
onFileCreated: (fileCreated: Uri?)->Unit) {
val resultCallback = ActivityResultCallback<Uri> { result ->
onFileCreated.invoke(result)
}
createDocumentResultLauncher = if (fragment != null) {
fragment?.registerForActivityResult(
CreateDocument(typeString),
resultCallback
)
} else {
activity?.registerForActivityResult(
CreateDocument(typeString),
resultCallback
)
}
}
fun openDocument(getContent: Boolean = false,
typeString: String = "*/*") {
try {
if (getContent) {
getContentResultLauncher?.launch(typeString)
} else {
openDocumentResultLauncher?.launch(arrayOf(typeString))
}
} catch (e: Exception) {
Log.e(TAG, "Unable to open document", e)
showFileManagerDialogFragment()
}
}
fun createDocument(titleString: String) {
try {
createDocumentResultLauncher?.launch(titleString)
} catch (e: Exception) {
Log.e(TAG, "Unable to create document", e)
showFileManagerDialogFragment()
}
}
/**
* 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)
}
}
class OpenDocument : ActivityResultContracts.OpenDocument() {
@SuppressLint("InlinedApi")
override fun createIntent(context: Context, input: Array<out String>): Intent {
return super.createIntent(context, input).apply {
addCategory(Intent.CATEGORY_OPENABLE)
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)
}
}
}
class GetContent : ActivityResultContracts.GetContent() {
@SuppressLint("InlinedApi")
override fun createIntent(context: Context, input: String): Intent {
return super.createIntent(context, input).apply {
addCategory(Intent.CATEGORY_OPENABLE)
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)
}
}
}
class CreateDocument(private val typeString: String) : ActivityResultContracts.CreateDocument() {
override fun createIntent(context: Context, input: String): Intent {
return super.createIntent(context, input).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
}
}
}
companion object {
private const val TAG = "OpenFileHelper"
@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(false)
}
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,80 +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.model.MainCredential
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
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 ->
val databaseWasReloaded = database?.wasReloaded == true
if (databaseWasReloaded && finishActivityIfReloadRequested()) {
finish()
} else if (mDatabase == null || mDatabase != database || databaseWasReloaded) {
database?.wasReloaded = false
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 onResume() {
super.onResume()
mDatabaseTaskProvider?.registerProgressTask()
}
override fun onPause() {
mDatabaseTaskProvider?.unregisterProgressTask()
super.onPause()
}
}

View File

@@ -1,468 +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?.startDatabaseSaveDefaultUsername(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
mExitLock = true
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)
}
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 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.os.Bundle
import android.view.View 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.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.helpers.TypeMode import com.kunzisoft.keepass.activities.helpers.TypeMode
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.SpecialModeView 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 protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
private var mTypeMode: TypeMode = TypeMode.DEFAULT private var mTypeMode: TypeMode = TypeMode.DEFAULT

View File

@@ -37,16 +37,12 @@ object Stylish {
* Initialize the class with a theme preference * Initialize the class with a theme preference
* @param context Context to retrieve the 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) Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
try { themeString = PreferencesUtil.getStyle(context)
themeString = PreferencesUtil.getStyle(context)
} catch (e: Exception) {
Log.e("Stylish", "Unable to get preference style", e)
}
} }
fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String { private fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
val systemNightMode = when (PreferencesUtil.getStyleBrightness(context)) { val systemNightMode = when (PreferencesUtil.getStyleBrightness(context)) {
context.getString(R.string.list_style_brightness_light) -> false context.getString(R.string.list_style_brightness_light) -> false
context.getString(R.string.list_style_brightness_night) -> true context.getString(R.string.list_style_brightness_night) -> true
@@ -88,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 * Assign the style to the class attribute
* @param styleString Style id String * @param styleString Style id String
*/ */
fun assignStyle(context: Context, styleString: 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.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.os.Bundle 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.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_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 * 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 @StyleRes
private var themeId: Int = 0 private var themeId: Int = 0
private var customStyle = true
/* (non-Javadoc) Workaround for HTC Linkify issues /* (non-Javadoc) Workaround for HTC Linkify issues
* @see android.app.Activity#startActivity(android.content.Intent) * @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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
this.themeId = Stylish.getThemeId(this)
customStyle = applyCustomStyle() setTheme(themeId)
if (customStyle) {
this.themeId = Stylish.getThemeId(this)
setTheme(themeId)
}
// Several gingerbread devices have problems with FLAG_SECURE // Several gingerbread devices have problems with FLAG_SECURE
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
@@ -87,17 +63,9 @@ abstract class StylishActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (Stylish.getThemeId(this) != this.themeId) {
if ((customStyle && Stylish.getThemeId(this) != this.themeId)
|| DATABASE_PREFERENCE_CHANGED) {
DATABASE_PREFERENCE_CHANGED = false
Log.d(this.javaClass.name, "Theme change detected, restarting activity") 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

@@ -23,12 +23,12 @@ import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.annotation.StyleRes
import androidx.fragment.app.Fragment
import androidx.appcompat.view.ContextThemeWrapper
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.StyleRes
import androidx.appcompat.view.ContextThemeWrapper
import androidx.fragment.app.Fragment
abstract class StylishFragment : Fragment() { abstract class StylishFragment : Fragment() {
@@ -42,6 +42,7 @@ abstract class StylishFragment : Fragment() {
contextThemed = ContextThemeWrapper(context, themeId) contextThemed = ContextThemeWrapper(context, themeId)
} }
@Suppress("DEPRECATION")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// To fix status bar color // To fix status bar color
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
@@ -57,7 +58,6 @@ abstract class StylishFragment : Fragment() {
try { try {
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar)) val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
if (taWindowStatusLight?.getBoolean(0, false) == true) { if (taWindowStatusLight?.getBoolean(0, false) == true) {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
} }
taWindowStatusLight?.recycle() taWindowStatusLight?.recycle()

View File

@@ -1,150 +0,0 @@
package com.kunzisoft.keepass.adapters
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
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.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.strikeOut
class BreadcrumbAdapter(val context: Context)
: RecyclerView.Adapter<BreadcrumbAdapter.BreadcrumbGroupViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
var iconDrawableFactory: IconDrawableFactory? = null
@SuppressLint("NotifyDataSetChanged")
set(value) {
field = value
notifyDataSetChanged()
}
private var mNodeBreadcrumb: MutableList<Node?> = mutableListOf()
var onItemClickListener: ((item: Node, position: Int)->Unit)? = null
var onLongItemClickListener: ((item: Node, position: Int)->Unit)? = null
private var mShowNumberEntries = false
private var mShowUUID = false
private var mIconColor: Int = 0
init {
mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
mShowUUID = PreferencesUtil.showUUID(context)
// Retrieve the textColor to tint the icon
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
mIconColor = taTextColor.getColor(0, Color.WHITE)
taTextColor.recycle()
}
@SuppressLint("NotifyDataSetChanged")
fun setNode(node: Node?) {
mNodeBreadcrumb.clear()
node?.let {
var currentNode = it
mNodeBreadcrumb.add(0, currentNode)
while (currentNode.containsParent()) {
currentNode.parent?.let { parent ->
currentNode = parent
mNodeBreadcrumb.add(0, currentNode)
}
}
}
notifyDataSetChanged()
}
override fun getItemViewType(position: Int): Int {
return when (position) {
mNodeBreadcrumb.size - 1 -> 0
else -> 1
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BreadcrumbGroupViewHolder {
return BreadcrumbGroupViewHolder(inflater.inflate(
when (viewType) {
0 -> R.layout.item_group
else -> R.layout.item_breadcrumb
}, parent, false)
)
}
override fun onBindViewHolder(holder: BreadcrumbGroupViewHolder, position: Int) {
val node = mNodeBreadcrumb[position]
holder.groupNameView.apply {
text = node?.title ?: ""
strikeOut(node?.isCurrentlyExpires ?: false)
}
holder.itemView.apply {
setOnClickListener {
node?.let {
onItemClickListener?.invoke(it, position)
}
}
setOnLongClickListener {
node?.let {
onLongItemClickListener?.invoke(it, position)
}
true
}
}
if (node?.type == Type.GROUP) {
(node as Group).let { group ->
holder.groupIconView?.let { imageView ->
iconDrawableFactory?.assignDatabaseIcon(
imageView,
group.icon,
mIconColor
)
}
holder.groupNumbersView?.apply {
if (mShowNumberEntries) {
group.refreshNumberOfChildEntries(Group.ChildFilter.getDefaults(context))
text = group.numberOfChildEntries.toString()
visibility = View.VISIBLE
} else {
visibility = View.GONE
}
}
holder.groupMetaView?.apply {
val meta = group.nodeId.toVisualString()
visibility = if (meta != null
&& !group.isVirtual
&& mShowUUID
) {
text = meta
View.VISIBLE
} else {
View.GONE
}
}
}
}
}
override fun getItemCount(): Int {
return mNodeBreadcrumb.size
}
inner class BreadcrumbGroupViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var groupIconView: ImageView? = itemView.findViewById(R.id.group_icon)
var groupNumbersView: TextView? = itemView.findViewById(R.id.group_numbers)
var groupNameView: TextView = itemView.findViewById(R.id.group_name)
var groupMetaView: TextView? = itemView.findViewById(R.id.group_meta)
}
}

View File

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

View File

@@ -30,8 +30,6 @@ import android.view.ViewGroup
import android.widget.* import android.widget.*
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.DatabaseFile import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.view.collapse import com.kunzisoft.keepass.view.collapse
@@ -46,43 +44,11 @@ class FileDatabaseHistoryAdapter(context: Context)
private var fileSelectClearListener: ((DatabaseFile)->Boolean)? = null private var fileSelectClearListener: ((DatabaseFile)->Boolean)? = null
private var saveAliasListener: ((DatabaseFile)->Unit)? = null private var saveAliasListener: ((DatabaseFile)->Unit)? = null
private var mDefaultDatabase: DatabaseFile? = null private val listDatabaseFiles = ArrayList<DatabaseFile>()
private var mExpandedDatabaseFile: SuperDatabaseFile? = null
private var mPreviousExpandedDatabaseFile: SuperDatabaseFile? = null
private val mListPosition = mutableListOf<SuperDatabaseFile>() private var mDefaultDatabaseFile: DatabaseFile? = null
private val mSortedListDatabaseFiles = SortedList(SuperDatabaseFile::class.java, private var mExpandedDatabaseFile: DatabaseFile? = null
object: SortedListAdapterCallback<SuperDatabaseFile>(this) { private var mPreviousExpandedDatabaseFile: DatabaseFile? = null
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
}
}
)
@ColorInt @ColorInt
private val defaultColor: Int private val defaultColor: Int
@@ -105,8 +71,7 @@ class FileDatabaseHistoryAdapter(context: Context)
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) { override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
// Get info from position // Get info from position
val superDatabaseFile = mSortedListDatabaseFiles[position] val databaseFile = listDatabaseFiles[position]
val databaseFile = superDatabaseFile.databaseFile
// Click item to open file // Click item to open file
holder.fileContainer.setOnClickListener { holder.fileContainer.setOnClickListener {
@@ -115,7 +80,7 @@ class FileDatabaseHistoryAdapter(context: Context)
// Default database // Default database
holder.defaultFileButton.apply { holder.defaultFileButton.apply {
this.isChecked = superDatabaseFile.default this.isChecked = mDefaultDatabaseFile == databaseFile
setOnClickListener { setOnClickListener {
defaultDatabaseListener?.invoke(if (isChecked) databaseFile else null) defaultDatabaseListener?.invoke(if (isChecked) databaseFile else null)
} }
@@ -150,7 +115,7 @@ class FileDatabaseHistoryAdapter(context: Context)
} }
// Click on information // Click on information
val isExpanded = superDatabaseFile == mExpandedDatabaseFile val isExpanded = databaseFile == mExpandedDatabaseFile
// Hides or shows info // Hides or shows info
holder.fileExpandContainer.apply { holder.fileExpandContainer.apply {
if (isExpanded) { if (isExpanded) {
@@ -186,16 +151,16 @@ class FileDatabaseHistoryAdapter(context: Context)
} }
if (isExpanded) { if (isExpanded) {
mPreviousExpandedDatabaseFile = superDatabaseFile mPreviousExpandedDatabaseFile = databaseFile
} }
holder.fileInformationButton.apply { holder.fileInformationButton.apply {
animate().rotation(if (isExpanded) 180F else 0F).start() animate().rotation(if (isExpanded) 180F else 0F).start()
setOnClickListener { setOnClickListener {
mExpandedDatabaseFile = if (isExpanded) null else superDatabaseFile mExpandedDatabaseFile = if (isExpanded) null else databaseFile
// Notify change // Notify change
val previousExpandedPosition = mListPosition.indexOf(mPreviousExpandedDatabaseFile) val previousExpandedPosition = listDatabaseFiles.indexOf(mPreviousExpandedDatabaseFile)
notifyItemChanged(previousExpandedPosition) notifyItemChanged(previousExpandedPosition)
val expandedPosition = mListPosition.indexOf(mExpandedDatabaseFile) val expandedPosition = listDatabaseFiles.indexOf(mExpandedDatabaseFile)
notifyItemChanged(expandedPosition) notifyItemChanged(expandedPosition)
} }
} }
@@ -207,67 +172,50 @@ class FileDatabaseHistoryAdapter(context: Context)
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {
return mSortedListDatabaseFiles.size() return listDatabaseFiles.size
} }
fun clearDatabaseFileHistoryList() { fun clearDatabaseFileHistoryList() {
mListPosition.clear() listDatabaseFiles.clear()
mSortedListDatabaseFiles.clear()
} }
fun addDatabaseFileHistory(fileDatabaseHistoryToAdd: DatabaseFile) { fun addDatabaseFileHistory(fileDatabaseHistoryToAdd: DatabaseFile) {
val superToAdd = SuperDatabaseFile(fileDatabaseHistoryToAdd) listDatabaseFiles.add(0, fileDatabaseHistoryToAdd)
mListPosition.add(0, superToAdd) notifyItemInserted(0)
mSortedListDatabaseFiles.add(superToAdd)
} }
fun updateDatabaseFileHistory(fileDatabaseHistoryToUpdate: DatabaseFile) { fun updateDatabaseFileHistory(fileDatabaseHistoryToUpdate: DatabaseFile) {
val superToUpdate = SuperDatabaseFile(fileDatabaseHistoryToUpdate) val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToUpdate)
val index = mListPosition.indexOf(superToUpdate) if (listDatabaseFiles.remove(fileDatabaseHistoryToUpdate)) {
if (mListPosition.remove(superToUpdate)) { listDatabaseFiles.add(index, fileDatabaseHistoryToUpdate)
mListPosition.add(index, superToUpdate) notifyItemChanged(index)
} }
mSortedListDatabaseFiles.updateItemAt(index, superToUpdate)
} }
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: DatabaseFile) { fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: DatabaseFile) {
val superToDelete = SuperDatabaseFile(fileDatabaseHistoryToDelete) val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToDelete)
val index = mListPosition.indexOf(superToDelete) if (listDatabaseFiles.remove(fileDatabaseHistoryToDelete)) {
mListPosition.remove(superToDelete) notifyItemRemoved(index)
mSortedListDatabaseFiles.removeItemAt(index) }
} }
fun replaceAllDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<DatabaseFile>) { fun replaceAllDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<DatabaseFile>) {
val superMapToReplace = listFileDatabaseHistoryToAdd.map { if (listDatabaseFiles.isEmpty()) {
SuperDatabaseFile(it) 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?) { fun setDefaultDatabase(databaseUri: Uri?) {
// Remove default from last item val defaultDatabaseFile = listDatabaseFiles.firstOrNull { it.databaseUri == databaseUri }
val oldDefaultDatabasePosition = mListPosition.indexOfFirst { mDefaultDatabaseFile = defaultDatabaseFile
it.default notifyDataSetChanged()
}
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)
}
} }
fun setOnDefaultDatabaseListener(listener: ((DatabaseFile?) -> Unit)?) { fun setOnDefaultDatabaseListener(listener: ((DatabaseFile?) -> Unit)?) {
@@ -286,30 +234,6 @@ class FileDatabaseHistoryAdapter(context: Context)
this.saveAliasListener = listener 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) { class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info) 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.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.icon.IconImageDraw 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) { override fun onBindViewHolder(holder: CustomIconViewHolder, position: Int) {
val icon = iconList[position] val icon = iconList[position]
iconDrawableFactory?.assignDatabaseIcon(holder.iconImageView, icon, tintIcon) 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.iconContainerView.isSelected = icon.selected
holder.itemView.setOnClickListener { holder.itemView.setOnClickListener {
iconPickerListener?.onIconClickListener(icon) 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) { inner class CustomIconViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var iconContainerView: ViewGroup = itemView.findViewById(R.id.icon_container) var iconContainerView: ViewGroup = itemView.findViewById(R.id.icon_container)
var iconImageView: ImageView = itemView.findViewById(R.id.icon_image) var iconImageView: ImageView = itemView.findViewById(R.id.icon_image)
var iconTextView: TextView = itemView.findViewById(R.id.icon_name)
} }
} }

View File

@@ -26,15 +26,12 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedList import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback import androidx.recyclerview.widget.SortedListAdapterCallback
import com.google.android.material.progressindicator.CircularProgressIndicator
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
@@ -43,11 +40,7 @@ import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.setTextSize import com.kunzisoft.keepass.view.setTextSize
import com.kunzisoft.keepass.view.strikeOut import com.kunzisoft.keepass.view.strikeOut
import java.util.* import java.util.*
@@ -56,9 +49,8 @@ import java.util.*
* Create node list adapter with contextMenu or not * Create node list adapter with contextMenu or not
* @param context Context to use * @param context Context to use
*/ */
class NodesAdapter (private val context: Context, class NodeAdapter (private val context: Context)
private val database: Database) : RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
: RecyclerView.Adapter<NodesAdapter.NodeViewHolder>() {
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
private val mNodeSortedListCallback: NodeSortedListCallback private val mNodeSortedListCallback: NodeSortedListCallback
@@ -68,37 +60,26 @@ class NodesAdapter (private val context: Context,
private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
private var mPrefSizeMultiplier: Float = 0F private var mPrefSizeMultiplier: Float = 0F
private var mTextDefaultDimension: Float = 0F private var mSubtextDefaultDimension: Float = 0F
private var mSubTextDefaultDimension: Float = 0F private var mInfoTextDefaultDimension: Float = 0F
private var mMetaTextDefaultDimension: Float = 0F
private var mOtpTokenTextDefaultDimension: Float = 0F
private var mNumberChildrenTextDefaultDimension: Float = 0F private var mNumberChildrenTextDefaultDimension: Float = 0F
private var mIconDefaultDimension: Float = 0F private var mIconDefaultDimension: Float = 0F
private var mShowUserNames: Boolean = true private var mShowUserNames: Boolean = true
private var mShowNumberEntries: 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 mEntryFilters = arrayOf<Group.ChildFilter>()
private var mOldVirtualGroup = false
private var mVirtualGroup = false
private var mActionNodesList = LinkedList<Node>() private var mActionNodesList = LinkedList<Node>()
private var mNodeClickCallback: NodeClickCallback? = null private var mNodeClickCallback: NodeClickCallback? = null
private var mClipboardHelper = ClipboardHelper(context)
private val mDatabase: Database
@ColorInt @ColorInt
private val mContentSelectionColor: Int private val mContentSelectionColor: Int
@ColorInt @ColorInt
private val mTextColorPrimary: Int private val mIconGroupColor: Int
@ColorInt @ColorInt
private val mTextColor: Int private val mIconEntryColor: Int
@ColorInt
private val mTextColorSecondary: Int
@ColorInt
private val mColorAccentLight: Int
@ColorInt
private val mTextColorInverse: Int
/** /**
* Determine if the adapter contains or not any element * Determine if the adapter contains or not any element
@@ -115,31 +96,22 @@ class NodesAdapter (private val context: Context,
this.mNodeSortedListCallback = NodeSortedListCallback() this.mNodeSortedListCallback = NodeSortedListCallback()
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback) this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback)
// Database
this.mDatabase = Database.getInstance()
// Color of content selection // Color of content selection
this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white) this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white)
// Retrieve the color to tint the icon // Retrieve the color to tint the icon
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary)) val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
this.mTextColorPrimary = taTextColorPrimary.getColor(0, Color.BLACK) this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
taTextColorPrimary.recycle() taTextColorPrimary.recycle()
// To get text color // In two times to fix bug compilation
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
this.mTextColor = taTextColor.getColor(0, Color.BLACK) this.mIconEntryColor = taTextColor.getColor(0, Color.BLACK)
taTextColor.recycle() taTextColor.recycle()
// To get text color secondary
val taTextColorSecondary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorSecondary))
this.mTextColorSecondary = taTextColorSecondary.getColor(0, Color.BLACK)
taTextColorSecondary.recycle()
// To get background color for selection
val taSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccentLight))
this.mColorAccentLight = taSelectionColor.getColor(0, Color.GRAY)
taSelectionColor.recycle()
// To get text color for selection
val taSelectionTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
this.mTextColorInverse = taSelectionTextColor.getColor(0, Color.WHITE)
taSelectionTextColor.recycle()
} }
private fun assignPreferences() { fun assignPreferences() {
this.mPrefSizeMultiplier = PreferencesUtil.getListTextSize(context) this.mPrefSizeMultiplier = PreferencesUtil.getListTextSize(context)
notifyChangeSort( notifyChangeSort(
@@ -153,8 +125,6 @@ class NodesAdapter (private val context: Context,
this.mShowUserNames = PreferencesUtil.showUsernamesListEntries(context) this.mShowUserNames = PreferencesUtil.showUsernamesListEntries(context)
this.mShowNumberEntries = PreferencesUtil.showNumberEntries(context) this.mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
this.mShowOTP = PreferencesUtil.showOTPToken(context)
this.mShowUUID = PreferencesUtil.showUUID(context)
this.mEntryFilters = Group.ChildFilter.getDefaults(context) this.mEntryFilters = Group.ChildFilter.getDefaults(context)
@@ -166,8 +136,6 @@ class NodesAdapter (private val context: Context,
* Rebuild the list by clear and build children from the group * Rebuild the list by clear and build children from the group
*/ */
fun rebuildList(group: Group) { fun rebuildList(group: Group) {
mOldVirtualGroup = mVirtualGroup
mVirtualGroup = group.isVirtual
assignPreferences() assignPreferences()
mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters)) mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters))
} }
@@ -178,26 +146,9 @@ class NodesAdapter (private val context: Context,
} }
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean { override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
if (mOldVirtualGroup != mVirtualGroup) return oldItem.type == newItem.type
return false
var typeContentTheSame = true
if (oldItem is Entry && newItem is Entry) {
typeContentTheSame = oldItem.getVisualTitle() == newItem.getVisualTitle()
&& oldItem.username == newItem.username
&& oldItem.backgroundColor == newItem.backgroundColor
&& oldItem.foregroundColor == newItem.foregroundColor
&& oldItem.getOtpElement() == newItem.getOtpElement()
&& oldItem.containsAttachment() == newItem.containsAttachment()
} else if (oldItem is Group && newItem is Group) {
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
&& oldItem.notes == newItem.notes
}
return typeContentTheSame
&& oldItem.nodeId == newItem.nodeId
&& oldItem.type == newItem.type
&& oldItem.title == newItem.title && oldItem.title == newItem.title
&& oldItem.icon == newItem.icon && oldItem.icon == newItem.icon
&& oldItem.isCurrentlyExpires == newItem.isCurrentlyExpires
} }
override fun areItemsTheSame(item1: Node, item2: Node): Boolean { override fun areItemsTheSame(item1: Node, item2: Node): Boolean {
@@ -290,10 +241,6 @@ class NodesAdapter (private val context: Context,
mNodeSortedList.endBatchedUpdates() mNodeSortedList.endBatchedUpdates()
} }
fun indexOf(node: Node): Int {
return mNodeSortedList.indexOf(node)
}
fun notifyNodeChanged(node: Node) { fun notifyNodeChanged(node: Node) {
notifyItemChanged(mNodeSortedList.indexOf(node)) notifyItemChanged(mNodeSortedList.indexOf(node))
} }
@@ -319,7 +266,7 @@ class NodesAdapter (private val context: Context,
*/ */
fun notifyChangeSort(sortNodeEnum: SortNodeEnum, fun notifyChangeSort(sortNodeEnum: SortNodeEnum,
sortNodeParameters: SortNodeEnum.SortNodeParameters) { sortNodeParameters: SortNodeEnum.SortNodeParameters) {
this.mNodeComparator = sortNodeEnum.getNodeComparator(database, sortNodeParameters) this.mNodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters)
} }
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
@@ -333,10 +280,8 @@ class NodesAdapter (private val context: Context,
mInflater.inflate(R.layout.item_list_nodes_entry, parent, false) mInflater.inflate(R.layout.item_list_nodes_entry, parent, false)
} }
val nodeViewHolder = NodeViewHolder(view) val nodeViewHolder = NodeViewHolder(view)
mTextDefaultDimension = nodeViewHolder.text.textSize mInfoTextDefaultDimension = nodeViewHolder.text.textSize
mSubTextDefaultDimension = nodeViewHolder.subText?.textSize ?: mSubTextDefaultDimension mSubtextDefaultDimension = nodeViewHolder.subText.textSize
mMetaTextDefaultDimension = nodeViewHolder.meta.textSize
mOtpTokenTextDefaultDimension = nodeViewHolder.otpToken?.textSize ?: mOtpTokenTextDefaultDimension
nodeViewHolder.numberChildren?.let { nodeViewHolder.numberChildren?.let {
mNumberChildrenTextDefaultDimension = it.textSize mNumberChildrenTextDefaultDimension = it.textSize
} }
@@ -347,20 +292,18 @@ class NodesAdapter (private val context: Context,
val subNode = mNodeSortedList.get(position) val subNode = mNodeSortedList.get(position)
// Node selection // Node selection
holder.container.apply { holder.container.isSelected = mActionNodesList.contains(subNode)
isSelected = mActionNodesList.contains(subNode)
}
// Assign image // Assign image
val iconColor = if (holder.container.isSelected) val iconColor = if (holder.container.isSelected)
mContentSelectionColor mContentSelectionColor
else when (subNode.type) { else when (subNode.type) {
Type.GROUP -> mTextColorPrimary Type.GROUP -> mIconGroupColor
Type.ENTRY -> mTextColor Type.ENTRY -> mIconEntryColor
} }
holder.imageIdentifier?.setColorFilter(iconColor) holder.imageIdentifier?.setColorFilter(iconColor)
holder.icon.apply { holder.icon.apply {
database.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor) mDatabase.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
// Relative size of the icon // Relative size of the icon
layoutParams?.apply { layoutParams?.apply {
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt() height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
@@ -371,116 +314,35 @@ class NodesAdapter (private val context: Context,
// Assign text // Assign text
holder.text.apply { holder.text.apply {
text = subNode.title text = subNode.title
setTextSize(mTextSizeUnit, mTextDefaultDimension, mPrefSizeMultiplier) setTextSize(mTextSizeUnit, mInfoTextDefaultDimension, mPrefSizeMultiplier)
strikeOut(subNode.isCurrentlyExpires) strikeOut(subNode.isCurrentlyExpires)
} }
// Add meta text to show UUID // Add subText with username
holder.meta.apply { holder.subText.apply {
val nodeId = subNode.nodeId?.toVisualString() text = ""
if (mShowUUID && nodeId != null) { strikeOut(subNode.isCurrentlyExpires)
text = nodeId visibility = View.GONE
setTextSize(mTextSizeUnit, mMetaTextDefaultDimension, mPrefSizeMultiplier)
visibility = View.VISIBLE
} else {
visibility = View.GONE
}
}
// Add path to virtual group
if (mVirtualGroup) {
holder.path?.apply {
text = subNode.getPathString()
visibility = View.VISIBLE
}
} else {
holder.path?.visibility = View.GONE
} }
// Specific elements for entry // Specific elements for entry
if (subNode.type == Type.ENTRY) { if (subNode.type == Type.ENTRY) {
val entry = subNode as Entry val entry = subNode as Entry
database.startManageEntry(entry) mDatabase.startManageEntry(entry)
holder.text.text = entry.getVisualTitle() holder.text.text = entry.getVisualTitle()
// Add subText with username holder.subText.apply {
holder.subText?.apply {
val username = entry.username val username = entry.username
if (mShowUserNames && username.isNotEmpty()) { if (mShowUserNames && username.isNotEmpty()) {
visibility = View.VISIBLE visibility = View.VISIBLE
text = username text = username
setTextSize(mTextSizeUnit, mSubTextDefaultDimension, mPrefSizeMultiplier) setTextSize(mTextSizeUnit, mSubtextDefaultDimension, mPrefSizeMultiplier)
strikeOut(subNode.isCurrentlyExpires)
} else {
visibility = View.GONE
} }
} }
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 = holder.attachmentIcon?.visibility =
if (entry.containsAttachment()) View.VISIBLE else View.GONE if (entry.containsAttachment()) View.VISIBLE else View.GONE
// Assign colors mDatabase.stopManageEntry(entry)
val backgroundColor = entry.backgroundColor
if (!holder.container.isSelected) {
if (backgroundColor != null) {
holder.container.setBackgroundColor(backgroundColor)
} else {
holder.container.setBackgroundColor(Color.TRANSPARENT)
}
} else {
holder.container.setBackgroundColor(mColorAccentLight)
}
val foregroundColor = entry.foregroundColor
if (!holder.container.isSelected) {
if (foregroundColor != null) {
holder.text.setTextColor(foregroundColor)
holder.subText?.setTextColor(foregroundColor)
holder.otpToken?.setTextColor(foregroundColor)
holder.otpProgress?.setIndicatorColor(foregroundColor)
holder.attachmentIcon?.setColorFilter(foregroundColor)
holder.meta.setTextColor(foregroundColor)
holder.icon.apply {
database.iconDrawableFactory.assignDatabaseIcon(
this,
subNode.icon,
foregroundColor
)
}
} else {
holder.text.setTextColor(mTextColor)
holder.subText?.setTextColor(mTextColorSecondary)
holder.otpToken?.setTextColor(mTextColorSecondary)
holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
holder.meta.setTextColor(mTextColor)
}
} else {
holder.text.setTextColor(mTextColorInverse)
holder.subText?.setTextColor(mTextColorInverse)
holder.otpToken?.setTextColor(mTextColorInverse)
holder.otpProgress?.setIndicatorColor(mTextColorInverse)
holder.attachmentIcon?.setColorFilter(mTextColorInverse)
holder.meta.setTextColor(mTextColorInverse)
}
database.stopManageEntry(entry)
} }
// Add number of entries in groups // Add number of entries in groups
@@ -488,7 +350,7 @@ class NodesAdapter (private val context: Context,
if (mShowNumberEntries) { if (mShowNumberEntries) {
holder.numberChildren?.apply { holder.numberChildren?.apply {
text = (subNode as Group) text = (subNode as Group)
.numberOfChildEntries .getNumberOfChildEntries(mEntryFilters)
.toString() .toString()
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier) setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
visibility = View.VISIBLE visibility = View.VISIBLE
@@ -500,60 +362,13 @@ class NodesAdapter (private val context: Context,
// Assign click // Assign click
holder.container.setOnClickListener { holder.container.setOnClickListener {
mNodeClickCallback?.onNodeClick(database, subNode) mNodeClickCallback?.onNodeClick(subNode)
} }
holder.container.setOnLongClickListener { 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
setProgressCompat(100, true)
}
}
OtpType.TOTP -> {
holder?.otpProgress?.apply {
max = otpElement.period
setProgressCompat(otpElement.secondsRemaining, true)
}
}
null -> {}
}
holder?.otpToken?.apply {
text = otpElement?.token
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
}
holder?.otpContainer?.setOnClickListener {
otpElement?.token?.let { token ->
Toast.makeText(
context,
context.getString(R.string.copy_field,
TemplateField.getLocalizedName(context, TemplateField.LABEL_TOKEN)),
Toast.LENGTH_LONG
).show()
mClipboardHelper.copyToClipboard(token)
}
}
}
class OtpRunnable(val view: View?): Runnable {
var action: (() -> Unit)? = null
override fun run() {
action?.invoke()
postDelayed()
}
fun postDelayed() {
view?.postDelayed(this, 1000)
}
}
override fun getItemCount(): Int { override fun getItemCount(): Int {
return mNodeSortedList.size() return mNodeSortedList.size()
} }
@@ -569,8 +384,8 @@ class NodesAdapter (private val context: Context,
* Callback listener to redefine to do an action when a node is click * Callback listener to redefine to do an action when a node is click
*/ */
interface NodeClickCallback { interface NodeClickCallback {
fun onNodeClick(database: Database, node: Node) fun onNodeClick(node: Node)
fun onNodeLongClick(database: Database, node: Node): Boolean fun onNodeLongClick(node: Node): Boolean
} }
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
@@ -578,18 +393,12 @@ class NodesAdapter (private val context: Context,
var imageIdentifier: ImageView? = itemView.findViewById(R.id.node_image_identifier) var imageIdentifier: ImageView? = itemView.findViewById(R.id.node_image_identifier)
var icon: ImageView = itemView.findViewById(R.id.node_icon) var icon: ImageView = itemView.findViewById(R.id.node_icon)
var text: TextView = itemView.findViewById(R.id.node_text) var text: TextView = itemView.findViewById(R.id.node_text)
var subText: TextView? = itemView.findViewById(R.id.node_subtext) var subText: TextView = itemView.findViewById(R.id.node_subtext)
var meta: TextView = itemView.findViewById(R.id.node_meta)
var path: TextView? = itemView.findViewById(R.id.node_path)
var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container)
var otpProgress: CircularProgressIndicator? = 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 numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon) var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
} }
companion object { companion object {
private val TAG = NodesAdapter::class.java.name private val TAG = NodeAdapter::class.java.name
} }
} }

View File

@@ -106,6 +106,7 @@ class SearchEntryCursorAdapter(private val context: Context,
private fun getEntryFrom(cursor: Cursor): Entry? { private fun getEntryFrom(cursor: Cursor): Entry? {
return database.createEntry()?.apply { return database.createEntry()?.apply {
database.startManageEntry(this)
entryKDB?.let { entryKDB -> entryKDB?.let { entryKDB ->
(cursor as EntryCursorKDB).populateEntry(entryKDB, (cursor as EntryCursorKDB).populateEntry(entryKDB,
{ standardIconId -> { 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) { if (searchGroup != null) {
// Search in hide entries but not meta-stream // Search in hide entries but not meta-stream
for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) { for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) {
database.startManageEntry(entry)
entry.entryKDB?.let { entry.entryKDB?.let {
cursorKDB?.addEntry(it) cursorKDB?.addEntry(it)
} }
entry.entryKDBX?.let { entry.entryKDBX?.let {
cursorKDBX?.addEntry(it) 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 var templates: List<Template>): BaseAdapter() {
var iconDrawableFactory: IconDrawableFactory? = null
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 androidx.multidex.MultiDexApplication
import com.kunzisoft.keepass.activities.stylish.Stylish import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.utils.UriUtil
class App : MultiDexApplication() { class App : MultiDexApplication() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Stylish.load(this) Stylish.init(this)
PRNGFixes.apply() 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 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.net.Uri
import android.os.IBinder import android.os.IBinder
import android.util.Log
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.SingletonHolderParameter 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 // Temp DAO to easily remove content if object no longer in memory
private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext) private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
private val mIntentAdvancedUnlockService = Intent(applicationContext,
AdvancedUnlockNotificationService::class.java)
private var mBinder: AdvancedUnlockNotificationService.AdvancedUnlockBinder? = null private var mBinder: AdvancedUnlockNotificationService.AdvancedUnlockBinder? = null
private var mServiceConnection: ServiceConnection? = null private var mServiceConnection: ServiceConnection? = null
private var mDatabaseListeners = LinkedList<CipherDatabaseListener>() private var mDatabaseListeners = LinkedList<DatabaseListener>()
private var mAdvancedUnlockBroadcastReceiver = AdvancedUnlockNotificationService.AdvancedUnlockReceiver {
deleteAll()
removeAllDataAndDetach()
}
fun reloadPreferences() { fun reloadPreferences() {
useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext) 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 @Synchronized
private fun attachService(performedAction: () -> Unit) { private fun attachService(performedAction: () -> Unit) {
applicationContext.registerReceiver(mAdvancedUnlockBroadcastReceiver, IntentFilter().apply { // Check if a service is currently running else do nothing
addAction(AdvancedUnlockNotificationService.REMOVE_ADVANCED_UNLOCK_KEY_ACTION) if (mBinder != null) {
})
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)
performedAction.invoke() 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 fun registerDatabaseListener(listener: DatabaseListener) {
private fun detachService() { mDatabaseListeners.add(listener)
try {
applicationContext.unregisterReceiver(mAdvancedUnlockBroadcastReceiver)
} catch (e: Exception) {}
mServiceConnection?.let {
AdvancedUnlockNotificationService.unbindService(applicationContext, it)
}
} }
private fun removeAllDataAndDetach() { fun unregisterDatabaseListener(listener: DatabaseListener) {
detachService() mDatabaseListeners.remove(listener)
onClear()
} }
fun registerDatabaseListener(listenerCipher: CipherDatabaseListener) { interface DatabaseListener {
mDatabaseListeners.add(listenerCipher) fun onDatabaseCleared()
}
fun unregisterDatabaseListener(listenerCipher: CipherDatabaseListener) {
mDatabaseListeners.remove(listenerCipher)
}
private fun onClear() {
mBinder = null
mServiceConnection = null
mDatabaseListeners.forEach {
it.onCipherDatabaseCleared()
}
}
interface CipherDatabaseListener {
fun onCipherDatabaseCleared()
} }
fun getCipherDatabase(databaseUri: Uri, fun getCipherDatabase(databaseUri: Uri,
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) { cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
if (useTempDao) { if (useTempDao) {
serviceActionTask { attachService {
cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri)) cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri))
} }
} else { } else {
@@ -152,8 +121,7 @@ class CipherDatabaseAction(context: Context) {
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity, fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
cipherDatabaseResultListener: (() -> Unit)? = null) { cipherDatabaseResultListener: (() -> Unit)? = null) {
if (useTempDao) { if (useTempDao) {
// The only case to create service (not needed to get an info) attachService {
serviceActionTask(true) {
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity) mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
cipherDatabaseResultListener?.invoke() cipherDatabaseResultListener?.invoke()
} }
@@ -178,7 +146,7 @@ class CipherDatabaseAction(context: Context) {
fun deleteByDatabaseUri(databaseUri: Uri, fun deleteByDatabaseUri(databaseUri: Uri,
cipherDatabaseResultListener: (() -> Unit)? = null) { cipherDatabaseResultListener: (() -> Unit)? = null) {
if (useTempDao) { if (useTempDao) {
serviceActionTask { attachService {
mBinder?.deleteByDatabaseUri(databaseUri) mBinder?.deleteByDatabaseUri(databaseUri)
cipherDatabaseResultListener?.invoke() cipherDatabaseResultListener?.invoke()
} }
@@ -195,22 +163,15 @@ class CipherDatabaseAction(context: Context) {
} }
fun deleteAll() { fun deleteAll() {
if (useTempDao) { attachService {
serviceActionTask { mBinder?.deleteAll()
mBinder?.deleteAll()
}
} }
// To erase the residues
IOActionTask( IOActionTask(
{ {
cipherDatabaseDao.deleteAll() cipherDatabaseDao.deleteAll()
} }
).execute() ).execute()
// Unbind
removeAllDataAndDetach()
} }
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) { companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction)
private val TAG = CipherDatabaseAction::class.java.name
}
} }

View File

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

View File

@@ -4,4 +4,4 @@ import android.app.assist.AssistStructure
import android.view.inputmethod.InlineSuggestionsRequest import android.view.inputmethod.InlineSuggestionsRequest
data class AutofillComponent(val assistStructure: AssistStructure, data class AutofillComponent(val assistStructure: AssistStructure,
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?) val inlineSuggestionsRequest: InlineSuggestionsRequest?)

View File

@@ -34,35 +34,31 @@ import android.service.autofill.InlinePresentation
import android.util.Log import android.util.Log
import android.view.autofill.AutofillManager import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue import android.view.autofill.AutofillValue
import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.RemoteViews import android.widget.RemoteViews
import android.widget.Toast import android.widget.Toast
import android.widget.inline.InlinePresentationSpec
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.autofill.inline.UiVersions import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi import androidx.autofill.inline.v1.InlineSuggestionUi
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
object AutofillHelper { object AutofillHelper {
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST" const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? { fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure -> intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
@@ -89,14 +85,13 @@ object AutofillHelper {
} }
private fun newRemoteViews(context: Context, private fun newRemoteViews(context: Context,
database: Database,
remoteViewsText: String, remoteViewsText: String,
remoteViewsIcon: IconImage? = null): RemoteViews { remoteViewsIcon: IconImage? = null): RemoteViews {
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry) val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText) presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
if (remoteViewsIcon != null) { if (remoteViewsIcon != null) {
try { try {
database.iconDrawableFactory.getBitmapFromIcon(context, Database.getInstance().iconDrawableFactory.getBitmapFromIcon(context,
remoteViewsIcon, ContextCompat.getColor(context, R.color.green))?.let { bitmap -> remoteViewsIcon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
presentation.setImageViewBitmap(R.id.autofill_entry_icon, bitmap) presentation.setImageViewBitmap(R.id.autofill_entry_icon, bitmap)
} }
@@ -108,104 +103,31 @@ object AutofillHelper {
} }
private fun buildDataset(context: Context, private fun buildDataset(context: Context,
database: Database, entryInfo: EntryInfo,
entryInfo: EntryInfo, struct: StructureParser.Result,
struct: StructureParser.Result, inlinePresentation: InlinePresentation?): Dataset? {
additionalBuild: ((build: Dataset.Builder) -> Unit)? = null): Dataset? {
val title = makeEntryTitle(entryInfo) val title = makeEntryTitle(entryInfo)
val views = newRemoteViews(context, database, title, entryInfo.icon) val views = newRemoteViews(context, title, entryInfo.icon)
val builder = Dataset.Builder(views) val builder = Dataset.Builder(views)
builder.setId(entryInfo.id.toString()) builder.setId(entryInfo.id)
struct.usernameId?.let { usernameId -> struct.usernameId?.let { usernameId ->
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username)) builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
} }
struct.passwordId?.let { passwordId -> struct.passwordId?.let { password ->
builder.setValue(passwordId, AutofillValue.forText(entryInfo.password)) builder.setValue(password, AutofillValue.forText(entryInfo.password))
} }
if (entryInfo.expires) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val year = entryInfo.expiryTime.getYearInt() inlinePresentation?.let {
val month = entryInfo.expiryTime.getMonthInt() builder.setInlinePresentation(it)
val monthString = month.toString().padStart(2, '0')
val day = entryInfo.expiryTime.getDay()
val dayString = day.toString().padStart(2, '0')
struct.creditCardExpirationDateId?.let {
if (struct.isWebView) {
// set date string as defined in https://html.spec.whatwg.org
builder.setValue(it, AutofillValue.forText("$year\u002D$monthString"))
} 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(monthString))
} else {
if (struct.creditCardExpirationMonthOptions != null) {
// index starts at 0
builder.setValue(it, AutofillValue.forList(month - 1))
} else {
builder.setValue(it, AutofillValue.forText(monthString))
}
}
}
struct.creditCardExpirationDayId?.let {
if (struct.isWebView) {
builder.setValue(it, AutofillValue.forText(dayString))
} else {
if (struct.creditCardExpirationDayOptions != null) {
builder.setValue(it, AutofillValue.forList(day - 1))
} else {
builder.setValue(it, AutofillValue.forText(dayString))
}
}
} }
} }
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))
}
}
}
additionalBuild?.invoke(builder)
return try { return try {
builder.build() builder.build()
} catch (e: Exception) { } catch (e: IllegalArgumentException) {
// at least one value must be set // if not value be set
null null
} }
} }
@@ -213,11 +135,9 @@ object AutofillHelper {
/** /**
* Method to assign a drawable to a new icon from a database icon * Method to assign a drawable to a new icon from a database icon
*/ */
private fun buildIconFromEntry(context: Context, private fun buildIconFromEntry(context: Context, entryInfo: EntryInfo): Icon? {
database: Database,
entryInfo: EntryInfo): Icon? {
try { try {
database.iconDrawableFactory.getBitmapFromIcon(context, Database.getInstance().iconDrawableFactory.getBitmapFromIcon(context,
entryInfo.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap -> entryInfo.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
return Icon.createWithBitmap(bitmap) return Icon.createWithBitmap(bitmap)
} }
@@ -230,82 +150,48 @@ object AutofillHelper {
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private fun buildInlinePresentationForEntry(context: Context, private fun buildInlinePresentationForEntry(context: Context,
database: Database, inlineSuggestionsRequest: InlineSuggestionsRequest,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
positionItem: Int, positionItem: Int,
entryInfo: EntryInfo): InlinePresentation? { entryInfo: EntryInfo): InlinePresentation? {
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest -> val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
if (positionItem <= maxSuggestion - 1 if (positionItem <= maxSuggestion-1
&& inlinePresentationSpecs.size > positionItem && inlinePresentationSpecs.size > positionItem) {
) { val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
// Make sure that the IME spec claims support for v1 UI template. // Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style val imeStyle = inlinePresentationSpec.style
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1)) if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
return null return null
// Build the content for IME UI // Build the content for IME UI
val pendingIntent = PendingIntent.getActivity( val pendingIntent = PendingIntent.getActivity(context,
context,
0, 0,
Intent(context, AutofillSettingsActivity::class.java), Intent(context, AutofillSettingsActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 0)
PendingIntent.FLAG_IMMUTABLE return InlinePresentation(
} else {
0
}
)
return InlinePresentation(
InlineSuggestionUi.newContentBuilder(pendingIntent).apply { InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
setContentDescription(context.getString(R.string.autofill_sign_in_prompt)) setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
setTitle(entryInfo.title) setTitle(entryInfo.title)
setSubtitle(entryInfo.username) setSubtitle(entryInfo.username)
setStartIcon( setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply { setTintBlendMode(BlendMode.DST)
setTintBlendMode(BlendMode.DST) })
}) buildIconFromEntry(context, entryInfo)?.let { icon ->
buildIconFromEntry(context, database, entryInfo)?.let { icon ->
setEndIcon(icon.apply { setEndIcon(icon.apply {
setTintBlendMode(BlendMode.DST) setTintBlendMode(BlendMode.DST)
}) })
} }
}.build().slice, inlinePresentationSpec, false }.build().slice, inlinePresentationSpec, false)
)
}
} }
return null return null
} }
@RequiresApi(Build.VERSION_CODES.R)
@SuppressLint("RestrictedApi")
private fun buildInlinePresentationForManualSelection(context: Context,
inlinePresentationSpec: InlinePresentationSpec,
pendingIntent: PendingIntent): InlinePresentation? {
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
return null
// Build the content for IME UI
return InlinePresentation(
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
setTitle(context.getString(R.string.autofill_select_entry))
setStartIcon(Icon.createWithResource(context, R.drawable.ic_arrow_right_green_24dp).apply {
setTintBlendMode(BlendMode.DST)
})
}.build().slice, inlinePresentationSpec, false)
}
fun buildResponse(context: Context, fun buildResponse(context: Context,
database: Database,
entriesInfo: List<EntryInfo>, entriesInfo: List<EntryInfo>,
parseResult: StructureParser.Result, parseResult: StructureParser.Result,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? { inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse {
val responseBuilder = FillResponse.Builder() val responseBuilder = FillResponse.Builder()
// Add Header // Add Header
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@@ -322,90 +208,31 @@ object AutofillHelper {
} }
} }
} }
// Add inline suggestion for new IME and dataset // Add inline suggestion for new IME and dataset
var numberInlineSuggestions = 0 entriesInfo.forEachIndexed { index, entryInfo ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val inlinePresentation = inlineSuggestionsRequest?.let {
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size) buildInlinePresentationForEntry(context, inlineSuggestionsRequest, index, entryInfo)
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) { } else {
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) { null
--numberInlineSuggestions
}
} }
} }
responseBuilder.addDataset(buildDataset(context, entryInfo, parseResult, inlinePresentation))
}
entriesInfo.forEachIndexed { _, entry ->
if (numberInlineSuggestions > 0
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& compatInlineSuggestionsRequest != null) {
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult) { builder ->
buildInlinePresentationForEntry(context, database,
compatInlineSuggestionsRequest, numberInlineSuggestions--, entry
)?.let { inlinePresentation ->
builder.setInlinePresentation(inlinePresentation)
}
})
} else {
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult))
}
}
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
manualSelection = true
}
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
searchInfo, compatInlineSuggestionsRequest)
parseResult.allAutofillIds().let { autofillIds ->
autofillIds.forEach { id ->
val builder = Dataset.Builder(manualSelectionView)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
inlinePresentation?.let {
builder.setInlinePresentation(it)
}
}
}
builder.setValue(id, null)
builder.setAuthentication(pendingIntent.intentSender)
responseBuilder.addDataset(builder.build())
}
}
}
return try {
responseBuilder.build()
} catch (e: Exception) {
null
} }
return responseBuilder.build()
} }
/** /**
* Build the Autofill response for one entry * Build the Autofill response for one entry
*/ */
fun buildResponseAndSetResult(activity: Activity, fun buildResponseAndSetResult(activity: Activity, entryInfo: EntryInfo) {
database: Database, buildResponseAndSetResult(activity, ArrayList<EntryInfo>().apply { add(entryInfo) })
entryInfo: EntryInfo) {
buildResponseAndSetResult(activity, database, ArrayList<EntryInfo>().apply { add(entryInfo) })
} }
/** /**
* Build the Autofill response for many entry * Build the Autofill response for many entry
*/ */
fun buildResponseAndSetResult(activity: Activity, fun buildResponseAndSetResult(activity: Activity, entriesInfo: List<EntryInfo>) {
database: Database,
entriesInfo: List<EntryInfo>) {
if (entriesInfo.isEmpty()) { if (entriesInfo.isEmpty()) {
activity.setResult(Activity.RESULT_CANCELED) activity.setResult(Activity.RESULT_CANCELED)
} else { } else {
@@ -414,13 +241,13 @@ object AutofillHelper {
StructureParser(structure).parse()?.let { result -> StructureParser(structure).parse()?.let { result ->
// New Response // New Response
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtra<CompatInlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST) val inlineSuggestionsRequest = activity.intent?.getParcelableExtra<InlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
if (compatInlineSuggestionsRequest != null) { if (inlineSuggestionsRequest != null) {
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show() Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
} }
buildResponse(activity, database, entriesInfo, result, compatInlineSuggestionsRequest) buildResponse(activity, entriesInfo, result, inlineSuggestionsRequest)
} else { } else {
buildResponse(activity, database, entriesInfo, result, null) buildResponse(activity, entriesInfo, result, null)
} }
val mReplyIntent = Intent() val mReplyIntent = Intent()
Log.d(activity.javaClass.name, "Successed Autofill auth.") Log.d(activity.javaClass.name, "Successed Autofill auth.")
@@ -438,44 +265,37 @@ object AutofillHelper {
} }
} }
fun buildActivityResultLauncher(activity: AppCompatActivity,
lockDatabase: Boolean = false): ActivityResultLauncher<Intent> {
return activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
// Utility method to loop and close each activity with return data
if (it.resultCode == Activity.RESULT_OK) {
activity.setResult(it.resultCode, it.data)
}
if (it.resultCode == Activity.RESULT_CANCELED) {
activity.setResult(Activity.RESULT_CANCELED)
}
activity.finish()
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(activity)) {
// Close the database
activity.sendBroadcast(Intent(LOCK_ACTION))
}
}
}
/** /**
* Utility method to start an activity with an Autofill for result * Utility method to start an activity with an Autofill for result
*/ */
fun startActivityForAutofillResult(activity: AppCompatActivity, fun startActivityForAutofillResult(activity: Activity,
intent: Intent, intent: Intent,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent, autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) { searchInfo: SearchInfo?) {
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION) EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure) intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) { && PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
autofillComponent.compatInlineSuggestionsRequest?.let { autofillComponent.inlineSuggestionsRequest?.let {
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it) intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
} }
} }
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo) EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
activityResultLauncher?.launch(intent) activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
}
/**
* Utility method to loop and close each activity with return data
*/
fun onActivityResultSetResultAndFinish(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == AUTOFILL_RESPONSE_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
activity.setResult(resultCode, data)
}
if (resultCode == Activity.RESULT_CANCELED) {
activity.setResult(Activity.RESULT_CANCELED)
}
activity.finish()
}
} }
} }

View File

@@ -1,83 +0,0 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.autofill
import android.annotation.TargetApi
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.service.autofill.FillRequest
import android.view.inputmethod.InlineSuggestionsRequest
import androidx.annotation.RequiresApi
/**
* Utility class only to prevent java.lang.NoClassDefFoundError for old Android version and new lib compilation
*/
@RequiresApi(Build.VERSION_CODES.O)
class CompatInlineSuggestionsRequest : Parcelable {
@TargetApi(Build.VERSION_CODES.R)
var inlineSuggestionsRequest: InlineSuggestionsRequest? = null
private set
constructor(fillRequest: FillRequest) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
this.inlineSuggestionsRequest = fillRequest.inlineSuggestionsRequest
} else {
this.inlineSuggestionsRequest = null
}
}
@RequiresApi(Build.VERSION_CODES.R)
constructor(inlineSuggestionsRequest: InlineSuggestionsRequest?) {
this.inlineSuggestionsRequest = inlineSuggestionsRequest
}
constructor(parcel: Parcel) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
this.inlineSuggestionsRequest =
parcel.readParcelable(FillRequest::class.java.classLoader)
}
else {
this.inlineSuggestionsRequest = null
}
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
parcel.writeParcelable(inlineSuggestionsRequest, flags)
}
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<CompatInlineSuggestionsRequest> {
override fun createFromParcel(parcel: Parcel): CompatInlineSuggestionsRequest {
return CompatInlineSuggestionsRequest(parcel)
}
override fun newArray(size: Int): Array<CompatInlineSuggestionsRequest?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -36,47 +36,29 @@ import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi import androidx.autofill.inline.v1.InlineSuggestionUi
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.AutofillLauncherActivity 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.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.model.CreditCard
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import org.joda.time.DateTime
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
class KeeAutofillService : AutofillService() { class KeeAutofillService : AutofillService() {
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null var applicationIdBlocklist: Set<String>? = null
private var mDatabase: Database? = null var webDomainBlocklist: Set<String>? = null
private var applicationIdBlocklist: Set<String>? = null var askToSaveData: Boolean = false
private var webDomainBlocklist: Set<String>? = null var autofillInlineSuggestionsEnabled: Boolean = false
private var askToSaveData: Boolean = false
private var autofillInlineSuggestionsEnabled: Boolean = false
private var mLock = AtomicBoolean() private var mLock = AtomicBoolean()
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
mDatabaseTaskProvider = DatabaseTaskProvider(this)
mDatabaseTaskProvider?.registerProgressTask()
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
this.mDatabase = database
}
getPreferences() getPreferences()
} }
override fun onDestroy() {
mDatabaseTaskProvider?.unregisterProgressTask()
super.onDestroy()
}
private fun getPreferences() { private fun getPreferences() {
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this) applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this) webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
@@ -109,12 +91,11 @@ class KeeAutofillService : AutofillService() {
searchInfo.webDomain = webDomainWithoutSubDomain searchInfo.webDomain = webDomainWithoutSubDomain
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) { && autofillInlineSuggestionsEnabled) {
CompatInlineSuggestionsRequest(request) request.inlineSuggestionsRequest
} else { } else {
null null
} }
launchSelection(mDatabase, launchSelection(searchInfo,
searchInfo,
parseResult, parseResult,
inlineSuggestionsRequest, inlineSuggestionsRequest,
callback) callback)
@@ -124,28 +105,27 @@ class KeeAutofillService : AutofillService() {
} }
} }
private fun launchSelection(database: Database?, private fun launchSelection(searchInfo: SearchInfo,
searchInfo: SearchInfo,
parseResult: StructureParser.Result, parseResult: StructureParser.Result,
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?, inlineSuggestionsRequest: InlineSuggestionsRequest?,
callback: FillCallback) { callback: FillCallback) {
SearchHelper.checkAutoSearchInfo(this, SearchHelper.checkAutoSearchInfo(this,
database, Database.getInstance(),
searchInfo, searchInfo,
{ openedDatabase, items -> { items ->
callback.onSuccess( callback.onSuccess(
AutofillHelper.buildResponse(this, openedDatabase, AutofillHelper.buildResponse(this,
items, parseResult, inlineSuggestionsRequest) items, parseResult, inlineSuggestionsRequest)
) )
}, },
{ openedDatabase -> {
// Show UI if no search result // Show UI if no search result
showUIForEntrySelection(parseResult, openedDatabase, showUIForEntrySelection(parseResult,
searchInfo, inlineSuggestionsRequest, callback) searchInfo, inlineSuggestionsRequest, callback)
}, },
{ {
// Show UI if database not open // Show UI if database not open
showUIForEntrySelection(parseResult, null, showUIForEntrySelection(parseResult,
searchInfo, inlineSuggestionsRequest, callback) searchInfo, inlineSuggestionsRequest, callback)
} }
) )
@@ -153,95 +133,45 @@ class KeeAutofillService : AutofillService() {
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private fun showUIForEntrySelection(parseResult: StructureParser.Result, private fun showUIForEntrySelection(parseResult: StructureParser.Result,
database: Database?,
searchInfo: SearchInfo, searchInfo: SearchInfo,
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?, inlineSuggestionsRequest: InlineSuggestionsRequest?,
callback: FillCallback) { callback: FillCallback) {
parseResult.allAutofillIds().let { autofillIds -> parseResult.allAutofillIds().let { autofillIds ->
if (autofillIds.isNotEmpty()) { if (autofillIds.isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used // If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response. // to generate Response.
val intentSender = AutofillLauncherActivity.getPendingIntentForSelection(this, val intentSender = AutofillLauncherActivity.getAuthIntentSenderForSelection(this,
searchInfo, inlineSuggestionsRequest).intentSender searchInfo, inlineSuggestionsRequest)
val responseBuilder = FillResponse.Builder() val responseBuilder = FillResponse.Builder()
val remoteViewsUnlock: RemoteViews = if (database == null) { val remoteViewsUnlock: RemoteViews = if (!parseResult.webDomain.isNullOrEmpty()) {
if (!parseResult.webDomain.isNullOrEmpty()) { RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply {
RemoteViews( setTextViewText(R.id.autofill_web_domain_text, parseResult.webDomain)
packageName, }
R.layout.item_autofill_unlock_web_domain } else if (!parseResult.applicationId.isNullOrEmpty()) {
).apply { RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
setTextViewText( setTextViewText(R.id.autofill_app_id_text, parseResult.applicationId)
R.id.autofill_web_domain_text,
parseResult.webDomain
)
}
} else if (!parseResult.applicationId.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
setTextViewText(
R.id.autofill_app_id_text,
parseResult.applicationId
)
}
} else {
RemoteViews(packageName, R.layout.item_autofill_unlock)
} }
} else { } else {
if (!parseResult.webDomain.isNullOrEmpty()) { RemoteViews(packageName, R.layout.item_autofill_unlock)
RemoteViews(
packageName,
R.layout.item_autofill_select_entry_web_domain
).apply {
setTextViewText(
R.id.autofill_web_domain_text,
parseResult.webDomain
)
}
} else if (!parseResult.applicationId.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_select_entry_app_id).apply {
setTextViewText(
R.id.autofill_app_id_text,
parseResult.applicationId
)
}
} else {
RemoteViews(packageName, R.layout.item_autofill_select_entry)
}
} }
// Tell the autofill framework the interest to save credentials // Tell to service the interest to save credentials
if (askToSaveData) { if (askToSaveData) {
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
val requiredIds = ArrayList<AutofillId>() val info = ArrayList<AutofillId>()
val optionalIds = ArrayList<AutofillId>()
// Only if at least a password // Only if at least a password
parseResult.passwordId?.let { passwordInfo -> parseResult.passwordId?.let { passwordInfo ->
parseResult.usernameId?.let { usernameInfo -> parseResult.usernameId?.let { usernameInfo ->
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
requiredIds.add(usernameInfo) info.add(usernameInfo)
} }
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
requiredIds.add(passwordInfo) info.add(passwordInfo)
} }
// or a credit card form if (info.isNotEmpty()) {
if (requiredIds.isEmpty()) { responseBuilder.setSaveInfo(
parseResult.creditCardNumberId?.let { numberId -> SaveInfo.Builder(types, info.toTypedArray()).build()
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())
} }
} }
@@ -249,7 +179,7 @@ class KeeAutofillService : AutofillService() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) { && autofillInlineSuggestionsEnabled) {
var inlinePresentation: InlinePresentation? = null var inlinePresentation: InlinePresentation? = null
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest -> inlineSuggestionsRequest?.let {
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
if (inlineSuggestionsRequest.maxSuggestionCount > 0 if (inlineSuggestionsRequest.maxSuggestionCount > 0
&& inlinePresentationSpecs.size > 0) { && inlinePresentationSpecs.size > 0) {
@@ -262,13 +192,9 @@ class KeeAutofillService : AutofillService() {
inlinePresentation = InlinePresentation( inlinePresentation = InlinePresentation(
InlineSuggestionUi.newContentBuilder( InlineSuggestionUi.newContentBuilder(
PendingIntent.getActivity(this, PendingIntent.getActivity(this,
0, 0,
Intent(this, AutofillSettingsActivity::class.java), Intent(this, AutofillSettingsActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 0)
PendingIntent.FLAG_IMMUTABLE
} else {
0
})
).apply { ).apply {
setContentDescription(getString(R.string.autofill_sign_in_prompt)) setContentDescription(getString(R.string.autofill_sign_in_prompt))
setTitle(getString(R.string.autofill_sign_in_prompt)) setTitle(getString(R.string.autofill_sign_in_prompt))
@@ -281,9 +207,8 @@ class KeeAutofillService : AutofillService() {
} }
// Build response // Build response
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation) responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
} else {
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
} }
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
callback.onSuccess(responseBuilder.build()) callback.onSuccess(responseBuilder.build())
} }
} }
@@ -298,35 +223,14 @@ class KeeAutofillService : AutofillService() {
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) { && autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
Log.d(TAG, "autofill onSaveRequest password") 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 // Show UI to save data
val registerInfo = RegisterInfo( val registerInfo = RegisterInfo(SearchInfo().apply {
SearchInfo().apply { applicationId = parseResult.applicationId
applicationId = parseResult.applicationId webDomain = parseResult.webDomain
webDomain = parseResult.webDomain webScheme = parseResult.webScheme
webScheme = parseResult.webScheme },
},
parseResult.usernameValue?.textValue?.toString(), parseResult.usernameValue?.textValue?.toString(),
parseResult.passwordValue?.textValue?.toString(), parseResult.passwordValue?.textValue?.toString())
CreditCard(
parseResult.creditCardHolder,
parseResult.creditCardNumber,
expiration,
parseResult.cardVerificationValue
))
// TODO Callback in each activity #765 // TODO Callback in each activity #765
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { //if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this, // callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,

View File

@@ -21,14 +21,12 @@ package com.kunzisoft.keepass.autofill
import android.app.assist.AssistStructure import android.app.assist.AssistStructure
import android.os.Build import android.os.Build
import android.text.InputType import android.text.InputType
import androidx.annotation.RequiresApi
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.view.autofill.AutofillId import android.view.autofill.AutofillId
import android.view.autofill.AutofillValue import android.view.autofill.AutofillValue
import androidx.annotation.RequiresApi
import org.joda.time.DateTime
import java.util.* import java.util.*
import kotlin.collections.ArrayList
/** /**
@@ -37,8 +35,10 @@ import kotlin.collections.ArrayList
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
class StructureParser(private val structure: AssistStructure) { class StructureParser(private val structure: AssistStructure) {
private var result: Result? = null private var result: Result? = null
private var usernameNeeded = true private var usernameNeeded = true
private var usernameIdCandidate: AutofillId? = null
private var usernameCandidate: AutofillId? = null
private var usernameValueCandidate: AutofillValue? = null private var usernameValueCandidate: AutofillValue? = null
fun parse(saveValue: Boolean = false): Result? { fun parse(saveValue: Boolean = false): Result? {
@@ -46,42 +46,37 @@ class StructureParser(private val structure: AssistStructure) {
result = Result() result = Result()
result?.apply { result?.apply {
allowSaveValues = saveValue allowSaveValues = saveValue
usernameIdCandidate = null usernameCandidate = null
usernameValueCandidate = null usernameValueCandidate = null
mainLoop@ for (i in 0 until structure.windowNodeCount) { mainLoop@ for (i in 0 until structure.windowNodeCount) {
val windowNode = structure.getWindowNodeAt(i) val windowNode = structure.getWindowNodeAt(i)
applicationId = windowNode.title.toString().split("/")[0] applicationId = windowNode.title.toString().split("/")[0]
Log.d(TAG, "Autofill applicationId: $applicationId") Log.d(TAG, "Autofill applicationId: $applicationId")
if (applicationId?.contains("PopupWindow:") == false) { if (parseViewNode(windowNode.rootViewNode))
if (parseViewNode(windowNode.rootViewNode)) break@mainLoop
break@mainLoop
}
} }
// If not explicit username field found, add the field just before password field. // If not explicit username field found, add the field just before password field.
if (usernameId == null && passwordId != null && usernameIdCandidate != null) { if (usernameId == null && passwordId != null && usernameCandidate != null) {
usernameId = usernameIdCandidate usernameId = usernameCandidate
if (allowSaveValues) { if (allowSaveValues) {
usernameValue = usernameValueCandidate usernameValue = usernameValueCandidate
} }
} }
} }
return if (result?.passwordId != null || result?.creditCardNumberId != null) // Return the result only if password field is retrieved
result return if ((!usernameNeeded || result?.usernameId != null)
else && result?.passwordId != null)
null result
else
null
} catch (e: Exception) { } catch (e: Exception) {
return null return null
} }
} }
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean { 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 // Get the domain of a web app
node.webDomain?.let { webDomain -> node.webDomain?.let { webDomain ->
if (webDomain.isNotEmpty()) { if (webDomain.isNotEmpty()) {
@@ -102,7 +97,8 @@ class StructureParser(private val structure: AssistStructure) {
var returnValue = false var returnValue = false
// Only parse visible nodes // Only parse visible nodes
if (node.visibility == View.VISIBLE) { if (node.visibility == View.VISIBLE) {
if (node.autofillId != null) { if (node.autofillId != null
&& node.autofillType == View.AUTOFILL_TYPE_TEXT) {
// Parse methods // Parse methods
val hints = node.autofillHints val hints = node.autofillHints
if (hints != null && hints.isNotEmpty()) { if (hints != null && hints.isNotEmpty()) {
@@ -134,7 +130,7 @@ class StructureParser(private val structure: AssistStructure) {
it.contains(View.AUTOFILL_HINT_USERNAME, true) it.contains(View.AUTOFILL_HINT_USERNAME, true)
|| it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true) || it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|| it.contains("email", true) || it.contains("email", true)
|| it.contains(View.AUTOFILL_HINT_PHONE, true) -> { || it.contains(View.AUTOFILL_HINT_PHONE, true)-> {
result?.usernameId = autofillId result?.usernameId = autofillId
result?.usernameValue = node.autofillValue result?.usernameValue = node.autofillValue
Log.d(TAG, "Autofill username hint") Log.d(TAG, "Autofill username hint")
@@ -143,123 +139,14 @@ class StructureParser(private val structure: AssistStructure) {
result?.passwordId = autofillId result?.passwordId = autofillId
result?.passwordValue = node.autofillValue result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password hint") Log.d(TAG, "Autofill password hint")
// Username not needed in this case
usernameNeeded = false
return true return true
} }
it.equals("cc-name", true) -> {
Log.d(TAG, "Autofill credit card name hint")
result?.creditCardHolderId = autofillId
result?.creditCardHolder = node.autofillValue?.textValue?.toString()
}
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_NUMBER, true)
|| it.equals("cc-number", true) -> {
Log.d(TAG, "Autofill credit card number hint")
result?.creditCardNumberId = autofillId
result?.creditCardNumber = node.autofillValue?.textValue?.toString()
}
// expect date string as defined in https://html.spec.whatwg.org, e.g. 2014-12
it.equals("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.equals("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.equals("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.equals("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" // Ignore autocomplete="off"
// https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion // https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
it.equals("off", true) || it.equals("off", true) ||
it.equals("on", true) -> { it.equals("on", true) -> {
Log.d(TAG, "Autofill web hint") Log.d(TAG, "Autofill web hint")
return parseNodeByHtmlAttributes(node) return parseNodeByHtmlAttributes(node)
} }
@@ -272,19 +159,19 @@ class StructureParser(private val structure: AssistStructure) {
private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean { private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
val autofillId = node.autofillId val autofillId = node.autofillId
val nodHtml = node.htmlInfo val nodHtml = node.htmlInfo
when (nodHtml?.tag?.lowercase(Locale.ENGLISH)) { when (nodHtml?.tag?.toLowerCase(Locale.ENGLISH)) {
"input" -> { "input" -> {
nodHtml.attributes?.forEach { pairAttribute -> nodHtml.attributes?.forEach { pairAttribute ->
when (pairAttribute.first.lowercase(Locale.ENGLISH)) { when (pairAttribute.first.toLowerCase(Locale.ENGLISH)) {
"type" -> { "type" -> {
when (pairAttribute.second.lowercase(Locale.ENGLISH)) { when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
"tel", "email" -> { "tel", "email" -> {
result?.usernameId = autofillId result?.usernameId = autofillId
result?.usernameValue = node.autofillValue result?.usernameValue = node.autofillValue
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}") Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
} }
"text" -> { "text" -> {
usernameIdCandidate = autofillId usernameCandidate = autofillId
usernameValueCandidate = node.autofillValue usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}") Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
} }
@@ -332,30 +219,18 @@ class StructureParser(private val structure: AssistStructure) {
InputType.TYPE_TEXT_VARIATION_NORMAL, InputType.TYPE_TEXT_VARIATION_NORMAL,
InputType.TYPE_TEXT_VARIATION_PERSON_NAME, InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> { InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
usernameIdCandidate = autofillId usernameCandidate = autofillId
usernameValueCandidate = node.autofillValue usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}") 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, inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_PASSWORD, InputType.TYPE_TEXT_VARIATION_PASSWORD,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> { InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
result?.passwordId = autofillId result?.passwordId = autofillId
result?.passwordValue = node.autofillValue result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}") Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}")
usernameNeeded = false
return true return true
} }
inputIsVariationType(inputType, inputIsVariationType(inputType,
@@ -377,15 +252,16 @@ class StructureParser(private val structure: AssistStructure) {
when { when {
inputIsVariationType(inputType, inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> { InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
usernameIdCandidate = autofillId usernameCandidate = autofillId
usernameValueCandidate = node.autofillValue 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, inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> { InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
result?.passwordId = autofillId result?.passwordId = autofillId
result?.passwordValue = node.autofillValue result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}") Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}")
usernameNeeded = false
return true return true
} }
else -> { else -> {
@@ -399,8 +275,8 @@ class StructureParser(private val structure: AssistStructure) {
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
class Result { class Result {
var isWebView: Boolean = false
var applicationId: String? = null var applicationId: String? = null
var webDomain: String? = null var webDomain: String? = null
set(value) { set(value) {
if (field == null) if (field == null)
@@ -413,12 +289,6 @@ class StructureParser(private val structure: AssistStructure) {
field = value 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 var usernameId: AutofillId? = null
set(value) { set(value) {
if (field == null) if (field == null)
@@ -431,48 +301,6 @@ class StructureParser(private val structure: AssistStructure) {
field = value 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> { fun allAutofillIds(): Array<AutofillId> {
val all = ArrayList<AutofillId>() val all = ArrayList<AutofillId>()
usernameId?.let { usernameId?.let {
@@ -481,15 +309,6 @@ class StructureParser(private val structure: AssistStructure) {
passwordId?.let { passwordId?.let {
all.add(it) all.add(it)
} }
creditCardHolderId?.let {
all.add(it)
}
creditCardNumberId?.let {
all.add(it)
}
cardVerificationValueId?.let {
all.add(it)
}
return all.toTypedArray() return all.toTypedArray()
} }
@@ -507,52 +326,6 @@ class StructureParser(private val structure: AssistStructure) {
if (allowSaveValues && field == null) if (allowSaveValues && field == null)
field = value 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 { companion object {

View File

@@ -19,7 +19,6 @@
*/ */
package com.kunzisoft.keepass.biometric package com.kunzisoft.keepass.biometric
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@@ -28,23 +27,18 @@ import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.view.* import android.view.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import com.getkeepsafe.taptargetview.TapTargetView import com.getkeepsafe.taptargetview.TapTargetView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.database.exception.IODatabaseException import com.kunzisoft.keepass.database.exception.IODatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedUnlockCallback { class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedUnlockCallback {
@@ -63,12 +57,9 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
/** /**
* Manage setting to auto open biometric prompt * Manage setting to auto open biometric prompt
*/ */
private var mAutoOpenPrompt: Boolean private var mAutoOpenPrompt: Boolean = false
get() { get() {
return mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt && mAutoOpenPromptEnabled return field && mAutoOpenPromptEnabled
}
set(value) {
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = value
} }
// Variable to check if the prompt can be open (if the right activity is currently shown) // Variable to check if the prompt can be open (if the right activity is currently shown)
@@ -77,9 +68,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
private lateinit var cipherDatabaseAction : CipherDatabaseAction private lateinit var cipherDatabaseAction : CipherDatabaseAction
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null private var cipherDatabaseListener: CipherDatabaseAction.DatabaseListener? = null
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by activityViewModels()
// Only to fix multiple fingerprint menu #332 // Only to fix multiple fingerprint menu #332
private var mAllowAdvancedUnlockMenu = false private var mAllowAdvancedUnlockMenu = false
@@ -88,15 +77,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
// Only keep connection when we request a device credential activity // Only keep connection when we request a device credential activity
private var keepConnection = false private var keepConnection = false
private var mDeviceCredentialResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
// To wait resume
if (keepConnection) {
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = result.resultCode == Activity.RESULT_OK
}
keepConnection = false
}
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
@@ -115,21 +95,10 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
retainInstance = true
setHasOptionsMenu(true) setHasOptionsMenu(true)
cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext) cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext)
mAdvancedUnlockViewModel.onInitAdvancedUnlockModeRequested.observe(this) {
initAdvancedUnlockMode()
}
mAdvancedUnlockViewModel.onUnlockAvailabilityCheckRequested.observe(this) {
checkUnlockAvailability()
}
mAdvancedUnlockViewModel.onDatabaseFileLoaded.observe(this) {
onDatabaseLoaded(it)
}
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@@ -143,12 +112,21 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
return rootView return rootView
} }
private data class ActivityResult(var requestCode: Int, var resultCode: Int, var data: Intent?)
private var activityResult: ActivityResult? = null
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// To wait resume
if (keepConnection) {
activityResult = ActivityResult(requestCode, resultCode, data)
}
keepConnection = false
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
context?.let { mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(requireContext())
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(it) mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(requireContext())
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(it)
}
keepConnection = false keepConnection = false
} }
@@ -172,69 +150,61 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
private fun onDatabaseLoaded(databaseUri: Uri?) { fun loadDatabase(databaseUri: Uri?, autoOpenPrompt: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// To get device credential unlock result, only if same database uri // To get device credential unlock result, only if same database uri
if (databaseUri != null if (databaseUri != null
&& mAdvancedUnlockEnabled) { && mAdvancedUnlockEnabled) {
val deviceCredentialAuthSucceeded = mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded activityResult?.let {
deviceCredentialAuthSucceeded?.let {
if (databaseUri == databaseFileUri) { if (databaseUri == databaseFileUri) {
if (deviceCredentialAuthSucceeded == true) { advancedUnlockManager?.onActivityResult(it.requestCode, it.resultCode)
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationSucceeded()
} else {
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationFailed()
}
} else { } else {
disconnect() disconnect()
} }
} ?: run { } ?: run {
if (databaseUri != databaseFileUri) { this.mAutoOpenPrompt = autoOpenPrompt
connect(databaseUri) connect(databaseUri)
}
} }
} else { } else {
disconnect() disconnect()
} }
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = null activityResult = null
} }
} }
/** /**
* Check unlock availability and change the current mode depending of device's state * Check unlock availability and change the current mode depending of device's state
*/ */
private fun checkUnlockAvailability() { fun checkUnlockAvailability() {
context?.let { context -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { allowOpenBiometricPrompt = true
allowOpenBiometricPrompt = true if (PreferencesUtil.isBiometricUnlockEnable(requireContext())) {
if (PreferencesUtil.isBiometricUnlockEnable(context)) { mAdvancedUnlockInfoView?.setIconResource(R.drawable.fingerprint)
mAdvancedUnlockInfoView?.setIconResource(R.drawable.fingerprint)
// biometric not supported (by API level or hardware) so keep option hidden // biometric not supported (by API level or hardware) so keep option hidden
// or manually disable // or manually disable
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(context) val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(requireContext())
if (!PreferencesUtil.isAdvancedUnlockEnable(context) if (!PreferencesUtil.isAdvancedUnlockEnable(requireContext())
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) { || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
toggleMode(Mode.BIOMETRIC_UNAVAILABLE) toggleMode(Mode.BIOMETRIC_UNAVAILABLE)
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) { } else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) {
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED) toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
} else { } else {
// biometric is available but not configured, show icon but in disabled state with some information // biometric is available but not configured, show icon but in disabled state with some information
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) { 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 {
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED) 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)
}
} }
} }
} }
@@ -291,7 +261,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
private fun openBiometricSetting() { private fun openBiometricSetting() {
mAdvancedUnlockInfoView?.setIconViewClickListener(false) { mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices... // ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
context?.startActivity(Intent(Settings.ACTION_SETTINGS)) requireContext().startActivity(Intent(Settings.ACTION_SETTINGS))
} }
} }
@@ -326,23 +296,20 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
setAdvancedUnlockedTitleView(R.string.no_credentials_stored) setAdvancedUnlockedTitleView(R.string.no_credentials_stored)
setAdvancedUnlockedMessageView("") setAdvancedUnlockedMessageView("")
context?.let { context -> mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
mAdvancedUnlockInfoView?.setIconViewClickListener(false) { onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS, requireContext().getString(R.string.credential_before_click_advanced_unlock_button))
context.getString(R.string.credential_before_click_advanced_unlock_button))
}
} }
} }
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
private fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) { private fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
lifecycleScope.launch(Dispatchers.Main) { activity?.runOnUiThread {
if (allowOpenBiometricPrompt) { if (allowOpenBiometricPrompt) {
if (cryptoPrompt.isDeviceCredentialOperation) if (cryptoPrompt.isDeviceCredentialOperation)
keepConnection = true keepConnection = true
try { try {
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt, advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt)
mDeviceCredentialResultLauncher)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to open advanced unlock prompt", e) Log.e(TAG, "Unable to open advanced unlock prompt", e)
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized) setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
@@ -394,7 +361,8 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} ?: throw Exception("AdvancedUnlockManager not initialized") } ?: throw Exception("AdvancedUnlockManager not initialized")
} }
private fun initAdvancedUnlockMode() { @Synchronized
fun initAdvancedUnlockMode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mAllowAdvancedUnlockMenu = false mAllowAdvancedUnlockMenu = false
try { try {
@@ -434,10 +402,9 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
fun connect(databaseUri: Uri) { fun connect(databaseUri: Uri) {
showViews(true) showViews(true)
this.databaseFileUri = databaseUri this.databaseFileUri = databaseUri
cipherDatabaseListener = object: CipherDatabaseAction.CipherDatabaseListener { cipherDatabaseListener = object: CipherDatabaseAction.DatabaseListener {
override fun onCipherDatabaseCleared() { override fun onDatabaseCleared() {
advancedUnlockManager?.closeBiometricPrompt() deleteEncryptedDatabaseKey()
checkUnlockAvailability()
} }
} }
cipherDatabaseAction.apply { cipherDatabaseAction.apply {
@@ -468,17 +435,18 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
fun deleteEncryptedDatabaseKey() { fun deleteEncryptedDatabaseKey() {
mAllowAdvancedUnlockMenu = false allowOpenBiometricPrompt = false
mAdvancedUnlockInfoView?.setIconViewClickListener(false, null)
advancedUnlockManager?.closeBiometricPrompt() advancedUnlockManager?.closeBiometricPrompt()
databaseFileUri?.let { databaseUri -> databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) { cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
checkUnlockAvailability() checkUnlockAvailability()
} }
} ?: checkUnlockAvailability() }
} }
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
lifecycleScope.launch(Dispatchers.Main) { activity?.runOnUiThread {
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString") Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
setAdvancedUnlockedMessageView(errString.toString()) setAdvancedUnlockedMessageView(errString.toString())
} }
@@ -486,7 +454,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationFailed() { override fun onAuthenticationFailed() {
lifecycleScope.launch(Dispatchers.Main) { activity?.runOnUiThread {
Log.e(TAG, "Biometric authentication failed, biometric not recognized") Log.e(TAG, "Biometric authentication failed, biometric not recognized")
setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized) setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized)
} }
@@ -494,7 +462,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationSucceeded() { override fun onAuthenticationSucceeded() {
lifecycleScope.launch(Dispatchers.Main) { activity?.runOnUiThread {
when (biometricMode) { when (biometricMode) {
Mode.BIOMETRIC_UNAVAILABLE -> { Mode.BIOMETRIC_UNAVAILABLE -> {
} }
@@ -511,6 +479,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
mBuilderListener?.retrieveCredentialForEncryption()?.let { credential -> mBuilderListener?.retrieveCredentialForEncryption()?.let { credential ->
advancedUnlockManager?.encryptData(credential) advancedUnlockManager?.encryptData(credential)
} }
AdvancedUnlockNotificationService.startServiceForTimeout(requireContext())
} }
Mode.EXTRACT_CREDENTIAL -> { Mode.EXTRACT_CREDENTIAL -> {
// retrieve the encrypted value from preferences // retrieve the encrypted value from preferences
@@ -541,11 +510,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} }
} }
@RequiresApi(Build.VERSION_CODES.M)
override fun onUnrecoverableKeyException(e: Exception) {
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
}
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
override fun onInvalidKeyException(e: Exception) { override fun onInvalidKeyException(e: Exception) {
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key) setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
@@ -557,7 +521,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} }
private fun showViews(show: Boolean) { private fun showViews(show: Boolean) {
lifecycleScope.launch(Dispatchers.Main) { activity?.runOnUiThread {
mAdvancedUnlockInfoView?.visibility = if (show) mAdvancedUnlockInfoView?.visibility = if (show)
View.VISIBLE View.VISIBLE
else { else {
@@ -568,20 +532,20 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedTitleView(textId: Int) { private fun setAdvancedUnlockedTitleView(textId: Int) {
lifecycleScope.launch(Dispatchers.Main) { activity?.runOnUiThread {
mAdvancedUnlockInfoView?.setTitle(textId) mAdvancedUnlockInfoView?.setTitle(textId)
} }
} }
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedMessageView(textId: Int) { private fun setAdvancedUnlockedMessageView(textId: Int) {
lifecycleScope.launch(Dispatchers.Main) { activity?.runOnUiThread {
mAdvancedUnlockInfoView?.setMessage(textId) mAdvancedUnlockInfoView?.setMessage(textId)
} }
} }
private fun setAdvancedUnlockedMessageView(text: CharSequence) { private fun setAdvancedUnlockedMessageView(text: CharSequence) {
lifecycleScope.launch(Dispatchers.Main) { activity?.runOnUiThread {
mAdvancedUnlockInfoView?.message = text mAdvancedUnlockInfoView?.message = text
} }
} }

View File

@@ -19,18 +19,15 @@
*/ */
package com.kunzisoft.keepass.biometric package com.kunzisoft.keepass.biometric
import android.app.Activity
import android.app.KeyguardManager import android.app.KeyguardManager
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.* import androidx.biometric.BiometricManager.Authenticators.*
@@ -38,7 +35,6 @@ import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import java.security.KeyStore import java.security.KeyStore
import java.security.UnrecoverableKeyException import java.security.UnrecoverableKeyException
@@ -140,24 +136,18 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
// and the constrains (purposes) in the constructor of the Builder // and the constrains (purposes) in the constructor of the Builder
keyGenerator?.init( keyGenerator?.init(
KeyGenParameterSpec.Builder( KeyGenParameterSpec.Builder(
ADVANCED_UNLOCK_KEYSTORE_KEY, ADVANCED_UNLOCK_KEYSTORE_KEY,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.apply {
// Require the user to authenticate with a fingerprint to authorize every use // Require the user to authenticate with a fingerprint to authorize every use
// of the key, don't use it for device credential because it's the user authentication // of the key, don't use it for device credential because it's the user authentication
if (biometricUnlockEnable) { .apply {
setUserAuthenticationRequired(true) if (biometricUnlockEnable) {
setUserAuthenticationRequired(true)
}
} }
// To store in the security chip .build())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
&& retrieveContext().packageManager.hasSystemFeature(
PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
setIsStrongBoxBacked(true)
}
}
.build())
keyGenerator?.generateKey() keyGenerator?.generateKey()
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -174,12 +164,8 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
return null return null
} }
fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) { fun initEncryptData(actionIfCypherInit
initEncryptData(actionIfCypherInit, true) : (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
}
private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
firstLaunch: Boolean) {
if (!isKeyManagerInitialized) { if (!isKeyManagerInitialized) {
return return
} }
@@ -199,15 +185,10 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} catch (unrecoverableKeyException: UnrecoverableKeyException) { } catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException) Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException) advancedUnlockCallback?.onInvalidKeyException(unrecoverableKeyException)
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) { } catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException) Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
if (firstLaunch) { advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
initEncryptData(actionIfCypherInit, false)
} else {
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to initialize encrypt data", e) Log.e(TAG, "Unable to initialize encrypt data", e)
advancedUnlockCallback?.onGenericException(e) advancedUnlockCallback?.onGenericException(e)
@@ -233,14 +214,8 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} }
fun initDecryptData(ivSpecValue: String, fun initDecryptData(ivSpecValue: String, actionIfCypherInit
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) { : (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
initDecryptData(ivSpecValue, actionIfCypherInit, true)
}
private fun initDecryptData(ivSpecValue: String,
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
firstLaunch: Boolean = true) {
if (!isKeyManagerInitialized) { if (!isKeyManagerInitialized) {
return return
} }
@@ -264,20 +239,10 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} catch (unrecoverableKeyException: UnrecoverableKeyException) { } catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException) Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
if (firstLaunch) { deleteKeystoreKey()
deleteKeystoreKey()
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
} else {
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
}
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) { } catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException) Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
if (firstLaunch) { advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
} else {
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to initialize decrypt data", e) Log.e(TAG, "Unable to initialize decrypt data", e)
advancedUnlockCallback?.onGenericException(e) advancedUnlockCallback?.onGenericException(e)
@@ -313,9 +278,9 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} }
fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt, @Suppress("DEPRECATION")
deviceCredentialResultLauncher: ActivityResultLauncher<Intent> @Synchronized
) { fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
// Init advanced unlock prompt // Init advanced unlock prompt
if (biometricPrompt == null) { if (biometricPrompt == null) {
biometricPrompt = BiometricPrompt(retrieveContext(), biometricPrompt = BiometricPrompt(retrieveContext(),
@@ -346,10 +311,20 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
else if (cryptoPrompt.isDeviceCredentialOperation) { else if (cryptoPrompt.isDeviceCredentialOperation) {
val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java) val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java)
@Suppress("DEPRECATION") retrieveContext().startActivityForResult(
deviceCredentialResultLauncher.launch( keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription),
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription) REQUEST_DEVICE_CREDENTIAL)
) }
}
@Synchronized
fun onActivityResult(requestCode: Int, resultCode: Int) {
if (requestCode == REQUEST_DEVICE_CREDENTIAL) {
if (resultCode == Activity.RESULT_OK) {
advancedUnlockCallback?.onAuthenticationSucceeded()
} else {
advancedUnlockCallback?.onAuthenticationFailed()
}
} }
} }
@@ -358,7 +333,6 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
interface AdvancedUnlockErrorCallback { interface AdvancedUnlockErrorCallback {
fun onUnrecoverableKeyException(e: Exception)
fun onInvalidKeyException(e: Exception) fun onInvalidKeyException(e: Exception)
fun onGenericException(e: Exception) fun onGenericException(e: Exception)
} }
@@ -381,6 +355,8 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7 private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
private const val REQUEST_DEVICE_CREDENTIAL = 556
@RequiresApi(api = Build.VERSION_CODES.M) @RequiresApi(api = Build.VERSION_CODES.M)
fun canAuthenticate(context: Context): Int { fun canAuthenticate(context: Context): Int {
return try { return try {
@@ -473,10 +449,6 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
override fun handleDecryptedResult(decryptedValue: String) {} override fun handleDecryptedResult(decryptedValue: String) {}
override fun onUnrecoverableKeyException(e: Exception) {
advancedCallback.onUnrecoverableKeyException(e)
}
override fun onInvalidKeyException(e: Exception) { override fun onInvalidKeyException(e: Exception) {
advancedCallback.onInvalidKeyException(e) advancedCallback.onInvalidKeyException(e)
} }
@@ -488,33 +460,6 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
deleteKeystoreKey() deleteKeystoreKey()
} }
} }
fun deleteAllEntryKeysInKeystoreForBiometric(activity: FragmentActivity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
deleteEntryKeyInKeystoreForBiometric(
activity,
object : AdvancedUnlockErrorCallback {
fun showException(e: Exception) {
Toast.makeText(activity,
activity.getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
Toast.LENGTH_SHORT).show()
}
override fun onUnrecoverableKeyException(e: Exception) {
showException(e)
}
override fun onInvalidKeyException(e: Exception) {
showException(e)
}
override fun onGenericException(e: Exception) {
showException(e)
}
})
}
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
}
} }
} }

View File

@@ -26,13 +26,13 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil
class CreateDatabaseRunnable(context: Context, class CreateDatabaseRunnable(context: Context,
private val mDatabase: Database, private val mDatabase: Database,
databaseUri: Uri, databaseUri: Uri,
private val databaseName: String, private val databaseName: String,
private val rootName: String, private val rootName: String,
private val templateGroupName: String?,
mainCredential: MainCredential, mainCredential: MainCredential,
private val createDatabaseResult: ((Result) -> Unit)?) private val createDatabaseResult: ((Result) -> Unit)?)
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) { : AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
@@ -41,10 +41,10 @@ class CreateDatabaseRunnable(context: Context,
try { try {
// Create new database record // Create new database record
mDatabase.apply { mDatabase.apply {
createData(mDatabaseUri, databaseName, rootName, templateGroupName) createData(mDatabaseUri, databaseName, rootName)
} }
} catch (e: Exception) { } catch (e: Exception) {
mDatabase.clearAndClose(context) mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
setError(e) 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.CipherDatabaseEntity
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.binary.LoadedKey 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.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
@@ -47,7 +47,7 @@ class LoadDatabaseRunnable(private val context: Context,
override fun onStartRun() { override fun onStartRun() {
// Clear before we load // Clear before we load
mDatabase.clearAndClose(context) mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
} }
override fun onActionRun() { override fun onActionRun() {
@@ -85,7 +85,7 @@ class LoadDatabaseRunnable(private val context: Context,
// Register the current time to init the lock timer // Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context) PreferencesUtil.saveCurrentTime(context)
} else { } else {
mDatabase.clearAndClose(context) mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
} }
} }

View File

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

View File

@@ -36,11 +36,7 @@ abstract class ActionNodeDatabaseRunnable(
abstract fun nodeAction() abstract fun nodeAction()
override fun onStartRun() { override fun onStartRun() {
try { nodeAction()
nodeAction()
} catch (e: Exception) {
setError(e)
}
super.onStartRun() super.onStartRun()
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,9 +36,6 @@ abstract class CipherEngine {
return 16 return 16
} }
// Used only with padding workaround
var forcePaddingCompatibility = false
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class) @Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
abstract fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher 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) @Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher { 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 { 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.encrypt.HashManager
import com.kunzisoft.keepass.utils.UnsignedLong import com.kunzisoft.keepass.utils.UnsignedLong
import com.kunzisoft.encrypt.aes.AESTransformer
import com.kunzisoft.keepass.utils.bytes16ToUuid import com.kunzisoft.keepass.utils.bytes16ToUuid
import encrypt.Encrypt
import java.io.IOException import java.io.IOException
import java.security.SecureRandom import java.security.SecureRandom
import java.util.* import java.util.*
@@ -58,7 +58,8 @@ class AesKdf : KdfEngine() {
val rounds = kdfParameters.getUInt64(PARAM_ROUNDS)?.toKotlinLong() 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) { 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.UnsignedInt
import com.kunzisoft.keepass.utils.UnsignedLong 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 com.kunzisoft.keepass.utils.bytes16ToUuid
import encrypt.Encrypt
import java.io.IOException import java.io.IOException
import java.security.SecureRandom import java.security.SecureRandom
import java.util.* import java.util.*
@@ -63,7 +62,14 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
// val secretKey = kdfParameters.getByteArray(PARAM_SECRET_KEY) // val secretKey = kdfParameters.getByteArray(PARAM_SECRET_KEY)
// val assocData = kdfParameters.getByteArray(PARAM_ASSOC_DATA) // 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( return Argon2Transformer.transformKey(
argonType, argonType,
@@ -73,6 +79,7 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
memory, memory,
iterations, iterations,
version) version)
*/
} }
override fun randomize(kdfParameters: KdfParameters) { override fun randomize(kdfParameters: KdfParameters) {
@@ -192,17 +199,17 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
private val MIN_VERSION = UnsignedInt(0x10) private val MIN_VERSION = UnsignedInt(0x10)
private val MAX_VERSION = UnsignedInt(0x13) private val MAX_VERSION = UnsignedInt(0x13)
private val DEFAULT_ITERATIONS = UnsignedLong(3L) private val DEFAULT_ITERATIONS = UnsignedLong(2L)
private val MIN_ITERATIONS = UnsignedLong(1L) private val MIN_ITERATIONS = UnsignedLong(1L)
private val MAX_ITERATIONS = UnsignedLong(4294967295L) private val MAX_ITERATIONS = UnsignedLong(4294967295L)
private val DEFAULT_MEMORY = UnsignedLong((1024L * 1024L * 16L)) private val DEFAULT_MEMORY = UnsignedLong((1024L * 1024L))
private val MIN_MEMORY = UnsignedLong(1024L * 8L) private val MIN_MEMORY = UnsignedLong(1024L * 8L)
private val MAX_MEMORY = UnsignedInt.MAX_VALUE.toKotlinLong() private val MAX_MEMORY = UnsignedInt.MAX_VALUE.toKotlinLong()
private const val MEMORY_BLOCK_SIZE: Long = 1024L private const val MEMORY_BLOCK_SIZE: Long = 1024L
private val DEFAULT_PARALLELISM = UnsignedInt(4) private val DEFAULT_PARALLELISM = UnsignedInt(2)
private val MIN_PARALLELISM = UnsignedInt.fromKotlinLong(1L) private val MIN_PARALLELISM = UnsignedInt.fromKotlinLong(1L)
private val MAX_PARALLELISM = UnsignedInt.fromKotlinLong(((1 shl 24) - 1).toLong()) private val MAX_PARALLELISM = UnsignedInt.fromKotlinLong(((1 shl 24) - 1))
} }
} }

View File

@@ -68,7 +68,7 @@ abstract class EntryCursor<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>>
pwEntry.notes = getString(getColumnIndex(COLUMN_INDEX_NOTES)) pwEntry.notes = getString(getColumnIndex(COLUMN_INDEX_NOTES))
pwEntry.expiryTime = DateInstant(getString(getColumnIndex(COLUMN_INDEX_EXPIRY_TIME))) pwEntry.expiryTime = DateInstant(getString(getColumnIndex(COLUMN_INDEX_EXPIRY_TIME)))
pwEntry.expires = getString(getColumnIndex(COLUMN_INDEX_EXPIRES)) pwEntry.expires = getString(getColumnIndex(COLUMN_INDEX_EXPIRES))
.lowercase(Locale.ENGLISH) != "false" .toLowerCase(Locale.ENGLISH) != "false"
} }
companion object { companion object {

View File

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

View File

@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.database.cursor
import android.database.MatrixCursor import android.database.MatrixCursor
import android.provider.BaseColumns 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.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
@@ -37,17 +36,13 @@ class ExtraFieldCursor : MatrixCursor(arrayOf(
private var fieldId: Long = 0 private var fieldId: Long = 0
@Synchronized @Synchronized
fun addExtraField(entryId: Long, field: Field) { fun addExtraField(entryId: Long, label: String, value: ProtectedString) {
addRow(arrayOf(fieldId, addRow(arrayOf(fieldId, entryId, label, if (value.isProtected) 1 else 0, value.toString()))
entryId,
field.name,
if (field.protectedValue.isProtected) 1 else 0,
field.protectedValue.toString()))
fieldId++ fieldId++
} }
fun populateExtraFieldInEntry(pwEntry: EntryKDBX) { fun populateExtraFieldInEntry(pwEntry: EntryKDBX) {
pwEntry.putField(getString(getColumnIndex(COLUMN_LABEL)), pwEntry.putExtraField(getString(getColumnIndex(COLUMN_LABEL)),
ProtectedString(getInt(getColumnIndex(COLUMN_PROTECTION)) > 0, ProtectedString(getInt(getColumnIndex(COLUMN_PROTECTION)) > 0,
getString(getColumnIndex(COLUMN_VALUE)))) 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 package com.kunzisoft.keepass.database.element
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import com.kunzisoft.androidclearchroma.ChromaUtil import com.kunzisoft.keepass.utils.readBytes4ToUInt
import com.kunzisoft.keepass.database.action.node.NodeHandler import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine 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.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdInt import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID 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.exception.*
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX 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.DatabaseInputKDB
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB 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.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.SingletonHolder import com.kunzisoft.keepass.utils.SingletonHolder
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.readBytes4ToUInt
import java.io.* import java.io.*
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@@ -110,6 +105,10 @@ class Database {
return mDatabaseKDB?.binaryCache ?: mDatabaseKDBX?.binaryCache ?: BinaryCache() return mDatabaseKDB?.binaryCache ?: mDatabaseKDBX?.binaryCache ?: BinaryCache()
} }
fun setCacheDirectory(cacheDirectory: File) {
binaryCache.cacheDirectory = cacheDirectory
}
private val iconsManager: IconsManager private val iconsManager: IconsManager
get() { get() {
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager(binaryCache) return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager(binaryCache)
@@ -147,61 +146,6 @@ class Database {
iconsManager.removeCustomIcon(binaryCache, customIcon.uuid) iconsManager.removeCustomIcon(binaryCache, customIcon.uuid)
} }
fun updateCustomIcon(customIcon: IconImageCustom) {
iconsManager.getIcon(customIcon.uuid).updateWith(customIcon)
}
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 val allowName: Boolean
get() = mDatabaseKDBX != null get() = mDatabaseKDBX != null
@@ -226,33 +170,31 @@ class Database {
mDatabaseKDBX?.descriptionChanged = DateInstant() mDatabaseKDBX?.descriptionChanged = DateInstant()
} }
val allowDefaultUsername: Boolean
get() = mDatabaseKDBX != null
// TODO get() = mDatabaseKDB != null || mDatabaseKDBX != null
var defaultUsername: String var defaultUsername: String
get() { get() {
return mDatabaseKDB?.defaultUserName ?: mDatabaseKDBX?.defaultUserName ?: "" return mDatabaseKDBX?.defaultUserName ?: "" // TODO mDatabaseKDB default username
} }
set(username) { set(username) {
mDatabaseKDB?.defaultUserName = username
mDatabaseKDBX?.defaultUserName = username mDatabaseKDBX?.defaultUserName = username
mDatabaseKDBX?.defaultUserNameChanged = DateInstant() mDatabaseKDBX?.defaultUserNameChanged = DateInstant()
} }
var customColor: Int? val allowCustomColor: Boolean
get() = mDatabaseKDBX != null
// TODO get() = mDatabaseKDB != null || mDatabaseKDBX != null
// with format "#000000"
var customColor: String
get() { get() {
var colorInt: Int? = null return mDatabaseKDBX?.color ?: "" // TODO mDatabaseKDB color
mDatabaseKDBX?.color?.let {
try {
colorInt = Color.parseColor(it)
} catch (e: Exception) {}
}
return mDatabaseKDB?.color ?: colorInt
} }
set(value) { set(value) {
mDatabaseKDB?.color = value // TODO Check color string
mDatabaseKDBX?.color = if (value == null) { mDatabaseKDBX?.color = value
""
} else {
ChromaUtil.getFormattedColorString(value, false)
}
} }
val allowOTP: Boolean val allowOTP: Boolean
@@ -284,7 +226,7 @@ class Database {
// Default compression not necessary if stored in header // Default compression not necessary if stored in header
mDatabaseKDBX?.let { mDatabaseKDBX?.let {
return it.compressionAlgorithm == CompressionAlgorithm.GZip return it.compressionAlgorithm == CompressionAlgorithm.GZip
&& it.kdbxVersion.isBefore(FILE_VERSION_40) && it.kdbxVersion.isBefore(FILE_VERSION_32_4)
} }
return false return false
} }
@@ -366,7 +308,7 @@ class Database {
mDatabaseKDBX?.masterKey = masterKey mDatabaseKDBX?.masterKey = masterKey
} }
var rootGroup: Group? val rootGroup: Group?
get() { get() {
mDatabaseKDB?.rootGroup?.let { mDatabaseKDB?.rootGroup?.let {
return Group(it) return Group(it)
@@ -376,34 +318,6 @@ class Database {
} }
return null return null
} }
set(value) {
value?.groupKDB?.let { rootKDB ->
mDatabaseKDB?.rootGroup = rootKDB
}
value?.groupKDBX?.let { rootKDBX ->
mDatabaseKDBX?.rootGroup = rootKDBX
}
}
val rootGroupIsVirtual: Boolean
get() {
mDatabaseKDB?.let {
return true
}
mDatabaseKDBX?.let {
return false
}
return true
}
/**
* 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 val manageHistory: Boolean
get() = mDatabaseKDBX != null get() = mDatabaseKDBX != null
@@ -431,18 +345,12 @@ class Database {
val allowConfigurableRecycleBin: Boolean val allowConfigurableRecycleBin: Boolean
get() = mDatabaseKDBX != null get() = mDatabaseKDBX != null
val isRecycleBinEnabled: Boolean var isRecycleBinEnabled: Boolean
// Backup is always enabled in KDB database // Backup is always enabled in KDB database
get() = mDatabaseKDB != null || mDatabaseKDBX?.isRecycleBinEnabled ?: false get() = mDatabaseKDB != null || mDatabaseKDBX?.isRecycleBinEnabled ?: false
set(value) {
fun enableRecycleBin(enable: Boolean, resources: Resources) { mDatabaseKDBX?.isRecycleBinEnabled = value
mDatabaseKDBX?.isRecycleBinEnabled = enable
if (enable) {
ensureRecycleBinExists(resources)
} else {
mDatabaseKDBX?.removeRecycleBin()
} }
}
val recycleBin: Group? val recycleBin: Group?
get() { get() {
@@ -455,52 +363,16 @@ class Database {
return null return null
} }
fun setRecycleBin(group: Group?) { fun ensureRecycleBinExists(resources: Resources) {
// Only the kdbx recycle bin can be changed mDatabaseKDB?.ensureBackupExists()
if (group != null) { mDatabaseKDBX?.ensureRecycleBinExists(resources)
mDatabaseKDBX?.recycleBinUUID = group.nodeIdKDBX.id
} else {
mDatabaseKDBX?.removeTemplatesGroup()
}
} }
/** fun removeRecycleBin() {
* Determine if a configurable templates group is available or not for this version of database // Don't allow remove backup in KDB
* @return true if a configurable templates group available mDatabaseKDBX?.removeRecycleBin()
*/
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)
} }
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) { private fun setDatabaseKDB(databaseKDB: DatabaseKDB) {
this.mDatabaseKDB = databaseKDB this.mDatabaseKDB = databaseKDB
this.mDatabaseKDBX = null this.mDatabaseKDBX = null
@@ -511,11 +383,8 @@ class Database {
this.mDatabaseKDBX = databaseKDBX this.mDatabaseKDBX = databaseKDBX
} }
fun createData(databaseUri: Uri, fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
databaseName: String, val newDatabase = DatabaseKDBX(databaseName, rootName)
rootName: String,
templateGroupName: String?) {
val newDatabase = DatabaseKDBX(databaseName, rootName, templateGroupName)
setDatabaseKDBX(newDatabase) setDatabaseKDBX(newDatabase)
this.fileUri = databaseUri this.fileUri = databaseUri
// Set Database state // Set Database state
@@ -677,28 +546,25 @@ class Database {
omitBackup: Boolean, omitBackup: Boolean,
max: Int = Integer.MAX_VALUE): Group? { max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this, return mSearchHelper?.createVirtualGroupWithSearchResult(this,
SearchParameters().apply { searchQuery, SearchParameters(), omitBackup, max)
this.searchQuery = searchQuery
}, omitBackup, max)
} }
fun createVirtualGroupFromSearchInfo(searchInfoString: String, fun createVirtualGroupFromSearchInfo(searchInfoString: String,
omitBackup: Boolean, omitBackup: Boolean,
max: Int = Integer.MAX_VALUE): Group? { max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this, return mSearchHelper?.createVirtualGroupWithSearchResult(this,
SearchParameters().apply { searchInfoString, SearchParameters().apply {
searchQuery = searchInfoString searchInTitles = true
searchInTitles = true searchInUserNames = false
searchInUserNames = false searchInPasswords = false
searchInPasswords = false searchInUrls = true
searchInUrls = true searchInNotes = true
searchInNotes = true searchInOTP = false
searchInOTP = false searchInOther = true
searchInOther = true searchInUUIDs = false
searchInUUIDs = false searchInTags = false
searchInTags = false ignoreCase = true
searchInTemplates = false }, omitBackup, max)
}, omitBackup, max)
} }
val attachmentPool: AttachmentPool val attachmentPool: AttachmentPool
@@ -715,11 +581,10 @@ class Database {
return false return false
} }
fun buildNewBinaryAttachment(): BinaryData? { fun buildNewBinaryAttachment(compressed: Boolean = false,
protected: Boolean = false): BinaryData? {
return mDatabaseKDB?.buildNewAttachment() return mDatabaseKDB?.buildNewAttachment()
?: mDatabaseKDBX?.buildNewAttachment( false, ?: mDatabaseKDBX?.buildNewAttachment( false, compressed, protected)
compressionForNewEntry(),
false)
} }
fun removeAttachmentIfNotUsed(attachment: Attachment) { fun removeAttachmentIfNotUsed(attachment: Attachment) {
@@ -810,8 +675,8 @@ class Database {
} }
} }
fun clearAndClose(context: Context? = null) { fun clearAndClose(filesDirectory: File? = null) {
clear(context?.let { UriUtil.getBinaryDir(context) }) clear(filesDirectory)
this.mDatabaseKDB = null this.mDatabaseKDB = null
this.mDatabaseKDBX = null this.mDatabaseKDBX = null
this.fileUri = null this.fileUri = null
@@ -929,11 +794,11 @@ class Database {
} }
fun addGroupTo(group: Group, parent: Group) { fun addGroupTo(group: Group, parent: Group) {
group.groupKDB?.let { groupKDB -> group.groupKDB?.let { entryKDB ->
mDatabaseKDB?.addGroupTo(groupKDB, parent.groupKDB) mDatabaseKDB?.addGroupTo(entryKDB, parent.groupKDB)
} }
group.groupKDBX?.let { groupKDBX -> group.groupKDBX?.let { entryKDBX ->
mDatabaseKDBX?.addGroupTo(groupKDBX, parent.groupKDBX) mDatabaseKDBX?.addGroupTo(entryKDBX, parent.groupKDBX)
} }
group.afterAssignNewParent() group.afterAssignNewParent()
} }
@@ -948,11 +813,11 @@ class Database {
} }
fun removeGroupFrom(group: Group, parent: Group) { fun removeGroupFrom(group: Group, parent: Group) {
group.groupKDB?.let { groupKDB -> group.groupKDB?.let { entryKDB ->
mDatabaseKDB?.removeGroupFrom(groupKDB, parent.groupKDB) mDatabaseKDB?.removeGroupFrom(entryKDB, parent.groupKDB)
} }
group.groupKDBX?.let { groupKDBX -> group.groupKDBX?.let { entryKDBX ->
mDatabaseKDBX?.removeGroupFrom(groupKDBX, parent.groupKDBX) mDatabaseKDBX?.removeGroupFrom(entryKDBX, parent.groupKDBX)
} }
group.afterAssignNewParent() group.afterAssignNewParent()
} }
@@ -1027,11 +892,6 @@ class Database {
} }
} }
fun ensureRecycleBinExists(resources: Resources) {
mDatabaseKDB?.ensureBackupExists()
mDatabaseKDBX?.ensureRecycleBinExists(resources)
}
fun canRecycle(entry: Entry): Boolean { fun canRecycle(entry: Entry): Boolean {
var canRecycle: Boolean? = null var canRecycle: Boolean? = null
entry.entryKDB?.let { entry.entryKDB?.let {

View File

@@ -23,209 +23,95 @@ import android.content.res.Resources
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import androidx.core.os.ConfigurationCompat import androidx.core.os.ConfigurationCompat
import com.kunzisoft.keepass.utils.readEnum import org.joda.time.Duration
import com.kunzisoft.keepass.utils.writeEnum import org.joda.time.Instant
import org.joda.time.*
import java.text.DateFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
class DateInstant : Parcelable { class DateInstant : Parcelable {
private var jDate: Date = Date() private var jDate: Date = Date()
private var mType: Type = Type.DATE_TIME
val date: Date val date: Date
get() = jDate get() = jDate
var type: Type
get() = mType
set(value) {
mType = value
}
constructor(source: DateInstant) { constructor(source: DateInstant) {
this.jDate = Date(source.jDate.time) 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) jDate = Date(date.time)
mType = type
} }
constructor(millis: Long, type: Type = Type.DATE_TIME) { constructor(millis: Long) {
jDate = Date(millis) jDate = Date(millis)
mType = type
} }
private fun parse(value: String, type: Type): Date { constructor(string: String) {
return when (type) { jDate = dateFormat.parse(string) ?: jDate
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() { constructor() {
jDate = Date() jDate = Date()
} }
constructor(parcel: Parcel) { protected constructor(parcel: Parcel) {
jDate = parcel.readSerializable() as? Date? ?: jDate jDate = parcel.readSerializable() as Date
mType = parcel.readEnum<Type>() ?: mType
} }
override fun describeContents(): Int { override fun describeContents(): Int {
return 0 return 0
} }
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeSerializable(jDate)
dest.writeEnum(mType)
}
fun getDateTimeString(resources: Resources): String { fun getDateTimeString(resources: Resources): String {
return when (mType) { return Companion.getDateTimeString(resources, this.date)
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)
}
} }
fun getYearInt(): Int { override fun writeToParcel(dest: Parcel, flags: Int) {
val dateFormat = SimpleDateFormat("yyyy", Locale.ENGLISH) dest.writeSerializable(date)
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 equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) {
if (other !is DateInstant) return false return true
}
if (other == null) {
return false
}
if (javaClass != other.javaClass) {
return false
}
if (jDate != other.jDate) return false val date = other as DateInstant
if (mType != other.mType) return false return isSameDate(jDate, date.jDate)
return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = jDate.hashCode() return jDate.hashCode()
result = 31 * result + mType.hashCode()
return result
} }
enum class Type { override fun toString(): String {
DATE_TIME, DATE, TIME return dateFormat.format(jDate)
} }
companion object { companion object {
val NEVER_EXPIRES = DateInstant(Calendar.getInstance().apply { val NEVER_EXPIRE = neverExpire
set(Calendar.YEAR, 2999) val IN_ONE_MONTH = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
set(Calendar.MONTH, 11) private val dateFormat = SimpleDateFormat.getDateTimeInstance()
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)
private val dateTimeFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.ROOT).apply { private val neverExpire: DateInstant
timeZone = TimeZone.getTimeZone("UTC") get() {
} val cal = Calendar.getInstance()
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'Z'", Locale.ROOT).apply { cal.set(Calendar.YEAR, 2999)
timeZone = TimeZone.getTimeZone("UTC") cal.set(Calendar.MONTH, 11)
} cal.set(Calendar.DAY_OF_MONTH, 28)
private val timeFormat = SimpleDateFormat("HH:mm'Z'", Locale.ROOT).apply { cal.set(Calendar.HOUR, 23)
timeZone = TimeZone.getTimeZone("UTC") cal.set(Calendar.MINUTE, 59)
} cal.set(Calendar.SECOND, 59)
return DateInstant(cal.time)
}
@JvmField @JvmField
val CREATOR: Parcelable.Creator<DateInstant> = object : Parcelable.Creator<DateInstant> { val CREATOR: Parcelable.Creator<DateInstant> = object : Parcelable.Creator<DateInstant> {
@@ -237,5 +123,31 @@ class DateInstant : Parcelable {
return arrayOfNulls(size) 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 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 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 var uuid: UUID = DatabaseVersioned.UUID_ZERO
private var mDeletionTime: DateInstant? = null private var mDeletionTime: Date? = null
constructor() fun getDeletionTime(): Date {
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 {
if (mDeletionTime == null) { if (mDeletionTime == null) {
mDeletionTime = DateInstant(System.currentTimeMillis()) mDeletionTime = Date(System.currentTimeMillis())
} }
return mDeletionTime!! 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 this.mDeletionTime = deletionTime
} }
@@ -66,23 +59,4 @@ class DeletedObject : Parcelable {
override fun hashCode(): Int { override fun hashCode(): Int {
return uuid.hashCode() 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

@@ -19,13 +19,10 @@
*/ */
package com.kunzisoft.keepass.database.element package com.kunzisoft.keepass.database.element
import android.graphics.Color
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.keepass.database.element.binary.AttachmentPool import com.kunzisoft.keepass.database.element.binary.AttachmentPool
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX 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.EntryKDB
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
@@ -35,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.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields
import java.util.* import java.util.*
@@ -47,6 +45,15 @@ class Entry : Node, EntryVersionedInterface<Group> {
var entryKDBX: EntryKDBX? = null var entryKDBX: EntryKDBX? = null
private set 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 * Use this constructor to copy an Entry with exact same values
*/ */
@@ -57,12 +64,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
if (entry.entryKDBX != null) { if (entry.entryKDBX != null) {
this.entryKDBX = EntryKDBX() this.entryKDBX = EntryKDBX()
} }
entry.entryKDB?.let { updateWith(entry, copyHistory)
this.entryKDB?.updateWith(it)
}
entry.entryKDBX?.let {
this.entryKDBX?.updateWith(it, copyHistory)
}
} }
constructor(entry: EntryKDB) { constructor(entry: EntryKDB) {
@@ -112,20 +114,6 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryKDBX?.icon = value 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 override val type: Type
get() = Type.ENTRY get() = Type.ENTRY
@@ -240,42 +228,6 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryKDBX?.notes = value entryKDBX?.notes = value
} }
var backgroundColor: Int?
get() {
var colorInt: Int? = null
entryKDBX?.backgroundColor?.let {
try {
colorInt = Color.parseColor(it)
} catch (e: Exception) {}
}
return colorInt
}
set(value) {
entryKDBX?.backgroundColor = if (value == null) {
""
} else {
ChromaUtil.getFormattedColorString(value, false)
}
}
var foregroundColor: Int?
get() {
var colorInt: Int? = null
entryKDBX?.foregroundColor?.let {
try {
colorInt = Color.parseColor(it)
} catch (e: Exception) {}
}
return colorInt
}
set(value) {
entryKDBX?.foregroundColor = if (value == null) {
""
} else {
ChromaUtil.getFormattedColorString(value, false)
}
}
private fun isTan(): Boolean { private fun isTan(): Boolean {
return title == PMS_TAN_ENTRY && username.isNotEmpty() return title == PMS_TAN_ENTRY && username.isNotEmpty()
} }
@@ -316,8 +268,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
fun getExtraFields(): List<Field> { fun getExtraFields(): List<Field> {
val extraFields = ArrayList<Field>() val extraFields = ArrayList<Field>()
entryKDBX?.let { entryKDBX?.let {
it.doForEachDecodedCustomField { field -> for (field in it.customFields) {
extraFields.add(field) extraFields.add(Field(field.key, field.value))
} }
} }
return extraFields return extraFields
@@ -327,7 +279,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
* Update or add an extra field to the list (standard or custom) * Update or add an extra field to the list (standard or custom)
*/ */
fun putExtraField(field: Field) { fun putExtraField(field: Field) {
entryKDBX?.putField(field) entryKDBX?.putExtraField(field.name, field.protectedValue)
} }
private fun addExtraFields(fields: List<Field>) { private fun addExtraFields(fields: List<Field>) {
@@ -343,7 +295,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
fun getOtpElement(): OtpElement? { fun getOtpElement(): OtpElement? {
entryKDBX?.let { entryKDBX?.let {
return OtpEntryFields.parseFields { key -> return OtpEntryFields.parseFields { key ->
it.getFieldValue(key)?.toString() it.customFields[key]?.toString()
} }
} }
return null return null
@@ -421,6 +373,10 @@ class Entry : Node, EntryVersionedInterface<Group> {
return entryKDBX?.getSize(attachmentPool) ?: 0L return entryKDBX?.getSize(attachmentPool) ?: 0L
} }
fun containsCustomData(): Boolean {
return entryKDBX?.containsCustomData() ?: false
}
/* /*
------------ ------------
Converter Converter
@@ -431,48 +387,37 @@ class Entry : Node, EntryVersionedInterface<Group> {
* Retrieve generated entry info. * Retrieve generated entry info.
* If are not [raw] data, remove parameter fields and add auto generated elements in auto custom fields * If are not [raw] data, remove parameter fields and add auto generated elements in auto custom fields
*/ */
fun getEntryInfo(database: Database?, fun getEntryInfo(database: Database?, raw: Boolean = false): EntryInfo {
raw: Boolean = false,
removeTemplateConfiguration: Boolean = true): EntryInfo {
val entryInfo = EntryInfo() val entryInfo = EntryInfo()
// Remove unwanted template fields if (raw)
val baseInfo = if (removeTemplateConfiguration) database?.stopManageEntry(this)
database?.removeTemplateConfiguration(this) ?: this
else else
this database?.startManageEntry(this)
baseInfo.apply {
if (raw)
database?.stopManageEntry(this)
else
database?.startManageEntry(this)
entryInfo.id = nodeId.id entryInfo.id = nodeId.toString()
entryInfo.title = title entryInfo.title = title
entryInfo.icon = icon entryInfo.icon = icon
entryInfo.username = username entryInfo.username = username
entryInfo.password = password entryInfo.password = password
entryInfo.creationTime = creationTime entryInfo.creationTime = creationTime
entryInfo.lastModificationTime = lastModificationTime entryInfo.lastModificationTime = lastModificationTime
entryInfo.expires = expires entryInfo.expires = expires
entryInfo.expiryTime = expiryTime entryInfo.expiryTime = expiryTime
entryInfo.url = url entryInfo.url = url
entryInfo.notes = notes entryInfo.notes = notes
entryInfo.backgroundColor = backgroundColor entryInfo.customFields = getExtraFields()
entryInfo.foregroundColor = foregroundColor // Add otpElement to generate token
entryInfo.customFields = getExtraFields().toMutableList() entryInfo.otpModel = getOtpElement()?.otpModel
// Add otpElement to generate token if (!raw) {
entryInfo.otpModel = getOtpElement()?.otpModel // Replace parameter fields by generated OTP fields
if (!raw) { entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
// 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)
} }
database?.attachmentPool?.let { binaryPool ->
entryInfo.attachments = getAttachments(binaryPool)
}
if (!raw)
database?.stopManageEntry(this)
return entryInfo return entryInfo
} }
@@ -493,8 +438,6 @@ class Entry : Node, EntryVersionedInterface<Group> {
expiryTime = newEntryInfo.expiryTime expiryTime = newEntryInfo.expiryTime
url = newEntryInfo.url url = newEntryInfo.url
notes = newEntryInfo.notes notes = newEntryInfo.notes
backgroundColor = newEntryInfo.backgroundColor
foregroundColor = newEntryInfo.foregroundColor
addExtraFields(newEntryInfo.customFields) addExtraFields(newEntryInfo.customFields)
database?.attachmentPool?.let { binaryPool -> database?.attachmentPool?.let { binaryPool ->
newEntryInfo.attachments.forEach { attachment -> newEntryInfo.attachments.forEach { attachment ->
@@ -523,7 +466,16 @@ class Entry : Node, EntryVersionedInterface<Group> {
return result 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>" const val PMS_TAN_ENTRY = "<TAN>"
/** /**
@@ -532,16 +484,5 @@ class Entry : Node, EntryVersionedInterface<Group> {
fun newExtraFieldNameAllowed(field: Field): Boolean { fun newExtraFieldNameAllowed(field: Field): Boolean {
return EntryKDBX.newCustomNameAllowed(field.name) 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.content.Context
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable 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.GroupKDB
import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
@@ -31,7 +30,6 @@ import com.kunzisoft.keepass.database.element.node.*
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.GroupInfo import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UuidUtil
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@@ -45,7 +43,14 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
// Virtual group is used to defined a detached database group // Virtual group is used to defined a detached database group
var isVirtual = false 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 * Use this constructor to copy a Group
@@ -59,12 +64,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
if (this.groupKDBX == null) if (this.groupKDBX == null)
this.groupKDBX = GroupKDBX() this.groupKDBX = GroupKDBX()
} }
group.groupKDB?.let { updateWith(group)
this.groupKDB?.updateWith(it)
}
group.groupKDBX?.let {
this.groupKDBX?.updateWith(it)
}
} }
constructor(group: GroupKDB) { constructor(group: GroupKDB) {
@@ -117,8 +117,8 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
dest.writeByte((if (isVirtual) 1 else 0).toByte()) dest.writeByte((if (isVirtual) 1 else 0).toByte())
} }
override val nodeId: NodeId<*> override val nodeId: NodeId<*>?
get() = groupKDBX?.nodeId ?: groupKDB?.nodeId ?: NodeIdUUID() get() = groupKDBX?.nodeId ?: groupKDB?.nodeId
override var title: String override var title: String
get() = groupKDB?.title ?: groupKDBX?.title ?: "" get() = groupKDB?.title ?: groupKDBX?.title ?: ""
@@ -134,20 +134,6 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
groupKDBX?.icon = value 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 override val type: Type
get() = Type.GROUP get() = Type.GROUP
@@ -178,20 +164,16 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
fun addChildrenFrom(group: Group) { fun addChildrenFrom(group: Group) {
group.groupKDB?.getChildEntries()?.forEach { entryToAdd -> group.groupKDB?.getChildEntries()?.forEach { entryToAdd ->
groupKDB?.addChildEntry(entryToAdd) groupKDB?.addChildEntry(entryToAdd)
entryToAdd.parent = groupKDB
} }
group.groupKDB?.getChildGroups()?.forEach { groupToAdd -> group.groupKDB?.getChildGroups()?.forEach { groupToAdd ->
groupKDB?.addChildGroup(groupToAdd) groupKDB?.addChildGroup(groupToAdd)
groupToAdd.parent = groupKDB
} }
group.groupKDBX?.getChildEntries()?.forEach { entryToAdd -> group.groupKDBX?.getChildEntries()?.forEach { entryToAdd ->
groupKDBX?.addChildEntry(entryToAdd) groupKDBX?.addChildEntry(entryToAdd)
entryToAdd.parent = groupKDBX
} }
group.groupKDBX?.getChildGroups()?.forEach { groupToAdd -> group.groupKDBX?.getChildGroups()?.forEach { groupToAdd ->
groupKDBX?.addChildGroup(groupToAdd) groupKDBX?.addChildGroup(groupToAdd)
groupToAdd.parent = groupKDBX
} }
} }
@@ -273,20 +255,6 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
ArrayList() 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> { override fun getChildEntries(): List<Entry> {
return groupKDB?.getChildEntries()?.map { return groupKDB?.getChildEntries()?.map {
Entry(it) Entry(it)
@@ -309,9 +277,8 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
val withoutMetaStream = filters.contains(ChildFilter.META_STREAM) val withoutMetaStream = filters.contains(ChildFilter.META_STREAM)
val showExpiredEntries = !filters.contains(ChildFilter.EXPIRED) val showExpiredEntries = !filters.contains(ChildFilter.EXPIRED)
// TODO Change KDB parser to remove meta entries
return groupKDB?.getChildEntries()?.filter { return groupKDB?.getChildEntries()?.filter {
(!withoutMetaStream || (withoutMetaStream && !it.isMetaStream())) (!withoutMetaStream || (withoutMetaStream && !it.isMetaStream))
&& (!it.isCurrentlyExpires or showExpiredEntries) && (!it.isCurrentlyExpires or showExpiredEntries)
}?.map { }?.map {
Entry(it) Entry(it)
@@ -324,8 +291,8 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
ArrayList() ArrayList()
} }
fun refreshNumberOfChildEntries(filters: Array<ChildFilter> = emptyArray()) { fun getNumberOfChildEntries(filters: Array<ChildFilter> = emptyArray()): Int {
this.numberOfChildEntries = getFilteredChildEntries(filters).size return getFilteredChildEntries(filters).size
} }
/** /**
@@ -337,9 +304,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
} }
fun getFilteredChildren(filters: Array<ChildFilter>): List<Node> { fun getFilteredChildren(filters: Array<ChildFilter>): List<Node> {
val nodes = getFilteredChildGroups(filters) + getFilteredChildEntries(filters) return getChildGroups() + getFilteredChildEntries(filters)
refreshNumberOfChildEntries(filters)
return nodes
} }
override fun addChildGroup(group: Group) { override fun addChildGroup(group: Group) {
@@ -360,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) { override fun removeChildGroup(group: Group) {
group.groupKDB?.let { group.groupKDB?.let {
groupKDB?.removeChildGroup(it) groupKDB?.removeChildGroup(it)
@@ -421,6 +368,14 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
groupKDB?.nodeId = id groupKDB?.nodeId = id
} }
fun getLevel(): Int {
return groupKDB?.level ?: -1
}
fun setLevel(level: Int) {
groupKDB?.level = level
}
/* /*
------------ ------------
KDBX Methods KDBX Methods
@@ -447,6 +402,10 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
groupKDBX?.isExpanded = expanded groupKDBX?.isExpanded = expanded
} }
fun containsCustomData(): Boolean {
return groupKDBX?.containsCustomData() ?: false
}
/* /*
------------ ------------
Converter Converter
@@ -455,7 +414,6 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
fun getGroupInfo(): GroupInfo { fun getGroupInfo(): GroupInfo {
val groupInfo = GroupInfo() val groupInfo = GroupInfo()
groupInfo.id = groupKDBX?.nodeId?.id
groupInfo.title = title groupInfo.title = title
groupInfo.icon = icon groupInfo.icon = icon
groupInfo.creationTime = creationTime groupInfo.creationTime = creationTime
@@ -494,10 +452,4 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
result = 31 * result + (groupKDBX?.hashCode() ?: 0) result = 31 * result + (groupKDBX?.hashCode() ?: 0)
return result return result
} }
override fun toString(): String {
return groupKDB?.toString() ?: groupKDBX?.toString() ?: "Undefined"
}
} }

View File

@@ -28,17 +28,15 @@ import java.util.*
enum class SortNodeEnum { enum class SortNodeEnum {
DB, TITLE, USERNAME, CREATION_TIME, LAST_MODIFY_TIME, LAST_ACCESS_TIME; DB, TITLE, USERNAME, CREATION_TIME, LAST_MODIFY_TIME, LAST_ACCESS_TIME;
fun <G: GroupVersionedInterface<G, *>> getNodeComparator( fun <G: GroupVersionedInterface<G, *>> getNodeComparator(sortNodeParameters: SortNodeParameters)
database: Database,
sortNodeParameters: SortNodeParameters)
: Comparator<NodeVersionedInterface<G>> { : Comparator<NodeVersionedInterface<G>> {
return when (this) { return when (this) {
DB -> NodeNaturalComparator(database, sortNodeParameters) // Force false because natural order contains recycle bin DB -> NodeNaturalComparator(sortNodeParameters) // Force false because natural order contains recycle bin
TITLE -> NodeTitleComparator(database, sortNodeParameters) TITLE -> NodeTitleComparator(sortNodeParameters)
USERNAME -> NodeUsernameComparator(database, sortNodeParameters) USERNAME -> NodeUsernameComparator(sortNodeParameters)
CREATION_TIME -> NodeCreationComparator(database, sortNodeParameters) CREATION_TIME -> NodeCreationComparator(sortNodeParameters)
LAST_MODIFY_TIME -> NodeLastModificationComparator(database, sortNodeParameters) LAST_MODIFY_TIME -> NodeLastModificationComparator(sortNodeParameters)
LAST_ACCESS_TIME -> NodeLastAccessComparator(database, sortNodeParameters) LAST_ACCESS_TIME -> NodeLastAccessComparator(sortNodeParameters)
} }
} }
@@ -50,9 +48,11 @@ enum class SortNodeEnum {
< <
G: GroupVersionedInterface<*, *>, G: GroupVersionedInterface<*, *>,
T: NodeVersionedInterface<G> T: NodeVersionedInterface<G>
>(var database: Database, var sortNodeParameters: SortNodeParameters) >(var sortNodeParameters: SortNodeParameters)
: Comparator<T> { : Comparator<T> {
val database = Database.getInstance()
abstract fun compareBySpecificOrder(object1: T, object2: T): Int abstract fun compareBySpecificOrder(object1: T, object2: T): Int
private fun specificOrderOrHashIfEquals(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 * Comparator of node by natural database placement
*/ */
class NodeNaturalComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>( class NodeNaturalComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
database: Database,
sortNodeParameters: SortNodeParameters) sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(database, sortNodeParameters) { : NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int { override fun compareBySpecificOrder(object1: T, object2: T): Int {
return object1.nodeIndexInParentForNaturalOrder() return object1.nodeIndexInParentForNaturalOrder()
@@ -124,14 +123,13 @@ enum class SortNodeEnum {
* Comparator of Node by Title * Comparator of Node by Title
*/ */
class NodeTitleComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>( class NodeTitleComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
database: Database,
sortNodeParameters: SortNodeParameters) sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(database, sortNodeParameters) { : NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int { override fun compareBySpecificOrder(object1: T, object2: T): Int {
val titleCompare = object1.title.compareTo(object2.title, ignoreCase = true) val titleCompare = object1.title.compareTo(object2.title, ignoreCase = true)
return if (titleCompare == 0) return if (titleCompare == 0)
NodeNaturalComparator<G, T>(database, sortNodeParameters) NodeNaturalComparator<G, T>(sortNodeParameters)
.compare(object1, object2) .compare(object1, object2)
else else
titleCompare titleCompare
@@ -142,9 +140,8 @@ enum class SortNodeEnum {
* Comparator of Node by Username, Groups by title * Comparator of Node by Username, Groups by title
*/ */
class NodeUsernameComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>( class NodeUsernameComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
database: Database,
sortNodeParameters: SortNodeParameters) sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(database, sortNodeParameters) { : NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int { override fun compareBySpecificOrder(object1: T, object2: T): Int {
return if (object1.type == Type.ENTRY && object2.type == Type.ENTRY) { return if (object1.type == Type.ENTRY && object2.type == Type.ENTRY) {
@@ -153,12 +150,12 @@ enum class SortNodeEnum {
.compareTo((object2 as Entry).getEntryInfo(database).username, .compareTo((object2 as Entry).getEntryInfo(database).username,
ignoreCase = true) ignoreCase = true)
if (usernameCompare == 0) if (usernameCompare == 0)
NodeTitleComparator<G, T>(database, sortNodeParameters) NodeTitleComparator<G, T>(sortNodeParameters)
.compare(object1, object2) .compare(object1, object2)
else else
usernameCompare usernameCompare
} else { } else {
NodeTitleComparator<G, T>(database, sortNodeParameters) NodeTitleComparator<G, T>(sortNodeParameters)
.compare(object1, object2) .compare(object1, object2)
} }
} }
@@ -168,15 +165,14 @@ enum class SortNodeEnum {
* Comparator of node by creation * Comparator of node by creation
*/ */
class NodeCreationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>( class NodeCreationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
database: Database,
sortNodeParameters: SortNodeParameters) sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(database, sortNodeParameters) { : NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int { override fun compareBySpecificOrder(object1: T, object2: T): Int {
val creationCompare = object1.creationTime.date val creationCompare = object1.creationTime.date
.compareTo(object2.creationTime.date) .compareTo(object2.creationTime.date)
return if (creationCompare == 0) return if (creationCompare == 0)
NodeNaturalComparator<G, T>(database, sortNodeParameters) NodeNaturalComparator<G, T>(sortNodeParameters)
.compare(object1, object2) .compare(object1, object2)
else else
creationCompare creationCompare
@@ -187,15 +183,14 @@ enum class SortNodeEnum {
* Comparator of node by last modification * Comparator of node by last modification
*/ */
class NodeLastModificationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>( class NodeLastModificationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
database: Database,
sortNodeParameters: SortNodeParameters) sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(database, sortNodeParameters) { : NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int { override fun compareBySpecificOrder(object1: T, object2: T): Int {
val lastModificationCompare = object1.lastModificationTime.date val lastModificationCompare = object1.lastModificationTime.date
.compareTo(object2.lastModificationTime.date) .compareTo(object2.lastModificationTime.date)
return if (lastModificationCompare == 0) return if (lastModificationCompare == 0)
NodeNaturalComparator<G, T>(database, sortNodeParameters) NodeNaturalComparator<G, T>(sortNodeParameters)
.compare(object1, object2) .compare(object1, object2)
else else
lastModificationCompare lastModificationCompare
@@ -206,15 +201,14 @@ enum class SortNodeEnum {
* Comparator of node by last access * Comparator of node by last access
*/ */
class NodeLastAccessComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>( class NodeLastAccessComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
database: Database,
sortNodeParameters: SortNodeParameters) sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(database, sortNodeParameters) { : NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int { override fun compareBySpecificOrder(object1: T, object2: T): Int {
val lastAccessCompare = object1.lastAccessTime.date val lastAccessCompare = object1.lastAccessTime.date
.compareTo(object2.lastAccessTime.date) .compareTo(object2.lastAccessTime.date)
return if (lastAccessCompare == 0) return if (lastAccessCompare == 0)
NodeNaturalComparator<G, T>(database, sortNodeParameters) NodeNaturalComparator<G, T>(sortNodeParameters)
.compare(object1, object2) .compare(object1, object2)
else else
lastAccessCompare 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 loadedCipherKey: LoadedKey = LoadedKey.generateNewCipherKey()
var cacheDirectory: File? = null lateinit var cacheDirectory: File
private val voidBinary = KeyByteArray(UNKNOWN, ByteArray(0)) private val voidBinary = KeyByteArray(UNKNOWN, ByteArray(0))
@@ -19,16 +19,15 @@ class BinaryCache {
smallSize: Boolean = false, smallSize: Boolean = false,
compression: Boolean = false, compression: Boolean = false,
protection: Boolean = false): BinaryData { protection: Boolean = false): BinaryData {
val cacheDir = cacheDirectory return if (smallSize) {
return if (smallSize || cacheDir == null) {
BinaryByte(binaryId, compression, protection) BinaryByte(binaryId, compression, protection)
} else { } else {
val fileInCache = File(cacheDir, binaryId) val fileInCache = File(cacheDirectory, binaryId)
BinaryFile(fileInCache, compression, protection) 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>() private val byteArrayList = HashMap<String, ByteArray>()
fun getByteArray(key: String): KeyByteArray { fun getByteArray(key: String): KeyByteArray {

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