mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
82 Commits
feature/Pa
...
4.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
208f1e97d5 | ||
|
|
e4e0628e20 | ||
|
|
f60f31771f | ||
|
|
ff6367bac4 | ||
|
|
540e72812e | ||
|
|
5fe4af8e9d | ||
|
|
ae42ab43b7 | ||
|
|
c463055971 | ||
|
|
1849dca81d | ||
|
|
b3dd3dcfb5 | ||
|
|
fef88ff270 | ||
|
|
f1f7dd1e6c | ||
|
|
96c3af097a | ||
|
|
4fe6b2e115 | ||
|
|
cc936b9304 | ||
|
|
e7f2a22583 | ||
|
|
4bf905ecda | ||
|
|
f8d80525d9 | ||
|
|
65857596a6 | ||
|
|
e6253336bd | ||
|
|
e5595a3275 | ||
|
|
366e8bf1d7 | ||
|
|
fa63265599 | ||
|
|
755e0ea9a5 | ||
|
|
50b1ac388e | ||
|
|
51c62034df | ||
|
|
e4d0cd89c6 | ||
|
|
e085d5d277 | ||
|
|
05336e93a0 | ||
|
|
90b3b56893 | ||
|
|
02c514272e | ||
|
|
989e47ed12 | ||
|
|
1caf132558 | ||
|
|
1b98bd740c | ||
|
|
5adeb5cde0 | ||
|
|
b949d5d861 | ||
|
|
b4264a30a4 | ||
|
|
cf799c0f68 | ||
|
|
97f0ca519b | ||
|
|
cf4047b701 | ||
|
|
40608a3eb5 | ||
|
|
99cb50d031 | ||
|
|
b0d0c35241 | ||
|
|
6044c93a4a | ||
|
|
b544b5d54d | ||
|
|
852378e484 | ||
|
|
711a344860 | ||
|
|
72087c7e5c | ||
|
|
a337de3679 | ||
|
|
75b37f5a9f | ||
|
|
075f54b286 | ||
|
|
e07cbc2e14 | ||
|
|
ac29b7bac7 | ||
|
|
b9129cb941 | ||
|
|
6957fcd81a | ||
|
|
cfe56fc055 | ||
|
|
6f3e065ad1 | ||
|
|
abfa7a3f47 | ||
|
|
dd0d85e54e | ||
|
|
76c20263f7 | ||
|
|
e447388611 | ||
|
|
1bfec67c02 | ||
|
|
45041216d6 | ||
|
|
e075e9018c | ||
|
|
eed304ec40 | ||
|
|
5bcbbac97f | ||
|
|
ea4750fc11 | ||
|
|
5037821529 | ||
|
|
3a4c88f19a | ||
|
|
e960a8e169 | ||
|
|
1d4e1687cf | ||
|
|
033fa95285 | ||
|
|
f17d211fbd | ||
|
|
cae69e7572 | ||
|
|
ae903ad236 | ||
|
|
7c3a15ce79 | ||
|
|
b609d4e182 | ||
|
|
e8ecf28f7c | ||
|
|
3d5adbfc01 | ||
|
|
72bfc50703 | ||
|
|
a60e2e780d | ||
|
|
9210851765 |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Bug Report
|
name: Bug report
|
||||||
description: Report a bug.
|
description: Report a bug.
|
||||||
labels: ["bug"]
|
labels: ["bug"]
|
||||||
body:
|
body:
|
||||||
|
|||||||
14
CHANGELOG
14
CHANGELOG
@@ -1,5 +1,17 @@
|
|||||||
KeePassDX(4.2.0)
|
KeePassDX(4.2.0)
|
||||||
* Passkeys management #1421 #2097 (Thx @cali-95)
|
* Passkeys management #1421 #2097 (@cali-95)
|
||||||
|
* Confirm usage of passkey #2165 #2124
|
||||||
|
* Dialog to manage missing signature #2152 #2155 #2161 #2160
|
||||||
|
* Capture error #2159 #2215
|
||||||
|
* Change Passkey Backup Eligibility & Backup State #2135 #2150 #2212
|
||||||
|
* Search settings #2112 #2181 #2187 #2204
|
||||||
|
* Autofill refactoring #765 #2196
|
||||||
|
* Small fixes #2157 #2164 #2171 #2122 #2180 #2209 #2214
|
||||||
|
|
||||||
|
KeePassDX(4.1.9)
|
||||||
|
* Fix landscape UI #2198 #2200 (@chenxiaolong)
|
||||||
|
* Fix start loop and flash screen #2201
|
||||||
|
* Small fixes
|
||||||
|
|
||||||
KeePassDX(4.1.8)
|
KeePassDX(4.1.8)
|
||||||
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)
|
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 19
|
minSdkVersion 19
|
||||||
targetSdkVersion 35
|
targetSdkVersion 35
|
||||||
versionCode = 142
|
versionCode = 145
|
||||||
versionName = "4.2.0beta02"
|
versionName = "4.2.0"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
|
|||||||
@@ -178,18 +178,22 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
|
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity"
|
||||||
android:theme="@style/Theme.Transparent" />
|
android:theme="@style/Theme.Transparent"
|
||||||
|
android:exported="false"
|
||||||
|
android:excludeFromRecents="true" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity"
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity"
|
||||||
android:theme="@style/Theme.Transparent"
|
android:theme="@style/Theme.Transparent"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:excludeFromRecents="true"/>
|
android:exported="false"
|
||||||
|
android:excludeFromRecents="true" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
|
||||||
android:theme="@style/Theme.Transparent"
|
android:theme="@style/Theme.Transparent"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
android:exported="true">
|
android:exported="true"
|
||||||
|
android:excludeFromRecents="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" />
|
||||||
@@ -208,8 +212,8 @@
|
|||||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
|
||||||
android:theme="@style/Theme.Transparent"
|
android:theme="@style/Theme.Transparent"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
tools:targetApi="upside_down_cake" />
|
tools:targetApi="upside_down_cake" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
||||||
|
|||||||
@@ -79,11 +79,13 @@ import com.kunzisoft.keepass.view.hideByFading
|
|||||||
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||||
|
import java.util.EnumSet
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class EntryActivity : DatabaseLockActivity() {
|
class EntryActivity : DatabaseLockActivity() {
|
||||||
|
|
||||||
private var footer: ViewGroup? = null
|
private var footer: ViewGroup? = null
|
||||||
|
private var container: View? = null
|
||||||
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 appBarLayout: AppBarLayout? = null
|
||||||
@@ -135,6 +137,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
|
|
||||||
// Get views
|
// Get views
|
||||||
footer = findViewById(R.id.activity_entry_footer)
|
footer = findViewById(R.id.activity_entry_footer)
|
||||||
|
container = findViewById(R.id.activity_entry_container)
|
||||||
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)
|
appBarLayout = findViewById(R.id.app_bar)
|
||||||
@@ -150,8 +153,12 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
setTransparentNavigationBar {
|
setTransparentNavigationBar {
|
||||||
// To fix margin with API 27
|
// To fix margin with API 27
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null)
|
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null)
|
||||||
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
|
container?.applyWindowInsets(EnumSet.of(
|
||||||
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
|
WindowInsetPosition.TOP_MARGINS,
|
||||||
|
WindowInsetPosition.BOTTOM_MARGINS,
|
||||||
|
WindowInsetPosition.START_MARGINS,
|
||||||
|
WindowInsetPosition.END_MARGINS,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty title
|
// Empty title
|
||||||
@@ -305,11 +312,11 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
launch(
|
launch(
|
||||||
this,
|
activity = this,
|
||||||
database,
|
database = database,
|
||||||
historySelected.nodeId,
|
entryId = historySelected.nodeId,
|
||||||
historySelected.historyPosition,
|
historyPosition = historySelected.historyPosition,
|
||||||
mEntryActivityResultLauncher
|
activityResultLauncher = mEntryActivityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,9 +330,8 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
return coordinatorLayout
|
return coordinatorLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
mEntryViewModel.loadDatabase(database)
|
mEntryViewModel.loadDatabase(database)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,11 +477,12 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
R.id.menu_edit -> {
|
R.id.menu_edit -> {
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
mMainEntryId?.let { entryId ->
|
mMainEntryId?.let { entryId ->
|
||||||
EntryEditActivity.launchToUpdate(
|
EntryEditActivity.launch(
|
||||||
this,
|
activity = this,
|
||||||
database,
|
database = database,
|
||||||
entryId,
|
registrationType = EntryEditActivity.RegistrationType.UPDATE,
|
||||||
mEntryActivityResultLauncher
|
nodeId = entryId,
|
||||||
|
activityResultLauncher = mEntryActivityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -513,7 +520,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
// 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, mMainEntryId)
|
||||||
setResult(Activity.RESULT_OK, this)
|
setResult(RESULT_OK, this)
|
||||||
}
|
}
|
||||||
super.finish()
|
super.finish()
|
||||||
}
|
}
|
||||||
@@ -527,34 +534,22 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
|
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open standard Entry activity
|
* Open standard or history Entry activity
|
||||||
*/
|
*/
|
||||||
fun launch(activity: Activity,
|
fun launch(
|
||||||
|
activity: Activity,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
entryId: NodeId<UUID>,
|
entryId: NodeId<UUID>,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
historyPosition: Int? = null,
|
||||||
if (database.loaded) {
|
activityResultLauncher: ActivityResultLauncher<Intent>
|
||||||
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: ContextualDatabase,
|
|
||||||
entryId: NodeId<UUID>,
|
|
||||||
historyPosition: Int,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
val intent = Intent(activity, EntryActivity::class.java)
|
val intent = Intent(activity, EntryActivity::class.java)
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
intent.putExtra(KEY_ENTRY, entryId)
|
||||||
|
historyPosition?.let {
|
||||||
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
||||||
|
}
|
||||||
activityResultLauncher.launch(intent)
|
activityResultLauncher.launch(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,14 +36,14 @@ import android.widget.Spinner
|
|||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.NestedScrollView
|
import androidx.core.widget.NestedScrollView
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.google.android.material.datepicker.MaterialDatePicker
|
import com.google.android.material.datepicker.MaterialDatePicker
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.android.material.timepicker.MaterialTimePicker
|
import com.google.android.material.timepicker.MaterialTimePicker
|
||||||
@@ -59,9 +59,10 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
|||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
|
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildSpecialModeResponseAndSetResult
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
|
||||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
@@ -101,6 +102,8 @@ import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
|||||||
import com.kunzisoft.keepass.view.updateLockPaddingStart
|
import com.kunzisoft.keepass.view.updateLockPaddingStart
|
||||||
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
||||||
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.EnumSet
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class EntryEditActivity : DatabaseLockActivity(),
|
class EntryEditActivity : DatabaseLockActivity(),
|
||||||
@@ -155,9 +158,6 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// To ask data lost only one time
|
|
||||||
private var backPressedAlreadyApproved = false
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_entry_edit)
|
setContentView(R.layout.activity_entry_edit)
|
||||||
@@ -181,8 +181,12 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
// To apply fit window with transparency
|
// To apply fit window with transparency
|
||||||
setTransparentNavigationBar(applyToStatusBar = true) {
|
setTransparentNavigationBar(applyToStatusBar = true) {
|
||||||
container?.applyWindowInsets(WindowInsetPosition.TOP_BOTTOM_IME)
|
container?.applyWindowInsets(EnumSet.of(
|
||||||
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM_IME)
|
WindowInsetPosition.TOP_MARGINS,
|
||||||
|
WindowInsetPosition.BOTTOM_MARGINS,
|
||||||
|
WindowInsetPosition.START_MARGINS,
|
||||||
|
WindowInsetPosition.END_MARGINS,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||||
@@ -206,8 +210,8 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
mDatabase,
|
mDatabase,
|
||||||
entryId,
|
entryId,
|
||||||
parentId,
|
parentId,
|
||||||
EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent),
|
intent.retrieveRegisterInfo()
|
||||||
EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
?: intent.retrieveSearchInfo()?.toRegisterInfo()
|
||||||
)
|
)
|
||||||
|
|
||||||
// To retrieve attachment
|
// To retrieve attachment
|
||||||
@@ -374,30 +378,30 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
} ?: run {
|
} ?: run {
|
||||||
updateEntry(entrySave.oldEntry, entrySave.newEntry)
|
updateEntry(entrySave.oldEntry, entrySave.newEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't wait for saving if it's to provide autofill
|
|
||||||
mDatabase?.let { database ->
|
|
||||||
EntrySelectionHelper.doSpecialAction(
|
|
||||||
intent = intent,
|
|
||||||
defaultAction = {},
|
|
||||||
searchAction = {},
|
|
||||||
saveAction = {},
|
|
||||||
keyboardSelectionAction = {
|
|
||||||
entryValidatedForKeyboardSelection(database, entrySave.newEntry)
|
|
||||||
},
|
|
||||||
autofillSelectionAction = { _, _ ->
|
|
||||||
entryValidatedForAutofillSelection(database, entrySave.newEntry)
|
|
||||||
},
|
|
||||||
autofillRegistrationAction = {
|
|
||||||
entryValidatedForAutofillRegistration(entrySave.newEntry)
|
|
||||||
},
|
|
||||||
passkeySelectionAction = {
|
|
||||||
entryValidatedForPasskeySelection(database, entrySave.newEntry)
|
|
||||||
},
|
|
||||||
passkeyRegistrationAction = {
|
|
||||||
entryValidatedForPasskeyRegistration(database, entrySave.newEntry)
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
mEntryEditViewModel.uiState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
EntryEditViewModel.UIState.Loading -> {}
|
||||||
|
EntryEditViewModel.UIState.ShowOverwriteMessage -> {
|
||||||
|
if (mEntryEditViewModel.warningOverwriteDataAlreadyApproved.not()) {
|
||||||
|
AlertDialog.Builder(this@EntryEditActivity)
|
||||||
|
.setTitle(R.string.warning_overwrite_data_title)
|
||||||
|
.setMessage(R.string.warning_overwrite_data_description)
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
mEntryEditViewModel.backPressedAlreadyApproved = true
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
mEntryEditViewModel.warningOverwriteDataAlreadyApproved = true
|
||||||
|
}
|
||||||
|
.create().show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,13 +414,13 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
mAllowCustomFields = database?.allowEntryCustomFields() == true
|
mAllowCustomFields = database.allowEntryCustomFields() == true
|
||||||
mAllowOTP = database?.allowOTP == true
|
mAllowOTP = database.allowOTP == true
|
||||||
mEntryEditViewModel.loadDatabase(database)
|
mEntryEditViewModel.loadTemplateEntry(database)
|
||||||
mTemplatesSelectorAdapter?.apply {
|
mTemplatesSelectorAdapter?.apply {
|
||||||
iconDrawableFactory = mDatabase?.iconDrawableFactory
|
iconDrawableFactory = database.iconDrawableFactory
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -427,6 +431,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
super.onDatabaseActionFinished(database, actionTask, result)
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
mEntryEditViewModel.unlockAction()
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_CREATE_ENTRY_TASK,
|
ACTION_DATABASE_CREATE_ENTRY_TASK,
|
||||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||||
@@ -442,23 +447,27 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
searchAction = {
|
searchAction = {
|
||||||
// Nothing when search retrieved
|
// Nothing when search retrieved
|
||||||
},
|
},
|
||||||
saveAction = {
|
selectionAction = { intentSender, typeMode, searchInfo ->
|
||||||
entryValidatedForSave(entry)
|
when(typeMode) {
|
||||||
},
|
TypeMode.DEFAULT -> {}
|
||||||
keyboardSelectionAction = {
|
TypeMode.MAGIKEYBOARD ->
|
||||||
entryValidatedForKeyboardSelection(database, entry)
|
entryValidatedForKeyboardSelection(database, entry)
|
||||||
|
TypeMode.PASSKEY ->
|
||||||
|
entryValidatedForPasskey(database, entry)
|
||||||
|
TypeMode.AUTOFILL ->
|
||||||
|
entryValidatedForAutofill(database, entry)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
autofillSelectionAction = { _, _ ->
|
registrationAction = { _, typeMode, _ ->
|
||||||
entryValidatedForAutofillSelection(database, entry)
|
when(typeMode) {
|
||||||
},
|
TypeMode.DEFAULT ->
|
||||||
autofillRegistrationAction = {
|
entryValidatedForSave(entry)
|
||||||
entryValidatedForAutofillRegistration(entry)
|
TypeMode.MAGIKEYBOARD -> {}
|
||||||
},
|
TypeMode.PASSKEY ->
|
||||||
passkeySelectionAction = {
|
entryValidatedForPasskey(database, entry)
|
||||||
entryValidatedForPasskeySelection(database, entry)
|
TypeMode.AUTOFILL ->
|
||||||
},
|
entryValidatedForAutofill(database, entry)
|
||||||
passkeyRegistrationAction = {
|
}
|
||||||
entryValidatedForPasskeyRegistration(database, entry)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -487,36 +496,18 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
finishForEntryResult(entry)
|
finishForEntryResult(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun entryValidatedForAutofillSelection(database: ContextualDatabase, entry: Entry) {
|
private fun entryValidatedForAutofill(database: ContextualDatabase, entry: Entry) {
|
||||||
// Build Autofill response with the entry selected
|
// Build Autofill response with the entry selected
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity,
|
this.buildSpecialModeResponseAndSetResult(
|
||||||
database,
|
entryInfo = entry.getEntryInfo(database),
|
||||||
entry.getEntryInfo(database))
|
extras = buildEntryResult(entry)
|
||||||
}
|
|
||||||
onValidateSpecialMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun entryValidatedForPasskeySelection(database: ContextualDatabase, entry: Entry) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
||||||
this.buildPasskeyResponseAndSetResult(
|
|
||||||
entryInfo = entry.getEntryInfo(database)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun entryValidatedForAutofillRegistration(entry: Entry) {
|
private fun entryValidatedForPasskey(database: ContextualDatabase, entry: Entry) {
|
||||||
//if (isIntentSender()) {
|
|
||||||
// TODO Autofill Callback #765
|
|
||||||
//}
|
|
||||||
onValidateSpecialMode()
|
|
||||||
if (!isIntentSender()) {
|
|
||||||
finishForEntryResult(entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun entryValidatedForPasskeyRegistration(database: ContextualDatabase, entry: Entry) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
this.buildPasskeyResponseAndSetResult(
|
this.buildPasskeyResponseAndSetResult(
|
||||||
entryInfo = entry.getEntryInfo(database),
|
entryInfo = entry.getEntryInfo(database),
|
||||||
@@ -757,13 +748,13 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onApprovedBackPressed(approved: () -> Unit) {
|
private fun onApprovedBackPressed(approved: () -> Unit) {
|
||||||
if (!backPressedAlreadyApproved) {
|
if (mEntryEditViewModel.backPressedAlreadyApproved.not()) {
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setMessage(R.string.discard_changes)
|
.setMessage(R.string.discard_changes)
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.discard) { _, _ ->
|
.setPositiveButton(R.string.discard) { _, _ ->
|
||||||
mAttachmentFileBinderManager?.stopUploadAllAttachments()
|
mAttachmentFileBinderManager?.stopUploadAllAttachments()
|
||||||
backPressedAlreadyApproved = true
|
mEntryEditViewModel.backPressedAlreadyApproved = true
|
||||||
approved.invoke()
|
approved.invoke()
|
||||||
}.create().show()
|
}.create().show()
|
||||||
} else {
|
} else {
|
||||||
@@ -783,7 +774,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
val bundle = buildEntryResult(entry)
|
val bundle = buildEntryResult(entry)
|
||||||
val intentEntry = Intent()
|
val intentEntry = Intent()
|
||||||
intentEntry.putExtras(bundle)
|
intentEntry.putExtras(bundle)
|
||||||
setResult(Activity.RESULT_OK, intentEntry)
|
setResult(RESULT_OK, intentEntry)
|
||||||
super.finish()
|
super.finish()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Exception when parcelable can't be done
|
// Exception when parcelable can't be done
|
||||||
@@ -791,6 +782,10 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class RegistrationType {
|
||||||
|
UPDATE, CREATE
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val TAG = EntryEditActivity::class.java.name
|
private val TAG = EntryEditActivity::class.java.name
|
||||||
@@ -800,23 +795,12 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
const val KEY_PARENT = "parent"
|
const val KEY_PARENT = "parent"
|
||||||
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
|
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
|
||||||
|
|
||||||
fun registerForEntryResult(fragment: Fragment,
|
fun registerForEntryResult(
|
||||||
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
|
activity: FragmentActivity,
|
||||||
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
): ActivityResultLauncher<Intent> {
|
||||||
entryAddedOrUpdatedListener.invoke(
|
|
||||||
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
entryAddedOrUpdatedListener.invoke(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerForEntryResult(activity: FragmentActivity,
|
|
||||||
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
|
|
||||||
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
if (result.resultCode == RESULT_OK) {
|
||||||
entryAddedOrUpdatedListener.invoke(
|
entryAddedOrUpdatedListener.invoke(
|
||||||
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
|
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
|
||||||
)
|
)
|
||||||
@@ -827,176 +811,72 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch EntryEditActivity to update an existing entry by his [entryId]
|
* Launch EntryEditActivity to update an existing entry or to add a new entry in an existing group
|
||||||
*/
|
*/
|
||||||
fun launchToUpdate(activity: Activity,
|
fun launch(
|
||||||
|
activity: Activity,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
entryId: NodeId<UUID>,
|
registrationType: RegistrationType,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
nodeId: NodeId<*>,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>
|
||||||
|
) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
when (registrationType) {
|
||||||
|
RegistrationType.UPDATE -> intent.putExtra(KEY_ENTRY, nodeId)
|
||||||
|
RegistrationType.CREATE -> intent.putExtra(KEY_PARENT, nodeId)
|
||||||
|
}
|
||||||
activityResultLauncher.launch(intent)
|
activityResultLauncher.launch(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch EntryEditActivity to add a new entry in an existent group
|
* Launch EntryEditActivity to add a new entry in special selection
|
||||||
*/
|
*/
|
||||||
fun launchToCreate(activity: Activity,
|
fun launchForSelection(
|
||||||
|
context: Context,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
|
typeMode: TypeMode,
|
||||||
groupId: NodeId<*>,
|
groupId: NodeId<*>,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
searchInfo: SearchInfo? = null,
|
||||||
if (database.loaded && !database.isReadOnly) {
|
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
) {
|
||||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
|
||||||
activityResultLauncher.launch(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launchToUpdateForSave(context: Context,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
entryId: NodeId<UUID>,
|
|
||||||
searchInfo: SearchInfo) {
|
|
||||||
if (database.loaded && !database.isReadOnly) {
|
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
|
||||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
|
||||||
context,
|
|
||||||
intent,
|
|
||||||
searchInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launchToCreateForSave(context: Context,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
groupId: NodeId<*>,
|
|
||||||
searchInfo: SearchInfo) {
|
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
val intent = Intent(context, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
intent.putExtra(KEY_PARENT, groupId)
|
||||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
EntrySelectionHelper.startActivityForSelectionModeResult(
|
||||||
context,
|
context = context,
|
||||||
intent,
|
intent = intent,
|
||||||
searchInfo
|
typeMode = typeMode,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
activityResultLauncher = activityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch EntryEditActivity to add a new entry in keyboard selection
|
* Launch EntryEditActivity to update an updated entry or register a new entry (from autofill)
|
||||||
*/
|
*/
|
||||||
fun launchForKeyboardSelectionResult(context: Context,
|
fun launchForRegistration(
|
||||||
|
context: Context,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
groupId: NodeId<*>,
|
nodeId: NodeId<*>,
|
||||||
searchInfo: SearchInfo? = null) {
|
|
||||||
if (database.loaded && !database.isReadOnly) {
|
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
|
||||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
|
||||||
context,
|
|
||||||
intent,
|
|
||||||
searchInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch EntryEditActivity to add a new entry in autofill selection
|
|
||||||
*/
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
autofillComponent: AutofillComponent,
|
|
||||||
groupId: NodeId<*>,
|
|
||||||
searchInfo: SearchInfo? = null) {
|
|
||||||
if (database.loaded && !database.isReadOnly) {
|
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
|
||||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
|
||||||
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
|
|
||||||
activity,
|
|
||||||
intent,
|
|
||||||
activityResultLauncher,
|
|
||||||
autofillComponent,
|
|
||||||
searchInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch EntryEditActivity to add a new passkey entry
|
|
||||||
*/
|
|
||||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
||||||
fun launchForPasskeySelectionResult(context: Context,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
groupId: NodeId<*>,
|
|
||||||
searchInfo: SearchInfo? = null) {
|
|
||||||
if (database.loaded && !database.isReadOnly) {
|
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
|
||||||
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
|
||||||
context,
|
|
||||||
intent,
|
|
||||||
activityResultLauncher,
|
|
||||||
searchInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch EntryEditActivity to register an updated entry (from autofill)
|
|
||||||
*/
|
|
||||||
fun launchToUpdateForRegistration(context: Context,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
entryId: NodeId<UUID>,
|
|
||||||
registerInfo: RegisterInfo?,
|
|
||||||
typeMode: TypeMode) {
|
|
||||||
if (database.loaded && !database.isReadOnly) {
|
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
|
||||||
context,
|
|
||||||
activityResultLauncher,
|
|
||||||
intent,
|
|
||||||
registerInfo,
|
|
||||||
typeMode
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch EntryEditActivity to register a new entry (from autofill)
|
|
||||||
*/
|
|
||||||
fun launchToCreateForRegistration(context: Context,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
groupId: NodeId<*>,
|
|
||||||
registerInfo: RegisterInfo? = null,
|
registerInfo: RegisterInfo? = null,
|
||||||
typeMode: TypeMode) {
|
typeMode: TypeMode,
|
||||||
|
registrationType: RegistrationType,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
|
||||||
|
) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
val intent = Intent(context, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
when (registrationType) {
|
||||||
|
RegistrationType.UPDATE -> intent.putExtra(KEY_ENTRY, nodeId)
|
||||||
|
RegistrationType.CREATE -> intent.putExtra(KEY_PARENT, nodeId)
|
||||||
|
}
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
context,
|
context,
|
||||||
activityResultLauncher,
|
activityResultLauncher,
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
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
|
||||||
@@ -33,8 +32,6 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
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.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
@@ -50,10 +47,8 @@ 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.credentialprovider.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||||
@@ -99,9 +94,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
|
||||||
this.buildActivityResultLauncher()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -131,7 +123,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
mExternalFileHelper = ExternalFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
uri?.let {
|
uri?.let {
|
||||||
launchPasswordActivityWithPath(uri)
|
launchMainCredentialActivityWithPath(uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
|
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
|
||||||
@@ -160,7 +152,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
||||||
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
||||||
launchPasswordActivity(
|
launchMainCredentialActivity(
|
||||||
databaseFileUri,
|
databaseFileUri,
|
||||||
fileDatabaseHistoryEntityToOpen.keyFileUri,
|
fileDatabaseHistoryEntityToOpen.keyFileUri,
|
||||||
fileDatabaseHistoryEntityToOpen.hardwareKey
|
fileDatabaseHistoryEntityToOpen.hardwareKey
|
||||||
@@ -179,7 +171,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
// Load default database the first time
|
// Load default database the first time
|
||||||
databaseFilesViewModel.doForDefaultDatabase { databaseFileUri ->
|
databaseFilesViewModel.doForDefaultDatabase { databaseFileUri ->
|
||||||
launchPasswordActivityWithPath(databaseFileUri)
|
launchMainCredentialActivityWithPath(databaseFileUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the database URI provided by file manager after an orientation change
|
// Retrieve the database URI provided by file manager after an orientation change
|
||||||
@@ -224,20 +216,15 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
if (database != null) {
|
|
||||||
launchGroupActivityIfLoaded(database)
|
launchGroupActivityIfLoaded(database)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
super.onDatabaseActionFinished(database, actionTask, result)
|
|
||||||
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
// Update list
|
// Update list
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
@@ -287,17 +274,58 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
|
private fun launchMainCredentialActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
|
||||||
MainCredentialActivity.launch(this,
|
try {
|
||||||
databaseUri,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
keyFile,
|
intent = this.intent,
|
||||||
hardwareKey,
|
defaultAction = {
|
||||||
{ exception ->
|
MainCredentialActivity.launch(
|
||||||
fileNoFoundAction(exception)
|
activity = this,
|
||||||
|
databaseFile = databaseUri,
|
||||||
|
keyFile = keyFile,
|
||||||
|
hardwareKey = hardwareKey
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{ onCancelSpecialMode() },
|
searchAction = { searchInfo ->
|
||||||
{ onLaunchActivitySpecialMode() },
|
MainCredentialActivity.launchForSearchResult(
|
||||||
mCredentialActivityResultLauncher)
|
activity = this,
|
||||||
|
databaseFile = databaseUri,
|
||||||
|
keyFile = keyFile,
|
||||||
|
hardwareKey = hardwareKey,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
selectionAction = { intentSenderMode, typeMode, searchInfo ->
|
||||||
|
MainCredentialActivity.launchForSelection(
|
||||||
|
activity = this,
|
||||||
|
activityResultLauncher = if (intentSenderMode)
|
||||||
|
mCredentialActivityResultLauncher else null,
|
||||||
|
databaseFile = databaseUri,
|
||||||
|
keyFile = keyFile,
|
||||||
|
hardwareKey = hardwareKey,
|
||||||
|
typeMode = typeMode,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
registrationAction = { intentSenderMode, typeMode, registerInfo ->
|
||||||
|
MainCredentialActivity.launchForRegistration(
|
||||||
|
activity = this,
|
||||||
|
activityResultLauncher = if (intentSenderMode)
|
||||||
|
mCredentialActivityResultLauncher else null,
|
||||||
|
databaseFile = databaseUri,
|
||||||
|
keyFile = keyFile,
|
||||||
|
hardwareKey = hardwareKey,
|
||||||
|
typeMode = typeMode,
|
||||||
|
registerInfo = registerInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
fileNoFoundAction(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
||||||
@@ -307,12 +335,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
{ onValidateSpecialMode() },
|
{ onValidateSpecialMode() },
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() },
|
{ onLaunchActivitySpecialMode() },
|
||||||
mCredentialActivityResultLauncher)
|
mCredentialActivityResultLauncher
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
private fun launchMainCredentialActivityWithPath(databaseUri: Uri) {
|
||||||
launchPasswordActivity(databaseUri, null, null)
|
launchMainCredentialActivity(databaseUri, null, null)
|
||||||
// Delete flickering for kitkat <=
|
// Delete flickering for kitkat <=
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||||
@@ -336,10 +365,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabase?.let { database ->
|
|
||||||
launchGroupActivityIfLoaded(database)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show recent files if allowed
|
// Show recent files if allowed
|
||||||
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
|
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
|
||||||
databaseFilesViewModel.loadListOfDatabases()
|
databaseFilesViewModel.loadListOfDatabases()
|
||||||
@@ -358,7 +383,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
try {
|
try {
|
||||||
mDatabaseFileUri?.let { databaseUri ->
|
mDatabaseFileUri?.let { databaseUri ->
|
||||||
// Create the new database
|
// Create the new database
|
||||||
createDatabase(databaseUri, mainCredential)
|
mDatabaseViewModel.createDatabase(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)
|
||||||
@@ -442,71 +467,35 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun launchForSearchResult(context: Context,
|
fun launchForSearchResult(
|
||||||
searchInfo: SearchInfo) {
|
context: Context,
|
||||||
EntrySelectionHelper.startActivityForSearchModeResult(context,
|
searchInfo: SearchInfo
|
||||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
) {
|
||||||
searchInfo)
|
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||||
|
context = context,
|
||||||
|
intent = Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Save Launch
|
* Selection Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun launchForSaveResult(context: Context,
|
fun launchForSelection(
|
||||||
searchInfo: SearchInfo) {
|
context: Context,
|
||||||
EntrySelectionHelper.startActivityForSaveModeResult(context,
|
typeMode: TypeMode,
|
||||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
searchInfo: SearchInfo? = null,
|
||||||
searchInfo)
|
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
|
||||||
}
|
) {
|
||||||
|
EntrySelectionHelper.startActivityForSelectionModeResult(
|
||||||
/*
|
context = context,
|
||||||
* -------------------------
|
intent = Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
* Keyboard Launch
|
searchInfo = searchInfo,
|
||||||
* -------------------------
|
typeMode = typeMode,
|
||||||
*/
|
activityResultLauncher = activityResultLauncher
|
||||||
|
|
||||||
fun launchForKeyboardSelectionResult(activity: Activity,
|
|
||||||
searchInfo: SearchInfo? = null) {
|
|
||||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(activity,
|
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Autofill Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
autofillComponent: AutofillComponent,
|
|
||||||
searchInfo: SearchInfo? = null) {
|
|
||||||
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(activity,
|
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
|
||||||
activityResultLauncher,
|
|
||||||
autofillComponent,
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Passkey Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
||||||
fun launchForPasskeySelectionResult(activity: Activity,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
searchInfo: SearchInfo? = null) {
|
|
||||||
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
|
||||||
activity,
|
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
|
||||||
activityResultLauncher,
|
|
||||||
searchInfo
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,16 +504,18 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
* Registration Launch
|
* Registration Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launchForRegistration(context: Context,
|
fun launchForRegistration(
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
context: Context,
|
||||||
|
typeMode: TypeMode,
|
||||||
registerInfo: RegisterInfo? = null,
|
registerInfo: RegisterInfo? = null,
|
||||||
typeMode: TypeMode) {
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
) {
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
context,
|
context = context,
|
||||||
activityResultLauncher,
|
activityResultLauncher = activityResultLauncher,
|
||||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
intent = Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
registerInfo,
|
registerInfo = registerInfo,
|
||||||
typeMode
|
typeMode = typeMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -174,10 +174,10 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
if (database?.allowCustomIcons == true) {
|
if (database.allowCustomIcons) {
|
||||||
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
|
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
} else {
|
} else {
|
||||||
uploadButton.visibility = View.GONE
|
uploadButton.visibility = View.GONE
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -119,7 +119,6 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
|||||||
resources.displayMetrics.heightPixels * 2
|
resources.displayMetrics.heightPixels * 2
|
||||||
)
|
)
|
||||||
|
|
||||||
database?.let { database ->
|
|
||||||
BinaryDatabaseManager.loadBitmap(
|
BinaryDatabaseManager.loadBitmap(
|
||||||
database,
|
database,
|
||||||
attachment.binaryData,
|
attachment.binaryData,
|
||||||
@@ -132,7 +131,6 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
|||||||
imageView.setImageBitmap(bitmapLoaded)
|
imageView.setImageBitmap(bitmapLoaded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} ?: finish()
|
} ?: finish()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to view the binary", e)
|
Log.e(TAG, "Unable to view the binary", e)
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import android.widget.TextView
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
@@ -56,10 +55,8 @@ import com.kunzisoft.keepass.biometric.DeviceUnlockFragment
|
|||||||
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
||||||
import com.kunzisoft.keepass.biometric.deviceUnlockError
|
import com.kunzisoft.keepass.biometric.deviceUnlockError
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
@@ -128,9 +125,6 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
private var mReadOnly: Boolean = false
|
private var mReadOnly: Boolean = false
|
||||||
private var mForceReadOnly: Boolean = false
|
private var mForceReadOnly: Boolean = false
|
||||||
|
|
||||||
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
|
||||||
this.buildActivityResultLauncher()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -310,15 +304,10 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
mDatabaseFileUri?.let { databaseFileUri ->
|
mDatabaseFileUri?.let { databaseFileUri ->
|
||||||
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabase?.let { database ->
|
|
||||||
launchGroupActivityIfLoaded(database)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
if (database != null) {
|
|
||||||
// Trying to load another database
|
// Trying to load another database
|
||||||
if (mDatabaseFileUri != null
|
if (mDatabaseFileUri != null
|
||||||
&& database.fileUri != null
|
&& database.fileUri != null
|
||||||
@@ -330,7 +319,6 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
}
|
}
|
||||||
launchGroupActivityIfLoaded(database)
|
launchGroupActivityIfLoaded(database)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
@@ -514,10 +502,11 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
val password = intent.getStringExtra(KEY_PASSWORD)
|
val password = intent.getStringExtra(KEY_PASSWORD)
|
||||||
// Consume the intent extra password
|
// Consume the intent extra password
|
||||||
intent.removeExtra(KEY_PASSWORD)
|
intent.removeExtra(KEY_PASSWORD)
|
||||||
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
|
||||||
if (password != null) {
|
if (password != null) {
|
||||||
mainCredentialView?.populatePasswordTextView(password)
|
mainCredentialView?.populatePasswordTextView(password)
|
||||||
}
|
}
|
||||||
|
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
||||||
|
intent.removeExtra(KEY_LAUNCH_IMMEDIATELY)
|
||||||
if (launchImmediately) {
|
if (launchImmediately) {
|
||||||
loadDatabase()
|
loadDatabase()
|
||||||
} else {
|
} else {
|
||||||
@@ -572,10 +561,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
clearCredentialsViews()
|
clearCredentialsViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mReadOnly && (
|
if (mReadOnly && mSpecialMode == SpecialMode.REGISTRATION) {
|
||||||
mSpecialMode == SpecialMode.SAVE
|
|
||||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
|
||||||
) {
|
|
||||||
Log.e(TAG, getString(R.string.error_save_read_only))
|
Log.e(TAG, getString(R.string.error_save_read_only))
|
||||||
Snackbar.make(coordinatorLayout,
|
Snackbar.make(coordinatorLayout,
|
||||||
R.string.error_save_read_only,
|
R.string.error_save_read_only,
|
||||||
@@ -599,7 +585,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||||
fixDuplicateUUID: Boolean) {
|
fixDuplicateUUID: Boolean) {
|
||||||
loadDatabase(
|
mDatabaseViewModel.loadDatabase(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
mainCredential,
|
mainCredential,
|
||||||
readOnly,
|
readOnly,
|
||||||
@@ -752,11 +738,13 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
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 fun buildAndLaunchIntent(activity: Activity,
|
private fun buildAndLaunchIntent(
|
||||||
|
activity: Activity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
hardwareKey: HardwareKey?,
|
hardwareKey: HardwareKey?,
|
||||||
intentBuildLauncher: (Intent) -> Unit) {
|
intentBuildLauncher: (Intent) -> Unit
|
||||||
|
) {
|
||||||
val intent = Intent(activity, MainCredentialActivity::class.java)
|
val intent = Intent(activity, MainCredentialActivity::class.java)
|
||||||
intent.putExtra(KEY_FILENAME, databaseFile)
|
intent.putExtra(KEY_FILENAME, databaseFile)
|
||||||
if (keyFile != null)
|
if (keyFile != null)
|
||||||
@@ -773,10 +761,12 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launch(activity: Activity,
|
fun launch(
|
||||||
|
activity: Activity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
hardwareKey: HardwareKey?) {
|
hardwareKey: HardwareKey?
|
||||||
|
) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
}
|
}
|
||||||
@@ -789,103 +779,45 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launchForSearchResult(activity: Activity,
|
fun launchForSearchResult(
|
||||||
|
activity: Activity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
hardwareKey: HardwareKey?,
|
hardwareKey: HardwareKey?,
|
||||||
searchInfo: SearchInfo) {
|
searchInfo: SearchInfo
|
||||||
|
) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
EntrySelectionHelper.startActivityForSearchModeResult(
|
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||||
activity,
|
context = activity,
|
||||||
intent,
|
intent = intent,
|
||||||
searchInfo)
|
searchInfo = searchInfo
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Save Launch
|
* Selection Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launchForSaveResult(activity: Activity,
|
fun launchForSelection(
|
||||||
|
activity: AppCompatActivity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
hardwareKey: HardwareKey?,
|
hardwareKey: HardwareKey?,
|
||||||
searchInfo: SearchInfo) {
|
typeMode: TypeMode,
|
||||||
|
searchInfo: SearchInfo?,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
|
||||||
|
) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
EntrySelectionHelper.startActivityForSelectionModeResult(
|
||||||
activity,
|
context = activity,
|
||||||
intent,
|
intent = intent,
|
||||||
searchInfo)
|
typeMode = typeMode,
|
||||||
}
|
searchInfo = searchInfo,
|
||||||
}
|
activityResultLauncher = activityResultLauncher
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Keyboard Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun launchForKeyboardResult(activity: Activity,
|
|
||||||
databaseFile: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
hardwareKey: HardwareKey?,
|
|
||||||
searchInfo: SearchInfo?) {
|
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
|
||||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
|
||||||
activity,
|
|
||||||
intent,
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Autofill Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
databaseFile: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
hardwareKey: HardwareKey?,
|
|
||||||
autofillComponent: AutofillComponent,
|
|
||||||
searchInfo: SearchInfo?) {
|
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
|
||||||
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
|
|
||||||
activity,
|
|
||||||
intent,
|
|
||||||
activityResultLauncher,
|
|
||||||
autofillComponent,
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Passkey Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun launchForPasskeyResult(activity: Activity,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
databaseFile: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
hardwareKey: HardwareKey?,
|
|
||||||
searchInfo: SearchInfo?) {
|
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
|
||||||
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
|
||||||
activity,
|
|
||||||
intent,
|
|
||||||
activityResultLauncher,
|
|
||||||
searchInfo
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -895,6 +827,8 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
* Registration Launch
|
* Registration Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
fun launchForRegistration(
|
fun launchForRegistration(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
@@ -914,121 +848,5 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Global Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
fun launch(activity: AppCompatActivity,
|
|
||||||
databaseUri: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
hardwareKey: HardwareKey?,
|
|
||||||
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
|
||||||
onCancelSpecialMode: () -> Unit,
|
|
||||||
onLaunchActivitySpecialMode: () -> Unit,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
EntrySelectionHelper.doSpecialAction(
|
|
||||||
intent = activity.intent,
|
|
||||||
defaultAction = {
|
|
||||||
launch(
|
|
||||||
activity = activity,
|
|
||||||
databaseFile = databaseUri,
|
|
||||||
keyFile = keyFile,
|
|
||||||
hardwareKey = hardwareKey
|
|
||||||
)
|
|
||||||
},
|
|
||||||
searchAction = { searchInfo ->
|
|
||||||
launchForSearchResult(
|
|
||||||
activity = activity,
|
|
||||||
databaseFile = databaseUri,
|
|
||||||
keyFile = keyFile,
|
|
||||||
hardwareKey = hardwareKey,
|
|
||||||
searchInfo = searchInfo
|
|
||||||
)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
},
|
|
||||||
saveAction = { searchInfo ->
|
|
||||||
launchForSaveResult(
|
|
||||||
activity = activity,
|
|
||||||
databaseFile = databaseUri,
|
|
||||||
keyFile = keyFile,
|
|
||||||
hardwareKey = hardwareKey,
|
|
||||||
searchInfo = searchInfo
|
|
||||||
)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
},
|
|
||||||
keyboardSelectionAction = { searchInfo ->
|
|
||||||
launchForKeyboardResult(
|
|
||||||
activity = activity,
|
|
||||||
databaseFile = databaseUri,
|
|
||||||
keyFile = keyFile,
|
|
||||||
hardwareKey = hardwareKey,
|
|
||||||
searchInfo = searchInfo
|
|
||||||
)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
},
|
|
||||||
autofillSelectionAction = { searchInfo, autofillComponent ->
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
launchForAutofillResult(
|
|
||||||
activity = activity,
|
|
||||||
activityResultLauncher = activityResultLauncher,
|
|
||||||
databaseFile = databaseUri,
|
|
||||||
keyFile = keyFile,
|
|
||||||
hardwareKey = hardwareKey,
|
|
||||||
autofillComponent = autofillComponent,
|
|
||||||
searchInfo = searchInfo
|
|
||||||
)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
} else {
|
|
||||||
onCancelSpecialMode()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
autofillRegistrationAction = { registerInfo ->
|
|
||||||
launchForRegistration(
|
|
||||||
activity = activity,
|
|
||||||
activityResultLauncher = activityResultLauncher,
|
|
||||||
databaseFile = databaseUri,
|
|
||||||
keyFile = keyFile,
|
|
||||||
hardwareKey = hardwareKey,
|
|
||||||
typeMode = TypeMode.AUTOFILL,
|
|
||||||
registerInfo = registerInfo
|
|
||||||
)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
},
|
|
||||||
passkeySelectionAction = { searchInfo ->
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
||||||
launchForPasskeyResult(
|
|
||||||
activity = activity,
|
|
||||||
activityResultLauncher = activityResultLauncher,
|
|
||||||
databaseFile = databaseUri,
|
|
||||||
keyFile = keyFile,
|
|
||||||
hardwareKey = hardwareKey,
|
|
||||||
searchInfo = searchInfo
|
|
||||||
)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
} else {
|
|
||||||
onCancelSpecialMode()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
passkeyRegistrationAction = { registerInfo ->
|
|
||||||
launchForRegistration(
|
|
||||||
activity = activity,
|
|
||||||
activityResultLauncher = activityResultLauncher,
|
|
||||||
databaseFile = databaseUri,
|
|
||||||
keyFile = keyFile,
|
|
||||||
hardwareKey = hardwareKey,
|
|
||||||
typeMode = TypeMode.PASSKEY,
|
|
||||||
registerInfo = registerInfo
|
|
||||||
)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
fileNoFoundAction(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
builder.setMessage(stringBuilder)
|
builder.setMessage(stringBuilder)
|
||||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
actionDatabaseListener?.validateDatabaseChanged()
|
actionDatabaseListener?.onDatabaseChangeValidated()
|
||||||
}
|
}
|
||||||
return builder.create()
|
return builder.create()
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ActionDatabaseChangedListener {
|
interface ActionDatabaseChangedListener {
|
||||||
fun validateDatabaseChanged()
|
fun onDatabaseChangeValidated()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -86,7 +86,8 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
|||||||
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
|
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
|
||||||
private const val READ_ONLY_DATABASE = "READ_ONLY_DATABASE"
|
private const val READ_ONLY_DATABASE = "READ_ONLY_DATABASE"
|
||||||
|
|
||||||
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
fun getInstance(
|
||||||
|
oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
readOnly: Boolean
|
readOnly: Boolean
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import android.view.View
|
|||||||
import android.view.WindowManager.LayoutParams.FLAG_SECURE
|
import android.view.WindowManager.LayoutParams.FLAG_SECURE
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||||
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
@@ -12,23 +15,40 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
|||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
||||||
|
|
||||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||||
private var mDatabase: ContextualDatabase? = null
|
private val mDatabase: ContextualDatabase?
|
||||||
|
get() = mDatabaseViewModel.database
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
lifecycleScope.launch {
|
||||||
mDatabaseViewModel.database.observe(this) { database ->
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
this.mDatabase = database
|
mDatabaseViewModel.actionState.collect { uiState ->
|
||||||
resetAppTimeoutOnTouchOrFocus()
|
when (uiState) {
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||||
|
onDatabaseActionFinished(
|
||||||
|
uiState.database,
|
||||||
|
uiState.actionTask,
|
||||||
|
uiState.result
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
mDatabaseViewModel.databaseState.collect { database ->
|
||||||
|
database?.let {
|
||||||
onDatabaseRetrieved(database)
|
onDatabaseRetrieved(database)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
mDatabaseViewModel.actionFinished.observe(this) { result ->
|
}
|
||||||
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +72,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
|||||||
resetAppTimeoutOnTouchOrFocus()
|
resetAppTimeoutOnTouchOrFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
// Can be overridden by a subclass
|
// Can be overridden by a subclass
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,14 +62,14 @@ class GroupDialogFragment : DatabaseDialogFragment() {
|
|||||||
private lateinit var uuidContainerView: ViewGroup
|
private lateinit var uuidContainerView: ViewGroup
|
||||||
private lateinit var uuidReferenceView: TextView
|
private lateinit var uuidReferenceView: TextView
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
mPopulateIconMethod = { imageView, icon ->
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
}
|
}
|
||||||
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
|
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
|
||||||
|
|
||||||
if (database?.allowCustomSearchableGroup() == true) {
|
if (database.allowCustomSearchableGroup()) {
|
||||||
searchableLabelView.visibility = View.VISIBLE
|
searchableLabelView.visibility = View.VISIBLE
|
||||||
searchableView.visibility = View.VISIBLE
|
searchableView.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -112,32 +112,32 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
mPopulateIconMethod = { imageView, icon ->
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
}
|
}
|
||||||
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
||||||
|
|
||||||
searchableContainerView.visibility = if (database?.allowCustomSearchableGroup() == true) {
|
searchableContainerView.visibility = if (database.allowCustomSearchableGroup()) {
|
||||||
View.VISIBLE
|
View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
View.GONE
|
View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
if (database?.allowAutoType() == true) {
|
if (database.allowAutoType()) {
|
||||||
autoTypeContainerView.visibility = View.VISIBLE
|
autoTypeContainerView.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
autoTypeContainerView.visibility = View.GONE
|
autoTypeContainerView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
|
tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
|
||||||
tagsCompletionView.apply {
|
tagsCompletionView.apply {
|
||||||
threshold = 1
|
threshold = 1
|
||||||
setAdapter(tagsAdapter)
|
setAdapter(tagsAdapter)
|
||||||
}
|
}
|
||||||
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
|
tagsContainerView.visibility = if (database.allowTags()) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ class IconEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
|
|
||||||
private var mCustomIcon: IconImageCustom? = null
|
private var mCustomIcon: IconImageCustom? = null
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
mPopulateIconMethod = { imageView, icon ->
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
|
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon)
|
||||||
}
|
}
|
||||||
mCustomIcon?.let { customIcon ->
|
mCustomIcon?.let { customIcon ->
|
||||||
populateViewsWithCustomIcon(customIcon)
|
populateViewsWithCustomIcon(customIcon)
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ 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.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
|
|
||||||
import com.kunzisoft.keepass.password.PasswordEntropy
|
import com.kunzisoft.keepass.password.PasswordEntropy
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
@@ -258,8 +258,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
showEmptyPasswordConfirmationDialog()
|
showEmptyPasswordConfirmationDialog()
|
||||||
} else if (!error
|
} else if (!error
|
||||||
&& hardwareKey != null
|
&& hardwareKey != null
|
||||||
&& !HardwareKeyActivity.isHardwareKeyAvailable(
|
&& !HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)
|
||||||
requireActivity(), hardwareKey, false)
|
|
||||||
) {
|
) {
|
||||||
// show hardware driver dialog if required
|
// show hardware driver dialog if required
|
||||||
error = true
|
error = true
|
||||||
|
|||||||
@@ -4,36 +4,59 @@ import android.os.Bundle
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||||
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
|
abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
|
||||||
|
|
||||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
protected val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||||
protected var mDatabase: ContextualDatabase? = null
|
protected val mDatabase: ContextualDatabase?
|
||||||
|
get() = mDatabaseViewModel.database
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
if (mDatabase == null || mDatabase != database) {
|
super.onCreate(savedInstanceState)
|
||||||
this.mDatabase = database
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
mDatabaseViewModel.actionState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||||
|
onDatabaseActionFinished(
|
||||||
|
uiState.database,
|
||||||
|
uiState.actionTask,
|
||||||
|
uiState.result
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
mDatabaseViewModel.databaseState.collect { database ->
|
||||||
|
database?.let {
|
||||||
onDatabaseRetrieved(database)
|
onDatabaseRetrieved(database)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) { result ->
|
|
||||||
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
|
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
|
||||||
context?.let {
|
context?.let {
|
||||||
view?.resetAppTimeoutWhenViewTouchedOrFocused(it, mDatabase?.loaded)
|
view?.resetAppTimeoutWhenViewTouchedOrFocused(
|
||||||
|
context = it,
|
||||||
|
databaseLoaded = mDatabase?.loaded
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,8 +67,4 @@ abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
|
|||||||
) {
|
) {
|
||||||
// Can be overridden by a subclass
|
// Can be overridden by a subclass
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun buildNewBinaryAttachment(): BinaryData? {
|
|
||||||
return mDatabase?.buildNewBinaryAttachment()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -230,7 +230,7 @@ class EntryEditFragment: DatabaseFragment() {
|
|||||||
val attachmentToUploadUri = it.attachmentToUploadUri
|
val attachmentToUploadUri = it.attachmentToUploadUri
|
||||||
val fileName = it.fileName
|
val fileName = it.fileName
|
||||||
|
|
||||||
buildNewBinaryAttachment()?.let { binaryAttachment ->
|
mDatabaseViewModel.buildNewAttachment()?.let { binaryAttachment ->
|
||||||
val entryAttachment = Attachment(fileName, binaryAttachment)
|
val entryAttachment = Attachment(fileName, binaryAttachment)
|
||||||
// Ask to replace the current attachment
|
// Ask to replace the current attachment
|
||||||
if ((!mAllowMultipleAttachments
|
if ((!mAllowMultipleAttachments
|
||||||
@@ -273,13 +273,13 @@ class EntryEditFragment: DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
|
|
||||||
templateView.populateIconMethod = { imageView, icon ->
|
templateView.populateIconMethod = { imageView, icon ->
|
||||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
mAllowMultipleAttachments = database?.allowMultipleAttachments == true
|
mAllowMultipleAttachments = database.allowMultipleAttachments == true
|
||||||
|
|
||||||
attachmentsAdapter?.database = database
|
attachmentsAdapter?.database = database
|
||||||
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
|
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
|
||||||
@@ -290,12 +290,12 @@ class EntryEditFragment: DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
|
tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
|
||||||
tagsCompletionView.apply {
|
tagsCompletionView.apply {
|
||||||
threshold = 1
|
threshold = 1
|
||||||
setAdapter(tagsAdapter)
|
setAdapter(tagsAdapter)
|
||||||
}
|
}
|
||||||
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
|
tagsContainerView.visibility = if (database.allowTags()) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
|
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
|
||||||
attachmentsAdapter?.database = database
|
attachmentsAdapter?.database = database
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.adapters.NodesAdapter
|
import com.kunzisoft.keepass.adapters.NodesAdapter
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
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
|
||||||
@@ -154,9 +154,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
super.onDetach()
|
super.onDetach()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
database?.let { database ->
|
|
||||||
mAdapter = NodesAdapter(context, database).apply {
|
mAdapter = NodesAdapter(context, database).apply {
|
||||||
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
|
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
|
||||||
override fun onNodeClick(database: ContextualDatabase, node: Node) {
|
override fun onNodeClick(database: ContextualDatabase, node: Node) {
|
||||||
@@ -195,7 +194,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
mNodesRecyclerView?.adapter = mAdapter
|
mNodesRecyclerView?.adapter = mAdapter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
@@ -248,7 +246,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
|
|
||||||
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
|
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
|
||||||
activity?.intent?.let {
|
activity?.intent?.let {
|
||||||
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
|
specialMode = it.retrieveSpecialMode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,9 +297,9 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun containsRecycleBin(nodes: List<Node>): Boolean {
|
private fun containsRecycleBin(database: ContextualDatabase?, nodes: List<Node>): Boolean {
|
||||||
return mDatabase?.isRecycleBinEnabled == true
|
return database?.isRecycleBinEnabled == true
|
||||||
&& nodes.any { it == mDatabase?.recycleBin }
|
&& nodes.any { it == database.recycleBin }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun actionNodesCallback(database: ContextualDatabase,
|
fun actionNodesCallback(database: ContextualDatabase,
|
||||||
@@ -328,7 +326,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
// 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 || containsRecycleBin(nodes)) {
|
if (database.isReadOnly || containsRecycleBin(database, nodes)) {
|
||||||
menu?.removeItem(R.id.menu_edit)
|
menu?.removeItem(R.id.menu_edit)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -348,7 +346,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Deletion
|
// Deletion
|
||||||
if (database.isReadOnly || containsRecycleBin(nodes)) {
|
if (database.isReadOnly || containsRecycleBin(database, nodes)) {
|
||||||
menu?.removeItem(R.id.menu_delete)
|
menu?.removeItem(R.id.menu_delete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
|
|||||||
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory
|
iconPickerAdapter.iconDrawableFactory = database.iconDrawableFactory
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val populateList = launch {
|
val populateList = launch {
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ class IconPickerFragment : DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
|
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
|
||||||
if (database?.allowCustomIcons == true) 2 else 1)
|
if (database.allowCustomIcons) 2 else 1)
|
||||||
viewPager.adapter = iconPickerPagerAdapter
|
viewPager.adapter = iconPickerPagerAdapter
|
||||||
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
||||||
tab.text = when (position) {
|
tab.text = when (position) {
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class KeyGeneratorFragment : DatabaseFragment() {
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
// Nothing here
|
// Nothing here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
// Nothing here
|
// Nothing here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -293,20 +293,22 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
|||||||
private fun generatePassword() {
|
private fun generatePassword() {
|
||||||
var password = ""
|
var password = ""
|
||||||
try {
|
try {
|
||||||
password = PasswordGenerator(resources).generatePassword(getPasswordLength(),
|
password = PasswordGenerator(resources).generatePassword(
|
||||||
uppercaseCompound.isChecked,
|
length = getPasswordLength(),
|
||||||
lowercaseCompound.isChecked,
|
upperCase = uppercaseCompound.isChecked,
|
||||||
digitsCompound.isChecked,
|
lowerCase = lowercaseCompound.isChecked,
|
||||||
minusCompound.isChecked,
|
digits = digitsCompound.isChecked,
|
||||||
underlineCompound.isChecked,
|
minus = minusCompound.isChecked,
|
||||||
spaceCompound.isChecked,
|
underline = underlineCompound.isChecked,
|
||||||
specialsCompound.isChecked,
|
space = spaceCompound.isChecked,
|
||||||
bracketsCompound.isChecked,
|
specials = specialsCompound.isChecked,
|
||||||
extendedCompound.isChecked,
|
brackets = bracketsCompound.isChecked,
|
||||||
getConsiderChars(),
|
extended = extendedCompound.isChecked,
|
||||||
getIgnoreChars(),
|
considerChars = getConsiderChars(),
|
||||||
atLeastOneCompound.isChecked,
|
ignoreChars = getIgnoreChars(),
|
||||||
excludeAmbiguousCompound.isChecked)
|
atLeastOneFromEach = atLeastOneCompound.isChecked,
|
||||||
|
excludeAmbiguousChar = excludeAmbiguousCompound.isChecked
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to generate a password", e)
|
Log.e(TAG, "Unable to generate a password", e)
|
||||||
}
|
}
|
||||||
@@ -318,7 +320,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
// Nothing here
|
// Nothing here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,104 +1,240 @@
|
|||||||
package com.kunzisoft.keepass.activities.legacy
|
package com.kunzisoft.keepass.activities.legacy
|
||||||
|
|
||||||
import android.net.Uri
|
import android.Manifest
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider.Companion.startDatabaseService
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.ProgressMessage
|
||||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.getBinaryDir
|
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
||||||
|
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
|
abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
|
||||||
|
|
||||||
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
|
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
|
||||||
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
protected val mDatabase: ContextualDatabase?
|
||||||
protected var mDatabase: ContextualDatabase? = null
|
get() = mDatabaseViewModel.database
|
||||||
|
|
||||||
|
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
|
||||||
|
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
|
||||||
|
|
||||||
|
private val mActionDatabaseListener =
|
||||||
|
object : DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
|
||||||
|
override fun onDatabaseChangeValidated() {
|
||||||
|
mDatabaseViewModel.onDatabaseChangeValidated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val tempServiceParameters = mutableListOf<Pair<Bundle?, String>>()
|
||||||
|
private val requestPermissionLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { _ ->
|
||||||
|
// Whether or not the user has accepted, the service can be started,
|
||||||
|
// There just won't be any notification if it's not allowed.
|
||||||
|
tempServiceParameters.removeFirstOrNull()?.let {
|
||||||
|
startDatabaseService(it.first, it.second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
lifecycleScope.launch {
|
||||||
mDatabaseTaskProvider = DatabaseTaskProvider(this, showDatabaseDialog())
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
mDatabaseViewModel.actionState.collect { uiState ->
|
||||||
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
when (uiState) {
|
||||||
val databaseWasReloaded = database?.wasReloaded == true
|
is DatabaseViewModel.ActionState.Loading -> {}
|
||||||
if (databaseWasReloaded && finishActivityIfReloadRequested()) {
|
is DatabaseViewModel.ActionState.OnDatabaseReloaded -> {
|
||||||
|
if (finishActivityIfReloadRequested()) {
|
||||||
finish()
|
finish()
|
||||||
} else if (mDatabase == null || mDatabase != database || databaseWasReloaded) {
|
}
|
||||||
database?.wasReloaded = false
|
}
|
||||||
|
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseInfoChanged -> {
|
||||||
|
if (manageDatabaseInfo()) {
|
||||||
|
showDatabaseChangedDialog(
|
||||||
|
uiState.previousDatabaseInfo,
|
||||||
|
uiState.newDatabaseInfo,
|
||||||
|
uiState.readOnlyDatabase
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionRequested -> {
|
||||||
|
startDatabasePermissionService(
|
||||||
|
uiState.bundle,
|
||||||
|
uiState.actionTask
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionStarted -> {
|
||||||
|
if (showDatabaseDialog())
|
||||||
|
startDialog(uiState.progressMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionUpdated -> {
|
||||||
|
if (showDatabaseDialog())
|
||||||
|
updateDialog(uiState.progressMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionStopped -> {
|
||||||
|
// Remove the progress task
|
||||||
|
stopDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||||
|
onDatabaseActionFinished(
|
||||||
|
uiState.database,
|
||||||
|
uiState.actionTask,
|
||||||
|
uiState.result
|
||||||
|
)
|
||||||
|
stopDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
mDatabaseViewModel.databaseState.collect { database ->
|
||||||
|
// Nullable function
|
||||||
|
onUnknownDatabaseRetrieved(database)
|
||||||
|
database?.let {
|
||||||
onDatabaseRetrieved(database)
|
onDatabaseRetrieved(database)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mDatabaseTaskProvider?.onActionFinish = { database, actionTask, result ->
|
}
|
||||||
onDatabaseActionFinished(database, actionTask, result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun showDatabaseDialog(): Boolean {
|
/**
|
||||||
return true
|
* Nullable function to retrieve a database
|
||||||
}
|
*/
|
||||||
|
open fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
mDatabaseTaskProvider?.destroy()
|
|
||||||
mDatabaseTaskProvider = null
|
|
||||||
mDatabase = null
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
|
||||||
mDatabase = database
|
|
||||||
mDatabaseViewModel.defineDatabase(database)
|
|
||||||
// optional method implementation
|
// optional method implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
|
// optional method implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun manageDatabaseInfo(): Boolean = true
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
mDatabaseViewModel.onActionFinished(database, actionTask, result)
|
|
||||||
// optional method implementation
|
// optional method implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createDatabase(
|
private fun startDatabasePermissionService(bundle: Bundle?, actionTask: String) {
|
||||||
databaseUri: Uri,
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
mainCredential: MainCredential
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
== PackageManager.PERMISSION_GRANTED
|
||||||
) {
|
) {
|
||||||
mDatabaseTaskProvider?.startDatabaseCreate(databaseUri, mainCredential)
|
startDatabaseService(bundle, actionTask)
|
||||||
|
} else if (ActivityCompat.shouldShowRequestPermissionRationale(
|
||||||
|
this,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// it's not the first time, so the user deliberately chooses not to display the notification
|
||||||
|
startDatabaseService(bundle, actionTask)
|
||||||
|
} else {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setMessage(R.string.warning_database_notification_permission)
|
||||||
|
.setNegativeButton(R.string.later) { _, _ ->
|
||||||
|
// Refuses the notification, so start the service
|
||||||
|
startDatabaseService(bundle, actionTask)
|
||||||
|
}
|
||||||
|
.setPositiveButton(R.string.ask) { _, _ ->
|
||||||
|
// Save the temp parameters to ask the permission
|
||||||
|
tempServiceParameters.add(Pair(bundle, actionTask))
|
||||||
|
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
}.create().show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startDatabaseService(bundle, actionTask)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadDatabase(
|
private fun showDatabaseChangedDialog(
|
||||||
databaseUri: Uri,
|
previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
mainCredential: MainCredential,
|
newDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
readOnly: Boolean,
|
readOnlyDatabase: Boolean
|
||||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
|
||||||
fixDuplicateUuid: Boolean
|
|
||||||
) {
|
) {
|
||||||
mDatabaseTaskProvider?.startDatabaseLoad(
|
lifecycleScope.launch {
|
||||||
databaseUri,
|
if (databaseChangedDialogFragment == null) {
|
||||||
mainCredential,
|
databaseChangedDialogFragment = supportFragmentManager
|
||||||
readOnly,
|
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
|
||||||
cipherEncryptDatabase,
|
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||||
fixDuplicateUuid
|
mActionDatabaseListener
|
||||||
|
}
|
||||||
|
if (progressTaskDialogFragment == null) {
|
||||||
|
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
|
||||||
|
previousDatabaseInfo,
|
||||||
|
newDatabaseInfo,
|
||||||
|
readOnlyDatabase
|
||||||
|
)
|
||||||
|
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||||
|
mActionDatabaseListener
|
||||||
|
databaseChangedDialogFragment?.show(
|
||||||
|
supportFragmentManager,
|
||||||
|
DATABASE_CHANGED_DIALOG_TAG
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
protected fun closeDatabase() {
|
|
||||||
mDatabase?.clearAndClose(this.getBinaryDir())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
private fun startDialog(progressMessage: ProgressMessage) {
|
||||||
super.onResume()
|
lifecycleScope.launch {
|
||||||
mDatabaseTaskProvider?.registerProgressTask()
|
if (progressTaskDialogFragment == null) {
|
||||||
|
progressTaskDialogFragment = supportFragmentManager
|
||||||
|
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
|
||||||
|
}
|
||||||
|
if (progressTaskDialogFragment == null) {
|
||||||
|
progressTaskDialogFragment = ProgressTaskDialogFragment()
|
||||||
|
progressTaskDialogFragment?.show(
|
||||||
|
supportFragmentManager,
|
||||||
|
PROGRESS_TASK_DIALOG_TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
updateDialog(progressMessage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
private fun updateDialog(progressMessage: ProgressMessage) {
|
||||||
mDatabaseTaskProvider?.unregisterProgressTask()
|
progressTaskDialogFragment?.apply {
|
||||||
super.onPause()
|
updateTitle(progressMessage.titleId)
|
||||||
|
updateMessage(progressMessage.messageId)
|
||||||
|
updateWarning(progressMessage.warningId)
|
||||||
|
setCancellable(progressMessage.cancelable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopDialog() {
|
||||||
|
progressTaskDialogFragment?.dismissAllowingStateLoss()
|
||||||
|
progressTaskDialogFragment = null
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun showDatabaseDialog(): Boolean {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ import androidx.appcompat.app.AlertDialog
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
|
||||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
@@ -87,106 +87,23 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
deleteDatabaseNodes(nodes)
|
deleteDatabaseNodes(nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabaseViewModel.saveDatabase.observe(this) { save ->
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSave(save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.mergeDatabase.observe(this) { save ->
|
|
||||||
mDatabaseTaskProvider?.startDatabaseMerge(save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
|
|
||||||
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
|
|
||||||
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
|
mExitLock = false
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun finishActivityIfDatabaseNotLoaded(): Boolean {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
|
|
||||||
// End activity if database not loaded
|
// End activity if database not loaded
|
||||||
if (finishActivityIfDatabaseNotLoaded() && (database == null || !database.loaded)) {
|
if (database.loaded.not())
|
||||||
finish()
|
finish()
|
||||||
}
|
|
||||||
|
|
||||||
// Focus view to reinitialize timeout,
|
// Focus view to reinitialize timeout,
|
||||||
// view is not necessary loaded so retry later in resume
|
// view is not necessary loaded so retry later in resume
|
||||||
viewToInvalidateTimeout()
|
viewToInvalidateTimeout()
|
||||||
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database?.loaded)
|
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database.loaded)
|
||||||
|
|
||||||
database?.let {
|
|
||||||
// check timeout
|
// check timeout
|
||||||
if (mTimeoutEnable) {
|
if (mTimeoutEnable) {
|
||||||
if (mLockReceiver == null) {
|
if (mLockReceiver == null) {
|
||||||
mLockReceiver = LockReceiver {
|
mLockReceiver = LockReceiver {
|
||||||
mDatabase = null
|
|
||||||
closeDatabase(database)
|
closeDatabase(database)
|
||||||
mExitLock = true
|
mExitLock = true
|
||||||
closeOptionsMenu()
|
closeOptionsMenu()
|
||||||
@@ -209,7 +126,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
checkRegister()
|
checkRegister()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
// To fix weird crash
|
// To fix weird crash
|
||||||
@@ -227,7 +143,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
super.onDatabaseActionFinished(database, actionTask, result)
|
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
|
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
|
||||||
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
|
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
|
||||||
@@ -249,24 +164,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
databaseUri: Uri?,
|
databaseUri: Uri?,
|
||||||
mainCredential: MainCredential
|
mainCredential: MainCredential
|
||||||
) {
|
) {
|
||||||
assignDatabasePassword(databaseUri, mainCredential)
|
mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assignDatabasePassword(
|
fun assignMainCredential(mainCredential: MainCredential) {
|
||||||
databaseUri: Uri?,
|
|
||||||
mainCredential: MainCredential
|
|
||||||
) {
|
|
||||||
if (databaseUri != null) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseAssignCredential(databaseUri, mainCredential)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun assignPassword(mainCredential: MainCredential) {
|
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
database.fileUri?.let { databaseUri ->
|
database.fileUri?.let { databaseUri ->
|
||||||
// Show the progress dialog now or after dialog confirmation
|
// Show the progress dialog now or after dialog confirmation
|
||||||
if (database.isValidCredential(mainCredential.toMasterCredential(contentResolver))) {
|
if (database.isValidCredential(mainCredential.toMasterCredential(contentResolver))) {
|
||||||
assignDatabasePassword(databaseUri, mainCredential)
|
mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
|
||||||
} else {
|
} else {
|
||||||
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
|
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
|
||||||
.show(supportFragmentManager, "passwordEncodingTag")
|
.show(supportFragmentManager, "passwordEncodingTag")
|
||||||
@@ -276,45 +182,51 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun saveDatabase() {
|
fun saveDatabase() {
|
||||||
mDatabaseTaskProvider?.startDatabaseSave(true)
|
mDatabaseViewModel.saveDatabase(save = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveDatabaseTo(uri: Uri) {
|
fun saveDatabaseTo(uri: Uri) {
|
||||||
mDatabaseTaskProvider?.startDatabaseSave(true, uri)
|
mDatabaseViewModel.saveDatabase(save = true, saveToUri = uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mergeDatabase() {
|
fun mergeDatabase() {
|
||||||
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable)
|
mDatabaseViewModel.mergeDatabase(save = mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
|
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
|
||||||
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable, uri, mainCredential)
|
mDatabaseViewModel.mergeDatabase(mAutoSaveEnable, uri, mainCredential)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reloadDatabase() {
|
fun reloadDatabase() {
|
||||||
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
|
mDatabaseViewModel.reloadDatabase(fixDuplicateUuid = false)
|
||||||
mDatabaseTaskProvider?.startDatabaseReload(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createEntry(newEntry: Entry,
|
fun createEntry(
|
||||||
parent: Group) {
|
newEntry: Entry,
|
||||||
mDatabaseTaskProvider?.startDatabaseCreateEntry(newEntry, parent, mAutoSaveEnable)
|
parent: Group
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.createEntry(newEntry, parent, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateEntry(oldEntry: Entry,
|
fun updateEntry(
|
||||||
entryToUpdate: Entry) {
|
oldEntry: Entry,
|
||||||
mDatabaseTaskProvider?.startDatabaseUpdateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
|
entryToUpdate: Entry
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.updateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun copyNodes(nodesToCopy: List<Node>,
|
fun copyNodes(
|
||||||
newParent: Group) {
|
nodesToCopy: List<Node>,
|
||||||
mDatabaseTaskProvider?.startDatabaseCopyNodes(nodesToCopy, newParent, mAutoSaveEnable)
|
newParent: Group
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.copyNodes(nodesToCopy, newParent, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun moveNodes(nodesToMove: List<Node>,
|
fun moveNodes(
|
||||||
newParent: Group) {
|
nodesToMove: List<Node>,
|
||||||
mDatabaseTaskProvider?.startDatabaseMoveNodes(nodesToMove, newParent, mAutoSaveEnable)
|
newParent: Group
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.moveNodes(nodesToMove, newParent, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun eachNodeRecyclable(database: ContextualDatabase, nodes: List<Node>): Boolean {
|
private fun eachNodeRecyclable(database: ContextualDatabase, nodes: List<Node>): Boolean {
|
||||||
@@ -330,6 +242,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false) {
|
fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false) {
|
||||||
|
// TODO Move in ViewModel
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
// If recycle bin enabled, ensure it exists
|
// If recycle bin enabled, ensure it exists
|
||||||
if (database.isRecycleBinEnabled) {
|
if (database.isRecycleBinEnabled) {
|
||||||
@@ -350,11 +263,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteDatabaseNodes(nodes: List<Node>) {
|
private fun deleteDatabaseNodes(nodes: List<Node>) {
|
||||||
mDatabaseTaskProvider?.startDatabaseDeleteNodes(nodes, mAutoSaveEnable)
|
mDatabaseViewModel.deleteNodes(nodes, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createGroup(parent: Group,
|
fun createGroup(
|
||||||
groupInfo: GroupInfo?) {
|
parent: Group,
|
||||||
|
groupInfo: GroupInfo?
|
||||||
|
) {
|
||||||
|
// TODO Move in ViewModel
|
||||||
// Build the group
|
// Build the group
|
||||||
mDatabase?.createGroup()?.let { newGroup ->
|
mDatabase?.createGroup()?.let { newGroup ->
|
||||||
groupInfo?.let { info ->
|
groupInfo?.let { info ->
|
||||||
@@ -362,12 +278,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
// Not really needed here because added in runnable but safe
|
// Not really needed here because added in runnable but safe
|
||||||
newGroup.parent = parent
|
newGroup.parent = parent
|
||||||
mDatabaseTaskProvider?.startDatabaseCreateGroup(newGroup, parent, mAutoSaveEnable)
|
mDatabaseViewModel.createGroup(newGroup, parent, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateGroup(oldGroup: Group,
|
fun updateGroup(
|
||||||
groupInfo: GroupInfo) {
|
oldGroup: Group,
|
||||||
|
groupInfo: GroupInfo
|
||||||
|
) {
|
||||||
|
// TODO Move in ViewModel
|
||||||
// If group updated save it in the database
|
// If group updated save it in the database
|
||||||
val updateGroup = Group(oldGroup).let { updateGroup ->
|
val updateGroup = Group(oldGroup).let { updateGroup ->
|
||||||
updateGroup.apply {
|
updateGroup.apply {
|
||||||
@@ -377,27 +296,28 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
this.setGroupInfo(groupInfo)
|
this.setGroupInfo(groupInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mDatabaseTaskProvider?.startDatabaseUpdateGroup(oldGroup, updateGroup, mAutoSaveEnable)
|
mDatabaseViewModel.updateGroup(oldGroup, updateGroup, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restoreEntryHistory(mainEntryId: NodeId<UUID>,
|
fun restoreEntryHistory(
|
||||||
entryHistoryPosition: Int) {
|
mainEntryId: NodeId<UUID>,
|
||||||
mDatabaseTaskProvider
|
entryHistoryPosition: Int
|
||||||
?.startDatabaseRestoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
) {
|
||||||
|
mDatabaseViewModel.restoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteEntryHistory(mainEntryId: NodeId<UUID>,
|
fun deleteEntryHistory(
|
||||||
entryHistoryPosition: Int) {
|
mainEntryId: NodeId<UUID>,
|
||||||
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
entryHistoryPosition: Int
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.deleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkRegister() {
|
private fun checkRegister() {
|
||||||
// If in ave or registration mode, don't allow read only
|
// If in registration mode, don't allow read only
|
||||||
if ((mSpecialMode == SpecialMode.SAVE
|
if (mSpecialMode == SpecialMode.REGISTRATION && mDatabaseReadOnly) {
|
||||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
|
||||||
&& mDatabaseReadOnly) {
|
|
||||||
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
|
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
|
||||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
intent.removeModes()
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
package com.kunzisoft.keepass.activities.legacy
|
package com.kunzisoft.keepass.activities.legacy
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveTypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
@@ -21,10 +30,25 @@ import com.kunzisoft.keepass.view.ToolbarSpecial
|
|||||||
abstract class DatabaseModeActivity : DatabaseActivity() {
|
abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||||
|
|
||||||
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
|
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
|
||||||
private var mTypeMode: TypeMode = TypeMode.DEFAULT
|
protected var mTypeMode: TypeMode = TypeMode.DEFAULT
|
||||||
|
|
||||||
private var mToolbarSpecial: ToolbarSpecial? = null
|
private var mToolbarSpecial: ToolbarSpecial? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility activity result launcher,
|
||||||
|
* Used recursively, close each activity with return data
|
||||||
|
*/
|
||||||
|
protected open var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) {
|
||||||
|
setActivityResult(
|
||||||
|
lockDatabase = false,
|
||||||
|
resultCode = it.resultCode,
|
||||||
|
data = it.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
open fun onDatabaseBackPressed() {
|
open fun onDatabaseBackPressed() {
|
||||||
if (mSpecialMode != SpecialMode.DEFAULT)
|
if (mSpecialMode != SpecialMode.DEFAULT)
|
||||||
onCancelSpecialMode()
|
onCancelSpecialMode()
|
||||||
@@ -50,8 +74,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
|
|
||||||
fun onLaunchActivitySpecialMode() {
|
fun onLaunchActivitySpecialMode() {
|
||||||
if (!isIntentSender()) {
|
if (!isIntentSender()) {
|
||||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
intent.removeModes()
|
||||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
intent.removeInfo()
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,8 +84,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
if (isIntentSender()) {
|
if (isIntentSender()) {
|
||||||
super.finish()
|
super.finish()
|
||||||
} else {
|
} else {
|
||||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
intent.removeModes()
|
||||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
intent.removeInfo()
|
||||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||||
backToTheMainAppAndFinish()
|
backToTheMainAppAndFinish()
|
||||||
}
|
}
|
||||||
@@ -73,8 +97,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
// To get the app caller, only for IntentSender
|
// To get the app caller, only for IntentSender
|
||||||
onRegularBackPressed()
|
onRegularBackPressed()
|
||||||
} else {
|
} else {
|
||||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
intent.removeModes()
|
||||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
intent.removeInfo()
|
||||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||||
backToTheMainAppAndFinish()
|
backToTheMainAppAndFinish()
|
||||||
}
|
}
|
||||||
@@ -105,18 +129,18 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
mSpecialMode = intent.retrieveSpecialMode()
|
||||||
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
mTypeMode = intent.retrieveTypeMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
mSpecialMode = intent.retrieveSpecialMode()
|
||||||
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
mTypeMode = intent.retrieveTypeMode()
|
||||||
val registerInfo: RegisterInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)
|
val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
|
||||||
val searchInfo: SearchInfo? = registerInfo?.searchInfo
|
val searchInfo: SearchInfo? = registerInfo?.searchInfo
|
||||||
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
?: intent.retrieveSearchInfo()
|
||||||
|
|
||||||
// To show the selection mode
|
// To show the selection mode
|
||||||
mToolbarSpecial = findViewById(R.id.special_mode_view)
|
mToolbarSpecial = findViewById(R.id.special_mode_view)
|
||||||
@@ -125,9 +149,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
val selectionModeStringId = when (mSpecialMode) {
|
val selectionModeStringId = when (mSpecialMode) {
|
||||||
SpecialMode.DEFAULT, // Not important because hidden
|
SpecialMode.DEFAULT, // Not important because hidden
|
||||||
SpecialMode.SEARCH -> R.string.search_mode
|
SpecialMode.SEARCH -> R.string.search_mode
|
||||||
SpecialMode.SAVE -> R.string.save_mode
|
|
||||||
SpecialMode.SELECTION -> R.string.selection_mode
|
SpecialMode.SELECTION -> R.string.selection_mode
|
||||||
SpecialMode.REGISTRATION -> R.string.registration_mode
|
SpecialMode.REGISTRATION -> R.string.save_mode // Save is registration mode
|
||||||
}
|
}
|
||||||
val typeModeStringId = when (mTypeMode) {
|
val typeModeStringId = when (mTypeMode) {
|
||||||
TypeMode.DEFAULT, // Not important because hidden
|
TypeMode.DEFAULT, // Not important because hidden
|
||||||
@@ -145,7 +168,6 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
visible = when (mSpecialMode) {
|
visible = when (mSpecialMode) {
|
||||||
SpecialMode.DEFAULT -> false
|
SpecialMode.DEFAULT -> false
|
||||||
SpecialMode.SEARCH -> true
|
SpecialMode.SEARCH -> true
|
||||||
SpecialMode.SAVE -> true
|
|
||||||
SpecialMode.SELECTION -> true
|
SpecialMode.SELECTION -> true
|
||||||
SpecialMode.REGISTRATION -> true
|
SpecialMode.REGISTRATION -> true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import com.kunzisoft.keepass.database.ContextualDatabase
|
|||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
|
||||||
interface DatabaseRetrieval {
|
interface DatabaseRetrieval {
|
||||||
fun onDatabaseRetrieved(database: ContextualDatabase?)
|
fun onDatabaseRetrieved(database: ContextualDatabase)
|
||||||
fun onDatabaseActionFinished(database: ContextualDatabase,
|
|
||||||
|
fun onDatabaseActionFinished(
|
||||||
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result)
|
result: ActionRunnable.Result
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -24,26 +24,26 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.ParcelUuid
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
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.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutofillComponent
|
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
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.utils.LOCK_ACTION
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
import com.kunzisoft.keepass.utils.getEnumExtra
|
import com.kunzisoft.keepass.utils.getEnumExtra
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableList
|
||||||
import com.kunzisoft.keepass.utils.putEnumExtra
|
import com.kunzisoft.keepass.utils.putEnumExtra
|
||||||
|
import com.kunzisoft.keepass.utils.putParcelableList
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
object EntrySelectionHelper {
|
object EntrySelectionHelper {
|
||||||
|
|
||||||
@@ -51,6 +51,8 @@ object EntrySelectionHelper {
|
|||||||
private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE"
|
private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE"
|
||||||
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
|
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
|
||||||
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
|
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
|
||||||
|
private const val EXTRA_NODES_IDS = "com.kunzisoft.keepass.extra.NODES_IDS"
|
||||||
|
private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.NODE_ID"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finish the activity by passing the result code and by locking the database if necessary
|
* Finish the activity by passing the result code and by locking the database if necessary
|
||||||
@@ -58,7 +60,7 @@ object EntrySelectionHelper {
|
|||||||
fun Activity.setActivityResult(
|
fun Activity.setActivityResult(
|
||||||
lockDatabase: Boolean = false,
|
lockDatabase: Boolean = false,
|
||||||
resultCode: Int,
|
resultCode: Int,
|
||||||
data: Intent? = null,
|
data: Intent? = null
|
||||||
) {
|
) {
|
||||||
when (resultCode) {
|
when (resultCode) {
|
||||||
Activity.RESULT_OK ->
|
Activity.RESULT_OK ->
|
||||||
@@ -68,170 +70,167 @@ object EntrySelectionHelper {
|
|||||||
}
|
}
|
||||||
this.finish()
|
this.finish()
|
||||||
|
|
||||||
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
|
if (lockDatabase) {
|
||||||
// Close the database
|
// Close the database
|
||||||
this.sendBroadcast(Intent(LOCK_ACTION))
|
this.sendBroadcast(Intent(LOCK_ACTION))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun startActivityForSearchModeResult(
|
||||||
* Utility method to build a registerForActivityResult,
|
|
||||||
* Used recursively, close each activity with return data
|
|
||||||
*/
|
|
||||||
fun AppCompatActivity.buildActivityResultLauncher(
|
|
||||||
lockDatabase: Boolean = false,
|
|
||||||
dataTransformation: (data: Intent?) -> Intent? = { it },
|
|
||||||
): ActivityResultLauncher<Intent> {
|
|
||||||
return this.registerForActivityResult(
|
|
||||||
ActivityResultContracts.StartActivityForResult()
|
|
||||||
) {
|
|
||||||
setActivityResult(
|
|
||||||
lockDatabase,
|
|
||||||
it.resultCode,
|
|
||||||
dataTransformation(it.data)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startActivityForSearchModeResult(context: Context,
|
|
||||||
intent: Intent,
|
|
||||||
searchInfo: SearchInfo) {
|
|
||||||
addSpecialModeInIntent(intent, SpecialMode.SEARCH)
|
|
||||||
addSearchInfoInIntent(intent, searchInfo)
|
|
||||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startActivityForSaveModeResult(context: Context,
|
|
||||||
intent: Intent,
|
|
||||||
searchInfo: SearchInfo) {
|
|
||||||
addSpecialModeInIntent(intent, SpecialMode.SAVE)
|
|
||||||
addTypeModeInIntent(intent, TypeMode.DEFAULT)
|
|
||||||
addSearchInfoInIntent(intent, searchInfo)
|
|
||||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startActivityForKeyboardSelectionModeResult(context: Context,
|
|
||||||
intent: Intent,
|
|
||||||
searchInfo: SearchInfo?) {
|
|
||||||
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
|
||||||
addTypeModeInIntent(intent, TypeMode.MAGIKEYBOARD)
|
|
||||||
addSearchInfoInIntent(intent, searchInfo)
|
|
||||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility method to start an activity with an Autofill for result
|
|
||||||
*/
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
fun startActivityForAutofillSelectionModeResult(
|
|
||||||
context: Context,
|
context: Context,
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
searchInfo: SearchInfo
|
||||||
autofillComponent: AutofillComponent,
|
|
||||||
searchInfo: SearchInfo?
|
|
||||||
) {
|
) {
|
||||||
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
intent.addSpecialMode(SpecialMode.SEARCH)
|
||||||
addTypeModeInIntent(intent, TypeMode.AUTOFILL)
|
intent.addSearchInfo(searchInfo)
|
||||||
intent.addAutofillComponent(context, autofillComponent)
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
addSearchInfoInIntent(intent, searchInfo)
|
context.startActivity(intent)
|
||||||
activityResultLauncher?.launch(intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
fun startActivityForSelectionModeResult(
|
||||||
fun startActivityForPasskeySelectionModeResult(
|
|
||||||
context: Context,
|
context: Context,
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
typeMode: TypeMode,
|
||||||
searchInfo: SearchInfo?
|
searchInfo: SearchInfo?,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
|
||||||
) {
|
) {
|
||||||
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
intent.addSpecialMode(SpecialMode.SELECTION)
|
||||||
addTypeModeInIntent(intent, TypeMode.PASSKEY)
|
intent.addTypeMode(typeMode)
|
||||||
addSearchInfoInIntent(intent, searchInfo)
|
intent.addSearchInfo(searchInfo)
|
||||||
activityResultLauncher?.launch(intent)
|
if (activityResultLauncher == null) {
|
||||||
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
}
|
||||||
|
activityResultLauncher?.launch(intent) ?: context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startActivityForRegistrationModeResult(
|
fun startActivityForRegistrationModeResult(
|
||||||
context: Context?,
|
context: Context,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
registerInfo: RegisterInfo?,
|
registerInfo: RegisterInfo?,
|
||||||
typeMode: TypeMode
|
typeMode: TypeMode
|
||||||
) {
|
) {
|
||||||
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
intent.addSpecialMode(SpecialMode.REGISTRATION)
|
||||||
addTypeModeInIntent(intent, typeMode)
|
intent.addTypeMode(typeMode)
|
||||||
addRegisterInfoInIntent(intent, registerInfo)
|
intent.addRegisterInfo(registerInfo)
|
||||||
if (activityResultLauncher == null) {
|
if (activityResultLauncher == null) {
|
||||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
}
|
}
|
||||||
activityResultLauncher?.launch(intent) ?: context?.startActivity(intent) ?:
|
activityResultLauncher?.launch(intent) ?: context.startActivity(intent)
|
||||||
throw IllegalStateException("At least Context or ActivityResultLauncher must not be null")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) {
|
/**
|
||||||
|
* Build the special mode response for internal entry selection for one entry
|
||||||
|
*/
|
||||||
|
fun Activity.buildSpecialModeResponseAndSetResult(
|
||||||
|
entryInfo: EntryInfo,
|
||||||
|
extras: Bundle? = null
|
||||||
|
) {
|
||||||
|
this.buildSpecialModeResponseAndSetResult(listOf(entryInfo), extras)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the special mode response for internal entry selection for multiple entries
|
||||||
|
*/
|
||||||
|
fun Activity.buildSpecialModeResponseAndSetResult(
|
||||||
|
entriesInfo: List<EntryInfo>,
|
||||||
|
extras: Bundle? = null
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
val mReplyIntent = Intent()
|
||||||
|
Log.d(javaClass.name, "Success special mode manual selection")
|
||||||
|
mReplyIntent.addNodesIds(entriesInfo.map { it.id })
|
||||||
|
extras?.let {
|
||||||
|
mReplyIntent.putExtras(it)
|
||||||
|
}
|
||||||
|
setResult(Activity.RESULT_OK, mReplyIntent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(javaClass.name, "Unable to add the result", e)
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.addSearchInfo(searchInfo: SearchInfo?): Intent {
|
||||||
searchInfo?.let {
|
searchInfo?.let {
|
||||||
intent.putExtra(KEY_SEARCH_INFO, it)
|
putExtra(KEY_SEARCH_INFO, it)
|
||||||
}
|
}
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retrieveSearchInfoFromIntent(intent: Intent): SearchInfo? {
|
fun Intent.retrieveSearchInfo(): SearchInfo? {
|
||||||
return intent.getParcelableExtraCompat(KEY_SEARCH_INFO)
|
return getParcelableExtraCompat(KEY_SEARCH_INFO)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
|
fun Intent.addRegisterInfo(registerInfo: RegisterInfo?): Intent {
|
||||||
registerInfo?.let {
|
registerInfo?.let {
|
||||||
intent.putExtra(KEY_REGISTER_INFO, it)
|
putExtra(KEY_REGISTER_INFO, it)
|
||||||
}
|
}
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retrieveRegisterInfoFromIntent(intent: Intent): RegisterInfo? {
|
fun Intent.retrieveRegisterInfo(): RegisterInfo? {
|
||||||
return intent.getParcelableExtraCompat(KEY_REGISTER_INFO)
|
return getParcelableExtraCompat(KEY_REGISTER_INFO)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeInfoFromIntent(intent: Intent) {
|
fun Intent.removeInfo() {
|
||||||
intent.removeExtra(KEY_SEARCH_INFO)
|
removeExtra(KEY_SEARCH_INFO)
|
||||||
intent.removeExtra(KEY_REGISTER_INFO)
|
removeExtra(KEY_REGISTER_INFO)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
|
|
||||||
// TODO Replace by Intent.addSpecialMode
|
|
||||||
intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
|
||||||
}
|
|
||||||
fun Intent.addSpecialMode(specialMode: SpecialMode): Intent {
|
fun Intent.addSpecialMode(specialMode: SpecialMode): Intent {
|
||||||
this.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
this.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
|
fun Intent.retrieveSpecialMode(): SpecialMode {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
return getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
|
||||||
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
|
|
||||||
return SpecialMode.SELECTION
|
|
||||||
}
|
|
||||||
return intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
|
||||||
// TODO Replace by Intent.addTypeMode
|
|
||||||
intent.putEnumExtra(KEY_TYPE_MODE, typeMode)
|
|
||||||
}
|
|
||||||
fun Intent.addTypeMode(typeMode: TypeMode): Intent {
|
fun Intent.addTypeMode(typeMode: TypeMode): Intent {
|
||||||
this.putEnumExtra(KEY_TYPE_MODE, typeMode)
|
this.putEnumExtra(KEY_TYPE_MODE, typeMode)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
|
fun Intent.retrieveTypeMode(): TypeMode {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
return getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
|
||||||
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
|
|
||||||
return TypeMode.AUTOFILL
|
|
||||||
}
|
|
||||||
return intent.getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeModesFromIntent(intent: Intent) {
|
fun Intent.removeModes() {
|
||||||
intent.removeExtra(KEY_SPECIAL_MODE)
|
removeExtra(KEY_SPECIAL_MODE)
|
||||||
intent.removeExtra(KEY_TYPE_MODE)
|
removeExtra(KEY_TYPE_MODE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.addNodesIds(nodesIds: List<UUID>): Intent {
|
||||||
|
this.putParcelableList(EXTRA_NODES_IDS, nodesIds.map { ParcelUuid(it) })
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.retrieveNodesIds(): List<UUID>? {
|
||||||
|
return getParcelableList<ParcelUuid>(EXTRA_NODES_IDS)?.map { it.uuid }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.removeNodesIds() {
|
||||||
|
removeExtra(EXTRA_NODES_IDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the node id to the intent
|
||||||
|
*/
|
||||||
|
fun Intent.addNodeId(nodeId: UUID?) {
|
||||||
|
nodeId?.let {
|
||||||
|
putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the node id from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.retrieveNodeId(): UUID? {
|
||||||
|
return getParcelableExtraCompat<ParcelUuid>(EXTRA_NODE_ID)?.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.removeNodeId() {
|
||||||
|
removeExtra(EXTRA_NODE_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -240,73 +239,75 @@ object EntrySelectionHelper {
|
|||||||
fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean {
|
fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean {
|
||||||
return (specialMode == SpecialMode.SELECTION
|
return (specialMode == SpecialMode.SELECTION
|
||||||
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
|
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
|
||||||
// TODO Autofill Registration callback #765 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
|
||||||
|| (specialMode == SpecialMode.REGISTRATION
|
|| (specialMode == SpecialMode.REGISTRATION
|
||||||
&& typeMode == TypeMode.PASSKEY)
|
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun doSpecialAction(intent: Intent,
|
fun doSpecialAction(
|
||||||
|
intent: Intent,
|
||||||
defaultAction: () -> Unit,
|
defaultAction: () -> Unit,
|
||||||
searchAction: (searchInfo: SearchInfo) -> Unit,
|
searchAction: (searchInfo: SearchInfo) -> Unit,
|
||||||
saveAction: (searchInfo: SearchInfo) -> Unit,
|
selectionAction: (
|
||||||
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
intentSenderMode: Boolean,
|
||||||
autofillSelectionAction: (searchInfo: SearchInfo?,
|
typeMode: TypeMode,
|
||||||
autofillComponent: AutofillComponent) -> Unit,
|
searchInfo: SearchInfo?
|
||||||
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit,
|
) -> Unit,
|
||||||
passkeySelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
registrationAction: (
|
||||||
passkeyRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
|
intentSenderMode: Boolean,
|
||||||
|
typeMode: TypeMode,
|
||||||
when (retrieveSpecialModeFromIntent(intent)) {
|
registerInfo: RegisterInfo?
|
||||||
|
) -> Unit
|
||||||
|
) {
|
||||||
|
when (val specialMode = intent.retrieveSpecialMode()) {
|
||||||
SpecialMode.DEFAULT -> {
|
SpecialMode.DEFAULT -> {
|
||||||
removeModesFromIntent(intent)
|
intent.removeModes()
|
||||||
removeInfoFromIntent(intent)
|
intent.removeInfo()
|
||||||
defaultAction.invoke()
|
defaultAction.invoke()
|
||||||
}
|
}
|
||||||
SpecialMode.SEARCH -> {
|
SpecialMode.SEARCH -> {
|
||||||
val searchInfo = retrieveSearchInfoFromIntent(intent)
|
val searchInfo = intent.retrieveSearchInfo()
|
||||||
removeModesFromIntent(intent)
|
intent.removeModes()
|
||||||
removeInfoFromIntent(intent)
|
intent.removeInfo()
|
||||||
if (searchInfo != null)
|
if (searchInfo != null)
|
||||||
searchAction.invoke(searchInfo)
|
searchAction.invoke(searchInfo)
|
||||||
else {
|
else {
|
||||||
defaultAction.invoke()
|
defaultAction.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SpecialMode.SAVE -> {
|
|
||||||
val searchInfo = retrieveSearchInfoFromIntent(intent)
|
|
||||||
removeModesFromIntent(intent)
|
|
||||||
removeInfoFromIntent(intent)
|
|
||||||
if (searchInfo != null)
|
|
||||||
saveAction.invoke(searchInfo)
|
|
||||||
else {
|
|
||||||
defaultAction.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SpecialMode.SELECTION -> {
|
SpecialMode.SELECTION -> {
|
||||||
val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent)
|
val searchInfo: SearchInfo? = intent.retrieveSearchInfo()
|
||||||
var autofillComponentInit = false
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
AutofillHelper.retrieveAutofillComponent(intent)?.let { autofillComponent ->
|
|
||||||
autofillSelectionAction.invoke(searchInfo, autofillComponent)
|
|
||||||
autofillComponentInit = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!autofillComponentInit) {
|
|
||||||
if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) {
|
if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) {
|
||||||
when (retrieveTypeModeFromIntent(intent)) {
|
when (val typeMode = intent.retrieveTypeMode()) {
|
||||||
TypeMode.DEFAULT -> {
|
TypeMode.DEFAULT -> {
|
||||||
removeModesFromIntent(intent)
|
intent.removeModes()
|
||||||
if (searchInfo != null)
|
if (searchInfo != null)
|
||||||
searchAction.invoke(searchInfo)
|
searchAction.invoke(searchInfo)
|
||||||
else
|
else
|
||||||
defaultAction.invoke()
|
defaultAction.invoke()
|
||||||
}
|
}
|
||||||
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
|
TypeMode.MAGIKEYBOARD -> selectionAction.invoke(
|
||||||
TypeMode.PASSKEY -> passkeySelectionAction.invoke(searchInfo)
|
isIntentSenderMode(specialMode, typeMode),
|
||||||
else -> {
|
typeMode,
|
||||||
// In this case, error
|
searchInfo
|
||||||
removeModesFromIntent(intent)
|
)
|
||||||
removeInfoFromIntent(intent)
|
TypeMode.PASSKEY ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
selectionAction.invoke(
|
||||||
|
isIntentSenderMode(specialMode, typeMode),
|
||||||
|
typeMode,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
} else
|
||||||
|
defaultAction.invoke()
|
||||||
|
TypeMode.AUTOFILL -> {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
selectionAction.invoke(
|
||||||
|
isIntentSenderMode(specialMode, typeMode),
|
||||||
|
typeMode,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
} else
|
||||||
|
defaultAction.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -316,26 +317,22 @@ object EntrySelectionHelper {
|
|||||||
defaultAction.invoke()
|
defaultAction.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
SpecialMode.REGISTRATION -> {
|
SpecialMode.REGISTRATION -> {
|
||||||
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
|
val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
|
||||||
if (!isIntentSenderMode(
|
val typeMode = intent.retrieveTypeMode()
|
||||||
specialMode = retrieveSpecialModeFromIntent(intent),
|
val intentSenderMode = isIntentSenderMode(specialMode, typeMode)
|
||||||
typeMode = retrieveTypeModeFromIntent(intent))
|
if (!intentSenderMode) {
|
||||||
) {
|
intent.removeModes()
|
||||||
removeModesFromIntent(intent)
|
intent.removeInfo()
|
||||||
removeInfoFromIntent(intent)
|
|
||||||
}
|
|
||||||
when (retrieveTypeModeFromIntent(intent)) {
|
|
||||||
TypeMode.AUTOFILL -> {
|
|
||||||
autofillRegistrationAction.invoke(registerInfo)
|
|
||||||
}
|
|
||||||
TypeMode.PASSKEY -> {
|
|
||||||
passkeyRegistrationAction.invoke(registerInfo)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// Do other registration type
|
|
||||||
}
|
}
|
||||||
|
if (registerInfo != null)
|
||||||
|
registrationAction.invoke(
|
||||||
|
intentSenderMode,
|
||||||
|
typeMode,
|
||||||
|
registerInfo
|
||||||
|
)
|
||||||
|
else {
|
||||||
|
defaultAction.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -367,7 +364,7 @@ object EntrySelectionHelper {
|
|||||||
try {
|
try {
|
||||||
database.iconDrawableFactory.getBitmapFromIcon(context,
|
database.iconDrawableFactory.getBitmapFromIcon(context,
|
||||||
this.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
this.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
||||||
return Icon.createWithBitmap(bitmap)
|
return IconCompat.createWithBitmap(bitmap).toIcon(context)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.kunzisoft.keepass.credentialprovider
|
|||||||
enum class SpecialMode {
|
enum class SpecialMode {
|
||||||
DEFAULT,
|
DEFAULT,
|
||||||
SEARCH,
|
SEARCH,
|
||||||
SAVE,
|
|
||||||
SELECTION,
|
SELECTION,
|
||||||
REGISTRATION;
|
REGISTRATION;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
package com.kunzisoft.keepass.credentialprovider
|
package com.kunzisoft.keepass.credentialprovider
|
||||||
|
|
||||||
enum class TypeMode {
|
enum class TypeMode {
|
||||||
DEFAULT, MAGIKEYBOARD, AUTOFILL, PASSKEY
|
DEFAULT, MAGIKEYBOARD, PASSKEY, AUTOFILL
|
||||||
}
|
}
|
||||||
@@ -27,34 +27,45 @@ import android.os.Bundle
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
import com.kunzisoft.keepass.activities.GroupActivity
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addRegisterInfo
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutofillComponent
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.CompatInlineSuggestionsRequest
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.AutofillLauncherViewModel
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
|
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
|
||||||
import com.kunzisoft.keepass.database.helper.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.utils.AppUtil
|
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
|
||||||
import com.kunzisoft.keepass.view.toastError
|
import com.kunzisoft.keepass.view.toastError
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class AutofillLauncherActivity : DatabaseModeActivity() {
|
class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
private val autofillLauncherViewModel: AutofillLauncherViewModel by viewModels()
|
||||||
this.buildActivityResultLauncher(lockDatabase = true)
|
|
||||||
|
private var mAutofillSelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
autofillLauncherViewModel.manageSelectionResult(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mAutofillRegistrationActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
autofillLauncherViewModel.manageRegistrationResult(it)
|
||||||
|
}
|
||||||
|
|
||||||
override fun applyCustomStyle(): Boolean {
|
override fun applyCustomStyle(): Boolean {
|
||||||
return false
|
return false
|
||||||
@@ -64,177 +75,103 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onCreate(savedInstanceState)
|
||||||
|
autofillLauncherViewModel.initialize()
|
||||||
// Retrieve selection mode
|
lifecycleScope.launch {
|
||||||
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
|
// Initialize the parameters
|
||||||
when (specialMode) {
|
autofillLauncherViewModel.uiState.collect { uiState ->
|
||||||
SpecialMode.SELECTION -> {
|
when (uiState) {
|
||||||
intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle ->
|
AutofillLauncherViewModel.UIState.Loading -> {}
|
||||||
// To pass extra inline request
|
is AutofillLauncherViewModel.UIState.ShowBlockRestartMessage -> {
|
||||||
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
compatInlineSuggestionsRequest = bundle.getParcelableCompat(
|
|
||||||
KEY_INLINE_SUGGESTION
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// Build search param
|
|
||||||
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
|
||||||
AppUtil.getConcreteWebDomain(
|
|
||||||
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 -> {
|
|
||||||
// To register info
|
|
||||||
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>(
|
|
||||||
KEY_REGISTER_INFO
|
|
||||||
)
|
|
||||||
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
|
||||||
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
|
||||||
searchInfo.webDomain = concreteWebDomain
|
|
||||||
launchRegistration(database, searchInfo, registerInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// Not an autofill call
|
|
||||||
setResult(RESULT_CANCELED)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchSelection(database: ContextualDatabase?,
|
|
||||||
autofillComponent: AutofillComponent?,
|
|
||||||
searchInfo: SearchInfo) {
|
|
||||||
if (autofillComponent == null) {
|
|
||||||
setResult(RESULT_CANCELED)
|
|
||||||
finish()
|
|
||||||
} else if (KeeAutofillService.autofillAllowedFor(
|
|
||||||
applicationId = searchInfo.applicationId,
|
|
||||||
webDomain = searchInfo.webDomain,
|
|
||||||
context = this
|
|
||||||
)) {
|
|
||||||
// If database is open
|
|
||||||
SearchHelper.checkAutoSearchInfo(
|
|
||||||
context = this,
|
|
||||||
database = database,
|
|
||||||
searchInfo = searchInfo,
|
|
||||||
onItemsFound = { openedDatabase, items ->
|
|
||||||
// Items found
|
|
||||||
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
|
|
||||||
finish()
|
|
||||||
},
|
|
||||||
onItemNotFound = { openedDatabase ->
|
|
||||||
// Show the database UI to select the entry
|
|
||||||
GroupActivity.launchForAutofillSelectionResult(
|
|
||||||
this,
|
|
||||||
openedDatabase,
|
|
||||||
mCredentialActivityResultLauncher,
|
|
||||||
autofillComponent,
|
|
||||||
searchInfo,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onDatabaseClosed = {
|
|
||||||
// If database not open
|
|
||||||
FileDatabaseSelectActivity.launchForAutofillResult(
|
|
||||||
this,
|
|
||||||
mCredentialActivityResultLauncher,
|
|
||||||
autofillComponent,
|
|
||||||
searchInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
showBlockRestartMessage()
|
showBlockRestartMessage()
|
||||||
setResult(RESULT_CANCELED)
|
autofillLauncherViewModel.cancelResult()
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
}
|
is AutofillLauncherViewModel.UIState.ShowReadOnlyMessage -> {
|
||||||
|
|
||||||
private fun launchRegistration(database: ContextualDatabase?,
|
|
||||||
searchInfo: SearchInfo,
|
|
||||||
registerInfo: RegisterInfo?) {
|
|
||||||
if (KeeAutofillService.autofillAllowedFor(
|
|
||||||
applicationId = searchInfo.applicationId,
|
|
||||||
webDomain = searchInfo.webDomain,
|
|
||||||
context = this
|
|
||||||
)) {
|
|
||||||
val readOnly = database?.isReadOnly != false
|
|
||||||
SearchHelper.checkAutoSearchInfo(
|
|
||||||
context = this,
|
|
||||||
database = database,
|
|
||||||
searchInfo = searchInfo,
|
|
||||||
onItemsFound = { openedDatabase, _ ->
|
|
||||||
if (!readOnly) {
|
|
||||||
// Show the database UI to select the entry
|
|
||||||
GroupActivity.launchForRegistration(
|
|
||||||
context = this,
|
|
||||||
activityResultLauncher = null, // TODO Autofill result launcher #765
|
|
||||||
database = openedDatabase,
|
|
||||||
registerInfo = registerInfo,
|
|
||||||
typeMode = TypeMode.AUTOFILL
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
showReadOnlySaveMessage()
|
showReadOnlySaveMessage()
|
||||||
|
autofillLauncherViewModel.cancelResult()
|
||||||
}
|
}
|
||||||
},
|
is AutofillLauncherViewModel.UIState.ShowAutofillSuggestionMessage -> {
|
||||||
onItemNotFound = { openedDatabase ->
|
showAutofillSuggestionMessage()
|
||||||
if (!readOnly) {
|
}
|
||||||
// Show the database UI to select the entry
|
}
|
||||||
GroupActivity.launchForRegistration(
|
}
|
||||||
context = this,
|
}
|
||||||
activityResultLauncher = null, // TODO Autofill result launcher #765
|
lifecycleScope.launch {
|
||||||
database = openedDatabase,
|
// Retrieve the UI
|
||||||
registerInfo = registerInfo,
|
autofillLauncherViewModel.credentialUiState.collect { uiState ->
|
||||||
typeMode = TypeMode.AUTOFILL
|
when (uiState) {
|
||||||
|
is CredentialLauncherViewModel.UIState.Loading -> {}
|
||||||
|
is CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection -> {
|
||||||
|
GroupActivity.launchForSelection(
|
||||||
|
context = this@AutofillLauncherActivity,
|
||||||
|
database = uiState.database,
|
||||||
|
searchInfo = uiState.searchInfo,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
activityResultLauncher = mAutofillSelectionActivityResultLauncher,
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
showReadOnlySaveMessage()
|
|
||||||
}
|
}
|
||||||
},
|
is CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> {
|
||||||
onDatabaseClosed = {
|
GroupActivity.launchForRegistration(
|
||||||
// If database not open
|
context = this@AutofillLauncherActivity,
|
||||||
|
database = uiState.database,
|
||||||
|
registerInfo = uiState.registerInfo,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
activityResultLauncher = mAutofillRegistrationActivityResultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> {
|
||||||
|
FileDatabaseSelectActivity.launchForSelection(
|
||||||
|
context = this@AutofillLauncherActivity,
|
||||||
|
searchInfo = uiState.searchInfo,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
activityResultLauncher = mAutofillSelectionActivityResultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration -> {
|
||||||
FileDatabaseSelectActivity.launchForRegistration(
|
FileDatabaseSelectActivity.launchForRegistration(
|
||||||
context = this,
|
context = this@AutofillLauncherActivity,
|
||||||
activityResultLauncher = null, // TODO Autofill result launcher #765
|
registerInfo = uiState.registerInfo,
|
||||||
registerInfo = registerInfo,
|
typeMode = uiState.typeMode,
|
||||||
typeMode = TypeMode.AUTOFILL
|
activityResultLauncher = mAutofillRegistrationActivityResultLauncher,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is CredentialLauncherViewModel.UIState.SetActivityResult -> {
|
||||||
|
setActivityResult(
|
||||||
|
lockDatabase = uiState.lockDatabase,
|
||||||
|
resultCode = uiState.resultCode,
|
||||||
|
data = uiState.data
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
showBlockRestartMessage()
|
|
||||||
setResult(RESULT_CANCELED)
|
|
||||||
}
|
}
|
||||||
finish()
|
is CredentialLauncherViewModel.UIState.ShowError -> {
|
||||||
|
toastError(uiState.error)
|
||||||
|
autofillLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
super.onUnknownDatabaseRetrieved(database)
|
||||||
|
autofillLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showBlockRestartMessage() {
|
private fun showBlockRestartMessage() {
|
||||||
// If item not allowed, show a toast
|
// If item not allowed, show a toast
|
||||||
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
|
Toast.makeText(
|
||||||
|
applicationContext,
|
||||||
|
R.string.autofill_block_restart,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showAutofillSuggestionMessage() {
|
||||||
|
Toast.makeText(
|
||||||
|
applicationContext,
|
||||||
|
R.string.autofill_inline_suggestions_keyboard,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showReadOnlySaveMessage() {
|
private fun showReadOnlySaveMessage() {
|
||||||
@@ -245,27 +182,21 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
|
|
||||||
private val TAG = AutofillLauncherActivity::class.java.name
|
private val TAG = AutofillLauncherActivity::class.java.name
|
||||||
|
|
||||||
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
fun getPendingIntentForSelection(
|
||||||
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
context: Context,
|
||||||
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
|
|
||||||
|
|
||||||
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
|
||||||
|
|
||||||
fun getPendingIntentForSelection(context: Context,
|
|
||||||
searchInfo: SearchInfo? = null,
|
searchInfo: SearchInfo? = null,
|
||||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent? {
|
autofillComponent: AutofillComponent
|
||||||
|
): PendingIntent? {
|
||||||
try {
|
try {
|
||||||
return PendingIntent.getActivity(
|
return PendingIntent.getActivity(
|
||||||
context, 0,
|
context,
|
||||||
|
randomRequestCode(),
|
||||||
// Doesn't work with direct extra Parcelable (don't know why?)
|
// Doesn't work with direct extra Parcelable (don't know why?)
|
||||||
// Wrap into a bundle to bypass the problem
|
// Wrap into a bundle to bypass the problem
|
||||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
|
addSpecialMode(SpecialMode.SELECTION)
|
||||||
putParcelable(KEY_SEARCH_INFO, searchInfo)
|
addSearchInfo(searchInfo)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
addAutofillComponent(autofillComponent)
|
||||||
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
@@ -279,14 +210,17 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPendingIntentForRegistration(context: Context,
|
fun getPendingIntentForRegistration(
|
||||||
registerInfo: RegisterInfo): PendingIntent? {
|
context: Context,
|
||||||
|
registerInfo: RegisterInfo
|
||||||
|
): PendingIntent? {
|
||||||
try {
|
try {
|
||||||
return PendingIntent.getActivity(
|
return PendingIntent.getActivity(
|
||||||
context, 0,
|
context,
|
||||||
|
randomRequestCode(),
|
||||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
addSpecialMode(SpecialMode.REGISTRATION)
|
||||||
putExtra(KEY_REGISTER_INFO, registerInfo)
|
addRegisterInfo(registerInfo)
|
||||||
},
|
},
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
@@ -299,14 +233,5 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchForRegistration(context: Context,
|
|
||||||
registerInfo: RegisterInfo) {
|
|
||||||
val intent = Intent(context, AutofillLauncherActivity::class.java)
|
|
||||||
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
|
||||||
intent.putExtra(KEY_REGISTER_INFO, registerInfo)
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,19 +22,17 @@ package com.kunzisoft.keepass.credentialprovider.activity
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
import com.kunzisoft.keepass.activities.GroupActivity
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
|
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
|
||||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.utils.AppUtil
|
|
||||||
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
|
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import com.kunzisoft.keepass.view.toastError
|
import com.kunzisoft.keepass.view.toastError
|
||||||
@@ -53,8 +51,8 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onUnknownDatabaseRetrieved(database)
|
||||||
|
|
||||||
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
|
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
|
||||||
if (keySelectionBundle != null) {
|
if (keySelectionBundle != null) {
|
||||||
@@ -88,7 +86,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
if (OtpEntryFields.isOTPUri(extra))
|
if (OtpEntryFields.isOTPUri(extra))
|
||||||
otpString = extra
|
otpString = extra
|
||||||
}
|
}
|
||||||
launchSelection(database, sharedWebDomain, otpString)
|
launchSelection(database, null, otpString)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
if (database != null) {
|
if (database != null) {
|
||||||
@@ -110,12 +108,8 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
this.webDomain = sharedWebDomain
|
this.webDomain = sharedWebDomain
|
||||||
this.otpString = otpString
|
this.otpString = otpString
|
||||||
}
|
}
|
||||||
|
|
||||||
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
|
||||||
searchInfo.webDomain = concreteWebDomain
|
|
||||||
launch(database, searchInfo)
|
launch(database, searchInfo)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun launch(database: ContextualDatabase?,
|
private fun launch(database: ContextualDatabase?,
|
||||||
searchInfo: SearchInfo) {
|
searchInfo: SearchInfo) {
|
||||||
@@ -133,11 +127,12 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
// Items found
|
// Items found
|
||||||
if (searchInfo.otpString != null) {
|
if (searchInfo.otpString != null) {
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
GroupActivity.launchForSaveResult(
|
GroupActivity.launchForRegistration(
|
||||||
this,
|
context = this,
|
||||||
openedDatabase,
|
activityResultLauncher = null,
|
||||||
searchInfo,
|
database = openedDatabase,
|
||||||
false
|
registerInfo = searchInfo.toRegisterInfo(),
|
||||||
|
typeMode = TypeMode.DEFAULT
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
toastError(RegisterInReadOnlyDatabaseException())
|
toastError(RegisterInReadOnlyDatabaseException())
|
||||||
@@ -153,11 +148,12 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
{ autoSearch ->
|
{ autoSearch ->
|
||||||
GroupActivity.launchForKeyboardSelectionResult(
|
GroupActivity.launchForSelection(
|
||||||
this,
|
context = this,
|
||||||
openedDatabase,
|
database = openedDatabase,
|
||||||
searchInfo,
|
typeMode = TypeMode.MAGIKEYBOARD,
|
||||||
autoSearch
|
searchInfo = searchInfo,
|
||||||
|
autoSearch = autoSearch
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -174,21 +170,23 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
// 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(
|
GroupActivity.launchForRegistration(
|
||||||
this,
|
context = this,
|
||||||
openedDatabase,
|
activityResultLauncher = null,
|
||||||
searchInfo,
|
database = openedDatabase,
|
||||||
false
|
registerInfo = searchInfo.toRegisterInfo(),
|
||||||
|
typeMode = TypeMode.DEFAULT
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
toastError(RegisterInReadOnlyDatabaseException())
|
toastError(RegisterInReadOnlyDatabaseException())
|
||||||
}
|
}
|
||||||
} else if (searchShareForMagikeyboard) {
|
} else if (searchShareForMagikeyboard) {
|
||||||
GroupActivity.launchForKeyboardSelectionResult(
|
GroupActivity.launchForSelection(
|
||||||
this,
|
context = this,
|
||||||
openedDatabase,
|
database = openedDatabase,
|
||||||
searchInfo,
|
typeMode = TypeMode.MAGIKEYBOARD,
|
||||||
false
|
searchInfo = searchInfo,
|
||||||
|
autoSearch = false
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
GroupActivity.launchForSearchResult(
|
GroupActivity.launchForSearchResult(
|
||||||
@@ -202,14 +200,17 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
onDatabaseClosed = {
|
onDatabaseClosed = {
|
||||||
// If database not open
|
// If database not open
|
||||||
if (searchInfo.otpString != null) {
|
if (searchInfo.otpString != null) {
|
||||||
FileDatabaseSelectActivity.launchForSaveResult(
|
FileDatabaseSelectActivity.launchForRegistration(
|
||||||
this,
|
context = this,
|
||||||
searchInfo
|
activityResultLauncher = null,
|
||||||
|
registerInfo = searchInfo.toRegisterInfo(),
|
||||||
|
typeMode = TypeMode.DEFAULT
|
||||||
)
|
)
|
||||||
} else if (searchShareForMagikeyboard) {
|
} else if (searchShareForMagikeyboard) {
|
||||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(
|
FileDatabaseSelectActivity.launchForSelection(
|
||||||
this,
|
context = this,
|
||||||
searchInfo
|
typeMode = TypeMode.MAGIKEYBOARD,
|
||||||
|
searchInfo = searchInfo
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
FileDatabaseSelectActivity.launchForSearchResult(
|
FileDatabaseSelectActivity.launchForSearchResult(
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package com.kunzisoft.keepass.credentialprovider.activity
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.addHardwareKey
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.addSeed
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.buildHardwareKeyChallenge
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.isYubikeyDriverAvailable
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.UIState
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
|
||||||
|
import com.kunzisoft.keepass.view.toastError
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Special activity to deal with hardware key drivers,
|
||||||
|
* return the response to the database service once finished
|
||||||
|
*/
|
||||||
|
class HardwareKeyActivity: DatabaseModeActivity(){
|
||||||
|
|
||||||
|
private val mHardwareKeyLauncherViewModel: HardwareKeyLauncherViewModel by viewModels()
|
||||||
|
|
||||||
|
private var activityResultLauncher: ActivityResultLauncher<Intent> =
|
||||||
|
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
mHardwareKeyLauncherViewModel.manageSelectionResult(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun applyCustomStyle(): Boolean = false
|
||||||
|
|
||||||
|
override fun showDatabaseDialog(): Boolean = false
|
||||||
|
|
||||||
|
override fun manageDatabaseInfo(): Boolean = false
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
mHardwareKeyLauncherViewModel.uiState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
is UIState.Loading -> {}
|
||||||
|
is UIState.ShowHardwareKeyDriverNeeded -> {
|
||||||
|
showHardwareKeyDriverNeeded(
|
||||||
|
this@HardwareKeyActivity,
|
||||||
|
uiState.hardwareKey
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.onChallengeResponded(null)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is UIState.LaunchChallengeActivityForResponse -> {
|
||||||
|
// Send to the driver
|
||||||
|
activityResultLauncher.launch(
|
||||||
|
buildHardwareKeyChallenge(uiState.challenge)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is UIState.OnChallengeResponded -> {
|
||||||
|
mDatabaseViewModel.onChallengeResponded(uiState.response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
mHardwareKeyLauncherViewModel.credentialUiState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
is CredentialLauncherViewModel.UIState.SetActivityResult -> {
|
||||||
|
setActivityResult(
|
||||||
|
lockDatabase = uiState.lockDatabase,
|
||||||
|
resultCode = uiState.resultCode,
|
||||||
|
data = uiState.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.UIState.ShowError -> {
|
||||||
|
toastError(uiState.error)
|
||||||
|
mHardwareKeyLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
mHardwareKeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseActionFinished(
|
||||||
|
database: ContextualDatabase,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showHardwareKeyDriverNeeded(
|
||||||
|
context: Context,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
|
onDialogDismissed: DialogInterface.OnDismissListener
|
||||||
|
) {
|
||||||
|
val builder = AlertDialog.Builder(context)
|
||||||
|
builder
|
||||||
|
.setMessage(
|
||||||
|
context.getString(R.string.error_driver_required, hardwareKey.toString())
|
||||||
|
)
|
||||||
|
.setPositiveButton(R.string.download) { _, _ ->
|
||||||
|
context.openExternalApp(
|
||||||
|
context.getString(R.string.key_driver_app_id),
|
||||||
|
context.getString(R.string.key_driver_url)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
.setOnDismissListener(onDialogDismissed)
|
||||||
|
builder.create().show()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = HardwareKeyActivity::class.java.simpleName
|
||||||
|
|
||||||
|
fun launchHardwareKeyActivity(
|
||||||
|
context: Context,
|
||||||
|
hardwareKey: HardwareKey,
|
||||||
|
seed: ByteArray?
|
||||||
|
) {
|
||||||
|
context.startActivity(
|
||||||
|
Intent(
|
||||||
|
context,
|
||||||
|
HardwareKeyActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||||
|
Intent.FLAG_ACTIVITY_MULTIPLE_TASK
|
||||||
|
addHardwareKey(hardwareKey)
|
||||||
|
addSeed(seed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isHardwareKeyAvailable(
|
||||||
|
context: Context,
|
||||||
|
hardwareKey: HardwareKey?
|
||||||
|
): Boolean {
|
||||||
|
if (hardwareKey == null)
|
||||||
|
return false
|
||||||
|
return when (hardwareKey) {
|
||||||
|
/*
|
||||||
|
HardwareKey.FIDO2_SECRET -> {
|
||||||
|
// TODO FIDO2 under development
|
||||||
|
false
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
|
||||||
|
// Check available intent
|
||||||
|
isYubikeyDriverAvailable(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,8 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
import com.kunzisoft.keepass.activities.GroupActivity
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addNodeId
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||||
@@ -44,14 +46,14 @@ import com.kunzisoft.keepass.credentialprovider.TypeMode
|
|||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addNodeId
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addSearchInfo
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.model.AppOrigin
|
import com.kunzisoft.keepass.model.AppOrigin
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||||
import com.kunzisoft.keepass.view.toastError
|
import com.kunzisoft.keepass.view.toastError
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -79,10 +81,6 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun finishActivityIfDatabaseNotLoaded(): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
@@ -105,61 +103,69 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
|||||||
nodeId = uiState.nodeId
|
nodeId = uiState.nodeId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is PasskeyLauncherViewModel.UIState.SetActivityResult -> {
|
|
||||||
setActivityResult(
|
|
||||||
lockDatabase = uiState.lockDatabase,
|
|
||||||
resultCode = uiState.resultCode,
|
|
||||||
data = uiState.data
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is PasskeyLauncherViewModel.UIState.ShowError -> {
|
|
||||||
toastError(uiState.error)
|
|
||||||
passkeyLauncherViewModel.cancelResult()
|
|
||||||
}
|
|
||||||
is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForSelection -> {
|
|
||||||
GroupActivity.launchForPasskeySelectionResult(
|
|
||||||
context = this@PasskeyLauncherActivity,
|
|
||||||
database = uiState.database,
|
|
||||||
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
|
|
||||||
searchInfo = null,
|
|
||||||
autoSearch = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> {
|
|
||||||
GroupActivity.launchForRegistration(
|
|
||||||
context = this@PasskeyLauncherActivity,
|
|
||||||
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
|
|
||||||
database = uiState.database,
|
|
||||||
registerInfo = uiState.registerInfo,
|
|
||||||
typeMode = uiState.typeMode
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> {
|
|
||||||
FileDatabaseSelectActivity.launchForPasskeySelectionResult(
|
|
||||||
activity = this@PasskeyLauncherActivity,
|
|
||||||
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
|
|
||||||
searchInfo = uiState.searchInfo,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration -> {
|
|
||||||
FileDatabaseSelectActivity.launchForRegistration(
|
|
||||||
context = this@PasskeyLauncherActivity,
|
|
||||||
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
|
|
||||||
registerInfo = uiState.registerInfo,
|
|
||||||
typeMode = uiState.typeMode
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is PasskeyLauncherViewModel.UIState.UpdateEntry -> {
|
is PasskeyLauncherViewModel.UIState.UpdateEntry -> {
|
||||||
updateEntry(uiState.oldEntry, uiState.newEntry)
|
updateEntry(uiState.oldEntry, uiState.newEntry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
passkeyLauncherViewModel.credentialUiState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
is CredentialLauncherViewModel.UIState.Loading -> {}
|
||||||
|
is CredentialLauncherViewModel.UIState.SetActivityResult -> {
|
||||||
|
setActivityResult(
|
||||||
|
lockDatabase = uiState.lockDatabase,
|
||||||
|
resultCode = uiState.resultCode,
|
||||||
|
data = uiState.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.UIState.ShowError -> {
|
||||||
|
toastError(uiState.error)
|
||||||
|
passkeyLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection -> {
|
||||||
|
GroupActivity.launchForSelection(
|
||||||
|
context = this@PasskeyLauncherActivity,
|
||||||
|
database = uiState.database,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
searchInfo = uiState.searchInfo,
|
||||||
|
activityResultLauncher = mPasskeySelectionActivityResultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> {
|
||||||
|
GroupActivity.launchForRegistration(
|
||||||
|
context = this@PasskeyLauncherActivity,
|
||||||
|
database = uiState.database,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
registerInfo = uiState.registerInfo,
|
||||||
|
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> {
|
||||||
|
FileDatabaseSelectActivity.launchForSelection(
|
||||||
|
context = this@PasskeyLauncherActivity,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
searchInfo = uiState.searchInfo,
|
||||||
|
activityResultLauncher = mPasskeySelectionActivityResultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration -> {
|
||||||
|
FileDatabaseSelectActivity.launchForRegistration(
|
||||||
|
context = this@PasskeyLauncherActivity,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
registerInfo = uiState.registerInfo,
|
||||||
|
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onUnknownDatabaseRetrieved(database)
|
||||||
passkeyLauncherViewModel.launchPasskeyActionIfNeeded(intent, mSpecialMode, database)
|
passkeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
@@ -170,7 +176,8 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
|||||||
super.onDatabaseActionFinished(database, actionTask, result)
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||||
passkeyLauncherViewModel.autoSelectPasskey(result, database)
|
// TODO When auto save is enabled, WARNING filter by the calling activity
|
||||||
|
// passkeyLauncherViewModel.autoSelectPasskey(result, database)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,6 +241,8 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
|||||||
)
|
)
|
||||||
.append("\n\n")
|
.append("\n\n")
|
||||||
.append(getString(R.string.passkeys_missing_signature_app_ask_explanation))
|
.append(getString(R.string.passkeys_missing_signature_app_ask_explanation))
|
||||||
|
.append("\n\n")
|
||||||
|
.append(getString(R.string.passkeys_missing_signature_app_ask_question))
|
||||||
.toString()
|
.toString()
|
||||||
)
|
)
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
@@ -273,7 +282,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
|||||||
): PendingIntent? {
|
): PendingIntent? {
|
||||||
return PendingIntent.getActivity(
|
return PendingIntent.getActivity(
|
||||||
context,
|
context,
|
||||||
(Math.random() * Integer.MAX_VALUE).toInt(),
|
randomRequestCode(),
|
||||||
Intent(context, PasskeyLauncherActivity::class.java).apply {
|
Intent(context, PasskeyLauncherActivity::class.java).apply {
|
||||||
addSpecialMode(specialMode)
|
addSpecialMode(specialMode)
|
||||||
addTypeMode(TypeMode.PASSKEY)
|
addTypeMode(TypeMode.PASSKEY)
|
||||||
|
|||||||
@@ -2,5 +2,7 @@ package com.kunzisoft.keepass.credentialprovider.autofill
|
|||||||
|
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
|
|
||||||
data class AutofillComponent(val assistStructure: AssistStructure,
|
data class AutofillComponent(
|
||||||
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)
|
val assistStructure: AssistStructure,
|
||||||
|
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?
|
||||||
|
)
|
||||||
@@ -20,7 +20,6 @@
|
|||||||
package com.kunzisoft.keepass.credentialprovider.autofill
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -38,7 +37,6 @@ import android.view.autofill.AutofillId
|
|||||||
import android.view.autofill.AutofillManager
|
import android.view.autofill.AutofillManager
|
||||||
import android.view.autofill.AutofillValue
|
import android.view.autofill.AutofillValue
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import android.widget.Toast
|
|
||||||
import android.widget.inline.InlinePresentationSpec
|
import android.widget.inline.InlinePresentationSpec
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.autofill.inline.UiVersions
|
import androidx.autofill.inline.UiVersions
|
||||||
@@ -54,21 +52,32 @@ 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.AppUtil.randomRequestCode
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import java.io.IOException
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
object AutofillHelper {
|
object AutofillHelper {
|
||||||
|
|
||||||
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
private const val EXTRA_BASE_STRUCTURE = "com.kunzisoft.keepass.autofill.BASE_STRUCTURE"
|
||||||
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
|
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
|
||||||
|
|
||||||
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
|
fun Intent.addAutofillComponent(autofillComponent: AutofillComponent) {
|
||||||
intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
|
this.putExtra(EXTRA_BASE_STRUCTURE, autofillComponent.assistStructure)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
autofillComponent.compatInlineSuggestionsRequest?.let {
|
||||||
|
this.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.retrieveAutofillComponent(): AutofillComponent? {
|
||||||
|
getParcelableExtraCompat<AssistStructure>(EXTRA_BASE_STRUCTURE)?.let { assistStructure ->
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
AutofillComponent(assistStructure,
|
AutofillComponent(assistStructure,
|
||||||
intent.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST))
|
getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST))
|
||||||
} else {
|
} else {
|
||||||
AutofillComponent(assistStructure, null)
|
AutofillComponent(assistStructure, null)
|
||||||
}
|
}
|
||||||
@@ -127,11 +136,13 @@ object AutofillHelper {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildDatasetForEntry(context: Context,
|
private fun buildDatasetForEntry(
|
||||||
|
context: Context,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
entryInfo: EntryInfo,
|
entryInfo: EntryInfo,
|
||||||
struct: StructureParser.Result,
|
struct: StructureParser.Result,
|
||||||
inlinePresentation: InlinePresentation?): Dataset {
|
inlinePresentation: InlinePresentation?
|
||||||
|
): Dataset {
|
||||||
val remoteViews: RemoteViews = newRemoteViews(context, database, makeEntryTitle(entryInfo), entryInfo.icon)
|
val remoteViews: RemoteViews = newRemoteViews(context, database, makeEntryTitle(entryInfo), entryInfo.icon)
|
||||||
|
|
||||||
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
@@ -291,11 +302,13 @@ object AutofillHelper {
|
|||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
private fun buildInlinePresentationForEntry(context: Context,
|
private fun buildInlinePresentationForEntry(
|
||||||
|
context: Context,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
|
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
|
||||||
positionItem: Int,
|
positionItem: Int,
|
||||||
entryInfo: EntryInfo): InlinePresentation? {
|
entryInfo: EntryInfo
|
||||||
|
): InlinePresentation? {
|
||||||
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||||
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
||||||
@@ -314,7 +327,7 @@ object AutofillHelper {
|
|||||||
// Build the content for IME UI
|
// Build the content for IME UI
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
context,
|
context,
|
||||||
0,
|
randomRequestCode(),
|
||||||
Intent(context, AutofillSettingsActivity::class.java),
|
Intent(context, AutofillSettingsActivity::class.java),
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
)
|
)
|
||||||
@@ -341,9 +354,11 @@ object AutofillHelper {
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
private fun buildInlinePresentationForManualSelection(context: Context,
|
private fun buildInlinePresentationForManualSelection(
|
||||||
|
context: Context,
|
||||||
inlinePresentationSpec: InlinePresentationSpec,
|
inlinePresentationSpec: InlinePresentationSpec,
|
||||||
pendingIntent: PendingIntent): InlinePresentation? {
|
pendingIntent: PendingIntent
|
||||||
|
): InlinePresentation? {
|
||||||
// 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))
|
||||||
@@ -360,11 +375,13 @@ object AutofillHelper {
|
|||||||
}.build().slice, inlinePresentationSpec, false)
|
}.build().slice, inlinePresentationSpec, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildResponse(context: Context,
|
fun buildResponse(
|
||||||
|
context: Context,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
entriesInfo: List<EntryInfo>,
|
entriesInfo: List<EntryInfo>,
|
||||||
parseResult: StructureParser.Result,
|
parseResult: StructureParser.Result,
|
||||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
|
autofillComponent: AutofillComponent
|
||||||
|
): 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) {
|
||||||
@@ -385,7 +402,8 @@ object AutofillHelper {
|
|||||||
// Add inline suggestion for new IME and dataset
|
// Add inline suggestion for new IME and dataset
|
||||||
var numberInlineSuggestions = 0
|
var numberInlineSuggestions = 0
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
autofillComponent.compatInlineSuggestionsRequest
|
||||||
|
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
|
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
|
||||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||||
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
|
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
|
||||||
@@ -401,21 +419,27 @@ object AutofillHelper {
|
|||||||
var inlinePresentation: InlinePresentation? = null
|
var inlinePresentation: InlinePresentation? = null
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
&& numberInlineSuggestions > 0
|
&& numberInlineSuggestions > 0
|
||||||
&& compatInlineSuggestionsRequest != null) {
|
&& autofillComponent.compatInlineSuggestionsRequest != null) {
|
||||||
inlinePresentation = buildInlinePresentationForEntry(
|
inlinePresentation = buildInlinePresentationForEntry(
|
||||||
context,
|
context,
|
||||||
database,
|
database,
|
||||||
compatInlineSuggestionsRequest,
|
autofillComponent.compatInlineSuggestionsRequest,
|
||||||
numberInlineSuggestions--,
|
numberInlineSuggestions--,
|
||||||
entry
|
entry
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Create dataset for each entry
|
// Create dataset for each entry
|
||||||
responseBuilder.addDataset(
|
responseBuilder.addDataset(
|
||||||
buildDatasetForEntry(context, database, entry, parseResult, inlinePresentation)
|
buildDatasetForEntry(
|
||||||
|
context = context,
|
||||||
|
database = database,
|
||||||
|
entryInfo = entry,
|
||||||
|
struct = parseResult,
|
||||||
|
inlinePresentation = inlinePresentation
|
||||||
|
)
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to add dataset")
|
Log.e(TAG, "Unable to add dataset", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,13 +451,20 @@ object AutofillHelper {
|
|||||||
webScheme = parseResult.webScheme
|
webScheme = parseResult.webScheme
|
||||||
manualSelection = true
|
manualSelection = true
|
||||||
}
|
}
|
||||||
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
|
val manualSelectionView = RemoteViews(
|
||||||
AutofillLauncherActivity.getPendingIntentForSelection(context,
|
context.packageName,
|
||||||
searchInfo, compatInlineSuggestionsRequest)?.let { pendingIntent ->
|
R.layout.item_autofill_select_entry
|
||||||
|
)
|
||||||
|
AutofillLauncherActivity.getPendingIntentForSelection(
|
||||||
|
context,
|
||||||
|
searchInfo,
|
||||||
|
autofillComponent
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
|
||||||
var inlinePresentation: InlinePresentation? = null
|
var inlinePresentation: InlinePresentation? = null
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
autofillComponent.compatInlineSuggestionsRequest
|
||||||
|
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
val inlinePresentationSpec =
|
val inlinePresentationSpec =
|
||||||
inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
||||||
inlinePresentation = buildInlinePresentationForManualSelection(
|
inlinePresentation = buildInlinePresentationForManualSelection(
|
||||||
@@ -486,61 +517,31 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the Autofill response for one entry
|
* Build the Autofill response
|
||||||
*/
|
*/
|
||||||
fun buildResponseAndSetResult(activity: Activity,
|
fun buildResponse(
|
||||||
|
context: Context,
|
||||||
|
autofillComponent: AutofillComponent,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
entryInfo: EntryInfo) {
|
entriesInfo: List<EntryInfo>,
|
||||||
buildResponseAndSetResult(activity, database, ArrayList<EntryInfo>().apply { add(entryInfo) })
|
onIntentCreated: (Intent) -> Unit
|
||||||
}
|
) {
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the Autofill response for many entry
|
|
||||||
*/
|
|
||||||
fun buildResponseAndSetResult(activity: Activity,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
entriesInfo: List<EntryInfo>) {
|
|
||||||
if (entriesInfo.isEmpty()) {
|
if (entriesInfo.isEmpty()) {
|
||||||
activity.setResult(Activity.RESULT_CANCELED)
|
throw IOException("No entries found")
|
||||||
} else {
|
} else {
|
||||||
var setResultOk = false
|
StructureParser(autofillComponent.assistStructure).parse()?.let { result ->
|
||||||
activity.intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { structure ->
|
|
||||||
StructureParser(structure).parse()?.let { result ->
|
|
||||||
// New Response
|
// New Response
|
||||||
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
onIntentCreated(Intent().putExtra(
|
||||||
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat<CompatInlineSuggestionsRequest>(
|
|
||||||
EXTRA_INLINE_SUGGESTIONS_REQUEST
|
|
||||||
)
|
|
||||||
if (compatInlineSuggestionsRequest != null) {
|
|
||||||
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
buildResponse(activity, database, entriesInfo, result, compatInlineSuggestionsRequest)
|
|
||||||
} else {
|
|
||||||
buildResponse(activity, database, entriesInfo, result, null)
|
|
||||||
}
|
|
||||||
val mReplyIntent = Intent()
|
|
||||||
Log.d(activity.javaClass.name, "Success Autofill auth.")
|
|
||||||
mReplyIntent.putExtra(
|
|
||||||
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
||||||
response)
|
buildResponse(
|
||||||
setResultOk = true
|
context = context,
|
||||||
activity.setResult(Activity.RESULT_OK, mReplyIntent)
|
database = database,
|
||||||
}
|
entriesInfo = entriesInfo,
|
||||||
}
|
parseResult = result,
|
||||||
if (!setResultOk) {
|
autofillComponent = autofillComponent
|
||||||
Log.w(activity.javaClass.name, "Failed Autofill auth.")
|
)
|
||||||
activity.setResult(Activity.RESULT_CANCELED)
|
))
|
||||||
}
|
} ?: throw IOException("Unable to parse the structure")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Intent.addAutofillComponent(context: Context, autofillComponent: AutofillComponent) {
|
|
||||||
this.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
|
||||||
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(context)) {
|
|
||||||
autofillComponent.compatInlineSuggestionsRequest?.let {
|
|
||||||
this.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ 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 com.kunzisoft.keepass.utils.AppUtil
|
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
|
|
||||||
@@ -92,10 +92,11 @@ class KeeAutofillService : AutofillService() {
|
|||||||
autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this)
|
autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFillRequest(request: FillRequest,
|
override fun onFillRequest(
|
||||||
|
request: FillRequest,
|
||||||
cancellationSignal: CancellationSignal,
|
cancellationSignal: CancellationSignal,
|
||||||
callback: FillCallback) {
|
callback: FillCallback
|
||||||
|
) {
|
||||||
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
|
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
|
||||||
|
|
||||||
if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) {
|
if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) {
|
||||||
@@ -120,67 +121,64 @@ class KeeAutofillService : AutofillService() {
|
|||||||
webDomain = parseResult.webDomain
|
webDomain = parseResult.webDomain
|
||||||
webScheme = parseResult.webScheme
|
webScheme = parseResult.webScheme
|
||||||
}
|
}
|
||||||
AppUtil.getConcreteWebDomain(this, 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)
|
CompatInlineSuggestionsRequest(request)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
launchSelection(mDatabase,
|
val autofillComponent = AutofillComponent(
|
||||||
searchInfo,
|
latestStructure,
|
||||||
parseResult,
|
inlineSuggestionsRequest
|
||||||
inlineSuggestionsRequest,
|
)
|
||||||
callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchSelection(database: ContextualDatabase?,
|
|
||||||
searchInfo: SearchInfo,
|
|
||||||
parseResult: StructureParser.Result,
|
|
||||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
|
||||||
callback: FillCallback) {
|
|
||||||
SearchHelper.checkAutoSearchInfo(
|
SearchHelper.checkAutoSearchInfo(
|
||||||
context = this,
|
context = this,
|
||||||
database = database,
|
database = mDatabase,
|
||||||
searchInfo = searchInfo,
|
searchInfo = searchInfo,
|
||||||
onItemsFound = { openedDatabase, items ->
|
onItemsFound = { openedDatabase, items ->
|
||||||
callback.onSuccess(
|
callback.onSuccess(
|
||||||
AutofillHelper.buildResponse(
|
AutofillHelper.buildResponse(
|
||||||
this, openedDatabase,
|
context = this,
|
||||||
items, parseResult, inlineSuggestionsRequest
|
database = openedDatabase,
|
||||||
|
entriesInfo = items,
|
||||||
|
parseResult = parseResult,
|
||||||
|
autofillComponent = autofillComponent
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onItemNotFound = { openedDatabase ->
|
onItemNotFound = { openedDatabase ->
|
||||||
// Show UI if no search result
|
// Show UI if no search result
|
||||||
showUIForEntrySelection(parseResult, openedDatabase,
|
showUIForEntrySelection(parseResult, openedDatabase,
|
||||||
searchInfo, inlineSuggestionsRequest, callback)
|
searchInfo, autofillComponent, callback)
|
||||||
},
|
},
|
||||||
onDatabaseClosed = {
|
onDatabaseClosed = {
|
||||||
// Show UI if database not open
|
// Show UI if database not open
|
||||||
showUIForEntrySelection(parseResult, null,
|
showUIForEntrySelection(parseResult, null,
|
||||||
searchInfo, inlineSuggestionsRequest, callback)
|
searchInfo, autofillComponent, callback)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
private fun showUIForEntrySelection(
|
||||||
|
parseResult: StructureParser.Result,
|
||||||
database: ContextualDatabase?,
|
database: ContextualDatabase?,
|
||||||
searchInfo: SearchInfo,
|
searchInfo: SearchInfo,
|
||||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
autofillComponent: AutofillComponent,
|
||||||
callback: FillCallback) {
|
callback: FillCallback
|
||||||
|
) {
|
||||||
var success = false
|
var success = false
|
||||||
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.
|
||||||
AutofillLauncherActivity.getPendingIntentForSelection(this,
|
AutofillLauncherActivity.getPendingIntentForSelection(
|
||||||
searchInfo, inlineSuggestionsRequest)?.intentSender?.let { intentSender ->
|
this,
|
||||||
|
searchInfo,
|
||||||
|
autofillComponent
|
||||||
|
)?.intentSender?.let { intentSender ->
|
||||||
val responseBuilder = FillResponse.Builder()
|
val responseBuilder = FillResponse.Builder()
|
||||||
val remoteViewsUnlock: RemoteViews = if (database == null) {
|
val remoteViewsUnlock: RemoteViews = if (database == null) {
|
||||||
if (!parseResult.webDomain.isNullOrEmpty()) {
|
if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||||
@@ -271,7 +269,8 @@ class KeeAutofillService : AutofillService() {
|
|||||||
&& autofillInlineSuggestionsEnabled
|
&& autofillInlineSuggestionsEnabled
|
||||||
) {
|
) {
|
||||||
var inlinePresentation: InlinePresentation? = null
|
var inlinePresentation: InlinePresentation? = null
|
||||||
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
autofillComponent.compatInlineSuggestionsRequest
|
||||||
|
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
val inlinePresentationSpecs =
|
val inlinePresentationSpecs =
|
||||||
inlineSuggestionsRequest.inlinePresentationSpecs
|
inlineSuggestionsRequest.inlinePresentationSpecs
|
||||||
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
||||||
@@ -289,7 +288,7 @@ class KeeAutofillService : AutofillService() {
|
|||||||
InlineSuggestionUi.newContentBuilder(
|
InlineSuggestionUi.newContentBuilder(
|
||||||
PendingIntent.getActivity(
|
PendingIntent.getActivity(
|
||||||
this,
|
this,
|
||||||
0,
|
randomRequestCode(),
|
||||||
Intent(this, AutofillSettingsActivity::class.java),
|
Intent(this, AutofillSettingsActivity::class.java),
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
)
|
)
|
||||||
@@ -361,7 +360,7 @@ class KeeAutofillService : AutofillService() {
|
|||||||
|
|
||||||
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
|
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
|
||||||
var success = false
|
var success = false
|
||||||
if (askToSaveData) {
|
if (askToSaveData && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
val latestStructure = request.fillContexts.last().structure
|
val latestStructure = request.fillContexts.last().structure
|
||||||
StructureParser(latestStructure).parse(true)?.let { parseResult ->
|
StructureParser(latestStructure).parse(true)?.let { parseResult ->
|
||||||
|
|
||||||
@@ -387,32 +386,32 @@ class KeeAutofillService : AutofillService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show UI to save data
|
// Show UI to save data
|
||||||
val registerInfo = RegisterInfo(
|
val searchInfo = SearchInfo().apply {
|
||||||
searchInfo = SearchInfo().apply {
|
|
||||||
applicationId = parseResult.applicationId
|
applicationId = parseResult.applicationId
|
||||||
webDomain = parseResult.webDomain
|
webDomain = parseResult.webDomain
|
||||||
webScheme = parseResult.webScheme
|
webScheme = parseResult.webScheme
|
||||||
},
|
}
|
||||||
|
val registerInfo = RegisterInfo(
|
||||||
|
searchInfo = searchInfo,
|
||||||
username = parseResult.usernameValue?.textValue?.toString(),
|
username = parseResult.usernameValue?.textValue?.toString(),
|
||||||
password = parseResult.passwordValue?.textValue?.toString(),
|
password = parseResult.passwordValue?.textValue?.toString(),
|
||||||
creditCard =
|
creditCard = parseResult.creditCardNumber?.let { cardNumber ->
|
||||||
CreditCard(
|
CreditCard(
|
||||||
parseResult.creditCardHolder,
|
parseResult.creditCardHolder,
|
||||||
parseResult.creditCardNumber,
|
cardNumber,
|
||||||
expiration,
|
expiration,
|
||||||
parseResult.cardVerificationValue
|
parseResult.cardVerificationValue
|
||||||
)
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO Callback in each activity #765
|
AutofillLauncherActivity.getPendingIntentForRegistration(
|
||||||
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
this,
|
||||||
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
|
registerInfo
|
||||||
// registerInfo))
|
)?.intentSender?.let { intentSender ->
|
||||||
//} else {
|
|
||||||
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
|
|
||||||
success = true
|
success = true
|
||||||
callback.onSuccess()
|
callback.onSuccess(intentSender)
|
||||||
//}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -362,9 +362,9 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
if (result?.passwordId == null) {
|
if (result?.passwordId == null) {
|
||||||
usernameIdCandidate = autofillId
|
usernameIdCandidate = 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,
|
inputIsVariationType(inputType,
|
||||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
|
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
|
||||||
// Some forms used visible password as username
|
// Some forms used visible password as username
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
|
||||||
import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity
|
import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
@@ -484,7 +485,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
// Populate Magikeyboard with entry
|
// Populate Magikeyboard with entry
|
||||||
addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
||||||
// Consume the selection mode
|
// Consume the selection mode
|
||||||
EntrySelectionHelper.removeModesFromIntent(activity.intent)
|
activity.intent.removeModes()
|
||||||
activity.moveTaskToBack(true)
|
activity.moveTaskToBack(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,22 +93,18 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
private fun buildPasskeySearchInfo(relyingParty: String): SearchInfo {
|
private fun buildPasskeySearchInfo(relyingParty: String): SearchInfo {
|
||||||
return SearchInfo().apply {
|
return SearchInfo().apply {
|
||||||
this.relyingParty = relyingParty
|
this.relyingParty = relyingParty
|
||||||
this.isAPasskeySearch = true
|
|
||||||
this.query = relyingParty
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBeginGetCredentialRequest(
|
override fun onBeginGetCredentialRequest(
|
||||||
request: BeginGetCredentialRequest,
|
request: BeginGetCredentialRequest,
|
||||||
cancellationSignal: CancellationSignal,
|
cancellationSignal: CancellationSignal,
|
||||||
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
|
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>
|
||||||
) {
|
) {
|
||||||
Log.d(javaClass.simpleName, "onBeginGetCredentialRequest called")
|
Log.d(javaClass.simpleName, "onBeginGetCredentialRequest called")
|
||||||
try {
|
try {
|
||||||
processGetCredentialsRequest(request)?.let { response ->
|
processGetCredentialsRequest(request) { response ->
|
||||||
callback.onResult(response)
|
callback.onResult(response)
|
||||||
} ?: run {
|
|
||||||
callback.onError(GetCredentialUnknownException())
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(javaClass.simpleName, "onBeginGetCredentialRequest error", e)
|
Log.e(javaClass.simpleName, "onBeginGetCredentialRequest error", e)
|
||||||
@@ -116,24 +112,30 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processGetCredentialsRequest(request: BeginGetCredentialRequest): BeginGetCredentialResponse? {
|
private fun processGetCredentialsRequest(
|
||||||
val credentialEntries: MutableList<CredentialEntry> = mutableListOf()
|
request: BeginGetCredentialRequest,
|
||||||
|
callback: (BeginGetCredentialResponse?) -> Unit
|
||||||
|
) {
|
||||||
|
var knownOption = false
|
||||||
for (option in request.beginGetCredentialOptions) {
|
for (option in request.beginGetCredentialOptions) {
|
||||||
when (option) {
|
when (option) {
|
||||||
is BeginGetPublicKeyCredentialOption -> {
|
is BeginGetPublicKeyCredentialOption -> {
|
||||||
credentialEntries.addAll(
|
knownOption = true
|
||||||
populatePasskeyData(option)
|
populatePasskeyData(option) { listCredentials ->
|
||||||
)
|
callback(BeginGetCredentialResponse(listCredentials))
|
||||||
return BeginGetCredentialResponse(credentialEntries)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.w(javaClass.simpleName, "unknown beginGetCredentialOption")
|
}
|
||||||
return null
|
if (knownOption.not()) {
|
||||||
|
throw IOException("unknown type of beginGetCredentialOption")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun populatePasskeyData(option: BeginGetPublicKeyCredentialOption): List<CredentialEntry> {
|
private fun populatePasskeyData(
|
||||||
|
option: BeginGetPublicKeyCredentialOption,
|
||||||
|
callback: (List<CredentialEntry>) -> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
|
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
|
||||||
|
|
||||||
@@ -169,6 +171,7 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
callback(passkeyEntries)
|
||||||
},
|
},
|
||||||
onItemNotFound = { _ ->
|
onItemNotFound = { _ ->
|
||||||
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
|
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
|
||||||
@@ -191,6 +194,7 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
callback(passkeyEntries)
|
||||||
},
|
},
|
||||||
onDatabaseClosed = {
|
onDatabaseClosed = {
|
||||||
Log.d(TAG, "Add pending intent for passkey selection in closed database")
|
Log.d(TAG, "Add pending intent for passkey selection in closed database")
|
||||||
@@ -213,9 +217,9 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
callback(passkeyEntries)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return passkeyEntries
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBeginCreateCredentialRequest(
|
override fun onBeginCreateCredentialRequest(
|
||||||
@@ -225,7 +229,9 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
) {
|
) {
|
||||||
Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called")
|
Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called")
|
||||||
try {
|
try {
|
||||||
callback.onResult(processCreateCredentialRequest(request))
|
processCreateCredentialRequest(request) {
|
||||||
|
callback.onResult(BeginCreateCredentialResponse(it))
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e)
|
Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e)
|
||||||
toastError(e)
|
toastError(e)
|
||||||
@@ -233,16 +239,21 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse {
|
private fun processCreateCredentialRequest(
|
||||||
|
request: BeginCreateCredentialRequest,
|
||||||
|
callback: (List<CreateEntry>) -> Unit
|
||||||
|
) {
|
||||||
when (request) {
|
when (request) {
|
||||||
is BeginCreatePublicKeyCredentialRequest -> {
|
is BeginCreatePublicKeyCredentialRequest -> {
|
||||||
// Request is passkey type
|
// Request is passkey type
|
||||||
return handleCreatePasskeyQuery(request)
|
handleCreatePasskeyQuery(request, callback)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else -> {
|
||||||
// request type not supported
|
// request type not supported
|
||||||
throw IOException("unknown type of BeginCreateCredentialRequest")
|
throw IOException("unknown type of BeginCreateCredentialRequest")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun MutableList<CreateEntry>.addPendingIntentCreationNewEntry(
|
private fun MutableList<CreateEntry>.addPendingIntentCreationNewEntry(
|
||||||
accountName: String,
|
accountName: String,
|
||||||
@@ -266,9 +277,15 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleCreatePasskeyQuery(request: BeginCreatePublicKeyCredentialRequest): BeginCreateCredentialResponse {
|
private fun handleCreatePasskeyQuery(
|
||||||
|
request: BeginCreatePublicKeyCredentialRequest,
|
||||||
val accountName = mDatabase?.name ?: getString(R.string.passkey_database_username)
|
callback: (List<CreateEntry>) -> Unit
|
||||||
|
) {
|
||||||
|
val databaseName = mDatabase?.name
|
||||||
|
val accountName =
|
||||||
|
if (databaseName?.isBlank() != false)
|
||||||
|
getString(R.string.passkey_database_username)
|
||||||
|
else databaseName
|
||||||
val createEntries: MutableList<CreateEntry> = mutableListOf()
|
val createEntries: MutableList<CreateEntry> = mutableListOf()
|
||||||
val relyingPartyId = PublicKeyCredentialCreationOptions(
|
val relyingPartyId = PublicKeyCredentialCreationOptions(
|
||||||
requestJson = request.requestJson,
|
requestJson = request.requestJson,
|
||||||
@@ -309,6 +326,7 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
}
|
}
|
||||||
|
callback(createEntries)
|
||||||
},
|
},
|
||||||
onItemNotFound = { database ->
|
onItemNotFound = { database ->
|
||||||
// To create a new entry
|
// To create a new entry
|
||||||
@@ -317,6 +335,7 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
} else {
|
} else {
|
||||||
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
|
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
|
||||||
}
|
}
|
||||||
|
callback(createEntries)
|
||||||
},
|
},
|
||||||
onDatabaseClosed = {
|
onDatabaseClosed = {
|
||||||
// Launch the passkey launcher activity to open the database
|
// Launch the passkey launcher activity to open the database
|
||||||
@@ -334,10 +353,9 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
callback(createEntries)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return BeginCreateCredentialResponse(createEntries)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClearCredentialStateRequest(
|
override fun onClearCredentialStateRequest(
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.ParcelUuid
|
|
||||||
import android.security.keystore.KeyGenParameterSpec
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
import android.security.keystore.KeyProperties
|
import android.security.keystore.KeyProperties
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -44,6 +43,7 @@ import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
|
|||||||
import com.kunzisoft.encrypt.Signature
|
import com.kunzisoft.encrypt.Signature
|
||||||
import com.kunzisoft.encrypt.Signature.getApplicationFingerprints
|
import com.kunzisoft.encrypt.Signature.getApplicationFingerprints
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addNodeId
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
|
||||||
@@ -60,7 +60,6 @@ import com.kunzisoft.keepass.model.AndroidOrigin
|
|||||||
import com.kunzisoft.keepass.model.AppOrigin
|
import com.kunzisoft.keepass.model.AppOrigin
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.Passkey
|
import com.kunzisoft.keepass.model.Passkey
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
|
||||||
import com.kunzisoft.keepass.utils.AppUtil
|
import com.kunzisoft.keepass.utils.AppUtil
|
||||||
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
@@ -88,10 +87,7 @@ object PasskeyHelper {
|
|||||||
|
|
||||||
private const val HMAC_TYPE = "HmacSHA256"
|
private const val HMAC_TYPE = "HmacSHA256"
|
||||||
|
|
||||||
|
|
||||||
private const val EXTRA_SEARCH_INFO = "com.kunzisoft.keepass.extra.searchInfo"
|
|
||||||
private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
|
private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
|
||||||
private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.nodeId"
|
|
||||||
private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp"
|
private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp"
|
||||||
private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode"
|
private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode"
|
||||||
|
|
||||||
@@ -110,38 +106,6 @@ object PasskeyHelper {
|
|||||||
|
|
||||||
private val internalSecureRandom: SecureRandom = SecureRandom()
|
private val internalSecureRandom: SecureRandom = SecureRandom()
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the Passkey response for one entry
|
|
||||||
*/
|
|
||||||
fun Activity.buildPasskeyResponseAndSetResult(
|
|
||||||
entryInfo: EntryInfo,
|
|
||||||
extras: Bundle? = null
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
entryInfo.passkey?.let { passkey ->
|
|
||||||
val mReplyIntent = Intent()
|
|
||||||
Log.d(javaClass.name, "Success Passkey manual selection")
|
|
||||||
mReplyIntent.addPasskey(passkey)
|
|
||||||
mReplyIntent.addAppOrigin(entryInfo.appOrigin)
|
|
||||||
mReplyIntent.addNodeId(entryInfo.id)
|
|
||||||
extras?.let {
|
|
||||||
mReplyIntent.putExtras(it)
|
|
||||||
}
|
|
||||||
setResult(Activity.RESULT_OK, mReplyIntent)
|
|
||||||
} ?: run {
|
|
||||||
throw IOException("No passkey found")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(javaClass.name, "Unable to add the passkey as result", e)
|
|
||||||
Toast.makeText(
|
|
||||||
this,
|
|
||||||
getString(R.string.error_passkey_result),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
setResult(Activity.RESULT_CANCELED)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an authentication code generated by an entry to the intent
|
* Add an authentication code generated by an entry to the intent
|
||||||
*/
|
*/
|
||||||
@@ -181,22 +145,6 @@ object PasskeyHelper {
|
|||||||
return this.removeExtra(EXTRA_PASSKEY)
|
return this.removeExtra(EXTRA_PASSKEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the search info to the intent
|
|
||||||
*/
|
|
||||||
fun Intent.addSearchInfo(searchInfo: SearchInfo?) {
|
|
||||||
searchInfo?.let {
|
|
||||||
putExtra(EXTRA_SEARCH_INFO, searchInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the search info from the intent
|
|
||||||
*/
|
|
||||||
fun Intent.retrieveSearchInfo(): SearchInfo? {
|
|
||||||
return this.getParcelableExtraCompat(EXTRA_SEARCH_INFO)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the app origin to the intent
|
* Add the app origin to the intent
|
||||||
*/
|
*/
|
||||||
@@ -221,19 +169,35 @@ object PasskeyHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the node id to the intent, useful for auto passkey selection
|
* Build the Passkey response for one entry
|
||||||
*/
|
*/
|
||||||
fun Intent.addNodeId(nodeId: UUID?) {
|
fun Activity.buildPasskeyResponseAndSetResult(
|
||||||
nodeId?.let {
|
entryInfo: EntryInfo,
|
||||||
putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId))
|
extras: Bundle? = null
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
entryInfo.passkey?.let { passkey ->
|
||||||
|
val mReplyIntent = Intent()
|
||||||
|
Log.d(javaClass.name, "Success Passkey manual selection")
|
||||||
|
mReplyIntent.addPasskey(passkey)
|
||||||
|
mReplyIntent.addAppOrigin(entryInfo.appOrigin)
|
||||||
|
mReplyIntent.addNodeId(entryInfo.id)
|
||||||
|
extras?.let {
|
||||||
|
mReplyIntent.putExtras(it)
|
||||||
}
|
}
|
||||||
|
setResult(Activity.RESULT_OK, mReplyIntent)
|
||||||
|
} ?: run {
|
||||||
|
throw IOException("No passkey found")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(javaClass.name, "Unable to add the passkey as result", e)
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
getString(R.string.error_passkey_result),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the node id from the intent
|
|
||||||
*/
|
|
||||||
fun Intent.retrieveNodeId(): UUID? {
|
|
||||||
return getParcelableExtraCompat<ParcelUuid>(EXTRA_NODE_ID)?.uuid
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -424,11 +388,15 @@ object PasskeyHelper {
|
|||||||
* Utility method to create a passkey and the associated creation request parameters
|
* Utility method to create a passkey and the associated creation request parameters
|
||||||
* [intent] allows to retrieve the request
|
* [intent] allows to retrieve the request
|
||||||
* [context] context to manage package verification files
|
* [context] context to manage package verification files
|
||||||
|
* [defaultBackupEligibility] the default backup eligibility to add the the passkey entry
|
||||||
|
* [defaultBackupState] the default backup state to add the the passkey entry
|
||||||
* [passkeyCreated] is called asynchronously when the passkey has been created
|
* [passkeyCreated] is called asynchronously when the passkey has been created
|
||||||
*/
|
*/
|
||||||
suspend fun retrievePasskeyCreationRequestParameters(
|
suspend fun retrievePasskeyCreationRequestParameters(
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
context: Context,
|
context: Context,
|
||||||
|
defaultBackupEligibility: Boolean?,
|
||||||
|
defaultBackupState: Boolean?,
|
||||||
passkeyCreated: suspend (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit
|
passkeyCreated: suspend (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit
|
||||||
) {
|
) {
|
||||||
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
|
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
|
||||||
@@ -456,7 +424,9 @@ object PasskeyHelper {
|
|||||||
privateKeyPem = privateKeyPem,
|
privateKeyPem = privateKeyPem,
|
||||||
credentialId = b64Encode(credentialId),
|
credentialId = b64Encode(credentialId),
|
||||||
userHandle = b64Encode(userHandle),
|
userHandle = b64Encode(userHandle),
|
||||||
relyingParty = relyingParty
|
relyingParty = relyingParty,
|
||||||
|
backupEligibility = defaultBackupEligibility,
|
||||||
|
backupState = defaultBackupState
|
||||||
)
|
)
|
||||||
|
|
||||||
// create new entry in database
|
// create new entry in database
|
||||||
@@ -590,8 +560,8 @@ object PasskeyHelper {
|
|||||||
requestOptions: PublicKeyCredentialRequestOptions,
|
requestOptions: PublicKeyCredentialRequestOptions,
|
||||||
clientDataResponse: ClientDataResponse,
|
clientDataResponse: ClientDataResponse,
|
||||||
passkey: Passkey,
|
passkey: Passkey,
|
||||||
backupEligibility: Boolean,
|
defaultBackupEligibility: Boolean,
|
||||||
backupState: Boolean
|
defaultBackupState: Boolean
|
||||||
): PublicKeyCredential {
|
): PublicKeyCredential {
|
||||||
val getCredentialResponse = FidoPublicKeyCredential(
|
val getCredentialResponse = FidoPublicKeyCredential(
|
||||||
id = passkey.credentialId,
|
id = passkey.credentialId,
|
||||||
@@ -599,8 +569,8 @@ object PasskeyHelper {
|
|||||||
requestOptions = requestOptions,
|
requestOptions = requestOptions,
|
||||||
userPresent = true,
|
userPresent = true,
|
||||||
userVerified = true,
|
userVerified = true,
|
||||||
backupEligibility = backupEligibility,
|
backupEligibility = passkey.backupEligibility ?: defaultBackupEligibility,
|
||||||
backupState = backupState,
|
backupState = passkey.backupState ?: defaultBackupState,
|
||||||
userHandle = passkey.userHandle,
|
userHandle = passkey.userHandle,
|
||||||
privateKey = passkey.privateKeyPem,
|
privateKey = passkey.privateKeyPem,
|
||||||
clientDataResponse = clientDataResponse
|
clientDataResponse = clientDataResponse
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
package com.kunzisoft.keepass.credentialprovider.viewmodel
|
||||||
|
|
||||||
|
import android.app.Activity.RESULT_CANCELED
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeNodesIds
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveNodesIds
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.retrieveAutofillComponent
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
|
class AutofillLauncherViewModel(application: Application): CredentialLauncherViewModel(application) {
|
||||||
|
|
||||||
|
private var mAutofillComponent: AutofillComponent? = null
|
||||||
|
|
||||||
|
private var mLockDatabaseAfterSelection: Boolean = false
|
||||||
|
|
||||||
|
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
|
||||||
|
val uiState: StateFlow<UIState> = mUiState
|
||||||
|
|
||||||
|
fun initialize() {
|
||||||
|
mLockDatabaseAfterSelection = PreferencesUtil.isAutofillCloseDatabaseEnable(getApplication())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResult() {
|
||||||
|
super.onResult()
|
||||||
|
mAutofillComponent = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun launchAction(
|
||||||
|
intent: Intent,
|
||||||
|
specialMode: SpecialMode,
|
||||||
|
database: ContextualDatabase?
|
||||||
|
) {
|
||||||
|
// Retrieve selection mode
|
||||||
|
when (intent.retrieveSpecialMode()) {
|
||||||
|
SpecialMode.SELECTION -> {
|
||||||
|
val searchInfo = intent.retrieveSearchInfo()
|
||||||
|
if (searchInfo == null)
|
||||||
|
throw IOException("Search info is null")
|
||||||
|
mAutofillComponent = intent.retrieveAutofillComponent()
|
||||||
|
// Build search param
|
||||||
|
launchSelection(database, mAutofillComponent, searchInfo)
|
||||||
|
}
|
||||||
|
SpecialMode.REGISTRATION -> {
|
||||||
|
// To register info
|
||||||
|
val registerInfo = intent.retrieveRegisterInfo()
|
||||||
|
if (registerInfo == null)
|
||||||
|
throw IOException("Register info is null")
|
||||||
|
launchRegistration(database, registerInfo)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Not an autofill call
|
||||||
|
cancelResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun launchSelection(
|
||||||
|
database: ContextualDatabase?,
|
||||||
|
autofillComponent: AutofillComponent?,
|
||||||
|
searchInfo: SearchInfo
|
||||||
|
) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
if (autofillComponent == null) {
|
||||||
|
throw IOException("Autofill component is null")
|
||||||
|
}
|
||||||
|
if (KeeAutofillService.autofillAllowedFor(
|
||||||
|
applicationId = searchInfo.applicationId,
|
||||||
|
webDomain = searchInfo.webDomain,
|
||||||
|
context = getApplication()
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// If database is open
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = getApplication(),
|
||||||
|
database = database,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { openedDatabase, items ->
|
||||||
|
// Items found
|
||||||
|
if (autofillComponent.compatInlineSuggestionsRequest != null) {
|
||||||
|
mUiState.value = UIState.ShowAutofillSuggestionMessage
|
||||||
|
}
|
||||||
|
AutofillHelper.buildResponse(
|
||||||
|
context = getApplication(),
|
||||||
|
autofillComponent = autofillComponent,
|
||||||
|
database = openedDatabase,
|
||||||
|
entriesInfo = items
|
||||||
|
) { intent ->
|
||||||
|
setResult(intent, lockDatabase = mLockDatabaseAfterSelection)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onItemNotFound = { openedDatabase ->
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
mCredentialUiState.value =
|
||||||
|
CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection(
|
||||||
|
database = openedDatabase,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
// If database not open
|
||||||
|
mCredentialUiState.value =
|
||||||
|
CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection(
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
mUiState.value = UIState.ShowBlockRestartMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun manageSelectionResult(
|
||||||
|
database: ContextualDatabase,
|
||||||
|
activityResult: ActivityResult
|
||||||
|
) {
|
||||||
|
super.manageSelectionResult(database, activityResult)
|
||||||
|
val intent = activityResult.data
|
||||||
|
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
||||||
|
Log.e(TAG, "Unable to create selection response for autofill", e)
|
||||||
|
showError(e)
|
||||||
|
}) {
|
||||||
|
when (activityResult.resultCode) {
|
||||||
|
RESULT_OK -> {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
Log.d(TAG, "Autofill selection result")
|
||||||
|
if (intent == null)
|
||||||
|
throw IOException("Intent is null")
|
||||||
|
val nodesIds = intent.retrieveNodesIds()
|
||||||
|
?: throw IOException("NodesIds is null")
|
||||||
|
intent.removeNodesIds()
|
||||||
|
val autofillComponent = mAutofillComponent
|
||||||
|
if (autofillComponent == null)
|
||||||
|
throw IOException("Autofill component is null")
|
||||||
|
val entries = nodesIds.mapNotNull { nodeId ->
|
||||||
|
database
|
||||||
|
.getEntryById(NodeIdUUID(nodeId))
|
||||||
|
?.getEntryInfo(database)
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
AutofillHelper.buildResponse(
|
||||||
|
context = getApplication(),
|
||||||
|
autofillComponent = autofillComponent,
|
||||||
|
database = database,
|
||||||
|
entriesInfo = entries
|
||||||
|
) { intent ->
|
||||||
|
setResult(intent, lockDatabase = mLockDatabaseAfterSelection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RESULT_CANCELED -> {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
cancelResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------
|
||||||
|
// Registration
|
||||||
|
// -------------
|
||||||
|
|
||||||
|
private fun launchRegistration(
|
||||||
|
database: ContextualDatabase?,
|
||||||
|
registerInfo: RegisterInfo
|
||||||
|
) {
|
||||||
|
val searchInfo = registerInfo.searchInfo
|
||||||
|
if (KeeAutofillService.autofillAllowedFor(
|
||||||
|
applicationId = searchInfo.applicationId,
|
||||||
|
webDomain = searchInfo.webDomain,
|
||||||
|
context = getApplication()
|
||||||
|
)) {
|
||||||
|
val readOnly = database?.isReadOnly != false
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = getApplication(),
|
||||||
|
database = database,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { openedDatabase, _ ->
|
||||||
|
if (!readOnly) {
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
mCredentialUiState.value =
|
||||||
|
CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
|
||||||
|
database = openedDatabase,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
mUiState.value = UIState.ShowReadOnlyMessage
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onItemNotFound = { openedDatabase ->
|
||||||
|
if (!readOnly) {
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
mCredentialUiState.value =
|
||||||
|
CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
|
||||||
|
database = openedDatabase,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
mUiState.value = UIState.ShowReadOnlyMessage
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
// If database not open
|
||||||
|
mCredentialUiState.value =
|
||||||
|
CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration(
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
mUiState.value = UIState.ShowBlockRestartMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun manageRegistrationResult(activityResult: ActivityResult) {
|
||||||
|
isResultLauncherRegistered = false
|
||||||
|
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
||||||
|
Log.e(TAG, "Unable to create registration response for autofill", e)
|
||||||
|
showError(e)
|
||||||
|
}) {
|
||||||
|
val responseIntent = Intent()
|
||||||
|
when (activityResult.resultCode) {
|
||||||
|
RESULT_OK -> {
|
||||||
|
Log.d(TAG, "Autofill registration result")
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
setResult(responseIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RESULT_CANCELED -> {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
cancelResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class UIState {
|
||||||
|
object Loading: UIState()
|
||||||
|
object ShowBlockRestartMessage: UIState()
|
||||||
|
object ShowReadOnlyMessage: UIState()
|
||||||
|
object ShowAutofillSuggestionMessage: UIState()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = AutofillLauncherViewModel::class.java.name
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
package com.kunzisoft.keepass.credentialprovider.viewmodel
|
||||||
|
|
||||||
|
import android.app.Activity.RESULT_CANCELED
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
abstract class CredentialLauncherViewModel(application: Application): AndroidViewModel(application) {
|
||||||
|
|
||||||
|
protected var mDatabase: ContextualDatabase? = null
|
||||||
|
|
||||||
|
protected var isResultLauncherRegistered: Boolean = false
|
||||||
|
private var mSelectionResult: ActivityResult? = null
|
||||||
|
|
||||||
|
protected val mCredentialUiState = MutableStateFlow<UIState>(UIState.Loading)
|
||||||
|
val credentialUiState: StateFlow<UIState> = mCredentialUiState
|
||||||
|
|
||||||
|
fun showError(error: Throwable) {
|
||||||
|
Log.e(TAG, "Error on credential provider launch", error)
|
||||||
|
mCredentialUiState.value = UIState.ShowError(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onResult() {
|
||||||
|
isResultLauncherRegistered = false
|
||||||
|
mSelectionResult = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setResult(intent: Intent, lockDatabase: Boolean = false) {
|
||||||
|
// Remove the launcher register
|
||||||
|
onResult()
|
||||||
|
mCredentialUiState.value = UIState.SetActivityResult(
|
||||||
|
lockDatabase = lockDatabase,
|
||||||
|
resultCode = RESULT_OK,
|
||||||
|
data = intent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelResult(lockDatabase: Boolean = false) {
|
||||||
|
onResult()
|
||||||
|
mCredentialUiState.value = UIState.SetActivityResult(
|
||||||
|
lockDatabase = lockDatabase,
|
||||||
|
resultCode = RESULT_CANCELED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
|
mDatabase = database
|
||||||
|
mSelectionResult?.let { selectionResult ->
|
||||||
|
manageSelectionResult(database, selectionResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun manageSelectionResult(activityResult: ActivityResult) {
|
||||||
|
// Waiting for the database if needed
|
||||||
|
when (activityResult.resultCode) {
|
||||||
|
RESULT_OK -> {
|
||||||
|
mSelectionResult = activityResult
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
manageSelectionResult(database, activityResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RESULT_CANCELED -> {
|
||||||
|
cancelResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun manageSelectionResult(database: ContextualDatabase, activityResult: ActivityResult) {
|
||||||
|
mSelectionResult = null
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun manageRegistrationResult(activityResult: ActivityResult) {}
|
||||||
|
|
||||||
|
open fun onExceptionOccurred(e: Throwable) {
|
||||||
|
showError(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun launchActionIfNeeded(
|
||||||
|
intent: Intent,
|
||||||
|
specialMode: SpecialMode,
|
||||||
|
database: ContextualDatabase?
|
||||||
|
) {
|
||||||
|
if (database != null) {
|
||||||
|
onDatabaseRetrieved(database)
|
||||||
|
}
|
||||||
|
if (isResultLauncherRegistered.not()) {
|
||||||
|
isResultLauncherRegistered = true
|
||||||
|
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
||||||
|
onExceptionOccurred(e)
|
||||||
|
}) {
|
||||||
|
launchAction(intent, specialMode, database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch the main action
|
||||||
|
*/
|
||||||
|
protected abstract suspend fun launchAction(
|
||||||
|
intent: Intent,
|
||||||
|
specialMode: SpecialMode,
|
||||||
|
database: ContextualDatabase?
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed class UIState {
|
||||||
|
object Loading : UIState()
|
||||||
|
data class LaunchGroupActivityForSelection(
|
||||||
|
val database: ContextualDatabase,
|
||||||
|
val searchInfo: SearchInfo?,
|
||||||
|
val typeMode: TypeMode
|
||||||
|
): UIState()
|
||||||
|
data class LaunchGroupActivityForRegistration(
|
||||||
|
val database: ContextualDatabase,
|
||||||
|
val registerInfo: RegisterInfo?,
|
||||||
|
val typeMode: TypeMode
|
||||||
|
): UIState()
|
||||||
|
data class LaunchFileDatabaseSelectActivityForSelection(
|
||||||
|
val searchInfo: SearchInfo?,
|
||||||
|
val typeMode: TypeMode
|
||||||
|
): UIState()
|
||||||
|
data class LaunchFileDatabaseSelectActivityForRegistration(
|
||||||
|
val registerInfo: RegisterInfo?,
|
||||||
|
val typeMode: TypeMode
|
||||||
|
): UIState()
|
||||||
|
data class SetActivityResult(
|
||||||
|
val lockDatabase: Boolean,
|
||||||
|
val resultCode: Int,
|
||||||
|
val data: Intent? = null
|
||||||
|
): UIState()
|
||||||
|
data class ShowError(
|
||||||
|
val error: Throwable
|
||||||
|
): UIState()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = CredentialLauncherViewModel::class.java.name
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package com.kunzisoft.keepass.credentialprovider.viewmodel
|
||||||
|
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity.Companion.isHardwareKeyAvailable
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
class HardwareKeyLauncherViewModel(application: Application): CredentialLauncherViewModel(application) {
|
||||||
|
|
||||||
|
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
|
||||||
|
val uiState: StateFlow<UIState> = mUiState
|
||||||
|
|
||||||
|
override suspend fun launchAction(
|
||||||
|
intent: Intent,
|
||||||
|
specialMode: SpecialMode,
|
||||||
|
database: ContextualDatabase?
|
||||||
|
) {
|
||||||
|
val hardwareKey = HardwareKey.Companion.getHardwareKeyFromString(
|
||||||
|
intent.getStringExtra(DATA_HARDWARE_KEY)
|
||||||
|
)
|
||||||
|
if (isHardwareKeyAvailable(getApplication(), hardwareKey)) {
|
||||||
|
when (hardwareKey) {
|
||||||
|
/*
|
||||||
|
HardwareKey.FIDO2_SECRET -> {
|
||||||
|
// TODO FIDO2 under development
|
||||||
|
throw Exception("FIDO2 not implemented")
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
|
||||||
|
launchYubikeyChallengeForResponse(intent.getByteArrayExtra(DATA_SEED))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
UIState.OnChallengeResponded(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mUiState.value = UIState.ShowHardwareKeyDriverNeeded(hardwareKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchYubikeyChallengeForResponse(seed: ByteArray?) {
|
||||||
|
// Transform the seed before sending
|
||||||
|
var challenge: ByteArray? = null
|
||||||
|
if (seed != null) {
|
||||||
|
challenge = ByteArray(64)
|
||||||
|
seed.copyInto(challenge, 0, 0, 32)
|
||||||
|
challenge.fill(32, 32, 64)
|
||||||
|
}
|
||||||
|
mUiState.value = UIState.LaunchChallengeActivityForResponse(challenge)
|
||||||
|
Log.d(TAG, "Challenge sent")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun manageSelectionResult(
|
||||||
|
database: ContextualDatabase,
|
||||||
|
activityResult: ActivityResult
|
||||||
|
) {
|
||||||
|
super.manageSelectionResult(database, activityResult)
|
||||||
|
|
||||||
|
if (activityResult.resultCode == RESULT_OK) {
|
||||||
|
val challengeResponse: ByteArray? =
|
||||||
|
activityResult.data?.getByteArrayExtra(HARDWARE_KEY_RESPONSE_KEY)
|
||||||
|
Log.d(TAG, "Response form challenge")
|
||||||
|
mUiState.value = UIState.OnChallengeResponded(challengeResponse)
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Response from challenge error")
|
||||||
|
mUiState.value = UIState.OnChallengeResponded(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class UIState {
|
||||||
|
object Loading : UIState()
|
||||||
|
data class ShowHardwareKeyDriverNeeded(
|
||||||
|
val hardwareKey: HardwareKey?
|
||||||
|
): UIState()
|
||||||
|
data class LaunchChallengeActivityForResponse(
|
||||||
|
val challenge: ByteArray?,
|
||||||
|
): UIState() {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as LaunchChallengeActivityForResponse
|
||||||
|
|
||||||
|
return challenge.contentEquals(other.challenge)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return challenge?.contentHashCode() ?: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data class OnChallengeResponded(
|
||||||
|
val response: ByteArray?
|
||||||
|
): UIState() {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as OnChallengeResponded
|
||||||
|
|
||||||
|
return response.contentEquals(other.response)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return response?.contentHashCode() ?: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = HardwareKeyLauncherViewModel::class.java.name
|
||||||
|
|
||||||
|
private const val DATA_HARDWARE_KEY = "DATA_HARDWARE_KEY"
|
||||||
|
private const val DATA_SEED = "DATA_SEED"
|
||||||
|
|
||||||
|
// Driver call
|
||||||
|
private const val YUBIKEY_CHALLENGE_RESPONSE_INTENT = "android.yubikey.intent.action.CHALLENGE_RESPONSE"
|
||||||
|
private const val HARDWARE_KEY_CHALLENGE_KEY = "challenge"
|
||||||
|
private const val HARDWARE_KEY_RESPONSE_KEY = "response"
|
||||||
|
|
||||||
|
fun isYubikeyDriverAvailable(context: Context): Boolean {
|
||||||
|
return Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT)
|
||||||
|
.resolveActivity(context.packageManager) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildHardwareKeyChallenge(challenge: ByteArray?): Intent {
|
||||||
|
return Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply {
|
||||||
|
putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.addHardwareKey(hardwareKey: HardwareKey) {
|
||||||
|
putExtra(DATA_HARDWARE_KEY, hardwareKey.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.addSeed(seed: ByteArray?) {
|
||||||
|
putExtra(DATA_SEED, seed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,8 +11,11 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.credentials.GetCredentialResponse
|
import androidx.credentials.GetCredentialResponse
|
||||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||||
import androidx.credentials.provider.PendingIntentHandler
|
import androidx.credentials.provider.PendingIntentHandler
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeNodeId
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveNodeId
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
|
||||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||||
@@ -25,11 +28,9 @@ import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getVe
|
|||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveNodeId
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveSearchInfo
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
@@ -56,22 +57,21 @@ import java.io.InvalidObjectException
|
|||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
class PasskeyLauncherViewModel(application: Application): AndroidViewModel(application) {
|
class PasskeyLauncherViewModel(application: Application): CredentialLauncherViewModel(application) {
|
||||||
|
|
||||||
private var mUsageParameters: PublicKeyCredentialUsageParameters? = null
|
private var mUsageParameters: PublicKeyCredentialUsageParameters? = null
|
||||||
private var mCreationParameters: PublicKeyCredentialCreationParameters? = null
|
private var mCreationParameters: PublicKeyCredentialCreationParameters? = null
|
||||||
private var mPasskey: Passkey? = null
|
private var mPasskey: Passkey? = null
|
||||||
|
|
||||||
|
private var mLockDatabaseAfterSelection: Boolean = false
|
||||||
private var mBackupEligibility: Boolean = true
|
private var mBackupEligibility: Boolean = true
|
||||||
private var mBackupState: Boolean = false
|
private var mBackupState: Boolean = false
|
||||||
private var mLockDatabase: Boolean = true
|
|
||||||
|
|
||||||
private var isResultLauncherRegistered: Boolean = false
|
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
|
||||||
|
val uiState: StateFlow<UIState> = mUiState
|
||||||
private val _uiState = MutableStateFlow<UIState>(UIState.Loading)
|
|
||||||
val uiState: StateFlow<UIState> = _uiState
|
|
||||||
|
|
||||||
fun initialize() {
|
fun initialize() {
|
||||||
|
mLockDatabaseAfterSelection = PreferencesUtil.isPasskeyCloseDatabaseEnable(getApplication())
|
||||||
mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication())
|
mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication())
|
||||||
mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication())
|
mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication())
|
||||||
}
|
}
|
||||||
@@ -79,19 +79,14 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
fun showAppPrivilegedDialog(
|
fun showAppPrivilegedDialog(
|
||||||
temptingApp: AndroidPrivilegedApp
|
temptingApp: AndroidPrivilegedApp
|
||||||
) {
|
) {
|
||||||
_uiState.value = UIState.ShowAppPrivilegedDialog(temptingApp)
|
mUiState.value = UIState.ShowAppPrivilegedDialog(temptingApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showAppSignatureDialog(
|
fun showAppSignatureDialog(
|
||||||
temptingApp: AppOrigin,
|
temptingApp: AppOrigin,
|
||||||
nodeId: UUID
|
nodeId: UUID
|
||||||
) {
|
) {
|
||||||
_uiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId)
|
mUiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId)
|
||||||
}
|
|
||||||
|
|
||||||
fun showError(error: Throwable) {
|
|
||||||
Log.e(TAG, "Error on passkey launch", error)
|
|
||||||
_uiState.value = UIState.ShowError(error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveCustomPrivilegedApp(
|
fun saveCustomPrivilegedApp(
|
||||||
@@ -107,7 +102,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
context = getApplication(),
|
context = getApplication(),
|
||||||
privilegedApps = listOf(temptingApp)
|
privilegedApps = listOf(temptingApp)
|
||||||
)
|
)
|
||||||
launchPasskeyAction(
|
launchAction(
|
||||||
intent = intent,
|
intent = intent,
|
||||||
specialMode = specialMode,
|
specialMode = specialMode,
|
||||||
database = database
|
database = database
|
||||||
@@ -139,54 +134,33 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
)
|
)
|
||||||
entryInfo.saveAppOrigin(database, temptingApp)
|
entryInfo.saveAppOrigin(database, temptingApp)
|
||||||
newEntry.setEntryInfo(database, entryInfo)
|
newEntry.setEntryInfo(database, entryInfo)
|
||||||
_uiState.value = UIState.UpdateEntry(
|
mUiState.value = UIState.UpdateEntry(
|
||||||
oldEntry = entry,
|
oldEntry = entry,
|
||||||
newEntry = newEntry
|
newEntry = newEntry
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setResult(intent: Intent) {
|
override fun onExceptionOccurred(e: Throwable) {
|
||||||
// Remove the launcher register
|
if (e is PrivilegedAllowLists.PrivilegedException) {
|
||||||
isResultLauncherRegistered = false
|
showAppPrivilegedDialog(e.temptingApp)
|
||||||
_uiState.value = UIState.SetActivityResult(
|
} else {
|
||||||
lockDatabase = mLockDatabase,
|
super.onExceptionOccurred(e)
|
||||||
resultCode = RESULT_OK,
|
}
|
||||||
data = intent
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelResult() {
|
override fun launchActionIfNeeded(
|
||||||
isResultLauncherRegistered = false
|
|
||||||
_uiState.value = UIState.SetActivityResult(
|
|
||||||
lockDatabase = mLockDatabase,
|
|
||||||
resultCode = RESULT_CANCELED
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launchPasskeyActionIfNeeded(
|
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
specialMode: SpecialMode,
|
specialMode: SpecialMode,
|
||||||
database: ContextualDatabase?
|
database: ContextualDatabase?
|
||||||
) {
|
) {
|
||||||
if (isResultLauncherRegistered.not()) {
|
// Launch with database when a nodeId is present
|
||||||
isResultLauncherRegistered = true
|
if ((database != null && database.loaded) || intent.retrieveNodeId() == null) {
|
||||||
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
super.launchActionIfNeeded(intent, specialMode, database)
|
||||||
if (e is PrivilegedAllowLists.PrivilegedException) {
|
|
||||||
showAppPrivilegedDialog(e.temptingApp)
|
|
||||||
} else {
|
|
||||||
showError(e)
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
launchPasskeyAction(intent, specialMode, database)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override suspend fun launchAction(
|
||||||
* Launch the main action to manage Passkey
|
|
||||||
*/
|
|
||||||
private suspend fun launchPasskeyAction(
|
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
specialMode: SpecialMode,
|
specialMode: SpecialMode,
|
||||||
database: ContextualDatabase?
|
database: ContextualDatabase?
|
||||||
@@ -194,6 +168,9 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
|
val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
|
||||||
val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false)
|
val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false)
|
||||||
val nodeId = intent.retrieveNodeId()
|
val nodeId = intent.retrieveNodeId()
|
||||||
|
intent.removeInfo()
|
||||||
|
intent.removeAppOrigin()
|
||||||
|
intent.removeNodeId()
|
||||||
checkSecurity(intent, nodeId)
|
checkSecurity(intent, nodeId)
|
||||||
when (specialMode) {
|
when (specialMode) {
|
||||||
SpecialMode.SELECTION -> {
|
SpecialMode.SELECTION -> {
|
||||||
@@ -260,15 +237,19 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
TAG, "No Passkey found for selection," +
|
TAG, "No Passkey found for selection," +
|
||||||
"launch manual selection in opened database"
|
"launch manual selection in opened database"
|
||||||
)
|
)
|
||||||
_uiState.value = UIState.LaunchGroupActivityForSelection(
|
mCredentialUiState.value =
|
||||||
database = openedDatabase
|
CredentialLauncherViewModel.UIState.LaunchGroupActivityForSelection(
|
||||||
|
database = openedDatabase,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
typeMode = TypeMode.PASSKEY
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onDatabaseClosed = {
|
onDatabaseClosed = {
|
||||||
Log.d(TAG, "Manual passkey selection in closed database")
|
Log.d(TAG, "Manual passkey selection in closed database")
|
||||||
_uiState.value =
|
mCredentialUiState.value =
|
||||||
UIState.LaunchFileDatabaseSelectActivityForSelection(
|
CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection(
|
||||||
searchInfo = searchInfo
|
searchInfo = searchInfo,
|
||||||
|
typeMode = TypeMode.PASSKEY
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -326,12 +307,12 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
appOrigin = appOrigin
|
appOrigin = appOrigin
|
||||||
),
|
),
|
||||||
passkey = passkey,
|
passkey = passkey,
|
||||||
backupEligibility = mBackupEligibility,
|
defaultBackupEligibility = mBackupEligibility,
|
||||||
backupState = mBackupState
|
defaultBackupState = mBackupState
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
setResult(result)
|
setResult(result, lockDatabase = mLockDatabaseAfterSelection)
|
||||||
} catch (e: SignatureNotFoundException) {
|
} catch (e: SignatureNotFoundException) {
|
||||||
// Request the dialog if signature exception
|
// Request the dialog if signature exception
|
||||||
showAppSignatureDialog(e.temptingApp, nodeId)
|
showAppSignatureDialog(e.temptingApp, nodeId)
|
||||||
@@ -340,9 +321,11 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun manageSelectionResult(
|
override fun manageSelectionResult(
|
||||||
|
database: ContextualDatabase,
|
||||||
activityResult: ActivityResult
|
activityResult: ActivityResult
|
||||||
) {
|
) {
|
||||||
|
super.manageSelectionResult(database, activityResult)
|
||||||
val intent = activityResult.data
|
val intent = activityResult.data
|
||||||
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
||||||
Log.e(TAG, "Unable to create selection response for passkey", e)
|
Log.e(TAG, "Unable to create selection response for passkey", e)
|
||||||
@@ -380,8 +363,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
appOrigin = appOrigin
|
appOrigin = appOrigin
|
||||||
),
|
),
|
||||||
passkey = passkey,
|
passkey = passkey,
|
||||||
backupEligibility = mBackupEligibility,
|
defaultBackupEligibility = mBackupEligibility,
|
||||||
backupState = mBackupState
|
defaultBackupState = mBackupState
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -389,7 +372,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
throw IOException("Usage parameters is null")
|
throw IOException("Usage parameters is null")
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
setResult(responseIntent)
|
setResult(responseIntent, lockDatabase = mLockDatabaseAfterSelection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,6 +400,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
retrievePasskeyCreationRequestParameters(
|
retrievePasskeyCreationRequestParameters(
|
||||||
intent = intent,
|
intent = intent,
|
||||||
context = getApplication(),
|
context = getApplication(),
|
||||||
|
defaultBackupEligibility = mBackupEligibility,
|
||||||
|
defaultBackupState = mBackupState,
|
||||||
passkeyCreated = { passkey, appInfoToStore, publicKeyCredentialParameters ->
|
passkeyCreated = { passkey, appInfoToStore, publicKeyCredentialParameters ->
|
||||||
// Save the requested parameters
|
// Save the requested parameters
|
||||||
mPasskey = passkey
|
mPasskey = passkey
|
||||||
@@ -440,7 +425,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
TAG, "Passkey found for registration, " +
|
TAG, "Passkey found for registration, " +
|
||||||
"but launch manual registration for a new entry"
|
"but launch manual registration for a new entry"
|
||||||
)
|
)
|
||||||
_uiState.value = UIState.LaunchGroupActivityForRegistration(
|
mCredentialUiState.value =
|
||||||
|
CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
|
||||||
database = openedDatabase,
|
database = openedDatabase,
|
||||||
registerInfo = registerInfo,
|
registerInfo = registerInfo,
|
||||||
typeMode = TypeMode.PASSKEY
|
typeMode = TypeMode.PASSKEY
|
||||||
@@ -448,7 +434,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
},
|
},
|
||||||
onItemNotFound = { openedDatabase ->
|
onItemNotFound = { openedDatabase ->
|
||||||
Log.d(TAG, "Launch new manual registration in opened database")
|
Log.d(TAG, "Launch new manual registration in opened database")
|
||||||
_uiState.value = UIState.LaunchGroupActivityForRegistration(
|
mCredentialUiState.value =
|
||||||
|
CredentialLauncherViewModel.UIState.LaunchGroupActivityForRegistration(
|
||||||
database = openedDatabase,
|
database = openedDatabase,
|
||||||
registerInfo = registerInfo,
|
registerInfo = registerInfo,
|
||||||
typeMode = TypeMode.PASSKEY
|
typeMode = TypeMode.PASSKEY
|
||||||
@@ -456,8 +443,8 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
},
|
},
|
||||||
onDatabaseClosed = {
|
onDatabaseClosed = {
|
||||||
Log.d(TAG, "Manual passkey registration in closed database")
|
Log.d(TAG, "Manual passkey registration in closed database")
|
||||||
_uiState.value =
|
mCredentialUiState.value =
|
||||||
UIState.LaunchFileDatabaseSelectActivityForRegistration(
|
CredentialLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration(
|
||||||
registerInfo = registerInfo,
|
registerInfo = registerInfo,
|
||||||
typeMode = TypeMode.PASSKEY
|
typeMode = TypeMode.PASSKEY
|
||||||
)
|
)
|
||||||
@@ -490,7 +477,7 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun manageRegistrationResult(activityResult: ActivityResult) {
|
override fun manageRegistrationResult(activityResult: ActivityResult) {
|
||||||
val intent = activityResult.data
|
val intent = activityResult.data
|
||||||
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
||||||
Log.e(TAG, "Unable to create registration response for passkey", e)
|
Log.e(TAG, "Unable to create registration response for passkey", e)
|
||||||
@@ -518,8 +505,10 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
intent = responseIntent,
|
intent = responseIntent,
|
||||||
response = buildCreatePublicKeyCredentialResponse(
|
response = buildCreatePublicKeyCredentialResponse(
|
||||||
publicKeyCredentialCreationParameters = it,
|
publicKeyCredentialCreationParameters = it,
|
||||||
backupEligibility = mBackupEligibility,
|
backupEligibility = passkey?.backupEligibility
|
||||||
backupState = mBackupState
|
?: mBackupEligibility,
|
||||||
|
backupState = passkey?.backupState
|
||||||
|
?: mBackupState
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -549,29 +538,6 @@ class PasskeyLauncherViewModel(application: Application): AndroidViewModel(appli
|
|||||||
val temptingApp: AppOrigin,
|
val temptingApp: AppOrigin,
|
||||||
val nodeId: UUID
|
val nodeId: UUID
|
||||||
): UIState()
|
): UIState()
|
||||||
data class LaunchGroupActivityForSelection(
|
|
||||||
val database: ContextualDatabase
|
|
||||||
): UIState()
|
|
||||||
data class LaunchGroupActivityForRegistration(
|
|
||||||
val database: ContextualDatabase,
|
|
||||||
val registerInfo: RegisterInfo,
|
|
||||||
val typeMode: TypeMode
|
|
||||||
): UIState()
|
|
||||||
data class LaunchFileDatabaseSelectActivityForSelection(
|
|
||||||
val searchInfo: SearchInfo
|
|
||||||
): UIState()
|
|
||||||
data class LaunchFileDatabaseSelectActivityForRegistration(
|
|
||||||
val registerInfo: RegisterInfo,
|
|
||||||
val typeMode: TypeMode
|
|
||||||
): UIState()
|
|
||||||
data class SetActivityResult(
|
|
||||||
val lockDatabase: Boolean,
|
|
||||||
val resultCode: Int,
|
|
||||||
val data: Intent? = null
|
|
||||||
): UIState()
|
|
||||||
data class ShowError(
|
|
||||||
val error: Throwable
|
|
||||||
): UIState()
|
|
||||||
data class UpdateEntry(
|
data class UpdateEntry(
|
||||||
val oldEntry: Entry,
|
val oldEntry: Entry,
|
||||||
val newEntry: Entry
|
val newEntry: Entry
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database
|
package com.kunzisoft.keepass.database
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -29,23 +28,15 @@ import android.content.Context.BIND_IMPORTANT
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED
|
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
|
||||||
import com.kunzisoft.keepass.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.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
@@ -55,7 +46,6 @@ import com.kunzisoft.keepass.database.element.node.Node
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK
|
||||||
@@ -89,13 +79,9 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
|||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_RECYCLE_BIN_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.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.ProgressTaskDialogFragment
|
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
|
|
||||||
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
||||||
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
||||||
import com.kunzisoft.keepass.utils.putParcelableList
|
import com.kunzisoft.keepass.utils.putParcelableList
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,122 +89,30 @@ import java.util.UUID
|
|||||||
* Useful to retrieve a database instance and sending tasks commands
|
* Useful to retrieve a database instance and sending tasks commands
|
||||||
*/
|
*/
|
||||||
class DatabaseTaskProvider(
|
class DatabaseTaskProvider(
|
||||||
private var context: Context,
|
private var context: Context
|
||||||
private var showDialog: Boolean = true
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// To show dialog only if context is an activity
|
|
||||||
private var activity: FragmentActivity? = try {
|
|
||||||
context as? FragmentActivity?
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
|
var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
|
||||||
|
|
||||||
var onActionFinish: ((
|
var onStartActionRequested: ((bundle: Bundle?, actionTask: String) -> Unit)? = null
|
||||||
database: ContextualDatabase,
|
var actionTaskListener: DatabaseTaskNotificationService.ActionTaskListener? = null
|
||||||
actionTask: String,
|
var databaseInfoListener: DatabaseTaskNotificationService.DatabaseInfoListener? = null
|
||||||
result: ActionRunnable.Result
|
|
||||||
) -> Unit)? = null
|
|
||||||
|
|
||||||
private var intentDatabaseTask: Intent = Intent(
|
|
||||||
context.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
|
||||||
|
|
||||||
private var serviceConnection: ServiceConnection? = null
|
private var serviceConnection: ServiceConnection? = null
|
||||||
|
|
||||||
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
|
|
||||||
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
|
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
this.activity = null
|
|
||||||
this.onDatabaseRetrieved = null
|
this.onDatabaseRetrieved = null
|
||||||
this.onActionFinish = null
|
|
||||||
this.databaseTaskBroadcastReceiver = null
|
this.databaseTaskBroadcastReceiver = null
|
||||||
this.mBinder = null
|
this.mBinder = null
|
||||||
this.serviceConnection = null
|
this.serviceConnection = null
|
||||||
this.progressTaskDialogFragment = null
|
|
||||||
this.databaseChangedDialogFragment = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val actionTaskListener = object : DatabaseTaskNotificationService.ActionTaskListener {
|
fun onDatabaseChangeValidated() {
|
||||||
override fun onActionStarted(
|
|
||||||
database: ContextualDatabase,
|
|
||||||
progressMessage: ProgressMessage
|
|
||||||
) {
|
|
||||||
if (showDialog)
|
|
||||||
startDialog(progressMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActionUpdated(
|
|
||||||
database: ContextualDatabase,
|
|
||||||
progressMessage: ProgressMessage
|
|
||||||
) {
|
|
||||||
if (showDialog)
|
|
||||||
updateDialog(progressMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActionStopped(
|
|
||||||
database: ContextualDatabase
|
|
||||||
) {
|
|
||||||
// Remove the progress task
|
|
||||||
stopDialog()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActionFinished(
|
|
||||||
database: ContextualDatabase,
|
|
||||||
actionTask: String,
|
|
||||||
result: ActionRunnable.Result
|
|
||||||
) {
|
|
||||||
onActionFinish?.invoke(database, actionTask, result)
|
|
||||||
onActionStopped(database)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val mActionDatabaseListener =
|
|
||||||
object : DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
|
|
||||||
override fun validateDatabaseChanged() {
|
|
||||||
mBinder?.getService()?.saveDatabaseInfo()
|
mBinder?.getService()?.saveDatabaseInfo()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private var databaseInfoListener = object :
|
|
||||||
DatabaseTaskNotificationService.DatabaseInfoListener {
|
|
||||||
override fun onDatabaseInfoChanged(
|
|
||||||
previousDatabaseInfo: SnapFileDatabaseInfo,
|
|
||||||
newDatabaseInfo: SnapFileDatabaseInfo,
|
|
||||||
readOnlyDatabase: Boolean
|
|
||||||
) {
|
|
||||||
activity?.let { activity ->
|
|
||||||
activity.lifecycleScope.launch {
|
|
||||||
if (databaseChangedDialogFragment == null) {
|
|
||||||
databaseChangedDialogFragment = activity.supportFragmentManager
|
|
||||||
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
|
|
||||||
databaseChangedDialogFragment?.actionDatabaseListener =
|
|
||||||
mActionDatabaseListener
|
|
||||||
}
|
|
||||||
if (progressTaskDialogFragment == null) {
|
|
||||||
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
|
|
||||||
previousDatabaseInfo,
|
|
||||||
newDatabaseInfo,
|
|
||||||
readOnlyDatabase
|
|
||||||
)
|
|
||||||
databaseChangedDialogFragment?.actionDatabaseListener =
|
|
||||||
mActionDatabaseListener
|
|
||||||
databaseChangedDialogFragment?.show(
|
|
||||||
activity.supportFragmentManager,
|
|
||||||
DATABASE_CHANGED_DIALOG_TAG
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var databaseListener = object : DatabaseTaskNotificationService.DatabaseListener {
|
private var databaseListener = object : DatabaseTaskNotificationService.DatabaseListener {
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
@@ -226,48 +120,18 @@ class DatabaseTaskProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startDialog(progressMessage: ProgressMessage) {
|
|
||||||
activity?.let { activity ->
|
|
||||||
activity.lifecycleScope.launch {
|
|
||||||
if (progressTaskDialogFragment == null) {
|
|
||||||
progressTaskDialogFragment = activity.supportFragmentManager
|
|
||||||
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
|
|
||||||
}
|
|
||||||
if (progressTaskDialogFragment == null) {
|
|
||||||
progressTaskDialogFragment = ProgressTaskDialogFragment()
|
|
||||||
progressTaskDialogFragment?.show(
|
|
||||||
activity.supportFragmentManager,
|
|
||||||
PROGRESS_TASK_DIALOG_TAG
|
|
||||||
)
|
|
||||||
}
|
|
||||||
updateDialog(progressMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateDialog(progressMessage: ProgressMessage) {
|
|
||||||
progressTaskDialogFragment?.apply {
|
|
||||||
updateTitle(progressMessage.titleId)
|
|
||||||
updateMessage(progressMessage.messageId)
|
|
||||||
updateWarning(progressMessage.warningId)
|
|
||||||
setCancellable(progressMessage.cancelable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopDialog() {
|
|
||||||
progressTaskDialogFragment?.dismissAllowingStateLoss()
|
|
||||||
progressTaskDialogFragment = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initServiceConnection() {
|
private fun initServiceConnection() {
|
||||||
|
actionTaskListener?.onActionStopped()
|
||||||
if (serviceConnection == null) {
|
if (serviceConnection == null) {
|
||||||
serviceConnection = object : ServiceConnection {
|
serviceConnection = object : ServiceConnection {
|
||||||
override fun onBindingDied(name: ComponentName?) {
|
override fun onBindingDied(name: ComponentName?) {
|
||||||
stopDialog()
|
actionTaskListener?.onActionStopped()
|
||||||
|
onDatabaseRetrieved?.invoke(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNullBinding(name: ComponentName?) {
|
override fun onNullBinding(name: ComponentName?) {
|
||||||
stopDialog()
|
actionTaskListener?.onActionStopped()
|
||||||
|
onDatabaseRetrieved?.invoke(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||||
@@ -290,21 +154,33 @@ class DatabaseTaskProvider(
|
|||||||
|
|
||||||
private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
|
private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
|
||||||
service?.addDatabaseListener(databaseListener)
|
service?.addDatabaseListener(databaseListener)
|
||||||
service?.addDatabaseFileInfoListener(databaseInfoListener)
|
databaseInfoListener?.let { infoListener ->
|
||||||
service?.addActionTaskListener(actionTaskListener)
|
service?.addDatabaseFileInfoListener(infoListener)
|
||||||
|
}
|
||||||
|
actionTaskListener?.let { taskListener ->
|
||||||
|
service?.addActionTaskListener(taskListener)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
|
private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
|
||||||
service?.removeActionTaskListener(actionTaskListener)
|
actionTaskListener?.let { taskListener ->
|
||||||
service?.removeDatabaseFileInfoListener(databaseInfoListener)
|
service?.removeActionTaskListener(taskListener)
|
||||||
|
}
|
||||||
|
databaseInfoListener?.let { infoListener ->
|
||||||
|
service?.removeDatabaseFileInfoListener(infoListener)
|
||||||
|
}
|
||||||
service?.removeDatabaseListener(databaseListener)
|
service?.removeDatabaseListener(databaseListener)
|
||||||
|
onDatabaseRetrieved?.invoke(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindService() {
|
private fun bindService() {
|
||||||
initServiceConnection()
|
initServiceConnection()
|
||||||
serviceConnection?.let {
|
serviceConnection?.let {
|
||||||
context.bindService(
|
context.bindService(
|
||||||
intentDatabaseTask,
|
Intent(
|
||||||
|
context.applicationContext,
|
||||||
|
DatabaseTaskNotificationService::class.java
|
||||||
|
),
|
||||||
it,
|
it,
|
||||||
BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT
|
BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT
|
||||||
)
|
)
|
||||||
@@ -368,58 +244,9 @@ class DatabaseTaskProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val tempServiceParameters = mutableListOf<Pair<Bundle?, String>>()
|
|
||||||
private val requestPermissionLauncher = activity?.registerForActivityResult(
|
|
||||||
ActivityResultContracts.RequestPermission()
|
|
||||||
) { _ ->
|
|
||||||
// Whether or not the user has accepted, the service can be started,
|
|
||||||
// There just won't be any notification if it's not allowed.
|
|
||||||
tempServiceParameters.removeFirstOrNull()?.let {
|
|
||||||
startService(it.first, it.second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun start(bundle: Bundle? = null, actionTask: String) {
|
private fun start(bundle: Bundle? = null, actionTask: String) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
onStartActionRequested?.invoke(bundle, actionTask) ?: run {
|
||||||
val contextActivity = activity
|
context.startDatabaseService(bundle, actionTask)
|
||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
|
|
||||||
== PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
|
||||||
startService(bundle, actionTask)
|
|
||||||
} else if (contextActivity != null && shouldShowRequestPermissionRationale(
|
|
||||||
contextActivity,
|
|
||||||
Manifest.permission.POST_NOTIFICATIONS
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// it's not the first time, so the user deliberately chooses not to display the notification
|
|
||||||
startService(bundle, actionTask)
|
|
||||||
} else {
|
|
||||||
AlertDialog.Builder(context)
|
|
||||||
.setMessage(R.string.warning_database_notification_permission)
|
|
||||||
.setNegativeButton(R.string.later) { _, _ ->
|
|
||||||
// Refuses the notification, so start the service
|
|
||||||
startService(bundle, actionTask)
|
|
||||||
}
|
|
||||||
.setPositiveButton(R.string.ask) { _, _ ->
|
|
||||||
// Save the temp parameters to ask the permission
|
|
||||||
tempServiceParameters.add(Pair(bundle, actionTask))
|
|
||||||
requestPermissionLauncher?.launch(Manifest.permission.POST_NOTIFICATIONS)
|
|
||||||
}.create().show()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
startService(bundle, actionTask)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startService(bundle: Bundle? = null, actionTask: String) {
|
|
||||||
try {
|
|
||||||
if (bundle != null)
|
|
||||||
intentDatabaseTask.putExtras(bundle)
|
|
||||||
intentDatabaseTask.action = actionTask
|
|
||||||
context.startService(intentDatabaseTask)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to perform database action", e)
|
|
||||||
Toast.makeText(context, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -842,5 +669,21 @@ class DatabaseTaskProvider(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = DatabaseTaskProvider::class.java.name
|
private val TAG = DatabaseTaskProvider::class.java.name
|
||||||
|
|
||||||
|
fun Context.startDatabaseService(bundle: Bundle? = null, actionTask: String) {
|
||||||
|
try {
|
||||||
|
val intentDatabaseTask = Intent(
|
||||||
|
applicationContext,
|
||||||
|
DatabaseTaskNotificationService::class.java
|
||||||
|
)
|
||||||
|
if (bundle != null)
|
||||||
|
intentDatabaseTask.putExtras(bundle)
|
||||||
|
intentDatabaseTask.action = actionTask
|
||||||
|
startService(intentDatabaseTask)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to perform database action", e)
|
||||||
|
Toast.makeText(this, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
|||||||
import com.kunzisoft.keepass.database.exception.VersionDatabaseException
|
import com.kunzisoft.keepass.database.exception.VersionDatabaseException
|
||||||
import com.kunzisoft.keepass.database.exception.XMLMalformedDatabaseException
|
import com.kunzisoft.keepass.database.exception.XMLMalformedDatabaseException
|
||||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_CREDENTIAL_ID
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_CREDENTIAL_ID
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_FLAG_BE
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_FLAG_BS
|
||||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_PRIVATE_KEY
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_PRIVATE_KEY
|
||||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_RELYING_PARTY
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_RELYING_PARTY
|
||||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USERNAME
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USERNAME
|
||||||
@@ -146,6 +148,8 @@ fun TemplateField.getLocalizedName(context: Context?, name: String): String {
|
|||||||
FIELD_CREDENTIAL_ID.equals(name, true) -> context.getString(R.string.passkey_credential_id)
|
FIELD_CREDENTIAL_ID.equals(name, true) -> context.getString(R.string.passkey_credential_id)
|
||||||
FIELD_USER_HANDLE.equals(name, true) -> context.getString(R.string.passkey_user_handle)
|
FIELD_USER_HANDLE.equals(name, true) -> context.getString(R.string.passkey_user_handle)
|
||||||
FIELD_RELYING_PARTY.equals(name, true) -> context.getString(R.string.passkey_relying_party)
|
FIELD_RELYING_PARTY.equals(name, true) -> context.getString(R.string.passkey_relying_party)
|
||||||
|
FIELD_FLAG_BE.equals(name, true) -> context.getString(R.string.passkey_backup_eligibility)
|
||||||
|
FIELD_FLAG_BS.equals(name, true) -> context.getString(R.string.passkey_backup_state)
|
||||||
|
|
||||||
else -> name
|
else -> name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,16 @@ package com.kunzisoft.keepass.database.helper
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
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.PreferencesUtil.searchSubDomains
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
||||||
|
|
||||||
object SearchHelper {
|
object SearchHelper {
|
||||||
|
|
||||||
@@ -40,6 +47,76 @@ object SearchHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the concrete web domain AKA without sub domain if needed
|
||||||
|
*/
|
||||||
|
private fun getConcreteWebDomain(
|
||||||
|
context: Context,
|
||||||
|
webDomain: String?,
|
||||||
|
concreteWebDomain: (searchSubDomains: Boolean, concreteWebDomain: String?) -> Unit
|
||||||
|
) {
|
||||||
|
val domain = webDomain
|
||||||
|
val searchSubDomains = searchSubDomains(context)
|
||||||
|
if (domain != null) {
|
||||||
|
// Warning, web domain can contains IP, don't crop in this case
|
||||||
|
if (searchSubDomains
|
||||||
|
|| Regex(SearchInfo.WEB_IP_REGEX).matches(domain)) {
|
||||||
|
concreteWebDomain.invoke(searchSubDomains, webDomain)
|
||||||
|
} else {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val publicSuffixList = PublicSuffixList(context)
|
||||||
|
val publicSuffix = publicSuffixList
|
||||||
|
.getPublicSuffixPlusOne(domain).await()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
concreteWebDomain.invoke(false, publicSuffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
concreteWebDomain.invoke(searchSubDomains, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create search parameters asynchronously from [SearchInfo]
|
||||||
|
*/
|
||||||
|
fun SearchInfo.getSearchParametersFromSearchInfo(
|
||||||
|
context: Context,
|
||||||
|
callback: (SearchParameters) -> Unit
|
||||||
|
) {
|
||||||
|
getConcreteWebDomain(
|
||||||
|
context,
|
||||||
|
webDomain
|
||||||
|
) { searchSubDomains, concreteDomain ->
|
||||||
|
var query = this.toString()
|
||||||
|
if (isDomainSearch && concreteDomain != null)
|
||||||
|
query = concreteDomain
|
||||||
|
callback.invoke(
|
||||||
|
SearchParameters().apply {
|
||||||
|
searchQuery = query
|
||||||
|
allowEmptyQuery = false
|
||||||
|
searchInTitles = false
|
||||||
|
searchInUsernames = false
|
||||||
|
searchInPasswords = false
|
||||||
|
searchInAppIds = isAppIdSearch
|
||||||
|
searchInUrls = isDomainSearch
|
||||||
|
searchByDomain = true
|
||||||
|
searchBySubDomain = searchSubDomains
|
||||||
|
searchInRelyingParty = isPasskeySearch
|
||||||
|
searchInNotes = false
|
||||||
|
searchInOTP = isOTPSearch
|
||||||
|
searchInOther = false
|
||||||
|
searchInUUIDs = false
|
||||||
|
searchInTags = isTagSearch
|
||||||
|
searchInCurrentGroup = false
|
||||||
|
searchInSearchableGroup = true
|
||||||
|
searchInRecycleBin = false
|
||||||
|
searchInTemplates = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility method to perform actions if item is found or not after an auto search in [database]
|
* Utility method to perform actions if item is found or not after an auto search in [database]
|
||||||
*/
|
*/
|
||||||
@@ -52,28 +129,31 @@ object SearchHelper {
|
|||||||
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
|
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
|
||||||
onDatabaseClosed: () -> Unit
|
onDatabaseClosed: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
// Do not place coroutine at start, bug in Passkey implementation
|
||||||
if (database == null || !database.loaded) {
|
if (database == null || !database.loaded) {
|
||||||
onDatabaseClosed.invoke()
|
onDatabaseClosed.invoke()
|
||||||
} else if (TimeoutHelper.checkTime(context)) {
|
} else if (TimeoutHelper.checkTime(context)) {
|
||||||
var searchWithoutUI = false
|
|
||||||
if (searchInfo != null
|
if (searchInfo != null
|
||||||
&& !searchInfo.manualSelection
|
&& !searchInfo.manualSelection
|
||||||
&& !searchInfo.containsOnlyNullValues()) {
|
&& !searchInfo.containsOnlyNullValues()
|
||||||
|
) {
|
||||||
|
searchInfo.getSearchParametersFromSearchInfo(context) { searchParameters ->
|
||||||
// If search provide results
|
// If search provide results
|
||||||
database.createVirtualGroupFromSearchInfo(
|
database.createVirtualGroupFromSearchInfo(
|
||||||
searchInfo,
|
searchParameters = searchParameters,
|
||||||
MAX_SEARCH_ENTRY
|
max = MAX_SEARCH_ENTRY
|
||||||
)?.let { searchGroup ->
|
)?.let { searchGroup ->
|
||||||
if (searchGroup.numberOfChildEntries > 0) {
|
if (searchGroup.numberOfChildEntries > 0) {
|
||||||
searchWithoutUI = true
|
onItemsFound.invoke(
|
||||||
onItemsFound.invoke(database,
|
database,
|
||||||
searchGroup.getChildEntriesInfo(database))
|
searchGroup.getChildEntriesInfo(database)
|
||||||
|
)
|
||||||
|
} else
|
||||||
|
onItemNotFound.invoke(database)
|
||||||
|
} ?: onItemNotFound.invoke(database)
|
||||||
}
|
}
|
||||||
}
|
} else
|
||||||
}
|
|
||||||
if (!searchWithoutUI) {
|
|
||||||
onItemNotFound.invoke(database)
|
onItemNotFound.invoke(database)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.hardware
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.result.ActivityResult
|
|
||||||
import androidx.activity.result.ActivityResultCallback
|
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
|
||||||
import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Special activity to deal with hardware key drivers,
|
|
||||||
* return the response to the database service once finished
|
|
||||||
*/
|
|
||||||
class HardwareKeyActivity: DatabaseModeActivity(){
|
|
||||||
|
|
||||||
// To manage hardware key challenge response
|
|
||||||
private val resultCallback = ActivityResultCallback<ActivityResult> { result ->
|
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
|
||||||
val challengeResponse: ByteArray? = result.data?.getByteArrayExtra(HARDWARE_KEY_RESPONSE_KEY)
|
|
||||||
Log.d(TAG, "Response form challenge")
|
|
||||||
mDatabaseTaskProvider?.startChallengeResponded(challengeResponse ?: ByteArray(0))
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "Response from challenge error")
|
|
||||||
mDatabaseTaskProvider?.startChallengeResponded(ByteArray(0))
|
|
||||||
}
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var activityResultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
|
|
||||||
ActivityResultContracts.StartActivityForResult(),
|
|
||||||
resultCallback
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun applyCustomStyle(): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showDatabaseDialog(): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
|
|
||||||
val hardwareKey = HardwareKey.getHardwareKeyFromString(
|
|
||||||
intent.getStringExtra(DATA_HARDWARE_KEY)
|
|
||||||
)
|
|
||||||
if (isHardwareKeyAvailable(this, hardwareKey, true) {
|
|
||||||
mDatabaseTaskProvider?.startChallengeResponded(ByteArray(0))
|
|
||||||
}) {
|
|
||||||
when (hardwareKey) {
|
|
||||||
/*
|
|
||||||
HardwareKey.FIDO2_SECRET -> {
|
|
||||||
// TODO FIDO2 under development
|
|
||||||
throw Exception("FIDO2 not implemented")
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
|
|
||||||
launchYubikeyChallengeForResponse(intent.getByteArrayExtra(DATA_SEED))
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchYubikeyChallengeForResponse(seed: ByteArray?) {
|
|
||||||
// Transform the seed before sending
|
|
||||||
var challenge: ByteArray? = null
|
|
||||||
if (seed != null) {
|
|
||||||
challenge = ByteArray(64)
|
|
||||||
seed.copyInto(challenge, 0, 0, 32)
|
|
||||||
challenge.fill(32, 32, 64)
|
|
||||||
}
|
|
||||||
// Send to the driver
|
|
||||||
activityResultLauncher.launch(
|
|
||||||
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply {
|
|
||||||
putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Log.d(TAG, "Challenge sent")
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val TAG = HardwareKeyActivity::class.java.simpleName
|
|
||||||
|
|
||||||
private const val DATA_HARDWARE_KEY = "DATA_HARDWARE_KEY"
|
|
||||||
private const val DATA_SEED = "DATA_SEED"
|
|
||||||
private const val YUBIKEY_CHALLENGE_RESPONSE_INTENT = "android.yubikey.intent.action.CHALLENGE_RESPONSE"
|
|
||||||
private const val HARDWARE_KEY_CHALLENGE_KEY = "challenge"
|
|
||||||
private const val HARDWARE_KEY_RESPONSE_KEY = "response"
|
|
||||||
|
|
||||||
fun launchHardwareKeyActivity(
|
|
||||||
context: Context,
|
|
||||||
hardwareKey: HardwareKey,
|
|
||||||
seed: ByteArray?
|
|
||||||
) {
|
|
||||||
context.startActivity(Intent(context, HardwareKeyActivity::class.java).apply {
|
|
||||||
flags = FLAG_ACTIVITY_NEW_TASK
|
|
||||||
putExtra(DATA_HARDWARE_KEY, hardwareKey.value)
|
|
||||||
putExtra(DATA_SEED, seed)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isHardwareKeyAvailable(
|
|
||||||
context: Context,
|
|
||||||
hardwareKey: HardwareKey?,
|
|
||||||
showDialog: Boolean = true,
|
|
||||||
onDialogDismissed: DialogInterface.OnDismissListener? = null
|
|
||||||
): Boolean {
|
|
||||||
if (hardwareKey == null)
|
|
||||||
return false
|
|
||||||
return when (hardwareKey) {
|
|
||||||
/*
|
|
||||||
HardwareKey.FIDO2_SECRET -> {
|
|
||||||
// TODO FIDO2 under development
|
|
||||||
if (showDialog)
|
|
||||||
UnderDevelopmentFeatureDialogFragment()
|
|
||||||
.show(activity.supportFragmentManager, "underDevFeatureDialog")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
|
|
||||||
// Check available intent
|
|
||||||
val yubikeyDriverAvailable =
|
|
||||||
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT)
|
|
||||||
.resolveActivity(context.packageManager) != null
|
|
||||||
if (showDialog && !yubikeyDriverAvailable
|
|
||||||
&& context is Activity)
|
|
||||||
showHardwareKeyDriverNeeded(context, hardwareKey) {
|
|
||||||
onDialogDismissed?.onDismiss(it)
|
|
||||||
context.finish()
|
|
||||||
}
|
|
||||||
yubikeyDriverAvailable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showHardwareKeyDriverNeeded(
|
|
||||||
context: Context,
|
|
||||||
hardwareKey: HardwareKey,
|
|
||||||
onDialogDismissed: DialogInterface.OnDismissListener
|
|
||||||
) {
|
|
||||||
val builder = AlertDialog.Builder(context)
|
|
||||||
builder
|
|
||||||
.setMessage(
|
|
||||||
context.getString(R.string.error_driver_required, hardwareKey.toString())
|
|
||||||
)
|
|
||||||
.setPositiveButton(R.string.download) { _, _ ->
|
|
||||||
context.openExternalApp(
|
|
||||||
context.getString(R.string.key_driver_app_id),
|
|
||||||
context.getString(R.string.key_driver_url)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
|
||||||
.setOnDismissListener(onDialogDismissed)
|
|
||||||
builder.create().show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -33,7 +33,8 @@ import java.util.*
|
|||||||
class PasswordGenerator(private val resources: Resources) {
|
class PasswordGenerator(private val resources: Resources) {
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class)
|
@Throws(IllegalArgumentException::class)
|
||||||
fun generatePassword(length: Int,
|
fun generatePassword(
|
||||||
|
length: Int,
|
||||||
upperCase: Boolean,
|
upperCase: Boolean,
|
||||||
lowerCase: Boolean,
|
lowerCase: Boolean,
|
||||||
digits: Boolean,
|
digits: Boolean,
|
||||||
@@ -46,7 +47,8 @@ class PasswordGenerator(private val resources: Resources) {
|
|||||||
considerChars: String,
|
considerChars: String,
|
||||||
ignoreChars: String,
|
ignoreChars: String,
|
||||||
atLeastOneFromEach: Boolean,
|
atLeastOneFromEach: Boolean,
|
||||||
excludeAmbiguousChar: Boolean): String {
|
excludeAmbiguousChar: Boolean
|
||||||
|
): String {
|
||||||
// Desired password length is 0 or less
|
// Desired password length is 0 or less
|
||||||
if (length <= 0) {
|
if (length <= 0) {
|
||||||
throw IllegalArgumentException(resources.getString(R.string.error_wrong_length))
|
throw IllegalArgumentException(resources.getString(R.string.error_wrong_length))
|
||||||
@@ -228,7 +230,7 @@ class PasswordGenerator(private val resources: Resources) {
|
|||||||
private const val MINUS_CHAR = "-"
|
private const val MINUS_CHAR = "-"
|
||||||
private const val UNDERLINE_CHAR = "_"
|
private const val UNDERLINE_CHAR = "_"
|
||||||
private const val SPACE_CHAR = " "
|
private const val SPACE_CHAR = " "
|
||||||
private const val SPECIAL_CHARS = "!\"#$%&'*+,./:;=?@\\^`"
|
private const val SPECIAL_CHARS = "&/,^@.#:%\\='$!?*`;+\"|~"
|
||||||
private const val BRACKET_CHARS = "[]{}()<>"
|
private const val BRACKET_CHARS = "[]{}()<>"
|
||||||
private const val AMBIGUOUS_CHARS = "iI|lLoO01"
|
private const val AMBIGUOUS_CHARS = "iI|lLoO01"
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import com.kunzisoft.keepass.model.AttachmentState
|
|||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.StreamDirection
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||||
|
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -194,7 +195,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
private fun newNotification(attachmentNotification: AttachmentNotification) {
|
private fun newNotification(attachmentNotification: AttachmentNotification) {
|
||||||
|
|
||||||
val pendingContentIntent = PendingIntent.getActivity(this,
|
val pendingContentIntent = PendingIntent.getActivity(this,
|
||||||
0,
|
randomRequestCode(),
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
action = Intent.ACTION_VIEW
|
action = Intent.ACTION_VIEW
|
||||||
setDataAndType(attachmentNotification.uri,
|
setDataAndType(attachmentNotification.uri,
|
||||||
@@ -208,7 +209,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val pendingDeleteIntent = PendingIntent.getService(this,
|
val pendingDeleteIntent = PendingIntent.getService(this,
|
||||||
0,
|
randomRequestCode(),
|
||||||
Intent(this, AttachmentFileNotificationService::class.java).apply {
|
Intent(this, AttachmentFileNotificationService::class.java).apply {
|
||||||
// No action to delete the service
|
// No action to delete the service
|
||||||
putExtra(FILE_URI_KEY, attachmentNotification.uri)
|
putExtra(FILE_URI_KEY, attachmentNotification.uri)
|
||||||
|
|||||||
@@ -61,13 +61,14 @@ import com.kunzisoft.keepass.database.element.node.Node
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
|
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
|
||||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||||
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 com.kunzisoft.keepass.utils.LOCK_ACTION
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
@@ -175,7 +176,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
progressMessage: ProgressMessage
|
progressMessage: ProgressMessage
|
||||||
)
|
)
|
||||||
fun onActionStopped(
|
fun onActionStopped(
|
||||||
database: ContextualDatabase
|
database: ContextualDatabase? = null
|
||||||
)
|
)
|
||||||
fun onActionFinished(
|
fun onActionFinished(
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
@@ -550,7 +551,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
// Build Intents for notification action
|
// Build Intents for notification action
|
||||||
val pendingDatabaseIntent = PendingIntent.getActivity(
|
val pendingDatabaseIntent = PendingIntent.getActivity(
|
||||||
this,
|
this,
|
||||||
0,
|
randomRequestCode(),
|
||||||
Intent(this, GroupActivity::class.java),
|
Intent(this, GroupActivity::class.java),
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
@@ -675,6 +676,12 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
override fun actionOnLock() {
|
override fun actionOnLock() {
|
||||||
if (!TimeoutHelper.temporarilyDisableLock) {
|
if (!TimeoutHelper.temporarilyDisableLock) {
|
||||||
closeDatabase(mDatabase)
|
closeDatabase(mDatabase)
|
||||||
|
// Remove the database during the lock
|
||||||
|
// And notify each subscriber
|
||||||
|
mDatabase = null
|
||||||
|
mDatabaseListeners.forEach { listener ->
|
||||||
|
listener.onDatabaseRetrieved(null)
|
||||||
|
}
|
||||||
// Remove the lock timer (no more needed if it exists)
|
// Remove the lock timer (no more needed if it exists)
|
||||||
TimeoutHelper.cancelLockTimer(this)
|
TimeoutHelper.cancelLockTimer(this)
|
||||||
// Service is stopped after receive the broadcast
|
// Service is stopped after receive the broadcast
|
||||||
@@ -709,9 +716,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
notifyProgressMessage()
|
notifyProgressMessage()
|
||||||
HardwareKeyActivity
|
HardwareKeyActivity
|
||||||
.launchHardwareKeyActivity(
|
.launchHardwareKeyActivity(
|
||||||
this@DatabaseTaskNotificationService,
|
context = this@DatabaseTaskNotificationService,
|
||||||
hardwareKey,
|
hardwareKey = hardwareKey,
|
||||||
seed
|
seed = seed
|
||||||
)
|
)
|
||||||
// Wait the response
|
// Wait the response
|
||||||
mProgressMessage.apply {
|
mProgressMessage.apply {
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
|
|||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
autofillInlineSuggestionsPreference?.isVisible = false
|
autofillInlineSuggestionsPreference?.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val autofillAskSaveDataPreference: TwoStatePreference? = findPreference(getString(R.string.autofill_ask_to_save_data_key))
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||||
|
autofillAskSaveDataPreference?.isVisible = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDisplayPreferenceDialog(preference: Preference) {
|
override fun onDisplayPreferenceDialog(preference: Preference) {
|
||||||
|
|||||||
@@ -21,20 +21,25 @@ package com.kunzisoft.keepass.settings
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceCategory
|
import androidx.preference.PreferenceCategory
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class MainPreferenceFragment : PreferenceFragmentCompat() {
|
class MainPreferenceFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
private var mCallback: Callback? = null
|
private var mCallback: Callback? = null
|
||||||
|
|
||||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||||
private var mDatabaseLoaded: Boolean = false
|
private val mDatabase: ContextualDatabase?
|
||||||
|
get() = mDatabaseViewModel.database
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
@@ -50,20 +55,24 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
mCallback = null
|
mCallback = null
|
||||||
super.onDetach()
|
super.onDetach()
|
||||||
}
|
}
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
mDatabaseLoaded = database?.loaded == true
|
super.onCreate(savedInstanceState)
|
||||||
checkDatabaseLoaded()
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
mDatabaseViewModel.databaseState.collect { database ->
|
||||||
|
checkDatabaseLoaded(database?.loaded == true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkDatabaseLoaded() {
|
private fun checkDatabaseLoaded(isDatabaseLoaded: Boolean) {
|
||||||
findPreference<Preference>(getString(R.string.settings_database_key))
|
findPreference<Preference>(getString(R.string.settings_database_key))
|
||||||
?.isEnabled = mDatabaseLoaded
|
?.isEnabled = isDatabaseLoaded
|
||||||
|
|
||||||
findPreference<PreferenceCategory>(getString(R.string.settings_database_category_key))
|
findPreference<PreferenceCategory>(getString(R.string.settings_database_category_key))
|
||||||
?.isVisible = mDatabaseLoaded
|
?.isVisible = isDatabaseLoaded
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
@@ -119,7 +128,7 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkDatabaseLoaded()
|
checkDatabaseLoaded(mDatabase?.loaded == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
|
|||||||
@@ -19,13 +19,21 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.settings
|
package com.kunzisoft.keepass.settings
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.graphics.toColorInt
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceCategory
|
import androidx.preference.PreferenceCategory
|
||||||
import androidx.preference.TwoStatePreference
|
import androidx.preference.TwoStatePreference
|
||||||
@@ -39,19 +47,40 @@ 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.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.helper.*
|
import com.kunzisoft.keepass.database.helper.getLocalizedName
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.settings.preference.*
|
import com.kunzisoft.keepass.settings.preference.DialogColorPreference
|
||||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.*
|
import com.kunzisoft.keepass.settings.preference.DialogListExplanationPreference
|
||||||
|
import com.kunzisoft.keepass.settings.preference.InputKdfNumberPreference
|
||||||
|
import com.kunzisoft.keepass.settings.preference.InputKdfSizePreference
|
||||||
|
import com.kunzisoft.keepass.settings.preference.InputNumberPreference
|
||||||
|
import com.kunzisoft.keepass.settings.preference.InputTextPreference
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseColorPreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDataCompressionPreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDefaultUsernamePreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseDescriptionPreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseKeyDerivationPreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMaxHistoryItemsPreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMaxHistorySizePreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseMemoryUsagePreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseNamePreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseParallelismPreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRecycleBinGroupPreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseRoundsPreferenceDialogFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DatabaseTemplatesGroupPreferenceDialogFragmentCompat
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import com.kunzisoft.keepass.utils.getSerializableCompat
|
import com.kunzisoft.keepass.utils.getSerializableCompat
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval {
|
class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval {
|
||||||
|
|
||||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||||
private var mDatabase: ContextualDatabase? = null
|
private val mDatabase: ContextualDatabase?
|
||||||
|
get() = mDatabaseViewModel.database
|
||||||
private var mDatabaseReadOnly: Boolean = false
|
private var mDatabaseReadOnly: Boolean = false
|
||||||
private var mMergeDataAllowed: Boolean = false
|
private var mMergeDataAllowed: Boolean = false
|
||||||
private var mDatabaseAutoSaveEnabled: Boolean = true
|
private var mDatabaseAutoSaveEnabled: Boolean = true
|
||||||
@@ -114,19 +143,46 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
lifecycleScope.launch {
|
||||||
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
mDatabaseViewModel.actionState.collect { uiState ->
|
||||||
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
|
when (uiState) {
|
||||||
mDatabase = database
|
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||||
view.resetAppTimeoutWhenViewTouchedOrFocused(requireContext(), database?.loaded)
|
onDatabaseActionFinished(
|
||||||
onDatabaseRetrieved(database)
|
uiState.database,
|
||||||
|
uiState.actionTask,
|
||||||
|
uiState.result
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) {
|
else -> {}
|
||||||
onDatabaseActionFinished(it.database, it.actionTask, it.result)
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
mDatabaseViewModel.databaseState.collect { database ->
|
||||||
|
database?.let {
|
||||||
|
onDatabaseRetrieved(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
mDatabaseViewModel.databaseState.collect { database ->
|
||||||
|
view.resetAppTimeoutWhenViewTouchedOrFocused(
|
||||||
|
context = requireContext(),
|
||||||
|
databaseLoaded = database?.loaded
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,22 +223,20 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
mDatabaseViewModel.reloadDatabase(false)
|
mDatabaseViewModel.reloadDatabase(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
mDatabase = database
|
mDatabaseReadOnly = database.isReadOnly
|
||||||
mDatabaseReadOnly = database?.isReadOnly == true
|
mMergeDataAllowed = database.isMergeDataAllowed()
|
||||||
mMergeDataAllowed = database?.isMergeDataAllowed() == true
|
|
||||||
|
|
||||||
mDatabase?.let {
|
if (database.loaded) {
|
||||||
if (it.loaded) {
|
|
||||||
when (mScreen) {
|
when (mScreen) {
|
||||||
Screen.DATABASE -> {
|
Screen.DATABASE -> {
|
||||||
onCreateDatabasePreference(it)
|
onCreateDatabasePreference(database)
|
||||||
}
|
}
|
||||||
Screen.DATABASE_SECURITY -> {
|
Screen.DATABASE_SECURITY -> {
|
||||||
onCreateDatabaseSecurityPreference(it)
|
onCreateDatabaseSecurityPreference(database)
|
||||||
}
|
}
|
||||||
Screen.DATABASE_MASTER_KEY -> {
|
Screen.DATABASE_MASTER_KEY -> {
|
||||||
onCreateDatabaseMasterKeyPreference(it)
|
onCreateDatabaseMasterKeyPreference(database)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
}
|
}
|
||||||
@@ -191,7 +245,6 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
Log.e(javaClass.name, "Database isn't ready")
|
Log.e(javaClass.name, "Database isn't ready")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun onCreateDatabasePreference(database: ContextualDatabase) {
|
private fun onCreateDatabasePreference(database: ContextualDatabase) {
|
||||||
val dbGeneralPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_category_general_key))
|
val dbGeneralPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_category_general_key))
|
||||||
@@ -458,7 +511,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
newDefaultUsername
|
newDefaultUsername
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.defaultUsername = oldDefaultUsername
|
database.defaultUsername = oldDefaultUsername
|
||||||
oldDefaultUsername
|
oldDefaultUsername
|
||||||
}
|
}
|
||||||
dbDefaultUsernamePref?.summary = defaultUsernameToShow
|
dbDefaultUsernamePref?.summary = defaultUsernameToShow
|
||||||
@@ -471,7 +524,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
newColor
|
newColor
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.customColor = Color.parseColor(oldColor)
|
database.customColor = oldColor.toColorInt()
|
||||||
oldColor
|
oldColor
|
||||||
}
|
}
|
||||||
dbCustomColorPref?.summary = defaultColorToShow
|
dbCustomColorPref?.summary = defaultColorToShow
|
||||||
@@ -483,7 +536,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
newCompression
|
newCompression
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.compressionAlgorithm = oldCompression
|
database.compressionAlgorithm = oldCompression
|
||||||
oldCompression
|
oldCompression
|
||||||
}
|
}
|
||||||
dbDataCompressionPref?.summary = algorithmToShow?.getLocalizedName(resources)
|
dbDataCompressionPref?.summary = algorithmToShow?.getLocalizedName(resources)
|
||||||
@@ -497,7 +550,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
} else {
|
} else {
|
||||||
oldRecycleBin
|
oldRecycleBin
|
||||||
}
|
}
|
||||||
mDatabase?.setRecycleBin(recycleBinToShow)
|
database.setRecycleBin(recycleBinToShow)
|
||||||
refreshRecycleBinGroup(database)
|
refreshRecycleBinGroup(database)
|
||||||
}
|
}
|
||||||
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK -> {
|
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK -> {
|
||||||
@@ -509,7 +562,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
} else {
|
} else {
|
||||||
oldTemplatesGroup
|
oldTemplatesGroup
|
||||||
}
|
}
|
||||||
mDatabase?.setTemplatesGroup(templatesGroupToShow)
|
database.setTemplatesGroup(templatesGroupToShow)
|
||||||
refreshTemplatesGroup(database)
|
refreshTemplatesGroup(database)
|
||||||
}
|
}
|
||||||
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK -> {
|
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK -> {
|
||||||
@@ -519,7 +572,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
newMaxHistoryItems
|
newMaxHistoryItems
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.historyMaxItems = oldMaxHistoryItems
|
database.historyMaxItems = oldMaxHistoryItems
|
||||||
oldMaxHistoryItems
|
oldMaxHistoryItems
|
||||||
}
|
}
|
||||||
dbMaxHistoryItemsPref?.summary = maxHistoryItemsToShow.toString()
|
dbMaxHistoryItemsPref?.summary = maxHistoryItemsToShow.toString()
|
||||||
@@ -531,7 +584,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
newMaxHistorySize
|
newMaxHistorySize
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.historyMaxSize = oldMaxHistorySize
|
database.historyMaxSize = oldMaxHistorySize
|
||||||
oldMaxHistorySize
|
oldMaxHistorySize
|
||||||
}
|
}
|
||||||
dbMaxHistorySizePref?.summary = maxHistorySizeToShow.toString()
|
dbMaxHistorySizePref?.summary = maxHistorySizeToShow.toString()
|
||||||
@@ -549,7 +602,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
newEncryption
|
newEncryption
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.encryptionAlgorithm = oldEncryption
|
database.encryptionAlgorithm = oldEncryption
|
||||||
oldEncryption
|
oldEncryption
|
||||||
}
|
}
|
||||||
mEncryptionAlgorithmPref?.summary = algorithmToShow.toString()
|
mEncryptionAlgorithmPref?.summary = algorithmToShow.toString()
|
||||||
@@ -561,7 +614,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
newKeyDerivationEngine
|
newKeyDerivationEngine
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.kdfEngine = oldKeyDerivationEngine
|
database.kdfEngine = oldKeyDerivationEngine
|
||||||
oldKeyDerivationEngine
|
oldKeyDerivationEngine
|
||||||
}
|
}
|
||||||
mKeyDerivationPref?.summary = kdfEngineToShow.toString()
|
mKeyDerivationPref?.summary = kdfEngineToShow.toString()
|
||||||
@@ -578,7 +631,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
newIterations
|
newIterations
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.numberKeyEncryptionRounds = oldIterations
|
database.numberKeyEncryptionRounds = oldIterations
|
||||||
oldIterations
|
oldIterations
|
||||||
}
|
}
|
||||||
mRoundPref?.summary = roundsToShow.toString()
|
mRoundPref?.summary = roundsToShow.toString()
|
||||||
@@ -590,7 +643,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
newMemoryUsage
|
newMemoryUsage
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.memoryUsage = oldMemoryUsage
|
database.memoryUsage = oldMemoryUsage
|
||||||
oldMemoryUsage
|
oldMemoryUsage
|
||||||
}
|
}
|
||||||
mMemoryPref?.summary = memoryToShow.toString()
|
mMemoryPref?.summary = memoryToShow.toString()
|
||||||
@@ -602,7 +655,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
newParallelism
|
newParallelism
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.parallelism = oldParallelism
|
database.parallelism = oldParallelism
|
||||||
oldParallelism
|
oldParallelism
|
||||||
}
|
}
|
||||||
mParallelismPref?.summary = parallelismToShow.toString()
|
mParallelismPref?.summary = parallelismToShow.toString()
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ object PreferencesUtil {
|
|||||||
context.resources.getBoolean(R.bool.auto_focus_search_default))
|
context.resources.getBoolean(R.bool.auto_focus_search_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchSubdomains(context: Context): Boolean {
|
fun searchSubDomains(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.subdomain_search_key),
|
return prefs.getBoolean(context.getString(R.string.subdomain_search_key),
|
||||||
context.resources.getBoolean(R.bool.subdomain_search_default))
|
context.resources.getBoolean(R.bool.subdomain_search_default))
|
||||||
@@ -352,6 +352,8 @@ object PreferencesUtil {
|
|||||||
context.resources.getBoolean(R.bool.search_option_username_default))
|
context.resources.getBoolean(R.bool.search_option_username_default))
|
||||||
searchInPasswords = prefs.getBoolean(context.getString(R.string.search_option_password_key),
|
searchInPasswords = prefs.getBoolean(context.getString(R.string.search_option_password_key),
|
||||||
context.resources.getBoolean(R.bool.search_option_password_default))
|
context.resources.getBoolean(R.bool.search_option_password_default))
|
||||||
|
searchInAppIds = prefs.getBoolean(context.getString(R.string.search_option_application_id_key),
|
||||||
|
context.resources.getBoolean(R.bool.search_option_application_id_default))
|
||||||
searchInUrls = prefs.getBoolean(context.getString(R.string.search_option_url_key),
|
searchInUrls = prefs.getBoolean(context.getString(R.string.search_option_url_key),
|
||||||
context.resources.getBoolean(R.bool.search_option_url_default))
|
context.resources.getBoolean(R.bool.search_option_url_default))
|
||||||
searchInExpired = prefs.getBoolean(context.getString(R.string.search_option_expired_key),
|
searchInExpired = prefs.getBoolean(context.getString(R.string.search_option_expired_key),
|
||||||
@@ -389,6 +391,8 @@ object PreferencesUtil {
|
|||||||
searchParameters.searchInUsernames)
|
searchParameters.searchInUsernames)
|
||||||
putBoolean(context.getString(R.string.search_option_password_key),
|
putBoolean(context.getString(R.string.search_option_password_key),
|
||||||
searchParameters.searchInPasswords)
|
searchParameters.searchInPasswords)
|
||||||
|
putBoolean(context.getString(R.string.search_option_application_id_key),
|
||||||
|
searchParameters.searchInAppIds)
|
||||||
putBoolean(context.getString(R.string.search_option_url_key),
|
putBoolean(context.getString(R.string.search_option_url_key),
|
||||||
searchParameters.searchInUrls)
|
searchParameters.searchInUrls)
|
||||||
putBoolean(context.getString(R.string.search_option_expired_key),
|
putBoolean(context.getString(R.string.search_option_expired_key),
|
||||||
@@ -686,6 +690,12 @@ object PreferencesUtil {
|
|||||||
context.resources.getBoolean(R.bool.keyboard_previous_lock_default))
|
context.resources.getBoolean(R.bool.keyboard_previous_lock_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isPasskeyCloseDatabaseEnable(context: Context): Boolean {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return prefs.getBoolean(context.getString(R.string.passkeys_close_database_key),
|
||||||
|
context.resources.getBoolean(R.bool.passkeys_close_database_default))
|
||||||
|
}
|
||||||
|
|
||||||
fun isPasskeyBackupEligibilityEnable(context: Context): Boolean {
|
fun isPasskeyBackupEligibilityEnable(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.passkeys_backup_eligibility_key),
|
return prefs.getBoolean(context.getString(R.string.passkeys_backup_eligibility_key),
|
||||||
@@ -854,6 +864,10 @@ object PreferencesUtil {
|
|||||||
context.getString(R.string.keyboard_previous_search_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.keyboard_previous_search_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.keyboard_previous_fill_in_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.keyboard_previous_fill_in_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.keyboard_previous_lock_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.keyboard_previous_lock_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.passkeys_close_database_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.passkeys_auto_select_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.passkeys_backup_eligibility_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.passkeys_backup_state_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.autofill_close_database_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.autofill_close_database_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.autofill_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.autofill_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.autofill_manual_selection_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.autofill_manual_selection_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
|||||||
@@ -70,8 +70,12 @@ open class SettingsActivity
|
|||||||
// To apply navigation bar with background color
|
// To apply navigation bar with background color
|
||||||
/* TODO Settings nav bar
|
/* TODO Settings nav bar
|
||||||
setTransparentNavigationBar {
|
setTransparentNavigationBar {
|
||||||
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
|
coordinatorLayout?.applyWindowInsets(EnumSet.of(
|
||||||
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
|
WindowInsetPosition.TOP_MARGINS,
|
||||||
|
WindowInsetPosition.BOTTOM_MARGINS,
|
||||||
|
WindowInsetPosition.START_MARGINS,
|
||||||
|
WindowInsetPosition.END_MARGINS,
|
||||||
|
))
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
mExternalFileHelper = ExternalFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
@@ -155,10 +159,6 @@ open class SettingsActivity
|
|||||||
return coordinatorLayout
|
return coordinatorLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun finishActivityIfDatabaseNotLoaded(): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
@@ -188,7 +188,7 @@ open class SettingsActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
|
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
|
||||||
assignPassword(mainCredential)
|
assignMainCredential(mainCredential)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
|
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
|
||||||
|
|||||||
@@ -95,11 +95,8 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
|
|||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
var initColor = database.customColor
|
||||||
|
|
||||||
database?.let {
|
|
||||||
var initColor = it.customColor
|
|
||||||
if (initColor != null) {
|
if (initColor != null) {
|
||||||
enableSwitchView.isChecked = true
|
enableSwitchView.isChecked = true
|
||||||
} else {
|
} else {
|
||||||
@@ -109,7 +106,6 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
|
|||||||
chromaColorView.currentColor = initColor
|
chromaColorView.currentColor = initColor
|
||||||
arguments?.putInt(ARG_INITIAL_COLOR, initColor)
|
arguments?.putInt(ARG_INITIAL_COLOR, initColor)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
super.onDialogClosed(database, positiveResult)
|
super.onDialogClosed(database, positiveResult)
|
||||||
|
|||||||
@@ -50,16 +50,14 @@ class DatabaseDataCompressionPreferenceDialogFragmentCompat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
setExplanationText(R.string.database_data_compression_summary)
|
setExplanationText(R.string.database_data_compression_summary)
|
||||||
|
|
||||||
mRecyclerView?.adapter = mCompressionAdapter
|
mRecyclerView?.adapter = mCompressionAdapter
|
||||||
|
compressionSelected = database.compressionAlgorithm
|
||||||
database?.let {
|
mCompressionAdapter?.setItems(
|
||||||
compressionSelected = it.compressionAlgorithm
|
items = database.availableCompressionAlgorithms,
|
||||||
mCompressionAdapter?.setItems(it.availableCompressionAlgorithms, compressionSelected)
|
itemUsed = compressionSelected
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
|
|||||||
@@ -24,9 +24,8 @@ import com.kunzisoft.keepass.database.ContextualDatabase
|
|||||||
|
|
||||||
class DatabaseDefaultUsernamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
class DatabaseDefaultUsernamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
inputText = database.defaultUsername
|
||||||
inputText = database?.defaultUsername?: ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
|
|||||||
@@ -24,9 +24,8 @@ import com.kunzisoft.keepass.database.ContextualDatabase
|
|||||||
|
|
||||||
class DatabaseDescriptionPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
class DatabaseDescriptionPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
inputText = database.description
|
||||||
inputText = database?.description ?: ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
|
|||||||
@@ -51,13 +51,10 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
database?.let {
|
|
||||||
algorithmSelected = database.encryptionAlgorithm
|
algorithmSelected = database.encryptionAlgorithm
|
||||||
mEncryptionAlgorithmAdapter?.setItems(database.availableEncryptionAlgorithms, algorithmSelected)
|
mEncryptionAlgorithmAdapter?.setItems(database.availableEncryptionAlgorithms, algorithmSelected)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
super.onDialogClosed(database, positiveResult)
|
super.onDialogClosed(database, positiveResult)
|
||||||
|
|||||||
@@ -54,12 +54,12 @@ class DatabaseKeyDerivationPreferenceDialogFragmentCompat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
database?.let {
|
|
||||||
kdfEngineSelected = database.kdfEngine
|
kdfEngineSelected = database.kdfEngine
|
||||||
mKdfAdapter?.setItems(database.availableKdfEngines, kdfEngineSelected)
|
mKdfAdapter?.setItems(
|
||||||
}
|
items = database.availableKdfEngines,
|
||||||
|
itemUsed = kdfEngineSelected
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
|
|||||||
@@ -31,9 +31,8 @@ class DatabaseMaxHistoryItemsPreferenceDialogFragmentCompat : DatabaseSavePrefer
|
|||||||
setExplanationText(R.string.max_history_items_summary)
|
setExplanationText(R.string.max_history_items_summary)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
val maxItemsDatabase = database.historyMaxItems
|
||||||
database?.historyMaxItems?.let { maxItemsDatabase ->
|
|
||||||
inputText = maxItemsDatabase.toString()
|
inputText = maxItemsDatabase.toString()
|
||||||
setSwitchAction({ isChecked ->
|
setSwitchAction({ isChecked ->
|
||||||
inputText = if (!isChecked) {
|
inputText = if (!isChecked) {
|
||||||
@@ -44,7 +43,6 @@ class DatabaseMaxHistoryItemsPreferenceDialogFragmentCompat : DatabaseSavePrefer
|
|||||||
showInputText(isChecked)
|
showInputText(isChecked)
|
||||||
}, maxItemsDatabase > NONE_MAX_HISTORY_ITEMS)
|
}, maxItemsDatabase > NONE_MAX_HISTORY_ITEMS)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
super.onDialogClosed(database, positiveResult)
|
super.onDialogClosed(database, positiveResult)
|
||||||
|
|||||||
@@ -34,9 +34,8 @@ class DatabaseMaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePrefere
|
|||||||
setExplanationText(R.string.max_history_size_summary)
|
setExplanationText(R.string.max_history_size_summary)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
val maxItemsDatabase = database.historyMaxSize
|
||||||
database?.historyMaxSize?.let { maxItemsDatabase ->
|
|
||||||
dataByte = DataByte(maxItemsDatabase, DataByte.ByteFormat.BYTE)
|
dataByte = DataByte(maxItemsDatabase, DataByte.ByteFormat.BYTE)
|
||||||
.toBetterByteFormat()
|
.toBetterByteFormat()
|
||||||
inputText = dataByte.number.toString()
|
inputText = dataByte.number.toString()
|
||||||
@@ -59,7 +58,6 @@ class DatabaseMaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePrefere
|
|||||||
showInputText(isChecked)
|
showInputText(isChecked)
|
||||||
}, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE)
|
}, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
super.onDialogClosed(database, positiveResult)
|
super.onDialogClosed(database, positiveResult)
|
||||||
|
|||||||
@@ -34,16 +34,13 @@ class DatabaseMemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreference
|
|||||||
setExplanationText(R.string.memory_usage_explanation)
|
setExplanationText(R.string.memory_usage_explanation)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
database?.let {
|
|
||||||
val memoryBytes = database.memoryUsage
|
val memoryBytes = database.memoryUsage
|
||||||
dataByte = DataByte(memoryBytes, DataByte.ByteFormat.BYTE)
|
dataByte = DataByte(memoryBytes, DataByte.ByteFormat.BYTE)
|
||||||
.toBetterByteFormat()
|
.toBetterByteFormat()
|
||||||
inputText = dataByte.number.toString()
|
inputText = dataByte.number.toString()
|
||||||
setUnitText(dataByte.format.stringId)
|
setUnitText(dataByte.format.stringId)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
if (positiveResult) {
|
if (positiveResult) {
|
||||||
|
|||||||
@@ -24,9 +24,8 @@ import com.kunzisoft.keepass.database.ContextualDatabase
|
|||||||
|
|
||||||
class DatabaseNamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
class DatabaseNamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
inputText = database.name
|
||||||
inputText = database?.name ?: ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
|
|||||||
@@ -31,9 +31,8 @@ class DatabaseParallelismPreferenceDialogFragmentCompat : DatabaseSavePreference
|
|||||||
setExplanationText(R.string.parallelism_explanation)
|
setExplanationText(R.string.parallelism_explanation)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
inputText = database.parallelism.toString()
|
||||||
inputText = database?.parallelism?.toString() ?: MIN_PARALLELISM.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
|
|||||||
@@ -48,13 +48,10 @@ class DatabaseRecycleBinGroupPreferenceDialogFragmentCompat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
database?.let {
|
|
||||||
mGroupRecycleBin = database.recycleBin
|
mGroupRecycleBin = database.recycleBin
|
||||||
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupRecycleBin)
|
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupRecycleBin)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemSelected(item: Group) {
|
override fun onItemSelected(item: Group) {
|
||||||
mGroupRecycleBin = item
|
mGroupRecycleBin = item
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ class DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat : DatabaseSavePre
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun newInstance(key: String): DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat {
|
fun newInstance(key: String): DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat {
|
||||||
|
|||||||
@@ -32,9 +32,8 @@ class DatabaseRoundsPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialo
|
|||||||
explanationText = getString(R.string.rounds_explanation)
|
explanationText = getString(R.string.rounds_explanation)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
inputText = database.numberKeyEncryptionRounds.toString()
|
||||||
inputText = database?.numberKeyEncryptionRounds?.toString() ?: MIN_ITERATIONS.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
override fun onDialogClosed(database: ContextualDatabase?, positiveResult: Boolean) {
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.kunzisoft.androidclearchroma.ChromaUtil
|
import com.kunzisoft.androidclearchroma.ChromaUtil
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
@@ -32,13 +35,15 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
|||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
abstract class DatabaseSavePreferenceDialogFragmentCompat
|
abstract class DatabaseSavePreferenceDialogFragmentCompat
|
||||||
: InputPreferenceDialogFragmentCompat(), DatabaseRetrieval {
|
: InputPreferenceDialogFragmentCompat(), DatabaseRetrieval {
|
||||||
|
|
||||||
private var mDatabaseAutoSaveEnable = true
|
private var mDatabaseAutoSaveEnable = true
|
||||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||||
private var mDatabase: ContextualDatabase? = null
|
protected val mDatabase: ContextualDatabase?
|
||||||
|
get() = mDatabaseViewModel.database
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
@@ -47,18 +52,32 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
mDatabaseViewModel.database.observe(this) { database ->
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
mDatabaseViewModel.actionState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||||
|
onDatabaseActionFinished(
|
||||||
|
uiState.database,
|
||||||
|
uiState.actionTask,
|
||||||
|
uiState.result
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
mDatabaseViewModel.databaseState.collect { database ->
|
||||||
|
database?.let {
|
||||||
onDatabaseRetrieved(database)
|
onDatabaseRetrieved(database)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
onDatabaseRetrieved(mDatabase)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
|
||||||
this.mDatabase = database
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
@@ -77,8 +96,10 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
|
|||||||
// To inherit to save element in database
|
// To inherit to save element in database
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveColor(oldColor: Int?,
|
protected fun saveColor(
|
||||||
newColor: Int?) {
|
oldColor: Int?,
|
||||||
|
newColor: Int?
|
||||||
|
) {
|
||||||
val oldColorString = if (oldColor != null)
|
val oldColorString = if (oldColor != null)
|
||||||
ChromaUtil.getFormattedColorString(oldColor, false)
|
ChromaUtil.getFormattedColorString(oldColor, false)
|
||||||
else
|
else
|
||||||
@@ -87,77 +108,158 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat
|
|||||||
ChromaUtil.getFormattedColorString(newColor, false)
|
ChromaUtil.getFormattedColorString(newColor, false)
|
||||||
else
|
else
|
||||||
""
|
""
|
||||||
mDatabaseViewModel.saveColor(oldColorString, newColorString, mDatabaseAutoSaveEnable)
|
mDatabaseViewModel.saveColor(
|
||||||
|
oldColorString,
|
||||||
|
newColorString,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveCompression(oldCompression: CompressionAlgorithm,
|
protected fun saveCompression(
|
||||||
|
oldCompression: CompressionAlgorithm,
|
||||||
newCompression: CompressionAlgorithm
|
newCompression: CompressionAlgorithm
|
||||||
) {
|
) {
|
||||||
mDatabaseViewModel.saveCompression(oldCompression, newCompression, mDatabaseAutoSaveEnable)
|
mDatabaseViewModel.saveCompression(
|
||||||
|
oldCompression,
|
||||||
|
newCompression,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveDefaultUsername(oldUsername: String,
|
protected fun saveDefaultUsername(
|
||||||
newUsername: String) {
|
oldUsername: String,
|
||||||
mDatabaseViewModel.saveDefaultUsername(oldUsername, newUsername, mDatabaseAutoSaveEnable)
|
newUsername: String
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveDefaultUsername(
|
||||||
|
oldUsername,
|
||||||
|
newUsername,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveDescription(oldDescription: String,
|
protected fun saveDescription(
|
||||||
newDescription: String) {
|
oldDescription: String,
|
||||||
mDatabaseViewModel.saveDescription(oldDescription, newDescription, mDatabaseAutoSaveEnable)
|
newDescription: String
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveDescription(
|
||||||
|
oldDescription,
|
||||||
|
newDescription,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveEncryption(oldEncryption: EncryptionAlgorithm,
|
protected fun saveEncryption(
|
||||||
newEncryptionAlgorithm: EncryptionAlgorithm) {
|
oldEncryption: EncryptionAlgorithm,
|
||||||
mDatabaseViewModel.saveEncryption(oldEncryption, newEncryptionAlgorithm, mDatabaseAutoSaveEnable)
|
newEncryptionAlgorithm: EncryptionAlgorithm
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveEncryption(
|
||||||
|
oldEncryption,
|
||||||
|
newEncryptionAlgorithm,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveKeyDerivation(oldKeyDerivation: KdfEngine,
|
protected fun saveKeyDerivation(
|
||||||
newKeyDerivation: KdfEngine) {
|
oldKeyDerivation: KdfEngine,
|
||||||
mDatabaseViewModel.saveKeyDerivation(oldKeyDerivation, newKeyDerivation, mDatabaseAutoSaveEnable)
|
newKeyDerivation: KdfEngine
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveKeyDerivation(
|
||||||
|
oldKeyDerivation,
|
||||||
|
newKeyDerivation,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveName(oldName: String,
|
protected fun saveName(
|
||||||
newName: String) {
|
oldName: String,
|
||||||
mDatabaseViewModel.saveName(oldName, newName, mDatabaseAutoSaveEnable)
|
newName: String
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveName(
|
||||||
|
oldName,
|
||||||
|
newName,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveRecycleBin(oldGroup: Group?,
|
protected fun saveRecycleBin(
|
||||||
newGroup: Group?) {
|
oldGroup: Group?,
|
||||||
mDatabaseViewModel.saveRecycleBin(oldGroup, newGroup, mDatabaseAutoSaveEnable)
|
newGroup: Group?
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveRecycleBin(
|
||||||
|
oldGroup,
|
||||||
|
newGroup,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun removeUnlinkedData() {
|
protected fun removeUnlinkedData() {
|
||||||
mDatabaseViewModel.removeUnlinkedData(mDatabaseAutoSaveEnable)
|
mDatabaseViewModel.removeUnlinkedData(mDatabaseAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveTemplatesGroup(oldGroup: Group?,
|
protected fun saveTemplatesGroup(
|
||||||
newGroup: Group?) {
|
oldGroup: Group?,
|
||||||
mDatabaseViewModel.saveTemplatesGroup(oldGroup, newGroup, mDatabaseAutoSaveEnable)
|
newGroup: Group?
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveTemplatesGroup(
|
||||||
|
oldGroup,
|
||||||
|
newGroup,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveMaxHistoryItems(oldNumber: Int,
|
protected fun saveMaxHistoryItems(
|
||||||
newNumber: Int) {
|
oldNumber: Int,
|
||||||
mDatabaseViewModel.saveMaxHistoryItems(oldNumber, newNumber, mDatabaseAutoSaveEnable)
|
newNumber: Int
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveMaxHistoryItems(
|
||||||
|
oldNumber,
|
||||||
|
newNumber,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveMaxHistorySize(oldNumber: Long,
|
protected fun saveMaxHistorySize(
|
||||||
newNumber: Long) {
|
oldNumber: Long,
|
||||||
mDatabaseViewModel.saveMaxHistorySize(oldNumber, newNumber, mDatabaseAutoSaveEnable)
|
newNumber: Long
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveMaxHistorySize(
|
||||||
|
oldNumber,
|
||||||
|
newNumber,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveMemoryUsage(oldNumber: Long,
|
protected fun saveMemoryUsage(
|
||||||
newNumber: Long) {
|
oldNumber: Long,
|
||||||
mDatabaseViewModel.saveMemoryUsage(oldNumber, newNumber, mDatabaseAutoSaveEnable)
|
newNumber: Long
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveMemoryUsage(
|
||||||
|
oldNumber,
|
||||||
|
newNumber,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveParallelism(oldNumber: Long,
|
protected fun saveParallelism(
|
||||||
newNumber: Long) {
|
oldNumber: Long,
|
||||||
mDatabaseViewModel.saveParallelism(oldNumber, newNumber, mDatabaseAutoSaveEnable)
|
newNumber: Long
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveParallelism(
|
||||||
|
oldNumber,
|
||||||
|
newNumber,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun saveIterations(oldNumber: Long,
|
protected fun saveIterations(
|
||||||
newNumber: Long) {
|
oldNumber: Long,
|
||||||
mDatabaseViewModel.saveIterations(oldNumber, newNumber, mDatabaseAutoSaveEnable)
|
newNumber: Long
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.saveIterations(
|
||||||
|
oldNumber,
|
||||||
|
newNumber,
|
||||||
|
mDatabaseAutoSaveEnable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -48,13 +48,10 @@ class DatabaseTemplatesGroupPreferenceDialogFragmentCompat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
database?.let {
|
|
||||||
mGroupTemplates = database.templatesGroup
|
mGroupTemplates = database.templatesGroup
|
||||||
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupTemplates)
|
mGroupsAdapter?.setItems(database.getAllGroupsWithoutRoot(), mGroupTemplates)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemSelected(item: Group) {
|
override fun onItemSelected(item: Group) {
|
||||||
mGroupTemplates = item
|
mGroupTemplates = item
|
||||||
|
|||||||
@@ -13,15 +13,13 @@ import com.kunzisoft.keepass.BuildConfig
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||||
import com.kunzisoft.keepass.education.Education
|
import com.kunzisoft.keepass.education.Education
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
|
||||||
|
|
||||||
object AppUtil {
|
object AppUtil {
|
||||||
|
|
||||||
|
fun randomRequestCode(): Int {
|
||||||
|
return (Math.random() * Integer.MAX_VALUE).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
fun Context.isExternalAppInstalled(packageName: String, showError: Boolean = true): Boolean {
|
fun Context.isExternalAppInstalled(packageName: String, showError: Boolean = true): Boolean {
|
||||||
try {
|
try {
|
||||||
this.applicationContext.packageManager.getPackageInfoCompat(
|
this.applicationContext.packageManager.getPackageInfoCompat(
|
||||||
@@ -79,29 +77,6 @@ object AppUtil {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the concrete web domain AKA without sub domain if needed
|
|
||||||
*/
|
|
||||||
fun getConcreteWebDomain(context: Context,
|
|
||||||
webDomain: String?,
|
|
||||||
concreteWebDomain: (String?) -> Unit) {
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
if (webDomain != null) {
|
|
||||||
// Warning, web domain can contains IP, don't crop in this case
|
|
||||||
if (PreferencesUtil.searchSubdomains(context)
|
|
||||||
|| Regex(SearchInfo.WEB_IP_REGEX).matches(webDomain)) {
|
|
||||||
concreteWebDomain.invoke(webDomain)
|
|
||||||
} else {
|
|
||||||
val publicSuffixList = PublicSuffixList(context)
|
|
||||||
concreteWebDomain.invoke(publicSuffixList
|
|
||||||
.getPublicSuffixPlusOne(webDomain).await())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
concreteWebDomain.invoke(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.P)
|
@RequiresApi(Build.VERSION_CODES.P)
|
||||||
fun getInstalledBrowsersWithSignatures(context: Context): List<AndroidPrivilegedApp> {
|
fun getInstalledBrowsersWithSignatures(context: Context): List<AndroidPrivilegedApp> {
|
||||||
val packageManager = context.packageManager
|
val packageManager = context.packageManager
|
||||||
|
|||||||
@@ -19,22 +19,31 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.utils
|
package com.kunzisoft.keepass.utils
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to invoke action in a separate IO thread
|
* Class to invoke action in a separate IO thread
|
||||||
*/
|
*/
|
||||||
class IOActionTask<T>(
|
class IOActionTask<T>(
|
||||||
private val action: () -> T ,
|
private val action: () -> T,
|
||||||
private val afterActionListener: ((T?) -> Unit)? = null) {
|
private val onActionComplete: ((T?) -> Unit)? = null,
|
||||||
|
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main),
|
||||||
private val mainScope = CoroutineScope(Dispatchers.Main)
|
private val exceptionHandler: CoroutineExceptionHandler? = null
|
||||||
|
) {
|
||||||
fun execute() {
|
fun execute() {
|
||||||
mainScope.launch {
|
scope.launch(exceptionHandler ?: EmptyCoroutineContext) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val asyncResult: Deferred<T?> = async {
|
val asyncResult: Deferred<T?> = async {
|
||||||
try {
|
exceptionHandler?.let {
|
||||||
|
action.invoke()
|
||||||
|
} ?: try {
|
||||||
action.invoke()
|
action.invoke()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -42,7 +51,7 @@ class IOActionTask<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
afterActionListener?.invoke(asyncResult.await())
|
onActionComplete?.invoke(asyncResult.await())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.kunzisoft.keepass.view
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
@@ -30,8 +29,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
private var searchTitle: CompoundButton
|
private var searchTitle: CompoundButton
|
||||||
private var searchUsername: CompoundButton
|
private var searchUsername: CompoundButton
|
||||||
private var searchPassword: CompoundButton
|
private var searchPassword: CompoundButton
|
||||||
|
private var searchApplicationId: CompoundButton
|
||||||
private var searchURL: CompoundButton
|
private var searchURL: CompoundButton
|
||||||
private var searchByURLDomain: Boolean = false
|
private var searchByURLDomain: Boolean = false
|
||||||
|
private var searchByURLSubDomain: Boolean = false
|
||||||
private var searchExpired: CompoundButton
|
private var searchExpired: CompoundButton
|
||||||
private var searchNotes: CompoundButton
|
private var searchNotes: CompoundButton
|
||||||
private var searchOther: CompoundButton
|
private var searchOther: CompoundButton
|
||||||
@@ -50,8 +51,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
this.searchInTitles = searchTitle.isChecked
|
this.searchInTitles = searchTitle.isChecked
|
||||||
this.searchInUsernames = searchUsername.isChecked
|
this.searchInUsernames = searchUsername.isChecked
|
||||||
this.searchInPasswords = searchPassword.isChecked
|
this.searchInPasswords = searchPassword.isChecked
|
||||||
|
this.searchInAppIds = searchApplicationId.isChecked
|
||||||
this.searchInUrls = searchURL.isChecked
|
this.searchInUrls = searchURL.isChecked
|
||||||
this.searchByDomain = searchByURLDomain
|
this.searchByDomain = searchByURLDomain
|
||||||
|
this.searchBySubDomain = searchByURLSubDomain
|
||||||
this.searchInExpired = searchExpired.isChecked
|
this.searchInExpired = searchExpired.isChecked
|
||||||
this.searchInNotes = searchNotes.isChecked
|
this.searchInNotes = searchNotes.isChecked
|
||||||
this.searchInOther = searchOther.isChecked
|
this.searchInOther = searchOther.isChecked
|
||||||
@@ -71,8 +74,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
searchTitle.isChecked = value.searchInTitles
|
searchTitle.isChecked = value.searchInTitles
|
||||||
searchUsername.isChecked = value.searchInUsernames
|
searchUsername.isChecked = value.searchInUsernames
|
||||||
searchPassword.isChecked = value.searchInPasswords
|
searchPassword.isChecked = value.searchInPasswords
|
||||||
|
searchApplicationId.isChecked = value.searchInAppIds
|
||||||
searchURL.isChecked = value.searchInUrls
|
searchURL.isChecked = value.searchInUrls
|
||||||
searchByURLDomain = value.searchByDomain
|
searchByURLDomain = value.searchByDomain
|
||||||
|
searchByURLSubDomain = value.searchBySubDomain
|
||||||
searchExpired.isChecked = value.searchInExpired
|
searchExpired.isChecked = value.searchInExpired
|
||||||
searchNotes.isChecked = value.searchInNotes
|
searchNotes.isChecked = value.searchInNotes
|
||||||
searchOther.isChecked = value.searchInOther
|
searchOther.isChecked = value.searchInOther
|
||||||
@@ -87,7 +92,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
var onParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = null
|
var onParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = null
|
||||||
private var mOnParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = {
|
private var mOnParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = {
|
||||||
// To recalculate height
|
// To recalculate height
|
||||||
if (searchAdvanceFiltersContainer?.visibility == View.VISIBLE) {
|
if (searchAdvanceFiltersContainer?.visibility == VISIBLE) {
|
||||||
searchAdvanceFiltersContainer?.expand(
|
searchAdvanceFiltersContainer?.expand(
|
||||||
false,
|
false,
|
||||||
searchAdvanceFiltersContainer?.getFullHeight()
|
searchAdvanceFiltersContainer?.getFullHeight()
|
||||||
@@ -110,6 +115,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
searchTitle = findViewById(R.id.search_chip_title)
|
searchTitle = findViewById(R.id.search_chip_title)
|
||||||
searchUsername = findViewById(R.id.search_chip_username)
|
searchUsername = findViewById(R.id.search_chip_username)
|
||||||
searchPassword = findViewById(R.id.search_chip_password)
|
searchPassword = findViewById(R.id.search_chip_password)
|
||||||
|
searchApplicationId = findViewById(R.id.search_chip_application_id)
|
||||||
searchURL = findViewById(R.id.search_chip_url)
|
searchURL = findViewById(R.id.search_chip_url)
|
||||||
searchExpired = findViewById(R.id.search_chip_expires)
|
searchExpired = findViewById(R.id.search_chip_expires)
|
||||||
searchNotes = findViewById(R.id.search_chip_note)
|
searchNotes = findViewById(R.id.search_chip_note)
|
||||||
@@ -125,7 +131,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
|
|
||||||
// Expand menu with button
|
// Expand menu with button
|
||||||
searchExpandButton.setOnClickListener {
|
searchExpandButton.setOnClickListener {
|
||||||
val isVisible = searchAdvanceFiltersContainer?.visibility == View.VISIBLE
|
val isVisible = searchAdvanceFiltersContainer?.visibility == VISIBLE
|
||||||
if (isVisible)
|
if (isVisible)
|
||||||
closeAdvancedFilters()
|
closeAdvancedFilters()
|
||||||
else
|
else
|
||||||
@@ -156,6 +162,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
searchParameters.searchInPasswords = isChecked
|
searchParameters.searchInPasswords = isChecked
|
||||||
mOnParametersChangeListener?.invoke(searchParameters)
|
mOnParametersChangeListener?.invoke(searchParameters)
|
||||||
}
|
}
|
||||||
|
searchApplicationId.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
searchParameters.searchInAppIds = isChecked
|
||||||
|
mOnParametersChangeListener?.invoke(searchParameters)
|
||||||
|
}
|
||||||
searchURL.setOnCheckedChangeListener { _, isChecked ->
|
searchURL.setOnCheckedChangeListener { _, isChecked ->
|
||||||
searchParameters.searchInUrls = isChecked
|
searchParameters.searchInUrls = isChecked
|
||||||
mOnParametersChangeListener?.invoke(searchParameters)
|
mOnParametersChangeListener?.invoke(searchParameters)
|
||||||
@@ -200,10 +210,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
searchNumbers.text = SearchHelper.showNumberOfSearchResults(numbers)
|
searchNumbers.text = SearchHelper.showNumberOfSearchResults(numbers)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCurrentGroupText(text: String) {
|
fun setCurrentGroupText(text: String?) {
|
||||||
val maxChars = 12
|
val maxChars = 12
|
||||||
searchCurrentGroup.text = when {
|
searchCurrentGroup.text = when {
|
||||||
text.isEmpty() -> context.getString(R.string.current_group)
|
text.isNullOrEmpty() -> context.getString(R.string.current_group)
|
||||||
text.length > maxChars -> text.substring(0, maxChars) + "…"
|
text.length > maxChars -> text.substring(0, maxChars) + "…"
|
||||||
else -> text
|
else -> text
|
||||||
}
|
}
|
||||||
@@ -213,6 +223,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
searchOther.isVisible = available
|
searchOther.isVisible = available
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun availableApplicationIds(available: Boolean) {
|
||||||
|
searchApplicationId.isVisible = available
|
||||||
|
}
|
||||||
|
|
||||||
fun availableTags(available: Boolean) {
|
fun availableTags(available: Boolean) {
|
||||||
searchTag.isVisible = available
|
searchTag.isVisible = available
|
||||||
}
|
}
|
||||||
@@ -243,16 +257,20 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showSearchExpandButton(show: Boolean) {
|
||||||
|
searchExpandButton.isVisible = show
|
||||||
|
}
|
||||||
|
|
||||||
override fun setVisibility(visibility: Int) {
|
override fun setVisibility(visibility: Int) {
|
||||||
when (visibility) {
|
when (visibility) {
|
||||||
View.VISIBLE -> {
|
VISIBLE -> {
|
||||||
searchAdvanceFiltersContainer?.visibility = View.GONE
|
searchAdvanceFiltersContainer?.visibility = GONE
|
||||||
searchContainer.showByFading()
|
searchContainer.showByFading()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
searchContainer.hideByFading()
|
searchContainer.hideByFading()
|
||||||
if (searchAdvanceFiltersContainer?.visibility == View.VISIBLE) {
|
if (searchAdvanceFiltersContainer?.visibility == VISIBLE) {
|
||||||
searchAdvanceFiltersContainer?.visibility = View.INVISIBLE
|
searchAdvanceFiltersContainer?.visibility = INVISIBLE
|
||||||
searchAdvanceFiltersContainer?.collapse()
|
searchAdvanceFiltersContainer?.collapse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import android.animation.AnimatorSet
|
|||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.PorterDuff
|
import android.graphics.PorterDuff
|
||||||
@@ -66,6 +65,7 @@ import com.kunzisoft.keepass.database.exception.LocalizedException
|
|||||||
import com.kunzisoft.keepass.database.helper.getLocalizedMessage
|
import com.kunzisoft.keepass.database.helper.getLocalizedMessage
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import java.util.EnumSet
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -317,9 +317,7 @@ fun CollapsingToolbarLayout.changeTitleColor(color: Int) {
|
|||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, applyWindowInsets: () -> Unit) {
|
fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, applyWindowInsets: () -> Unit) {
|
||||||
// Only in portrait
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1
|
|
||||||
&& resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
window.navigationBarColor = ContextCompat.getColor(this, R.color.surface_selector)
|
window.navigationBarColor = ContextCompat.getColor(this, R.color.surface_selector)
|
||||||
if (applyToStatusBar) {
|
if (applyToStatusBar) {
|
||||||
@@ -335,7 +333,7 @@ fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, appl
|
|||||||
/**
|
/**
|
||||||
* Apply a margin to a view to fix the window inset
|
* Apply a margin to a view to fix the window inset
|
||||||
*/
|
*/
|
||||||
fun View.applyWindowInsets(position: WindowInsetPosition = WindowInsetPosition.BOTTOM) {
|
fun View.applyWindowInsets(positions: EnumSet<WindowInsetPosition>) {
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
|
ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
|
||||||
var consumed = false
|
var consumed = false
|
||||||
|
|
||||||
@@ -351,52 +349,78 @@ fun View.applyWindowInsets(position: WindowInsetPosition = WindowInsetPosition.B
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()
|
||||||
when (position) {
|
or WindowInsetsCompat.Type.displayCutout()
|
||||||
WindowInsetPosition.TOP -> {
|
or WindowInsetsCompat.Type.ime())
|
||||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
|
||||||
|
val isRtl = layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||||
|
|
||||||
|
val wantTopMargins = positions.contains(WindowInsetPosition.TOP_MARGINS)
|
||||||
|
val wantBottomMargins = positions.contains(WindowInsetPosition.BOTTOM_MARGINS)
|
||||||
|
val wantStartMargins = positions.contains(WindowInsetPosition.START_MARGINS)
|
||||||
|
val wantEndMargins = positions.contains(WindowInsetPosition.END_MARGINS)
|
||||||
|
|
||||||
|
if (view.layoutParams is ViewGroup.MarginLayoutParams
|
||||||
|
&& (wantTopMargins || wantBottomMargins || wantStartMargins || wantEndMargins)) {
|
||||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
|
if (wantTopMargins) {
|
||||||
topMargin = insets.top
|
topMargin = insets.top
|
||||||
}
|
}
|
||||||
}
|
if (wantBottomMargins) {
|
||||||
}
|
|
||||||
WindowInsetPosition.LEGIT_TOP -> {
|
|
||||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
|
||||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
topMargin = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WindowInsetPosition.BOTTOM -> {
|
|
||||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
|
||||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin = insets.bottom
|
bottomMargin = insets.bottom
|
||||||
}
|
}
|
||||||
|
if (wantStartMargins) {
|
||||||
|
if (isRtl) {
|
||||||
|
rightMargin = insets.right
|
||||||
|
} else {
|
||||||
|
leftMargin = insets.left
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WindowInsetPosition.BOTTOM_IME -> {
|
if (wantEndMargins) {
|
||||||
val imeHeight = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
if (isRtl) {
|
||||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
leftMargin = insets.left
|
||||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
} else {
|
||||||
bottomMargin = if (imeHeight > 1) 0 else insets.bottom
|
rightMargin = insets.right
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WindowInsetPosition.TOP_BOTTOM_IME -> {
|
|
||||||
val imeHeight = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
|
||||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
|
||||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
topMargin = insets.top
|
|
||||||
bottomMargin = if (imeHeight > 1) imeHeight else 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val wantTopPadding = positions.contains(WindowInsetPosition.TOP_PADDING)
|
||||||
|
val wantBottomPadding = positions.contains(WindowInsetPosition.BOTTOM_PADDING)
|
||||||
|
val wantStartPadding = positions.contains(WindowInsetPosition.START_PADDING)
|
||||||
|
val wantEndPadding = positions.contains(WindowInsetPosition.END_PADDING)
|
||||||
|
|
||||||
|
if (wantTopPadding || wantBottomPadding || wantStartPadding || wantEndPadding) {
|
||||||
|
val topPadding = if (wantTopPadding) insets.top else 0
|
||||||
|
val bottomPadding = if (wantBottomPadding) insets.bottom else 0
|
||||||
|
var leftPadding = 0
|
||||||
|
var rightPadding = 0
|
||||||
|
|
||||||
|
if (wantStartPadding) {
|
||||||
|
if (isRtl) {
|
||||||
|
rightPadding = insets.right
|
||||||
|
} else {
|
||||||
|
leftPadding = insets.left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (wantEndPadding) {
|
||||||
|
if (isRtl) {
|
||||||
|
leftPadding = insets.left
|
||||||
|
} else {
|
||||||
|
rightPadding = insets.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPadding(leftPadding, topPadding, rightPadding, bottomPadding)
|
||||||
|
}
|
||||||
|
|
||||||
// If any of the children consumed the insets, return an appropriate value
|
// If any of the children consumed the insets, return an appropriate value
|
||||||
if (consumed) WindowInsetsCompat.CONSUMED else windowInsets
|
if (consumed) WindowInsetsCompat.CONSUMED else windowInsets
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class WindowInsetPosition {
|
enum class WindowInsetPosition {
|
||||||
TOP, BOTTOM, LEGIT_TOP, BOTTOM_IME, TOP_BOTTOM_IME
|
TOP_MARGINS, BOTTOM_MARGINS, START_MARGINS, END_MARGINS,
|
||||||
|
TOP_PADDING, BOTTOM_PADDING, START_PADDING, END_PADDING,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,214 +1,500 @@
|
|||||||
package com.kunzisoft.keepass.viewmodels
|
package com.kunzisoft.keepass.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import android.app.Application
|
||||||
import androidx.lifecycle.MutableLiveData
|
import android.net.Uri
|
||||||
import androidx.lifecycle.ViewModel
|
import android.os.Bundle
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
|
import com.kunzisoft.keepass.database.ProgressMessage
|
||||||
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.Entry
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
|
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class DatabaseViewModel: ViewModel() {
|
class DatabaseViewModel(application: Application): AndroidViewModel(application) {
|
||||||
|
|
||||||
val database : LiveData<ContextualDatabase?> get() = _database
|
private val mDatabaseState = MutableStateFlow<ContextualDatabase?>(null)
|
||||||
private val _database = MutableLiveData<ContextualDatabase?>()
|
val databaseState: StateFlow<ContextualDatabase?> = mDatabaseState
|
||||||
|
|
||||||
val actionFinished : LiveData<ActionResult> get() = _actionFinished
|
val database: ContextualDatabase?
|
||||||
private val _actionFinished = SingleLiveEvent<ActionResult>()
|
get() = databaseState.value
|
||||||
|
|
||||||
val saveDatabase : LiveData<Boolean> get() = _saveDatabase
|
private val mActionState = MutableStateFlow<ActionState>(ActionState.Loading)
|
||||||
private val _saveDatabase = SingleLiveEvent<Boolean>()
|
val actionState: StateFlow<ActionState> = mActionState
|
||||||
|
|
||||||
val mergeDatabase : LiveData<Boolean> get() = _mergeDatabase
|
private var mDatabaseTaskProvider: DatabaseTaskProvider = DatabaseTaskProvider(
|
||||||
private val _mergeDatabase = SingleLiveEvent<Boolean>()
|
context = application
|
||||||
|
)
|
||||||
|
|
||||||
val reloadDatabase : LiveData<Boolean> get() = _reloadDatabase
|
init {
|
||||||
private val _reloadDatabase = SingleLiveEvent<Boolean>()
|
mDatabaseTaskProvider.onDatabaseRetrieved = { databaseRetrieved ->
|
||||||
|
val databaseWasReloaded = databaseRetrieved?.wasReloaded == true
|
||||||
val saveName : LiveData<SuperString> get() = _saveName
|
if (databaseWasReloaded) {
|
||||||
private val _saveName = SingleLiveEvent<SuperString>()
|
mActionState.value = ActionState.OnDatabaseReloaded
|
||||||
|
}
|
||||||
val saveDescription : LiveData<SuperString> get() = _saveDescription
|
if (database == null || database != databaseRetrieved || databaseWasReloaded) {
|
||||||
private val _saveDescription = SingleLiveEvent<SuperString>()
|
databaseRetrieved?.wasReloaded = false
|
||||||
|
mDatabaseState.value = databaseRetrieved
|
||||||
val saveDefaultUsername : LiveData<SuperString> get() = _saveDefaultUsername
|
}
|
||||||
private val _saveDefaultUsername = SingleLiveEvent<SuperString>()
|
}
|
||||||
|
mDatabaseTaskProvider.onStartActionRequested = { bundle, actionTask ->
|
||||||
val saveColor : LiveData<SuperString> get() = _saveColor
|
mActionState.value = ActionState.OnDatabaseActionRequested(bundle, actionTask)
|
||||||
private val _saveColor = SingleLiveEvent<SuperString>()
|
}
|
||||||
|
mDatabaseTaskProvider.databaseInfoListener = object : DatabaseTaskNotificationService.DatabaseInfoListener {
|
||||||
val saveCompression : LiveData<SuperCompression> get() = _saveCompression
|
override fun onDatabaseInfoChanged(
|
||||||
private val _saveCompression = SingleLiveEvent<SuperCompression>()
|
previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
newDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
val removeUnlinkData : LiveData<Boolean> get() = _removeUnlinkData
|
readOnlyDatabase: Boolean
|
||||||
private val _removeUnlinkData = SingleLiveEvent<Boolean>()
|
) {
|
||||||
|
mActionState.value = ActionState.OnDatabaseInfoChanged(
|
||||||
val saveRecycleBin : LiveData<SuperGroup> get() = _saveRecycleBin
|
previousDatabaseInfo,
|
||||||
private val _saveRecycleBin = SingleLiveEvent<SuperGroup>()
|
newDatabaseInfo,
|
||||||
|
readOnlyDatabase
|
||||||
val saveTemplatesGroup : LiveData<SuperGroup> get() = _saveTemplatesGroup
|
)
|
||||||
private val _saveTemplatesGroup = SingleLiveEvent<SuperGroup>()
|
}
|
||||||
|
}
|
||||||
val saveMaxHistoryItems : LiveData<SuperInt> get() = _saveMaxHistoryItems
|
mDatabaseTaskProvider.actionTaskListener = object : DatabaseTaskNotificationService.ActionTaskListener {
|
||||||
private val _saveMaxHistoryItems = SingleLiveEvent<SuperInt>()
|
override fun onActionStarted(
|
||||||
|
database: ContextualDatabase,
|
||||||
val saveMaxHistorySize : LiveData<SuperLong> get() = _saveMaxHistorySize
|
progressMessage: ProgressMessage
|
||||||
private val _saveMaxHistorySize = SingleLiveEvent<SuperLong>()
|
) {
|
||||||
|
mActionState.value = ActionState.OnDatabaseActionStarted(database, progressMessage)
|
||||||
val saveEncryption : LiveData<SuperEncryption> get() = _saveEncryption
|
|
||||||
private val _saveEncryption = SingleLiveEvent<SuperEncryption>()
|
|
||||||
|
|
||||||
val saveKeyDerivation : LiveData<SuperKeyDerivation> get() = _saveKeyDerivation
|
|
||||||
private val _saveKeyDerivation = SingleLiveEvent<SuperKeyDerivation>()
|
|
||||||
|
|
||||||
val saveIterations : LiveData<SuperLong> get() = _saveIterations
|
|
||||||
private val _saveIterations = SingleLiveEvent<SuperLong>()
|
|
||||||
|
|
||||||
val saveMemoryUsage : LiveData<SuperLong> get() = _saveMemoryUsage
|
|
||||||
private val _saveMemoryUsage = SingleLiveEvent<SuperLong>()
|
|
||||||
|
|
||||||
val saveParallelism : LiveData<SuperLong> get() = _saveParallelism
|
|
||||||
private val _saveParallelism = SingleLiveEvent<SuperLong>()
|
|
||||||
|
|
||||||
|
|
||||||
fun defineDatabase(database: ContextualDatabase?) {
|
|
||||||
this._database.value = database
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onActionFinished(database: ContextualDatabase,
|
override fun onActionUpdated(
|
||||||
|
database: ContextualDatabase,
|
||||||
|
progressMessage: ProgressMessage
|
||||||
|
) {
|
||||||
|
mActionState.value = ActionState.OnDatabaseActionUpdated(database, progressMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActionStopped(database: ContextualDatabase?) {
|
||||||
|
mActionState.value = ActionState.OnDatabaseActionStopped(database)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActionFinished(
|
||||||
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result) {
|
result: ActionRunnable.Result
|
||||||
this._actionFinished.value = ActionResult(database, actionTask, result)
|
) {
|
||||||
|
mActionState.value = ActionState.OnDatabaseActionFinished(database, actionTask, result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveDatabase(save: Boolean) {
|
mDatabaseTaskProvider.registerProgressTask()
|
||||||
_saveDatabase.value = save
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mergeDatabase(save: Boolean) {
|
/*
|
||||||
_mergeDatabase.value = save
|
* Main database actions
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun loadDatabase(
|
||||||
|
databaseUri: Uri,
|
||||||
|
mainCredential: MainCredential,
|
||||||
|
readOnly: Boolean,
|
||||||
|
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||||
|
fixDuplicateUuid: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseLoad(
|
||||||
|
databaseUri,
|
||||||
|
mainCredential,
|
||||||
|
readOnly,
|
||||||
|
cipherEncryptDatabase,
|
||||||
|
fixDuplicateUuid
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createDatabase(
|
||||||
|
databaseUri: Uri,
|
||||||
|
mainCredential: MainCredential
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseCreate(databaseUri, mainCredential)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assignMainCredential(
|
||||||
|
databaseUri: Uri?,
|
||||||
|
mainCredential: MainCredential
|
||||||
|
) {
|
||||||
|
if (databaseUri != null) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseAssignCredential(databaseUri, mainCredential)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveDatabase(save: Boolean, saveToUri: Uri? = null) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSave(save, saveToUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mergeDatabase(
|
||||||
|
save: Boolean,
|
||||||
|
fromDatabaseUri: Uri? = null,
|
||||||
|
mainCredential: MainCredential? = null
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseMerge(save, fromDatabaseUri, mainCredential)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reloadDatabase(fixDuplicateUuid: Boolean) {
|
fun reloadDatabase(fixDuplicateUuid: Boolean) {
|
||||||
_reloadDatabase.value = fixDuplicateUuid
|
mDatabaseTaskProvider.askToStartDatabaseReload(
|
||||||
|
conditionToAsk = database?.dataModifiedSinceLastLoading != false
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseReload(fixDuplicateUuid)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveName(oldValue: String,
|
fun onDatabaseChangeValidated() {
|
||||||
|
mDatabaseTaskProvider.onDatabaseChangeValidated()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Nodes actions
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun createEntry(
|
||||||
|
newEntry: Entry,
|
||||||
|
parent: Group,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseCreateEntry(
|
||||||
|
newEntry,
|
||||||
|
parent,
|
||||||
|
save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateEntry(
|
||||||
|
oldEntry: Entry,
|
||||||
|
entryToUpdate: Entry,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseUpdateEntry(
|
||||||
|
oldEntry,
|
||||||
|
entryToUpdate,
|
||||||
|
save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreEntryHistory(
|
||||||
|
mainEntryId: NodeId<UUID>,
|
||||||
|
entryHistoryPosition: Int,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseRestoreEntryHistory(
|
||||||
|
mainEntryId,
|
||||||
|
entryHistoryPosition,
|
||||||
|
save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteEntryHistory(
|
||||||
|
mainEntryId: NodeId<UUID>,
|
||||||
|
entryHistoryPosition: Int,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseDeleteEntryHistory(
|
||||||
|
mainEntryId,
|
||||||
|
entryHistoryPosition,
|
||||||
|
save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createGroup(
|
||||||
|
newGroup: Group,
|
||||||
|
parent: Group,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseCreateGroup(
|
||||||
|
newGroup,
|
||||||
|
parent,
|
||||||
|
save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateGroup(
|
||||||
|
oldGroup: Group,
|
||||||
|
groupToUpdate: Group,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseUpdateGroup(
|
||||||
|
oldGroup,
|
||||||
|
groupToUpdate,
|
||||||
|
save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copyNodes(
|
||||||
|
nodesToCopy: List<Node>,
|
||||||
|
newParent: Group,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseCopyNodes(
|
||||||
|
nodesToCopy,
|
||||||
|
newParent,
|
||||||
|
save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveNodes(
|
||||||
|
nodesToMove: List<Node>,
|
||||||
|
newParent: Group,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseMoveNodes(
|
||||||
|
nodesToMove,
|
||||||
|
newParent,
|
||||||
|
save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteNodes(
|
||||||
|
nodes: List<Node>,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseDeleteNodes(
|
||||||
|
nodes,
|
||||||
|
save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Attributes
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun buildNewAttachment(): BinaryData? {
|
||||||
|
return database?.buildNewBinaryAttachment()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Settings actions
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun saveName(
|
||||||
|
oldValue: String,
|
||||||
newValue: String,
|
newValue: String,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
_saveName.value = SuperString(oldValue, newValue, save)
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveName(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveDescription(oldValue: String,
|
fun saveDescription(
|
||||||
|
oldValue: String,
|
||||||
newValue: String,
|
newValue: String,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
_saveDescription.value = SuperString(oldValue, newValue, save)
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveDescription(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveDefaultUsername(oldValue: String,
|
fun saveDefaultUsername(
|
||||||
|
oldValue: String,
|
||||||
newValue: String,
|
newValue: String,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
_saveDefaultUsername.value = SuperString(oldValue, newValue, save)
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveDefaultUsername(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveColor(oldValue: String,
|
fun saveColor(
|
||||||
|
oldValue: String,
|
||||||
newValue: String,
|
newValue: String,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
_saveColor.value = SuperString(oldValue, newValue, save)
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveColor(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveCompression(oldValue: CompressionAlgorithm,
|
fun saveCompression(
|
||||||
|
oldValue: CompressionAlgorithm,
|
||||||
newValue: CompressionAlgorithm,
|
newValue: CompressionAlgorithm,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
_saveCompression.value = SuperCompression(oldValue, newValue, save)
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveCompression(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeUnlinkedData(save: Boolean) {
|
fun removeUnlinkedData(save: Boolean) {
|
||||||
_removeUnlinkData.value = save
|
mDatabaseTaskProvider.startDatabaseRemoveUnlinkedData(save)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveRecycleBin(oldValue: Group?,
|
fun saveRecycleBin(
|
||||||
|
oldValue: Group?,
|
||||||
newValue: Group?,
|
newValue: Group?,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
_saveRecycleBin.value = SuperGroup(oldValue, newValue, save)
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveRecycleBin(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveTemplatesGroup(oldValue: Group?,
|
fun saveTemplatesGroup(
|
||||||
|
oldValue: Group?,
|
||||||
newValue: Group?,
|
newValue: Group?,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
_saveTemplatesGroup.value = SuperGroup(oldValue, newValue, save)
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveTemplatesGroup(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveMaxHistoryItems(oldValue: Int,
|
fun saveMaxHistoryItems(
|
||||||
|
oldValue: Int,
|
||||||
newValue: Int,
|
newValue: Int,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
_saveMaxHistoryItems.value = SuperInt(oldValue, newValue, save)
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveMaxHistoryItems(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveMaxHistorySize(oldValue: Long,
|
fun saveMaxHistorySize(
|
||||||
|
oldValue: Long,
|
||||||
newValue: Long,
|
newValue: Long,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
_saveMaxHistorySize.value = SuperLong(oldValue, newValue, save)
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveMaxHistorySize(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun saveEncryption(oldValue: EncryptionAlgorithm,
|
fun saveEncryption(
|
||||||
|
oldValue: EncryptionAlgorithm,
|
||||||
newValue: EncryptionAlgorithm,
|
newValue: EncryptionAlgorithm,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
_saveEncryption.value = SuperEncryption(oldValue, newValue, save)
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveEncryption(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveKeyDerivation(oldValue: KdfEngine,
|
fun saveKeyDerivation(
|
||||||
|
oldValue: KdfEngine,
|
||||||
newValue: KdfEngine,
|
newValue: KdfEngine,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
_saveKeyDerivation.value = SuperKeyDerivation(oldValue, newValue, save)
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveKeyDerivation(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveIterations(oldValue: Long,
|
fun saveIterations(
|
||||||
|
oldValue: Long,
|
||||||
newValue: Long,
|
newValue: Long,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
_saveIterations.value = SuperLong(oldValue, newValue, save)
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveIterations(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveMemoryUsage(oldValue: Long,
|
fun saveMemoryUsage(
|
||||||
|
oldValue: Long,
|
||||||
newValue: Long,
|
newValue: Long,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
_saveMemoryUsage.value = SuperLong(oldValue, newValue, save)
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveMemoryUsage(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveParallelism(oldValue: Long,
|
fun saveParallelism(
|
||||||
|
oldValue: Long,
|
||||||
newValue: Long,
|
newValue: Long,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
_saveParallelism.value = SuperLong(oldValue, newValue, save)
|
) {
|
||||||
|
mDatabaseTaskProvider.startDatabaseSaveParallelism(
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
save
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ActionResult(val database: ContextualDatabase,
|
/*
|
||||||
|
* Hardware Key
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun onChallengeResponded(challengeResponse: ByteArray?) {
|
||||||
|
mDatabaseTaskProvider.startChallengeResponded(
|
||||||
|
challengeResponse ?: ByteArray(0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
mDatabaseTaskProvider.unregisterProgressTask()
|
||||||
|
mDatabaseTaskProvider.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ActionState {
|
||||||
|
object Loading: ActionState()
|
||||||
|
object OnDatabaseReloaded: ActionState()
|
||||||
|
data class OnDatabaseActionRequested(
|
||||||
|
val bundle: Bundle? = null,
|
||||||
|
val actionTask: String
|
||||||
|
): ActionState()
|
||||||
|
data class OnDatabaseInfoChanged(
|
||||||
|
val previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
val newDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
val readOnlyDatabase: Boolean
|
||||||
|
): ActionState()
|
||||||
|
data class OnDatabaseActionStarted(
|
||||||
|
var database: ContextualDatabase,
|
||||||
|
val progressMessage: ProgressMessage
|
||||||
|
): ActionState()
|
||||||
|
data class OnDatabaseActionUpdated(
|
||||||
|
var database: ContextualDatabase,
|
||||||
|
val progressMessage: ProgressMessage
|
||||||
|
): ActionState()
|
||||||
|
data class OnDatabaseActionStopped(
|
||||||
|
var database: ContextualDatabase?
|
||||||
|
): ActionState()
|
||||||
|
data class OnDatabaseActionFinished(
|
||||||
|
var database: ContextualDatabase,
|
||||||
val actionTask: String,
|
val actionTask: String,
|
||||||
val result: ActionRunnable.Result)
|
val result: ActionRunnable.Result
|
||||||
data class SuperString(val oldValue: String,
|
): ActionState()
|
||||||
val newValue: String,
|
}
|
||||||
val save: Boolean)
|
|
||||||
data class SuperInt(val oldValue: Int,
|
|
||||||
val newValue: Int,
|
|
||||||
val save: Boolean)
|
|
||||||
data class SuperLong(val oldValue: Long,
|
|
||||||
val newValue: Long,
|
|
||||||
val save: Boolean)
|
|
||||||
data class SuperMerge(val fixDuplicateUuid: Boolean,
|
|
||||||
val save: Boolean)
|
|
||||||
data class SuperCompression(val oldValue: CompressionAlgorithm,
|
|
||||||
val newValue: CompressionAlgorithm,
|
|
||||||
val save: Boolean)
|
|
||||||
data class SuperEncryption(val oldValue: EncryptionAlgorithm,
|
|
||||||
val newValue: EncryptionAlgorithm,
|
|
||||||
val save: Boolean)
|
|
||||||
data class SuperKeyDerivation(val oldValue: KdfEngine,
|
|
||||||
val newValue: KdfEngine,
|
|
||||||
val save: Boolean)
|
|
||||||
data class SuperGroup(val oldValue: Group?,
|
|
||||||
val newValue: Group?,
|
|
||||||
val save: Boolean)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.kunzisoft.keepass.viewmodels
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
@@ -16,10 +17,11 @@ import com.kunzisoft.keepass.model.AttachmentState
|
|||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
|
||||||
import com.kunzisoft.keepass.model.StreamDirection
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.utils.IOActionTask
|
import com.kunzisoft.keepass.utils.IOActionTask
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
||||||
@@ -28,12 +30,18 @@ class EntryEditViewModel: NodeEditViewModel() {
|
|||||||
private var mEntryId: NodeId<UUID>? = null
|
private var mEntryId: NodeId<UUID>? = null
|
||||||
private var mParentId: NodeId<*>? = null
|
private var mParentId: NodeId<*>? = null
|
||||||
private var mRegisterInfo: RegisterInfo? = null
|
private var mRegisterInfo: RegisterInfo? = null
|
||||||
private var mSearchInfo: SearchInfo? = null
|
|
||||||
private var mParent: Group? = null
|
private var mParent: Group? = null
|
||||||
private var mEntry: Entry? = null
|
private var mEntry: Entry? = null
|
||||||
private var mIsTemplate: Boolean = false
|
private var mIsTemplate: Boolean = false
|
||||||
private val mTempAttachments = mutableListOf<EntryAttachmentState>()
|
private val mTempAttachments = mutableListOf<EntryAttachmentState>()
|
||||||
|
|
||||||
|
// To show dialog only one time
|
||||||
|
var backPressedAlreadyApproved = false
|
||||||
|
var warningOverwriteDataAlreadyApproved = false
|
||||||
|
|
||||||
|
// Useful to not relaunch a current action
|
||||||
|
private var actionLocked: Boolean = false
|
||||||
|
|
||||||
val templatesEntry : LiveData<TemplatesEntry?> get() = _templatesEntry
|
val templatesEntry : LiveData<TemplatesEntry?> get() = _templatesEntry
|
||||||
private val _templatesEntry = MutableLiveData<TemplatesEntry?>()
|
private val _templatesEntry = MutableLiveData<TemplatesEntry?>()
|
||||||
|
|
||||||
@@ -73,24 +81,28 @@ class EntryEditViewModel: NodeEditViewModel() {
|
|||||||
val onBinaryPreviewLoaded : LiveData<AttachmentPosition> get() = _onBinaryPreviewLoaded
|
val onBinaryPreviewLoaded : LiveData<AttachmentPosition> get() = _onBinaryPreviewLoaded
|
||||||
private val _onBinaryPreviewLoaded = SingleLiveEvent<AttachmentPosition>()
|
private val _onBinaryPreviewLoaded = SingleLiveEvent<AttachmentPosition>()
|
||||||
|
|
||||||
fun loadDatabase(database: ContextualDatabase?) {
|
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
|
||||||
loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo, mSearchInfo)
|
val uiState: StateFlow<UIState> = mUiState
|
||||||
|
|
||||||
|
fun loadTemplateEntry(database: ContextualDatabase?) {
|
||||||
|
loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadTemplateEntry(database: ContextualDatabase?,
|
fun loadTemplateEntry(
|
||||||
|
database: ContextualDatabase?,
|
||||||
entryId: NodeId<UUID>?,
|
entryId: NodeId<UUID>?,
|
||||||
parentId: NodeId<*>?,
|
parentId: NodeId<*>?,
|
||||||
registerInfo: RegisterInfo?,
|
registerInfo: RegisterInfo?
|
||||||
searchInfo: SearchInfo?) {
|
) {
|
||||||
this.mEntryId = entryId
|
this.mEntryId = entryId
|
||||||
this.mParentId = parentId
|
this.mParentId = parentId
|
||||||
this.mRegisterInfo = registerInfo
|
this.mRegisterInfo = registerInfo
|
||||||
this.mSearchInfo = searchInfo
|
|
||||||
|
|
||||||
database?.let {
|
database?.let {
|
||||||
mEntryId?.let {
|
mEntryId?.let {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
{
|
scope = viewModelScope,
|
||||||
|
action = {
|
||||||
// Create an Entry copy to modify from the database entry
|
// Create an Entry copy to modify from the database entry
|
||||||
mEntry = database.getEntryById(it)
|
mEntry = database.getEntryById(it)
|
||||||
// Retrieve the parent
|
// Retrieve the parent
|
||||||
@@ -105,21 +117,24 @@ class EntryEditViewModel: NodeEditViewModel() {
|
|||||||
database,
|
database,
|
||||||
entry,
|
entry,
|
||||||
mIsTemplate,
|
mIsTemplate,
|
||||||
registerInfo,
|
registerInfo
|
||||||
searchInfo
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ templatesEntry ->
|
onActionComplete = { templatesEntry ->
|
||||||
mEntryId = null
|
mEntryId = null
|
||||||
_templatesEntry.value = templatesEntry
|
_templatesEntry.value = templatesEntry
|
||||||
|
if (templatesEntry?.overwrittenData == true) {
|
||||||
|
mUiState.value = UIState.ShowOverwriteMessage
|
||||||
|
}
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
mParentId?.let {
|
mParentId?.let {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
{
|
scope = viewModelScope,
|
||||||
|
action = {
|
||||||
mParent = database.getGroupById(it)
|
mParent = database.getGroupById(it)
|
||||||
mParent?.let { parentGroup ->
|
mParent?.let { parentGroup ->
|
||||||
mEntry = database.createEntry()?.apply {
|
mEntry = database.createEntry()?.apply {
|
||||||
@@ -145,12 +160,11 @@ class EntryEditViewModel: NodeEditViewModel() {
|
|||||||
database,
|
database,
|
||||||
mEntry,
|
mEntry,
|
||||||
mIsTemplate,
|
mIsTemplate,
|
||||||
registerInfo,
|
registerInfo
|
||||||
searchInfo
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ templatesEntry ->
|
onActionComplete = { templatesEntry ->
|
||||||
mParentId = null
|
mParentId = null
|
||||||
_templatesEntry.value = templatesEntry
|
_templatesEntry.value = templatesEntry
|
||||||
}
|
}
|
||||||
@@ -159,33 +173,37 @@ class EntryEditViewModel: NodeEditViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decodeTemplateEntry(database: ContextualDatabase,
|
private fun decodeTemplateEntry(
|
||||||
|
database: ContextualDatabase,
|
||||||
entry: Entry?,
|
entry: Entry?,
|
||||||
isTemplate: Boolean,
|
isTemplate: Boolean,
|
||||||
registerInfo: RegisterInfo?,
|
registerInfo: RegisterInfo?
|
||||||
searchInfo: SearchInfo?): TemplatesEntry {
|
): TemplatesEntry {
|
||||||
val templates = database.getTemplates(isTemplate)
|
val templates = database.getTemplates(isTemplate)
|
||||||
val entryTemplate = entry?.let { database.getTemplate(it) }
|
val entryTemplate = entry?.let { database.getTemplate(it) }
|
||||||
?: Template.STANDARD
|
?: Template.STANDARD
|
||||||
var entryInfo: EntryInfo? = null
|
var entryInfo: EntryInfo? = null
|
||||||
|
var overwrittenData = false
|
||||||
// Decode the entry / load entry info
|
// Decode the entry / load entry info
|
||||||
entry?.let {
|
entry?.let {
|
||||||
database.decodeEntryWithTemplateConfiguration(it).let { entry ->
|
database.decodeEntryWithTemplateConfiguration(it).let { entry ->
|
||||||
// Load entry info
|
// Load entry info
|
||||||
entry.getEntryInfo(database, true).let { tempEntryInfo ->
|
entry.getEntryInfo(database, true).let { tempEntryInfo ->
|
||||||
// Retrieve data from registration
|
// Retrieve data from registration
|
||||||
// TODO only save registration
|
|
||||||
searchInfo?.let { tempSearchInfo ->
|
|
||||||
tempEntryInfo.saveSearchInfo(database, tempSearchInfo)
|
|
||||||
}
|
|
||||||
registerInfo?.let { regInfo ->
|
registerInfo?.let { regInfo ->
|
||||||
tempEntryInfo.saveRegisterInfo(database, regInfo)
|
overwrittenData = tempEntryInfo.saveRegisterInfo(database, regInfo)
|
||||||
}
|
}
|
||||||
entryInfo = tempEntryInfo
|
entryInfo = tempEntryInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return TemplatesEntry(isTemplate, templates, entryTemplate, entryInfo)
|
return TemplatesEntry(
|
||||||
|
isTemplate,
|
||||||
|
templates,
|
||||||
|
entryTemplate,
|
||||||
|
entryInfo,
|
||||||
|
overwrittenData
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeTemplate(template: Template) {
|
fun changeTemplate(template: Template) {
|
||||||
@@ -198,9 +216,16 @@ class EntryEditViewModel: NodeEditViewModel() {
|
|||||||
_requestEntryInfoUpdate.value = EntryUpdate(database, mEntry, mParent)
|
_requestEntryInfoUpdate.value = EntryUpdate(database, mEntry, mParent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun unlockAction() {
|
||||||
|
actionLocked = false
|
||||||
|
}
|
||||||
|
|
||||||
fun saveEntryInfo(database: ContextualDatabase?, entry: Entry?, parent: Group?, entryInfo: EntryInfo) {
|
fun saveEntryInfo(database: ContextualDatabase?, entry: Entry?, parent: Group?, entryInfo: EntryInfo) {
|
||||||
|
if (actionLocked.not()) {
|
||||||
|
actionLocked = true
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
{
|
scope = viewModelScope,
|
||||||
|
action = {
|
||||||
removeTempAttachmentsNotCompleted(entryInfo)
|
removeTempAttachmentsNotCompleted(entryInfo)
|
||||||
entry?.let { oldEntry ->
|
entry?.let { oldEntry ->
|
||||||
// Create a clone
|
// Create a clone
|
||||||
@@ -230,13 +255,14 @@ class EntryEditViewModel: NodeEditViewModel() {
|
|||||||
EntrySave(oldEntry, newEntry, parent)
|
EntrySave(oldEntry, newEntry, parent)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ entrySave ->
|
onActionComplete = { entrySave ->
|
||||||
entrySave?.let {
|
entrySave?.let {
|
||||||
_onEntrySaved.value = it
|
_onEntrySaved.value = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun removeTempAttachmentsNotCompleted(entryInfo: EntryInfo) {
|
private fun removeTempAttachmentsNotCompleted(entryInfo: EntryInfo) {
|
||||||
// Do not save entry in upload progression
|
// Do not save entry in upload progression
|
||||||
@@ -322,10 +348,13 @@ class EntryEditViewModel: NodeEditViewModel() {
|
|||||||
_onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition)
|
_onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class TemplatesEntry(val isTemplate: Boolean,
|
data class TemplatesEntry(
|
||||||
|
val isTemplate: Boolean,
|
||||||
val templates: List<Template>,
|
val templates: List<Template>,
|
||||||
val defaultTemplate: Template,
|
val defaultTemplate: Template,
|
||||||
val entryInfo: EntryInfo?)
|
val entryInfo: EntryInfo?,
|
||||||
|
val overwrittenData: Boolean = false
|
||||||
|
)
|
||||||
data class EntryUpdate(val database: ContextualDatabase?, val entry: Entry?, val parent: Group?)
|
data class EntryUpdate(val database: ContextualDatabase?, val entry: Entry?, val parent: Group?)
|
||||||
data class EntrySave(val oldEntry: Entry, val newEntry: Entry, val parent: Group?)
|
data class EntrySave(val oldEntry: Entry, val newEntry: Entry, val parent: Group?)
|
||||||
data class FieldEdition(val oldField: Field?, val newField: Field?)
|
data class FieldEdition(val oldField: Field?, val newField: Field?)
|
||||||
@@ -333,6 +362,11 @@ class EntryEditViewModel: NodeEditViewModel() {
|
|||||||
data class AttachmentUpload(val attachmentToUploadUri: Uri, val attachment: Attachment)
|
data class AttachmentUpload(val attachmentToUploadUri: Uri, val attachment: Attachment)
|
||||||
data class AttachmentPosition(val entryAttachmentState: EntryAttachmentState, val viewPosition: Float)
|
data class AttachmentPosition(val entryAttachmentState: EntryAttachmentState, val viewPosition: Float)
|
||||||
|
|
||||||
|
sealed class UIState {
|
||||||
|
object Loading: UIState()
|
||||||
|
object ShowOverwriteMessage: UIState()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = EntryEditViewModel::class.java.name
|
private val TAG = EntryEditViewModel::class.java.name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/activity_entry_container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:filterTouchesWhenObscured="true">
|
android:filterTouchesWhenObscured="true">
|
||||||
|
|||||||
@@ -101,6 +101,13 @@
|
|||||||
android:checked="false"
|
android:checked="false"
|
||||||
style="@style/KeepassDXStyle.Chip.Filter"
|
style="@style/KeepassDXStyle.Chip.Filter"
|
||||||
android:text="@string/entry_password"/>
|
android:text="@string/entry_password"/>
|
||||||
|
<com.google.android.material.chip.Chip
|
||||||
|
android:id="@+id/search_chip_application_id"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checked="false"
|
||||||
|
style="@style/KeepassDXStyle.Chip.Filter"
|
||||||
|
android:text="@string/entry_application_id"/>
|
||||||
<com.google.android.material.chip.Chip
|
<com.google.android.material.chip.Chip
|
||||||
android:id="@+id/search_chip_url"
|
android:id="@+id/search_chip_url"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -452,7 +452,6 @@
|
|||||||
<string name="menu_form_filling_settings">ملء النموذج</string>
|
<string name="menu_form_filling_settings">ملء النموذج</string>
|
||||||
<string name="menu_reload_database">أعد تحميل البيانات</string>
|
<string name="menu_reload_database">أعد تحميل البيانات</string>
|
||||||
<string name="menu_external_icon">أيقونة خارجية</string>
|
<string name="menu_external_icon">أيقونة خارجية</string>
|
||||||
<string name="registration_mode">وضع التسجيل</string>
|
|
||||||
<string name="import_app_properties_title">استورد خصائص التطبيق</string>
|
<string name="import_app_properties_title">استورد خصائص التطبيق</string>
|
||||||
<string name="import_app_properties_summary">اختر ملفًا لاستيراد إعدادات التطبيق</string>
|
<string name="import_app_properties_summary">اختر ملفًا لاستيراد إعدادات التطبيق</string>
|
||||||
<string name="export_app_properties_title">صدّر إعدادات التطبيق</string>
|
<string name="export_app_properties_title">صدّر إعدادات التطبيق</string>
|
||||||
|
|||||||
@@ -485,7 +485,6 @@
|
|||||||
<string name="search_mode">Axtarış modu</string>
|
<string name="search_mode">Axtarış modu</string>
|
||||||
<string name="save_mode">Yadda saxlama modu</string>
|
<string name="save_mode">Yadda saxlama modu</string>
|
||||||
<string name="selection_mode">Seçim modu</string>
|
<string name="selection_mode">Seçim modu</string>
|
||||||
<string name="registration_mode">Qeydiyyat modu</string>
|
|
||||||
<string name="remember_database_locations_title">Məlumat bazalarının yerlərini xatırlayın</string>
|
<string name="remember_database_locations_title">Məlumat bazalarının yerlərini xatırlayın</string>
|
||||||
<string name="remember_database_locations_summary">Məlumat bazalarının harada saxlanıldığını izlə</string>
|
<string name="remember_database_locations_summary">Məlumat bazalarının harada saxlanıldığını izlə</string>
|
||||||
<string name="remember_hardware_key_summary">Aparat-təchizat açarlarının harada istifadə olunduğunu izlə</string>
|
<string name="remember_hardware_key_summary">Aparat-təchizat açarlarının harada istifadə olunduğunu izlə</string>
|
||||||
|
|||||||
@@ -336,7 +336,6 @@
|
|||||||
<string name="menu_keystore_remove_key">Izbrišite ključ za otključavanje uređaja</string>
|
<string name="menu_keystore_remove_key">Izbrišite ključ za otključavanje uređaja</string>
|
||||||
<string name="subdomain_search_summary">Pretražujte veb domene sa ograničenjima poddomena</string>
|
<string name="subdomain_search_summary">Pretražujte veb domene sa ograničenjima poddomena</string>
|
||||||
<string name="export_app_properties_title">Izvezite podešavanja aplikacije</string>
|
<string name="export_app_properties_title">Izvezite podešavanja aplikacije</string>
|
||||||
<string name="registration_mode">Režim registracije</string>
|
|
||||||
<string name="remember_database_locations_title">Zapamtite lokacije baza podataka</string>
|
<string name="remember_database_locations_title">Zapamtite lokacije baza podataka</string>
|
||||||
<string name="remember_hardware_key_title">Zapamtite hardverske ključeve</string>
|
<string name="remember_hardware_key_title">Zapamtite hardverske ključeve</string>
|
||||||
<string name="remember_hardware_key_summary">Vodi evidenciju o korišćenim hardverskim ključevima</string>
|
<string name="remember_hardware_key_summary">Vodi evidenciju o korišćenim hardverskim ključevima</string>
|
||||||
|
|||||||
@@ -296,7 +296,6 @@
|
|||||||
<string name="search_mode">Рэжым пошуку</string>
|
<string name="search_mode">Рэжым пошуку</string>
|
||||||
<string name="save_mode">Рэжым захавання</string>
|
<string name="save_mode">Рэжым захавання</string>
|
||||||
<string name="selection_mode">Рэжым выбару</string>
|
<string name="selection_mode">Рэжым выбару</string>
|
||||||
<string name="registration_mode">Рэжым рэгістрацыі</string>
|
|
||||||
<string name="remember_database_locations_title">Запамінаць размяшчэнне баз дадзеных</string>
|
<string name="remember_database_locations_title">Запамінаць размяшчэнне баз дадзеных</string>
|
||||||
<string name="remember_database_locations_summary">Адсочвае, дзе захоўваюцца базы дадзеных</string>
|
<string name="remember_database_locations_summary">Адсочвае, дзе захоўваюцца базы дадзеных</string>
|
||||||
<string name="remember_keyfile_locations_title">Запамінаць размяшчэнне файлаў ключоў</string>
|
<string name="remember_keyfile_locations_title">Запамінаць размяшчэнне файлаў ключоў</string>
|
||||||
|
|||||||
@@ -462,7 +462,6 @@
|
|||||||
<string name="error_invalid_OTP">Неприемлива тайна за OTP.</string>
|
<string name="error_invalid_OTP">Неприемлива тайна за OTP.</string>
|
||||||
<string name="error_no_name">Въведете име.</string>
|
<string name="error_no_name">Въведете име.</string>
|
||||||
<string name="hide_broken_locations_summary">Скрива вече несъществуващи хранилища от списъка с последно отваряните</string>
|
<string name="hide_broken_locations_summary">Скрива вече несъществуващи хранилища от списъка с последно отваряните</string>
|
||||||
<string name="registration_mode">Режим регистрация</string>
|
|
||||||
<string name="remember_database_locations_title">Запомняне използваните хранилища</string>
|
<string name="remember_database_locations_title">Запомняне използваните хранилища</string>
|
||||||
<string name="show_recent_files_title">Показване на последните хранилища</string>
|
<string name="show_recent_files_title">Показване на последните хранилища</string>
|
||||||
<string name="search_mode">Режим търсене</string>
|
<string name="search_mode">Режим търсене</string>
|
||||||
|
|||||||
@@ -209,7 +209,6 @@
|
|||||||
<string name="search_mode">অনুসন্ধান মোড</string>
|
<string name="search_mode">অনুসন্ধান মোড</string>
|
||||||
<string name="save_mode">সেভ মোড</string>
|
<string name="save_mode">সেভ মোড</string>
|
||||||
<string name="selection_mode">নির্বাচন মোড</string>
|
<string name="selection_mode">নির্বাচন মোড</string>
|
||||||
<string name="registration_mode">রেজিস্ট্রেশন মোড</string>
|
|
||||||
<string name="remember_keyfile_locations_summary">কী ফাইলগুলি কোথায় সংরক্ষণ করা হয় তা ট্র্যাক রাখে</string>
|
<string name="remember_keyfile_locations_summary">কী ফাইলগুলি কোথায় সংরক্ষণ করা হয় তা ট্র্যাক রাখে</string>
|
||||||
<string name="show_recent_files_title">সাম্প্রতিক ফাইল দেখান</string>
|
<string name="show_recent_files_title">সাম্প্রতিক ফাইল দেখান</string>
|
||||||
<string name="show_recent_files_summary">সাম্প্রতিক ডাটাবেসের অবস্থান দেখান</string>
|
<string name="show_recent_files_summary">সাম্প্রতিক ডাটাবেসের অবস্থান দেখান</string>
|
||||||
|
|||||||
@@ -347,7 +347,6 @@
|
|||||||
<string name="place_of_issue">Lloc d\'expedició</string>
|
<string name="place_of_issue">Lloc d\'expedició</string>
|
||||||
<string name="style_brightness_summary">Escull tema clar o fosc</string>
|
<string name="style_brightness_summary">Escull tema clar o fosc</string>
|
||||||
<string name="hardware_key">Clau física</string>
|
<string name="hardware_key">Clau física</string>
|
||||||
<string name="registration_mode">Mode de registre</string>
|
|
||||||
<string name="ignore_chars_filter">Ignora caràcters</string>
|
<string name="ignore_chars_filter">Ignora caràcters</string>
|
||||||
<string name="ask">Pregunta</string>
|
<string name="ask">Pregunta</string>
|
||||||
<string name="searchable">Cercable</string>
|
<string name="searchable">Cercable</string>
|
||||||
|
|||||||
@@ -480,7 +480,6 @@
|
|||||||
<string name="biometric_security_update_required">Vyžadována aktualizace biometrického zabezpečení.</string>
|
<string name="biometric_security_update_required">Vyžadována aktualizace biometrického zabezpečení.</string>
|
||||||
<string name="configure_biometric">Žádné přihlašovací ani biometrické údaje nejsou registrovány.</string>
|
<string name="configure_biometric">Žádné přihlašovací ani biometrické údaje nejsou registrovány.</string>
|
||||||
<string name="warning_empty_recycle_bin">Trvale odstranit všechny uzly z koše\?</string>
|
<string name="warning_empty_recycle_bin">Trvale odstranit všechny uzly z koše\?</string>
|
||||||
<string name="registration_mode">Registrace</string>
|
|
||||||
<string name="save_mode">Režim ukládání</string>
|
<string name="save_mode">Režim ukládání</string>
|
||||||
<string name="search_mode">Vyhledávání</string>
|
<string name="search_mode">Vyhledávání</string>
|
||||||
<string name="error_field_name_already_exists">Jméno kolonky již existuje.</string>
|
<string name="error_field_name_already_exists">Jméno kolonky již existuje.</string>
|
||||||
|
|||||||
@@ -475,7 +475,6 @@
|
|||||||
\n
|
\n
|
||||||
\nDatabasen kan blive meget stor og reducere ydeevnen med denne overførelse.</string>
|
\nDatabasen kan blive meget stor og reducere ydeevnen med denne overførelse.</string>
|
||||||
<string name="warning_empty_recycle_bin">Slet alle noder permanent fra papirkurven\?</string>
|
<string name="warning_empty_recycle_bin">Slet alle noder permanent fra papirkurven\?</string>
|
||||||
<string name="registration_mode">Registreringstilstand</string>
|
|
||||||
<string name="save_mode">Gem-tilstand</string>
|
<string name="save_mode">Gem-tilstand</string>
|
||||||
<string name="search_mode">Søgetilstand</string>
|
<string name="search_mode">Søgetilstand</string>
|
||||||
<string name="error_registration_read_only">Det er ikke tilladt at gemme et nyt element i en skrivebeskyttet database.</string>
|
<string name="error_registration_read_only">Det er ikke tilladt at gemme et nyt element i en skrivebeskyttet database.</string>
|
||||||
|
|||||||
@@ -486,7 +486,6 @@
|
|||||||
<string name="notification">Benachrichtigung</string>
|
<string name="notification">Benachrichtigung</string>
|
||||||
<string name="biometric_security_update_required">Biometrische Sicherheitsaktualisierung erforderlich.</string>
|
<string name="biometric_security_update_required">Biometrische Sicherheitsaktualisierung erforderlich.</string>
|
||||||
<string name="configure_biometric">Es sind weder Biometrie- noch Geräteanmeldedaten registriert.</string>
|
<string name="configure_biometric">Es sind weder Biometrie- noch Geräteanmeldedaten registriert.</string>
|
||||||
<string name="registration_mode">Registrierungsmodus</string>
|
|
||||||
<string name="save_mode">Speichermodus</string>
|
<string name="save_mode">Speichermodus</string>
|
||||||
<string name="search_mode">Suchmodus</string>
|
<string name="search_mode">Suchmodus</string>
|
||||||
<string name="error_registration_read_only">Speichern eines neuen Elements in einer schreibgeschützten Datenbank ist unzulässig.</string>
|
<string name="error_registration_read_only">Speichern eines neuen Elements in einer schreibgeschützten Datenbank ist unzulässig.</string>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
--><resources>
|
--><resources>
|
||||||
<string name="feedback">Σχόλια</string>
|
<string name="feedback">Σχόλια</string>
|
||||||
<string name="homepage">Αρχική Σελίδα</string>
|
<string name="homepage">Αρχική Σελίδα</string>
|
||||||
<string name="about_description">Το KeePassDX είναι μία εφαρμογή Android του διαχειριστή κωδικών KeePass</string>
|
<string name="about_description">Υλοποίηση του διαχειριστή κωδικών πρόσβασης KeePass για Android.</string>
|
||||||
<string name="accept">Αποδοχή</string>
|
<string name="accept">Αποδοχή</string>
|
||||||
<string name="add_entry">Προσθήκη καταχώρησης</string>
|
<string name="add_entry">Προσθήκη καταχώρησης</string>
|
||||||
<string name="add_group">Προσθήκη ομάδας</string>
|
<string name="add_group">Προσθήκη ομάδας</string>
|
||||||
@@ -479,7 +479,6 @@
|
|||||||
<string name="biometric_security_update_required">Απαιτείται ενημέρωση βιομετρικής ασφάλειας.</string>
|
<string name="biometric_security_update_required">Απαιτείται ενημέρωση βιομετρικής ασφάλειας.</string>
|
||||||
<string name="configure_biometric">Κανένα πιστοποιητικό βιομετρίας ή συσκευής δεν είναι εγγεγραμμένο.</string>
|
<string name="configure_biometric">Κανένα πιστοποιητικό βιομετρίας ή συσκευής δεν είναι εγγεγραμμένο.</string>
|
||||||
<string name="warning_empty_recycle_bin">Να διαγραφούν οριστικά όλοι οι κόμβοι από τον κάδο ανακύκλωσης;</string>
|
<string name="warning_empty_recycle_bin">Να διαγραφούν οριστικά όλοι οι κόμβοι από τον κάδο ανακύκλωσης;</string>
|
||||||
<string name="registration_mode">Τρόπος εγγραφής</string>
|
|
||||||
<string name="save_mode">Λειτουργία αποθήκευσης</string>
|
<string name="save_mode">Λειτουργία αποθήκευσης</string>
|
||||||
<string name="search_mode">Λειτουργία αναζήτησης</string>
|
<string name="search_mode">Λειτουργία αναζήτησης</string>
|
||||||
<string name="error_registration_read_only">Η αποθήκευση ενός νέου αντικειμένου δεν επιτρέπεται σε μια βάση δεδομένων μόνο για ανάγνωση.</string>
|
<string name="error_registration_read_only">Η αποθήκευση ενός νέου αντικειμένου δεν επιτρέπεται σε μια βάση δεδομένων μόνο για ανάγνωση.</string>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
--><resources>
|
--><resources>
|
||||||
<string name="feedback">Comentarios</string>
|
<string name="feedback">Comentarios</string>
|
||||||
<string name="homepage">Página de inicio</string>
|
<string name="homepage">Página de inicio</string>
|
||||||
<string name="about_description">Implementación para Android del gestor de contraseñas KeePass</string>
|
<string name="about_description">Implementación para Android del gestor de contraseñas KeePass.</string>
|
||||||
<string name="accept">Aceptar</string>
|
<string name="accept">Aceptar</string>
|
||||||
<string name="add_entry">Añadir apunte</string>
|
<string name="add_entry">Añadir apunte</string>
|
||||||
<string name="add_group">Añadir grupo</string>
|
<string name="add_group">Añadir grupo</string>
|
||||||
@@ -452,7 +452,6 @@
|
|||||||
<string name="configure_biometric">No se ha inscrito ninguna credencial biométrica o del dispositivo.</string>
|
<string name="configure_biometric">No se ha inscrito ninguna credencial biométrica o del dispositivo.</string>
|
||||||
<string name="warning_empty_keyfile_explanation">El contenido del archivo de clave nunca debe modificarse y, en el mejor de los casos, debe contener datos generados al azar.</string>
|
<string name="warning_empty_keyfile_explanation">El contenido del archivo de clave nunca debe modificarse y, en el mejor de los casos, debe contener datos generados al azar.</string>
|
||||||
<string name="warning_empty_recycle_bin">¿Borrar permanentemente todos los nodos de la papelera de reciclaje\?</string>
|
<string name="warning_empty_recycle_bin">¿Borrar permanentemente todos los nodos de la papelera de reciclaje\?</string>
|
||||||
<string name="registration_mode">Modo de registro</string>
|
|
||||||
<string name="save_mode">Modo de guardado</string>
|
<string name="save_mode">Modo de guardado</string>
|
||||||
<string name="search_mode">Modo de búsqueda</string>
|
<string name="search_mode">Modo de búsqueda</string>
|
||||||
<string name="contains_duplicate_uuid_procedure">¿Solucionar el problema generando nuevos UUID para que los duplicados continúen?</string>
|
<string name="contains_duplicate_uuid_procedure">¿Solucionar el problema generando nuevos UUID para que los duplicados continúen?</string>
|
||||||
@@ -690,4 +689,5 @@
|
|||||||
<string name="generate_keyfile">Generar archivo de claves</string>
|
<string name="generate_keyfile">Generar archivo de claves</string>
|
||||||
<string name="recursive_number_entries_title">Número recursivo de entradas</string>
|
<string name="recursive_number_entries_title">Número recursivo de entradas</string>
|
||||||
<string name="hide_templates_summary">Las plantillas no se muestran</string>
|
<string name="hide_templates_summary">Las plantillas no se muestran</string>
|
||||||
|
<string name="error_otp_secret_length">La clave secreta debe tener al menos %1$d caracteres.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -383,7 +383,6 @@
|
|||||||
<string name="search_mode">Otsinguviis</string>
|
<string name="search_mode">Otsinguviis</string>
|
||||||
<string name="save_mode">Salvestusviis</string>
|
<string name="save_mode">Salvestusviis</string>
|
||||||
<string name="selection_mode">Valikuviis</string>
|
<string name="selection_mode">Valikuviis</string>
|
||||||
<string name="registration_mode">Registreerimisviis</string>
|
|
||||||
<string name="invalid_credentials">Salasõna või võtmefaili ei õnnestunud lugeda.</string>
|
<string name="invalid_credentials">Salasõna või võtmefaili ei õnnestunud lugeda.</string>
|
||||||
<string name="protection">Kaitse</string>
|
<string name="protection">Kaitse</string>
|
||||||
<string name="underline">Allajoonitud</string>
|
<string name="underline">Allajoonitud</string>
|
||||||
|
|||||||
@@ -355,7 +355,6 @@
|
|||||||
<string name="read_only_warning">Zure fitxategi nabigatzailearen arabera, KeePassDXek ez du baimenduta datuak idaztea.</string>
|
<string name="read_only_warning">Zure fitxategi nabigatzailearen arabera, KeePassDXek ez du baimenduta datuak idaztea.</string>
|
||||||
<string name="save_mode">Gorde modua</string>
|
<string name="save_mode">Gorde modua</string>
|
||||||
<string name="selection_mode">Hautaketa modua</string>
|
<string name="selection_mode">Hautaketa modua</string>
|
||||||
<string name="registration_mode">Erregistro-modua</string>
|
|
||||||
<string name="remember_database_locations_title">Gogoratu datu-baseen kokalekuak</string>
|
<string name="remember_database_locations_title">Gogoratu datu-baseen kokalekuak</string>
|
||||||
<string name="remember_database_locations_summary">Erregistratu datu-baseen kokapenak</string>
|
<string name="remember_database_locations_summary">Erregistratu datu-baseen kokapenak</string>
|
||||||
<string name="remember_keyfile_locations_title">Gogoratu fitxategi-gakoen kokapenak</string>
|
<string name="remember_keyfile_locations_title">Gogoratu fitxategi-gakoen kokapenak</string>
|
||||||
|
|||||||
@@ -271,7 +271,6 @@
|
|||||||
<string name="import_app_properties_summary">یک فایل برای وارد کردن ویژگی های برنامه انتخاب کنید</string>
|
<string name="import_app_properties_summary">یک فایل برای وارد کردن ویژگی های برنامه انتخاب کنید</string>
|
||||||
<string name="import_app_properties_title">وارد کردن ویژگی های برنامه</string>
|
<string name="import_app_properties_title">وارد کردن ویژگی های برنامه</string>
|
||||||
<string name="export_app_properties_title">صادر کردن ویژگی های برنامه</string>
|
<string name="export_app_properties_title">صادر کردن ویژگی های برنامه</string>
|
||||||
<string name="registration_mode">حالت ثبت</string>
|
|
||||||
<string name="save_mode">حالت ذخیره</string>
|
<string name="save_mode">حالت ذخیره</string>
|
||||||
<string name="search_mode">حالت جستجو</string>
|
<string name="search_mode">حالت جستجو</string>
|
||||||
<string name="menu_external_icon">نماد خارجی</string>
|
<string name="menu_external_icon">نماد خارجی</string>
|
||||||
|
|||||||
@@ -431,7 +431,6 @@
|
|||||||
<string name="warning_large_keyfile">Ei ole suositeltavaa lisätä suurta avantiedostos, sillä se voi estää tietokantaa avautumasta.</string>
|
<string name="warning_large_keyfile">Ei ole suositeltavaa lisätä suurta avantiedostos, sillä se voi estää tietokantaa avautumasta.</string>
|
||||||
<string name="warning_empty_keyfile_explanation">Avaintiedoston sisältöä ei tulisi koskaan muuttaa, ja parhaassa tapauksessa sen tulisi sisältää satunnaisesti tuotettua tietoa.</string>
|
<string name="warning_empty_keyfile_explanation">Avaintiedoston sisältöä ei tulisi koskaan muuttaa, ja parhaassa tapauksessa sen tulisi sisältää satunnaisesti tuotettua tietoa.</string>
|
||||||
<string name="save_mode">Tallennustila</string>
|
<string name="save_mode">Tallennustila</string>
|
||||||
<string name="registration_mode">Rekisteröintitila</string>
|
|
||||||
<string name="export_app_properties_summary">Luo tiedosto, johon sovellusasetukset viedään</string>
|
<string name="export_app_properties_summary">Luo tiedosto, johon sovellusasetukset viedään</string>
|
||||||
<string name="export_app_properties_title">Vie sovellusasetukset</string>
|
<string name="export_app_properties_title">Vie sovellusasetukset</string>
|
||||||
<string name="description_app_properties">KeePassDX ominaisuudet sovellusasetusten hallintaan</string>
|
<string name="description_app_properties">KeePassDX ominaisuudet sovellusasetusten hallintaan</string>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user