mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
205 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7182c2e66d | ||
|
|
503316bc70 | ||
|
|
d5af59f2c7 | ||
|
|
cb3ac1ad3a | ||
|
|
e161080e4c | ||
|
|
ceab7f917b | ||
|
|
41d8066c4c | ||
|
|
e373cbd776 | ||
|
|
05ea6b6b10 | ||
|
|
28eed3ae71 | ||
|
|
535c67ac9b | ||
|
|
68027a6e15 | ||
|
|
12dea6b499 | ||
|
|
d8bd078a02 | ||
|
|
439bc109b0 | ||
|
|
b1ec93ceb5 | ||
|
|
44b9aa0e48 | ||
|
|
0a59063027 | ||
|
|
5db4608abd | ||
|
|
6ece2aa6cb | ||
|
|
95ee45f666 | ||
|
|
556e90b8d8 | ||
|
|
42841e6247 | ||
|
|
324c82248a | ||
|
|
94f5a47918 | ||
|
|
3f70990956 | ||
|
|
d77635e572 | ||
|
|
0f26f1b751 | ||
|
|
456bc22138 | ||
|
|
b6fe91e396 | ||
|
|
0fb4d26949 | ||
|
|
3b3583a416 | ||
|
|
cac1d576c8 | ||
|
|
9cba0d0a48 | ||
|
|
3bf11e9dc0 | ||
|
|
edbb160ac6 | ||
|
|
ee111dc63c | ||
|
|
b06edb756a | ||
|
|
fd745494e0 | ||
|
|
702bf3f479 | ||
|
|
6adc02a91f | ||
|
|
55bae5a130 | ||
|
|
1aae817b17 | ||
|
|
8afc8c23fb | ||
|
|
36e473b139 | ||
|
|
e529723f86 | ||
|
|
6c5112c142 | ||
|
|
77ac68a603 | ||
|
|
e816f40872 | ||
|
|
65313f114b | ||
|
|
ef1f1342f5 | ||
|
|
9205fe6c08 | ||
|
|
75df3e81fe | ||
|
|
47fffbadb5 | ||
|
|
78354473fa | ||
|
|
8d7efb44b5 | ||
|
|
cab00b3d8c | ||
|
|
8efab01336 | ||
|
|
66feb8beb4 | ||
|
|
ca4cccffeb | ||
|
|
51b1760c50 | ||
|
|
42e1bda365 | ||
|
|
194b6b557a | ||
|
|
e7bc439997 | ||
|
|
ef76cce0ac | ||
|
|
63eec6d969 | ||
|
|
719ae74c06 | ||
|
|
37cf424eb8 | ||
|
|
3a87f7ba9d | ||
|
|
275428d825 | ||
|
|
7b9dac86ca | ||
|
|
46496ee2cc | ||
|
|
110cc402cc | ||
|
|
587f006259 | ||
|
|
ec45c0df81 | ||
|
|
dc575aeca4 | ||
|
|
a3f790f000 | ||
|
|
4e9e188d02 | ||
|
|
abb17efae4 | ||
|
|
16be990502 | ||
|
|
d1a496f9a3 | ||
|
|
cd730fcfef | ||
|
|
1b65bf665b | ||
|
|
6f4735790c | ||
|
|
040666f89d | ||
|
|
227cb800c3 | ||
|
|
0052569a14 | ||
|
|
f7561c4888 | ||
|
|
92200f19e7 | ||
|
|
4c01f18a62 | ||
|
|
a6ce3e49fe | ||
|
|
1b3a5d1bf6 | ||
|
|
db7bdc63c8 | ||
|
|
7aca550f02 | ||
|
|
b60980b3fd | ||
|
|
b578c2c584 | ||
|
|
fc45bd624e | ||
|
|
17be3d9d2c | ||
|
|
5b3a38a7bc | ||
|
|
4403835d50 | ||
|
|
b7a3d3eb46 | ||
|
|
29846b22fe | ||
|
|
32d235e8c7 | ||
|
|
cb982b3513 | ||
|
|
d7ed6c26dd | ||
|
|
8ff19f7e68 | ||
|
|
729e062c3a | ||
|
|
7d0340ac07 | ||
|
|
01960e74c1 | ||
|
|
8e40250985 | ||
|
|
8ae2edb61a | ||
|
|
0baa7bcbf1 | ||
|
|
fffee48918 | ||
|
|
515abb6e14 | ||
|
|
6c1c3ff87f | ||
|
|
5b65575c7a | ||
|
|
0f258fc5f8 | ||
|
|
206bc661dc | ||
|
|
d0e35b109e | ||
|
|
61769c4f20 | ||
|
|
95778ee5f4 | ||
|
|
155030fdca | ||
|
|
98237ef76c | ||
|
|
f0a7b38199 | ||
|
|
9fc5e6751b | ||
|
|
4c1630312b | ||
|
|
d397c5c996 | ||
|
|
f6c41b5a60 | ||
|
|
06eb5c01c3 | ||
|
|
7df49f91e8 | ||
|
|
96dcbb0ce7 | ||
|
|
5f828fb986 | ||
|
|
533d663938 | ||
|
|
ae788503a9 | ||
|
|
cf0acd9c73 | ||
|
|
0857f2f1cf | ||
|
|
c05d412bdb | ||
|
|
c8e0ce717d | ||
|
|
cc3485b201 | ||
|
|
81ea7080c2 | ||
|
|
76ff6f5ae0 | ||
|
|
0c0d0b7a6f | ||
|
|
ec8cf1f6b7 | ||
|
|
da44310d1b | ||
|
|
4bd9c84bb0 | ||
|
|
3b1269a770 | ||
|
|
7c986ccee8 | ||
|
|
903bad8f36 | ||
|
|
4b9577437c | ||
|
|
c316011fbc | ||
|
|
3fb1f18c22 | ||
|
|
53935058f5 | ||
|
|
a3860c9581 | ||
|
|
dc20899d26 | ||
|
|
62ac3ddb75 | ||
|
|
b792a61bf9 | ||
|
|
aae9f9e1cb | ||
|
|
d098bf5e6a | ||
|
|
b0e8a3ecd9 | ||
|
|
4efa684022 | ||
|
|
f2c8082990 | ||
|
|
1abba80045 | ||
|
|
68564a2b75 | ||
|
|
385b701b38 | ||
|
|
11c9a1d707 | ||
|
|
c5aef6b561 | ||
|
|
dfcf73cfd0 | ||
|
|
7fc9389700 | ||
|
|
d1af7349bc | ||
|
|
0fd955197d | ||
|
|
cbf33507d1 | ||
|
|
bc60a5d97e | ||
|
|
57596b2991 | ||
|
|
43b3602a52 | ||
|
|
c09ec961b8 | ||
|
|
d140b453b2 | ||
|
|
9eb42636ec | ||
|
|
ee2d663fce | ||
|
|
8e83615a22 | ||
|
|
0ff129c5ca | ||
|
|
e49858439f | ||
|
|
3df07f7f47 | ||
|
|
ff9e179593 | ||
|
|
7539fee04b | ||
|
|
71a339a58f | ||
|
|
9ef2ea016b | ||
|
|
de4936a16d | ||
|
|
574d2b8904 | ||
|
|
1f3f7634e7 | ||
|
|
3c0725baff | ||
|
|
b0e1411012 | ||
|
|
39daf4714d | ||
|
|
4706afa823 | ||
|
|
25977d389d | ||
|
|
f760110569 | ||
|
|
133e78fe97 | ||
|
|
d92e0c8620 | ||
|
|
62fdb69d6b | ||
|
|
def57c9fb2 | ||
|
|
21c9c898c3 | ||
|
|
1f03c922c2 | ||
|
|
3f6ae6bdac | ||
|
|
60615ee1eb | ||
|
|
92b0d1bfa9 | ||
|
|
237988dc1f |
24
CHANGELOG
24
CHANGELOG
@@ -1,3 +1,27 @@
|
||||
KeePassDX(2.5beta31)
|
||||
* Add write permission to keep compatibility with old file managers
|
||||
* Fix autofill for apps
|
||||
* Auto search for autofill
|
||||
* New keyfile input
|
||||
* Icon to hide keyfile input
|
||||
* New lock button
|
||||
* Setting to hide lock button in user interface
|
||||
* Clickable links in notes
|
||||
* Fix autofill for key-value pairs
|
||||
|
||||
KeePassDX(2.5beta30)
|
||||
* Fix Lock after screen off (wait 1.5 seconds)
|
||||
* Upgrade autofill algorithm
|
||||
* Fix ANR during file verifications
|
||||
|
||||
KeePassDX(2.5beta29)
|
||||
* Upgrade autofill algorithm
|
||||
* Delete registered KeyFile after save new credentials
|
||||
* Fix title and username entry view refresh after an update
|
||||
* Fix database lock request (open notification always active)
|
||||
* Allow empty title in entries
|
||||
* Add expiration datetime
|
||||
|
||||
KeePassDX(2.5beta28)
|
||||
* Fix read only database
|
||||
* Upgrade to Android SDK 29
|
||||
|
||||
@@ -30,7 +30,7 @@ KeePassDX is a **free open source password manager for Android**, which helps yo
|
||||
|
||||
Yes, KeePassDX is under **free license (OSI certified)** and **without advertising**. You can have a look at its full source and check whether the encryption algorithms are implemented correctly.
|
||||
|
||||
*Note : If you access the application from a store, visual features may not be available to incentivize the contribution to the work of open source projects. These optional visuals are accessible after a donation (and a small congratulation message :) or the purchase of an extended version, but do not worry, the main features remain completely free. If you contribute to the project and you do not have access to the themes, do not hesitate to contact me at [contact@kunzisoft.com](contact@kunzisoft.com), I will give you the procedure.*
|
||||
*Note : If you access the application from a store, visual styles may not be available to encourage contribution to the work of open source projects. These optional styles are accessible after a contribution (and a congratulation message :) or the purchase of an extended version, but do not worry, the main features remain completely free. If you contribute to the project and you do not have access to the themes, do not hesitate to contact me at [contact@kunzisoft.com](contact@kunzisoft.com), I will give you the procedure.*
|
||||
|
||||
## Contributions
|
||||
|
||||
@@ -53,9 +53,9 @@ You can contribute in different ways to help us on our work.
|
||||
alt="Get it on Google Play"
|
||||
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
|
||||
|
||||
## F.A.Q.
|
||||
## Frequently Asked Questions
|
||||
|
||||
Other questions? You can read the [F.A.Q.](https://github.com/Kunzisoft/KeePassDX/wiki/F.A.Q.)
|
||||
Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ)
|
||||
|
||||
## Other devices
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 29
|
||||
versionCode = 28
|
||||
versionName = "2.5beta28"
|
||||
versionCode = 31
|
||||
versionName = "2.5RC1"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
@@ -86,7 +86,7 @@ android {
|
||||
}
|
||||
|
||||
def spongycastleVersion = "1.58.0.0"
|
||||
def room_version = "2.2.4"
|
||||
def room_version = "2.2.5"
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
@@ -97,6 +97,7 @@ dependencies {
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.biometric:biometric:1.0.1'
|
||||
implementation "androidx.core:core-ktx:1.2.0"
|
||||
// To upgrade with style
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<uses-permission
|
||||
android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission
|
||||
android:maxSdkVersion="18"
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<application
|
||||
@@ -138,12 +137,12 @@
|
||||
<activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity"
|
||||
android:configChanges="keyboardHidden" />
|
||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsAutofillActivity" />
|
||||
<activity android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
||||
<activity android:name="com.kunzisoft.keepass.magikeyboard.KeyboardLauncherActivity"
|
||||
android:label="@string/keyboard_name"
|
||||
android:exported="true">
|
||||
</activity>
|
||||
<activity android:name="com.kunzisoft.keepass.settings.MagikIMESettings"
|
||||
<activity android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
||||
android:label="@string/keyboard_setting_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
@@ -52,7 +52,6 @@ import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.settings.SettingsAutofillActivity
|
||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
@@ -73,6 +72,7 @@ class EntryActivity : LockingActivity() {
|
||||
private var historyView: View? = null
|
||||
private var entryContentsView: EntryContentsView? = null
|
||||
private var entryProgress: ProgressBar? = null
|
||||
private var lockView: View? = null
|
||||
private var toolbar: Toolbar? = null
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
@@ -124,6 +124,11 @@ class EntryActivity : LockingActivity() {
|
||||
entryContentsView = findViewById(R.id.entry_contents)
|
||||
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
||||
entryProgress = findViewById(R.id.entry_progress)
|
||||
lockView = findViewById(R.id.lock_button)
|
||||
|
||||
lockView?.setOnClickListener {
|
||||
lockAndExit()
|
||||
}
|
||||
|
||||
// Init the clipboard helper
|
||||
clipboardHelper = ClipboardHelper(this)
|
||||
@@ -148,6 +153,13 @@ class EntryActivity : LockingActivity() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Show the lock button
|
||||
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
// Get Entry from UUID
|
||||
try {
|
||||
val keyEntry: NodeId<UUID>? = intent.getParcelableExtra(KEY_ENTRY)
|
||||
@@ -462,8 +474,7 @@ class EntryActivity : LockingActivity() {
|
||||
getString(R.string.entry_user_name)))
|
||||
},
|
||||
{
|
||||
// Launch autofill settings
|
||||
startActivity(Intent(this@EntryActivity, SettingsAutofillActivity::class.java))
|
||||
performedNextEducation(entryActivityEducation, menu)
|
||||
})
|
||||
|
||||
if (!entryCopyEducationPerformed) {
|
||||
@@ -526,10 +537,6 @@ class EntryActivity : LockingActivity() {
|
||||
!mReadOnly && mAutoSaveEnable)
|
||||
}
|
||||
}
|
||||
R.id.menu_lock -> {
|
||||
lockAndExit()
|
||||
return true
|
||||
}
|
||||
R.id.menu_save_database -> {
|
||||
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
|
||||
}
|
||||
@@ -541,12 +548,10 @@ class EntryActivity : LockingActivity() {
|
||||
|
||||
override fun finish() {
|
||||
// Transit data in previous Activity after an update
|
||||
/*
|
||||
TODO Slowdown when add entry as result
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry);
|
||||
onFinish(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
|
||||
*/
|
||||
Intent().apply {
|
||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry)
|
||||
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, this)
|
||||
}
|
||||
super.finish()
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.DatePickerDialog
|
||||
import android.app.TimePickerDialog
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
@@ -26,13 +28,15 @@ import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ScrollView
|
||||
import android.widget.DatePicker
|
||||
import android.widget.TimePicker
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.ActionMenuView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.*
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
@@ -52,12 +56,15 @@ import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.view.EntryEditContentsView
|
||||
import com.kunzisoft.keepass.view.showActionError
|
||||
import org.joda.time.DateTime
|
||||
import java.util.*
|
||||
|
||||
class EntryEditActivity : LockingActivity(),
|
||||
IconPickerDialogFragment.IconPickerListener,
|
||||
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
||||
SetOTPDialogFragment.CreateOtpListener {
|
||||
SetOTPDialogFragment.CreateOtpListener,
|
||||
DatePickerDialog.OnDateSetListener,
|
||||
TimePickerDialog.OnTimeSetListener {
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
@@ -70,9 +77,11 @@ class EntryEditActivity : LockingActivity(),
|
||||
|
||||
// Views
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var scrollView: ScrollView? = null
|
||||
private var scrollView: NestedScrollView? = null
|
||||
private var entryEditContentsView: EntryEditContentsView? = null
|
||||
private var entryEditAddToolBar: ActionMenuView? = null
|
||||
private var saveView: View? = null
|
||||
private var lockView: View? = null
|
||||
|
||||
// Education
|
||||
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
||||
@@ -94,6 +103,22 @@ class EntryEditActivity : LockingActivity(),
|
||||
|
||||
entryEditContentsView = findViewById(R.id.entry_edit_contents)
|
||||
entryEditContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
||||
entryEditContentsView?.onDateClickListener = View.OnClickListener {
|
||||
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
|
||||
val dateTime = DateTime(expiresDate)
|
||||
val defaultYear = dateTime.year
|
||||
val defaultMonth = dateTime.monthOfYear-1
|
||||
val defaultDay = dateTime.dayOfMonth
|
||||
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
|
||||
.show(supportFragmentManager, "DatePickerFragment")
|
||||
}
|
||||
}
|
||||
|
||||
lockView = findViewById(R.id.lock_button)
|
||||
lockView?.setOnClickListener {
|
||||
lockAndExit()
|
||||
}
|
||||
|
||||
// Focus view to reinitialize timeout
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(entryEditContentsView)
|
||||
|
||||
@@ -167,17 +192,46 @@ class EntryEditActivity : LockingActivity(),
|
||||
// Add listener to the icon
|
||||
entryEditContentsView?.setOnIconViewClickListener { IconPickerDialogFragment.launch(this@EntryEditActivity) }
|
||||
|
||||
// Generate password button
|
||||
entryEditContentsView?.setOnPasswordGeneratorClickListener { openPasswordGenerator() }
|
||||
// Bottom Bar
|
||||
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
|
||||
entryEditAddToolBar?.apply {
|
||||
menuInflater.inflate(R.menu.entry_edit, menu)
|
||||
|
||||
menu.findItem(R.id.menu_add_field).apply {
|
||||
val allowCustomField = mNewEntry?.allowCustomFields() == true
|
||||
isEnabled = allowCustomField
|
||||
isVisible = allowCustomField
|
||||
}
|
||||
|
||||
menu.findItem(R.id.menu_add_otp).apply {
|
||||
val allowOTP = mDatabase?.allowOTP == true
|
||||
isEnabled = allowOTP
|
||||
isVisible = allowOTP
|
||||
}
|
||||
|
||||
setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.menu_generate_password -> {
|
||||
openPasswordGenerator()
|
||||
true
|
||||
}
|
||||
R.id.menu_add_field -> {
|
||||
addNewCustomField()
|
||||
true
|
||||
}
|
||||
R.id.menu_add_otp -> {
|
||||
setupOTP()
|
||||
true
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save button
|
||||
saveView = findViewById(R.id.entry_edit_save)
|
||||
saveView = findViewById(R.id.entry_edit_validate)
|
||||
saveView?.setOnClickListener { saveEntry() }
|
||||
|
||||
entryEditContentsView?.allowCustomField(mNewEntry?.allowCustomFields() == true) {
|
||||
addNewCustomField()
|
||||
}
|
||||
|
||||
// Verify the education views
|
||||
entryEditActivityEducation = EntryEditActivityEducation(this)
|
||||
|
||||
@@ -194,6 +248,16 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateViewsWithEntry(newEntry: Entry) {
|
||||
// Don't start the field reference manager, we want to see the raw ref
|
||||
mDatabase?.stopManageEntry(newEntry)
|
||||
@@ -207,6 +271,9 @@ class EntryEditActivity : LockingActivity(),
|
||||
username = if (newEntry.username.isEmpty()) mDatabase?.defaultUsername ?:"" else newEntry.username
|
||||
url = newEntry.url
|
||||
password = newEntry.password
|
||||
expires = newEntry.expires
|
||||
if (expires)
|
||||
expiresDate = newEntry.expiryTime
|
||||
notes = newEntry.notes
|
||||
for (entry in newEntry.customFields.entries) {
|
||||
post {
|
||||
@@ -228,7 +295,11 @@ class EntryEditActivity : LockingActivity(),
|
||||
username = entryView.username
|
||||
url = entryView.url
|
||||
password = entryView.password
|
||||
notes = entryView.notes
|
||||
expires = entryView.expires
|
||||
if (entryView.expires) {
|
||||
expiryTime = entryView.expiresDate
|
||||
}
|
||||
notes = entryView. notes
|
||||
entryView.customFields.forEach { customField ->
|
||||
putExtraField(customField.name, customField.protectedValue)
|
||||
}
|
||||
@@ -259,6 +330,13 @@ class EntryEditActivity : LockingActivity(),
|
||||
entryEditContentsView?.addEmptyCustomField()
|
||||
}
|
||||
|
||||
private fun setupOTP() {
|
||||
// Retrieve the current otpElement if exists
|
||||
// and open the dialog to set up the OTP
|
||||
SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel)
|
||||
.show(supportFragmentManager, "addOTPDialog")
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the new entry or update an existing entry in the database
|
||||
*/
|
||||
@@ -307,8 +385,6 @@ class EntryEditActivity : LockingActivity(),
|
||||
// Save database not needed here
|
||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
if (mDatabase?.allowOTP == true)
|
||||
inflater.inflate(R.menu.entry_otp, menu)
|
||||
|
||||
entryEditActivityEducation?.let {
|
||||
Handler().post { performedNextEducation(it) }
|
||||
@@ -318,12 +394,10 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
||||
val passwordView = entryEditContentsView?.generatePasswordView
|
||||
val addNewFieldView = entryEditContentsView?.addNewFieldButton
|
||||
|
||||
val generatePasswordEducationPerformed = passwordView != null
|
||||
val passwordGeneratorView: View? = entryEditAddToolBar?.findViewById(R.id.menu_generate_password)
|
||||
val generatePasswordEducationPerformed = passwordGeneratorView != null
|
||||
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||
passwordView,
|
||||
passwordGeneratorView,
|
||||
{
|
||||
openPasswordGenerator()
|
||||
},
|
||||
@@ -332,23 +406,33 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
)
|
||||
if (!generatePasswordEducationPerformed) {
|
||||
// entryNewFieldEducationPerformed
|
||||
mNewEntry != null && mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
|
||||
val addNewFieldView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_field)
|
||||
val addNewFieldEducationPerformed = mNewEntry != null
|
||||
&& mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
|
||||
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
|
||||
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
||||
addNewFieldView,
|
||||
{
|
||||
addNewCustomField()
|
||||
})
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryEditActivityEducation)
|
||||
}
|
||||
)
|
||||
if (!addNewFieldEducationPerformed) {
|
||||
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
|
||||
setupOtpView != null && setupOtpView.visibility == View.VISIBLE
|
||||
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
|
||||
setupOtpView,
|
||||
{
|
||||
setupOTP()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_lock -> {
|
||||
lockAndExit()
|
||||
return true
|
||||
}
|
||||
R.id.menu_save_database -> {
|
||||
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
|
||||
}
|
||||
@@ -356,14 +440,9 @@ class EntryEditActivity : LockingActivity(),
|
||||
MenuUtil.onContributionItemSelected(this)
|
||||
return true
|
||||
}
|
||||
R.id.menu_add_otp -> {
|
||||
// Retrieve the current otpElement if exists
|
||||
// and open the dialog to set up the OTP
|
||||
SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel)
|
||||
.show(supportFragmentManager, "addOTPDialog")
|
||||
return true
|
||||
android.R.id.home -> {
|
||||
onBackPressed()
|
||||
}
|
||||
android.R.id.home -> finish()
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
@@ -383,6 +462,39 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
|
||||
// To fix android 4.4 issue
|
||||
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
|
||||
if (datePicker?.isShown == true) {
|
||||
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
|
||||
// Save the date
|
||||
entryEditContentsView?.expiresDate =
|
||||
DateInstant(DateTime(expiresDate)
|
||||
.withYear(year)
|
||||
.withMonthOfYear(month + 1)
|
||||
.withDayOfMonth(day)
|
||||
.toDate())
|
||||
// Launch the time picker
|
||||
val dateTime = DateTime(expiresDate)
|
||||
val defaultHour = dateTime.hourOfDay
|
||||
val defaultMinute = dateTime.minuteOfHour
|
||||
TimePickerFragment.getInstance(defaultHour, defaultMinute)
|
||||
.show(supportFragmentManager, "TimePickerFragment")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTimeSet(timePicker: TimePicker?, hours: Int, minutes: Int) {
|
||||
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
|
||||
// Save the date
|
||||
entryEditContentsView?.expiresDate =
|
||||
DateInstant(DateTime(expiresDate)
|
||||
.withHourOfDay(hours)
|
||||
.withMinuteOfHour(minutes)
|
||||
.toDate())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
mNewEntry?.let {
|
||||
populateEntryWithViews(it)
|
||||
@@ -406,6 +518,15 @@ class EntryEditActivity : LockingActivity(),
|
||||
// Do nothing here
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.discard_changes)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.discard) { _, _ ->
|
||||
super@EntryEditActivity.onBackPressed()
|
||||
}.create().show()
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
// Assign entry callback as a result in all case
|
||||
try {
|
||||
|
||||
@@ -26,15 +26,14 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
@@ -47,9 +46,11 @@ import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper.KEY_SEARCH_INFO
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
@@ -61,7 +62,8 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
||||
|
||||
// Views
|
||||
private var fileListContainer: View? = null
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var fileManagerExplanationButton: View? = null
|
||||
private var createButtonView: View? = null
|
||||
private var openDatabaseButtonView: View? = null
|
||||
|
||||
@@ -82,12 +84,17 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||
|
||||
setContentView(R.layout.activity_file_selection)
|
||||
fileListContainer = findViewById(R.id.container_file_list)
|
||||
coordinatorLayout = findViewById(R.id.activity_file_selection_coordinator_layout)
|
||||
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
toolbar.title = ""
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
fileManagerExplanationButton = findViewById(R.id.file_manager_explanation_button)
|
||||
fileManagerExplanationButton?.setOnClickListener {
|
||||
UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
|
||||
}
|
||||
|
||||
// Create button
|
||||
createButtonView = findViewById(R.id.create_database_button)
|
||||
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
||||
@@ -102,8 +109,13 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
createButtonView?.setOnClickListener { createNewFile() }
|
||||
|
||||
mOpenFileHelper = OpenFileHelper(this)
|
||||
openDatabaseButtonView = findViewById(R.id.open_database_button)
|
||||
openDatabaseButtonView?.setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
||||
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
||||
openDatabaseButtonView?.apply {
|
||||
mOpenFileHelper?.openFileOnClickViewListener?.let {
|
||||
setOnClickListener(it)
|
||||
setOnLongClickListener(it)
|
||||
}
|
||||
}
|
||||
|
||||
// History list
|
||||
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
||||
@@ -118,7 +130,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
databaseFileUri,
|
||||
UriUtil.parse(fileDatabaseHistoryEntityToOpen.keyFileUri))
|
||||
}
|
||||
updateFileListVisibility()
|
||||
}
|
||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
||||
// Remove from app database
|
||||
@@ -127,7 +138,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
fileHistoryDeleted?.let { databaseFileHistoryDeleted ->
|
||||
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileHistoryDeleted)
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
updateFileListVisibility()
|
||||
}
|
||||
}
|
||||
true
|
||||
@@ -161,9 +171,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
onActionFinish = { actionTask, _ ->
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_CREATE_TASK -> {
|
||||
// TODO Check
|
||||
// mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
// updateFileListVisibility()
|
||||
GroupActivity.launch(this@FileDatabaseSelectActivity)
|
||||
}
|
||||
}
|
||||
@@ -182,7 +189,9 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
|
||||
private fun fileNoFoundAction(e: FileNotFoundException) {
|
||||
val error = getString(R.string.file_not_found_content)
|
||||
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
coordinatorLayout?.let {
|
||||
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
Log.e(TAG, error, e)
|
||||
}
|
||||
|
||||
@@ -210,7 +219,8 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
try {
|
||||
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
||||
databaseUri, keyFile,
|
||||
assistStructure)
|
||||
assistStructure,
|
||||
intent.getParcelableExtra(KEY_SEARCH_INFO))
|
||||
} catch (e: FileNotFoundException) {
|
||||
fileNoFoundAction(e)
|
||||
}
|
||||
@@ -222,16 +232,21 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
private fun launchGroupActivity(readOnly: Boolean) {
|
||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||
{
|
||||
GroupActivity.launch(this@FileDatabaseSelectActivity, readOnly)
|
||||
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
||||
readOnly)
|
||||
},
|
||||
{
|
||||
GroupActivity.launchForKeyboardSelection(this@FileDatabaseSelectActivity, readOnly)
|
||||
GroupActivity.launchForKeyboardSelection(this@FileDatabaseSelectActivity,
|
||||
readOnly)
|
||||
// Do not keep history
|
||||
finish()
|
||||
},
|
||||
{ assistStructure ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity, assistStructure, readOnly)
|
||||
GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
||||
assistStructure,
|
||||
intent.getParcelableExtra(KEY_SEARCH_INFO),
|
||||
readOnly)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -243,25 +258,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
overridePendingTransition(0, 0)
|
||||
}
|
||||
|
||||
private fun updateExternalStorageWarning() {
|
||||
// To show errors
|
||||
var warning = -1
|
||||
val state = Environment.getExternalStorageState()
|
||||
if (state == Environment.MEDIA_MOUNTED_READ_ONLY) {
|
||||
warning = R.string.read_only_warning
|
||||
} else if (state != Environment.MEDIA_MOUNTED) {
|
||||
warning = R.string.warning_unmounted
|
||||
}
|
||||
|
||||
val labelWarningView = findViewById<TextView>(R.id.label_warning)
|
||||
if (warning != -1) {
|
||||
labelWarningView.setText(warning)
|
||||
labelWarningView.visibility = View.VISIBLE
|
||||
} else {
|
||||
labelWarningView.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
val database = Database.getInstance()
|
||||
if (database.loaded) {
|
||||
@@ -270,8 +266,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
|
||||
super.onResume()
|
||||
|
||||
updateExternalStorageWarning()
|
||||
|
||||
// Construct adapter with listeners
|
||||
if (PreferencesUtil.showRecentFiles(this)) {
|
||||
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
|
||||
@@ -281,20 +275,17 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
// Show only uri accessible
|
||||
historyList.filter {
|
||||
if (hideBrokenLocations) {
|
||||
UriUtil.parse(it.databaseUri)?.let { historyUri ->
|
||||
UriUtil.isUriAccessible(contentResolver, historyUri)
|
||||
} ?: false
|
||||
FileDatabaseInfo(this@FileDatabaseSelectActivity,
|
||||
it.databaseUri).exists
|
||||
} else
|
||||
true
|
||||
})
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
updateFileListVisibility()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
updateFileListVisibility()
|
||||
}
|
||||
|
||||
// Register progress task
|
||||
@@ -316,13 +307,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
||||
}
|
||||
|
||||
private fun updateFileListVisibility() {
|
||||
if (mAdapterDatabaseHistory?.itemCount == 0)
|
||||
fileListContainer?.visibility = View.INVISIBLE
|
||||
else
|
||||
fileListContainer?.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
override fun onAssignKeyDialogPositiveClick(
|
||||
masterPasswordChecked: Boolean, masterPassword: String?,
|
||||
keyFileChecked: Boolean, keyFile: Uri?) {
|
||||
@@ -372,10 +356,13 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
if (mDatabaseFileUri != null) {
|
||||
AssignMasterKeyDialogFragment.getInstance(true)
|
||||
.show(supportFragmentManager, "passwordDialog")
|
||||
} else {
|
||||
val error = getString(R.string.error_create_database)
|
||||
coordinatorLayout?.let {
|
||||
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
Log.e(TAG, error)
|
||||
}
|
||||
// else {
|
||||
// TODO Show error
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,10 +437,13 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
*/
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: Activity, assistStructure: AssistStructure) {
|
||||
fun launchForAutofillResult(activity: Activity,
|
||||
assistStructure: AssistStructure,
|
||||
searchInfo: SearchInfo?) {
|
||||
AutofillHelper.startActivityForAutofillResult(activity,
|
||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||
assistStructure)
|
||||
assistStructure,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.education.GroupActivityEducation
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||
@@ -89,6 +90,7 @@ class GroupActivity : LockingActivity(),
|
||||
|
||||
// Views
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var lockView: View? = null
|
||||
private var toolbar: Toolbar? = null
|
||||
private var searchTitleView: View? = null
|
||||
private var toolbarAction: ToolbarAction? = null
|
||||
@@ -134,6 +136,11 @@ class GroupActivity : LockingActivity(),
|
||||
groupNameView = findViewById(R.id.group_name)
|
||||
toolbarAction = findViewById(R.id.toolbar_action)
|
||||
modeTitleView = findViewById(R.id.mode_title_view)
|
||||
lockView = findViewById(R.id.lock_button)
|
||||
|
||||
lockView?.setOnClickListener {
|
||||
lockAndExit()
|
||||
}
|
||||
|
||||
toolbar?.title = ""
|
||||
setSupportActionBar(toolbar)
|
||||
@@ -347,7 +354,7 @@ class GroupActivity : LockingActivity(),
|
||||
// If it's a search
|
||||
if (Intent.ACTION_SEARCH == intent.action) {
|
||||
val searchString = intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
|
||||
return mDatabase?.search(searchString)
|
||||
return mDatabase?.createVirtualGroupFromSearch(searchString)
|
||||
}
|
||||
// else a real group
|
||||
else {
|
||||
@@ -439,8 +446,7 @@ class GroupActivity : LockingActivity(),
|
||||
enableAddGroup(addGroupEnabled)
|
||||
enableAddEntry(addEntryEnabled)
|
||||
|
||||
if (isEnable)
|
||||
showButton()
|
||||
showButton()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,7 +493,7 @@ class GroupActivity : LockingActivity(),
|
||||
// Build response with the entry selected
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) {
|
||||
mDatabase?.let { database ->
|
||||
AutofillHelper.buildResponseWhenEntrySelected(this@GroupActivity,
|
||||
AutofillHelper.buildResponse(this@GroupActivity,
|
||||
entryVersioned.getEntryInfo(database))
|
||||
}
|
||||
}
|
||||
@@ -504,6 +510,7 @@ class GroupActivity : LockingActivity(),
|
||||
private fun finishNodeAction() {
|
||||
actionNodeMode?.finish()
|
||||
actionNodeMode = null
|
||||
addNodeButtonView?.showButton()
|
||||
}
|
||||
|
||||
override fun onNodeSelected(nodes: List<Node>): Boolean {
|
||||
@@ -515,6 +522,7 @@ class GroupActivity : LockingActivity(),
|
||||
} else {
|
||||
actionNodeMode?.invalidate()
|
||||
}
|
||||
addNodeButtonView?.hideButton()
|
||||
} else {
|
||||
finishNodeAction()
|
||||
}
|
||||
@@ -631,6 +639,13 @@ class GroupActivity : LockingActivity(),
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Show the lock button
|
||||
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
// Refresh the elements
|
||||
assignGroupViewElements()
|
||||
// Refresh suggestions to change preferences
|
||||
@@ -664,13 +679,15 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
// Get the SearchView and set the searchable configuration
|
||||
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
|
||||
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager?
|
||||
|
||||
menu.findItem(R.id.menu_search)?.let {
|
||||
val searchView = it.actionView as SearchView?
|
||||
searchView?.apply {
|
||||
setSearchableInfo(searchManager.getSearchableInfo(
|
||||
ComponentName(this@GroupActivity, GroupActivity::class.java)))
|
||||
(searchManager?.getSearchableInfo(
|
||||
ComponentName(this@GroupActivity, GroupActivity::class.java)))?.let { searchableInfo ->
|
||||
setSearchableInfo(searchableInfo)
|
||||
}
|
||||
setIconifiedByDefault(false) // Do not iconify the widget; expand it by default
|
||||
suggestionsAdapter = mSearchSuggestionAdapter
|
||||
setOnSuggestionListener(object : SearchView.OnSuggestionListener {
|
||||
@@ -750,12 +767,11 @@ class GroupActivity : LockingActivity(),
|
||||
|
||||
if (!sortMenuEducationPerformed) {
|
||||
// lockMenuEducationPerformed
|
||||
toolbar != null
|
||||
&& toolbar!!.findViewById<View>(R.id.menu_lock) != null
|
||||
&& groupActivityEducation.checkAndPerformedLockMenuEducation(
|
||||
toolbar!!.findViewById(R.id.menu_lock),
|
||||
val lockButtonView = findViewById<View>(R.id.lock_button_icon)
|
||||
lockButtonView != null
|
||||
&& groupActivityEducation.checkAndPerformedLockMenuEducation(lockButtonView,
|
||||
{
|
||||
onOptionsItemSelected(menu.findItem(R.id.menu_lock))
|
||||
lockAndExit()
|
||||
},
|
||||
{
|
||||
performedNextEducation(groupActivityEducation, menu)
|
||||
@@ -774,10 +790,6 @@ class GroupActivity : LockingActivity(),
|
||||
R.id.menu_search ->
|
||||
//onSearchRequested();
|
||||
return true
|
||||
R.id.menu_lock -> {
|
||||
lockAndExit()
|
||||
return true
|
||||
}
|
||||
R.id.menu_save_database -> {
|
||||
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
|
||||
return true
|
||||
@@ -905,8 +917,8 @@ class GroupActivity : LockingActivity(),
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
// Not directly get the entry from intent data but from database
|
||||
mListNodesFragment?.rebuildList()
|
||||
// Directly used the onActivityResult in fragment
|
||||
mListNodesFragment?.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
private fun removeSearchInIntent(intent: Intent) {
|
||||
@@ -953,19 +965,41 @@ class GroupActivity : LockingActivity(),
|
||||
private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"
|
||||
private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY"
|
||||
|
||||
private fun buildAndLaunchIntent(context: Context, group: Group?, readOnly: Boolean,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
val checkTime = if (context is Activity)
|
||||
TimeoutHelper.checkTimeAndLockIfTimeout(context)
|
||||
else
|
||||
TimeoutHelper.checkTime(context)
|
||||
if (checkTime) {
|
||||
val intent = Intent(context, GroupActivity::class.java)
|
||||
if (group != null) {
|
||||
intent.putExtra(GROUP_ID_KEY, group.nodeId)
|
||||
}
|
||||
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
||||
intentBuildLauncher.invoke(intent)
|
||||
private fun buildIntent(context: Context,
|
||||
group: Group?,
|
||||
searchInfo: SearchInfo?,
|
||||
readOnly: Boolean,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
val intent = Intent(context, GroupActivity::class.java)
|
||||
if (group != null) {
|
||||
intent.putExtra(GROUP_ID_KEY, group.nodeId)
|
||||
}
|
||||
if (searchInfo != null) {
|
||||
intent.action = Intent.ACTION_SEARCH
|
||||
val searchQuery = searchInfo.webDomain ?: searchInfo.applicationId
|
||||
intent.putExtra(SearchManager.QUERY, searchQuery)
|
||||
}
|
||||
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
||||
intentBuildLauncher.invoke(intent)
|
||||
}
|
||||
|
||||
private fun checkTimeAndBuildIntent(activity: Activity,
|
||||
group: Group?,
|
||||
searchInfo: SearchInfo?,
|
||||
readOnly: Boolean,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
buildIntent(activity, group, searchInfo, readOnly, intentBuildLauncher)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkTimeAndBuildIntent(context: Context,
|
||||
group: Group?,
|
||||
searchInfo: SearchInfo?,
|
||||
readOnly: Boolean,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
if (TimeoutHelper.checkTime(context)) {
|
||||
buildIntent(context, group, searchInfo, readOnly, intentBuildLauncher)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,11 +1008,9 @@ class GroupActivity : LockingActivity(),
|
||||
* Standard Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@JvmOverloads
|
||||
fun launch(context: Context, readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
|
||||
TimeoutHelper.recordTime(context)
|
||||
buildAndLaunchIntent(context, null, readOnly) { intent ->
|
||||
fun launch(context: Context,
|
||||
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
|
||||
checkTimeAndBuildIntent(context, null, null, readOnly) { intent ->
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -989,10 +1021,9 @@ class GroupActivity : LockingActivity(),
|
||||
* -------------------------
|
||||
*/
|
||||
// TODO implement pre search to directly open the direct group
|
||||
|
||||
fun launchForKeyboardSelection(context: Context, readOnly: Boolean) {
|
||||
TimeoutHelper.recordTime(context)
|
||||
buildAndLaunchIntent(context, null, readOnly) { intent ->
|
||||
fun launchForKeyboardSelection(context: Context,
|
||||
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
|
||||
checkTimeAndBuildIntent(context, null, null, readOnly) { intent ->
|
||||
EntrySelectionHelper.startActivityForEntrySelection(context, intent)
|
||||
}
|
||||
}
|
||||
@@ -1002,13 +1033,13 @@ class GroupActivity : LockingActivity(),
|
||||
* Autofill Launch
|
||||
* -------------------------
|
||||
*/
|
||||
// TODO implement pre search to directly open the direct group
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: Activity, assistStructure: AssistStructure, readOnly: Boolean) {
|
||||
TimeoutHelper.recordTime(activity)
|
||||
buildAndLaunchIntent(activity, null, readOnly) { intent ->
|
||||
AutofillHelper.startActivityForAutofillResult(activity, intent, assistStructure)
|
||||
fun launchForAutofillResult(activity: Activity,
|
||||
assistStructure: AssistStructure,
|
||||
searchInfo: SearchInfo? = null,
|
||||
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(activity)) {
|
||||
checkTimeAndBuildIntent(activity, null, searchInfo, readOnly) { intent ->
|
||||
AutofillHelper.startActivityForAutofillResult(activity, intent, assistStructure, searchInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,13 +369,11 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE
|
||||
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
||||
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { newNode ->
|
||||
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { changedNode ->
|
||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
|
||||
mAdapter?.addNode(newNode)
|
||||
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
||||
//mAdapter.updateLastNodeRegister(newNode);
|
||||
rebuildList()
|
||||
}
|
||||
addNode(changedNode)
|
||||
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE)
|
||||
mAdapter?.notifyDataSetChanged()
|
||||
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.app.Activity
|
||||
import android.app.assist.AssistStructure
|
||||
import android.app.backup.BackupManager
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -32,13 +33,11 @@ import android.text.TextWatcher
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||
import android.widget.Button
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import android.widget.*
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.core.app.ActivityCompat
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||
@@ -50,11 +49,13 @@ import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper.KEY_SEARCH_INFO
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||
@@ -66,6 +67,7 @@ import com.kunzisoft.keepass.utils.FileDatabaseInfo
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import kotlinx.android.synthetic.main.activity_password.*
|
||||
import java.io.FileNotFoundException
|
||||
@@ -77,7 +79,7 @@ open class PasswordActivity : StylishActivity() {
|
||||
private var containerView: View? = null
|
||||
private var filenameView: TextView? = null
|
||||
private var passwordView: EditText? = null
|
||||
private var keyFileView: EditText? = null
|
||||
private var keyFileSelectionView: KeyFileSelectionView? = null
|
||||
private var confirmButtonView: Button? = null
|
||||
private var checkboxPasswordView: CompoundButton? = null
|
||||
private var checkboxKeyFileView: CompoundButton? = null
|
||||
@@ -92,6 +94,7 @@ open class PasswordActivity : StylishActivity() {
|
||||
private var mRememberKeyFile: Boolean = false
|
||||
private var mOpenFileHelper: OpenFileHelper? = null
|
||||
|
||||
private var mPermissionAsked = false
|
||||
private var readOnly: Boolean = false
|
||||
private var mForceReadOnly: Boolean = false
|
||||
set(value) {
|
||||
@@ -111,8 +114,6 @@ open class PasswordActivity : StylishActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||
|
||||
setContentView(R.layout.activity_password)
|
||||
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
@@ -125,18 +126,23 @@ open class PasswordActivity : StylishActivity() {
|
||||
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
||||
filenameView = findViewById(R.id.filename)
|
||||
passwordView = findViewById(R.id.password)
|
||||
keyFileView = findViewById(R.id.pass_keyfile)
|
||||
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
||||
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
||||
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
|
||||
advancedUnlockInfoView = findViewById(R.id.biometric_info)
|
||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||
|
||||
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||
|
||||
val browseView = findViewById<View>(R.id.open_database_button)
|
||||
mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
|
||||
browseView.setOnClickListener(mOpenFileHelper!!.openFileOnClickViewListener)
|
||||
keyFileSelectionView?.apply {
|
||||
mOpenFileHelper?.openFileOnClickViewListener?.let {
|
||||
setOnClickListener(it)
|
||||
setOnLongClickListener(it)
|
||||
}
|
||||
}
|
||||
|
||||
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
||||
passwordView?.addTextChangedListener(object : TextWatcher {
|
||||
@@ -149,22 +155,17 @@ open class PasswordActivity : StylishActivity() {
|
||||
checkboxPasswordView?.isChecked = true
|
||||
}
|
||||
})
|
||||
keyFileView?.setOnEditorActionListener(onEditorActionListener)
|
||||
keyFileView?.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun afterTextChanged(editable: Editable) {
|
||||
if (editable.toString().isNotEmpty() && checkboxKeyFileView?.isChecked != true)
|
||||
checkboxKeyFileView?.isChecked = true
|
||||
}
|
||||
})
|
||||
|
||||
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||
enableOrNotTheConfirmationButton()
|
||||
}
|
||||
|
||||
// If is a view intent
|
||||
getUriFromIntent(intent)
|
||||
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
||||
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
|
||||
}
|
||||
|
||||
mProgressDialogThread = ProgressDialogThread(this).apply {
|
||||
onActionFinish = { actionTask, result ->
|
||||
when (actionTask) {
|
||||
@@ -177,11 +178,9 @@ open class PasswordActivity : StylishActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the password in view in all cases
|
||||
removePassword()
|
||||
|
||||
if (result.isSuccess) {
|
||||
setEmptyViews()
|
||||
mDatabaseKeyFileUri = null
|
||||
clearCredentialsViews(true)
|
||||
launchGroupActivity()
|
||||
} else {
|
||||
var resultError = ""
|
||||
@@ -237,19 +236,59 @@ open class PasswordActivity : StylishActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUriFromIntent(intent: Intent?) {
|
||||
// If is a view intent
|
||||
val action = intent?.action
|
||||
if (action != null
|
||||
&& action == VIEW_INTENT) {
|
||||
mDatabaseFileUri = intent.data
|
||||
mDatabaseKeyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
|
||||
} else {
|
||||
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
|
||||
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
getUriFromIntent(intent)
|
||||
}
|
||||
|
||||
private fun launchGroupActivity() {
|
||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||
{
|
||||
GroupActivity.launch(this@PasswordActivity, readOnly)
|
||||
GroupActivity.launch(this@PasswordActivity,
|
||||
readOnly)
|
||||
},
|
||||
{
|
||||
GroupActivity.launchForKeyboardSelection(this@PasswordActivity, readOnly)
|
||||
GroupActivity.launchForKeyboardSelection(this@PasswordActivity,
|
||||
readOnly)
|
||||
// Do not keep history
|
||||
finish()
|
||||
},
|
||||
{ assistStructure ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
GroupActivity.launchForAutofillResult(this@PasswordActivity, assistStructure, readOnly)
|
||||
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||
AutofillHelper.checkAutoSearchInfo(this,
|
||||
Database.getInstance(),
|
||||
searchInfo,
|
||||
{ items ->
|
||||
// Response is build
|
||||
AutofillHelper.buildResponse(this, items)
|
||||
finish()
|
||||
},
|
||||
{
|
||||
// Here no search info found
|
||||
GroupActivity.launchForAutofillResult(this@PasswordActivity,
|
||||
assistStructure,
|
||||
null,
|
||||
readOnly)
|
||||
},
|
||||
{
|
||||
// Simply close if database not opened, normally not happened
|
||||
finish()
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -265,13 +304,16 @@ open class PasswordActivity : StylishActivity() {
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
||||
if (Database.getInstance().loaded)
|
||||
launchGroupActivity()
|
||||
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||
|
||||
// If the database isn't accessible make sure to clear the password field, if it
|
||||
// was saved in the instance state
|
||||
if (Database.getInstance().loaded) {
|
||||
setEmptyViews()
|
||||
clearCredentialsViews()
|
||||
}
|
||||
|
||||
// For check shutdown
|
||||
@@ -283,46 +325,40 @@ open class PasswordActivity : StylishActivity() {
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
|
||||
mDatabaseKeyFileUri?.let {
|
||||
outState.putString(KEY_KEYFILE, it.toString())
|
||||
}
|
||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun initUriFromIntent() {
|
||||
|
||||
val databaseUri: Uri?
|
||||
val keyFileUri: Uri?
|
||||
|
||||
// If is a view intent
|
||||
val action = intent.action
|
||||
if (action != null
|
||||
&& action == VIEW_INTENT) {
|
||||
databaseUri = intent.data
|
||||
keyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
|
||||
} else {
|
||||
databaseUri = intent.getParcelableExtra(KEY_FILENAME)
|
||||
keyFileUri = intent.getParcelableExtra(KEY_KEYFILE)
|
||||
}
|
||||
|
||||
mForceReadOnly = !UriUtil.isUriWritable(contentResolver, databaseUri)
|
||||
/*
|
||||
// "canXrite" doesn't work with Google Drive, don't really know why?
|
||||
mForceReadOnly = mDatabaseFileUri?.let {
|
||||
!FileDatabaseInfo(this, it).canWrite
|
||||
} ?: false
|
||||
*/
|
||||
mForceReadOnly = mDatabaseFileUri?.let {
|
||||
!FileDatabaseInfo(this, it).exists
|
||||
} ?: true
|
||||
|
||||
// Post init uri with KeyFile if needed
|
||||
if (mRememberKeyFile && (keyFileUri == null || keyFileUri.toString().isEmpty())) {
|
||||
if (mRememberKeyFile && (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
|
||||
// Retrieve KeyFile in a thread
|
||||
databaseUri?.let { databaseUriNotNull ->
|
||||
mDatabaseFileUri?.let { databaseUri ->
|
||||
FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||
.getKeyFileUriByDatabaseUri(databaseUriNotNull) {
|
||||
.getKeyFileUriByDatabaseUri(databaseUri) {
|
||||
onPostInitUri(databaseUri, it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onPostInitUri(databaseUri, keyFileUri)
|
||||
onPostInitUri(mDatabaseFileUri, mDatabaseKeyFileUri)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||
mDatabaseFileUri = databaseFileUri
|
||||
mDatabaseKeyFileUri = keyFileUri
|
||||
|
||||
// Define title
|
||||
databaseFileUri?.let {
|
||||
FileDatabaseInfo(this, it).retrieveDatabaseTitle { title ->
|
||||
@@ -331,9 +367,8 @@ open class PasswordActivity : StylishActivity() {
|
||||
}
|
||||
|
||||
// Define Key File text
|
||||
val keyUriString = keyFileUri?.toString() ?: ""
|
||||
if (keyUriString.isNotEmpty() && mRememberKeyFile) { // Bug KeepassDX #18
|
||||
populateKeyFileTextView(keyUriString)
|
||||
if (mRememberKeyFile) {
|
||||
populateKeyFileTextView(keyFileUri)
|
||||
}
|
||||
|
||||
// Define listeners for default database checkbox and validate button
|
||||
@@ -428,10 +463,9 @@ open class PasswordActivity : StylishActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setEmptyViews() {
|
||||
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
|
||||
populatePasswordTextView(null)
|
||||
// Bug KeepassDX #18
|
||||
if (!mRememberKeyFile) {
|
||||
if (clearKeyFile) {
|
||||
populateKeyFileTextView(null)
|
||||
}
|
||||
}
|
||||
@@ -448,13 +482,13 @@ open class PasswordActivity : StylishActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateKeyFileTextView(text: String?) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
keyFileView?.setText("")
|
||||
private fun populateKeyFileTextView(uri: Uri?) {
|
||||
if (uri == null || uri.toString().isEmpty()) {
|
||||
keyFileSelectionView?.uri = null
|
||||
if (checkboxKeyFileView?.isChecked == true)
|
||||
checkboxKeyFileView?.isChecked = false
|
||||
} else {
|
||||
keyFileView?.setText(text)
|
||||
keyFileSelectionView?.uri = uri
|
||||
if (checkboxKeyFileView?.isChecked != true)
|
||||
checkboxKeyFileView?.isChecked = true
|
||||
}
|
||||
@@ -475,7 +509,7 @@ open class PasswordActivity : StylishActivity() {
|
||||
|
||||
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||
val password: String? = passwordView?.text?.toString()
|
||||
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString())
|
||||
val keyFile: Uri? = keyFileSelectionView?.uri
|
||||
verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity)
|
||||
}
|
||||
|
||||
@@ -488,7 +522,7 @@ open class PasswordActivity : StylishActivity() {
|
||||
}
|
||||
|
||||
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
|
||||
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString())
|
||||
val keyFile: Uri? = keyFileSelectionView?.uri
|
||||
verifyKeyFileCheckbox(keyFile)
|
||||
loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
|
||||
}
|
||||
@@ -497,18 +531,13 @@ open class PasswordActivity : StylishActivity() {
|
||||
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
||||
}
|
||||
|
||||
private fun removePassword() {
|
||||
passwordView?.setText("")
|
||||
checkboxPasswordView?.isChecked = false
|
||||
}
|
||||
|
||||
private fun loadDatabase(databaseFileUri: Uri?,
|
||||
password: String?,
|
||||
keyFileUri: Uri?,
|
||||
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||
|
||||
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
||||
removePassword()
|
||||
clearCredentialsViews()
|
||||
}
|
||||
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
@@ -565,11 +594,42 @@ open class PasswordActivity : StylishActivity() {
|
||||
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
launchEducation(menu)
|
||||
launchEducation(menu) {
|
||||
launchCheckPermission()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Check permission
|
||||
private fun launchCheckPermission() {
|
||||
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
val permissions = arrayOf(writePermission)
|
||||
if (Build.VERSION.SDK_INT >= 23
|
||||
&& !readOnly
|
||||
&& !mPermissionAsked) {
|
||||
mPermissionAsked = true
|
||||
// Check self permission to show or not the dialog
|
||||
if (toolbar != null
|
||||
&& ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
when (requestCode) {
|
||||
WRITE_EXTERNAL_STORAGE_REQUEST -> {
|
||||
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
|
||||
Toast.makeText(this, R.string.read_only_warning, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// To fix multiple view education
|
||||
private var performedEductionInProgress = false
|
||||
private fun launchEducation(menu: Menu, onEducationFinished: (()-> Unit)? = null) {
|
||||
@@ -671,7 +731,8 @@ open class PasswordActivity : StylishActivity() {
|
||||
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
populateKeyFileTextView(uri.toString())
|
||||
mDatabaseKeyFileUri = uri
|
||||
populateKeyFileTextView(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -679,7 +740,7 @@ open class PasswordActivity : StylishActivity() {
|
||||
// this block if not a key file response
|
||||
when (resultCode) {
|
||||
LockingActivity.RESULT_EXIT_LOCK, Activity.RESULT_CANCELED -> {
|
||||
setEmptyViews()
|
||||
clearCredentialsViews()
|
||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
||||
}
|
||||
}
|
||||
@@ -696,6 +757,8 @@ open class PasswordActivity : StylishActivity() {
|
||||
|
||||
private const val KEY_PASSWORD = "password"
|
||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
|
||||
private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647
|
||||
|
||||
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
@@ -750,13 +813,15 @@ open class PasswordActivity : StylishActivity() {
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
assistStructure: AssistStructure?) {
|
||||
assistStructure: AssistStructure?,
|
||||
searchInfo: SearchInfo?) {
|
||||
if (assistStructure != null) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
AutofillHelper.startActivityForAutofillResult(
|
||||
activity,
|
||||
intent,
|
||||
assistStructure)
|
||||
assistStructure,
|
||||
searchInfo)
|
||||
}
|
||||
} else {
|
||||
launch(activity, databaseFile, keyFile)
|
||||
|
||||
@@ -35,7 +35,7 @@ import android.widget.CompoundButton
|
||||
import android.widget.TextView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||
|
||||
class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
|
||||
@@ -51,9 +51,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
private var passwordRepeatTextInputLayout: TextInputLayout? = null
|
||||
private var passwordRepeatView: TextView? = null
|
||||
|
||||
private var keyFileTextInputLayout: TextInputLayout? = null
|
||||
private var keyFileCheckBox: CompoundButton? = null
|
||||
private var keyFileView: TextView? = null
|
||||
private var keyFileSelectionView: KeyFileSelectionView? = null
|
||||
|
||||
private var mListener: AssignPasswordDialogListener? = null
|
||||
|
||||
@@ -69,16 +68,6 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private val keyFileTextWatcher = object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun afterTextChanged(editable: Editable) {
|
||||
keyFileCheckBox?.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
interface AssignPasswordDialogListener {
|
||||
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
|
||||
keyFileChecked: Boolean, keyFile: Uri?)
|
||||
@@ -121,13 +110,14 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
|
||||
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
|
||||
|
||||
keyFileTextInputLayout = rootView?.findViewById(R.id.keyfile_input_layout)
|
||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||
keyFileView = rootView?.findViewById(R.id.pass_keyfile)
|
||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||
|
||||
mOpenFileHelper = OpenFileHelper(this)
|
||||
rootView?.findViewById<View>(R.id.open_database_button)?.setOnClickListener { view ->
|
||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(view) }
|
||||
keyFileSelectionView?.apply {
|
||||
setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
||||
setOnLongClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
||||
}
|
||||
|
||||
val dialog = builder.create()
|
||||
|
||||
@@ -176,14 +166,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
|
||||
// To check checkboxes if a text is present
|
||||
passwordView?.addTextChangedListener(passwordTextWatcher)
|
||||
keyFileView?.addTextChangedListener(keyFileTextWatcher)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
passwordView?.removeTextChangedListener(passwordTextWatcher)
|
||||
keyFileView?.removeTextChangedListener(keyFileTextWatcher)
|
||||
}
|
||||
|
||||
private fun verifyPassword(): Boolean {
|
||||
@@ -216,11 +204,11 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
if (keyFileCheckBox != null
|
||||
&& keyFileCheckBox!!.isChecked) {
|
||||
|
||||
UriUtil.parse(keyFileView?.text?.toString())?.let { uri ->
|
||||
keyFileSelectionView?.uri?.let { uri ->
|
||||
mKeyFile = uri
|
||||
} ?: run {
|
||||
error = true
|
||||
keyFileTextInputLayout?.error = getString(R.string.error_nokeyfile)
|
||||
keyFileSelectionView?.error = getString(R.string.error_nokeyfile)
|
||||
}
|
||||
}
|
||||
return error
|
||||
@@ -265,8 +253,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
) { uri ->
|
||||
uri?.let { pathUri ->
|
||||
keyFileCheckBox?.isChecked = true
|
||||
keyFileView?.text = pathUri.toString()
|
||||
|
||||
keyFileSelectionView?.uri = pathUri
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.DatePickerDialog
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
|
||||
class DatePickerFragment : DialogFragment() {
|
||||
|
||||
private var mDefaultYear: Int = 2000
|
||||
private var mDefaultMonth: Int = 1
|
||||
private var mDefaultDay: Int = 1
|
||||
|
||||
private var mListener: DatePickerDialog.OnDateSetListener? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
mListener = context as DatePickerDialog.OnDateSetListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
// Create a new instance of DatePickerDialog and return it
|
||||
return context?.let {
|
||||
arguments?.apply {
|
||||
if (containsKey(DEFAULT_YEAR_BUNDLE_KEY))
|
||||
mDefaultYear = getInt(DEFAULT_YEAR_BUNDLE_KEY)
|
||||
if (containsKey(DEFAULT_MONTH_BUNDLE_KEY))
|
||||
mDefaultMonth = getInt(DEFAULT_MONTH_BUNDLE_KEY)
|
||||
if (containsKey(DEFAULT_DAY_BUNDLE_KEY))
|
||||
mDefaultDay = getInt(DEFAULT_DAY_BUNDLE_KEY)
|
||||
}
|
||||
|
||||
DatePickerDialog(it, mListener, mDefaultYear, mDefaultMonth, mDefaultDay)
|
||||
} ?: super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val DEFAULT_YEAR_BUNDLE_KEY = "DEFAULT_YEAR_BUNDLE_KEY"
|
||||
private const val DEFAULT_MONTH_BUNDLE_KEY = "DEFAULT_MONTH_BUNDLE_KEY"
|
||||
private const val DEFAULT_DAY_BUNDLE_KEY = "DEFAULT_DAY_BUNDLE_KEY"
|
||||
|
||||
fun getInstance(defaultYear: Int,
|
||||
defaultMonth: Int,
|
||||
defaultDay: Int): DatePickerFragment {
|
||||
return DatePickerFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putInt(DEFAULT_YEAR_BUNDLE_KEY, defaultYear)
|
||||
putInt(DEFAULT_MONTH_BUNDLE_KEY, defaultMonth)
|
||||
putInt(DEFAULT_DAY_BUNDLE_KEY, defaultDay)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.DatePickerDialog
|
||||
import android.app.Dialog
|
||||
import android.app.TimePickerDialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateFormat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
|
||||
class TimePickerFragment : DialogFragment() {
|
||||
|
||||
private var defaultHour: Int = 0
|
||||
private var defaultMinute: Int = 0
|
||||
|
||||
private var mListener: TimePickerDialog.OnTimeSetListener? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
mListener = context as TimePickerDialog.OnTimeSetListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
// Create a new instance of DatePickerDialog and return it
|
||||
return context?.let {
|
||||
arguments?.apply {
|
||||
if (containsKey(DEFAULT_HOUR_BUNDLE_KEY))
|
||||
defaultHour = getInt(DEFAULT_HOUR_BUNDLE_KEY)
|
||||
if (containsKey(DEFAULT_MINUTE_BUNDLE_KEY))
|
||||
defaultMinute = getInt(DEFAULT_MINUTE_BUNDLE_KEY)
|
||||
}
|
||||
|
||||
TimePickerDialog(it, mListener, defaultHour, defaultMinute, DateFormat.is24HourFormat(activity))
|
||||
} ?: super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val DEFAULT_HOUR_BUNDLE_KEY = "DEFAULT_HOUR_BUNDLE_KEY"
|
||||
private const val DEFAULT_MINUTE_BUNDLE_KEY = "DEFAULT_MINUTE_BUNDLE_KEY"
|
||||
|
||||
fun getInstance(defaultHour: Int,
|
||||
defaultMinute: Int): TimePickerFragment {
|
||||
return TimePickerFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putInt(DEFAULT_HOUR_BUNDLE_KEY, defaultHour)
|
||||
putInt(DEFAULT_MINUTE_BUNDLE_KEY, defaultMinute)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,14 +52,22 @@ class OpenFileHelper {
|
||||
this.fragment = context
|
||||
}
|
||||
|
||||
inner class OpenFileOnClickViewListener : View.OnClickListener {
|
||||
inner class OpenFileOnClickViewListener : View.OnClickListener, View.OnLongClickListener {
|
||||
|
||||
override fun onClick(v: View) {
|
||||
private fun onAbstractClick(longClick: Boolean = false) {
|
||||
try {
|
||||
try {
|
||||
openActivityWithActionOpenDocument()
|
||||
} catch(e: Exception) {
|
||||
openActivityWithActionGetContent()
|
||||
if (longClick) {
|
||||
try {
|
||||
openActivityWithActionGetContent()
|
||||
} catch (e: Exception) {
|
||||
openActivityWithActionOpenDocument()
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
openActivityWithActionOpenDocument()
|
||||
} catch (e: Exception) {
|
||||
openActivityWithActionGetContent()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Enable to start the file picker activity", e)
|
||||
@@ -68,6 +76,15 @@ class OpenFileHelper {
|
||||
showBrowserDialog()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
onAbstractClick()
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View?): Boolean {
|
||||
onAbstractClick(true)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
|
||||
@@ -20,11 +20,7 @@
|
||||
package com.kunzisoft.keepass.activities.lock
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
@@ -34,12 +30,9 @@ import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
|
||||
abstract class LockingActivity : StylishActivity() {
|
||||
|
||||
@@ -81,12 +74,10 @@ abstract class LockingActivity : StylishActivity() {
|
||||
}
|
||||
|
||||
if (mTimeoutEnable) {
|
||||
mLockReceiver = LockReceiver()
|
||||
val intentFilter = IntentFilter().apply {
|
||||
addAction(Intent.ACTION_SCREEN_OFF)
|
||||
addAction(LOCK_ACTION)
|
||||
mLockReceiver = LockReceiver {
|
||||
lockAndExit()
|
||||
}
|
||||
registerReceiver(mLockReceiver, intentFilter)
|
||||
registerLockReceiver(mLockReceiver)
|
||||
}
|
||||
|
||||
mExitLock = false
|
||||
@@ -151,29 +142,12 @@ abstract class LockingActivity : StylishActivity() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
unregisterLockReceiver(mLockReceiver)
|
||||
super.onDestroy()
|
||||
if (mLockReceiver != null)
|
||||
unregisterReceiver(mLockReceiver)
|
||||
}
|
||||
|
||||
inner class LockReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
// If allowed, lock and exit
|
||||
if (!TimeoutHelper.temporarilyDisableTimeout) {
|
||||
intent.action?.let {
|
||||
when (it) {
|
||||
Intent.ACTION_SCREEN_OFF -> if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(this@LockingActivity)) {
|
||||
lockAndExit()
|
||||
}
|
||||
LOCK_ACTION -> lockAndExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun lockAndExit() {
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
lock()
|
||||
}
|
||||
|
||||
@@ -208,20 +182,8 @@ abstract class LockingActivity : StylishActivity() {
|
||||
}
|
||||
|
||||
fun Activity.lock() {
|
||||
// Stop the Magikeyboard service
|
||||
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
||||
MagikIME.removeEntry(this)
|
||||
closeDatabase()
|
||||
|
||||
// Stop the notification service
|
||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||
|
||||
Log.i(Activity::class.java.name, "Shutdown " + localClassName +
|
||||
" after inactivity or manual lock")
|
||||
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).apply {
|
||||
cancelAll()
|
||||
}
|
||||
// Clear data
|
||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
||||
// Add onActivityForResult response
|
||||
setResult(LockingActivity.RESULT_EXIT_LOCK)
|
||||
finish()
|
||||
|
||||
@@ -84,24 +84,25 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
||||
// File path
|
||||
holder.filePath.text = UriUtil.decode(fileDatabaseInfo.fileUri?.toString())
|
||||
|
||||
if (fileDatabaseInfo.dataAccessible()) {
|
||||
if (fileDatabaseInfo.exists) {
|
||||
holder.fileInformation.clearColorFilter()
|
||||
} else {
|
||||
holder.fileInformation.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
|
||||
}
|
||||
|
||||
// Modification
|
||||
if (fileDatabaseInfo.lastModificationAccessible()) {
|
||||
holder.fileModification.text = fileDatabaseInfo.getModificationString()
|
||||
fileDatabaseInfo.getModificationString()?.let {
|
||||
holder.fileModification.text = it
|
||||
holder.fileModification.visibility = View.VISIBLE
|
||||
} else {
|
||||
} ?: run {
|
||||
holder.fileModification.visibility = View.GONE
|
||||
}
|
||||
|
||||
// Size
|
||||
if (fileDatabaseInfo.sizeAccessible()) {
|
||||
holder.fileSize.text = fileDatabaseInfo.getSizeString()
|
||||
fileDatabaseInfo.getSizeString()?.let {
|
||||
holder.fileSize.text = it
|
||||
holder.fileSize.visibility = View.VISIBLE
|
||||
} else {
|
||||
} ?: run {
|
||||
holder.fileSize.visibility = View.GONE
|
||||
}
|
||||
|
||||
|
||||
@@ -28,8 +28,14 @@ import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.cursor.EntryCursorKDB
|
||||
import com.kunzisoft.keepass.database.cursor.EntryCursorKDBX
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.strikeOut
|
||||
@@ -38,8 +44,8 @@ class SearchEntryCursorAdapter(private val context: Context,
|
||||
private val database: Database)
|
||||
: androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
|
||||
|
||||
private val cursorInflater: LayoutInflater = context.getSystemService(
|
||||
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
private val cursorInflater: LayoutInflater? = context.getSystemService(
|
||||
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
private var displayUsername: Boolean = false
|
||||
private val iconColor: Int
|
||||
|
||||
@@ -58,7 +64,7 @@ class SearchEntryCursorAdapter(private val context: Context,
|
||||
|
||||
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
|
||||
|
||||
val view = cursorInflater.inflate(R.layout.item_search_entry, parent, false)
|
||||
val view = cursorInflater!!.inflate(R.layout.item_search_entry, parent, false)
|
||||
val viewHolder = ViewHolder()
|
||||
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon)
|
||||
viewHolder.textViewTitle = view.findViewById(R.id.entry_text)
|
||||
@@ -69,8 +75,7 @@ class SearchEntryCursorAdapter(private val context: Context,
|
||||
}
|
||||
|
||||
override fun bindView(view: View, context: Context, cursor: Cursor) {
|
||||
|
||||
database.getEntryFrom(cursor)?.let { currentEntry ->
|
||||
getEntryFrom(cursor)?.let { currentEntry ->
|
||||
val viewHolder = view.tag as ViewHolder
|
||||
|
||||
// Assign image
|
||||
@@ -98,14 +103,46 @@ class SearchEntryCursorAdapter(private val context: Context,
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewHolder {
|
||||
internal var imageViewIcon: ImageView? = null
|
||||
internal var textViewTitle: TextView? = null
|
||||
internal var textViewSubTitle: TextView? = null
|
||||
private fun getEntryFrom(cursor: Cursor): Entry? {
|
||||
return database.createEntry()?.apply {
|
||||
database.startManageEntry(this)
|
||||
entryKDB?.let { entryKDB ->
|
||||
(cursor as EntryCursorKDB).populateEntry(entryKDB, database.iconFactory)
|
||||
}
|
||||
entryKDBX?.let { entryKDBX ->
|
||||
(cursor as EntryCursorKDBX).populateEntry(entryKDBX, database.iconFactory)
|
||||
}
|
||||
database.stopManageEntry(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
|
||||
return database.searchEntries(context, constraint.toString())
|
||||
return searchEntries(context, constraint.toString())
|
||||
}
|
||||
|
||||
private fun searchEntries(context: Context, query: String): Cursor? {
|
||||
var cursorKDB: EntryCursorKDB? = null
|
||||
var cursorKDBX: EntryCursorKDBX? = null
|
||||
|
||||
if (database.type == DatabaseKDB.TYPE)
|
||||
cursorKDB = EntryCursorKDB()
|
||||
if (database.type == DatabaseKDBX.TYPE)
|
||||
cursorKDBX = EntryCursorKDBX()
|
||||
|
||||
val searchGroup = database.createVirtualGroupFromSearch(query, SearchHelper.MAX_SEARCH_ENTRY)
|
||||
if (searchGroup != null) {
|
||||
// Search in hide entries but not meta-stream
|
||||
for (entry in searchGroup.getFilteredChildEntries(*Group.ChildFilter.getDefaults(context))) {
|
||||
entry.entryKDB?.let {
|
||||
cursorKDB?.addEntry(it)
|
||||
}
|
||||
entry.entryKDBX?.let {
|
||||
cursorKDBX?.addEntry(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cursorKDB ?: cursorKDBX
|
||||
}
|
||||
|
||||
fun getEntryFromPosition(position: Int): Entry? {
|
||||
@@ -113,9 +150,14 @@ class SearchEntryCursorAdapter(private val context: Context,
|
||||
|
||||
val cursor = this.cursor
|
||||
if (cursor.moveToFirst() && cursor.move(position)) {
|
||||
pwEntry = database.getEntryFrom(cursor)
|
||||
pwEntry = getEntryFrom(cursor)
|
||||
}
|
||||
return pwEntry
|
||||
}
|
||||
|
||||
private class ViewHolder {
|
||||
internal var imageViewIcon: ImageView? = null
|
||||
internal var textViewTitle: TextView? = null
|
||||
internal var textViewSubTitle: TextView? = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,14 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) {
|
||||
ActionDatabaseAsyncTask(
|
||||
{
|
||||
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun deleteAllKeyFiles() {
|
||||
ActionDatabaseAsyncTask(
|
||||
{
|
||||
|
||||
@@ -38,6 +38,9 @@ interface FileDatabaseHistoryDao {
|
||||
@Delete
|
||||
fun delete(fileDatabaseHistory: FileDatabaseHistoryEntity): Int
|
||||
|
||||
@Query("UPDATE file_database_history SET keyfile_uri=null WHERE database_uri = :databaseUriString")
|
||||
fun deleteKeyFileByDatabaseUri(databaseUriString: String)
|
||||
|
||||
@Query("UPDATE file_database_history SET keyfile_uri=null")
|
||||
fun deleteAllKeyFiles()
|
||||
|
||||
|
||||
@@ -26,15 +26,22 @@ import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.service.autofill.Dataset
|
||||
import android.service.autofill.FillResponse
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.util.Log
|
||||
import android.view.autofill.AutofillManager
|
||||
import android.view.autofill.AutofillValue
|
||||
import android.widget.RemoteViews
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import java.util.*
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@@ -43,6 +50,7 @@ object AutofillHelper {
|
||||
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
|
||||
|
||||
private const val ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
||||
const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||
|
||||
fun retrieveAssistStructure(intent: Intent?): AssistStructure? {
|
||||
intent?.let {
|
||||
@@ -56,27 +64,44 @@ object AutofillHelper {
|
||||
return String.format("%s (%s)", entryInfo.title, entryInfo.username)
|
||||
if (entryInfo.title.isNotEmpty())
|
||||
return entryInfo.title
|
||||
if (entryInfo.username.isNotEmpty())
|
||||
return entryInfo.username
|
||||
if (entryInfo.url.isNotEmpty())
|
||||
return entryInfo.url
|
||||
if (entryInfo.username.isNotEmpty())
|
||||
return entryInfo.username
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun buildDataset(context: Context,
|
||||
entryInfo: EntryInfo,
|
||||
struct: StructureParser.Result): Dataset? {
|
||||
internal fun addHeader(responseBuilder: FillResponse.Builder,
|
||||
packageName: String,
|
||||
webDomain: String?,
|
||||
applicationId: String?) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
if (webDomain != null) {
|
||||
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_web_domain).apply {
|
||||
setTextViewText(R.id.autofill_web_domain_text, webDomain)
|
||||
})
|
||||
} else if (applicationId != null) {
|
||||
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_app_id).apply {
|
||||
setTextViewText(R.id.autofill_app_id_text, applicationId)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun buildDataset(context: Context,
|
||||
entryInfo: EntryInfo,
|
||||
struct: StructureParser.Result): Dataset? {
|
||||
val title = makeEntryTitle(entryInfo)
|
||||
val views = newRemoteViews(context.packageName, title)
|
||||
val views = newRemoteViews(context, title, entryInfo.icon)
|
||||
val builder = Dataset.Builder(views)
|
||||
builder.setId(entryInfo.id)
|
||||
|
||||
struct.password.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.password)) }
|
||||
|
||||
val ids = ArrayList(struct.username)
|
||||
if (entryInfo.username.contains("@") || struct.username.isEmpty())
|
||||
ids.addAll(struct.email)
|
||||
ids.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.username)) }
|
||||
struct.usernameId?.let { usernameId ->
|
||||
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
|
||||
}
|
||||
struct.passwordId?.let { password ->
|
||||
builder.setValue(password, AutofillValue.forText(entryInfo.password))
|
||||
}
|
||||
|
||||
return try {
|
||||
builder.build()
|
||||
@@ -87,9 +112,16 @@ object AutofillHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to hit when right key is selected
|
||||
* Build the Autofill response for one entry
|
||||
*/
|
||||
fun buildResponseWhenEntrySelected(activity: Activity, entryInfo: EntryInfo) {
|
||||
fun buildResponse(activity: Activity, entryInfo: EntryInfo) {
|
||||
buildResponse(activity, ArrayList<EntryInfo>().apply { add(entryInfo) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Autofill response for many entry
|
||||
*/
|
||||
fun buildResponse(activity: Activity, entriesInfo: List<EntryInfo>) {
|
||||
var setResultOk = false
|
||||
activity.intent?.extras?.let { extras ->
|
||||
if (extras.containsKey(ASSIST_STRUCTURE)) {
|
||||
@@ -97,8 +129,9 @@ object AutofillHelper {
|
||||
StructureParser(structure).parse()?.let { result ->
|
||||
// New Response
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
val dataset = buildDataset(activity, entryInfo, result)
|
||||
responseBuilder.addDataset(dataset)
|
||||
entriesInfo.forEach {
|
||||
responseBuilder.addDataset(buildDataset(activity, it, result))
|
||||
}
|
||||
val mReplyIntent = Intent()
|
||||
Log.d(activity.javaClass.name, "Successed Autofill auth.")
|
||||
mReplyIntent.putExtra(
|
||||
@@ -116,12 +149,48 @@ object AutofillHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to perform actions if item is found or not after an auto search in [database]
|
||||
*/
|
||||
fun checkAutoSearchInfo(context: Context,
|
||||
database: Database,
|
||||
searchInfo: SearchInfo?,
|
||||
onItemsFound: (items: List<EntryInfo>) -> Unit,
|
||||
onItemNotFound: () -> Unit,
|
||||
onDatabaseClosed: () -> Unit) {
|
||||
if (database.loaded && TimeoutHelper.checkTime(context)) {
|
||||
var searchWithoutUI = false
|
||||
if (PreferencesUtil.isAutofillAutoSearchEnable(context)
|
||||
&& searchInfo != null) {
|
||||
// If search provide results
|
||||
database.createVirtualGroupFromSearch(searchInfo, SearchHelper.MAX_SEARCH_ENTRY)?.let { searchGroup ->
|
||||
if (searchGroup.getNumberOfChildEntries() > 0) {
|
||||
searchWithoutUI = true
|
||||
onItemsFound.invoke(
|
||||
searchGroup.getChildEntriesInfo(database))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!searchWithoutUI) {
|
||||
onItemNotFound.invoke()
|
||||
}
|
||||
} else {
|
||||
onDatabaseClosed.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to start an activity with an Autofill for result
|
||||
*/
|
||||
fun startActivityForAutofillResult(activity: Activity, intent: Intent, assistStructure: AssistStructure) {
|
||||
fun startActivityForAutofillResult(activity: Activity,
|
||||
intent: Intent,
|
||||
assistStructure: AssistStructure,
|
||||
searchInfo: SearchInfo?) {
|
||||
EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent)
|
||||
intent.putExtra(ASSIST_STRUCTURE, assistStructure)
|
||||
searchInfo?.let {
|
||||
intent.putExtra(KEY_SEARCH_INFO, it)
|
||||
}
|
||||
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
|
||||
}
|
||||
|
||||
@@ -140,9 +209,18 @@ object AutofillHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private fun newRemoteViews(packageName: String, remoteViewsText: String): RemoteViews {
|
||||
val presentation = RemoteViews(packageName, R.layout.item_autofill_service)
|
||||
presentation.setTextViewText(R.id.text, remoteViewsText)
|
||||
private fun newRemoteViews(context: Context,
|
||||
remoteViewsText: String,
|
||||
remoteViewsIcon: IconImage? = null): RemoteViews {
|
||||
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
|
||||
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
|
||||
if (remoteViewsIcon != null) {
|
||||
presentation.assignDatabaseIcon(context,
|
||||
R.id.autofill_entry_icon,
|
||||
Database.getInstance().drawFactory,
|
||||
remoteViewsIcon,
|
||||
ContextCompat.getColor(context, R.color.green))
|
||||
}
|
||||
return presentation
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||
import com.kunzisoft.keepass.activities.GroupActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
class AutofillLauncherActivity : AppCompatActivity() {
|
||||
@@ -41,11 +40,31 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
||||
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
|
||||
if (assistStructure != null) {
|
||||
if (Database.getInstance().loaded && TimeoutHelper.checkTime(this))
|
||||
GroupActivity.launchForAutofillResult(this, assistStructure, PreferencesUtil.enableReadOnlyDatabase(this))
|
||||
else {
|
||||
FileDatabaseSelectActivity.launchForAutofillResult(this, assistStructure)
|
||||
// Build search param
|
||||
val searchInfo = SearchInfo().apply {
|
||||
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
|
||||
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
||||
}
|
||||
// If database is open
|
||||
AutofillHelper.checkAutoSearchInfo(this,
|
||||
Database.getInstance(),
|
||||
searchInfo,
|
||||
{ items ->
|
||||
// Items found
|
||||
AutofillHelper.buildResponse(this, items)
|
||||
finish()
|
||||
},
|
||||
{
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForAutofillResult(this,
|
||||
assistStructure)
|
||||
},
|
||||
{
|
||||
// If database not open
|
||||
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
||||
assistStructure, searchInfo)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
@@ -61,10 +80,20 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
|
||||
fun getAuthIntentSenderForResponse(context: Context): IntentSender {
|
||||
val intent = Intent(context, AutofillLauncherActivity::class.java)
|
||||
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
|
||||
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
|
||||
|
||||
fun getAuthIntentSenderForResponse(context: Context,
|
||||
searchInfo: SearchInfo? = null): IntentSender {
|
||||
return PendingIntent.getActivity(context, 0,
|
||||
intent, PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
||||
// Doesn't work with Parcelable (don't know why?)
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
searchInfo?.let {
|
||||
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
|
||||
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
|
||||
}
|
||||
},
|
||||
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,31 +22,78 @@ package com.kunzisoft.keepass.autofill
|
||||
import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import android.service.autofill.*
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.util.Log
|
||||
import android.widget.RemoteViews
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
class KeeAutofillService : AutofillService() {
|
||||
|
||||
override fun onFillRequest(request: FillRequest, cancellationSignal: CancellationSignal,
|
||||
override fun onFillRequest(request: FillRequest,
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: FillCallback) {
|
||||
val fillContexts = request.fillContexts
|
||||
val latestStructure = fillContexts[fillContexts.size - 1].structure
|
||||
|
||||
cancellationSignal.setOnCancelListener { Log.e(TAG, "Cancel autofill not implemented in this sample.") }
|
||||
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
|
||||
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
// Check user's settings for authenticating Responses and Datasets.
|
||||
val parseResult = StructureParser(latestStructure).parse()
|
||||
parseResult?.allAutofillIds()?.let { autofillIds ->
|
||||
if (listOf(*autofillIds).isNotEmpty()) {
|
||||
StructureParser(latestStructure).parse()?.let { parseResult ->
|
||||
|
||||
val searchInfo = SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
webDomain = parseResult.domain
|
||||
}
|
||||
|
||||
AutofillHelper.checkAutoSearchInfo(this,
|
||||
Database.getInstance(),
|
||||
searchInfo,
|
||||
{ items ->
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
AutofillHelper.addHeader(responseBuilder, packageName,
|
||||
parseResult.domain, parseResult.applicationId)
|
||||
items.forEach {
|
||||
responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult))
|
||||
}
|
||||
callback.onSuccess(responseBuilder.build())
|
||||
},
|
||||
{
|
||||
// Show UI if no search result
|
||||
showUIForEntrySelection(parseResult, searchInfo, callback)
|
||||
},
|
||||
{
|
||||
// Show UI if database not open
|
||||
showUIForEntrySelection(parseResult, searchInfo, callback)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||
searchInfo: SearchInfo,
|
||||
callback: FillCallback) {
|
||||
parseResult.allAutofillIds().let { autofillIds ->
|
||||
if (autofillIds.isNotEmpty()) {
|
||||
// If the entire Autofill Response is authenticated, AuthActivity is used
|
||||
// to generate Response.
|
||||
val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this)
|
||||
val presentation = RemoteViews(packageName, R.layout.item_autofill_service_unlock)
|
||||
responseBuilder.setAuthentication(autofillIds, sender, presentation)
|
||||
val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this,
|
||||
searchInfo)
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
val remoteViewsUnlock: RemoteViews = if (!parseResult.domain.isNullOrEmpty()) {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply {
|
||||
setTextViewText(R.id.autofill_web_domain_text, parseResult.domain)
|
||||
}
|
||||
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
|
||||
setTextViewText(R.id.autofill_app_id_text, parseResult.applicationId)
|
||||
}
|
||||
} else {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock)
|
||||
}
|
||||
responseBuilder.setAuthentication(autofillIds, sender, remoteViewsUnlock)
|
||||
callback.onSuccess(responseBuilder.build())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ package com.kunzisoft.keepass.autofill
|
||||
|
||||
import android.app.assist.AssistStructure
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.text.InputType
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.autofill.AutofillId
|
||||
@@ -37,72 +37,196 @@ internal class StructureParser(private val structure: AssistStructure) {
|
||||
private var usernameCandidate: AutofillId? = null
|
||||
|
||||
fun parse(): Result? {
|
||||
result = Result()
|
||||
result?.apply {
|
||||
usernameCandidate = null
|
||||
for (i in 0 until structure.windowNodeCount) {
|
||||
val windowNode = structure.getWindowNodeAt(i)
|
||||
title.add(windowNode.title)
|
||||
windowNode.rootViewNode.webDomain?.let {
|
||||
webDomain.add(it)
|
||||
}
|
||||
parseViewNode(windowNode.rootViewNode)
|
||||
}
|
||||
// If not explicit username field found, add the field just before password field.
|
||||
if (username.isEmpty() && email.isEmpty()
|
||||
&& password.isNotEmpty() && usernameCandidate != null)
|
||||
username.add(usernameCandidate!!)
|
||||
}
|
||||
try {
|
||||
result = Result()
|
||||
result?.apply {
|
||||
usernameCandidate = null
|
||||
mainLoop@ for (i in 0 until structure.windowNodeCount) {
|
||||
val windowNode = structure.getWindowNodeAt(i)
|
||||
applicationId = windowNode.title.toString().split("/")[0]
|
||||
Log.d(TAG, "Autofill applicationId: $applicationId")
|
||||
|
||||
return result
|
||||
if (parseViewNode(windowNode.rootViewNode))
|
||||
break@mainLoop
|
||||
}
|
||||
// If not explicit username field found, add the field just before password field.
|
||||
if (usernameId == null && passwordId != null && usernameCandidate != null)
|
||||
usernameId = usernameCandidate
|
||||
}
|
||||
|
||||
// Return the result only if password field is retrieved
|
||||
return if (result?.usernameId != null
|
||||
&& result?.passwordId != null)
|
||||
result
|
||||
else
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseViewNode(node: AssistStructure.ViewNode) {
|
||||
val hints = node.autofillHints
|
||||
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
|
||||
// Get the domain of a web app
|
||||
node.webDomain?.let {
|
||||
result?.domain = it
|
||||
Log.d(TAG, "Autofill domain: $it")
|
||||
}
|
||||
|
||||
// Only parse visible nodes
|
||||
if (node.visibility == View.VISIBLE) {
|
||||
if (node.autofillId != null
|
||||
&& node.autofillType == View.AUTOFILL_TYPE_TEXT) {
|
||||
// Parse methods
|
||||
val hints = node.autofillHints
|
||||
if (hints != null && hints.isNotEmpty()) {
|
||||
if (parseNodeByAutofillHint(node))
|
||||
return true
|
||||
} else if (parseNodeByHtmlAttributes(node))
|
||||
return true
|
||||
else if (parseNodeByAndroidInput(node))
|
||||
return true
|
||||
}
|
||||
// Recursive method to process each node
|
||||
for (i in 0 until node.childCount) {
|
||||
if (parseViewNode(node.getChildAt(i)))
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun parseNodeByAutofillHint(node: AssistStructure.ViewNode): Boolean {
|
||||
val autofillId = node.autofillId
|
||||
if (autofillId != null) {
|
||||
if (hints != null && hints.isNotEmpty()) {
|
||||
when {
|
||||
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_USERNAME == it } -> result?.username?.add(autofillId)
|
||||
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_EMAIL_ADDRESS == it } -> result?.email?.add(autofillId)
|
||||
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_PASSWORD == it } -> result?.password?.add(autofillId)
|
||||
else -> Log.d(TAG, "unsupported hints")
|
||||
node.autofillHints?.forEach {
|
||||
when {
|
||||
it.equals(View.AUTOFILL_HINT_USERNAME, true)
|
||||
|| it.equals(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
||||
|| it.equals(View.AUTOFILL_HINT_PHONE, true)
|
||||
|| it.equals("usernameOrEmail", true)-> {
|
||||
result?.usernameId = autofillId
|
||||
Log.d(TAG, "Autofill username hint")
|
||||
}
|
||||
} else if (node.autofillType == View.AUTOFILL_TYPE_TEXT) {
|
||||
val inputType = node.inputType
|
||||
when {
|
||||
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS > 0 -> result?.email?.add(autofillId)
|
||||
inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD > 0 -> result?.password?.add(autofillId)
|
||||
result?.password?.isEmpty() == true -> usernameCandidate = autofillId
|
||||
it.equals(View.AUTOFILL_HINT_PASSWORD, true)
|
||||
|| it.contains("password", true) -> {
|
||||
result?.passwordId = autofillId
|
||||
Log.d(TAG, "Autofill password hint")
|
||||
return true
|
||||
}
|
||||
// Ignore autocomplete="off"
|
||||
// https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
|
||||
it.equals("off", true) ||
|
||||
it.equals("on", true) -> {
|
||||
Log.d(TAG, "Autofill web hint")
|
||||
return parseNodeByHtmlAttributes(node)
|
||||
}
|
||||
else -> Log.d(TAG, "Autofill unsupported hint $it")
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
|
||||
val autofillId = node.autofillId
|
||||
val nodHtml = node.htmlInfo
|
||||
when (nodHtml?.tag?.toLowerCase(Locale.ENGLISH)) {
|
||||
"input" -> {
|
||||
nodHtml.attributes?.forEach { pairAttribute ->
|
||||
when (pairAttribute.first.toLowerCase(Locale.ENGLISH)) {
|
||||
"type" -> {
|
||||
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
|
||||
"tel", "email" -> {
|
||||
result?.usernameId = autofillId
|
||||
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||
}
|
||||
"text" -> {
|
||||
usernameCandidate = autofillId
|
||||
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||
}
|
||||
"password" -> {
|
||||
result?.passwordId = autofillId
|
||||
Log.d(TAG, "Autofill password web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for (i in 0 until node.childCount)
|
||||
parseViewNode(node.getChildAt(i))
|
||||
private fun parseNodeByAndroidInput(node: AssistStructure.ViewNode): Boolean {
|
||||
val autofillId = node.autofillId
|
||||
val inputType = node.inputType
|
||||
if (inputType and InputType.TYPE_CLASS_TEXT != 0) {
|
||||
when {
|
||||
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS != 0 -> {
|
||||
result?.usernameId = autofillId
|
||||
Log.d(TAG, "Autofill username android type: $inputType")
|
||||
}
|
||||
inputType and InputType.TYPE_TEXT_VARIATION_NORMAL != 0 ||
|
||||
inputType and InputType.TYPE_NUMBER_VARIATION_NORMAL != 0 ||
|
||||
inputType and InputType.TYPE_TEXT_VARIATION_PERSON_NAME != 0 -> {
|
||||
usernameCandidate = autofillId
|
||||
Log.d(TAG, "Autofill username candidate android type: $inputType")
|
||||
}
|
||||
inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD != 0 ||
|
||||
inputType and InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD != 0 ||
|
||||
inputType and InputType.TYPE_NUMBER_VARIATION_PASSWORD != 0 -> {
|
||||
result?.passwordId = autofillId
|
||||
Log.d(TAG, "Autofill password android type: $inputType")
|
||||
return true
|
||||
}
|
||||
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT != 0 ||
|
||||
inputType and InputType.TYPE_TEXT_VARIATION_FILTER != 0 ||
|
||||
inputType and InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE != 0 ||
|
||||
inputType and InputType.TYPE_TEXT_VARIATION_PHONETIC != 0 ||
|
||||
inputType and InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS != 0 ||
|
||||
inputType and InputType.TYPE_TEXT_VARIATION_URI != 0 ||
|
||||
inputType and InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT != 0 ||
|
||||
inputType and InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS != 0 ||
|
||||
inputType and InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD != 0 -> {
|
||||
// Type not used
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Autofill unknown android type: $inputType")
|
||||
usernameCandidate = autofillId
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
internal class Result {
|
||||
val title: MutableList<CharSequence>
|
||||
val webDomain: MutableList<String>
|
||||
val username: MutableList<AutofillId>
|
||||
val email: MutableList<AutofillId>
|
||||
val password: MutableList<AutofillId>
|
||||
var applicationId: String? = null
|
||||
var domain: String? = null
|
||||
set(value) {
|
||||
if (field == null)
|
||||
field = value
|
||||
}
|
||||
|
||||
init {
|
||||
title = ArrayList()
|
||||
webDomain = ArrayList()
|
||||
username = ArrayList()
|
||||
email = ArrayList()
|
||||
password = ArrayList()
|
||||
}
|
||||
var usernameId: AutofillId? = null
|
||||
set(value) {
|
||||
if (field == null)
|
||||
field = value
|
||||
}
|
||||
|
||||
var passwordId: AutofillId? = null
|
||||
set(value) {
|
||||
if (field == null)
|
||||
field = value
|
||||
}
|
||||
|
||||
fun allAutofillIds(): Array<AutofillId> {
|
||||
val all = ArrayList<AutofillId>()
|
||||
all.addAll(username)
|
||||
all.addAll(email)
|
||||
all.addAll(password)
|
||||
usernameId?.let {
|
||||
all.add(it)
|
||||
}
|
||||
passwordId?.let {
|
||||
all.add(it)
|
||||
}
|
||||
return all.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
||||
// really not much to do when no fingerprint support found
|
||||
isKeyManagerInit = false
|
||||
} else {
|
||||
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
|
||||
|
||||
try {
|
||||
this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE)
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.action
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
@@ -70,6 +71,9 @@ open class AssignPasswordInDatabaseRunnable (
|
||||
// Erase the biometric
|
||||
CipherDatabaseAction.getInstance(context)
|
||||
.deleteByDatabaseUri(mDatabaseUri)
|
||||
// Erase the register keyfile
|
||||
FileDatabaseHistoryAction.getInstance(context)
|
||||
.deleteKeyFileByDatabaseUri(mDatabaseUri)
|
||||
|
||||
if (!result.isSuccess) {
|
||||
// Erase the current master key
|
||||
|
||||
@@ -86,8 +86,11 @@ class LoadDatabaseRunnable(private val context: Context,
|
||||
.addOrUpdateCipherDatabase(cipherDatabaseEntity) // return value not called
|
||||
}
|
||||
|
||||
// Register the current time to init the lock timer
|
||||
PreferencesUtil.saveCurrentTime(context)
|
||||
|
||||
// Start the opening notification
|
||||
DatabaseOpenNotificationService.startIfAllowed(context)
|
||||
DatabaseOpenNotificationService.start(context)
|
||||
} else {
|
||||
mDatabase.closeAndClear(cacheDirectory)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
@@ -35,6 +36,7 @@ import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
||||
@@ -85,12 +87,17 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
||||
|
||||
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
||||
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||
TimeoutHelper.temporarilyDisableTimeout(activity)
|
||||
TimeoutHelper.temporarilyDisableTimeout()
|
||||
// Stop the opening notification
|
||||
DatabaseOpenNotificationService.stop(activity)
|
||||
startOrUpdateDialog(titleId, messageId, warningId)
|
||||
|
||||
}
|
||||
|
||||
override fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||
TimeoutHelper.temporarilyDisableTimeout(activity)
|
||||
TimeoutHelper.temporarilyDisableTimeout()
|
||||
// Stop the opening notification
|
||||
DatabaseOpenNotificationService.stop(activity)
|
||||
startOrUpdateDialog(titleId, messageId, warningId)
|
||||
}
|
||||
|
||||
@@ -98,7 +105,18 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
||||
onActionFinish?.invoke(actionTask, result)
|
||||
// Remove the progress task
|
||||
ProgressTaskDialogFragment.stop(activity)
|
||||
TimeoutHelper.releaseTemporarilyDisableTimeoutAndLockIfTimeout(activity)
|
||||
TimeoutHelper.releaseTemporarilyDisableTimeout()
|
||||
|
||||
val inTime = if (activity is LockingActivity) {
|
||||
TimeoutHelper.checkTimeAndLockIfTimeout(activity)
|
||||
} else {
|
||||
TimeoutHelper.checkTime(activity)
|
||||
}
|
||||
// Start the opening notification if in time
|
||||
// (databaseOpenService is open manually in Action Open Task)
|
||||
if (actionTask != ACTION_DATABASE_LOAD_TASK && inTime) {
|
||||
DatabaseOpenNotificationService.start(activity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +144,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
||||
if (serviceConnection == null) {
|
||||
serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder).apply {
|
||||
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
|
||||
addActionTaskListener(actionTaskListener)
|
||||
getService().checkAction()
|
||||
}
|
||||
|
||||
@@ -20,15 +20,11 @@
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||
import com.kunzisoft.keepass.database.cursor.EntryCursorKDB
|
||||
import com.kunzisoft.keepass.database.cursor.EntryCursorKDBX
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
@@ -49,6 +45,7 @@ import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
|
||||
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.stream.readBytes4ToInt
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.utils.SingletonHolder
|
||||
@@ -137,6 +134,9 @@ class Database {
|
||||
val version: String
|
||||
get() = mDatabaseKDB?.version ?: mDatabaseKDBX?.version ?: "-"
|
||||
|
||||
val type: Class<*>?
|
||||
get() = mDatabaseKDB?.javaClass ?: mDatabaseKDBX?.javaClass
|
||||
|
||||
val allowDataCompression: Boolean
|
||||
get() = mDatabaseKDBX != null
|
||||
|
||||
@@ -397,54 +397,17 @@ class Database {
|
||||
false
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun search(str: String, max: Int = Integer.MAX_VALUE): Group? {
|
||||
return mSearchHelper?.search(this, str, max)
|
||||
fun createVirtualGroupFromSearch(searchQuery: String, max: Int = Integer.MAX_VALUE): Group? {
|
||||
return mSearchHelper?.createVirtualGroupWithSearchResult(this, searchQuery, max)
|
||||
}
|
||||
|
||||
fun searchEntries(context: Context, query: String): Cursor? {
|
||||
|
||||
var cursorKDB: EntryCursorKDB? = null
|
||||
var cursorKDBX: EntryCursorKDBX? = null
|
||||
|
||||
if (mDatabaseKDB != null)
|
||||
cursorKDB = EntryCursorKDB()
|
||||
if (mDatabaseKDBX != null)
|
||||
cursorKDBX = EntryCursorKDBX()
|
||||
|
||||
val searchResult = search(query, SearchHelper.MAX_SEARCH_ENTRY)
|
||||
if (searchResult != null) {
|
||||
// Search in hide entries but not meta-stream
|
||||
for (entry in searchResult.getFilteredChildEntries(*Group.ChildFilter.getDefaults(context))) {
|
||||
entry.entryKDB?.let {
|
||||
cursorKDB?.addEntry(it)
|
||||
}
|
||||
entry.entryKDBX?.let {
|
||||
cursorKDBX?.addEntry(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cursorKDB ?: cursorKDBX
|
||||
}
|
||||
|
||||
fun getEntryFrom(cursor: Cursor): Entry? {
|
||||
val iconFactory = mDatabaseKDB?.iconFactory ?: mDatabaseKDBX?.iconFactory ?: IconImageFactory()
|
||||
|
||||
return createEntry()?.apply {
|
||||
startManageEntry(this)
|
||||
mDatabaseKDB?.let {
|
||||
entryKDB?.let { entryKDB ->
|
||||
(cursor as EntryCursorKDB).populateEntry(entryKDB, iconFactory)
|
||||
}
|
||||
}
|
||||
mDatabaseKDBX?.let {
|
||||
entryKDBX?.let { entryKDBX ->
|
||||
(cursor as EntryCursorKDBX).populateEntry(entryKDBX, iconFactory)
|
||||
}
|
||||
}
|
||||
stopManageEntry(this)
|
||||
}
|
||||
fun createVirtualGroupFromSearch(searchInfo: SearchInfo, max: Int = Integer.MAX_VALUE): Group? {
|
||||
val query = (if (searchInfo.webDomain != null)
|
||||
searchInfo.webDomain
|
||||
else
|
||||
searchInfo.applicationId)
|
||||
?: return null
|
||||
return mSearchHelper?.createVirtualGroupWithSearchResult(this, query, max)
|
||||
}
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
|
||||
@@ -142,7 +142,7 @@ class DateInstant : Parcelable {
|
||||
fun getDateTimeString(resources: Resources, date: Date): String {
|
||||
return java.text.DateFormat.getDateTimeInstance(
|
||||
java.text.DateFormat.MEDIUM,
|
||||
java.text.DateFormat.MEDIUM,
|
||||
java.text.DateFormat.SHORT,
|
||||
ConfigurationCompat.getLocales(resources.configuration)[0])
|
||||
.format(date)
|
||||
}
|
||||
|
||||
@@ -398,6 +398,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
database?.startManageEntry(this)
|
||||
entryInfo.id = nodeId.toString()
|
||||
entryInfo.title = title
|
||||
entryInfo.icon = icon
|
||||
entryInfo.username = username
|
||||
entryInfo.password = password
|
||||
entryInfo.url = url
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import com.kunzisoft.keepass.database.element.node.*
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
@@ -251,6 +252,14 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
ArrayList()
|
||||
}
|
||||
|
||||
fun getChildEntriesInfo(database: Database): List<EntryInfo> {
|
||||
val entriesInfo = ArrayList<EntryInfo>()
|
||||
getChildEntries().forEach { entry ->
|
||||
entriesInfo.add(entry.getEntryInfo(database))
|
||||
}
|
||||
return entriesInfo
|
||||
}
|
||||
|
||||
fun getFilteredChildEntries(vararg filter: ChildFilter): List<Entry> {
|
||||
val withoutMetaStream = filter.contains(ChildFilter.META_STREAM)
|
||||
val showExpiredEntries = !filter.contains(ChildFilter.EXPIRED)
|
||||
|
||||
@@ -26,7 +26,6 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
||||
import java.io.IOException
|
||||
@@ -262,6 +261,7 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TYPE = DatabaseKDB::class.java
|
||||
|
||||
const val BACKUP_FOLDER_TITLE = "Backup"
|
||||
private const val BACKUP_FOLDER_UNDEFINED_ID = -1
|
||||
|
||||
@@ -29,7 +29,8 @@ import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.DeletedObject
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
@@ -551,6 +552,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TYPE = DatabaseKDBX::class.java
|
||||
private val TAG = DatabaseKDBX::class.java.name
|
||||
|
||||
private const val DEFAULT_HISTORY_MAX_ITEMS = 10 // -1 unlimited
|
||||
|
||||
@@ -90,6 +90,7 @@ class FieldReferencesEngine {
|
||||
|
||||
if (result != null) {
|
||||
val found = result.entry
|
||||
found?.stopToManageFieldReferences()
|
||||
val wanted = result.wanted
|
||||
|
||||
var data: String? = null
|
||||
@@ -145,22 +146,15 @@ class FieldReferencesEngine {
|
||||
searchParametersV4.setupNone()
|
||||
|
||||
searchParametersV4.searchString = ref.substring(4)
|
||||
if (scan == 'T') {
|
||||
searchParametersV4.searchInTitles = true
|
||||
} else if (scan == 'U') {
|
||||
searchParametersV4.searchInUserNames = true
|
||||
} else if (scan == 'A') {
|
||||
searchParametersV4.searchInUrls = true
|
||||
} else if (scan == 'P') {
|
||||
searchParametersV4.searchInPasswords = true
|
||||
} else if (scan == 'N') {
|
||||
searchParametersV4.searchInNotes = true
|
||||
} else if (scan == 'I') {
|
||||
searchParametersV4.searchInUUIDs = true
|
||||
} else if (scan == 'O') {
|
||||
searchParametersV4.searchInOther = true
|
||||
} else {
|
||||
return null
|
||||
when (scan) {
|
||||
'T' -> searchParametersV4.searchInTitles = true
|
||||
'U' -> searchParametersV4.searchInUserNames = true
|
||||
'A' -> searchParametersV4.searchInUrls = true
|
||||
'P' -> searchParametersV4.searchInPasswords = true
|
||||
'N' -> searchParametersV4.searchInNotes = true
|
||||
'I' -> searchParametersV4.searchInUUIDs = true
|
||||
'O' -> searchParametersV4.searchInOther = true
|
||||
else -> return null
|
||||
}
|
||||
|
||||
val list = ArrayList<EntryKDBX>()
|
||||
|
||||
@@ -21,8 +21,7 @@ package com.kunzisoft.keepass.database.element.node
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
||||
import java.util.UUID
|
||||
import java.util.*
|
||||
|
||||
class NodeIdUUID : NodeId<UUID> {
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIterator
|
||||
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDB
|
||||
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDBX
|
||||
import java.util.*
|
||||
|
||||
class SearchHelper(private val isOmitBackup: Boolean) {
|
||||
|
||||
@@ -36,22 +35,19 @@ class SearchHelper(private val isOmitBackup: Boolean) {
|
||||
|
||||
private var incrementEntry = 0
|
||||
|
||||
fun search(database: Database, qStr: String, max: Int): Group? {
|
||||
fun createVirtualGroupWithSearchResult(database: Database, searchQuery: String, max: Int): Group? {
|
||||
|
||||
val searchGroup = database.createGroup()
|
||||
searchGroup?.title = "\"" + qStr + "\""
|
||||
searchGroup?.title = "\"" + searchQuery + "\""
|
||||
|
||||
// Search all entries
|
||||
val loc = Locale.getDefault()
|
||||
val finalQStr = qStr.toLowerCase(loc)
|
||||
|
||||
incrementEntry = 0
|
||||
database.rootGroup?.doForEachChild(
|
||||
object : NodeHandler<Entry>() {
|
||||
override fun operate(node: Entry): Boolean {
|
||||
if (incrementEntry >= max)
|
||||
return false
|
||||
if (entryContainsString(node, finalQStr, loc)) {
|
||||
if (entryContainsString(node, searchQuery)) {
|
||||
searchGroup?.addChildEntry(node)
|
||||
incrementEntry++
|
||||
}
|
||||
@@ -73,7 +69,7 @@ class SearchHelper(private val isOmitBackup: Boolean) {
|
||||
return searchGroup
|
||||
}
|
||||
|
||||
private fun entryContainsString(entry: Entry, searchString: String, locale: Locale): Boolean {
|
||||
private fun entryContainsString(entry: Entry, searchString: String): Boolean {
|
||||
|
||||
// Entry don't contains string if the search string is empty
|
||||
if (searchString.isEmpty())
|
||||
@@ -90,11 +86,10 @@ class SearchHelper(private val isOmitBackup: Boolean) {
|
||||
|
||||
iterator?.let {
|
||||
while (it.hasNext()) {
|
||||
val str = it.next()
|
||||
if (str.isNotEmpty()) {
|
||||
if (str.toLowerCase(locale).contains(searchString)) {
|
||||
val currentString = it.next()
|
||||
if (currentString.isNotEmpty()
|
||||
&& currentString.contains(searchString, true)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ constructor(private val mEntry: EntryKDB,
|
||||
title -> mEntry.title
|
||||
url -> mEntry.url
|
||||
username -> mEntry.username
|
||||
comment -> mEntry.notes
|
||||
notes -> mEntry.notes
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ constructor(private val mEntry: EntryKDB,
|
||||
title -> mSearchParameters.searchInTitles
|
||||
url -> mSearchParameters.searchInUrls
|
||||
username -> mSearchParameters.searchInUserNames
|
||||
comment -> mSearchParameters.searchInNotes
|
||||
notes -> mSearchParameters.searchInNotes
|
||||
else -> true
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ constructor(private val mEntry: EntryKDB,
|
||||
private const val title = 0
|
||||
private const val url = 1
|
||||
private const val username = 2
|
||||
private const val comment = 3
|
||||
private const val notes = 3
|
||||
private const val maxEntries = 4
|
||||
}
|
||||
|
||||
|
||||
@@ -94,7 +94,8 @@ open class Education(val activity: Activity) {
|
||||
R.string.education_copy_username_key,
|
||||
R.string.education_entry_edit_key,
|
||||
R.string.education_password_generator_key,
|
||||
R.string.education_entry_new_field_key)
|
||||
R.string.education_entry_new_field_key,
|
||||
R.string.education_setup_OTP_key)
|
||||
|
||||
|
||||
/**
|
||||
@@ -271,6 +272,18 @@ open class Education(val activity: Activity) {
|
||||
context.resources.getBoolean(R.bool.education_entry_new_field_default))
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the explanatory view to setup OTP has already been displayed.
|
||||
*
|
||||
* @param context The context to open the SharedPreferences
|
||||
* @return boolean value of education_setup_OTP_key key
|
||||
*/
|
||||
fun isEducationSetupOTPPerformed(context: Context): Boolean {
|
||||
val prefs = getEducationSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.education_setup_OTP_key),
|
||||
context.resources.getBoolean(R.bool.education_setup_OTP_default))
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines if the reset education preference has been reclicked
|
||||
*
|
||||
|
||||
@@ -37,7 +37,7 @@ class EntryEditActivityEducation(activity: Activity)
|
||||
activity.getString(R.string.education_generate_password_title),
|
||||
activity.getString(R.string.education_generate_password_summary))
|
||||
.textColorInt(Color.WHITE)
|
||||
.tintTarget(false)
|
||||
.tintTarget(true)
|
||||
.cancelable(true),
|
||||
object : TapTargetView.Listener() {
|
||||
override fun onTargetClick(view: TapTargetView) {
|
||||
@@ -66,7 +66,7 @@ class EntryEditActivityEducation(activity: Activity)
|
||||
activity.getString(R.string.education_entry_new_field_title),
|
||||
activity.getString(R.string.education_entry_new_field_summary))
|
||||
.textColorInt(Color.WHITE)
|
||||
.tintTarget(false)
|
||||
.tintTarget(true)
|
||||
.cancelable(true),
|
||||
object : TapTargetView.Listener() {
|
||||
override fun onTargetClick(view: TapTargetView) {
|
||||
@@ -82,4 +82,33 @@ class EntryEditActivityEducation(activity: Activity)
|
||||
},
|
||||
R.string.education_entry_new_field_key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and display learning views
|
||||
* Displays the explanation to setup OTP
|
||||
*/
|
||||
fun checkAndPerformedSetUpOTPEducation(educationView: View,
|
||||
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
||||
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
|
||||
return checkAndPerformedEducation(!isEducationSetupOTPPerformed(activity),
|
||||
TapTarget.forView(educationView,
|
||||
activity.getString(R.string.education_setup_OTP_title),
|
||||
activity.getString(R.string.education_setup_OTP_summary))
|
||||
.textColorInt(Color.WHITE)
|
||||
.tintTarget(true)
|
||||
.cancelable(true),
|
||||
object : TapTargetView.Listener() {
|
||||
override fun onTargetClick(view: TapTargetView) {
|
||||
super.onTargetClick(view)
|
||||
onEducationViewClick?.invoke(view)
|
||||
}
|
||||
|
||||
override fun onOuterCircleClick(view: TapTargetView?) {
|
||||
super.onOuterCircleClick(view)
|
||||
view?.dismiss(false)
|
||||
onOuterViewClick?.invoke(view)
|
||||
}
|
||||
},
|
||||
R.string.education_setup_OTP_key)
|
||||
}
|
||||
}
|
||||
@@ -22,16 +22,16 @@ package com.kunzisoft.keepass.icons
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Color
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.widget.ImageViewCompat
|
||||
import android.util.Log
|
||||
import android.widget.ImageView
|
||||
import android.widget.RemoteViews
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.core.widget.ImageViewCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
@@ -70,6 +70,23 @@ class IconDrawableFactory {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to assign a drawable to a RemoteView and tint it
|
||||
*/
|
||||
fun assignDrawableToRemoteViews(superDrawable: SuperDrawable,
|
||||
remoteViews: RemoteViews,
|
||||
imageId: Int,
|
||||
tintColor: Int = Color.BLACK) {
|
||||
val bitmap = superDrawable.drawable.toBitmap()
|
||||
// Tint bitmap if it's not a custom icon
|
||||
if (!superDrawable.custom) {
|
||||
Canvas(bitmap).drawBitmap(bitmap, 0.0F, 0.0F, Paint().apply {
|
||||
colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)
|
||||
})
|
||||
}
|
||||
remoteViews.setImageViewBitmap(imageId, bitmap)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the [SuperDrawable] [icon] (from cache, or build it and add it to the cache if not exists yet), then [tint] it with [tintColor] if needed
|
||||
*/
|
||||
@@ -233,7 +250,6 @@ class IconDrawableFactory {
|
||||
*/
|
||||
fun ImageView.assignDefaultDatabaseIcon(iconFactory: IconDrawableFactory, tintColor: Int = Color.WHITE) {
|
||||
IconPackChooser.getSelectedIconPack(context)?.let { selectedIconPack ->
|
||||
|
||||
iconFactory.assignDrawableToImageView(
|
||||
iconFactory.getIconSuperDrawable(context,
|
||||
selectedIconPack.defaultIconId,
|
||||
@@ -249,9 +265,10 @@ fun ImageView.assignDefaultDatabaseIcon(iconFactory: IconDrawableFactory, tintCo
|
||||
/**
|
||||
* Assign a database [icon] to an ImageView and tint it with [tintColor] if needed
|
||||
*/
|
||||
fun ImageView.assignDatabaseIcon(iconFactory: IconDrawableFactory, icon: IconImage, tintColor: Int = Color.WHITE) {
|
||||
fun ImageView.assignDatabaseIcon(iconFactory: IconDrawableFactory,
|
||||
icon: IconImage,
|
||||
tintColor: Int = Color.WHITE) {
|
||||
IconPackChooser.getSelectedIconPack(context)?.let { selectedIconPack ->
|
||||
|
||||
iconFactory.assignDrawableToImageView(
|
||||
iconFactory.getIconSuperDrawable(context,
|
||||
icon,
|
||||
@@ -263,3 +280,19 @@ fun ImageView.assignDatabaseIcon(iconFactory: IconDrawableFactory, icon: IconIma
|
||||
tintColor)
|
||||
}
|
||||
}
|
||||
|
||||
fun RemoteViews.assignDatabaseIcon(context: Context,
|
||||
imageId: Int,
|
||||
iconFactory: IconDrawableFactory,
|
||||
icon: IconImage,
|
||||
tintColor: Int = Color.BLACK) {
|
||||
iconFactory.assignDrawableToRemoteViews(
|
||||
iconFactory.getIconSuperDrawable(context,
|
||||
icon,
|
||||
24,
|
||||
true,
|
||||
tintColor),
|
||||
this,
|
||||
imageId,
|
||||
tintColor)
|
||||
}
|
||||
|
||||
@@ -24,14 +24,13 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||
import com.kunzisoft.keepass.activities.GroupActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
|
||||
class KeyboardLauncherActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
if (Database.getInstance().loaded && TimeoutHelper.checkTime(this))
|
||||
GroupActivity.launchForKeyboardSelection(this, PreferencesUtil.enableReadOnlyDatabase(this))
|
||||
GroupActivity.launchForKeyboardSelection(this)
|
||||
else {
|
||||
// Pass extra to get entry
|
||||
FileDatabaseSelectActivity.launchForKeyboardSelection(this)
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.kunzisoft.keepass.magikeyboard
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.inputmethodservice.InputMethodService
|
||||
import android.inputmethodservice.Keyboard
|
||||
import android.inputmethodservice.KeyboardView
|
||||
@@ -42,8 +42,7 @@ import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import com.kunzisoft.keepass.utils.REMOVE_ENTRY_MAGIKEYBOARD_ACTION
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
|
||||
class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
|
||||
@@ -55,29 +54,18 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
private var fieldsAdapter: FieldsAdapter? = null
|
||||
private var playSoundDuringCLick: Boolean = false
|
||||
|
||||
private var lockBroadcastReceiver: BroadcastReceiver? = null
|
||||
private var lockReceiver: LockReceiver? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// Remove the entry and lock the keyboard when the lock signal is receive
|
||||
lockBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
REMOVE_ENTRY_MAGIKEYBOARD_ACTION, LOCK_ACTION -> {
|
||||
lockReceiver = LockReceiver {
|
||||
removeEntryInfo()
|
||||
assignKeyboardView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerReceiver(lockBroadcastReceiver,
|
||||
IntentFilter().apply {
|
||||
addAction(LOCK_ACTION)
|
||||
addAction(REMOVE_ENTRY_MAGIKEYBOARD_ACTION)
|
||||
}
|
||||
)
|
||||
registerLockReceiver(lockReceiver, true)
|
||||
}
|
||||
|
||||
override fun onCreateInputView(): View {
|
||||
@@ -187,12 +175,12 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
private fun switchToPreviousKeyboard() {
|
||||
var imeManager: InputMethodManager? = null
|
||||
try {
|
||||
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
switchToPreviousInputMethod()
|
||||
} else {
|
||||
window.window?.let { window ->
|
||||
imeManager.switchToLastInputMethod(window.attributes.token)
|
||||
imeManager?.switchToLastInputMethod(window.attributes.token)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -214,8 +202,8 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
KEY_BACK_KEYBOARD -> switchToPreviousKeyboard()
|
||||
|
||||
KEY_CHANGE_KEYBOARD -> {
|
||||
val imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imeManager.showInputMethodPicker()
|
||||
(getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?)
|
||||
?.showInputMethodPicker()
|
||||
}
|
||||
KEY_UNLOCK -> {
|
||||
}
|
||||
@@ -301,7 +289,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
|
||||
override fun onDestroy() {
|
||||
dismissCustomKeys()
|
||||
unregisterReceiver(lockBroadcastReceiver)
|
||||
unregisterLockReceiver(lockReceiver)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.model
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
||||
import java.util.*
|
||||
@@ -29,6 +30,7 @@ class EntryInfo : Parcelable {
|
||||
|
||||
var id: String = ""
|
||||
var title: String = ""
|
||||
var icon: IconImage? = null
|
||||
var username: String = ""
|
||||
var password: String = ""
|
||||
var url: String = ""
|
||||
@@ -41,6 +43,7 @@ class EntryInfo : Parcelable {
|
||||
private constructor(parcel: Parcel) {
|
||||
id = parcel.readString() ?: id
|
||||
title = parcel.readString() ?: title
|
||||
icon = parcel.readParcelable(IconImage::class.java.classLoader)
|
||||
username = parcel.readString() ?: username
|
||||
password = parcel.readString() ?: password
|
||||
url = parcel.readString() ?: url
|
||||
@@ -56,6 +59,7 @@ class EntryInfo : Parcelable {
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(id)
|
||||
parcel.writeString(title)
|
||||
parcel.writeParcelable(icon, flags)
|
||||
parcel.writeString(username)
|
||||
parcel.writeString(password)
|
||||
parcel.writeString(url)
|
||||
|
||||
42
app/src/main/java/com/kunzisoft/keepass/model/SearchInfo.kt
Normal file
42
app/src/main/java/com/kunzisoft/keepass/model/SearchInfo.kt
Normal file
@@ -0,0 +1,42 @@
|
||||
package com.kunzisoft.keepass.model
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
||||
class SearchInfo : Parcelable {
|
||||
|
||||
var applicationId: String? = null
|
||||
var webDomain: String? = null
|
||||
|
||||
constructor()
|
||||
|
||||
private constructor(parcel: Parcel) {
|
||||
val readAppId = parcel.readString()
|
||||
applicationId = if (readAppId.isNullOrEmpty()) null else readAppId
|
||||
val readDomain = parcel.readString()
|
||||
webDomain = if (readDomain.isNullOrEmpty()) null else readDomain
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(applicationId ?: "")
|
||||
parcel.writeString(webDomain ?: "")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<SearchInfo> = object : Parcelable.Creator<SearchInfo> {
|
||||
override fun createFromParcel(parcel: Parcel): SearchInfo {
|
||||
return SearchInfo(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<SearchInfo?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,9 @@ import android.os.Build
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.GroupActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import com.kunzisoft.keepass.utils.closeDatabase
|
||||
|
||||
class DatabaseOpenNotificationService: LockNotificationService() {
|
||||
|
||||
@@ -36,8 +37,14 @@ class DatabaseOpenNotificationService: LockNotificationService() {
|
||||
private fun stopNotificationAndSendLock() {
|
||||
// Send lock action
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
// Stop the service
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun actionOnLock() {
|
||||
closeDatabase()
|
||||
// Remove the lock timer (no more needed if it exists)
|
||||
TimeoutHelper.cancelLockTimer(this)
|
||||
// Service is stopped after receive the broadcast
|
||||
super.actionOnLock()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
@@ -60,13 +67,16 @@ class DatabaseOpenNotificationService: LockNotificationService() {
|
||||
|
||||
val database = Database.getInstance()
|
||||
if (database.loaded) {
|
||||
notificationManager?.notify(notificationId, buildNewNotification().apply {
|
||||
startForeground(notificationId, buildNewNotification().apply {
|
||||
setSmallIcon(R.drawable.notification_ic_database_open)
|
||||
setContentTitle(getString(R.string.database_opened))
|
||||
setContentText(database.name + " (" + database.version + ")")
|
||||
setAutoCancel(false)
|
||||
setContentIntent(pendingDatabaseIntent)
|
||||
// Unfortunately swipe is disabled in lollipop+
|
||||
setDeleteIntent(pendingDeleteIntent)
|
||||
addAction(R.drawable.ic_lock_white_24dp, getString(R.string.lock),
|
||||
pendingDeleteIntent)
|
||||
}.build())
|
||||
} else {
|
||||
stopSelf()
|
||||
@@ -80,9 +90,11 @@ class DatabaseOpenNotificationService: LockNotificationService() {
|
||||
companion object {
|
||||
const val ACTION_CLOSE_DATABASE = "ACTION_CLOSE_DATABASE"
|
||||
|
||||
fun startIfAllowed(context: Context) {
|
||||
if (PreferencesUtil.isPersistentNotificationEnable(context)) {
|
||||
// Start the opening notification
|
||||
fun start(context: Context) {
|
||||
// Start the opening notification, keep it active to receive lock
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(Intent(context, DatabaseOpenNotificationService::class.java))
|
||||
} else {
|
||||
context.startService(Intent(context, DatabaseOpenNotificationService::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,31 +19,28 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.notifications
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import com.kunzisoft.keepass.utils.LockReceiver
|
||||
import com.kunzisoft.keepass.utils.registerLockReceiver
|
||||
import com.kunzisoft.keepass.utils.unregisterLockReceiver
|
||||
|
||||
abstract class LockNotificationService : NotificationService() {
|
||||
|
||||
private var lockBroadcastReceiver: BroadcastReceiver? = null
|
||||
private var mLockReceiver: LockReceiver? = null
|
||||
|
||||
protected open fun actionOnLock() {
|
||||
// Stop the service in all cases
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// Register a lock receiver to stop notification service when lock on keyboard is performed
|
||||
lockBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
// Stop the service in all cases
|
||||
stopSelf()
|
||||
}
|
||||
mLockReceiver = LockReceiver {
|
||||
actionOnLock()
|
||||
}
|
||||
registerReceiver(lockBroadcastReceiver,
|
||||
IntentFilter().apply {
|
||||
addAction(LOCK_ACTION)
|
||||
}
|
||||
)
|
||||
registerLockReceiver(mLockReceiver)
|
||||
}
|
||||
|
||||
protected fun stopTask(task: Thread?) {
|
||||
@@ -59,7 +56,7 @@ abstract class LockNotificationService : NotificationService() {
|
||||
|
||||
override fun onDestroy() {
|
||||
|
||||
unregisterReceiver(lockBroadcastReceiver)
|
||||
unregisterLockReceiver(mLockReceiver)
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ object OtpEntryFields {
|
||||
// [^&=\s]+=[^&=\s]+(&[^&=\s]+=[^&=\s]+)*
|
||||
private const val validKeyValue = "[^&=\\s]+"
|
||||
private const val validKeyValuePair = "$validKeyValue=$validKeyValue"
|
||||
private const val validKeyValueRegex = "$validKeyValuePair&($validKeyValuePair)*"
|
||||
private const val validKeyValueRegex = "$validKeyValuePair(&$validKeyValuePair)*"
|
||||
|
||||
/**
|
||||
* Parse fields of an entry to retrieve an OtpElement
|
||||
@@ -243,21 +243,18 @@ object OtpEntryFields {
|
||||
val plainText = getField(OTP_FIELD)
|
||||
if (plainText != null && plainText.isNotEmpty()) {
|
||||
if (Pattern.matches(validKeyValueRegex, plainText)) {
|
||||
try {
|
||||
return try {
|
||||
// KeeOtp string format
|
||||
val query = breakDownKeyValuePairs(plainText)
|
||||
|
||||
var secretString = query[SEED_KEY]
|
||||
if (secretString == null)
|
||||
secretString = ""
|
||||
otpElement.setBase32Secret(secretString)
|
||||
otpElement.digits = query[DIGITS_KEY]?.toIntOrNull() ?: OTP_DEFAULT_DIGITS
|
||||
otpElement.period = query[STEP_KEY]?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
|
||||
otpElement.setBase32Secret(query[SEED_KEY] ?: "")
|
||||
otpElement.digits = query[DIGITS_KEY]?.toIntOrNull() ?: OTP_DEFAULT_DIGITS
|
||||
otpElement.period = query[STEP_KEY]?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
|
||||
|
||||
otpElement.type = OtpType.TOTP
|
||||
return true
|
||||
otpElement.type = OtpType.TOTP
|
||||
true
|
||||
} catch (exception: Exception) {
|
||||
return false
|
||||
false
|
||||
}
|
||||
} else {
|
||||
// Malformed
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
|
||||
class AutofillSettingsActivity : StylishActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_toolbar)
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
toolbar.setTitle(R.string.autofill_preference_title)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, AutofillSettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> onBackPressed()
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
@@ -20,16 +20,14 @@
|
||||
package com.kunzisoft.keepass.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
|
||||
class SettingsAutofillActivity : SettingsActivity() {
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
mTimeoutEnable = false
|
||||
}
|
||||
class AutofillSettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun retrieveMainFragment(): Fragment {
|
||||
return NestedSettingsFragment.newInstance(NestedSettingsFragment.Screen.FORM_FILLING)
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
// Load the preferences from an XML resource
|
||||
setPreferencesFromResource(R.xml.preferences_autofill, rootKey)
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import android.view.MenuItem
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
|
||||
class MagikIMESettings : StylishActivity() {
|
||||
class MagikeyboardSettingsActivity : StylishActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -39,7 +39,7 @@ class MagikIMESettings : StylishActivity() {
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, MagikIMESettingsFragment())
|
||||
.replace(R.id.fragment_container, MagikeyboardSettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import androidx.preference.PreferenceFragmentCompat
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
class MagikIMESettingsFragment : PreferenceFragmentCompat() {
|
||||
class MagikeyboardSettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
// Load the preferences from an XML resource
|
||||
@@ -24,7 +24,6 @@ import android.os.Bundle
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
|
||||
class MainPreferenceFragment : PreferenceFragmentCompat() {
|
||||
|
||||
@@ -160,12 +160,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
}
|
||||
|
||||
findPreference<Preference>(getString(R.string.magic_keyboard_preference_key))?.setOnPreferenceClickListener {
|
||||
startActivity(Intent(context, MagikIMESettings::class.java))
|
||||
false
|
||||
}
|
||||
|
||||
findPreference<Preference>(getString(R.string.clipboard_explanation_key))?.setOnPreferenceClickListener {
|
||||
UriUtil.gotoUrl(context!!, R.string.clipboard_explanation_url)
|
||||
startActivity(Intent(context, MagikeyboardSettingsActivity::class.java))
|
||||
false
|
||||
}
|
||||
|
||||
@@ -174,6 +169,16 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
false
|
||||
}
|
||||
|
||||
findPreference<Preference>(getString(R.string.settings_autofill_key))?.setOnPreferenceClickListener {
|
||||
startActivity(Intent(context, AutofillSettingsActivity::class.java))
|
||||
false
|
||||
}
|
||||
|
||||
findPreference<Preference>(getString(R.string.clipboard_explanation_key))?.setOnPreferenceClickListener {
|
||||
UriUtil.gotoUrl(context!!, R.string.clipboard_explanation_url)
|
||||
false
|
||||
}
|
||||
|
||||
// Present in two places
|
||||
allowCopyPassword()
|
||||
}
|
||||
@@ -248,13 +253,19 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
BiometricUnlockDatabaseHelper.deleteEntryKeyInKeystoreForBiometric(
|
||||
activity,
|
||||
object : BiometricUnlockDatabaseHelper.BiometricUnlockErrorCallback {
|
||||
override fun onInvalidKeyException(e: Exception) {}
|
||||
|
||||
override fun onBiometricException(e: Exception) {
|
||||
fun showException(e: Exception) {
|
||||
Toast.makeText(context,
|
||||
getString(R.string.biometric_scanning_error, e.localizedMessage),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onInvalidKeyException(e: Exception) {
|
||||
showException(e)
|
||||
}
|
||||
|
||||
override fun onBiometricException(e: Exception) {
|
||||
showException(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
CipherDatabaseAction.getInstance(context.applicationContext).deleteAll()
|
||||
|
||||
@@ -31,7 +31,6 @@ import com.kunzisoft.androidclearchroma.ChromaUtil
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.lock.lock
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
@@ -551,14 +550,10 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
||||
|
||||
val settingActivity = activity as SettingsActivity?
|
||||
|
||||
when (item.itemId) {
|
||||
R.id.menu_lock -> {
|
||||
settingActivity?.lock()
|
||||
return true
|
||||
}
|
||||
return when (item.itemId) {
|
||||
R.id.menu_save_database -> {
|
||||
settingActivity?.mProgressDialogThread?.startDatabaseSave(!mDatabaseReadOnly)
|
||||
return true
|
||||
true
|
||||
}
|
||||
|
||||
else -> {
|
||||
@@ -566,7 +561,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
||||
settingActivity?.let {
|
||||
MenuUtil.onDefaultMenuOptionsItemSelected(it, item, mDatabaseReadOnly, true)
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,15 +31,13 @@ object PreferencesUtil {
|
||||
|
||||
var APPEARANCE_CHANGED = false
|
||||
|
||||
private const val KEY_DEFAULT_DATABASE_PATH = "KEY_DEFAULT_DATABASE_PATH"
|
||||
|
||||
fun saveDefaultDatabasePath(context: Context, defaultDatabaseUri: Uri?) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
prefs?.edit()?.apply {
|
||||
defaultDatabaseUri?.let {
|
||||
putString(KEY_DEFAULT_DATABASE_PATH, it.toString())
|
||||
putString(context.getString(R.string.default_database_path_key), it.toString())
|
||||
} ?: kotlin.run {
|
||||
remove(KEY_DEFAULT_DATABASE_PATH)
|
||||
remove(context.getString(R.string.default_database_path_key))
|
||||
}
|
||||
apply()
|
||||
}
|
||||
@@ -47,7 +45,7 @@ object PreferencesUtil {
|
||||
|
||||
fun getDefaultDatabasePath(context: Context): String? {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getString(KEY_DEFAULT_DATABASE_PATH, "")
|
||||
return prefs.getString(context.getString(R.string.default_database_path_key), "")
|
||||
}
|
||||
|
||||
fun saveNodeSort(context: Context,
|
||||
@@ -201,18 +199,18 @@ object PreferencesUtil {
|
||||
context.resources.getBoolean(R.bool.lock_database_back_root_default))
|
||||
}
|
||||
|
||||
fun showLockDatabaseButton(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.lock_database_show_button_key),
|
||||
context.resources.getBoolean(R.bool.lock_database_show_button_default))
|
||||
}
|
||||
|
||||
fun isAutoSaveDatabaseEnabled(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.enable_auto_save_database_key),
|
||||
context.resources.getBoolean(R.bool.enable_auto_save_database_default))
|
||||
}
|
||||
|
||||
fun isPersistentNotificationEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.persistent_notification_key),
|
||||
context.resources.getBoolean(R.bool.persistent_notification_default))
|
||||
}
|
||||
|
||||
fun isBiometricUnlockEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
|
||||
@@ -356,4 +354,10 @@ object PreferencesUtil {
|
||||
return prefs.getBoolean(context.getString(R.string.keyboard_key_sound_key),
|
||||
context.resources.getBoolean(R.bool.keyboard_key_sound_default))
|
||||
}
|
||||
|
||||
fun isAutofillAutoSearchEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.autofill_auto_search_key),
|
||||
context.resources.getBoolean(R.bool.autofill_auto_search_default))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -47,6 +49,7 @@ open class SettingsActivity
|
||||
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var toolbar: Toolbar? = null
|
||||
private var lockView: View? = null
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -84,6 +87,11 @@ open class SettingsActivity
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
lockView = findViewById(R.id.lock_button)
|
||||
lockView?.setOnClickListener {
|
||||
lockAndExit()
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.fragment_container, retrieveMainFragment())
|
||||
@@ -154,6 +162,23 @@ open class SettingsActivity
|
||||
keyFile: Uri?) {
|
||||
}
|
||||
|
||||
private fun hideOrShowLockButton(key: NestedSettingsFragment.Screen) {
|
||||
if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||
when (key) {
|
||||
NestedSettingsFragment.Screen.DATABASE,
|
||||
NestedSettingsFragment.Screen.DATABASE_MASTER_KEY,
|
||||
NestedSettingsFragment.Screen.DATABASE_SECURITY -> {
|
||||
lockView?.visibility = View.VISIBLE
|
||||
}
|
||||
else -> {
|
||||
lockView?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lockView?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
// this if statement is necessary to navigate through nested and main fragments
|
||||
if (supportFragmentManager.backStackEntryCount == 0) {
|
||||
@@ -162,6 +187,7 @@ open class SettingsActivity
|
||||
supportFragmentManager.popBackStack()
|
||||
}
|
||||
toolbar?.setTitle(R.string.settings)
|
||||
hideOrShowLockButton(NestedSettingsFragment.Screen.APPLICATION)
|
||||
}
|
||||
|
||||
private fun replaceFragment(key: NestedSettingsFragment.Screen) {
|
||||
@@ -173,6 +199,7 @@ open class SettingsActivity
|
||||
.commit()
|
||||
|
||||
toolbar?.title = NestedSettingsFragment.retrieveTitle(resources, key)
|
||||
hideOrShowLockButton(key)
|
||||
}
|
||||
|
||||
override fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen) {
|
||||
|
||||
@@ -46,53 +46,63 @@ object TimeoutHelper {
|
||||
private set
|
||||
|
||||
private fun getLockPendingIntent(context: Context): PendingIntent {
|
||||
return PendingIntent.getBroadcast(context,
|
||||
return PendingIntent.getBroadcast(context.applicationContext,
|
||||
REQUEST_ID,
|
||||
Intent(LOCK_ACTION),
|
||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Record the current time to check it later with checkTime
|
||||
* Start the lock timer by creating an alarm,
|
||||
* if the method is recalled with a previous lock timer pending, the previous one is deleted
|
||||
*/
|
||||
fun recordTime(context: Context) {
|
||||
// Record timeout time in case timeout service is killed
|
||||
PreferencesUtil.saveCurrentTime(context)
|
||||
|
||||
private fun startLockTimer(context: Context) {
|
||||
if (Database.getInstance().loaded) {
|
||||
val timeout = PreferencesUtil.getAppTimeout(context)
|
||||
|
||||
// No timeout don't start timeout service
|
||||
if (timeout != NEVER) {
|
||||
val triggerTime = System.currentTimeMillis() + timeout
|
||||
val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
Log.d(TAG, "TimeoutHelper start")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
am.setExact(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
|
||||
} else {
|
||||
am.set(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
|
||||
// No timeout don't start timeout service
|
||||
(context.applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
|
||||
val triggerTime = System.currentTimeMillis() + timeout
|
||||
Log.d(TAG, "TimeoutHelper start")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
alarmManager.setExact(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
|
||||
} else {
|
||||
alarmManager.set(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the lock timer currently pending, useful if lock was triggered by another way
|
||||
*/
|
||||
fun cancelLockTimer(context: Context) {
|
||||
(context.applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
|
||||
Log.d(TAG, "TimeoutHelper cancel")
|
||||
alarmManager.cancel(getLockPendingIntent(context))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record the current time, to check it later with checkTime and start a new lock timer
|
||||
*/
|
||||
fun recordTime(context: Context) {
|
||||
// Record timeout time in case timeout service is killed
|
||||
PreferencesUtil.saveCurrentTime(context)
|
||||
startLockTimer(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the time previously record with recordTime and do the [timeoutAction] if timeout
|
||||
* if temporarilyDisableTimeout() is called, the function as no effect until releaseTemporarilyDisableTimeoutAndCheckTime() is called
|
||||
* return 'false' if timeout, 'true' if in time
|
||||
* return 'false' and send broadcast lock action if timeout, 'true' if in time
|
||||
*/
|
||||
fun checkTime(context: Context, timeoutAction: (() -> Unit)? = null): Boolean {
|
||||
// No effect if temporarily disable
|
||||
if (temporarilyDisableTimeout)
|
||||
return true
|
||||
|
||||
// Cancel the lock PendingIntent
|
||||
if (Database.getInstance().loaded) {
|
||||
val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
Log.d(TAG, "TimeoutHelper cancel")
|
||||
am.cancel(getLockPendingIntent(context))
|
||||
}
|
||||
|
||||
// Check whether the timeout has expired
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
@@ -115,6 +125,7 @@ object TimeoutHelper {
|
||||
if (diff >= appTimeout) {
|
||||
// We have timed out
|
||||
timeoutAction?.invoke()
|
||||
context.sendBroadcast(Intent(LOCK_ACTION))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -142,27 +153,14 @@ object TimeoutHelper {
|
||||
/**
|
||||
* Temporarily disable timeout, checkTime() function always return true
|
||||
*/
|
||||
fun temporarilyDisableTimeout(context: Context) {
|
||||
fun temporarilyDisableTimeout() {
|
||||
temporarilyDisableTimeout = true
|
||||
|
||||
// Stop the opening notification
|
||||
DatabaseOpenNotificationService.stop(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the temporarily disable timeout and directly call checkTime()
|
||||
* Release the temporarily disable timeout
|
||||
*/
|
||||
fun releaseTemporarilyDisableTimeoutAndLockIfTimeout(context: Context): Boolean {
|
||||
fun releaseTemporarilyDisableTimeout() {
|
||||
temporarilyDisableTimeout = false
|
||||
val inTime = if (context is LockingActivity) {
|
||||
checkTimeAndLockIfTimeout(context)
|
||||
} else {
|
||||
checkTime(context)
|
||||
}
|
||||
if (inTime) {
|
||||
// Start the opening notification
|
||||
DatabaseOpenNotificationService.startIfAllowed(context)
|
||||
}
|
||||
return inTime
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,110 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.utils
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Context.ALARM_SERVICE
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
|
||||
const val DATABASE_START_TASK_ACTION = "com.kunzisoft.keepass.DATABASE_START_TASK_ACTION"
|
||||
const val DATABASE_STOP_TASK_ACTION = "com.kunzisoft.keepass.DATABASE_STOP_TASK_ACTION"
|
||||
|
||||
const val LOCK_ACTION = "com.kunzisoft.keepass.LOCK"
|
||||
|
||||
const val REMOVE_ENTRY_MAGIKEYBOARD_ACTION = "com.kunzisoft.keepass.REMOVE_ENTRY_MAGIKEYBOARD"
|
||||
|
||||
class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
|
||||
|
||||
var mLockPendingIntent: PendingIntent? = null
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
// If allowed, lock and exit
|
||||
if (!TimeoutHelper.temporarilyDisableTimeout) {
|
||||
intent.action?.let {
|
||||
when (it) {
|
||||
Intent.ACTION_SCREEN_ON -> {
|
||||
cancelLockPendingIntent(context)
|
||||
}
|
||||
Intent.ACTION_SCREEN_OFF -> {
|
||||
if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(context)) {
|
||||
mLockPendingIntent = PendingIntent.getBroadcast(context,
|
||||
4575,
|
||||
Intent(intent).apply {
|
||||
action = LOCK_ACTION
|
||||
},
|
||||
0)
|
||||
// Launch the effective action after a small time
|
||||
val first: Long = System.currentTimeMillis() + context.getString(R.string.timeout_screen_off).toLong()
|
||||
val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager?
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
alarmManager?.setExact(AlarmManager.RTC_WAKEUP, first, mLockPendingIntent)
|
||||
} else {
|
||||
alarmManager?.set(AlarmManager.RTC_WAKEUP, first, mLockPendingIntent)
|
||||
}
|
||||
} else {
|
||||
cancelLockPendingIntent(context)
|
||||
}
|
||||
}
|
||||
LOCK_ACTION,
|
||||
REMOVE_ENTRY_MAGIKEYBOARD_ACTION -> lockAction.invoke()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelLockPendingIntent(context: Context) {
|
||||
mLockPendingIntent?.let {
|
||||
val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager?
|
||||
alarmManager?.cancel(mLockPendingIntent)
|
||||
mLockPendingIntent = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.registerLockReceiver(lockReceiver: LockReceiver?,
|
||||
registerRemoveEntryMagikeyboard: Boolean = false) {
|
||||
lockReceiver?.let {
|
||||
registerReceiver(it, IntentFilter().apply {
|
||||
addAction(Intent.ACTION_SCREEN_OFF)
|
||||
addAction(Intent.ACTION_SCREEN_ON)
|
||||
addAction(LOCK_ACTION)
|
||||
if (registerRemoveEntryMagikeyboard)
|
||||
addAction(REMOVE_ENTRY_MAGIKEYBOARD_ACTION)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.unregisterLockReceiver(lockReceiver: LockReceiver?) {
|
||||
lockReceiver?.let {
|
||||
unregisterReceiver(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.closeDatabase() {
|
||||
// Stop the Magikeyboard service
|
||||
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
||||
MagikIME.removeEntry(this)
|
||||
|
||||
// Stop the notification service
|
||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||
|
||||
Log.i(Context::class.java.name, "Shutdown after inactivity or manual lock")
|
||||
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager?)?.apply {
|
||||
cancelAll()
|
||||
}
|
||||
// Clear data
|
||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
||||
}
|
||||
@@ -21,25 +21,77 @@ package com.kunzisoft.keepass.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.text.format.Formatter
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.io.Serializable
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
class FileDatabaseInfo : FileInfo {
|
||||
class FileDatabaseInfo : Serializable {
|
||||
|
||||
constructor(context: Context, fileUri: Uri): super(context, fileUri)
|
||||
private var context: Context
|
||||
private var documentFile: DocumentFile? = null
|
||||
var fileUri: Uri?
|
||||
private set
|
||||
|
||||
constructor(context: Context, filePath: String): super(context, filePath)
|
||||
constructor(context: Context, fileUri: Uri) {
|
||||
this.context = context
|
||||
this.fileUri = fileUri
|
||||
init()
|
||||
}
|
||||
|
||||
constructor(context: Context, filePath: String) {
|
||||
this.context = context
|
||||
this.fileUri = UriUtil.parse(filePath)
|
||||
init()
|
||||
}
|
||||
|
||||
fun init() {
|
||||
documentFile = UriUtil.getFileData(context, fileUri)
|
||||
}
|
||||
|
||||
var exists: Boolean = false
|
||||
get() {
|
||||
return documentFile?.exists() ?: field
|
||||
}
|
||||
private set
|
||||
|
||||
var canRead: Boolean = false
|
||||
get() {
|
||||
return documentFile?.canRead() ?: field
|
||||
}
|
||||
private set
|
||||
|
||||
var canWrite: Boolean = false
|
||||
get() {
|
||||
return documentFile?.canWrite() ?: field
|
||||
}
|
||||
private set
|
||||
|
||||
fun getModificationString(): String? {
|
||||
return documentFile?.lastModified()?.let {
|
||||
DateFormat.getDateTimeInstance()
|
||||
.format(Date(it))
|
||||
}
|
||||
}
|
||||
|
||||
fun getSizeString(): String? {
|
||||
return documentFile?.let {
|
||||
Formatter.formatFileSize(context, it.length())
|
||||
}
|
||||
}
|
||||
|
||||
fun retrieveDatabaseAlias(alias: String): String {
|
||||
return when {
|
||||
alias.isNotEmpty() -> alias
|
||||
PreferencesUtil.isFullFilePathEnable(context) -> filePath ?: ""
|
||||
else -> fileName ?: ""
|
||||
PreferencesUtil.isFullFilePathEnable(context) -> fileUri?.path ?: ""
|
||||
else -> if (exists) documentFile?.name ?: "" else fileUri?.path ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
fun retrieveDatabaseTitle(titleCallback: (String)->Unit) {
|
||||
|
||||
fileUri?.let { fileUri ->
|
||||
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
||||
.getFileDatabaseHistory(fileUri) { fileDatabaseHistoryEntity ->
|
||||
@@ -48,5 +100,4 @@ class FileDatabaseInfo : FileInfo {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.text.format.Formatter
|
||||
import java.io.Serializable
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
open class FileInfo : Serializable {
|
||||
|
||||
var context: Context
|
||||
var fileUri: Uri?
|
||||
var filePath: String? = null
|
||||
var fileName: String? = ""
|
||||
var lastModification = Date(0L)
|
||||
var size: Long = 0L
|
||||
|
||||
constructor(context: Context, fileUri: Uri) {
|
||||
this.context = context
|
||||
this.fileUri = fileUri
|
||||
init()
|
||||
}
|
||||
|
||||
constructor(context: Context, filePath: String) {
|
||||
this.context = context
|
||||
this.fileUri = UriUtil.parse(filePath)
|
||||
init()
|
||||
}
|
||||
|
||||
fun init() {
|
||||
this.filePath = fileUri?.path
|
||||
|
||||
UriUtil.getFileData(context, fileUri)?.let { file ->
|
||||
size = file.length()
|
||||
fileName = file.name
|
||||
lastModification = Date(file.lastModified())
|
||||
}
|
||||
|
||||
if (fileName == null || fileName!!.isEmpty()) {
|
||||
fileName = filePath
|
||||
}
|
||||
}
|
||||
|
||||
fun lastModificationAccessible(): Boolean {
|
||||
return lastModification.after(Date(0L))
|
||||
}
|
||||
|
||||
fun sizeAccessible(): Boolean {
|
||||
return size != 0L
|
||||
}
|
||||
|
||||
fun dataAccessible(): Boolean {
|
||||
return UriUtil.isUriAccessible(context.contentResolver, fileUri)
|
||||
}
|
||||
|
||||
fun getModificationString(): String {
|
||||
return DateFormat.getDateTimeInstance()
|
||||
.format(lastModification)
|
||||
}
|
||||
|
||||
fun getSizeString(): String {
|
||||
return Formatter.formatFileSize(context, size)
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,7 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.utils
|
||||
|
||||
import java.util.ArrayList
|
||||
import java.util.Locale
|
||||
import java.util.*
|
||||
|
||||
object StringUtil {
|
||||
|
||||
@@ -85,5 +84,17 @@ object StringUtil {
|
||||
|
||||
return currentText
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun UUID.toKeePassRefString(): String {
|
||||
val tempString = toString().replace("-", "").toUpperCase(Locale.ENGLISH)
|
||||
return StringBuffer(reverseString2(tempString.substring(12, 16)))
|
||||
.append(reverseString2(tempString.substring(8, 12)))
|
||||
.append(reverseString2(tempString.substring(0, 8)))
|
||||
.append(reverseString2(tempString.substring(20, 32)))
|
||||
.append(reverseString2(tempString.substring(16, 20))).toString()
|
||||
}
|
||||
|
||||
private fun reverseString2(string: String): String {
|
||||
return string.chunked(2).reversed().joinToString("")
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.kunzisoft.keepass.R
|
||||
@@ -37,26 +36,6 @@ import java.util.*
|
||||
|
||||
object UriUtil {
|
||||
|
||||
fun isUriAccessible(contentResolver: ContentResolver, fileUri: Uri?): Boolean {
|
||||
if (fileUri == null)
|
||||
return false
|
||||
return try {
|
||||
//https://developer.android.com/reference/android/content/res/AssetFileDescriptor
|
||||
contentResolver.openInputStream(fileUri)?.close()
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e(UriUtil.javaClass.name, "Unable to access uri $fileUri : ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun isUriWritable(contentResolver: ContentResolver, fileUri: Uri?): Boolean {
|
||||
if (fileUri == null)
|
||||
return false
|
||||
// TODO Uri writeable detection
|
||||
return true
|
||||
}
|
||||
|
||||
fun getFileData(context: Context, fileUri: Uri?): DocumentFile? {
|
||||
if (fileUri == null)
|
||||
return null
|
||||
|
||||
@@ -69,8 +69,8 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
|
||||
}
|
||||
|
||||
private fun inflate(context: Context) {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
inflater.inflate(R.layout.view_button_add_node, this)
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
inflater?.inflate(R.layout.view_button_add_node, this)
|
||||
|
||||
addEntryEnable = true
|
||||
addGroupEnable = true
|
||||
@@ -132,7 +132,7 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
|
||||
}
|
||||
|
||||
fun showButton() {
|
||||
if (addButtonView?.visibility != VISIBLE)
|
||||
if (isEnable && addButtonView?.visibility != VISIBLE)
|
||||
addButtonView?.show(onAddButtonVisibilityChangedListener)
|
||||
}
|
||||
|
||||
|
||||
@@ -44,8 +44,8 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
|
||||
|
||||
init {
|
||||
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
inflater.inflate(R.layout.view_advanced_unlock, this)
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
inflater?.inflate(R.layout.view_advanced_unlock, this)
|
||||
|
||||
unlockContainerView = findViewById(R.id.fingerprint_container)
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ package com.kunzisoft.keepass.view
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.text.util.Linkify
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -31,6 +32,7 @@ import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsAdapter
|
||||
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
|
||||
@@ -40,8 +42,8 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.model.EntryAttachment
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpType
|
||||
import com.kunzisoft.keepass.utils.toKeePassRefString
|
||||
import java.util.*
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
|
||||
|
||||
class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
@@ -70,8 +72,8 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
private val urlContainerView: View
|
||||
private val urlView: TextView
|
||||
|
||||
private val commentContainerView: View
|
||||
private val commentView: TextView
|
||||
private val notesContainerView: View
|
||||
private val notesView: TextView
|
||||
|
||||
private val extrasContainerView: View
|
||||
private val extrasView: ViewGroup
|
||||
@@ -91,6 +93,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
private val historyAdapter = EntryHistoryAdapter(context)
|
||||
|
||||
private val uuidView: TextView
|
||||
private val uuidReferenceView: TextView
|
||||
|
||||
val isUserNamePresent: Boolean
|
||||
get() = userNameContainerView.visibility == View.VISIBLE
|
||||
@@ -99,8 +102,8 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
get() = passwordContainerView.visibility == View.VISIBLE
|
||||
|
||||
init {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
inflater.inflate(R.layout.view_entry_contents, this)
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
inflater?.inflate(R.layout.view_entry_contents, this)
|
||||
|
||||
userNameContainerView = findViewById(R.id.entry_user_name_container)
|
||||
userNameView = findViewById(R.id.entry_user_name)
|
||||
@@ -118,8 +121,8 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
urlContainerView = findViewById(R.id.entry_url_container)
|
||||
urlView = findViewById(R.id.entry_url)
|
||||
|
||||
commentContainerView = findViewById(R.id.entry_notes_container)
|
||||
commentView = findViewById(R.id.entry_notes)
|
||||
notesContainerView = findViewById(R.id.entry_notes_container)
|
||||
notesView = findViewById(R.id.entry_notes)
|
||||
|
||||
extrasContainerView = findViewById(R.id.extra_strings_container)
|
||||
extrasView = findViewById(R.id.extra_strings)
|
||||
@@ -146,6 +149,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
}
|
||||
|
||||
uuidView = findViewById(R.id.entry_UUID)
|
||||
uuidReferenceView = findViewById(R.id.entry_UUID_reference)
|
||||
|
||||
val attrColorAccent = intArrayOf(R.attr.colorAccent)
|
||||
val taColorAccent = context.theme.obtainStyledAttributes(attrColorAccent)
|
||||
@@ -286,15 +290,17 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
|
||||
fun assignComment(comment: String?) {
|
||||
if (comment != null && comment.isNotEmpty()) {
|
||||
commentContainerView.visibility = View.VISIBLE
|
||||
commentView.apply {
|
||||
notesContainerView.visibility = View.VISIBLE
|
||||
notesView.apply {
|
||||
text = comment
|
||||
if (fontInVisibility)
|
||||
applyFontVisibility()
|
||||
}
|
||||
|
||||
try {
|
||||
Linkify.addLinks(notesView, Linkify.ALL)
|
||||
} catch (e: Exception) {}
|
||||
} else {
|
||||
commentContainerView.visibility = View.GONE
|
||||
notesContainerView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,6 +352,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
|
||||
fun assignUUID(uuid: UUID) {
|
||||
uuidView.text = uuid.toString()
|
||||
uuidReferenceView.text = uuid.toKeePassRefString()
|
||||
}
|
||||
|
||||
/* -------------
|
||||
|
||||
@@ -43,8 +43,8 @@ open class EntryCustomField @JvmOverloads constructor(context: Context,
|
||||
|
||||
init {
|
||||
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
inflater.inflate(R.layout.item_entry_new_field, this)
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
inflater?.inflate(R.layout.item_entry_new_field, this)
|
||||
|
||||
labelView = findViewById(R.id.title)
|
||||
valueView = findViewById(R.id.value)
|
||||
|
||||
@@ -21,21 +21,21 @@ package com.kunzisoft.keepass.view
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.*
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.icons.assignDefaultDatabaseIcon
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
import org.joda.time.Duration
|
||||
import org.joda.time.Instant
|
||||
|
||||
class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@@ -52,16 +52,26 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
||||
private val entryPasswordLayoutView: TextInputLayout
|
||||
private val entryPasswordView: EditText
|
||||
private val entryConfirmationPasswordView: EditText
|
||||
val generatePasswordView: View
|
||||
private val entryCommentView: EditText
|
||||
private val entryExpiresCheckBox: CompoundButton
|
||||
private val entryExpiresTextView: TextView
|
||||
private val entryNotesView: EditText
|
||||
private val entryExtraFieldsContainer: ViewGroup
|
||||
val addNewFieldButton: View
|
||||
|
||||
private var iconColor: Int = 0
|
||||
private var expiresInstant: DateInstant = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
|
||||
|
||||
var onDateClickListener: OnClickListener? = null
|
||||
set(value) {
|
||||
field = value
|
||||
if (entryExpiresCheckBox.isChecked)
|
||||
entryExpiresTextView.setOnClickListener(value)
|
||||
else
|
||||
entryExpiresTextView.setOnClickListener(null)
|
||||
}
|
||||
|
||||
init {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
inflater.inflate(R.layout.view_entry_edit_contents, this)
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
inflater?.inflate(R.layout.view_entry_edit_contents, this)
|
||||
|
||||
entryTitleLayoutView = findViewById(R.id.entry_edit_container_title)
|
||||
entryTitleView = findViewById(R.id.entry_edit_title)
|
||||
@@ -71,10 +81,14 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
||||
entryPasswordLayoutView = findViewById(R.id.entry_edit_container_password)
|
||||
entryPasswordView = findViewById(R.id.entry_edit_password)
|
||||
entryConfirmationPasswordView = findViewById(R.id.entry_edit_confirmation_password)
|
||||
generatePasswordView = findViewById(R.id.entry_edit_generate_button)
|
||||
entryCommentView = findViewById(R.id.entry_edit_notes)
|
||||
entryExpiresCheckBox = findViewById(R.id.entry_edit_expires_checkbox)
|
||||
entryExpiresTextView = findViewById(R.id.entry_edit_expires_text)
|
||||
entryNotesView = findViewById(R.id.entry_edit_notes)
|
||||
entryExtraFieldsContainer = findViewById(R.id.entry_edit_advanced_container)
|
||||
addNewFieldButton = findViewById(R.id.entry_edit_add_new_field)
|
||||
|
||||
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
|
||||
assignExpiresDateText()
|
||||
}
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
@@ -141,43 +155,61 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
||||
}
|
||||
}
|
||||
|
||||
fun setOnPasswordGeneratorClickListener(clickListener: () -> Unit) {
|
||||
generatePasswordView.setOnClickListener { clickListener.invoke() }
|
||||
private fun assignExpiresDateText() {
|
||||
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
|
||||
entryExpiresTextView.setOnClickListener(onDateClickListener)
|
||||
expiresInstant.getDateTimeString(resources)
|
||||
} else {
|
||||
entryExpiresTextView.setOnClickListener(null)
|
||||
resources.getString(R.string.never)
|
||||
}
|
||||
if (fontInVisibility)
|
||||
entryExpiresTextView.applyFontVisibility()
|
||||
}
|
||||
|
||||
var expires: Boolean
|
||||
get() {
|
||||
return entryExpiresCheckBox.isChecked
|
||||
}
|
||||
set(value) {
|
||||
entryExpiresCheckBox.isChecked = value
|
||||
assignExpiresDateText()
|
||||
}
|
||||
|
||||
var expiresDate: DateInstant
|
||||
get() {
|
||||
return expiresInstant
|
||||
}
|
||||
set(value) {
|
||||
expiresInstant = value
|
||||
assignExpiresDateText()
|
||||
}
|
||||
|
||||
var notes: String
|
||||
get() {
|
||||
return entryCommentView.text.toString()
|
||||
return entryNotesView.text.toString()
|
||||
}
|
||||
set(value) {
|
||||
entryCommentView.setText(value)
|
||||
entryNotesView.setText(value)
|
||||
if (fontInVisibility)
|
||||
entryCommentView.applyFontVisibility()
|
||||
entryNotesView.applyFontVisibility()
|
||||
}
|
||||
|
||||
fun allowCustomField(allow: Boolean, action: () -> Unit) {
|
||||
addNewFieldButton.apply {
|
||||
if (allow) {
|
||||
visibility = View.VISIBLE
|
||||
setOnClickListener { action.invoke() }
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
setOnClickListener(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val customFields: MutableList<Field>
|
||||
get() {
|
||||
val customFieldsArray = ArrayList<Field>()
|
||||
// Add extra fields from views
|
||||
entryExtraFieldsContainer.let {
|
||||
for (i in 0 until it.childCount) {
|
||||
val view = it.getChildAt(i) as EntryEditCustomField
|
||||
val key = view.label
|
||||
val value = view.value
|
||||
val protect = view.isProtected
|
||||
customFieldsArray.add(Field(key, ProtectedString(protect, value)))
|
||||
try {
|
||||
for (i in 0 until it.childCount) {
|
||||
val view = it.getChildAt(i) as EntryEditCustomField
|
||||
val key = view.label
|
||||
val value = view.value
|
||||
val protect = view.isProtected
|
||||
customFieldsArray.add(Field(key, ProtectedString(protect, value)))
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
// Extra field container contains another type of view
|
||||
}
|
||||
}
|
||||
return customFieldsArray
|
||||
@@ -187,11 +219,14 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
||||
* Add a new view to fill in the information of the customized field and focus it
|
||||
*/
|
||||
fun addEmptyCustomField() {
|
||||
val entryEditCustomField = EntryEditCustomField(context).apply {
|
||||
setFontVisibility(fontInVisibility)
|
||||
requestFocus()
|
||||
// Fix current custom field before adding a new one
|
||||
if (isValid()) {
|
||||
val entryEditCustomField = EntryEditCustomField(context).apply {
|
||||
setFontVisibility(fontInVisibility)
|
||||
requestFocus()
|
||||
}
|
||||
entryExtraFieldsContainer.addView(entryEditCustomField)
|
||||
}
|
||||
entryExtraFieldsContainer.addView(entryEditCustomField)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,34 +261,34 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
||||
* @return ErrorValidation An error with a message or a validation without message
|
||||
*/
|
||||
fun isValid(): Boolean {
|
||||
var isValid = true
|
||||
|
||||
// Require title
|
||||
if (entryTitleView.text.toString().isEmpty()) {
|
||||
entryTitleLayoutView.error = context.getString(R.string.error_title_required)
|
||||
isValid = false
|
||||
} else {
|
||||
entryTitleLayoutView.error = null
|
||||
}
|
||||
|
||||
// Validate password
|
||||
if (entryPasswordView.text.toString() != entryConfirmationPasswordView.text.toString()) {
|
||||
entryPasswordLayoutView.error = context.getString(R.string.error_pass_match)
|
||||
isValid = false
|
||||
return false
|
||||
} else {
|
||||
entryPasswordLayoutView.error = null
|
||||
}
|
||||
|
||||
// Validate extra fields
|
||||
entryExtraFieldsContainer.let {
|
||||
for (i in 0 until it.childCount) {
|
||||
val entryEditCustomField = it.getChildAt(i) as EntryEditCustomField
|
||||
if (!entryEditCustomField.isValid()) {
|
||||
isValid = false
|
||||
try {
|
||||
val customFieldLabelSet = HashSet<String>()
|
||||
for (i in 0 until it.childCount) {
|
||||
val entryEditCustomField = it.getChildAt(i) as EntryEditCustomField
|
||||
if (customFieldLabelSet.contains(entryEditCustomField.label)) {
|
||||
entryEditCustomField.setError(R.string.error_label_exists)
|
||||
return false
|
||||
}
|
||||
customFieldLabelSet.add(entryEditCustomField.label)
|
||||
if (!entryEditCustomField.isValid()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return isValid
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import android.widget.CompoundButton
|
||||
import android.widget.EditText
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
|
||||
@@ -54,8 +55,8 @@ class EntryEditCustomField @JvmOverloads constructor(context: Context,
|
||||
|
||||
init {
|
||||
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
inflater.inflate(R.layout.view_entry_new_field, this)
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
inflater?.inflate(R.layout.view_entry_new_field, this)
|
||||
|
||||
val deleteView = findViewById<View>(R.id.entry_new_field_delete)
|
||||
deleteView.setOnClickListener { deleteViewFromParent() }
|
||||
@@ -84,14 +85,20 @@ class EntryEditCustomField @JvmOverloads constructor(context: Context,
|
||||
fun isValid(): Boolean {
|
||||
// Validate extra field
|
||||
if (label.isEmpty()) {
|
||||
labelLayoutView.error = context.getString(R.string.error_string_key)
|
||||
setError(R.string.error_string_key)
|
||||
return false
|
||||
} else {
|
||||
labelLayoutView.error = null
|
||||
setError(null)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun setError(@StringRes errorId: Int?) {
|
||||
labelLayoutView.error = if (errorId == null) null else {
|
||||
context.getString(errorId)
|
||||
}
|
||||
}
|
||||
|
||||
fun setFontVisibility(applyFontVisibility: Boolean) {
|
||||
if (applyFontVisibility)
|
||||
valueView.applyFontVisibility()
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.kunzisoft.keepass.view
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
class KeyFileSelectionView @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0)
|
||||
: ConstraintLayout(context, attrs, defStyle) {
|
||||
|
||||
private var mUri: Uri? = null
|
||||
|
||||
private val keyFileNameInputLayout: TextInputLayout
|
||||
private val keyFileNameView: TextView
|
||||
private val keyFileOpenView: ImageView
|
||||
|
||||
init {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
inflater?.inflate(R.layout.view_keyfile_selection, this)
|
||||
|
||||
keyFileNameInputLayout = findViewById(R.id.input_entry_keyfile)
|
||||
keyFileNameView = findViewById(R.id.keyfile_name)
|
||||
keyFileOpenView = findViewById(R.id.keyfile_open_button)
|
||||
}
|
||||
|
||||
override fun setOnClickListener(l: OnClickListener?) {
|
||||
super.setOnClickListener(l)
|
||||
keyFileNameView.setOnClickListener(l)
|
||||
}
|
||||
|
||||
override fun setOnLongClickListener(l: OnLongClickListener?) {
|
||||
super.setOnLongClickListener(l)
|
||||
keyFileNameView.setOnLongClickListener(l)
|
||||
}
|
||||
|
||||
var error: CharSequence?
|
||||
get() = keyFileNameInputLayout.error
|
||||
set(value) {
|
||||
keyFileNameInputLayout.error = value
|
||||
}
|
||||
|
||||
var uri: Uri?
|
||||
get() {
|
||||
return mUri
|
||||
}
|
||||
set(value) {
|
||||
mUri = value
|
||||
keyFileNameView.text = value?.let {
|
||||
DocumentFile.fromSingleUri(context, value)?.name ?: ""
|
||||
} ?: ""
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.kunzisoft.keepass.view
|
||||
|
||||
import android.content.Context
|
||||
|
||||
@@ -33,6 +33,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Replace font by monospace, must be called after seText()
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?attr/colorPrimaryDark" android:state_pressed="true" />
|
||||
<item android:color="@color/grey_dark" android:state_enabled="false" />
|
||||
<item android:color="?android:windowBackground" android:state_enabled="true" />
|
||||
</selector>
|
||||
@@ -5,8 +5,12 @@
|
||||
tools:targetApi="lollipop">
|
||||
<item>
|
||||
<shape>
|
||||
<stroke android:color="?attr/colorAccent" android:width="1dp"/>
|
||||
<solid android:color="@color/transparent"/>
|
||||
<corners
|
||||
android:topLeftRadius="0dp"
|
||||
android:topRightRadius="40dp"
|
||||
android:bottomLeftRadius="0dp"
|
||||
android:bottomRightRadius="0dp"/>
|
||||
<solid android:color="?android:attr/windowBackground"/>
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:color="@color/white"
|
||||
tools:targetApi="lollipop">
|
||||
<item>
|
||||
<shape>
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="?android:attr/textColorPrimary" />
|
||||
<corners
|
||||
android:topLeftRadius="0dp"
|
||||
android:topRightRadius="40dp"
|
||||
android:bottomLeftRadius="0dp"
|
||||
android:bottomRightRadius="0dp"/>
|
||||
<solid
|
||||
android:color="@color/transparent"/>
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape>
|
||||
<stroke android:color="@color/grey" android:width="1dp"/>
|
||||
<solid android:color="@color/transparent"/>
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:state_pressed="true">
|
||||
<shape>
|
||||
<corners
|
||||
android:topLeftRadius="0dp"
|
||||
android:topRightRadius="40dp"
|
||||
android:bottomLeftRadius="0dp"
|
||||
android:bottomRightRadius="0dp"/>
|
||||
<padding
|
||||
android:left="4dp"
|
||||
android:right="12dp"
|
||||
android:top="18dp"
|
||||
android:bottom="8dp"/>
|
||||
<solid android:color="@color/grey_dark"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape>
|
||||
<corners
|
||||
android:topLeftRadius="0dp"
|
||||
android:topRightRadius="40dp"
|
||||
android:bottomLeftRadius="0dp"
|
||||
android:bottomRightRadius="0dp"/>
|
||||
<padding
|
||||
android:left="4dp"
|
||||
android:right="12dp"
|
||||
android:top="18dp"
|
||||
android:bottom="8dp"/>
|
||||
<solid android:color="@color/grey"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/white">
|
||||
<item>
|
||||
<shape>
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/green" />
|
||||
<corners
|
||||
android:topLeftRadius="0dp"
|
||||
android:topRightRadius="40dp"
|
||||
android:bottomLeftRadius="0dp"
|
||||
android:bottomRightRadius="0dp"/>
|
||||
<solid
|
||||
android:color="@color/transparent"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
5
app/src/main/res/drawable/ic_apps_white_24dp.xml
Normal file
5
app/src/main/res/drawable/ic_apps_white_24dp.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M4,8h4L8,4L4,4v4zM10,20h4v-4h-4v4zM4,20h4v-4L4,16v4zM4,14h4v-4L4,10v4zM10,14h4v-4h-4v4zM16,4v4h4L20,4h-4zM10,8h4L14,4h-4v4zM16,14h4v-4h-4v4zM16,20h4v-4h-4v4z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_attach_file_white_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_attach_file_white_24dp.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M16.5,6v11.5c0,2.21 -1.79,4 -4,4s-4,-1.79 -4,-4V5c0,-1.38 1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5v10.5c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1V6H10v9.5c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5V5c0,-2.21 -1.79,-4 -4,-4S7,2.79 7,5v12.5c0,3.04 2.46,5.5 5.5,5.5s5.5,-2.46 5.5,-5.5V6h-1.5z"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M11,17c0,0.55 0.45,1 1,1s1,-0.45 1,-1 -0.45,-1 -1,-1 -1,0.45 -1,1zM11,3v4h2L13,5.08c3.39,0.49 6,3.39 6,6.92 0,3.87 -3.13,7 -7,7s-7,-3.13 -7,-7c0,-1.68 0.59,-3.22 1.58,-4.42L12,13l1.41,-1.41 -6.8,-6.8v0.02C4.42,6.45 3,9.05 3,12c0,4.97 4.02,9 9,9 4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9h-1zM18,12c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1 0.45,1 1,1 1,-0.45 1,-1zM6,12c0,0.55 0.45,1 1,1s1,-0.45 1,-1 -0.45,-1 -1,-1 -1,0.45 -1,1z"/>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_check_white_24dp.xml
Normal file
5
app/src/main/res/drawable/ic_check_white_24dp.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||
</vector>
|
||||
118
app/src/main/res/drawable/ic_generate_password_white_24dp.xml
Normal file
118
app/src/main/res/drawable/ic_generate_password_white_24dp.xml
Normal file
@@ -0,0 +1,118 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M9.77344,3.13281 C9.4515,3.1329,9.12601,3.22745,8.83789,3.42188 L1.73828,8.21289
|
||||
C1.25807,8.53693,1.00007,9.06311,1,9.59961
|
||||
C0.999958,9.92151,1.09268,10.247,1.28711,10.5352 L6.07813,17.6328
|
||||
C6.5966,18.4011,7.63207,18.6025,8.40039,18.084 L8.59375,17.9531
|
||||
C7.93444,17.5713,7.49368,16.9397,7.33398,16.2305
|
||||
C7.25701,16.1464,7.21481,16.0308,7.23828,15.9102 L7.27148,15.7422
|
||||
C7.271,15.7299,7.26758,15.7194,7.26758,15.707 L7.09961,15.8184
|
||||
C6.92859,15.9337,6.69736,15.8897,6.58203,15.7188
|
||||
C6.46665,15.5479,6.51062,15.3165,6.68164,15.2012 L6.86523,15.0801
|
||||
L6.65039,15.0371 C6.44804,14.9977,6.31618,14.8059,6.35547,14.6035
|
||||
C6.39476,14.4011,6.58863,14.2693,6.79102,14.3086 L7.00586,14.3516
|
||||
L6.88477,14.1699 C6.76939,13.9991,6.81335,13.7677,6.98438,13.6523
|
||||
C7.1554,13.537,7.38468,13.581,7.5,13.752 L7.62305,13.9316 L7.66406,13.7188
|
||||
C7.69855,13.5416,7.8519,13.4274,8.02539,13.4258 L11.1816,6.77344
|
||||
C11.4309,6.24809,11.8397,5.84958,12.3203,5.58984 L11.1602,3.87109
|
||||
C10.8361,3.3909,10.3119,3.13267,9.77539,3.13281 L9.77344,3.13281 Z
|
||||
M9.70703,5.15625 C9.90938,5.19561,10.0373,5.39136,9.99805,5.59375
|
||||
L9.95898,5.80859 L10.1387,5.68555
|
||||
C10.3097,5.57024,10.5409,5.61618,10.6563,5.78711
|
||||
C10.7716,5.95797,10.7257,6.18741,10.5547,6.30273 L10.375,6.42578
|
||||
L10.5879,6.46484 C10.7902,6.5042,10.922,6.70197,10.8828,6.9043
|
||||
C10.8435,7.1067,10.6496,7.23855,10.4473,7.19922 L10.2344,7.15625
|
||||
L10.3535,7.33594 C10.4689,7.5068,10.4249,7.73817,10.2539,7.85352
|
||||
C10.0829,7.9688,9.85163,7.92289,9.73633,7.75195 L9.61523,7.57031
|
||||
L9.57227,7.78516 C9.53298,7.98758,9.34104,8.11943,9.13867,8.08008
|
||||
C8.93633,8.04072,8.80451,7.84684,8.84375,7.64453 L8.88477,7.43164
|
||||
L8.70313,7.55273 C8.53228,7.66806,8.30282,7.62406,8.1875,7.45313
|
||||
C8.07212,7.28226,8.11609,7.05088,8.28711,6.93555 L8.46875,6.8125
|
||||
L8.25391,6.76953 C8.05154,6.73017,7.91971,6.53833,7.95898,6.33594
|
||||
C7.99827,6.13352,8.19412,6.00167,8.39648,6.04102 L8.61133,6.08398
|
||||
L8.48828,5.90234 C8.3729,5.73148,8.41882,5.50205,8.58984,5.38672
|
||||
C8.76069,5.27139,8.99014,5.31344,9.10547,5.48438 L9.22656,5.66602
|
||||
L9.26758,5.45313 C9.30687,5.25073,9.50467,5.11692,9.70703,5.15625 Z
|
||||
M13.6602,6.24414 C13.0088,6.21529,12.3742,6.57309,12.0762,7.20117
|
||||
L8.40625,14.9375 C8.00883,15.7749,8.36373,16.7686,9.20117,17.166
|
||||
L16.9375,20.8379 C17.7749,21.2353,18.7686,20.8804,19.166,20.043 L22.8379,12.3066
|
||||
C23.2353,11.4693,22.8804,10.4736,22.043,10.0762 L14.3066,6.40625
|
||||
C14.0973,6.30689,13.8773,6.25374,13.6602,6.24414 Z M4.62109,8.49023
|
||||
C4.66856,8.4801,4.71886,8.48028,4.76953,8.49023
|
||||
C4.97188,8.52959,5.10374,8.72142,5.06445,8.92383 L5.02148,9.13867
|
||||
L5.20313,9.01758 C5.37397,8.90227,5.60343,8.94625,5.71875,9.11719
|
||||
C5.83413,9.28805,5.78999,9.51944,5.61914,9.63477 L5.43945,9.75586
|
||||
L5.65039,9.79492 C5.85276,9.83428,5.9865,10.0321,5.94727,10.2344
|
||||
C5.90798,10.4368,5.71213,10.5686,5.50977,10.5293 L5.29688,10.4883
|
||||
L5.41797,10.668 C5.53335,10.8388,5.48938,11.0683,5.31836,11.1836
|
||||
C5.14751,11.2989,4.91806,11.253,4.80273,11.082 L4.67969,10.9023 L4.63672,11.1172
|
||||
C4.59743,11.3196,4.40355,11.4495,4.20117,11.4102
|
||||
C3.99883,11.3708,3.867,11.1769,3.90625,10.9746 L3.94922,10.7617 L3.76953,10.8828
|
||||
C3.59851,10.9981,3.36728,10.9541,3.25195,10.7832
|
||||
C3.13658,10.6123,3.18054,10.3829,3.35156,10.2676 L3.53516,10.1426
|
||||
L3.31836,10.1016 C3.11599,10.0622,2.98612,9.86842,3.02539,9.66602
|
||||
C3.06468,9.46362,3.25857,9.33371,3.46094,9.37305 L3.67578,9.41406
|
||||
L3.55078,9.23242 C3.4354,9.06156,3.48132,8.83214,3.65234,8.7168
|
||||
C3.82337,8.60149,4.05462,8.64547,4.16992,8.81641 L4.29102,8.99805
|
||||
L4.33203,8.7832 C4.36154,8.6314,4.47892,8.52021,4.62109,8.49023 Z
|
||||
M19.2363,10.7852 C19.3814,10.7935,19.5126,10.8876,19.5645,11.0332
|
||||
L19.6367,11.2363 L19.7305,11.041
|
||||
C19.8188,10.8547,20.0403,10.7749,20.2266,10.8633
|
||||
C20.4128,10.9517,20.4908,11.1712,20.4023,11.3574 L20.3086,11.5586
|
||||
L20.5156,11.4844 C20.7098,11.4152,20.9211,11.5148,20.9902,11.709
|
||||
C21.0594,11.9032,20.9597,12.1163,20.7656,12.1855 L20.5586,12.2578
|
||||
L20.7578,12.3516 C20.944,12.44,21.022,12.6614,20.9336,12.8477
|
||||
C20.8452,13.0339,20.6238,13.1118,20.4375,13.0234 L20.2402,12.9297
|
||||
L20.3125,13.1367 C20.3817,13.3309,20.282,13.5421,20.0879,13.6113
|
||||
C19.8937,13.6805,19.6825,13.5809,19.6133,13.3867 L19.5391,13.1777
|
||||
L19.4453,13.377 C19.357,13.5632,19.1374,13.6411,18.9512,13.5527 L18.9492,13.5527
|
||||
C18.763,13.4643,18.6832,13.2429,18.7715,13.0566 L18.8652,12.8613
|
||||
L18.6621,12.9336 C18.4679,13.0027,18.2547,12.9012,18.1855,12.707
|
||||
C18.1164,12.5128,18.218,12.3016,18.4121,12.2324 L18.6172,12.1582
|
||||
L18.4199,12.0664 C18.2337,11.978,18.1557,11.7566,18.2441,11.5703
|
||||
C18.3325,11.384,18.554,11.3042,18.7402,11.3926 L18.9355,11.4863 L18.8613,11.2832
|
||||
C18.7922,11.089,18.8938,10.8759,19.0879,10.8066
|
||||
C19.1364,10.7894,19.188,10.7824,19.2363,10.7852 Z M15.2695,12.1973
|
||||
C15.4146,12.2056,15.5478,12.2997,15.5996,12.4453 L15.6699,12.6484
|
||||
L15.7637,12.4531 C15.852,12.2669,16.0735,12.189,16.2598,12.2773
|
||||
C16.446,12.3657,16.524,12.5872,16.4355,12.7734 L16.3438,12.9707 L16.5488,12.8965
|
||||
C16.743,12.8273,16.9543,12.9289,17.0234,13.123
|
||||
C17.0926,13.3172,16.991,13.5304,16.7969,13.5996 L16.5938,13.6699
|
||||
L16.7891,13.7637 C16.9753,13.8521,17.0552,14.0735,16.9668,14.2598
|
||||
C16.8784,14.446,16.6569,14.5239,16.4707,14.4355 L16.2734,14.3438
|
||||
L16.3477,14.5488 C16.4168,14.743,16.3152,14.9542,16.1211,15.0234
|
||||
C15.9269,15.0926,15.7176,14.993,15.6484,14.7988 L15.5742,14.5918
|
||||
L15.4785,14.7891 C15.3902,14.9753,15.1687,15.0532,14.9824,14.9648
|
||||
C14.7962,14.8765,14.7182,14.657,14.8066,14.4707 L14.8984,14.2734
|
||||
L14.6953,14.3477 C14.5011,14.4168,14.2879,14.3152,14.2188,14.1211
|
||||
C14.1496,13.9269,14.2493,13.7157,14.4434,13.6465 L14.6504,13.5723
|
||||
L14.4531,13.4785 C14.2669,13.3901,14.1889,13.1687,14.2773,12.9824
|
||||
C14.3657,12.7962,14.5872,12.7183,14.7734,12.8066 L14.9688,12.8984
|
||||
L14.8945,12.6953 C14.8254,12.5011,14.9289,12.288,15.123,12.2188
|
||||
C15.1716,12.2015,15.2212,12.1945,15.2695,12.1973 Z M11.3008,13.6113
|
||||
C11.4458,13.6197,11.579,13.7138,11.6309,13.8594 L11.7051,14.0625
|
||||
L11.7969,13.8672 C11.8852,13.6809,12.1087,13.6011,12.2949,13.6895
|
||||
C12.4811,13.7778,12.5591,13.9993,12.4707,14.1855 L12.375,14.3848 L12.582,14.3105
|
||||
C12.7762,14.2414,12.9894,14.341,13.0586,14.5352
|
||||
C13.1277,14.7294,13.0242,14.9425,12.8301,15.0117 L12.627,15.084 L12.8242,15.1777
|
||||
C13.0104,15.2661,13.0885,15.4876,13,15.6738
|
||||
C12.9116,15.8601,12.6902,15.938,12.5039,15.8496 L12.3066,15.7559
|
||||
L12.3789,15.9629 C12.4481,16.1571,12.3484,16.3683,12.1543,16.4375
|
||||
C11.9601,16.5067,11.7489,16.4071,11.6797,16.2129 L11.6055,16.0039
|
||||
L11.5137,16.2031 C11.4253,16.3894,11.2019,16.4693,11.0156,16.3809
|
||||
L11.0156,16.3789 C10.8294,16.2905,10.7514,16.071,10.8398,15.8848
|
||||
L10.9336,15.6875 L10.7305,15.7598
|
||||
C10.5363,15.8289,10.3211,15.7273,10.252,15.5332
|
||||
C10.1828,15.339,10.2844,15.1278,10.4785,15.0586 L10.6855,14.9863
|
||||
L10.4863,14.8926 C10.3001,14.8042,10.2221,14.5828,10.3105,14.3965
|
||||
C10.3989,14.2102,10.6184,14.1304,10.8047,14.2188 L11.0039,14.3125
|
||||
L10.9297,14.1094 C10.8605,13.9152,10.9622,13.702,11.1563,13.6328
|
||||
C11.2048,13.6156,11.2524,13.6086,11.3008,13.6113 Z" />
|
||||
</vector>
|
||||
19
app/src/main/res/drawable/ic_new_field_white_24dp.xml
Normal file
19
app/src/main/res/drawable/ic_new_field_white_24dp.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<group>
|
||||
<path
|
||||
android:fillColor="#fffbfb"
|
||||
android:pathData="M 18 3 L 18 6 L 15 6 L 15 8 L 18 8 L 18 11 L 20 11 L 20 8 L 23 8 L 23 6 L 20 6 L
|
||||
20 3 L 18 3 z M 4 6 C 2.8920005 6 2.0000002 6.8920005 2 8 L 2 18 C 1.9999998
|
||||
19.108 2.8920005 20 4 20 L 18 20 C 19.108 20 19.999999 19.108 20 18 L 20 13 L 18
|
||||
13 L 18 16.533203 C 17.999999 17.345736 17.345736 18 16.533203 18 L 5.4667969 18
|
||||
C 4.6542635 18 3.9999998 17.345736 4 16.533203 L 4 9.4667969 C 4.0000002
|
||||
8.6542635 4.6542635 8 5.4667969 8 L 13 8 L 13 6 L 4 6 z M 5 10 L 5 12 L 17 12 L
|
||||
17 10 L 5 10 z M 5 14 L 5 16 L 17 16 L 17 14 L 5 14 z" />
|
||||
</group>
|
||||
</vector>
|
||||
74
app/src/main/res/drawable/ic_otp_white_24dp.xml
Normal file
74
app/src/main/res/drawable/ic_otp_white_24dp.xml
Normal file
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M9.19922,3.13867 L9.19922,4.73828 L9.19922,6.33984 L10.8008,6.33984
|
||||
L10.8008,4.79297 C13.6944,5.15457,15.9841,7.44622,16.3457,10.3398
|
||||
L14.8008,10.3398 L14.8008,11.9395 L16.3438,11.9395
|
||||
C16.2972,12.3042,16.211,12.6543,16.1074,12.998 L17.7559,12.998
|
||||
C17.9,12.399,18,11.7819,18,11.1387 C18,6.72041,14.4183,3.13867,10,3.13867
|
||||
L9.19922,3.13867 Z M4.7207,5.17383 L4.59766,5.25
|
||||
C4.30768,5.51569,4.03687,5.80202,3.78906,6.10742
|
||||
C3.74408,6.16236,3.76404,6.13658,3.75391,6.14844
|
||||
C2.62072,7.56491,2.00133,9.32361,2,11.1387
|
||||
C2,11.4166,2.02145,11.6889,2.05078,11.959
|
||||
C2.06909,12.128,2.09815,12.2932,2.12695,12.459
|
||||
C2.15024,12.5993,2.17255,12.7392,2.20313,12.877
|
||||
C2.21201,12.9175,2.21732,12.9597,2.22656,13 L2.23633,13
|
||||
C2.42308,13.7827,2.72966,14.5165,3.12695,15.1914
|
||||
C3.30082,14.5824,3.66062,14.0532,4.14453,13.666
|
||||
C4.05066,13.4483,3.95825,13.2294,3.88867,13 L3.89063,13
|
||||
C3.88529,12.9838,3.88033,12.9674,3.875,12.9512
|
||||
C3.77847,12.6238,3.7054,12.2864,3.66211,11.9395 L5.19727,11.9395
|
||||
L5.20703,11.9395 L5.20703,10.3398 L5.19727,10.3398 L3.66406,10.3398
|
||||
C3.69055,10.1356,3.73432,9.93472,3.7793,9.73438
|
||||
C3.81343,9.59208,3.85116,9.45203,3.89258,9.3125
|
||||
C4.09265,8.63652,4.39089,7.99073,4.80859,7.41211 L9.10156,11.7051
|
||||
C9.41495,12.0185,9.91902,12.0185,10.2324,11.7051
|
||||
C10.5458,11.3917,10.5458,10.8876,10.2324,10.5742 L5.14258,5.48242
|
||||
C5.02985,5.36975,4.72451,5.175,4.72266,5.17383 L4.7207,5.17383 Z M6,14
|
||||
C4.892,14,4,14.892,4,16 L4,19 C4,20.108,4.892,21,6,21 L21,21
|
||||
C22.108,21,23,20.108,23,19 L23,16 C23,14.892,22.108,14,21,14 L6,14 Z M7.5,15
|
||||
C7.777,15,8,15.2787,8,15.625 L8,16.293 L8.47266,15.8203
|
||||
C8.71749,15.5755,9.07172,15.5366,9.26758,15.7324
|
||||
C9.46352,15.9283,9.42449,16.2825,9.17969,16.5273 L8.70703,17 L9.375,17
|
||||
C9.72124,17,10,17.223,10,17.5 C10,17.777,9.72124,18,9.375,18 L8.70703,18
|
||||
L9.17969,18.4727 C9.42449,18.7175,9.46367,19.0717,9.26758,19.2676
|
||||
C9.07172,19.4634,8.71749,19.4245,8.47266,19.1797 L8,18.707 L8,19.375
|
||||
C8,19.7212,7.777,20,7.5,20 C7.223,20,7,19.7212,7,19.375 L7,18.707
|
||||
L6.52734,19.1797 C6.28251,19.4245,5.92828,19.4634,5.73242,19.2676
|
||||
C5.53649,19.0717,5.57551,18.7175,5.82031,18.4727 L6.29297,18 L5.625,18
|
||||
C5.27876,18,5,17.777,5,17.5 C5,17.223,5.27876,17,5.625,17 L6.29297,17
|
||||
L5.82031,16.5273 C5.57551,16.2825,5.53633,15.9283,5.73242,15.7324
|
||||
C5.92828,15.5366,6.28251,15.5755,6.52734,15.8203 L7,16.293 L7,15.625
|
||||
C7,15.2787,7.223,15,7.5,15 Z M13.5,15 C13.777,15,14,15.2787,14,15.625 L14,16.293
|
||||
L14.4727,15.8203 C14.7175,15.5755,15.0717,15.5366,15.2676,15.7324
|
||||
C15.4635,15.9283,15.4245,16.2825,15.1797,16.5273 L14.707,17 L15.375,17
|
||||
C15.7212,17,16,17.223,16,17.5 C16,17.777,15.7212,18,15.375,18 L14.707,18
|
||||
L15.1797,18.4727 C15.4245,18.7175,15.4637,19.0717,15.2676,19.2676
|
||||
C15.0717,19.4634,14.7175,19.4245,14.4727,19.1797 L14,18.707 L14,19.375
|
||||
C14,19.7212,13.777,20,13.5,20 C13.223,20,13,19.7212,13,19.375 L13,18.707
|
||||
L12.5273,19.1797 C12.2825,19.4245,11.9283,19.4634,11.7324,19.2676
|
||||
C11.5365,19.0717,11.5755,18.7175,11.8203,18.4727 L12.293,18 L11.625,18
|
||||
C11.2788,18,11,17.777,11,17.5 C11,17.223,11.2788,17,11.625,17 L12.293,17
|
||||
L11.8203,16.5273 C11.5755,16.2825,11.5363,15.9283,11.7324,15.7324
|
||||
C11.9283,15.5366,12.2825,15.5755,12.5273,15.8203 L13,16.293 L13,15.625
|
||||
C13,15.2787,13.223,15,13.5,15 Z M19.5,15 C19.777,15,20,15.2787,20,15.625
|
||||
L20,16.293 L20.4727,15.8203 C20.7175,15.5755,21.0717,15.5366,21.2676,15.7324
|
||||
C21.4635,15.9283,21.4245,16.2825,21.1797,16.5273 L20.707,17 L21.375,17
|
||||
C21.7212,17,22,17.223,22,17.5 C22,17.777,21.7212,18,21.375,18 L20.707,18
|
||||
L21.1797,18.4727 C21.4245,18.7175,21.4637,19.0717,21.2676,19.2676
|
||||
C21.0717,19.4634,20.7175,19.4245,20.4727,19.1797 L20,18.707 L20,19.375
|
||||
C20,19.7212,19.777,20,19.5,20 C19.223,20,19,19.7212,19,19.375 L19,18.707
|
||||
L18.5273,19.1797 C18.2825,19.4245,17.9283,19.4634,17.7324,19.2676
|
||||
C17.5365,19.0717,17.5755,18.7175,17.8203,18.4727 L18.293,18 L17.625,18
|
||||
C17.2788,18,17,17.777,17,17.5 C17,17.223,17.2788,17,17.625,17 L18.293,17
|
||||
L17.8203,16.5273 C17.5755,16.2825,17.5363,15.9283,17.7324,15.7324
|
||||
C17.9283,15.5366,18.2825,15.5755,18.5273,15.8203 L19,16.293 L19,15.625
|
||||
C19,15.2787,19.223,15,19.5,15 Z" />
|
||||
</vector>
|
||||
@@ -1,19 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:width="24dp"
|
||||
android:height="24dp">
|
||||
<group
|
||||
android:translateY="-8">
|
||||
<group
|
||||
android:scaleX="1.777778"
|
||||
android:scaleY="1.777778"
|
||||
android:translateX="-205.4844"
|
||||
android:translateY="-31.99788">
|
||||
<path
|
||||
android:pathData="M125.00684 31.305444l0 -6.644528c0 -0.263013 -0.21159 -0.47461 -0.4746 -0.47461l-6.48633 0c-1.04808 0 -1.89843 0.850344 -1.89843 1.898435l0 6.328127c0 1.048093 0.85035 1.898437 1.89843 1.898437l6.48633 0c0.26301 0 0.4746 -0.2116 0.4746 -0.474611l0 -0.316409c0 -0.148311 -0.0692 -0.282785 -0.176 -0.369792 -0.083 -0.304543 -0.083 -1.172685 0 -1.477229 0.10684 -0.08504 0.176 -0.219501 0.176 -0.36782zm-6.32812 -4.469236c0 -0.06529 0.0534 -0.118652 0.11866 -0.118652l4.19238 0c0.0653 0 0.11866 0.05343 0.11866 0.118652l0 0.39551c0 0.06529 -0.0534 0.118653 -0.11866 0.118653l-4.19238 0c-0.0653 0 -0.11866 -0.05343 -0.11866 -0.118653l0 -0.39551zm0 1.265621c0 -0.06529 0.0534 -0.118653 0.11866 -0.118653l4.19238 0c0.0653 0 0.11866 0.05343 0.11866 0.118653l0 0.395509c0 0.06529 -0.0534 0.118653 -0.11866 0.118653l-4.19238 0c-0.0653 0 -0.11866 -0.05342 -0.11866 -0.118653l0 -0.395509zm5.0111 4.943847l-5.64391 0c-0.35003 0 -0.63282 -0.282781 -0.63282 -0.632808 0 -0.348045 0.28476 -0.632813 0.63282 -0.632813l5.64391 0c-0.0375 0.338162 -0.0375 0.927466 0 1.265621z"
|
||||
android:fillColor="#ffffff" />
|
||||
</group>
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M 12 1 A 11.000003 11.000001 0 0 0 1 12 A 11.000003 11.000001 0 0 0 4 19.529297
|
||||
L 4 18 L 4 16.119141 A 9.0000001 9.0000001 0 0 1 3 12 A 9.0000001 9.0000001 0 0
|
||||
1 4.9746094 6.3886719 L 17.617188 19.03125 A 9.0000001 9.0000001 0 0 1 12 21 A
|
||||
9.0000001 9.0000001 0 0 1 7.9121094 20 L 6 20 L 4.4609375 20 A 11.000003
|
||||
11.000001 0 0 0 12 23 A 11.000003 11.000001 0 0 0 23 12 A 11.000003 11.000001 0
|
||||
0 0 12 1 z M 12 3 A 9.0000001 9.0000001 0 0 1 21 12 A 9.0000001 9.0000001 0 0 1
|
||||
19.025391 17.611328 L 6.3828125 4.96875 A 9.0000001 9.0000001 0 0 1 12 3 z M
|
||||
16.566406 5.0078125 C 16.256202 5.0423192 15.932855 5.1941212 15.669922
|
||||
5.4570312 L 14.46875 6.6601562 L 17.339844 9.53125 L 18.542969 8.3300781 C
|
||||
18.895877 7.9770126 19.030492 7.5200648 18.966797 7.1269531 C 18.396436
|
||||
6.3122027 17.687797 5.6035638 16.873047 5.0332031 C 16.772829 5.0168634
|
||||
16.672741 4.995984 16.566406 5.0078125 z M 13.449219 7.6777344 L 11.978516
|
||||
9.1503906 L 14.849609 12.021484 L 16.320312 10.548828 L 13.449219 7.6777344 z M
|
||||
9.1484375 11.980469 L 5.65625 15.472656 L 5.015625 18.390625 C 4.9299361
|
||||
18.781059 5.2170219 19.069875 5.6074219 18.984375 L 8.5253906 18.345703 L
|
||||
12.019531 14.851562 L 9.1484375 11.980469 z" />
|
||||
</group>
|
||||
</vector>
|
||||
@@ -1,25 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:width="24dp"
|
||||
android:height="24dp">
|
||||
<group
|
||||
android:translateY="-8">
|
||||
<group
|
||||
android:scaleX="1.777778"
|
||||
android:scaleY="1.777778"
|
||||
android:translateX="-205.4844"
|
||||
android:translateY="-31.99788">
|
||||
<group
|
||||
android:scaleX="0.5625"
|
||||
android:scaleY="0.5625"
|
||||
android:translateX="115.585"
|
||||
android:translateY="22.49881">
|
||||
<path
|
||||
android:pathData="M4.375 3C2.5117466 3 1 4.5117271 1 6.375l0 11.25C1 19.488276 2.5117466 21 4.375 21l11.53125 0C16.373823 21 16.75 20.623825 16.75 20.15625l0 -0.5625c0 -0.263664 -0.122669 -0.503524 -0.3125 -0.658203 -0.147556 -0.54141 -0.147556 -2.08359 0 -2.625 0.189938 -0.151182 0.3125 -0.390619 0.3125 -0.654297l0 -0.669922 -2.378906 2.378906c-0.0089 0.500068 -0.0036 1.014773 0.03711 1.384766l-1.525391 0 -3.0507808 0.667969C9.333649 19.527124 8.7483479 19.391267 8.3632812 19.005859 8.2870146 18.929486 8.2287106 18.840044 8.171875 18.75L4.375 18.75C3.7527244 18.75 3.25 18.24727 3.25 17.625 3.25 17.006253 3.7562267 16.5 4.375 16.5l3.8027344 0L8.6503906 14.349609 12.125 10.875l-6.4140625 0C5.5948486 10.875 5.5 10.780031 5.5 10.664062l0 -0.7031245C5.5 9.8448664 5.5949375 9.75 5.7109375 9.75l7.4531245 0c0.02365 0 0.03921 0.018137 0.06055 0.025391L16.550781 6.4492188 16.75 6.25l0 -2.40625C16.75 3.3761713 16.373823 3 15.90625 3L4.375 3Zm16.015625 1.5898438c-0.30546 0.033979 -0.623906 0.1844704 -0.882813 0.4433593l-1.183593 1.1835938 2.828125 2.828125 1.183594 -1.1835938c0.517848 -0.5180889 0.601704 -1.2752558 0.1875 -1.6894531L21.195312 4.8457031C20.988219 4.6386018 20.696085 4.5558644 20.390625 4.5898438ZM17.320312 7.21875L9.6464844 14.894531 9.015625 17.767578c-0.08448 0.384462 0.1995577 0.670133 0.5839844 0.585938L12.472656 17.724609 20.148438 10.046875 17.320312 7.21875ZM5.7109375 7.5L13.164062 7.5C13.280151 7.5 13.375 7.594987 13.375 7.7109375l0 0.703125C13.375 8.5301336 13.28008 8.625 13.164062 8.625l-7.4531245 0C5.5948486 8.625 5.5 8.5300127 5.5 8.4140625l0 -0.703125C5.5 7.5948664 5.5949375 7.5 5.7109375 7.5Z"
|
||||
android:fillColor="#ffffff" />
|
||||
</group>
|
||||
</group>
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeWidth="1.01553178"
|
||||
android:pathData="M 16.566406,5.0078125 C 15.782726,5.1133592 15.352352,5.8416456
|
||||
14.802734,6.3261719 14.631179,6.503043 14.326338,6.6788074 14.664062,6.8554688 L
|
||||
17.339844,9.53125 C 17.821017,9.0226367 18.368011,8.5699819 18.791016,8.0117188
|
||||
19.125433,7.5187285 19.054409,6.8047721 18.564453,6.4453125 18.109555,6.0127792
|
||||
17.694742,5.5314578 17.210938,5.1328125 17.01828,5.0182169 16.787242,4.9827103
|
||||
16.566406,5.0078125 Z M 13.449219,7.6777344 C 10.851548,10.276042
|
||||
8.2539034,12.874349 5.65625,15.472656 5.4423833,16.493124 5.1885689,17.506656
|
||||
5,18.53125 c 0.019556,0.598212 0.681759,0.466668 1.0722656,0.351562
|
||||
0.8179378,-0.179027 1.6351872,-0.358083 2.453125,-0.537109 2.5983114,-2.598958
|
||||
5.1966104,-5.197918 7.7949214,-7.796875 z" />
|
||||
</group>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_web_white_24dp.xml
Normal file
5
app/src/main/res/drawable/ic_web_white_24dp.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.95,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z"/>
|
||||
</vector>
|
||||
@@ -20,6 +20,7 @@
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/toolbar_coordinator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -31,7 +32,6 @@
|
||||
android:layout_height="@dimen/toolbar_parallax_height">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/toolbar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -126,4 +126,10 @@
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<include
|
||||
layout="@layout/view_button_lock"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="start|bottom" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -17,62 +17,105 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:importantForAutofill="noExcludeDescendants"
|
||||
tools:targetApi="o"
|
||||
android:id="@+id/entry_edit_coordinator_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:importantForAutofill="noExcludeDescendants"
|
||||
tools:targetApi="o" >
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<include
|
||||
android:id="@+id/toolbar"
|
||||
layout="@layout/toolbar_default"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:id="@+id/toolbar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:titleEnabled="false"
|
||||
app:toolbarId="@+id/toolbar"
|
||||
app:layout_scrollFlags="enterAlways|enterAlwaysCollapsed|scroll|exitUntilCollapsed|snap">
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/entry_edit_scroll"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:fillViewport="true"
|
||||
android:scrollbars="none">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.kunzisoft.keepass.view.EntryEditContentsView
|
||||
android:id="@+id/entry_edit_contents"
|
||||
android:layout_width="0dp"
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="?attr/actionBarSize">
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintWidth_percent="@dimen/content_percent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:background="?attr/colorPrimaryDark"
|
||||
android:theme="?attr/toolbarAppearance"
|
||||
android:popupTheme="?attr/toolbarPopupAppearance">
|
||||
<androidx.appcompat.widget.ActionMenuView
|
||||
android:id="@+id/entry_edit_bottom_bar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="?attr/actionBarSize"/>
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
<View
|
||||
android:id="@+id/biometric_delimiter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/colorAccent"/>
|
||||
</FrameLayout>
|
||||
|
||||
</ScrollView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:theme="?attr/toolbarAppearance"
|
||||
android:popupTheme="?attr/toolbarPopupAppearance"
|
||||
app:layout_collapseMode="pin"
|
||||
tools:targetApi="lollipop" />
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/entry_edit_scroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbarStyle="insideOverlay"
|
||||
android:scrollbars="none"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.kunzisoft.keepass.view.EntryEditContentsView
|
||||
android:id="@+id/entry_edit_contents"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent="@dimen/content_percent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<include
|
||||
layout="@layout/view_button_lock"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="start|bottom" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/entry_edit_save"
|
||||
android:layout_width="wrap_content"
|
||||
android:id="@+id/entry_edit_validate"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:src="@drawable/ic_save_white_24dp"
|
||||
android:contentDescription="@string/content_description_entry_save"
|
||||
android:layout_margin="16dp"
|
||||
app:layout_anchorGravity="bottom|end"
|
||||
app:layout_anchor="@+id/entry_edit_scroll"
|
||||
android:src="@drawable/ic_check_white_24dp"
|
||||
android:contentDescription="@string/validate"
|
||||
app:useCompatPadding="true"
|
||||
style="@style/KeepassDXStyle.Fab"/>
|
||||
|
||||
|
||||
@@ -24,45 +24,40 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:importantForAutofill="noExcludeDescendants"
|
||||
android:background="?attr/colorPrimaryDark"
|
||||
tools:targetApi="o">
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/content_description_background"
|
||||
android:background="@drawable/background_repeat"/>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/activity_file_selection_coordinator_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/background_repeat"
|
||||
android:backgroundTint="?attr/colorPrimary"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/file_selection_buttons_container">
|
||||
<androidx.core.widget.NestedScrollView
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="fill_vertical"
|
||||
android:fillViewport="true">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_height="@dimen/toolbar_parallax_height">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:id="@+id/toolbar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/file_selection_title_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
app:layout_collapseMode="parallax"
|
||||
android:layout_gravity="center_horizontal|bottom"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingTop="48dp"
|
||||
android:paddingBottom="24dp"
|
||||
android:layout_marginBottom="36dp">
|
||||
android:paddingBottom="48dp">
|
||||
<TextView
|
||||
android:id="@+id/file_selection_title_part_1"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -110,63 +105,49 @@
|
||||
android:gravity="center"
|
||||
android:text="@string/app_name_part3"/>
|
||||
</LinearLayout>
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_collapseMode="pin"
|
||||
android:background="?attr/colorPrimary" />
|
||||
|
||||
<TextView android:id="@+id/label_warning"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/default_margin"
|
||||
android:layout_marginStart="@dimen/default_margin"
|
||||
android:layout_marginRight="@dimen/default_margin"
|
||||
android:layout_marginEnd="@dimen/default_margin"
|
||||
android:layout_marginBottom="@dimen/default_margin"
|
||||
android:gravity="center"
|
||||
android:textColor="?attr/textColorInverse"
|
||||
app:layout_constraintTop_toBottomOf="@+id/file_selection_title_container"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/container_file_list"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_margin="24dp"
|
||||
android:elevation="6dp"
|
||||
android:background="?attr/colorPrimaryDark"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintWidth_percent="@dimen/content_percent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/label_warning"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:id="@+id/file_list_title"
|
||||
android:layout_width="match_parent"
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:theme="?attr/toolbarAppearance"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:elevation="4dp"
|
||||
app:layout_collapseMode="pin"
|
||||
android:popupTheme="?attr/toolbarPopupAppearance" />
|
||||
<FrameLayout
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:padding="12dp"
|
||||
android:elevation="4dp"
|
||||
android:layout_gravity="top|start"
|
||||
app:layout_collapseMode="pin">
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/file_manager_explanation_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="12dp"
|
||||
style="@style/KeepassDXStyle.TextAppearance.Title"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:textColor="?android:attr/textColorHintInverse"
|
||||
android:text="@string/open_recent"/>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/file_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
</LinearLayout>
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_folder_open_white_24dp"
|
||||
app:tint="?android:attr/textColorHintInverse"
|
||||
android:contentDescription="@string/about"/>
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/file_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbarStyle="insideOverlay"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:elevation="4dp"
|
||||
android:background="@color/transparent"
|
||||
android:theme="?attr/toolbarAppearance"
|
||||
android:popupTheme="?attr/toolbarPopupAppearance" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/file_selection_buttons_container"
|
||||
android:layout_width="0dp"
|
||||
@@ -182,7 +163,7 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/open_database_button"
|
||||
android:id="@+id/open_keyfile_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/select_database_file"
|
||||
@@ -210,6 +191,11 @@
|
||||
android:paddingEnd="24dp"
|
||||
android:text="@string/create_keepass_file"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_gravity="top"
|
||||
android:background="?attr/colorPrimaryDark"/>
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -140,12 +140,19 @@
|
||||
android:background="?android:attr/windowBackground" />
|
||||
</LinearLayout>
|
||||
|
||||
<include
|
||||
layout="@layout/view_button_lock"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
app:layout_anchor="@+id/node_list_container"
|
||||
app:layout_anchorGravity="start|bottom" />
|
||||
|
||||
<com.kunzisoft.keepass.view.AddNodeButtonView
|
||||
android:id="@+id/add_node_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_anchor="@+id/node_list_container"
|
||||
app:layout_anchorGravity="right|bottom" />
|
||||
app:layout_anchorGravity="end|bottom" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<com.kunzisoft.keepass.view.ToolbarAction
|
||||
|
||||
@@ -162,58 +162,33 @@
|
||||
</RelativeLayout>
|
||||
|
||||
<!-- File Input -->
|
||||
<RelativeLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/container_key_file"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatCheckBox
|
||||
android:id="@+id/keyfile_checkox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:paddingBottom="20dp"
|
||||
android:contentDescription="@string/content_description_keyfile_checkbox"
|
||||
android:focusable="false"
|
||||
android:layout_alignBottom="@+id/input_entry_keyfile"
|
||||
android:gravity="center_vertical"/>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_entry_keyfile"
|
||||
android:layout_width="wrap_content"
|
||||
<com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||
android:id="@+id/keyfile_selection"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/keyfile_checkox"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:importantForAccessibility="no"
|
||||
android:importantForAutofill="no"
|
||||
android:layout_toEndOf="@+id/keyfile_checkox"
|
||||
android:layout_toRightOf="@+id/keyfile_checkox"
|
||||
android:layout_toLeftOf="@+id/open_database_button"
|
||||
android:layout_toStartOf="@+id/open_database_button">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/pass_keyfile"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:hint="@string/entry_keyfile"
|
||||
android:inputType="textUri"
|
||||
android:importantForAccessibility="no"
|
||||
android:importantForAutofill="no"
|
||||
android:imeOptions="actionDone"
|
||||
android:maxLines="1"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/open_database_button"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:padding="12dp"
|
||||
android:layout_alignBottom="@+id/input_entry_keyfile"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:contentDescription="@string/content_description_open_file"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/ic_folder_white_24dp"
|
||||
android:tint="?attr/colorAccent"/>
|
||||
</RelativeLayout>
|
||||
android:importantForAutofill="no" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -239,7 +214,7 @@
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingRight="24dp"
|
||||
style="@style/KeepassDXStyle.TextAppearance.TinyText"
|
||||
android:text="@string/warning_database_read_only"
|
||||
android:text="@string/warning_database_link_revoked"
|
||||
android:textColor="?attr/textColorInverse"
|
||||
android:background="?attr/colorAccent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/activity_password_info_delimiter"
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/toolbar_coordinator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -38,4 +39,12 @@
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<include
|
||||
layout="@layout/view_button_lock"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:visibility="gone"
|
||||
android:layout_gravity="start|bottom" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -55,12 +55,18 @@
|
||||
tools:ignore="TextFields" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/generate_password_button"
|
||||
android:layout_margin="@dimen/button_margin"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:drawableEnd="@drawable/ic_generate_password_white_24dp"
|
||||
android:drawableRight="@drawable/ic_generate_password_white_24dp"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:text="@string/generate_password" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/nodes_list"
|
||||
android:contentDescription="@string/content_description_node_children"
|
||||
android:scrollbars="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/windowBackground" />
|
||||
|
||||
@@ -109,44 +109,14 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/entry_keyfile"/>
|
||||
|
||||
<RelativeLayout
|
||||
<com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||
android:id="@+id/keyfile_selection"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/open_database_button"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:contentDescription="@string/content_description_open_file"
|
||||
android:layout_alignTop="@+id/keyfile_input_layout"
|
||||
android:layout_alignBottom="@+id/keyfile_input_layout"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/ic_folder_white_24dp"
|
||||
android:tint="?attr/colorAccent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/keyfile_input_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toLeftOf="@+id/open_database_button"
|
||||
android:layout_toStartOf="@+id/open_database_button"
|
||||
android:importantForAccessibility="no"
|
||||
android:importantForAutofill="no"
|
||||
app:passwordToggleEnabled="true"
|
||||
app:passwordToggleTint="?attr/colorAccent">
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/pass_keyfile"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:importantForAccessibility="no"
|
||||
android:importantForAutofill="no"
|
||||
android:hint="@string/hint_keyfile"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</RelativeLayout>
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/keyfile_checkox"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</LinearLayout>
|
||||
|
||||
43
app/src/main/res/layout/item_autofill_app_id.xml
Normal file
43
app/src/main/res/layout/item_autofill_app_id.xml
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/green"
|
||||
android:minHeight="24dp"
|
||||
android:orientation="horizontal">
|
||||
<ImageView
|
||||
android:id="@+id/autofill_app_id_icon"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginLeft="12dp"
|
||||
android:contentDescription="@string/about"
|
||||
android:src="@drawable/ic_apps_white_24dp"/>
|
||||
<TextView
|
||||
android:id="@+id/autofill_app_id_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:textColor="@color/white"
|
||||
android:layout_gravity="center"
|
||||
android:paddingRight="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingStart="12dp" />
|
||||
</LinearLayout>
|
||||
@@ -22,7 +22,7 @@
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:id="@+id/autofill_entry_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
@@ -30,17 +30,17 @@
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginLeft="12dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:src="@drawable/ic_key_white_24dp"
|
||||
android:tint="@color/green"/>
|
||||
android:contentDescription="@string/content_description_entry_icon"
|
||||
android:src="@drawable/ic_key_white_24dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:id="@+id/autofill_entry_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:paddingRight="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingLeft="0dp"
|
||||
android:paddingStart="0dp"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSmall" />
|
||||
</LinearLayout>
|
||||
@@ -16,32 +16,27 @@
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/windowBackground"
|
||||
android:minHeight="48dp"
|
||||
android:minHeight="?android:attr/listPreferredItemHeightSmall"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginLeft="12dp"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:src="@mipmap/ic_launcher"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/autofill_sign_in_prompt"
|
||||
android:minHeight="?android:attr/listPreferredItemHeightSmall"
|
||||
android:layout_gravity="center"
|
||||
android:paddingRight="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingStart="12dp"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSmall" />
|
||||
|
||||
</LinearLayout>
|
||||
25
app/src/main/res/layout/item_autofill_unlock_app_id.xml
Normal file
25
app/src/main/res/layout/item_autofill_unlock_app_id.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
<include
|
||||
layout="@layout/item_autofill_app_id"/>
|
||||
<include
|
||||
layout="@layout/item_autofill_unlock"/>
|
||||
</LinearLayout>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user