Compare commits
196 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f907aa578a | ||
|
|
41e2620cc1 | ||
|
|
e7a82b167a | ||
|
|
088c556b00 | ||
|
|
c80343b6d4 | ||
|
|
4e52a8cf60 | ||
|
|
1ed1d4233f | ||
|
|
6e4626bc02 | ||
|
|
2608ae247f | ||
|
|
785586bfe9 | ||
|
|
bdcbb177ae | ||
|
|
15ac365d79 | ||
|
|
debbcb753b | ||
|
|
69d73aeaa4 | ||
|
|
dffe53370f | ||
|
|
4334e6dcdf | ||
|
|
c2c6c093d5 | ||
|
|
77e539eec2 | ||
|
|
a57970210e | ||
|
|
1b31a46fb7 | ||
|
|
87f19c74fc | ||
|
|
bd157a9724 | ||
|
|
5a327eb0db | ||
|
|
4b9c0b0109 | ||
|
|
df6b75cdbb | ||
|
|
0b4f8c122b | ||
|
|
2a87eaf3e5 | ||
|
|
c52266f5cf | ||
|
|
3b21f8add2 | ||
|
|
6574bd10a0 | ||
|
|
23f3335988 | ||
|
|
a5d7f33c82 | ||
|
|
3782c4dac0 | ||
|
|
1fc02fd2fe | ||
|
|
cc347c1dbe | ||
|
|
79ff20eb18 | ||
|
|
e6e8a447da | ||
|
|
233f0c5bdb | ||
|
|
9ed4271a14 | ||
|
|
470c0b6b43 | ||
|
|
afa8ae42b9 | ||
|
|
63d426503f | ||
|
|
ffb7f80b26 | ||
|
|
63f8826fd8 | ||
|
|
ef836e8b84 | ||
|
|
abc1c43a51 | ||
|
|
6b54dd9e0d | ||
|
|
1f54e7752d | ||
|
|
6ac941f276 | ||
|
|
a4fe92562f | ||
|
|
b9bd1d9d4b | ||
|
|
3b6c28488a | ||
|
|
875eb3500d | ||
|
|
3a88a2451c | ||
|
|
6800b73a4f | ||
|
|
983404e6d8 | ||
|
|
b95c0a18a7 | ||
|
|
36b317cad8 | ||
|
|
35d74888fb | ||
|
|
6c308483f7 | ||
|
|
9d25fb74ec | ||
|
|
d217b52744 | ||
|
|
319da4b174 | ||
|
|
9bee467942 | ||
|
|
44ac70fc97 | ||
|
|
d897611d62 | ||
|
|
c2ae251e73 | ||
|
|
35ad285864 | ||
|
|
97bdae21eb | ||
|
|
d6dc6e43c7 | ||
|
|
01e6e530d5 | ||
|
|
9ec0178beb | ||
|
|
d66f2f6d24 | ||
|
|
79cd4004cc | ||
|
|
991243e2df | ||
|
|
b91cf11d86 | ||
|
|
d182ec09fa | ||
|
|
8641822358 | ||
|
|
9665cbb428 | ||
|
|
a280dfaf3b | ||
|
|
3e56521ea8 | ||
|
|
b205230ea9 | ||
|
|
51645ab126 | ||
|
|
5d04897e75 | ||
|
|
1ac0ea5cc6 | ||
|
|
a07e8b51e5 | ||
|
|
a81f0238f4 | ||
|
|
2b81eb8ec7 | ||
|
|
e5eb642781 | ||
|
|
a4cbe25733 | ||
|
|
2042c85b22 | ||
|
|
3149f8745c | ||
|
|
15b9f1616f | ||
|
|
94c02b7288 | ||
|
|
7e70b59a59 | ||
|
|
2c7f5e41ed | ||
|
|
108b8df280 | ||
|
|
553098f9be | ||
|
|
131eb78407 | ||
|
|
f956a279a5 | ||
|
|
7150686b92 | ||
|
|
4b1fb2c173 | ||
|
|
94464bf608 | ||
|
|
2faa88784a | ||
|
|
e6607b53d8 | ||
|
|
3f6a6c864a | ||
|
|
30a578257d | ||
|
|
8411134adf | ||
|
|
f86a5d1a19 | ||
|
|
be72492537 | ||
|
|
76f9e8ec6e | ||
|
|
8fb1c44e58 | ||
|
|
f607b35cf3 | ||
|
|
0e56bec35a | ||
|
|
c890d10114 | ||
|
|
dee2fe5ce7 | ||
|
|
4a4d767bce | ||
|
|
d57e0cf601 | ||
|
|
7fa141dd1b | ||
|
|
c261a0cbca | ||
|
|
66661cbd49 | ||
|
|
100c126c3d | ||
|
|
d466e3077d | ||
|
|
24587dc34e | ||
|
|
32cc57dd03 | ||
|
|
a55488846b | ||
|
|
dcf61fd4e2 | ||
|
|
5bf998468a | ||
|
|
01c9625c59 | ||
|
|
772c378922 | ||
|
|
ee50a91379 | ||
|
|
9cfda3bad8 | ||
|
|
aa19b08bd9 | ||
|
|
87f69bb7e2 | ||
|
|
41c0aeedbe | ||
|
|
3cbe53d76f | ||
|
|
aed60d6c1e | ||
|
|
be7d35490d | ||
|
|
d0ea997c63 | ||
|
|
1fecffeba2 | ||
|
|
76319a56a2 | ||
|
|
1d71de7031 | ||
|
|
e9a1cfea11 | ||
|
|
9115856d19 | ||
|
|
8c45266c18 | ||
|
|
6b4130df89 | ||
|
|
e3e10e7dfa | ||
|
|
cbf900004d | ||
|
|
51b36dc460 | ||
|
|
9f8016afe2 | ||
|
|
d5a36db50a | ||
|
|
ecd458d8d0 | ||
|
|
9088297c41 | ||
|
|
5282deb088 | ||
|
|
75d661f12b | ||
|
|
83bc769d9e | ||
|
|
8324acadc8 | ||
|
|
6e42db41be | ||
|
|
3917bfc9e6 | ||
|
|
d11febb1ce | ||
|
|
360eb6f9cc | ||
|
|
e4ac5d01d0 | ||
|
|
6a51fc0668 | ||
|
|
ba03f07fbe | ||
|
|
7bf7d63f64 | ||
|
|
d3efaabc24 | ||
|
|
b4283ed98b | ||
|
|
de407e4cf9 | ||
|
|
60ed3a9836 | ||
|
|
7948358d85 | ||
|
|
96b82bb9b2 | ||
|
|
699ccf13f0 | ||
|
|
ae88aa4e42 | ||
|
|
bcce13b12f | ||
|
|
4fd4f660a7 | ||
|
|
518e59b33c | ||
|
|
165cdcc00d | ||
|
|
48c2115fdf | ||
|
|
d7d68ccdeb | ||
|
|
5cf6362db4 | ||
|
|
4efcc48160 | ||
|
|
383274ce0f | ||
|
|
c9dec3a2f7 | ||
|
|
2d4bf2903b | ||
|
|
2b88cfbda0 | ||
|
|
eb6ab7a156 | ||
|
|
9bed7bf213 | ||
|
|
99c2796014 | ||
|
|
9ee9bf12ae | ||
|
|
688cbe50f2 | ||
|
|
e0577d1628 | ||
|
|
de70925f8a | ||
|
|
0f8dd17fde | ||
|
|
4bc8a08606 | ||
|
|
cf34433186 | ||
|
|
21113d6fc8 |
13
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
#github: [J-Jamet] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
#patreon: # Replace with a single Patreon username
|
||||||
|
#open_collective: # Replace with a single Open Collective username
|
||||||
|
#ko_fi: # Replace with a single Ko-fi username
|
||||||
|
#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: Kunzisoft # Replace with a single Liberapay username
|
||||||
|
issuehunt: Kunzisoft/KeePassDX # Replace with a single IssueHunt username
|
||||||
|
#otechie: # Replace with a single Otechie username
|
||||||
|
#lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
custom: ['https://www.keepassdx.com/#donation'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -8,9 +8,11 @@ assignees: ''
|
|||||||
---
|
---
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
|
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
|
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
@@ -18,9 +20,11 @@ Steps to reproduce the behavior:
|
|||||||
4. See error
|
4. See error
|
||||||
|
|
||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
|
|
||||||
A clear and concise description of what you expected to happen.
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
**KeePass Database**
|
**KeePass Database**
|
||||||
|
|
||||||
- Created with: [e.g Windows KeePass 2.42]
|
- Created with: [e.g Windows KeePass 2.42]
|
||||||
- Version: [e.g. 2]
|
- Version: [e.g. 2]
|
||||||
- Location: [e.g. Remote file retrieved with GDrive app]
|
- Location: [e.g. Remote file retrieved with GDrive app]
|
||||||
@@ -28,15 +32,18 @@ A clear and concise description of what you expected to happen.
|
|||||||
- Size: [e.g. 150Mo]
|
- Size: [e.g. 150Mo]
|
||||||
- Contains attachment: [e.g. Yes]
|
- Contains attachment: [e.g. Yes]
|
||||||
|
|
||||||
**KeePassDX (please complete the following information):**
|
**KeePassDX:**
|
||||||
|
|
||||||
- Version: [e.g. 2.5.0.0beta23]
|
- Version: [e.g. 2.5.0.0beta23]
|
||||||
- Build: [e.g. Free]
|
- Build: [e.g. Free]
|
||||||
- Language: [e.g. French]
|
- Language: [e.g. French]
|
||||||
|
|
||||||
**Android (please complete the following information):**
|
**Android:**
|
||||||
|
|
||||||
- Device: [e.g. GalaxyS8]
|
- Device: [e.g. GalaxyS8]
|
||||||
- Version: [e.g. 8.1]
|
- Version: [e.g. 8.1]
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
|
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
- Browser for Autofill: [e.g. Chrome version X]
|
- Browser for Autofill: [e.g. Chrome version X]
|
||||||
|
|||||||
3
.gitignore
vendored
@@ -80,6 +80,9 @@ art/screen*.png
|
|||||||
art/logo_512.png
|
art/logo_512.png
|
||||||
art/store_screens/
|
art/store_screens/
|
||||||
|
|
||||||
|
# Release
|
||||||
|
releases/*
|
||||||
|
|
||||||
# Dir linux
|
# Dir linux
|
||||||
.directory
|
.directory
|
||||||
*/.directory
|
*/.directory
|
||||||
|
|||||||
37
CHANGELOG
@@ -1,3 +1,40 @@
|
|||||||
|
KeePassDX(3.4.3)
|
||||||
|
* Remove "Select share info" setting for Magikeyboard #1304
|
||||||
|
* Fix quick search and better loadGroup implementation #1302
|
||||||
|
* Fix small bugs
|
||||||
|
|
||||||
|
KeePassDX(3.4.2)
|
||||||
|
* Fix service parameter and workflow to remove notification when service is killed
|
||||||
|
* Fix color
|
||||||
|
|
||||||
|
KeePassDX(3.4.1)
|
||||||
|
* Fix search mode with Magikeyboard #1292
|
||||||
|
* Fix select another entry with Magikeyboard #1293
|
||||||
|
* Fix unexpected lock with Magikeyboard #1294
|
||||||
|
* Small UI changes
|
||||||
|
|
||||||
|
KeePassDX(3.4.0)
|
||||||
|
* Passphrase implementation #218
|
||||||
|
* Show visual password strength indicator with entropy #631 #869 #454 #1270
|
||||||
|
* Dynamically save password generator configuration #618 #696
|
||||||
|
* Add advanced password filters #1052 #448 #983 #271 #539
|
||||||
|
* Better search implementation #175 #1254 #1267
|
||||||
|
* Manage package name from Magikeyboard #1010 #1261
|
||||||
|
* Ask confirmation to lock if changes without save #970
|
||||||
|
* Fix small bugs #1282
|
||||||
|
|
||||||
|
KeePassDX(3.3.3)
|
||||||
|
* Fix shared otpauth link if database not open #1274
|
||||||
|
* Ellipsize attachment name #1253
|
||||||
|
* Add a warning to inform about KeyStore usage #1269
|
||||||
|
* Fingerprint unlock no more by default #1273
|
||||||
|
* Tabs to show main and advanced content separately
|
||||||
|
* Fix URL color
|
||||||
|
|
||||||
|
KeePassDX(3.3.2)
|
||||||
|
* Merge KeePassDX & KeePassDX Pro #1257
|
||||||
|
* Create new Contributor Pro app
|
||||||
|
|
||||||
KeePassDX(3.3.1)
|
KeePassDX(3.3.1)
|
||||||
* Fix Japanese keyboard in search #1248
|
* Fix Japanese keyboard in search #1248
|
||||||
* Better OOM management #256
|
* Better OOM management #256
|
||||||
|
|||||||
10
Gemfile
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Autogenerated by fastlane
|
||||||
|
#
|
||||||
|
# Ensure this file is checked in to source control!
|
||||||
|
|
||||||
|
source "https://rubygems.org"
|
||||||
|
|
||||||
|
gem 'fastlane'
|
||||||
|
|
||||||
|
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
|
||||||
|
eval_gemfile(plugins_path) if File.exist?(plugins_path)
|
||||||
220
Gemfile.lock
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
CFPropertyList (3.0.5)
|
||||||
|
rexml
|
||||||
|
addressable (2.8.0)
|
||||||
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
|
artifactory (3.0.15)
|
||||||
|
atomos (0.1.3)
|
||||||
|
aws-eventstream (1.2.0)
|
||||||
|
aws-partitions (1.577.0)
|
||||||
|
aws-sdk-core (3.130.1)
|
||||||
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
|
aws-partitions (~> 1, >= 1.525.0)
|
||||||
|
aws-sigv4 (~> 1.1)
|
||||||
|
jmespath (~> 1.0)
|
||||||
|
aws-sdk-kms (1.55.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.127.0)
|
||||||
|
aws-sigv4 (~> 1.1)
|
||||||
|
aws-sdk-s3 (1.113.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.127.0)
|
||||||
|
aws-sdk-kms (~> 1)
|
||||||
|
aws-sigv4 (~> 1.4)
|
||||||
|
aws-sigv4 (1.4.0)
|
||||||
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
|
babosa (1.0.4)
|
||||||
|
claide (1.1.0)
|
||||||
|
colored (1.2)
|
||||||
|
colored2 (3.1.2)
|
||||||
|
commander (4.6.0)
|
||||||
|
highline (~> 2.0.0)
|
||||||
|
declarative (0.0.20)
|
||||||
|
digest-crc (0.6.4)
|
||||||
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
|
domain_name (0.5.20190701)
|
||||||
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
|
dotenv (2.7.6)
|
||||||
|
emoji_regex (3.2.3)
|
||||||
|
excon (0.92.2)
|
||||||
|
faraday (1.10.0)
|
||||||
|
faraday-em_http (~> 1.0)
|
||||||
|
faraday-em_synchrony (~> 1.0)
|
||||||
|
faraday-excon (~> 1.1)
|
||||||
|
faraday-httpclient (~> 1.0)
|
||||||
|
faraday-multipart (~> 1.0)
|
||||||
|
faraday-net_http (~> 1.0)
|
||||||
|
faraday-net_http_persistent (~> 1.0)
|
||||||
|
faraday-patron (~> 1.0)
|
||||||
|
faraday-rack (~> 1.0)
|
||||||
|
faraday-retry (~> 1.0)
|
||||||
|
ruby2_keywords (>= 0.0.4)
|
||||||
|
faraday-cookie_jar (0.0.7)
|
||||||
|
faraday (>= 0.8.0)
|
||||||
|
http-cookie (~> 1.0.0)
|
||||||
|
faraday-em_http (1.0.0)
|
||||||
|
faraday-em_synchrony (1.0.0)
|
||||||
|
faraday-excon (1.1.0)
|
||||||
|
faraday-httpclient (1.0.1)
|
||||||
|
faraday-multipart (1.0.3)
|
||||||
|
multipart-post (>= 1.2, < 3)
|
||||||
|
faraday-net_http (1.0.1)
|
||||||
|
faraday-net_http_persistent (1.2.0)
|
||||||
|
faraday-patron (1.0.0)
|
||||||
|
faraday-rack (1.0.0)
|
||||||
|
faraday-retry (1.0.3)
|
||||||
|
faraday_middleware (1.2.0)
|
||||||
|
faraday (~> 1.0)
|
||||||
|
fastimage (2.2.6)
|
||||||
|
fastlane (2.205.1)
|
||||||
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
|
addressable (>= 2.8, < 3.0.0)
|
||||||
|
artifactory (~> 3.0)
|
||||||
|
aws-sdk-s3 (~> 1.0)
|
||||||
|
babosa (>= 1.0.3, < 2.0.0)
|
||||||
|
bundler (>= 1.12.0, < 3.0.0)
|
||||||
|
colored
|
||||||
|
commander (~> 4.6)
|
||||||
|
dotenv (>= 2.1.1, < 3.0.0)
|
||||||
|
emoji_regex (>= 0.1, < 4.0)
|
||||||
|
excon (>= 0.71.0, < 1.0.0)
|
||||||
|
faraday (~> 1.0)
|
||||||
|
faraday-cookie_jar (~> 0.0.6)
|
||||||
|
faraday_middleware (~> 1.0)
|
||||||
|
fastimage (>= 2.1.0, < 3.0.0)
|
||||||
|
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||||
|
google-apis-androidpublisher_v3 (~> 0.3)
|
||||||
|
google-apis-playcustomapp_v1 (~> 0.1)
|
||||||
|
google-cloud-storage (~> 1.31)
|
||||||
|
highline (~> 2.0)
|
||||||
|
json (< 3.0.0)
|
||||||
|
jwt (>= 2.1.0, < 3)
|
||||||
|
mini_magick (>= 4.9.4, < 5.0.0)
|
||||||
|
multipart-post (~> 2.0.0)
|
||||||
|
naturally (~> 2.2)
|
||||||
|
optparse (~> 0.1.1)
|
||||||
|
plist (>= 3.1.0, < 4.0.0)
|
||||||
|
rubyzip (>= 2.0.0, < 3.0.0)
|
||||||
|
security (= 0.1.3)
|
||||||
|
simctl (~> 1.6.3)
|
||||||
|
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||||
|
terminal-table (>= 1.4.5, < 2.0.0)
|
||||||
|
tty-screen (>= 0.6.3, < 1.0.0)
|
||||||
|
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||||
|
word_wrap (~> 1.0.0)
|
||||||
|
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||||
|
xcpretty (~> 0.3.0)
|
||||||
|
xcpretty-travis-formatter (>= 0.0.3)
|
||||||
|
fastlane-plugin-versioning_android (0.1.0)
|
||||||
|
gh_inspector (1.1.3)
|
||||||
|
google-apis-androidpublisher_v3 (0.19.0)
|
||||||
|
google-apis-core (>= 0.4, < 2.a)
|
||||||
|
google-apis-core (0.4.2)
|
||||||
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
|
mini_mime (~> 1.0)
|
||||||
|
representable (~> 3.0)
|
||||||
|
retriable (>= 2.0, < 4.a)
|
||||||
|
rexml
|
||||||
|
webrick
|
||||||
|
google-apis-iamcredentials_v1 (0.10.0)
|
||||||
|
google-apis-core (>= 0.4, < 2.a)
|
||||||
|
google-apis-playcustomapp_v1 (0.7.0)
|
||||||
|
google-apis-core (>= 0.4, < 2.a)
|
||||||
|
google-apis-storage_v1 (0.13.0)
|
||||||
|
google-apis-core (>= 0.4, < 2.a)
|
||||||
|
google-cloud-core (1.6.0)
|
||||||
|
google-cloud-env (~> 1.0)
|
||||||
|
google-cloud-errors (~> 1.0)
|
||||||
|
google-cloud-env (1.6.0)
|
||||||
|
faraday (>= 0.17.3, < 3.0)
|
||||||
|
google-cloud-errors (1.2.0)
|
||||||
|
google-cloud-storage (1.36.1)
|
||||||
|
addressable (~> 2.8)
|
||||||
|
digest-crc (~> 0.4)
|
||||||
|
google-apis-iamcredentials_v1 (~> 0.1)
|
||||||
|
google-apis-storage_v1 (~> 0.1)
|
||||||
|
google-cloud-core (~> 1.6)
|
||||||
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
|
mini_mime (~> 1.0)
|
||||||
|
googleauth (1.1.2)
|
||||||
|
faraday (>= 0.17.3, < 3.a)
|
||||||
|
jwt (>= 1.4, < 3.0)
|
||||||
|
memoist (~> 0.16)
|
||||||
|
multi_json (~> 1.11)
|
||||||
|
os (>= 0.9, < 2.0)
|
||||||
|
signet (>= 0.16, < 2.a)
|
||||||
|
highline (2.0.3)
|
||||||
|
http-cookie (1.0.4)
|
||||||
|
domain_name (~> 0.5)
|
||||||
|
httpclient (2.8.3)
|
||||||
|
jmespath (1.6.1)
|
||||||
|
json (2.6.1)
|
||||||
|
jwt (2.3.0)
|
||||||
|
memoist (0.16.2)
|
||||||
|
mini_magick (4.11.0)
|
||||||
|
mini_mime (1.1.2)
|
||||||
|
multi_json (1.15.0)
|
||||||
|
multipart-post (2.0.0)
|
||||||
|
nanaimo (0.3.0)
|
||||||
|
naturally (2.2.1)
|
||||||
|
optparse (0.1.1)
|
||||||
|
os (1.1.4)
|
||||||
|
plist (3.6.0)
|
||||||
|
public_suffix (4.0.7)
|
||||||
|
rake (13.0.6)
|
||||||
|
representable (3.1.1)
|
||||||
|
declarative (< 0.1.0)
|
||||||
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
|
uber (< 0.2.0)
|
||||||
|
retriable (3.1.2)
|
||||||
|
rexml (3.2.5)
|
||||||
|
rouge (2.0.7)
|
||||||
|
ruby2_keywords (0.0.5)
|
||||||
|
rubyzip (2.3.2)
|
||||||
|
security (0.1.3)
|
||||||
|
signet (0.16.1)
|
||||||
|
addressable (~> 2.8)
|
||||||
|
faraday (>= 0.17.5, < 3.0)
|
||||||
|
jwt (>= 1.5, < 3.0)
|
||||||
|
multi_json (~> 1.10)
|
||||||
|
simctl (1.6.8)
|
||||||
|
CFPropertyList
|
||||||
|
naturally
|
||||||
|
terminal-notifier (2.0.0)
|
||||||
|
terminal-table (1.8.0)
|
||||||
|
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||||
|
trailblazer-option (0.1.2)
|
||||||
|
tty-cursor (0.7.1)
|
||||||
|
tty-screen (0.8.1)
|
||||||
|
tty-spinner (0.9.3)
|
||||||
|
tty-cursor (~> 0.7)
|
||||||
|
uber (0.1.0)
|
||||||
|
unf (0.1.4)
|
||||||
|
unf_ext
|
||||||
|
unf_ext (0.0.8.1)
|
||||||
|
unicode-display_width (1.8.0)
|
||||||
|
webrick (1.7.0)
|
||||||
|
word_wrap (1.0.0)
|
||||||
|
xcodeproj (1.21.0)
|
||||||
|
CFPropertyList (>= 2.3.3, < 4.0)
|
||||||
|
atomos (~> 0.1.3)
|
||||||
|
claide (>= 1.0.2, < 2.0)
|
||||||
|
colored2 (~> 3.1)
|
||||||
|
nanaimo (~> 0.3.0)
|
||||||
|
rexml (~> 3.2.4)
|
||||||
|
xcpretty (0.3.0)
|
||||||
|
rouge (~> 2.0.7)
|
||||||
|
xcpretty-travis-formatter (1.0.1)
|
||||||
|
xcpretty (~> 0.2, >= 0.0.7)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
ruby
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
fastlane
|
||||||
|
fastlane-plugin-versioning_android
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
2.1.4
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Android KeePassDX
|
# Android KeePassDX
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> KeePassDX is a **multi-format KeePass manager for Android devices**. The app allows creating keys and passwords in a secure way by integrating with the Android design standards.
|
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> **Lightweight password manager for Android**, KeePassDX allows editing encrypted data in a single file in KeePass format and fill in the forms in a secure way.
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 15
|
minSdkVersion 15
|
||||||
targetSdkVersion 31
|
targetSdkVersion 31
|
||||||
versionCode = 103
|
versionCode = 112
|
||||||
versionName = "3.3.1"
|
versionName = "3.4.3"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
@@ -43,30 +43,20 @@ android {
|
|||||||
dimension "version"
|
dimension "version"
|
||||||
applicationIdSuffix = ".libre"
|
applicationIdSuffix = ".libre"
|
||||||
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "true"
|
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "false"
|
buildConfigField "boolean", "CLOSED_STORE", "false"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED",
|
buildConfigField "String[]", "STYLES_DISABLED",
|
||||||
"{\"KeepassDXStyle_Red\"," +
|
"{\"KeepassDXStyle_Red\"," +
|
||||||
"\"KeepassDXStyle_Red_Night\"," +
|
"\"KeepassDXStyle_Red_Night\"," +
|
||||||
|
"\"KeepassDXStyle_Reply\"," +
|
||||||
|
"\"KeepassDXStyle_Reply_Night\"," +
|
||||||
"\"KeepassDXStyle_Purple\"," +
|
"\"KeepassDXStyle_Purple\"," +
|
||||||
"\"KeepassDXStyle_Purple_Dark\"}"
|
"\"KeepassDXStyle_Purple_Dark\"}"
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
}
|
}
|
||||||
pro {
|
|
||||||
dimension "version"
|
|
||||||
applicationIdSuffix = ".pro"
|
|
||||||
buildConfigField "String", "BUILD_VERSION", "\"pro\""
|
|
||||||
buildConfigField "boolean", "FULL_VERSION", "true"
|
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
|
||||||
buildConfigField "String[]", "STYLES_DISABLED", "{}"
|
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
|
||||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIZiXvrQCzSV9LNI6-p7cjTKENZLHIrz_zaqZuQQ" ]
|
|
||||||
}
|
|
||||||
free {
|
free {
|
||||||
dimension "version"
|
dimension "version"
|
||||||
applicationIdSuffix = ".free"
|
applicationIdSuffix = ".free"
|
||||||
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "false"
|
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
buildConfigField "boolean", "CLOSED_STORE", "true"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED",
|
buildConfigField "String[]", "STYLES_DISABLED",
|
||||||
"{\"KeepassDXStyle_Simple\"," +
|
"{\"KeepassDXStyle_Simple\"," +
|
||||||
@@ -86,7 +76,6 @@ android {
|
|||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
libre.res.srcDir 'src/libre/res'
|
libre.res.srcDir 'src/libre/res'
|
||||||
pro.res.srcDir 'src/pro/res'
|
|
||||||
free.res.srcDir 'src/free/res'
|
free.res.srcDir 'src/free/res'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +127,8 @@ dependencies {
|
|||||||
// Apache Commons
|
// Apache Commons
|
||||||
implementation 'commons-io:commons-io:2.8.0'
|
implementation 'commons-io:commons-io:2.8.0'
|
||||||
implementation 'commons-codec:commons-codec:1.15'
|
implementation 'commons-codec:commons-codec:1.15'
|
||||||
|
// Password generator
|
||||||
|
implementation 'me.gosimple:nbvcxz:1.5.0'
|
||||||
// Encrypt lib
|
// Encrypt lib
|
||||||
implementation project(path: ':crypto')
|
implementation project(path: ':crypto')
|
||||||
// Icon pack
|
// Icon pack
|
||||||
|
|||||||
@@ -1,61 +1,31 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108"
|
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp">
|
android:height="108dp"
|
||||||
|
android:viewportWidth="120"
|
||||||
|
android:viewportHeight="120">
|
||||||
<group
|
<group
|
||||||
android:translateY="-332">
|
android:translateX="6"
|
||||||
<group
|
android:translateY="8">
|
||||||
android:translateY="332">
|
<path
|
||||||
<path
|
android:fillColor="#24000000"
|
||||||
android:pathData="M65.728516 32.791016L58.052734 35.904297 56.173828 48.380859 35.306641 69.267578 35.238281 73.759766 69.478516 108 108 108 108 70.810547 73.09375 35.904297 65.728516 32.791016Z"
|
android:strokeWidth="1.99999297"
|
||||||
android:strokeLineJoin="round"
|
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||||
android:strokeLineCap="round"
|
<path
|
||||||
android:strokeMiterLimit="4" >
|
android:fillColor="#24000000"
|
||||||
<aapt:attr name="android:fillColor">
|
android:strokeWidth="1.99999297"
|
||||||
<gradient
|
android:pathData="M63.9961,34.0059 C61.5643,34.096,59.2564,35.102,57.5352,36.8223 C53.7682,40.589,53.7682,46.6982,57.5352,50.4649 C61.3017,54.232,67.4073,54.232,71.1739,50.4649 C74.9409,46.6982,74.9409,40.589,71.1739,36.8223 C69.2766,34.9258,66.6768,33.9054,63.9962,34.0059 Z M68.1992,40.6954 C69.8278,40.6958,71.148,42.016,71.1484,43.6446 C71.148,45.2732,69.8278,46.5934,68.1992,46.5938 C66.5706,46.5934,65.2504,45.2732,65.25,43.6446 C65.2504,42.016,66.5706,40.6958,68.1992,40.6954 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||||
android:endColor="#0000"
|
</group>
|
||||||
android:endX="80"
|
<group
|
||||||
android:endY="80"
|
android:translateX="6"
|
||||||
android:startColor="#4e000000"
|
android:translateY="6">
|
||||||
android:startX="0"
|
<path
|
||||||
android:startY="0"
|
android:fillColor="#ffa726"
|
||||||
android:type="linear"/>
|
android:strokeWidth="1.99999297"
|
||||||
</aapt:attr>
|
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||||
</path>
|
<path
|
||||||
</group>
|
android:fillColor="#ffffff"
|
||||||
<group
|
android:strokeWidth="1.99999297"
|
||||||
android:scaleX="0.3939503"
|
android:pathData="M63.9961,34.0059 C61.5643,34.096,59.2564,35.102,57.5352,36.8223 C53.7682,40.589,53.7682,46.6982,57.5352,50.4649 C61.3017,54.232,67.4073,54.232,71.1739,50.4649 C74.9409,46.6982,74.9409,40.589,71.1739,36.8223 C69.2766,34.9258,66.6768,33.9054,63.9962,34.0059 Z M68.1992,40.6954 C69.8278,40.6958,71.148,42.016,71.1484,43.6446 C71.148,45.2732,69.8278,46.5934,68.1992,46.5938 C66.5706,46.5934,65.2504,45.2732,65.25,43.6446 C65.2504,42.016,66.5706,40.6958,68.1992,40.6954 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||||
android:scaleY="0.3939503"
|
|
||||||
android:translateX="33.66343"
|
|
||||||
android:translateY="233.998">
|
|
||||||
<path
|
|
||||||
android:pathData="M88.76953 339.91602L4.1718754 424.59766 4.0000004 436 15.400391 435.82813 27.240234 424 40 424l0 -12 12 0 0 -12.73438 34.01172 -33.97656A8 8 0 0 1 84 360a8 8 0 0 1 8 -8 8 8 0 0 1 5.296882 2.01367l2.787098 -2.7832 -11.31445 -11.31445z"
|
|
||||||
android:fillColor="#eaeaea"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#58000000" />
|
|
||||||
</group>
|
|
||||||
<group
|
|
||||||
android:scaleX="0.3939503"
|
|
||||||
android:scaleY="0.3939503"
|
|
||||||
android:translateX="33.66343"
|
|
||||||
android:translateY="233.998">
|
|
||||||
<path
|
|
||||||
android:pathData="M4.0000004 340L4.1718754 351.40137 88.59863 435.82812 100 436 99.828122 424.59863 15.401367 340.17188Z"
|
|
||||||
android:fillColor="#81c784" />
|
|
||||||
</group>
|
|
||||||
<group
|
|
||||||
android:scaleX="0.3939503"
|
|
||||||
android:scaleY="0.3939503"
|
|
||||||
android:translateX="33.66343"
|
|
||||||
android:translateY="233.998">
|
|
||||||
<path
|
|
||||||
android:pathData="M81.39454 332.00195a27 27 0 0 0 -19.48634 7.90625 27 27 0 0 0 0 38.1836 27 27 0 0 0 38.1836 0 27 27 0 0 0 0 -38.1836 27 27 0 0 0 -18.69726 -7.90625zM92 352a8 8 0 0 1 8 8 8 8 0 0 1 -8 8 8 8 0 0 1 -8 -8 8 8 0 0 1 8 -8z"
|
|
||||||
android:fillColor="#eaeaea"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#58000000" />
|
|
||||||
</group>
|
|
||||||
</group>
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
19
app/src/free/res/drawable/ic_app_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="84"
|
||||||
|
android:viewportHeight="84">
|
||||||
|
<group
|
||||||
|
android:translateX="-12"
|
||||||
|
android:translateY="-12">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffa726"
|
||||||
|
android:strokeWidth="1.99999297"
|
||||||
|
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeWidth="1.99999297"
|
||||||
|
android:pathData="M63.9961,34.0059 C61.5643,34.096,59.2564,35.102,57.5352,36.8223 C53.7682,40.589,53.7682,46.6982,57.5352,50.4649 C61.3017,54.232,67.4073,54.232,71.1739,50.4649 C74.9409,46.6982,74.9409,40.589,71.1739,36.8223 C69.2766,34.9258,66.6768,33.9054,63.9962,34.0059 Z M68.1992,40.6954 C69.8278,40.6958,71.148,42.016,71.1484,43.6446 C71.148,45.2732,69.8278,46.5934,68.1992,46.5938 C66.5706,46.5934,65.2504,45.2732,65.25,43.6446 C65.2504,42.016,66.5706,40.6958,68.1992,40.6954 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |
@@ -1,61 +1,31 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108"
|
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp">
|
android:height="108dp"
|
||||||
|
android:viewportWidth="120"
|
||||||
|
android:viewportHeight="120">
|
||||||
<group
|
<group
|
||||||
android:translateY="-332">
|
android:translateX="6"
|
||||||
<group
|
android:translateY="8">
|
||||||
android:translateY="332">
|
<path
|
||||||
<path
|
android:fillColor="#24000000"
|
||||||
android:pathData="M65.728516 32.791016L58.052734 35.904297 56.173828 48.380859 35.306641 69.267578 35.238281 73.759766 69.478516 108 108 108 108 70.810547 73.09375 35.904297 65.728516 32.791016Z"
|
android:strokeWidth="1.99999297"
|
||||||
android:strokeLineJoin="round"
|
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||||
android:strokeLineCap="round"
|
<path
|
||||||
android:strokeMiterLimit="4" >
|
android:fillColor="#24000000"
|
||||||
<aapt:attr name="android:fillColor">
|
android:strokeWidth="1.99999297"
|
||||||
<gradient
|
android:pathData="M64.501,35.0576 C63.7095,35.0576,62.918,35.3613,62.3115,35.9678 L55.0127,43.2666 C53.7998,44.4795,53.7998,46.4306,55.0127,47.6436 L62.3115,54.9424 C63.5244,56.1553,65.4775,56.1553,66.6904,54.9424 L73.9873,47.6436 C75.2002,46.4307,75.2002,44.4796,73.9873,43.2666 L66.6904,35.9678 C66.0839,35.3613,65.2924,35.0576,64.5009,35.0576 Z M67.6729,42.6006 C69.3298,42.6006,70.6729,43.9437,70.6729,45.6006 C70.6729,47.2575,69.3298,48.6006,67.6729,48.6006 C66.016,48.6006,64.6729,47.2575,64.6729,45.6006 C64.6729,43.9437,66.016,42.6006,67.6729,42.6006 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||||
android:endColor="#0000"
|
</group>
|
||||||
android:endX="80"
|
<group
|
||||||
android:endY="80"
|
android:translateX="6"
|
||||||
android:startColor="#4e000000"
|
android:translateY="6">
|
||||||
android:startX="0"
|
<path
|
||||||
android:startY="0"
|
android:fillColor="#ffa726"
|
||||||
android:type="linear"/>
|
android:strokeWidth="1.99999297"
|
||||||
</aapt:attr>
|
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||||
</path>
|
<path
|
||||||
</group>
|
android:fillColor="#ffffff"
|
||||||
<group
|
android:strokeWidth="1.99999297"
|
||||||
android:scaleX="0.3939503"
|
android:pathData="M64.501,35.0576 C63.7095,35.0576,62.918,35.3613,62.3115,35.9678 L55.0127,43.2666 C53.7998,44.4795,53.7998,46.4306,55.0127,47.6436 L62.3115,54.9424 C63.5244,56.1553,65.4775,56.1553,66.6904,54.9424 L73.9873,47.6436 C75.2002,46.4307,75.2002,44.4796,73.9873,43.2666 L66.6904,35.9678 C66.0839,35.3613,65.2924,35.0576,64.5009,35.0576 Z M67.6729,42.6006 C69.3298,42.6006,70.6729,43.9437,70.6729,45.6006 C70.6729,47.2575,69.3298,48.6006,67.6729,48.6006 C66.016,48.6006,64.6729,47.2575,64.6729,45.6006 C64.6729,43.9437,66.016,42.6006,67.6729,42.6006 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||||
android:scaleY="0.3939503"
|
|
||||||
android:translateX="33.66343"
|
|
||||||
android:translateY="233.998">
|
|
||||||
<path
|
|
||||||
android:pathData="M88.76953 339.91602L4.1718754 424.59766 4.0000004 436 15.400391 435.82813 27.240234 424 40 424l0 -12 12 0 0 -12.73438 34.01172 -33.97656A8 8 0 0 1 84 360a8 8 0 0 1 8 -8 8 8 0 0 1 5.296882 2.01367l2.787098 -2.7832 -11.31445 -11.31445z"
|
|
||||||
android:fillColor="#eaeaea"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#58000000"/>
|
|
||||||
</group>
|
|
||||||
<group
|
|
||||||
android:scaleX="0.3939503"
|
|
||||||
android:scaleY="0.3939503"
|
|
||||||
android:translateX="33.66343"
|
|
||||||
android:translateY="233.998">
|
|
||||||
<path
|
|
||||||
android:pathData="M4.0000004 340L4.1718754 351.40137 88.59863 435.82812 100 436 99.828122 424.59863 15.401367 340.17188Z"
|
|
||||||
android:fillColor="#64b5f6" />
|
|
||||||
</group>
|
|
||||||
<group
|
|
||||||
android:scaleX="0.3939503"
|
|
||||||
android:scaleY="0.3939503"
|
|
||||||
android:translateX="33.66343"
|
|
||||||
android:translateY="233.998">
|
|
||||||
<path
|
|
||||||
android:pathData="M81.39454 332.00195a27 27 0 0 0 -19.48634 7.90625 27 27 0 0 0 0 38.1836 27 27 0 0 0 38.1836 0 27 27 0 0 0 0 -38.1836 27 27 0 0 0 -18.69726 -7.90625zM92 352a8 8 0 0 1 8 8 8 8 0 0 1 -8 8 8 8 0 0 1 -8 -8 8 8 0 0 1 8 -8z"
|
|
||||||
android:fillColor="#eaeaea"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#58000000" />
|
|
||||||
</group>
|
|
||||||
</group>
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
19
app/src/libre/res/drawable/ic_app_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="84"
|
||||||
|
android:viewportHeight="84">
|
||||||
|
<group
|
||||||
|
android:translateX="-12"
|
||||||
|
android:translateY="-12">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffa726"
|
||||||
|
android:strokeWidth="1.99999297"
|
||||||
|
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeWidth="1.99999297"
|
||||||
|
android:pathData="M64.501,35.0576 C63.7095,35.0576,62.918,35.3613,62.3115,35.9678 L55.0127,43.2666 C53.7998,44.4795,53.7998,46.4306,55.0127,47.6436 L62.3115,54.9424 C63.5244,56.1553,65.4775,56.1553,66.6904,54.9424 L73.9873,47.6436 C75.2002,46.4307,75.2002,44.4796,73.9873,43.2666 L66.6904,35.9678 C66.0839,35.3613,65.2924,35.0576,64.5009,35.0576 Z M67.6729,42.6006 C69.3298,42.6006,70.6729,43.9437,70.6729,45.6006 C70.6729,47.2575,69.3298,48.6006,67.6729,48.6006 C66.016,48.6006,64.6729,47.2575,64.6729,45.6006 C64.6729,43.9437,66.016,42.6006,67.6729,42.6006 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/green" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/green" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 12 KiB |
@@ -131,6 +131,9 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.IconPickerActivity"
|
android:name="com.kunzisoft.keepass.activities.IconPickerActivity"
|
||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.KeyGeneratorActivity"
|
||||||
|
android:configChanges="keyboardHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.ImageViewerActivity"
|
android:name="com.kunzisoft.keepass.activities.ImageViewerActivity"
|
||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden" />
|
||||||
@@ -155,6 +158,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
||||||
android:theme="@style/Theme.Transparent"
|
android:theme="@style/Theme.Transparent"
|
||||||
|
android:launchMode="singleInstance"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
@@ -169,9 +173,6 @@
|
|||||||
<data android:scheme="otpauth" android:host="hotp" />
|
<data android:scheme="otpauth" android:host="hotp" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name="com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity"
|
|
||||||
android:theme="@style/Theme.Transparent" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
||||||
android:label="@string/keyboard_setting_label"
|
android:label="@string/keyboard_setting_label"
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import androidx.core.graphics.ColorUtils
|
|||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
@@ -83,6 +84,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
private var titleIconView: ImageView? = null
|
private var titleIconView: ImageView? = null
|
||||||
private var historyView: View? = null
|
private var historyView: View? = null
|
||||||
private var tagsListView: RecyclerView? = null
|
private var tagsListView: RecyclerView? = null
|
||||||
|
private var entryContentTab: TabLayout? = null
|
||||||
private var tagsAdapter: TagsAdapter? = null
|
private var tagsAdapter: TagsAdapter? = null
|
||||||
private var entryProgress: LinearProgressIndicator? = null
|
private var entryProgress: LinearProgressIndicator? = null
|
||||||
private var lockView: View? = null
|
private var lockView: View? = null
|
||||||
@@ -133,6 +135,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
titleIconView = findViewById(R.id.entry_icon)
|
titleIconView = findViewById(R.id.entry_icon)
|
||||||
historyView = findViewById(R.id.history_container)
|
historyView = findViewById(R.id.history_container)
|
||||||
tagsListView = findViewById(R.id.entry_tags_list_view)
|
tagsListView = findViewById(R.id.entry_tags_list_view)
|
||||||
|
entryContentTab = findViewById(R.id.entry_content_tab)
|
||||||
entryProgress = findViewById(R.id.entry_progress)
|
entryProgress = findViewById(R.id.entry_progress)
|
||||||
lockView = findViewById(R.id.lock_button)
|
lockView = findViewById(R.id.lock_button)
|
||||||
loadingView = findViewById(R.id.loading)
|
loadingView = findViewById(R.id.loading)
|
||||||
@@ -162,6 +165,19 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
adapter = tagsAdapter
|
adapter = tagsAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Init content tab
|
||||||
|
entryContentTab?.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||||
|
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||||
|
mEntryViewModel.selectSection(EntryViewModel.EntrySection.
|
||||||
|
getEntrySectionByPosition(tab?.position ?: 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||||
|
|
||||||
|
override fun onTabReselected(tab: TabLayout.Tab?) {}
|
||||||
|
})
|
||||||
|
|
||||||
// Get Entry from UUID
|
// Get Entry from UUID
|
||||||
try {
|
try {
|
||||||
intent.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { mainEntryId ->
|
intent.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { mainEntryId ->
|
||||||
@@ -193,6 +209,10 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
lockAndExit()
|
lockAndExit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mEntryViewModel.sectionSelected.observe(this) { entrySection ->
|
||||||
|
entryContentTab?.getTabAt(entrySection.position)?.select()
|
||||||
|
}
|
||||||
|
|
||||||
mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory ->
|
mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory ->
|
||||||
if (entryInfoHistory != null) {
|
if (entryInfoHistory != null) {
|
||||||
this.mMainEntryId = entryInfoHistory.mainEntryId
|
this.mMainEntryId = entryInfoHistory.mainEntryId
|
||||||
|
|||||||
@@ -58,9 +58,13 @@ import com.kunzisoft.keepass.autofill.AutofillHelper
|
|||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.*
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.template.*
|
import com.kunzisoft.keepass.database.element.template.Template
|
||||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||||
import com.kunzisoft.keepass.model.*
|
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||||
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||||
@@ -78,11 +82,9 @@ import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
|||||||
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
class EntryEditActivity : DatabaseLockActivity(),
|
class EntryEditActivity : DatabaseLockActivity(),
|
||||||
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
|
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
|
||||||
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
|
||||||
SetOTPDialogFragment.CreateOtpListener,
|
SetOTPDialogFragment.CreateOtpListener,
|
||||||
DatePickerDialog.OnDateSetListener,
|
DatePickerDialog.OnDateSetListener,
|
||||||
TimePickerDialog.OnTimeSetListener,
|
TimePickerDialog.OnTimeSetListener,
|
||||||
@@ -119,6 +121,20 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
mEntryEditViewModel.selectIcon(icon)
|
mEntryEditViewModel.selectIcon(icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var mPasswordField: Field? = null
|
||||||
|
private var mKeyGeneratorResultLauncher = KeyGeneratorActivity.registerForGeneratedKeyResult(this) { keyGenerated ->
|
||||||
|
keyGenerated?.let {
|
||||||
|
mPasswordField?.let {
|
||||||
|
it.protectedValue.stringValue = keyGenerated
|
||||||
|
mEntryEditViewModel.selectPassword(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mPasswordField = null
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
performedNextEducation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// To ask data lost only one time
|
// To ask data lost only one time
|
||||||
private var backPressedAlreadyApproved = false
|
private var backPressedAlreadyApproved = false
|
||||||
|
|
||||||
@@ -268,9 +284,8 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
mEntryEditViewModel.requestPasswordSelection.observe(this) { passwordField ->
|
mEntryEditViewModel.requestPasswordSelection.observe(this) { passwordField ->
|
||||||
GeneratePasswordDialogFragment
|
mPasswordField = passwordField
|
||||||
.getInstance(passwordField)
|
KeyGeneratorActivity.launch(this, mKeyGeneratorResultLauncher)
|
||||||
.show(supportFragmentManager, "PasswordGeneratorFragment")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mEntryEditViewModel.requestCustomFieldEdition.observe(this) { field ->
|
mEntryEditViewModel.requestCustomFieldEdition.observe(this) { field ->
|
||||||
@@ -420,9 +435,10 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
private fun entryValidatedForKeyboardSelection(database: Database, entry: Entry) {
|
private fun entryValidatedForKeyboardSelection(database: Database, entry: Entry) {
|
||||||
// Populate Magikeyboard with entry
|
// Populate Magikeyboard with entry
|
||||||
populateKeyboardAndMoveAppToBackground(this,
|
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||||
entry.getEntryInfo(database),
|
this,
|
||||||
intent)
|
entry.getEntryInfo(database)
|
||||||
|
)
|
||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
// Don't keep activity history for entry edition
|
// Don't keep activity history for entry edition
|
||||||
finishForEntryResult(entry)
|
finishForEntryResult(entry)
|
||||||
@@ -656,17 +672,6 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
mEntryEditViewModel.selectTime(hours, minutes)
|
mEntryEditViewModel.selectTime(hours, minutes)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun acceptPassword(passwordField: Field) {
|
|
||||||
mEntryEditViewModel.selectPassword(passwordField)
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
|
||||||
performedNextEducation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancelPassword(passwordField: Field) {
|
|
||||||
// Do nothing here
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
onApprovedBackPressed {
|
onApprovedBackPressed {
|
||||||
super@EntryEditActivity.onBackPressed()
|
super@EntryEditActivity.onBackPressed()
|
||||||
|
|||||||
@@ -19,17 +19,16 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.app.Activity
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
@@ -50,151 +49,164 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
var sharedWebDomain: String? = null
|
|
||||||
var otpString: String? = null
|
|
||||||
|
|
||||||
when (intent?.action) {
|
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
|
||||||
Intent.ACTION_SEND -> {
|
if (keySelectionBundle != null) {
|
||||||
if ("text/plain" == intent.type) {
|
// To manage package name
|
||||||
// Retrieve web domain or OTP
|
var searchInfo = SearchInfo()
|
||||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
|
keySelectionBundle.getParcelable<SearchInfo>(KEY_SEARCH_INFO)?.let { mSearchInfo ->
|
||||||
if (OtpEntryFields.isOTPUri(extra))
|
searchInfo = mSearchInfo
|
||||||
otpString = extra
|
}
|
||||||
else
|
launch(database, searchInfo)
|
||||||
sharedWebDomain = Uri.parse(extra).host
|
} else {
|
||||||
|
// To manage share
|
||||||
|
var sharedWebDomain: String? = null
|
||||||
|
var otpString: String? = null
|
||||||
|
|
||||||
|
when (intent?.action) {
|
||||||
|
Intent.ACTION_SEND -> {
|
||||||
|
if ("text/plain" == intent.type) {
|
||||||
|
// Retrieve web domain or OTP
|
||||||
|
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
|
||||||
|
if (OtpEntryFields.isOTPUri(extra))
|
||||||
|
otpString = extra
|
||||||
|
else
|
||||||
|
sharedWebDomain = Uri.parse(extra).host
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
Intent.ACTION_VIEW -> {
|
||||||
Intent.ACTION_VIEW -> {
|
// Retrieve OTP
|
||||||
// Retrieve OTP
|
intent.dataString?.let { extra ->
|
||||||
intent.dataString?.let { extra ->
|
if (OtpEntryFields.isOTPUri(extra))
|
||||||
if (OtpEntryFields.isOTPUri(extra))
|
otpString = extra
|
||||||
otpString = extra
|
}
|
||||||
}
|
}
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build domain search param
|
// Build domain search param
|
||||||
val searchInfo = SearchInfo().apply {
|
val searchInfo = SearchInfo().apply {
|
||||||
this.webDomain = sharedWebDomain
|
this.webDomain = sharedWebDomain
|
||||||
this.otpString = otpString
|
this.otpString = otpString
|
||||||
}
|
}
|
||||||
|
|
||||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
searchInfo.webDomain = concreteWebDomain
|
searchInfo.webDomain = concreteWebDomain
|
||||||
launch(database, searchInfo)
|
launch(database, searchInfo)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launch(database: Database?,
|
private fun launch(database: Database?,
|
||||||
searchInfo: SearchInfo) {
|
searchInfo: SearchInfo) {
|
||||||
|
|
||||||
if (!searchInfo.containsOnlyNullValues()) {
|
// Setting to integrate Magikeyboard
|
||||||
// Setting to integrate Magikeyboard
|
val searchShareForMagikeyboard = MagikeyboardService.activatedInSettings(this)
|
||||||
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
|
|
||||||
|
|
||||||
// If database is open
|
// If database is open
|
||||||
val readOnly = database?.isReadOnly != false
|
val readOnly = database?.isReadOnly != false
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
database,
|
database,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
{ openedDatabase, items ->
|
{ openedDatabase, items ->
|
||||||
// Items found
|
// Items found
|
||||||
if (searchInfo.otpString != null) {
|
if (searchInfo.otpString != null) {
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
GroupActivity.launchForSaveResult(
|
GroupActivity.launchForSaveResult(
|
||||||
this,
|
this,
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
false)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(applicationContext,
|
|
||||||
R.string.autofill_read_only_save,
|
|
||||||
Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
} else if (searchShareForMagikeyboard) {
|
|
||||||
if (items.size == 1) {
|
|
||||||
// Automatically populate keyboard
|
|
||||||
val entryPopulate = items[0]
|
|
||||||
populateKeyboardAndMoveAppToBackground(
|
|
||||||
this,
|
|
||||||
entryPopulate,
|
|
||||||
intent)
|
|
||||||
} else {
|
|
||||||
// Select the one we want
|
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
GroupActivity.launchForSearchResult(this,
|
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ openedDatabase ->
|
|
||||||
// Show the database UI to select the entry
|
|
||||||
if (searchInfo.otpString != null) {
|
|
||||||
if (!readOnly) {
|
|
||||||
GroupActivity.launchForSaveResult(this,
|
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
false)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(applicationContext,
|
|
||||||
R.string.autofill_read_only_save,
|
|
||||||
Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
} else if (readOnly || searchShareForMagikeyboard) {
|
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
|
||||||
openedDatabase,
|
openedDatabase,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
false)
|
||||||
} else {
|
} else {
|
||||||
|
Toast.makeText(applicationContext,
|
||||||
|
R.string.autofill_read_only_save,
|
||||||
|
Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
} else if (searchShareForMagikeyboard) {
|
||||||
|
MagikeyboardService.performSelection(
|
||||||
|
items,
|
||||||
|
{ entryInfo ->
|
||||||
|
// Automatically populate keyboard
|
||||||
|
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||||
|
this,
|
||||||
|
entryInfo
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ autoSearch ->
|
||||||
|
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||||
|
openedDatabase,
|
||||||
|
searchInfo,
|
||||||
|
autoSearch)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
GroupActivity.launchForSearchResult(this,
|
||||||
|
openedDatabase,
|
||||||
|
searchInfo,
|
||||||
|
true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ openedDatabase ->
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
if (searchInfo.otpString != null) {
|
||||||
|
if (!readOnly) {
|
||||||
GroupActivity.launchForSaveResult(this,
|
GroupActivity.launchForSaveResult(this,
|
||||||
openedDatabase,
|
openedDatabase,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
false)
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// If database not open
|
|
||||||
if (searchInfo.otpString != null) {
|
|
||||||
if (!readOnly) {
|
|
||||||
FileDatabaseSelectActivity.launchForSaveResult(this,
|
|
||||||
searchInfo)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(applicationContext,
|
|
||||||
R.string.autofill_read_only_save,
|
|
||||||
Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
} else if (searchShareForMagikeyboard) {
|
|
||||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
|
|
||||||
searchInfo)
|
|
||||||
} else {
|
} else {
|
||||||
FileDatabaseSelectActivity.launchForSearchResult(this,
|
Toast.makeText(applicationContext,
|
||||||
searchInfo)
|
R.string.autofill_read_only_save,
|
||||||
|
Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
|
} else if (searchShareForMagikeyboard) {
|
||||||
|
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||||
|
openedDatabase,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
|
} else {
|
||||||
|
GroupActivity.launchForSearchResult(this,
|
||||||
|
openedDatabase,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
}
|
{
|
||||||
|
// If database not open
|
||||||
|
if (searchInfo.otpString != null) {
|
||||||
|
FileDatabaseSelectActivity.launchForSaveResult(this,
|
||||||
|
searchInfo)
|
||||||
|
} else if (searchShareForMagikeyboard) {
|
||||||
|
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
|
||||||
|
searchInfo)
|
||||||
|
} else {
|
||||||
|
FileDatabaseSelectActivity.launchForSearchResult(this,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
companion object {
|
||||||
entry: EntryInfo,
|
|
||||||
intent: Intent,
|
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
||||||
toast: Boolean = true) {
|
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||||
// Populate Magikeyboard with entry
|
|
||||||
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
fun launch(context: Context,
|
||||||
// Consume the selection mode
|
searchInfo: SearchInfo? = null) {
|
||||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
val intent = Intent(context, EntrySelectionLauncherActivity::class.java).apply {
|
||||||
activity.moveTaskToBack(true)
|
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
|
||||||
|
putParcelable(KEY_SEARCH_INFO, searchInfo)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// New task needed because don't launch from an Activity context
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||||
|
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
@@ -73,6 +74,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
// Views
|
// Views
|
||||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
|
private var specialTitle: View? = null
|
||||||
private var createDatabaseButtonView: View? = null
|
private var createDatabaseButtonView: View? = null
|
||||||
private var openDatabaseButtonView: View? = null
|
private var openDatabaseButtonView: View? = null
|
||||||
|
|
||||||
@@ -112,6 +114,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
toolbar.title = ""
|
toolbar.title = ""
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
|
|
||||||
|
// Special title
|
||||||
|
specialTitle = findViewById(R.id.file_selection_title_part_3)
|
||||||
|
|
||||||
// Create database button
|
// Create database button
|
||||||
createDatabaseButtonView = findViewById(R.id.create_database_button)
|
createDatabaseButtonView = findViewById(R.id.create_database_button)
|
||||||
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
||||||
@@ -335,6 +340,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
|
// Define special title
|
||||||
|
specialTitle?.isVisible = UriUtil.contributingUser(this)
|
||||||
|
|
||||||
// Show open and create button or special mode
|
// Show open and create button or special mode
|
||||||
when (mSpecialMode) {
|
when (mSpecialMode) {
|
||||||
SpecialMode.DEFAULT -> {
|
SpecialMode.DEFAULT -> {
|
||||||
@@ -391,7 +399,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
if (mSpecialMode == SpecialMode.DEFAULT) {
|
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||||
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
MenuUtil.defaultMenuInflater(this, menuInflater, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import com.kunzisoft.keepass.database.element.node.Type
|
|||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
import com.kunzisoft.keepass.education.GroupActivityEducation
|
import com.kunzisoft.keepass.education.GroupActivityEducation
|
||||||
|
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.model.GroupInfo
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
@@ -79,6 +80,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
|||||||
import com.kunzisoft.keepass.settings.SettingsActivity
|
import com.kunzisoft.keepass.settings.SettingsActivity
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.*
|
import com.kunzisoft.keepass.view.*
|
||||||
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
||||||
@@ -122,8 +124,6 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
private var mBreadcrumbAdapter: BreadcrumbAdapter? = null
|
private var mBreadcrumbAdapter: BreadcrumbAdapter? = null
|
||||||
|
|
||||||
private var mSearchMenuItem: MenuItem? = null
|
|
||||||
|
|
||||||
private var mGroupFragment: GroupFragment? = null
|
private var mGroupFragment: GroupFragment? = null
|
||||||
private var mRecyclingBinEnabled = false
|
private var mRecyclingBinEnabled = false
|
||||||
private var mRecyclingBinIsCurrentGroup = false
|
private var mRecyclingBinIsCurrentGroup = false
|
||||||
@@ -182,6 +182,11 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
addSearch()
|
addSearch()
|
||||||
//loadGroup()
|
//loadGroup()
|
||||||
|
|
||||||
|
// Back to previous keyboard
|
||||||
|
if (PreferencesUtil.isKeyboardPreviousSearchEnable(this@GroupActivity)) {
|
||||||
|
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +205,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
finishNodeAction()
|
finishNodeAction()
|
||||||
if (mSearchState == null) {
|
if (mSearchState == null) {
|
||||||
mSearchState = SearchState(searchFiltersView?.searchParameters
|
mSearchState = SearchState(searchFiltersView?.searchParameters
|
||||||
?: SearchParameters(), 0)
|
?: PreferencesUtil.getDefaultSearchParameters(this), 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,20 +407,14 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
val currentGroup = it.group
|
val currentGroup = it.group
|
||||||
mCurrentGroup = currentGroup
|
mCurrentGroup = currentGroup
|
||||||
if (currentGroup.isVirtual) {
|
if (currentGroup.isVirtual) {
|
||||||
val searchParameters = it.searchParameters
|
mSearchState = SearchState(
|
||||||
mSearchState = SearchState(searchParameters, it.showFromPosition)
|
it.searchParameters,
|
||||||
|
it.showFromPosition
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// Main and search groups in activity are managed with another variables
|
// Main and search groups in activity are managed with another variables
|
||||||
// to keep values during orientation
|
// to keep values during orientation
|
||||||
|
|
||||||
// Expand the search view if defined in settings
|
|
||||||
if (mRequestStartupSearch
|
|
||||||
&& PreferencesUtil.automaticallyFocusSearch(this@GroupActivity)) {
|
|
||||||
// To request search only one time
|
|
||||||
mRequestStartupSearch = false
|
|
||||||
mSearchMenuItem?.expandActionView()
|
|
||||||
}
|
|
||||||
|
|
||||||
loadingView?.hideByFading()
|
loadingView?.hideByFading()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -719,11 +718,16 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
val stringQuery = intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
|
val stringQuery = intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
|
||||||
intent.action = Intent.ACTION_DEFAULT
|
intent.action = Intent.ACTION_DEFAULT
|
||||||
intent.removeExtra(SearchManager.QUERY)
|
intent.removeExtra(SearchManager.QUERY)
|
||||||
mSearchState = SearchState(SearchParameters().apply {
|
mSearchState = SearchState(PreferencesUtil.getDefaultSearchParameters(this).apply {
|
||||||
searchQuery = stringQuery
|
searchQuery = stringQuery
|
||||||
}, mSearchState?.firstVisibleItem ?: 0)
|
}, mSearchState?.firstVisibleItem ?: 0)
|
||||||
|
} else if (mRequestStartupSearch
|
||||||
|
&& PreferencesUtil.automaticallyFocusSearch(this@GroupActivity)) {
|
||||||
|
// Expand the search view if defined in settings
|
||||||
|
// To request search only one time
|
||||||
|
mRequestStartupSearch = false
|
||||||
|
addSearch()
|
||||||
}
|
}
|
||||||
loadGroup()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -884,10 +888,9 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
private fun entrySelectedForKeyboardSelection(database: Database, entry: Entry) {
|
private fun entrySelectedForKeyboardSelection(database: Database, entry: Entry) {
|
||||||
reloadCurrentGroup()
|
reloadCurrentGroup()
|
||||||
// Populate Magikeyboard with entry
|
// Populate Magikeyboard with entry
|
||||||
populateKeyboardAndMoveAppToBackground(
|
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||||
this,
|
this,
|
||||||
entry.getEntryInfo(database),
|
entry.getEntryInfo(database)
|
||||||
intent
|
|
||||||
)
|
)
|
||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
}
|
}
|
||||||
@@ -1126,6 +1129,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
finishNodeAction()
|
finishNodeAction()
|
||||||
searchView?.setOnQueryTextListener(null)
|
searchView?.setOnQueryTextListener(null)
|
||||||
|
searchFiltersView?.saveSearchParameters()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addSearchQueryInSearchView(searchQuery: String) {
|
private fun addSearchQueryInSearchView(searchQuery: String) {
|
||||||
@@ -1173,7 +1177,6 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
// Get the SearchView and set the searchable configuration
|
// Get the SearchView and set the searchable configuration
|
||||||
menu.findItem(R.id.menu_search)?.let {
|
menu.findItem(R.id.menu_search)?.let {
|
||||||
mLockSearchListeners = true
|
mLockSearchListeners = true
|
||||||
mSearchMenuItem = it
|
|
||||||
it.setOnActionExpandListener(mOnSearchActionExpandListener)
|
it.setOnActionExpandListener(mOnSearchActionExpandListener)
|
||||||
searchView = it.actionView as SearchView?
|
searchView = it.actionView as SearchView?
|
||||||
searchView?.apply {
|
searchView?.apply {
|
||||||
@@ -1605,50 +1608,31 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||||
{
|
{
|
||||||
GroupActivity.launch(
|
// Default action
|
||||||
|
launch(
|
||||||
activity,
|
activity,
|
||||||
database,
|
database,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
{ searchInfo ->
|
||||||
SearchHelper.checkAutoSearchInfo(activity,
|
// Search action
|
||||||
|
if (database.loaded) {
|
||||||
|
launchForSearchResult(activity,
|
||||||
database,
|
database,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
{ _, _ ->
|
true)
|
||||||
// Response is build
|
onLaunchActivitySpecialMode()
|
||||||
GroupActivity.launchForSearchResult(activity,
|
} else {
|
||||||
database,
|
// Simply close if database not opened
|
||||||
searchInfo,
|
onCancelSpecialMode()
|
||||||
true)
|
}
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Here no search info found
|
|
||||||
if (database.isReadOnly) {
|
|
||||||
GroupActivity.launchForSearchResult(activity,
|
|
||||||
database,
|
|
||||||
searchInfo,
|
|
||||||
false)
|
|
||||||
} else {
|
|
||||||
GroupActivity.launchForSaveResult(activity,
|
|
||||||
database,
|
|
||||||
searchInfo,
|
|
||||||
false)
|
|
||||||
}
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Simply close if database not opened, normally not happened
|
|
||||||
onCancelSpecialMode()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
{ searchInfo ->
|
||||||
// Save info used with OTP
|
// Save info
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
if (!database.isReadOnly) {
|
if (!database.isReadOnly) {
|
||||||
GroupActivity.launchForSaveResult(
|
launchForSaveResult(
|
||||||
activity,
|
activity,
|
||||||
database,
|
database,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
@@ -1667,28 +1651,33 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
{ searchInfo ->
|
||||||
|
// Keyboard selection
|
||||||
SearchHelper.checkAutoSearchInfo(activity,
|
SearchHelper.checkAutoSearchInfo(activity,
|
||||||
database,
|
database,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
{ _, items ->
|
{ _, items ->
|
||||||
// Response is build
|
MagikeyboardService.performSelection(
|
||||||
if (items.size == 1) {
|
items,
|
||||||
populateKeyboardAndMoveAppToBackground(activity,
|
{ entryInfo ->
|
||||||
items[0],
|
// Keyboard populated
|
||||||
activity.intent)
|
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||||
onValidateSpecialMode()
|
activity,
|
||||||
} else {
|
entryInfo
|
||||||
// Select the one we want
|
)
|
||||||
GroupActivity.launchForKeyboardSelectionResult(activity,
|
onValidateSpecialMode()
|
||||||
database,
|
},
|
||||||
searchInfo,
|
{ autoSearch ->
|
||||||
true)
|
launchForKeyboardSelectionResult(activity,
|
||||||
onLaunchActivitySpecialMode()
|
database,
|
||||||
}
|
searchInfo,
|
||||||
|
autoSearch)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Here no search info found, disable auto search
|
// Here no search info found, disable auto search
|
||||||
GroupActivity.launchForKeyboardSelectionResult(activity,
|
launchForKeyboardSelectionResult(activity,
|
||||||
database,
|
database,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
false)
|
||||||
@@ -1701,6 +1690,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
{ searchInfo, autofillComponent ->
|
{ searchInfo, autofillComponent ->
|
||||||
|
// Autofill selection
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
SearchHelper.checkAutoSearchInfo(activity,
|
SearchHelper.checkAutoSearchInfo(activity,
|
||||||
database,
|
database,
|
||||||
@@ -1712,7 +1702,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Here no search info found, disable auto search
|
// Here no search info found, disable auto search
|
||||||
GroupActivity.launchForAutofillResult(activity,
|
launchForAutofillResult(activity,
|
||||||
database,
|
database,
|
||||||
autofillActivityResultLauncher,
|
autofillActivityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
@@ -1730,20 +1720,21 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ registerInfo ->
|
{ registerInfo ->
|
||||||
|
// Autofill registration
|
||||||
if (!database.isReadOnly) {
|
if (!database.isReadOnly) {
|
||||||
SearchHelper.checkAutoSearchInfo(activity,
|
SearchHelper.checkAutoSearchInfo(activity,
|
||||||
database,
|
database,
|
||||||
registerInfo?.searchInfo,
|
registerInfo?.searchInfo,
|
||||||
{ _, _ ->
|
{ _, _ ->
|
||||||
// No auto search, it's a registration
|
// No auto search, it's a registration
|
||||||
GroupActivity.launchForRegistration(activity,
|
launchForRegistration(activity,
|
||||||
database,
|
database,
|
||||||
registerInfo)
|
registerInfo)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Here no search info found, disable auto search
|
// Here no search info found, disable auto search
|
||||||
GroupActivity.launchForRegistration(activity,
|
launchForRegistration(activity,
|
||||||
database,
|
database,
|
||||||
registerInfo)
|
registerInfo)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.fragment.app.commit
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.fragments.KeyGeneratorFragment
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
||||||
|
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
||||||
|
|
||||||
|
class KeyGeneratorActivity : DatabaseLockActivity() {
|
||||||
|
|
||||||
|
private lateinit var toolbar: Toolbar
|
||||||
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
|
private lateinit var validationButton: View
|
||||||
|
private var lockView: View? = null
|
||||||
|
|
||||||
|
private val keyGeneratorViewModel: KeyGeneratorViewModel by viewModels()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_key_generator)
|
||||||
|
|
||||||
|
toolbar = findViewById(R.id.toolbar)
|
||||||
|
toolbar.title = " "
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
|
coordinatorLayout = findViewById(R.id.key_generator_coordinator)
|
||||||
|
|
||||||
|
lockView = findViewById(R.id.lock_button)
|
||||||
|
lockView?.setOnClickListener {
|
||||||
|
lockAndExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
validationButton = findViewById(R.id.key_generator_validation)
|
||||||
|
validationButton.setOnClickListener {
|
||||||
|
keyGeneratorViewModel.validateKeyGenerated()
|
||||||
|
}
|
||||||
|
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
replace(R.id.key_generator_fragment, KeyGeneratorFragment.getInstance(
|
||||||
|
// Default selection tab
|
||||||
|
KeyGeneratorFragment.KeyGeneratorTab.PASSWORD
|
||||||
|
), KEY_GENERATED_FRAGMENT_TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyGeneratorViewModel.keyGenerated.observe(this) { keyGenerated ->
|
||||||
|
setResult(Activity.RESULT_OK, Intent().apply {
|
||||||
|
putExtra(KEY_GENERATED, keyGenerated)
|
||||||
|
})
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun viewToInvalidateTimeout(): View? {
|
||||||
|
return findViewById<ViewGroup>(R.id.key_generator_container)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// Show the lock button
|
||||||
|
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Padding if lock button visible
|
||||||
|
toolbar.updateLockPaddingLeft()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
super.onCreateOptionsMenu(menu)
|
||||||
|
menuInflater.inflate(R.menu.key_generator, menu)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
onBackPressed()
|
||||||
|
}
|
||||||
|
R.id.menu_generate -> {
|
||||||
|
keyGeneratorViewModel.requireKeyGeneration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
setResult(Activity.RESULT_CANCELED, Intent())
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_GENERATED = "KEY_GENERATED"
|
||||||
|
private const val KEY_GENERATED_FRAGMENT_TAG = "KEY_GENERATED_FRAGMENT_TAG"
|
||||||
|
|
||||||
|
fun registerForGeneratedKeyResult(activity: FragmentActivity,
|
||||||
|
keyGeneratedListener: (String?) -> Unit): ActivityResultLauncher<Intent> {
|
||||||
|
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
keyGeneratedListener.invoke(
|
||||||
|
result.data?.getStringExtra(KEY_GENERATED)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
keyGeneratedListener.invoke(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launch(context: FragmentActivity,
|
||||||
|
resultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
|
// Create an instance to return the picker icon
|
||||||
|
resultLauncher.launch(
|
||||||
|
Intent(context, KeyGeneratorActivity::class.java)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activity to select entry in database and populate it in Magikeyboard
|
|
||||||
*/
|
|
||||||
class MagikeyboardLauncherActivity : DatabaseModeActivity() {
|
|
||||||
|
|
||||||
override fun applyCustomStyle(): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun finishActivityIfReloadRequested(): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
|
||||||
database,
|
|
||||||
null,
|
|
||||||
{ _, _ ->
|
|
||||||
// Not called
|
|
||||||
// if items found directly returns before calling this activity
|
|
||||||
},
|
|
||||||
{ openedDatabase ->
|
|
||||||
// Select if not found
|
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this, openedDatabase)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Pass extra to get entry
|
|
||||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -34,11 +34,13 @@ import android.view.ViewGroup
|
|||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
@@ -52,6 +54,7 @@ import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
|||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
||||||
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||||
@@ -63,6 +66,7 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
|||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
@@ -79,6 +83,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
// Views
|
// Views
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
private var filenameView: TextView? = null
|
private var filenameView: TextView? = null
|
||||||
|
private var advancedUnlockButton: View? = null
|
||||||
private var mainCredentialView: MainCredentialView? = null
|
private var mainCredentialView: MainCredentialView? = null
|
||||||
private var confirmButtonView: Button? = null
|
private var confirmButtonView: Button? = null
|
||||||
private var infoContainerView: ViewGroup? = null
|
private var infoContainerView: ViewGroup? = null
|
||||||
@@ -116,6 +121,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
filenameView = findViewById(R.id.filename)
|
filenameView = findViewById(R.id.filename)
|
||||||
|
advancedUnlockButton = findViewById(R.id.activity_password_advanced_unlock_button)
|
||||||
mainCredentialView = findViewById(R.id.activity_password_credentials)
|
mainCredentialView = findViewById(R.id.activity_password_credentials)
|
||||||
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
||||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||||
@@ -143,6 +149,11 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
getUriFromIntent(intent)
|
getUriFromIntent(intent)
|
||||||
|
|
||||||
// Init Biometric elements
|
// Init Biometric elements
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
advancedUnlockButton?.setOnClickListener {
|
||||||
|
startActivity(Intent(this, SettingsAdvancedUnlockActivity::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
advancedUnlockFragment = supportFragmentManager
|
advancedUnlockFragment = supportFragmentManager
|
||||||
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
|
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
|
||||||
if (advancedUnlockFragment == null) {
|
if (advancedUnlockFragment == null) {
|
||||||
@@ -227,6 +238,15 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
if (database != null) {
|
if (database != null) {
|
||||||
|
// Trying to load another database
|
||||||
|
if (mDatabaseFileUri != null
|
||||||
|
&& database.fileUri != null
|
||||||
|
&& mDatabaseFileUri != database.fileUri) {
|
||||||
|
Toast.makeText(this,
|
||||||
|
R.string.warning_database_already_opened,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
launchGroupActivityIfLoaded(database)
|
launchGroupActivityIfLoaded(database)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -539,7 +559,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mSpecialMode == SpecialMode.DEFAULT) {
|
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||||
MenuUtil.defaultMenuInflater(inflater, menu)
|
MenuUtil.defaultMenuInflater(this, inflater, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
@@ -588,15 +608,29 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
{
|
{
|
||||||
performedNextEducation(menu)
|
performedNextEducation(menu)
|
||||||
})
|
})
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||||
|
&& !readOnlyEducationPerformed) {
|
||||||
|
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(this)
|
||||||
|
if ((biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||||
|
&& advancedUnlockButton != null) {
|
||||||
|
mPasswordActivityEducation.checkAndPerformedBiometricEducation(
|
||||||
|
advancedUnlockButton!!,
|
||||||
|
{
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
this,
|
||||||
|
SettingsAdvancedUnlockActivity::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
advancedUnlockFragment?.performEducation(mPasswordActivityEducation,
|
})
|
||||||
readOnlyEducationPerformed,
|
}
|
||||||
{
|
}
|
||||||
performedNextEducation(menu)
|
} catch (ignored: Exception) {}
|
||||||
},
|
|
||||||
{
|
|
||||||
performedNextEducation(menu)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,224 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.*
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.database.element.Field
|
|
||||||
import com.kunzisoft.keepass.password.PasswordGenerator
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
|
||||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
|
||||||
|
|
||||||
class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
|
|
||||||
|
|
||||||
private var mListener: GeneratePasswordListener? = null
|
|
||||||
|
|
||||||
private var root: View? = null
|
|
||||||
private var lengthTextView: EditText? = null
|
|
||||||
private var passwordInputLayoutView: TextInputLayout? = null
|
|
||||||
private var passwordView: EditText? = null
|
|
||||||
|
|
||||||
private var mPasswordField: Field? = null
|
|
||||||
|
|
||||||
private var uppercaseBox: CompoundButton? = null
|
|
||||||
private var lowercaseBox: CompoundButton? = null
|
|
||||||
private var digitsBox: CompoundButton? = null
|
|
||||||
private var minusBox: CompoundButton? = null
|
|
||||||
private var underlineBox: CompoundButton? = null
|
|
||||||
private var spaceBox: CompoundButton? = null
|
|
||||||
private var specialsBox: CompoundButton? = null
|
|
||||||
private var bracketsBox: CompoundButton? = null
|
|
||||||
private var extendedBox: CompoundButton? = null
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
try {
|
|
||||||
mListener = context as GeneratePasswordListener
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
throw ClassCastException(context.toString()
|
|
||||||
+ " must implement " + GeneratePasswordListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetach() {
|
|
||||||
mListener = null
|
|
||||||
super.onDetach()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
activity?.let { activity ->
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
|
||||||
val inflater = activity.layoutInflater
|
|
||||||
root = inflater.inflate(R.layout.fragment_generate_password, null)
|
|
||||||
|
|
||||||
passwordInputLayoutView = root?.findViewById(R.id.password_input_layout)
|
|
||||||
passwordView = root?.findViewById(R.id.password)
|
|
||||||
passwordView?.applyFontVisibility()
|
|
||||||
val passwordCopyView: ImageView? = root?.findViewById(R.id.password_copy_button)
|
|
||||||
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(activity))
|
|
||||||
View.VISIBLE else View.GONE
|
|
||||||
val clipboardHelper = ClipboardHelper(activity)
|
|
||||||
passwordCopyView?.setOnClickListener {
|
|
||||||
clipboardHelper.timeoutCopyToClipboard(passwordView!!.text.toString(),
|
|
||||||
getString(R.string.copy_field,
|
|
||||||
getString(R.string.entry_password)))
|
|
||||||
}
|
|
||||||
|
|
||||||
lengthTextView = root?.findViewById(R.id.length)
|
|
||||||
|
|
||||||
uppercaseBox = root?.findViewById(R.id.cb_uppercase)
|
|
||||||
lowercaseBox = root?.findViewById(R.id.cb_lowercase)
|
|
||||||
digitsBox = root?.findViewById(R.id.cb_digits)
|
|
||||||
minusBox = root?.findViewById(R.id.cb_minus)
|
|
||||||
underlineBox = root?.findViewById(R.id.cb_underline)
|
|
||||||
spaceBox = root?.findViewById(R.id.cb_space)
|
|
||||||
specialsBox = root?.findViewById(R.id.cb_specials)
|
|
||||||
bracketsBox = root?.findViewById(R.id.cb_brackets)
|
|
||||||
extendedBox = root?.findViewById(R.id.cb_extended)
|
|
||||||
|
|
||||||
mPasswordField = arguments?.getParcelable(KEY_PASSWORD_FIELD)
|
|
||||||
|
|
||||||
assignDefaultCharacters()
|
|
||||||
|
|
||||||
val seekBar = root?.findViewById<SeekBar>(R.id.seekbar_length)
|
|
||||||
seekBar?.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
||||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
|
||||||
lengthTextView?.setText(progress.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
|
||||||
|
|
||||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
|
||||||
})
|
|
||||||
|
|
||||||
context?.let { context ->
|
|
||||||
seekBar?.progress = PreferencesUtil.getDefaultPasswordLength(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
root?.findViewById<Button>(R.id.generate_password_button)
|
|
||||||
?.setOnClickListener { fillPassword() }
|
|
||||||
|
|
||||||
builder.setView(root)
|
|
||||||
.setPositiveButton(R.string.accept) { _, _ ->
|
|
||||||
mPasswordField?.let { passwordField ->
|
|
||||||
passwordView?.text?.toString()?.let { passwordValue ->
|
|
||||||
passwordField.protectedValue.stringValue = passwordValue
|
|
||||||
}
|
|
||||||
mListener?.acceptPassword(passwordField)
|
|
||||||
}
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
mPasswordField?.let { passwordField ->
|
|
||||||
mListener?.cancelPassword(passwordField)
|
|
||||||
}
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pre-populate a password to possibly save the user a few clicks
|
|
||||||
fillPassword()
|
|
||||||
|
|
||||||
return builder.create()
|
|
||||||
}
|
|
||||||
return super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun assignDefaultCharacters() {
|
|
||||||
uppercaseBox?.isChecked = false
|
|
||||||
lowercaseBox?.isChecked = false
|
|
||||||
digitsBox?.isChecked = false
|
|
||||||
minusBox?.isChecked = false
|
|
||||||
underlineBox?.isChecked = false
|
|
||||||
spaceBox?.isChecked = false
|
|
||||||
specialsBox?.isChecked = false
|
|
||||||
bracketsBox?.isChecked = false
|
|
||||||
extendedBox?.isChecked = false
|
|
||||||
|
|
||||||
context?.let { context ->
|
|
||||||
PreferencesUtil.getDefaultPasswordCharacters(context)?.let { charSet ->
|
|
||||||
for (passwordChar in charSet) {
|
|
||||||
when (passwordChar) {
|
|
||||||
getString(R.string.value_password_uppercase) -> uppercaseBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_lowercase) -> lowercaseBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_digits) -> digitsBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_minus) -> minusBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_underline) -> underlineBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_space) -> spaceBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_special) -> specialsBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_brackets) -> bracketsBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_extended) -> extendedBox?.isChecked = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fillPassword() {
|
|
||||||
root?.findViewById<EditText>(R.id.password)?.setText(generatePassword())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generatePassword(): String {
|
|
||||||
var password = ""
|
|
||||||
try {
|
|
||||||
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
|
|
||||||
password = PasswordGenerator(resources).generatePassword(length,
|
|
||||||
uppercaseBox?.isChecked == true,
|
|
||||||
lowercaseBox?.isChecked == true,
|
|
||||||
digitsBox?.isChecked == true,
|
|
||||||
minusBox?.isChecked == true,
|
|
||||||
underlineBox?.isChecked == true,
|
|
||||||
spaceBox?.isChecked == true,
|
|
||||||
specialsBox?.isChecked == true,
|
|
||||||
bracketsBox?.isChecked == true,
|
|
||||||
extendedBox?.isChecked == true)
|
|
||||||
passwordInputLayoutView?.error = null
|
|
||||||
} catch (e: NumberFormatException) {
|
|
||||||
passwordInputLayoutView?.error = getString(R.string.error_wrong_length)
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
passwordInputLayoutView?.error = e.message
|
|
||||||
}
|
|
||||||
|
|
||||||
return password
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GeneratePasswordListener {
|
|
||||||
fun acceptPassword(passwordField: Field)
|
|
||||||
fun cancelPassword(passwordField: Field)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val KEY_PASSWORD_FIELD = "KEY_PASSWORD_FIELD"
|
|
||||||
|
|
||||||
fun getInstance(field: Field): GeneratePasswordDialogFragment {
|
|
||||||
return GeneratePasswordDialogFragment().apply {
|
|
||||||
arguments = Bundle().apply {
|
|
||||||
putParcelable(KEY_PASSWORD_FIELD, field)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -36,8 +36,11 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
|
import com.kunzisoft.keepass.password.PasswordEntropy
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
|
import com.kunzisoft.keepass.view.PassKeyView
|
||||||
|
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||||
|
|
||||||
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
@@ -48,8 +51,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
|
|
||||||
private var passwordCheckBox: CompoundButton? = null
|
private var passwordCheckBox: CompoundButton? = null
|
||||||
|
|
||||||
private var passwordTextInputLayout: TextInputLayout? = null
|
private var passKeyView: PassKeyView? = null
|
||||||
private var passwordView: TextView? = null
|
|
||||||
private var passwordRepeatTextInputLayout: TextInputLayout? = null
|
private var passwordRepeatTextInputLayout: TextInputLayout? = null
|
||||||
private var passwordRepeatView: TextView? = null
|
private var passwordRepeatView: TextView? = null
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
private var mListener: AssignMainCredentialDialogListener? = null
|
private var mListener: AssignMainCredentialDialogListener? = null
|
||||||
|
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
private var mPasswordEntropyCalculator: PasswordEntropy? = null
|
||||||
|
|
||||||
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
||||||
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
||||||
@@ -100,6 +103,13 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
super.onDetach()
|
super.onDetach()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Create the password entropy object
|
||||||
|
mPasswordEntropyCalculator = PasswordEntropy()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
|
|
||||||
@@ -123,10 +133,10 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
||||||
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
|
passKeyView = rootView?.findViewById(R.id.password_view)
|
||||||
passwordView = rootView?.findViewById(R.id.pass_password)
|
|
||||||
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
|
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
|
||||||
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
|
passwordRepeatView = rootView?.findViewById(R.id.password_confirmation)
|
||||||
|
passwordRepeatView?.applyFontVisibility()
|
||||||
|
|
||||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||||
@@ -162,7 +172,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
if (allowNoMasterKey)
|
if (allowNoMasterKey)
|
||||||
showNoKeyConfirmationDialog()
|
showNoKeyConfirmationDialog()
|
||||||
else {
|
else {
|
||||||
passwordTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
|
passwordRepeatTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!error) {
|
if (!error) {
|
||||||
@@ -194,22 +204,22 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
// To check checkboxes if a text is present
|
// To check checkboxes if a text is present
|
||||||
passwordView?.addTextChangedListener(passwordTextWatcher)
|
passKeyView?.addTextChangedListener(passwordTextWatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
passwordView?.removeTextChangedListener(passwordTextWatcher)
|
passKeyView?.removeTextChangedListener(passwordTextWatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyPassword(): Boolean {
|
private fun verifyPassword(): Boolean {
|
||||||
var error = false
|
var error = false
|
||||||
if (passwordCheckBox != null
|
if (passwordCheckBox != null
|
||||||
&& passwordCheckBox!!.isChecked
|
&& passwordCheckBox!!.isChecked
|
||||||
&& passwordView != null
|
&& passKeyView != null
|
||||||
&& passwordRepeatView != null) {
|
&& passwordRepeatView != null) {
|
||||||
mMasterPassword = passwordView!!.text.toString()
|
mMasterPassword = passKeyView!!.passwordString
|
||||||
val confPassword = passwordRepeatView!!.text.toString()
|
val confPassword = passwordRepeatView!!.text.toString()
|
||||||
|
|
||||||
// Verify that passwords match
|
// Verify that passwords match
|
||||||
|
|||||||
@@ -204,9 +204,10 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
|||||||
android.R.layout.simple_spinner_item, mHotpTokenTypeArray!!).apply {
|
android.R.layout.simple_spinner_item, mHotpTokenTypeArray!!).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
// Proprietary only on closed and full version
|
// Proprietary only on full version
|
||||||
mTotpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
|
mTotpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
|
||||||
BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION)
|
UriUtil.contributingUser(activity)
|
||||||
|
)
|
||||||
totpTokenTypeAdapter = ArrayAdapter(activity,
|
totpTokenTypeAdapter = ArrayAdapter(activity,
|
||||||
android.R.layout.simple_spinner_item, mTotpTokenTypeArray!!).apply {
|
android.R.layout.simple_spinner_item, mTotpTokenTypeArray!!).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import android.text.SpannableStringBuilder
|
|||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
@@ -40,22 +39,12 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
|||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|
||||||
val stringBuilder = SpannableStringBuilder()
|
val stringBuilder = SpannableStringBuilder()
|
||||||
if (BuildConfig.CLOSED_STORE) {
|
if (UriUtil.contributingUser(activity)) {
|
||||||
if (BuildConfig.FULL_VERSION) {
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_thanks), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_thanks), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
.append(HtmlCompat.fromHtml(getString(R.string.html_rose), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_rose), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_work_hard), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
|
||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_work_hard), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_upgrade), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_upgrade), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
|
||||||
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
|
|
||||||
} else {
|
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
|
||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_buy_pro), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
|
|
||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
|
||||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
|
||||||
UriUtil.gotoUrl(requireContext(), R.string.app_pro_url)
|
|
||||||
}
|
|
||||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
@@ -25,6 +24,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
|||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
import com.kunzisoft.keepass.view.TemplateView
|
import com.kunzisoft.keepass.view.TemplateView
|
||||||
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
import com.kunzisoft.keepass.view.showByFading
|
import com.kunzisoft.keepass.view.showByFading
|
||||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -32,6 +32,9 @@ import java.util.*
|
|||||||
class EntryFragment: DatabaseFragment() {
|
class EntryFragment: DatabaseFragment() {
|
||||||
|
|
||||||
private lateinit var rootView: View
|
private lateinit var rootView: View
|
||||||
|
private lateinit var mainSection: View
|
||||||
|
private lateinit var advancedSection: View
|
||||||
|
|
||||||
private lateinit var templateView: TemplateView
|
private lateinit var templateView: TemplateView
|
||||||
|
|
||||||
private lateinit var creationDateView: TextView
|
private lateinit var creationDateView: TextView
|
||||||
@@ -72,6 +75,10 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
view.isVisible = false
|
view.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mainSection = view.findViewById(R.id.entry_section_main)
|
||||||
|
advancedSection = view.findViewById(R.id.entry_section_advanced)
|
||||||
|
|
||||||
templateView = view.findViewById(R.id.entry_template)
|
templateView = view.findViewById(R.id.entry_template)
|
||||||
loadTemplateSettings()
|
loadTemplateSettings()
|
||||||
|
|
||||||
@@ -111,6 +118,19 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mEntryViewModel.sectionSelected.observe(viewLifecycleOwner) { entrySection ->
|
||||||
|
when (entrySection ?: EntryViewModel.EntrySection.MAIN) {
|
||||||
|
EntryViewModel.EntrySection.MAIN -> {
|
||||||
|
mainSection.showByFading()
|
||||||
|
advancedSection.hideByFading()
|
||||||
|
}
|
||||||
|
EntryViewModel.EntrySection.ADVANCED -> {
|
||||||
|
mainSection.hideByFading()
|
||||||
|
advancedSection.showByFading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
|||||||
@@ -26,14 +26,14 @@ class IconPickerFragment : DatabaseFragment() {
|
|||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
return inflater.inflate(R.layout.fragment_icon_picker, container, false)
|
return inflater.inflate(R.layout.fragment_tabs_pagination, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
viewPager = view.findViewById(R.id.icon_picker_pager)
|
viewPager = view.findViewById(R.id.tabs_view_pager)
|
||||||
tabLayout = view.findViewById(R.id.icon_picker_tabs)
|
tabLayout = view.findViewById(R.id.tabs_layout)
|
||||||
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||||
|
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
|
import com.google.android.material.tabs.TabLayout
|
||||||
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.adapters.KeyGeneratorPagerAdapter
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
||||||
|
|
||||||
|
class KeyGeneratorFragment : DatabaseFragment() {
|
||||||
|
|
||||||
|
private var keyGeneratorPagerAdapter: KeyGeneratorPagerAdapter? = null
|
||||||
|
private lateinit var viewPager: ViewPager2
|
||||||
|
private lateinit var tabLayout: TabLayout
|
||||||
|
|
||||||
|
private val mKeyGeneratorViewModel: KeyGeneratorViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private var mSelectedTab = KeyGeneratorTab.PASSWORD
|
||||||
|
private var mOnPageChangeCallback: ViewPager2.OnPageChangeCallback = object:
|
||||||
|
ViewPager2.OnPageChangeCallback() {
|
||||||
|
override fun onPageSelected(position: Int) {
|
||||||
|
super.onPageSelected(position)
|
||||||
|
mSelectedTab = KeyGeneratorTab.getKeyGeneratorTabByPosition(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
return inflater.inflate(R.layout.fragment_tabs_pagination, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
keyGeneratorPagerAdapter = KeyGeneratorPagerAdapter(this, )
|
||||||
|
viewPager = view.findViewById(R.id.tabs_view_pager)
|
||||||
|
tabLayout = view.findViewById(R.id.tabs_layout)
|
||||||
|
viewPager.adapter = keyGeneratorPagerAdapter
|
||||||
|
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
||||||
|
tab.text = getString(KeyGeneratorTab.getKeyGeneratorTabByPosition(position).stringId)
|
||||||
|
}.attach()
|
||||||
|
viewPager.registerOnPageChangeCallback(mOnPageChangeCallback)
|
||||||
|
|
||||||
|
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||||
|
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(PASSWORD_TAB_ARG)) {
|
||||||
|
viewPager.currentItem = getInt(PASSWORD_TAB_ARG)
|
||||||
|
}
|
||||||
|
remove(PASSWORD_TAB_ARG)
|
||||||
|
}
|
||||||
|
|
||||||
|
mKeyGeneratorViewModel.requireKeyGeneration.observe(viewLifecycleOwner) {
|
||||||
|
when (mSelectedTab) {
|
||||||
|
KeyGeneratorTab.PASSWORD -> {
|
||||||
|
mKeyGeneratorViewModel.requirePasswordGeneration()
|
||||||
|
}
|
||||||
|
KeyGeneratorTab.PASSPHRASE -> {
|
||||||
|
mKeyGeneratorViewModel.requirePassphraseGeneration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mKeyGeneratorViewModel.keyGeneratedValidated.observe(viewLifecycleOwner) {
|
||||||
|
when (mSelectedTab) {
|
||||||
|
KeyGeneratorTab.PASSWORD -> {
|
||||||
|
mKeyGeneratorViewModel.validatePasswordGenerated()
|
||||||
|
}
|
||||||
|
KeyGeneratorTab.PASSPHRASE -> {
|
||||||
|
mKeyGeneratorViewModel.validatePassphraseGenerated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
viewPager.unregisterOnPageChangeCallback(mOnPageChangeCallback)
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
// Nothing here
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class KeyGeneratorTab(@StringRes val stringId: Int) {
|
||||||
|
PASSWORD(R.string.password), PASSPHRASE(R.string.passphrase);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getKeyGeneratorTabByPosition(position: Int): KeyGeneratorTab {
|
||||||
|
return when (position) {
|
||||||
|
0 -> PASSWORD
|
||||||
|
else -> PASSPHRASE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val PASSWORD_TAB_ARG = "PASSWORD_TAB_ARG"
|
||||||
|
|
||||||
|
fun getInstance(keyGeneratorTab: KeyGeneratorTab): KeyGeneratorFragment {
|
||||||
|
val fragment = KeyGeneratorFragment()
|
||||||
|
fragment.arguments = Bundle().apply {
|
||||||
|
putInt(PASSWORD_TAB_ARG, keyGeneratorTab.ordinal)
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.*
|
||||||
|
import androidx.core.widget.doOnTextChanged
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.google.android.material.slider.Slider
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.password.PassphraseGenerator
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
|
import com.kunzisoft.keepass.view.PassKeyView
|
||||||
|
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
||||||
|
|
||||||
|
class PassphraseGeneratorFragment : DatabaseFragment() {
|
||||||
|
|
||||||
|
private lateinit var passKeyView: PassKeyView
|
||||||
|
|
||||||
|
private lateinit var sliderWordCount: Slider
|
||||||
|
private lateinit var wordCountText: EditText
|
||||||
|
private lateinit var charactersCountText: TextView
|
||||||
|
private lateinit var wordSeparator: EditText
|
||||||
|
private lateinit var wordCaseSpinner: Spinner
|
||||||
|
|
||||||
|
private var minSliderWordCount: Int = 0
|
||||||
|
private var maxSliderWordCount: Int = 0
|
||||||
|
private var wordCaseAdapter: ArrayAdapter<String>? = null
|
||||||
|
|
||||||
|
private val mKeyGeneratorViewModel: KeyGeneratorViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?): View {
|
||||||
|
return inflater.inflate(R.layout.fragment_generate_passphrase, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
passKeyView = view.findViewById(R.id.passphrase_view)
|
||||||
|
val passphraseCopyView: ImageView? = view.findViewById(R.id.passphrase_copy_button)
|
||||||
|
sliderWordCount = view.findViewById(R.id.slider_word_count)
|
||||||
|
wordCountText = view.findViewById(R.id.word_count)
|
||||||
|
charactersCountText = view.findViewById(R.id.character_count)
|
||||||
|
wordSeparator = view.findViewById(R.id.word_separator)
|
||||||
|
wordCaseSpinner = view.findViewById(R.id.word_case)
|
||||||
|
|
||||||
|
minSliderWordCount = resources.getInteger(R.integer.passphrase_generator_word_count_min)
|
||||||
|
maxSliderWordCount = resources.getInteger(R.integer.passphrase_generator_word_count_max)
|
||||||
|
|
||||||
|
contextThemed?.let { context ->
|
||||||
|
passphraseCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(context))
|
||||||
|
View.VISIBLE else View.GONE
|
||||||
|
val clipboardHelper = ClipboardHelper(context)
|
||||||
|
passphraseCopyView?.setOnClickListener {
|
||||||
|
clipboardHelper.timeoutCopyToClipboard(passKeyView.passwordString,
|
||||||
|
getString(R.string.copy_field,
|
||||||
|
getString(R.string.entry_password)))
|
||||||
|
}
|
||||||
|
|
||||||
|
wordCaseAdapter = ArrayAdapter(context,
|
||||||
|
android.R.layout.simple_spinner_item, resources.getStringArray(R.array.word_case_array)).apply {
|
||||||
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
|
}
|
||||||
|
wordCaseSpinner.adapter = wordCaseAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSettings()
|
||||||
|
|
||||||
|
var listenSlider = true
|
||||||
|
var listenEditText = true
|
||||||
|
sliderWordCount.addOnChangeListener { _, value, _ ->
|
||||||
|
try {
|
||||||
|
listenEditText = false
|
||||||
|
if (listenSlider) {
|
||||||
|
wordCountText.setText(value.toInt().toString())
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to set the word count value", e)
|
||||||
|
} finally {
|
||||||
|
listenEditText = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sliderWordCount.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
|
||||||
|
// TODO upgrade material-components lib
|
||||||
|
// https://stackoverflow.com/questions/70873160/material-slider-onslidertouchlisteners-methods-can-only-be-called-from-within-t
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
override fun onStartTrackingTouch(slider: Slider) {}
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
override fun onStopTrackingTouch(slider: Slider) {
|
||||||
|
generatePassphrase()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
wordCountText.doOnTextChanged { _, _, _, _ ->
|
||||||
|
if (listenEditText) {
|
||||||
|
try {
|
||||||
|
listenSlider = false
|
||||||
|
setSliderValue(getWordCount())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to get the word count value", e)
|
||||||
|
} finally {
|
||||||
|
listenSlider = true
|
||||||
|
generatePassphrase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wordSeparator.doOnTextChanged { _, _, _, _ ->
|
||||||
|
generatePassphrase()
|
||||||
|
}
|
||||||
|
wordCaseSpinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
|
generatePassphrase()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
generatePassphrase()
|
||||||
|
|
||||||
|
mKeyGeneratorViewModel.passphraseGeneratedValidated.observe(viewLifecycleOwner) {
|
||||||
|
mKeyGeneratorViewModel.setKeyGenerated(passKeyView.passwordString)
|
||||||
|
}
|
||||||
|
|
||||||
|
mKeyGeneratorViewModel.requirePassphraseGeneration.observe(viewLifecycleOwner) {
|
||||||
|
generatePassphrase()
|
||||||
|
}
|
||||||
|
|
||||||
|
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getWordCount(): Int {
|
||||||
|
return try {
|
||||||
|
Integer.valueOf(wordCountText.text.toString())
|
||||||
|
} catch (numberException: NumberFormatException) {
|
||||||
|
minSliderWordCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setWordCount(wordCount: Int) {
|
||||||
|
setSliderValue(wordCount)
|
||||||
|
wordCountText.setText(wordCount.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setSliderValue(value: Int) {
|
||||||
|
when {
|
||||||
|
value < minSliderWordCount -> {
|
||||||
|
sliderWordCount.value = minSliderWordCount.toFloat()
|
||||||
|
}
|
||||||
|
value > maxSliderWordCount -> {
|
||||||
|
sliderWordCount.value = maxSliderWordCount.toFloat()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
sliderWordCount.value = value.toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getWordSeparator(): String {
|
||||||
|
return wordSeparator.text.toString().ifEmpty { " " }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getWordCase(): PassphraseGenerator.WordCase {
|
||||||
|
var wordCase = PassphraseGenerator.WordCase.LOWER_CASE
|
||||||
|
try {
|
||||||
|
wordCase = PassphraseGenerator.WordCase.getByOrdinal(wordCaseSpinner.selectedItemPosition)
|
||||||
|
} catch (caseException: Exception) {
|
||||||
|
Log.e(TAG, "Unable to retrieve the word case", caseException)
|
||||||
|
}
|
||||||
|
return wordCase
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setWordCase(wordCase: PassphraseGenerator.WordCase) {
|
||||||
|
wordCaseSpinner.setSelection(wordCase.ordinal)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSeparator(): String {
|
||||||
|
return wordSeparator.text?.toString() ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setSeparator(separator: String) {
|
||||||
|
wordSeparator.setText(separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generatePassphrase() {
|
||||||
|
var passphrase = ""
|
||||||
|
try {
|
||||||
|
passphrase = PassphraseGenerator().generatePassphrase(
|
||||||
|
getWordCount(),
|
||||||
|
getWordSeparator(),
|
||||||
|
getWordCase())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to generate a passphrase", e)
|
||||||
|
}
|
||||||
|
passKeyView.passwordString = passphrase
|
||||||
|
charactersCountText.text = getString(R.string.character_count, passphrase.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
saveSettings()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveSettings() {
|
||||||
|
context?.let { context ->
|
||||||
|
PreferencesUtil.setDefaultPassphraseWordCount(context, getWordCount())
|
||||||
|
PreferencesUtil.setDefaultPassphraseWordCase(context, getWordCase())
|
||||||
|
PreferencesUtil.setDefaultPassphraseSeparator(context, getSeparator())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadSettings() {
|
||||||
|
context?.let { context ->
|
||||||
|
setWordCount(PreferencesUtil.getDefaultPassphraseWordCount(context))
|
||||||
|
setWordCase(PreferencesUtil.getDefaultPassphraseWordCase(context))
|
||||||
|
setSeparator(PreferencesUtil.getDefaultPassphraseSeparator(context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
// Nothing here
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "PassphraseGnrtrFrgmt"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.core.widget.doOnTextChanged
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.google.android.material.slider.Slider
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
|
import com.kunzisoft.keepass.view.PassKeyView
|
||||||
|
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
||||||
|
|
||||||
|
class PasswordGeneratorFragment : DatabaseFragment() {
|
||||||
|
|
||||||
|
private lateinit var passKeyView: PassKeyView
|
||||||
|
|
||||||
|
private lateinit var sliderLength: Slider
|
||||||
|
private lateinit var lengthEditView: EditText
|
||||||
|
|
||||||
|
private lateinit var uppercaseCompound: CompoundButton
|
||||||
|
private lateinit var lowercaseCompound: CompoundButton
|
||||||
|
private lateinit var digitsCompound: CompoundButton
|
||||||
|
private lateinit var minusCompound: CompoundButton
|
||||||
|
private lateinit var underlineCompound: CompoundButton
|
||||||
|
private lateinit var spaceCompound: CompoundButton
|
||||||
|
private lateinit var specialsCompound: CompoundButton
|
||||||
|
private lateinit var bracketsCompound: CompoundButton
|
||||||
|
private lateinit var extendedCompound: CompoundButton
|
||||||
|
private lateinit var considerCharsEditText: EditText
|
||||||
|
private lateinit var ignoreCharsEditText: EditText
|
||||||
|
private lateinit var atLeastOneCompound: CompoundButton
|
||||||
|
private lateinit var excludeAmbiguousCompound: CompoundButton
|
||||||
|
|
||||||
|
private var minLengthSlider: Int = 0
|
||||||
|
private var maxLengthSlider: Int = 0
|
||||||
|
|
||||||
|
private val mKeyGeneratorViewModel: KeyGeneratorViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?): View {
|
||||||
|
return inflater.inflate(R.layout.fragment_generate_password, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
passKeyView = view.findViewById(R.id.password_view)
|
||||||
|
val passwordCopyView: ImageView? = view.findViewById(R.id.password_copy_button)
|
||||||
|
|
||||||
|
sliderLength = view.findViewById(R.id.slider_length)
|
||||||
|
lengthEditView = view.findViewById(R.id.length)
|
||||||
|
|
||||||
|
uppercaseCompound = view.findViewById(R.id.upperCase_filter)
|
||||||
|
lowercaseCompound = view.findViewById(R.id.lowerCase_filter)
|
||||||
|
digitsCompound = view.findViewById(R.id.digits_filter)
|
||||||
|
minusCompound = view.findViewById(R.id.minus_filter)
|
||||||
|
underlineCompound = view.findViewById(R.id.underline_filter)
|
||||||
|
spaceCompound = view.findViewById(R.id.space_filter)
|
||||||
|
specialsCompound = view.findViewById(R.id.special_filter)
|
||||||
|
bracketsCompound = view.findViewById(R.id.brackets_filter)
|
||||||
|
extendedCompound = view.findViewById(R.id.extendedASCII_filter)
|
||||||
|
considerCharsEditText = view.findViewById(R.id.consider_chars_filter)
|
||||||
|
ignoreCharsEditText = view.findViewById(R.id.ignore_chars_filter)
|
||||||
|
atLeastOneCompound = view.findViewById(R.id.atLeastOne_filter)
|
||||||
|
excludeAmbiguousCompound = view.findViewById(R.id.excludeAmbiguous_filter)
|
||||||
|
|
||||||
|
contextThemed?.let { context ->
|
||||||
|
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(context))
|
||||||
|
View.VISIBLE else View.GONE
|
||||||
|
val clipboardHelper = ClipboardHelper(context)
|
||||||
|
passwordCopyView?.setOnClickListener {
|
||||||
|
clipboardHelper.timeoutCopyToClipboard(passKeyView.passwordString,
|
||||||
|
getString(R.string.copy_field,
|
||||||
|
getString(R.string.entry_password)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
minLengthSlider = resources.getInteger(R.integer.password_generator_length_min)
|
||||||
|
maxLengthSlider = resources.getInteger(R.integer.password_generator_length_max)
|
||||||
|
|
||||||
|
loadSettings()
|
||||||
|
|
||||||
|
uppercaseCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
lowercaseCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
digitsCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
minusCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
underlineCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
spaceCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
specialsCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
bracketsCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
extendedCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
considerCharsEditText.doOnTextChanged { _, _, _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
ignoreCharsEditText.doOnTextChanged { _, _, _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
atLeastOneCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
excludeAmbiguousCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
|
||||||
|
var listenSlider = true
|
||||||
|
var listenEditText = true
|
||||||
|
sliderLength.addOnChangeListener { _, value, _ ->
|
||||||
|
try {
|
||||||
|
listenEditText = false
|
||||||
|
if (listenSlider) {
|
||||||
|
lengthEditView.setText(value.toInt().toString())
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to set the length value", e)
|
||||||
|
} finally {
|
||||||
|
listenEditText = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sliderLength.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
|
||||||
|
// TODO upgrade material-components lib
|
||||||
|
// https://stackoverflow.com/questions/70873160/material-slider-onslidertouchlisteners-methods-can-only-be-called-from-within-t
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
override fun onStartTrackingTouch(slider: Slider) {}
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
override fun onStopTrackingTouch(slider: Slider) {
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
lengthEditView.doOnTextChanged { _, _, _, _ ->
|
||||||
|
if (listenEditText) {
|
||||||
|
try {
|
||||||
|
listenSlider = false
|
||||||
|
setSliderValue(getPasswordLength())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to get the length value", e)
|
||||||
|
} finally {
|
||||||
|
listenSlider = true
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-populate a password to possibly save the user a few clicks
|
||||||
|
generatePassword()
|
||||||
|
|
||||||
|
mKeyGeneratorViewModel.passwordGeneratedValidated.observe(viewLifecycleOwner) {
|
||||||
|
mKeyGeneratorViewModel.setKeyGenerated(passKeyView.passwordString)
|
||||||
|
}
|
||||||
|
|
||||||
|
mKeyGeneratorViewModel.requirePasswordGeneration.observe(viewLifecycleOwner) {
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
|
||||||
|
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPasswordLength(): Int {
|
||||||
|
return try {
|
||||||
|
Integer.valueOf(lengthEditView.text.toString())
|
||||||
|
} catch (numberException: NumberFormatException) {
|
||||||
|
minLengthSlider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setPasswordLength(passwordLength: Int) {
|
||||||
|
setSliderValue(passwordLength)
|
||||||
|
lengthEditView.setText(passwordLength.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getOptions(): Set<String> {
|
||||||
|
val optionsSet = mutableSetOf<String>()
|
||||||
|
if (uppercaseCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_uppercase))
|
||||||
|
if (lowercaseCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_lowercase))
|
||||||
|
if (digitsCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_digits))
|
||||||
|
if (minusCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_minus))
|
||||||
|
if (underlineCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_underline))
|
||||||
|
if (spaceCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_space))
|
||||||
|
if (specialsCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_special))
|
||||||
|
if (bracketsCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_brackets))
|
||||||
|
if (extendedCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_extended))
|
||||||
|
if (atLeastOneCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_atLeastOne))
|
||||||
|
if (excludeAmbiguousCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_excludeAmbiguous))
|
||||||
|
return optionsSet
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setOptions(options: Set<String>) {
|
||||||
|
uppercaseCompound.isChecked = false
|
||||||
|
lowercaseCompound.isChecked = false
|
||||||
|
digitsCompound.isChecked = false
|
||||||
|
minusCompound.isChecked = false
|
||||||
|
underlineCompound.isChecked = false
|
||||||
|
spaceCompound.isChecked = false
|
||||||
|
specialsCompound.isChecked = false
|
||||||
|
bracketsCompound.isChecked = false
|
||||||
|
extendedCompound.isChecked = false
|
||||||
|
atLeastOneCompound.isChecked = false
|
||||||
|
excludeAmbiguousCompound.isChecked = false
|
||||||
|
for (option in options) {
|
||||||
|
when (option) {
|
||||||
|
getString(R.string.value_password_uppercase) -> uppercaseCompound.isChecked = true
|
||||||
|
getString(R.string.value_password_lowercase) -> lowercaseCompound.isChecked = true
|
||||||
|
getString(R.string.value_password_digits) -> digitsCompound.isChecked = true
|
||||||
|
getString(R.string.value_password_minus) -> minusCompound.isChecked = true
|
||||||
|
getString(R.string.value_password_underline) -> underlineCompound.isChecked = true
|
||||||
|
getString(R.string.value_password_space) -> spaceCompound.isChecked = true
|
||||||
|
getString(R.string.value_password_special) -> specialsCompound.isChecked = true
|
||||||
|
getString(R.string.value_password_brackets) -> bracketsCompound.isChecked = true
|
||||||
|
getString(R.string.value_password_extended) -> extendedCompound.isChecked = true
|
||||||
|
getString(R.string.value_password_atLeastOne) -> atLeastOneCompound.isChecked = true
|
||||||
|
getString(R.string.value_password_excludeAmbiguous) -> excludeAmbiguousCompound.isChecked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getConsiderChars(): String {
|
||||||
|
return considerCharsEditText.text.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setConsiderChars(chars: String) {
|
||||||
|
considerCharsEditText.setText(chars)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getIgnoreChars(): String {
|
||||||
|
return ignoreCharsEditText.text.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setIgnoreChars(chars: String) {
|
||||||
|
ignoreCharsEditText.setText(chars)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generatePassword() {
|
||||||
|
var password = ""
|
||||||
|
try {
|
||||||
|
password = PasswordGenerator(resources).generatePassword(getPasswordLength(),
|
||||||
|
uppercaseCompound.isChecked,
|
||||||
|
lowercaseCompound.isChecked,
|
||||||
|
digitsCompound.isChecked,
|
||||||
|
minusCompound.isChecked,
|
||||||
|
underlineCompound.isChecked,
|
||||||
|
spaceCompound.isChecked,
|
||||||
|
specialsCompound.isChecked,
|
||||||
|
bracketsCompound.isChecked,
|
||||||
|
extendedCompound.isChecked,
|
||||||
|
getConsiderChars(),
|
||||||
|
getIgnoreChars(),
|
||||||
|
atLeastOneCompound.isChecked,
|
||||||
|
excludeAmbiguousCompound.isChecked)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to generate a password", e)
|
||||||
|
}
|
||||||
|
passKeyView.passwordString = password
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
saveSettings()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
// Nothing here
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveSettings() {
|
||||||
|
context?.let { context ->
|
||||||
|
PreferencesUtil.setDefaultPasswordOptions(context, getOptions())
|
||||||
|
PreferencesUtil.setDefaultPasswordLength(context, getPasswordLength())
|
||||||
|
PreferencesUtil.setDefaultPasswordConsiderChars(context, getConsiderChars())
|
||||||
|
PreferencesUtil.setDefaultPasswordIgnoreChars(context, getIgnoreChars())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadSettings() {
|
||||||
|
context?.let { context ->
|
||||||
|
setOptions(PreferencesUtil.getDefaultPasswordOptions(context))
|
||||||
|
setPasswordLength(PreferencesUtil.getDefaultPasswordLength(context))
|
||||||
|
setConsiderChars(PreferencesUtil.getDefaultPasswordConsiderChars(context))
|
||||||
|
setIgnoreChars(PreferencesUtil.getDefaultPasswordIgnoreChars(context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setSliderValue(value: Int) {
|
||||||
|
when {
|
||||||
|
value < minLengthSlider -> {
|
||||||
|
sliderLength.value = minLengthSlider.toFloat()
|
||||||
|
}
|
||||||
|
value > maxLengthSlider -> {
|
||||||
|
sliderLength.value = maxLengthSlider.toFloat()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
sliderLength.value = value.toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "PasswordGeneratorFrgmt"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,7 +85,7 @@ object EntrySelectionHelper {
|
|||||||
return intent.getParcelableExtra(KEY_SEARCH_INFO)
|
return intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
|
private fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
|
||||||
registerInfo?.let {
|
registerInfo?.let {
|
||||||
intent.putExtra(KEY_REGISTER_INFO, it)
|
intent.putExtra(KEY_REGISTER_INFO, it)
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,7 @@ object EntrySelectionHelper {
|
|||||||
?: SpecialMode.DEFAULT
|
?: SpecialMode.DEFAULT
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
||||||
intent.putExtra(KEY_TYPE_MODE, typeMode as Serializable)
|
intent.putExtra(KEY_TYPE_MODE, typeMode as Serializable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,12 +24,15 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.DatabaseDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
@@ -431,7 +434,17 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected fun lockAndExit() {
|
protected fun lockAndExit() {
|
||||||
sendBroadcast(Intent(LOCK_ACTION))
|
// Ask confirmation if modification not saved
|
||||||
|
if (mDatabase?.dataModifiedSinceLastLoading == true) {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setMessage(R.string.discard_changes)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setPositiveButton(R.string.lock) { _, _ ->
|
||||||
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
|
}.create().show()
|
||||||
|
} else {
|
||||||
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetAppTimeout() {
|
fun resetAppTimeout() {
|
||||||
@@ -467,25 +480,33 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
*/
|
*/
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
fun View.resetAppTimeoutWhenViewTouchedOrFocused(context: Context, databaseLoaded: Boolean?) {
|
fun View.resetAppTimeoutWhenViewTouchedOrFocused(context: Context, databaseLoaded: Boolean?) {
|
||||||
// Log.d(DatabaseLockActivity.TAG, "View prepared to reset app timeout")
|
try {
|
||||||
setOnTouchListener { _, event ->
|
// Log.d(DatabaseLockActivity.TAG, "View prepared to reset app timeout")
|
||||||
when (event.action) {
|
setOnTouchListener { _, event ->
|
||||||
MotionEvent.ACTION_DOWN -> {
|
when (event.action) {
|
||||||
// Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout")
|
MotionEvent.ACTION_DOWN -> {
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
|
// Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout")
|
||||||
databaseLoaded ?: false)
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(
|
||||||
|
context,
|
||||||
|
databaseLoaded ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
setOnFocusChangeListener { _, _ ->
|
||||||
|
// Log.d(DatabaseLockActivity.TAG, "View focused, try to reset app timeout")
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(
|
||||||
|
context,
|
||||||
|
databaseLoaded ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (this is ViewGroup) {
|
||||||
|
for (i in 0..childCount) {
|
||||||
|
getChildAt(i)?.resetAppTimeoutWhenViewTouchedOrFocused(context, databaseLoaded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
} catch (e: Exception) {
|
||||||
}
|
Log.e("AppTimeout", "Unable to reset app timeout", e)
|
||||||
setOnFocusChangeListener { _, _ ->
|
|
||||||
// Log.d(DatabaseLockActivity.TAG, "View focused, try to reset app timeout")
|
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
|
|
||||||
databaseLoaded ?: false)
|
|
||||||
}
|
|
||||||
if (this is ViewGroup) {
|
|
||||||
for (i in 0..childCount) {
|
|
||||||
getChildAt(i)?.resetAppTimeoutWhenViewTouchedOrFocused(context, databaseLoaded)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.kunzisoft.keepass.activities.legacy
|
package com.kunzisoft.keepass.activities.legacy
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
@@ -95,11 +96,9 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
|
|
||||||
private fun backToTheMainAppAndFinish() {
|
private fun backToTheMainAppAndFinish() {
|
||||||
// To move the app in background and return to the main app
|
// To move the app in background and return to the main app
|
||||||
|
// Not visible as opened with FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
||||||
moveTaskToBack(true)
|
moveTaskToBack(true)
|
||||||
// To remove this instance in the OS app selector
|
// Not finish() to prevent service kill
|
||||||
Handler(Looper.getMainLooper()).postDelayed({
|
|
||||||
finish()
|
|
||||||
}, 500)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
|
import com.kunzisoft.keepass.activities.fragments.PassphraseGeneratorFragment
|
||||||
|
import com.kunzisoft.keepass.activities.fragments.PasswordGeneratorFragment
|
||||||
|
import com.kunzisoft.keepass.activities.fragments.KeyGeneratorFragment
|
||||||
|
|
||||||
|
class KeyGeneratorPagerAdapter(fragment: Fragment)
|
||||||
|
: FragmentStateAdapter(fragment) {
|
||||||
|
|
||||||
|
private val passwordGeneratorFragment = PasswordGeneratorFragment()
|
||||||
|
private val passphraseGeneratorFragment = PassphraseGeneratorFragment()
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return KeyGeneratorFragment.KeyGeneratorTab.values().size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createFragment(position: Int): Fragment {
|
||||||
|
return when (KeyGeneratorFragment.KeyGeneratorTab.getKeyGeneratorTabByPosition(position)) {
|
||||||
|
KeyGeneratorFragment.KeyGeneratorTab.PASSWORD -> passwordGeneratorFragment
|
||||||
|
KeyGeneratorFragment.KeyGeneratorTab.PASSPHRASE -> passphraseGeneratorFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -516,7 +516,7 @@ class NodesAdapter (private val context: Context,
|
|||||||
null -> {}
|
null -> {}
|
||||||
}
|
}
|
||||||
holder?.otpToken?.apply {
|
holder?.otpToken?.apply {
|
||||||
text = otpElement?.token
|
text = otpElement?.tokenString
|
||||||
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
|
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
|
||||||
}
|
}
|
||||||
holder?.otpContainer?.setOnClickListener {
|
holder?.otpContainer?.setOnClickListener {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import kotlinx.coroutines.*
|
|||||||
*/
|
*/
|
||||||
class IOActionTask<T>(
|
class IOActionTask<T>(
|
||||||
private val action: () -> T ,
|
private val action: () -> T ,
|
||||||
private val afterActionDatabaseListener: ((T?) -> Unit)? = null) {
|
private val afterActionListener: ((T?) -> Unit)? = null) {
|
||||||
|
|
||||||
private val mainScope = CoroutineScope(Dispatchers.Main)
|
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ class IOActionTask<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
afterActionDatabaseListener?.invoke(asyncResult.await())
|
afterActionListener?.invoke(asyncResult.await())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
|||||||
import com.kunzisoft.keepass.model.CredentialStorage
|
import com.kunzisoft.keepass.model.CredentialStorage
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||||
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
|
import com.kunzisoft.keepass.view.showByFading
|
||||||
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -579,10 +581,13 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
|
|
||||||
private fun showViews(show: Boolean) {
|
private fun showViews(show: Boolean) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
mAdvancedUnlockInfoView?.visibility = if (show)
|
if (show) {
|
||||||
View.VISIBLE
|
if (mAdvancedUnlockInfoView?.visibility != View.VISIBLE)
|
||||||
|
mAdvancedUnlockInfoView?.showByFading()
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
View.GONE
|
if (mAdvancedUnlockInfoView?.visibility == View.VISIBLE)
|
||||||
|
mAdvancedUnlockInfoView?.hideByFading()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -608,26 +613,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun performEducation(passwordActivityEducation: PasswordActivityEducation,
|
|
||||||
readOnlyEducationPerformed: Boolean,
|
|
||||||
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
|
||||||
onOuterViewClick: ((TapTargetView?) -> Unit)? = null) {
|
|
||||||
try {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
|
||||||
&& !readOnlyEducationPerformed) {
|
|
||||||
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(requireContext())
|
|
||||||
PreferencesUtil.isAdvancedUnlockEnable(requireContext())
|
|
||||||
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
|
||||||
&& mAdvancedUnlockInfoView != null && mAdvancedUnlockInfoView?.visibility == View.VISIBLE
|
|
||||||
&& mAdvancedUnlockInfoView?.unlockIconImageView != null
|
|
||||||
&& passwordActivityEducation.checkAndPerformedBiometricEducation(mAdvancedUnlockInfoView!!.unlockIconImageView!!,
|
|
||||||
onEducationViewClick,
|
|
||||||
onOuterViewClick)
|
|
||||||
}
|
|
||||||
} catch (ignored: Exception) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Mode {
|
enum class Mode {
|
||||||
BIOMETRIC_UNAVAILABLE,
|
BIOMETRIC_UNAVAILABLE,
|
||||||
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
|
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSecretKey(): SecretKey? {
|
@Synchronized private fun getSecretKey(): SecretKey? {
|
||||||
if (!isKeyManagerInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -141,8 +141,8 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
KeyGenParameterSpec.Builder(
|
KeyGenParameterSpec.Builder(
|
||||||
ADVANCED_UNLOCK_KEYSTORE_KEY,
|
ADVANCED_UNLOCK_KEYSTORE_KEY,
|
||||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||||
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
|
.setBlockModes(ADVANCED_UNLOCK_BLOCKS_MODES)
|
||||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
|
.setEncryptionPaddings(ADVANCED_UNLOCK_ENCRYPTION_PADDING)
|
||||||
.apply {
|
.apply {
|
||||||
// Require the user to authenticate with a fingerprint to authorize every use
|
// Require the user to authenticate with a fingerprint to authorize every use
|
||||||
// of the key, don't use it for device credential because it's the user authentication
|
// of the key, don't use it for device credential because it's the user authentication
|
||||||
@@ -173,11 +173,11 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) {
|
@Synchronized fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) {
|
||||||
initEncryptData(actionIfCypherInit, true)
|
initEncryptData(actionIfCypherInit, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
@Synchronized private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
||||||
firstLaunch: Boolean) {
|
firstLaunch: Boolean) {
|
||||||
if (!isKeyManagerInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return
|
return
|
||||||
@@ -213,7 +213,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun encryptData(value: ByteArray) {
|
@Synchronized fun encryptData(value: ByteArray) {
|
||||||
if (!isKeyManagerInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -229,12 +229,12 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initDecryptData(ivSpecValue: ByteArray,
|
@Synchronized fun initDecryptData(ivSpecValue: ByteArray,
|
||||||
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
||||||
initDecryptData(ivSpecValue, actionIfCypherInit, true)
|
initDecryptData(ivSpecValue, actionIfCypherInit, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initDecryptData(ivSpecValue: ByteArray,
|
@Synchronized private fun initDecryptData(ivSpecValue: ByteArray,
|
||||||
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
||||||
firstLaunch: Boolean = true) {
|
firstLaunch: Boolean = true) {
|
||||||
if (!isKeyManagerInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
@@ -278,7 +278,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decryptData(encryptedValue: ByteArray) {
|
@Synchronized fun decryptData(encryptedValue: ByteArray) {
|
||||||
if (!isKeyManagerInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -296,7 +296,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteKeystoreKey() {
|
@Synchronized fun deleteKeystoreKey() {
|
||||||
try {
|
try {
|
||||||
keyStore?.load(null)
|
keyStore?.load(null)
|
||||||
keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY)
|
keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY)
|
||||||
@@ -306,7 +306,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt,
|
@Synchronized fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt,
|
||||||
deviceCredentialResultLauncher: ActivityResultLauncher<Intent>
|
deviceCredentialResultLauncher: ActivityResultLauncher<Intent>
|
||||||
) {
|
) {
|
||||||
// Init advanced unlock prompt
|
// Init advanced unlock prompt
|
||||||
@@ -346,7 +346,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun closeBiometricPrompt() {
|
@Synchronized fun closeBiometricPrompt() {
|
||||||
biometricPrompt?.cancelAuthentication()
|
biometricPrompt?.cancelAuthentication()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ class DatabaseTaskProvider {
|
|||||||
private fun bindService() {
|
private fun bindService() {
|
||||||
initServiceConnection()
|
initServiceConnection()
|
||||||
serviceConnection?.let {
|
serviceConnection?.let {
|
||||||
context.bindService(intentDatabaseTask, it, BIND_AUTO_CREATE or BIND_NOT_FOREGROUND or BIND_ABOVE_CLIENT)
|
context.bindService(intentDatabaseTask, it, BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -913,53 +913,24 @@ class Database {
|
|||||||
try {
|
try {
|
||||||
val saveUri = databaseCopyUri ?: this.fileUri
|
val saveUri = databaseCopyUri ?: this.fileUri
|
||||||
if (saveUri != null) {
|
if (saveUri != null) {
|
||||||
if (saveUri.scheme == "file") {
|
var outputStream: OutputStream? = null
|
||||||
saveUri.path?.let { filename ->
|
try {
|
||||||
val tempFile = File("$filename.tmp")
|
outputStream = UriUtil.getUriOutputStream(contentResolver, saveUri)
|
||||||
|
outputStream?.let { definedOutputStream ->
|
||||||
var fileOutputStream: FileOutputStream? = null
|
val databaseOutput =
|
||||||
try {
|
mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
|
||||||
fileOutputStream = FileOutputStream(tempFile)
|
?: mDatabaseKDBX?.let {
|
||||||
val pmo = mDatabaseKDB?.let { DatabaseOutputKDB(it, fileOutputStream) }
|
DatabaseOutputKDBX(
|
||||||
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, fileOutputStream) }
|
it,
|
||||||
pmo?.output()
|
definedOutputStream
|
||||||
} catch (e: Exception) {
|
)
|
||||||
throw IOException(e)
|
}
|
||||||
} finally {
|
databaseOutput?.output()
|
||||||
fileOutputStream?.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force data to disk before continuing
|
|
||||||
try {
|
|
||||||
fileOutputStream?.fd?.sync()
|
|
||||||
} catch (e: SyncFailedException) {
|
|
||||||
// Ignore if fsync fails. We tried.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tempFile.renameTo(File(filename))) {
|
|
||||||
throw IOException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var outputStream: OutputStream? = null
|
|
||||||
try {
|
|
||||||
outputStream = contentResolver.openOutputStream(saveUri, "rwt")
|
|
||||||
outputStream?.let { definedOutputStream ->
|
|
||||||
val databaseOutput =
|
|
||||||
mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
|
|
||||||
?: mDatabaseKDBX?.let {
|
|
||||||
DatabaseOutputKDBX(
|
|
||||||
it,
|
|
||||||
definedOutputStream
|
|
||||||
)
|
|
||||||
}
|
|
||||||
databaseOutput?.output()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw IOException(e)
|
|
||||||
} finally {
|
|
||||||
outputStream?.close()
|
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IOException(e)
|
||||||
|
} finally {
|
||||||
|
outputStream?.close()
|
||||||
}
|
}
|
||||||
if (databaseCopyUri == null) {
|
if (databaseCopyUri == null) {
|
||||||
this.dataModifiedSinceLastLoading = false
|
this.dataModifiedSinceLastLoading = false
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ object TemplateField {
|
|||||||
const val LABEL_SECURE_NOTE = "Secure Note"
|
const val LABEL_SECURE_NOTE = "Secure Note"
|
||||||
const val LABEL_MEMBERSHIP = "Membership"
|
const val LABEL_MEMBERSHIP = "Membership"
|
||||||
|
|
||||||
|
fun isStandardPasswordName(context: Context, name: String): Boolean {
|
||||||
|
return name.equals(LABEL_PASSWORD, true)
|
||||||
|
|| name == getLocalizedName(context, LABEL_PASSWORD)
|
||||||
|
}
|
||||||
|
|
||||||
fun isStandardFieldName(name: String): Boolean {
|
fun isStandardFieldName(name: String): Boolean {
|
||||||
return arrayOf(
|
return arrayOf(
|
||||||
LABEL_TITLE,
|
LABEL_TITLE,
|
||||||
|
|||||||
@@ -148,9 +148,9 @@ class SearchHelper {
|
|||||||
onDatabaseClosed.invoke()
|
onDatabaseClosed.invoke()
|
||||||
} else if (TimeoutHelper.checkTime(context)) {
|
} else if (TimeoutHelper.checkTime(context)) {
|
||||||
var searchWithoutUI = false
|
var searchWithoutUI = false
|
||||||
if (PreferencesUtil.isAutofillAutoSearchEnable(context)
|
if (searchInfo != null
|
||||||
&& searchInfo != null && !searchInfo.manualSelection
|
&& !searchInfo.manualSelection
|
||||||
&& !searchInfo.containsOnlyNullValues()) {
|
&& !searchInfo.containsOnlyNullValues()) {
|
||||||
// If search provide results
|
// If search provide results
|
||||||
database.createVirtualGroupFromSearchInfo(
|
database.createVirtualGroupFromSearchInfo(
|
||||||
searchInfo.toString(),
|
searchInfo.toString(),
|
||||||
@@ -181,7 +181,7 @@ class SearchHelper {
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
// Exclude entry expired
|
// Exclude entry expired
|
||||||
if (searchParameters.excludeExpired) {
|
if (!searchParameters.searchInExpired) {
|
||||||
if (entry.isCurrentlyExpires)
|
if (entry.isCurrentlyExpires)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -237,14 +237,20 @@ class SearchHelper {
|
|||||||
return false
|
return false
|
||||||
return if (searchParameters.isRegex) {
|
return if (searchParameters.isRegex) {
|
||||||
val regex = if (searchParameters.caseSensitive) {
|
val regex = if (searchParameters.caseSensitive) {
|
||||||
searchParameters.searchQuery.toRegex(RegexOption.DOT_MATCHES_ALL)
|
searchParameters.searchQuery
|
||||||
|
.toRegex(RegexOption.DOT_MATCHES_ALL)
|
||||||
} else {
|
} else {
|
||||||
searchParameters.searchQuery
|
searchParameters.searchQuery
|
||||||
.toRegex(setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.IGNORE_CASE))
|
.toRegex(setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.IGNORE_CASE))
|
||||||
}
|
}
|
||||||
regex.matches(stringToCheck)
|
regex.matches(stringToCheck)
|
||||||
} else {
|
} else {
|
||||||
stringToCheck.contains(searchParameters.searchQuery, !searchParameters.caseSensitive)
|
var searchFound = true
|
||||||
|
searchParameters.searchQuery.split(" ").forEach { word ->
|
||||||
|
searchFound = searchFound
|
||||||
|
&& stringToCheck.contains(word, !searchParameters.caseSensitive)
|
||||||
|
}
|
||||||
|
searchFound
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class SearchParameters() : Parcelable{
|
|||||||
var searchInUsernames = true
|
var searchInUsernames = true
|
||||||
var searchInPasswords = false
|
var searchInPasswords = false
|
||||||
var searchInUrls = true
|
var searchInUrls = true
|
||||||
var excludeExpired = false
|
var searchInExpired = false
|
||||||
var searchInNotes = true
|
var searchInNotes = true
|
||||||
var searchInOTP = false
|
var searchInOTP = false
|
||||||
var searchInOther = true
|
var searchInOther = true
|
||||||
@@ -49,11 +49,12 @@ class SearchParameters() : Parcelable{
|
|||||||
constructor(parcel: Parcel) : this() {
|
constructor(parcel: Parcel) : this() {
|
||||||
searchQuery = parcel.readString() ?: searchQuery
|
searchQuery = parcel.readString() ?: searchQuery
|
||||||
caseSensitive = parcel.readByte() != 0.toByte()
|
caseSensitive = parcel.readByte() != 0.toByte()
|
||||||
|
isRegex = parcel.readByte() != 0.toByte()
|
||||||
searchInTitles = parcel.readByte() != 0.toByte()
|
searchInTitles = parcel.readByte() != 0.toByte()
|
||||||
searchInUsernames = parcel.readByte() != 0.toByte()
|
searchInUsernames = parcel.readByte() != 0.toByte()
|
||||||
searchInPasswords = parcel.readByte() != 0.toByte()
|
searchInPasswords = parcel.readByte() != 0.toByte()
|
||||||
searchInUrls = parcel.readByte() != 0.toByte()
|
searchInUrls = parcel.readByte() != 0.toByte()
|
||||||
excludeExpired = parcel.readByte() != 0.toByte()
|
searchInExpired = parcel.readByte() != 0.toByte()
|
||||||
searchInNotes = parcel.readByte() != 0.toByte()
|
searchInNotes = parcel.readByte() != 0.toByte()
|
||||||
searchInOTP = parcel.readByte() != 0.toByte()
|
searchInOTP = parcel.readByte() != 0.toByte()
|
||||||
searchInOther = parcel.readByte() != 0.toByte()
|
searchInOther = parcel.readByte() != 0.toByte()
|
||||||
@@ -68,11 +69,12 @@ class SearchParameters() : Parcelable{
|
|||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
parcel.writeString(searchQuery)
|
parcel.writeString(searchQuery)
|
||||||
parcel.writeByte(if (caseSensitive) 1 else 0)
|
parcel.writeByte(if (caseSensitive) 1 else 0)
|
||||||
|
parcel.writeByte(if (isRegex) 1 else 0)
|
||||||
parcel.writeByte(if (searchInTitles) 1 else 0)
|
parcel.writeByte(if (searchInTitles) 1 else 0)
|
||||||
parcel.writeByte(if (searchInUsernames) 1 else 0)
|
parcel.writeByte(if (searchInUsernames) 1 else 0)
|
||||||
parcel.writeByte(if (searchInPasswords) 1 else 0)
|
parcel.writeByte(if (searchInPasswords) 1 else 0)
|
||||||
parcel.writeByte(if (searchInUrls) 1 else 0)
|
parcel.writeByte(if (searchInUrls) 1 else 0)
|
||||||
parcel.writeByte(if (excludeExpired) 1 else 0)
|
parcel.writeByte(if (searchInExpired) 1 else 0)
|
||||||
parcel.writeByte(if (searchInNotes) 1 else 0)
|
parcel.writeByte(if (searchInNotes) 1 else 0)
|
||||||
parcel.writeByte(if (searchInOTP) 1 else 0)
|
parcel.writeByte(if (searchInOTP) 1 else 0)
|
||||||
parcel.writeByte(if (searchInOther) 1 else 0)
|
parcel.writeByte(if (searchInOther) 1 else 0)
|
||||||
|
|||||||
@@ -360,6 +360,13 @@ open class Education(val activity: Activity) {
|
|||||||
context.resources.getBoolean(R.bool.education_setup_OTP_default))
|
context.resources.getBoolean(R.bool.education_setup_OTP_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setEducationScreenReclickedPerformed(context: Context) {
|
||||||
|
getEducationSharedPreferences(context)
|
||||||
|
.edit()
|
||||||
|
.putBoolean(context.getString(R.string.education_screen_reclicked_key), true)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines if the reset education preference has been reclicked
|
* Defines if the reset education preference has been reclicked
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ package com.kunzisoft.keepass.magikeyboard;
|
|||||||
|
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_BACK_KEYBOARD;
|
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_BACK_KEYBOARD;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
|
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
|
||||||
|
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY;
|
||||||
|
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP;
|
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
|
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
|
||||||
|
|
||||||
@@ -1049,6 +1051,9 @@ public class KeyboardView extends View implements View.OnClickListener {
|
|||||||
if (popupKey.codes[0] == KEY_BACK_KEYBOARD) {
|
if (popupKey.codes[0] == KEY_BACK_KEYBOARD) {
|
||||||
mKeyboardActionListener.onKey(KEY_CHANGE_KEYBOARD, popupKey.codes);
|
mKeyboardActionListener.onKey(KEY_CHANGE_KEYBOARD, popupKey.codes);
|
||||||
return true;
|
return true;
|
||||||
|
} else if (popupKey.codes[0] == KEY_ENTRY) {
|
||||||
|
mKeyboardActionListener.onKey(KEY_ENTRY_ALT, popupKey.codes);
|
||||||
|
return true;
|
||||||
} else if (popupKey.codes[0] == KEY_OTP) {
|
} else if (popupKey.codes[0] == KEY_OTP) {
|
||||||
mKeyboardActionListener.onKey(KEY_OTP_ALT, popupKey.codes);
|
mKeyboardActionListener.onKey(KEY_OTP_ALT, popupKey.codes);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
package com.kunzisoft.keepass.magikeyboard
|
package com.kunzisoft.keepass.magikeyboard
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.inputmethodservice.InputMethodService
|
import android.inputmethodservice.InputMethodService
|
||||||
@@ -30,19 +31,28 @@ import android.view.*
|
|||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
import android.widget.PopupWindow
|
import android.widget.PopupWindow
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||||
|
import androidx.core.graphics.BlendModeCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity
|
import com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
||||||
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.Field
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
||||||
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
|
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||||
|
|
||||||
@@ -50,13 +60,19 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
private var mDatabase: Database? = null
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
private var keyboardView: KeyboardView? = null
|
private var keyboardView: KeyboardView? = null
|
||||||
|
private var entryContainer: View? = null
|
||||||
private var entryText: TextView? = null
|
private var entryText: TextView? = null
|
||||||
|
private var databaseText: TextView? = null
|
||||||
|
private var databaseColorView: ImageView? = null
|
||||||
|
private var packageText: TextView? = null
|
||||||
private var keyboard: Keyboard? = null
|
private var keyboard: Keyboard? = null
|
||||||
private var keyboardEntry: Keyboard? = null
|
private var keyboardEntry: Keyboard? = null
|
||||||
private var popupCustomKeys: PopupWindow? = null
|
private var popupCustomKeys: PopupWindow? = null
|
||||||
private var fieldsAdapter: FieldsAdapter? = null
|
private var fieldsAdapter: FieldsAdapter? = null
|
||||||
private var playSoundDuringCLick: Boolean = false
|
private var playSoundDuringCLick: Boolean = false
|
||||||
|
|
||||||
|
private var mFormPackageName: String? = null
|
||||||
|
|
||||||
private var lockReceiver: LockReceiver? = null
|
private var lockReceiver: LockReceiver? = null
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@@ -66,6 +82,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
mDatabaseTaskProvider?.registerProgressTask()
|
mDatabaseTaskProvider?.registerProgressTask()
|
||||||
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||||
this.mDatabase = database
|
this.mDatabase = database
|
||||||
|
assignKeyboardView()
|
||||||
}
|
}
|
||||||
// Remove the entry and lock the keyboard when the lock signal is receive
|
// Remove the entry and lock the keyboard when the lock signal is receive
|
||||||
lockReceiver = LockReceiver {
|
lockReceiver = LockReceiver {
|
||||||
@@ -82,7 +99,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
override fun onCreateInputView(): View {
|
override fun onCreateInputView(): View {
|
||||||
|
|
||||||
val rootKeyboardView = layoutInflater.inflate(R.layout.keyboard_container, null)
|
val rootKeyboardView = layoutInflater.inflate(R.layout.keyboard_container, null)
|
||||||
|
entryContainer = rootKeyboardView.findViewById(R.id.magikeyboard_entry_container)
|
||||||
entryText = rootKeyboardView.findViewById(R.id.magikeyboard_entry_text)
|
entryText = rootKeyboardView.findViewById(R.id.magikeyboard_entry_text)
|
||||||
|
databaseText = rootKeyboardView.findViewById(R.id.magikeyboard_database_text)
|
||||||
|
databaseColorView = rootKeyboardView.findViewById(R.id.magikeyboard_database_color)
|
||||||
|
packageText = rootKeyboardView.findViewById(R.id.magikeyboard_package_text)
|
||||||
keyboardView = rootKeyboardView.findViewById(R.id.magikeyboard_view)
|
keyboardView = rootKeyboardView.findViewById(R.id.magikeyboard_view)
|
||||||
|
|
||||||
if (keyboardView != null) {
|
if (keyboardView != null) {
|
||||||
@@ -94,7 +115,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
.inflate(R.layout.keyboard_popup_fields, FrameLayout(context))
|
.inflate(R.layout.keyboard_popup_fields, FrameLayout(context))
|
||||||
|
|
||||||
popupCustomKeys = PopupWindow(context).apply {
|
popupCustomKeys = PopupWindow(context).apply {
|
||||||
width = WindowManager.LayoutParams.WRAP_CONTENT
|
width = WindowManager.LayoutParams.MATCH_PARENT
|
||||||
height = WindowManager.LayoutParams.WRAP_CONTENT
|
height = WindowManager.LayoutParams.WRAP_CONTENT
|
||||||
inputMethodMode = PopupWindow.INPUT_METHOD_NEEDED
|
inputMethodMode = PopupWindow.INPUT_METHOD_NEEDED
|
||||||
contentView = popupFieldsView
|
contentView = popupFieldsView
|
||||||
@@ -104,7 +125,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
fieldsAdapter = FieldsAdapter(this)
|
fieldsAdapter = FieldsAdapter(this)
|
||||||
fieldsAdapter?.onItemClickListener = object : FieldsAdapter.OnItemClickListener {
|
fieldsAdapter?.onItemClickListener = object : FieldsAdapter.OnItemClickListener {
|
||||||
override fun onItemClick(item: Field) {
|
override fun onItemClick(item: Field) {
|
||||||
currentInputConnection.commitText(entryInfoKey?.getGeneratedFieldValue(item.name) , 1)
|
currentInputConnection.commitText(getEntryInfo()?.getGeneratedFieldValue(item.name) , 1)
|
||||||
actionTabAutomatically()
|
actionTabAutomatically()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,17 +135,6 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
val closeView = popupFieldsView.findViewById<View>(R.id.keyboard_popup_close)
|
val closeView = popupFieldsView.findViewById<View>(R.id.keyboard_popup_close)
|
||||||
closeView.setOnClickListener { popupCustomKeys?.dismiss() }
|
closeView.setOnClickListener { popupCustomKeys?.dismiss() }
|
||||||
|
|
||||||
// Remove entry info if the database is not loaded
|
|
||||||
// or if entry info timestamp is before database loaded timestamp
|
|
||||||
val databaseTime = mDatabase?.loadTimestamp
|
|
||||||
val entryTime = entryInfoTimestamp
|
|
||||||
if (mDatabase == null
|
|
||||||
|| mDatabase?.loaded != true
|
|
||||||
|| databaseTime == null
|
|
||||||
|| entryTime == null
|
|
||||||
|| entryTime < databaseTime) {
|
|
||||||
removeEntryInfo()
|
|
||||||
}
|
|
||||||
assignKeyboardView()
|
assignKeyboardView()
|
||||||
keyboardView?.onKeyboardActionListener = this
|
keyboardView?.onKeyboardActionListener = this
|
||||||
|
|
||||||
@@ -134,17 +144,27 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
return super.onCreateInputView()
|
return super.onCreateInputView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getEntryInfo(): EntryInfo? {
|
||||||
|
var entryInfoRetrieved: EntryInfo? = null
|
||||||
|
entryUUID?.let { entryId ->
|
||||||
|
entryInfoRetrieved = mDatabase
|
||||||
|
?.getEntryById(NodeIdUUID(entryId))
|
||||||
|
?.getEntryInfo(mDatabase)
|
||||||
|
}
|
||||||
|
return entryInfoRetrieved
|
||||||
|
}
|
||||||
|
|
||||||
private fun assignKeyboardView() {
|
private fun assignKeyboardView() {
|
||||||
dismissCustomKeys()
|
dismissCustomKeys()
|
||||||
if (keyboardView != null) {
|
if (keyboardView != null) {
|
||||||
if (entryInfoKey != null) {
|
val entryInfo = getEntryInfo()
|
||||||
|
populateEntryInfoInView(entryInfo)
|
||||||
|
if (entryInfo != null) {
|
||||||
if (keyboardEntry != null) {
|
if (keyboardEntry != null) {
|
||||||
populateEntryInfoInView()
|
|
||||||
keyboardView?.keyboard = keyboardEntry
|
keyboardView?.keyboard = keyboardEntry
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (keyboard != null) {
|
if (keyboard != null) {
|
||||||
hideEntryInfo()
|
|
||||||
keyboardView?.keyboard = keyboard
|
keyboardView?.keyboard = keyboard
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,23 +173,45 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
keyboardView?.isHapticFeedbackEnabled = PreferencesUtil.isKeyboardVibrationEnable(this)
|
keyboardView?.isHapticFeedbackEnabled = PreferencesUtil.isKeyboardVibrationEnable(this)
|
||||||
playSoundDuringCLick = PreferencesUtil.isKeyboardSoundEnable(this)
|
playSoundDuringCLick = PreferencesUtil.isKeyboardSoundEnable(this)
|
||||||
}
|
}
|
||||||
|
setDatabaseViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun populateEntryInfoInView() {
|
private fun setDatabaseViews() {
|
||||||
entryText?.visibility = View.VISIBLE
|
if (mDatabase == null || mDatabase?.loaded != true) {
|
||||||
if (entryInfoKey?.title?.isNotEmpty() == true) {
|
entryContainer?.visibility = View.GONE
|
||||||
entryText?.text = entryInfoKey?.title
|
|
||||||
} else {
|
} else {
|
||||||
hideEntryInfo()
|
entryContainer?.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
databaseText?.text = mDatabase?.name ?: ""
|
||||||
|
val databaseColor = mDatabase?.customColor
|
||||||
|
if (databaseColor != null) {
|
||||||
|
databaseColorView?.drawable?.colorFilter = BlendModeColorFilterCompat
|
||||||
|
.createBlendModeColorFilterCompat(databaseColor, BlendModeCompat.SRC_IN)
|
||||||
|
databaseColorView?.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
databaseColorView?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hideEntryInfo() {
|
private fun populateEntryInfoInView(entryInfo: EntryInfo?) {
|
||||||
entryText?.visibility = View.GONE
|
if (entryInfo == null) {
|
||||||
|
entryText?.text = ""
|
||||||
|
entryText?.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
entryText?.text = entryInfo.getVisualTitle()
|
||||||
|
entryText?.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartInputView(info: EditorInfo, restarting: Boolean) {
|
override fun onStartInputView(info: EditorInfo, restarting: Boolean) {
|
||||||
super.onStartInputView(info, restarting)
|
super.onStartInputView(info, restarting)
|
||||||
|
mFormPackageName = info.packageName
|
||||||
|
if (!mFormPackageName.isNullOrEmpty()) {
|
||||||
|
packageText?.text = mFormPackageName
|
||||||
|
packageText?.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
packageText?.visibility = View.GONE
|
||||||
|
}
|
||||||
assignKeyboardView()
|
assignKeyboardView()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,16 +270,17 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
(getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?)
|
(getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?)
|
||||||
?.showInputMethodPicker()
|
?.showInputMethodPicker()
|
||||||
}
|
}
|
||||||
KEY_UNLOCK -> {
|
|
||||||
}
|
|
||||||
KEY_ENTRY -> {
|
KEY_ENTRY -> {
|
||||||
// Stop current service and reinit entry
|
var searchInfo: SearchInfo? = null
|
||||||
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
if (mFormPackageName != null) {
|
||||||
removeEntryInfo()
|
searchInfo = SearchInfo().apply {
|
||||||
val intent = Intent(this, MagikeyboardLauncherActivity::class.java)
|
applicationId = mFormPackageName
|
||||||
// New task needed because don't launch from an Activity context
|
}
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
}
|
||||||
startActivity(intent)
|
actionKeyEntry(searchInfo)
|
||||||
|
}
|
||||||
|
KEY_ENTRY_ALT -> {
|
||||||
|
actionKeyEntry()
|
||||||
}
|
}
|
||||||
KEY_LOCK -> {
|
KEY_LOCK -> {
|
||||||
removeEntryInfo()
|
removeEntryInfo()
|
||||||
@@ -245,12 +288,13 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
dismissCustomKeys()
|
dismissCustomKeys()
|
||||||
}
|
}
|
||||||
KEY_USERNAME -> {
|
KEY_USERNAME -> {
|
||||||
entryInfoKey?.username?.let { username ->
|
getEntryInfo()?.username?.let { username ->
|
||||||
currentInputConnection.commitText(username, 1)
|
currentInputConnection.commitText(username, 1)
|
||||||
}
|
}
|
||||||
actionTabAutomatically()
|
actionTabAutomatically()
|
||||||
}
|
}
|
||||||
KEY_PASSWORD -> {
|
KEY_PASSWORD -> {
|
||||||
|
val entryInfoKey = getEntryInfo()
|
||||||
entryInfoKey?.password?.let { password ->
|
entryInfoKey?.password?.let { password ->
|
||||||
currentInputConnection.commitText(password, 1)
|
currentInputConnection.commitText(password, 1)
|
||||||
}
|
}
|
||||||
@@ -258,14 +302,14 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
actionGoAutomatically(!otpFieldExists)
|
actionGoAutomatically(!otpFieldExists)
|
||||||
}
|
}
|
||||||
KEY_OTP -> {
|
KEY_OTP -> {
|
||||||
entryInfoKey?.let { entryInfo ->
|
getEntryInfo()?.let { entryInfo ->
|
||||||
currentInputConnection.commitText(
|
currentInputConnection.commitText(
|
||||||
entryInfo.getGeneratedFieldValue(OTP_TOKEN_FIELD), 1)
|
entryInfo.getGeneratedFieldValue(OTP_TOKEN_FIELD), 1)
|
||||||
}
|
}
|
||||||
actionGoAutomatically()
|
actionGoAutomatically()
|
||||||
}
|
}
|
||||||
KEY_OTP_ALT -> {
|
KEY_OTP_ALT -> {
|
||||||
entryInfoKey?.let { entryInfo ->
|
getEntryInfo()?.let { entryInfo ->
|
||||||
val otpToken = entryInfo.getGeneratedFieldValue(OTP_TOKEN_FIELD)
|
val otpToken = entryInfo.getGeneratedFieldValue(OTP_TOKEN_FIELD)
|
||||||
if (otpToken.isNotEmpty()) {
|
if (otpToken.isNotEmpty()) {
|
||||||
// Cut to fill each digit separatelyKeyEvent.KEYCODE_TAB
|
// Cut to fill each digit separatelyKeyEvent.KEYCODE_TAB
|
||||||
@@ -282,19 +326,20 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
actionGoAutomatically()
|
actionGoAutomatically()
|
||||||
}
|
}
|
||||||
KEY_URL -> {
|
KEY_URL -> {
|
||||||
entryInfoKey?.url?.let { url ->
|
getEntryInfo()?.url?.let { url ->
|
||||||
currentInputConnection.commitText(url, 1)
|
currentInputConnection.commitText(url, 1)
|
||||||
}
|
}
|
||||||
actionGoAutomatically()
|
actionGoAutomatically()
|
||||||
}
|
}
|
||||||
KEY_FIELDS -> {
|
KEY_FIELDS -> {
|
||||||
entryInfoKey?.customFields?.let { customFields ->
|
getEntryInfo()?.customFields?.let { customFields ->
|
||||||
fieldsAdapter?.apply {
|
fieldsAdapter?.apply {
|
||||||
setFields(customFields.filter { it.name != OTP_TOKEN_FIELD})
|
setFields(customFields.filter { it.name != OTP_TOKEN_FIELD})
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
popupCustomKeys?.showAtLocation(keyboardView, Gravity.END or Gravity.TOP, 0, 0)
|
popupCustomKeys?.showAtLocation(keyboardView,
|
||||||
|
Gravity.END or Gravity.TOP, 0, 180)
|
||||||
}
|
}
|
||||||
Keyboard.KEYCODE_DELETE -> {
|
Keyboard.KEYCODE_DELETE -> {
|
||||||
inputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
|
inputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
|
||||||
@@ -303,6 +348,43 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun actionKeyEntry(searchInfo: SearchInfo? = null) {
|
||||||
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
|
mDatabase,
|
||||||
|
searchInfo,
|
||||||
|
{ _, items ->
|
||||||
|
performSelection(
|
||||||
|
items,
|
||||||
|
{
|
||||||
|
// Automatically populate keyboard
|
||||||
|
addEntryAndLaunchNotificationIfAllowed(
|
||||||
|
this,
|
||||||
|
items[0],
|
||||||
|
true
|
||||||
|
)
|
||||||
|
assignKeyboardView()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
launchEntrySelection(searchInfo)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Select if not found
|
||||||
|
launchEntrySelection(searchInfo)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Select if database not opened
|
||||||
|
removeEntryInfo()
|
||||||
|
launchEntrySelection(searchInfo)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchEntrySelection(searchInfo: SearchInfo?) {
|
||||||
|
EntrySelectionLauncherActivity.launch(this, searchInfo)
|
||||||
|
}
|
||||||
|
|
||||||
private fun actionTabAutomatically() {
|
private fun actionTabAutomatically() {
|
||||||
if (PreferencesUtil.isAutoGoActionEnable(this))
|
if (PreferencesUtil.isAutoGoActionEnable(this))
|
||||||
currentInputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB))
|
currentInputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB))
|
||||||
@@ -359,23 +441,20 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
|
|
||||||
const val KEY_BACK_KEYBOARD = 600
|
const val KEY_BACK_KEYBOARD = 600
|
||||||
const val KEY_CHANGE_KEYBOARD = 601
|
const val KEY_CHANGE_KEYBOARD = 601
|
||||||
private const val KEY_UNLOCK = 610
|
const val KEY_LOCK = 611
|
||||||
private const val KEY_LOCK = 611
|
const val KEY_ENTRY = 620
|
||||||
private const val KEY_ENTRY = 620
|
const val KEY_ENTRY_ALT = 621
|
||||||
private const val KEY_USERNAME = 500
|
const val KEY_USERNAME = 500
|
||||||
private const val KEY_PASSWORD = 510
|
const val KEY_PASSWORD = 510
|
||||||
const val KEY_OTP = 515
|
const val KEY_OTP = 515
|
||||||
const val KEY_OTP_ALT = 516
|
const val KEY_OTP_ALT = 516
|
||||||
private const val KEY_URL = 520
|
const val KEY_URL = 520
|
||||||
private const val KEY_FIELDS = 530
|
const val KEY_FIELDS = 530
|
||||||
|
|
||||||
// TODO Retrieve entry info from id and service when database is open
|
private var entryUUID: UUID? = null
|
||||||
private var entryInfoKey: EntryInfo? = null
|
|
||||||
private var entryInfoTimestamp: Long? = null
|
|
||||||
|
|
||||||
private fun removeEntryInfo() {
|
private fun removeEntryInfo() {
|
||||||
entryInfoKey = null
|
entryUUID = null
|
||||||
entryInfoTimestamp = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeEntry(context: Context) {
|
fun removeEntry(context: Context) {
|
||||||
@@ -384,10 +463,47 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
|
|
||||||
fun addEntryAndLaunchNotificationIfAllowed(context: Context, entry: EntryInfo, toast: Boolean = false) {
|
fun addEntryAndLaunchNotificationIfAllowed(context: Context, entry: EntryInfo, toast: Boolean = false) {
|
||||||
// Add a new entry
|
// Add a new entry
|
||||||
entryInfoKey = entry
|
entryUUID = entry.id
|
||||||
entryInfoTimestamp = System.currentTimeMillis()
|
|
||||||
// Launch notification if allowed
|
// Launch notification if allowed
|
||||||
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
|
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun activatedInSettings(context: Context): Boolean {
|
||||||
|
return ContextCompat.getSystemService(context, InputMethodManager::class.java)
|
||||||
|
?.enabledInputMethodList
|
||||||
|
?.any {
|
||||||
|
it.packageName == context.packageName
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun performSelection(items: List<EntryInfo>,
|
||||||
|
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
|
||||||
|
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
|
||||||
|
if (items.size == 1) {
|
||||||
|
val itemFound = items[0]
|
||||||
|
if (entryUUID != itemFound.id) {
|
||||||
|
actionPopulateKeyboard.invoke(itemFound)
|
||||||
|
} else {
|
||||||
|
// Force selection if magikeyboard already populated
|
||||||
|
actionEntrySelection.invoke(false)
|
||||||
|
}
|
||||||
|
} else if (items.size > 1) {
|
||||||
|
// Select the one we want in the selection
|
||||||
|
actionEntrySelection.invoke(true)
|
||||||
|
} else {
|
||||||
|
// Select an arbitrary one
|
||||||
|
actionEntrySelection.invoke(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
||||||
|
entry: EntryInfo,
|
||||||
|
toast: Boolean = true) {
|
||||||
|
// Populate Magikeyboard with entry
|
||||||
|
addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
||||||
|
// Consume the selection mode
|
||||||
|
EntrySelectionHelper.removeModesFromIntent(activity.intent)
|
||||||
|
activity.moveTaskToBack(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,6 +225,14 @@ class EntryInfo : NodeInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getVisualTitle(): String {
|
||||||
|
return title.ifEmpty {
|
||||||
|
url.ifEmpty {
|
||||||
|
username.ifEmpty { id.toString() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is EntryInfo) return false
|
if (other !is EntryInfo) return false
|
||||||
|
|||||||
@@ -178,6 +178,14 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token with space each 3 digits
|
||||||
|
*/
|
||||||
|
val tokenString: String
|
||||||
|
get() {
|
||||||
|
return token.replace("...".toRegex(), "$0 ")
|
||||||
|
}
|
||||||
|
|
||||||
val secondsRemaining: Int
|
val secondsRemaining: Int
|
||||||
get() = otpModel.period - (System.currentTimeMillis() / 1000 % otpModel.period).toInt()
|
get() = otpModel.period - (System.currentTimeMillis() / 1000 % otpModel.period).toInt()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* 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.password
|
||||||
|
|
||||||
|
import me.gosimple.nbvcxz.resources.Generator
|
||||||
|
|
||||||
|
class PassphraseGenerator {
|
||||||
|
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
|
fun generatePassphrase(wordCount: Int,
|
||||||
|
wordSeparator: String,
|
||||||
|
wordCase: WordCase): String {
|
||||||
|
// From eff_large dictionary
|
||||||
|
return when (wordCase) {
|
||||||
|
WordCase.LOWER_CASE -> {
|
||||||
|
Generator.generatePassphrase(wordSeparator, wordCount)
|
||||||
|
}
|
||||||
|
WordCase.UPPER_CASE -> {
|
||||||
|
applyWordCase(wordCount, wordSeparator) { word ->
|
||||||
|
word.uppercase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WordCase.TITLE_CASE -> {
|
||||||
|
applyWordCase(wordCount, wordSeparator) { word ->
|
||||||
|
word.replaceFirstChar { char -> char.uppercaseChar() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyWordCase(wordCount: Int,
|
||||||
|
wordSeparator: String,
|
||||||
|
wordAction: (word: String) -> String): String {
|
||||||
|
val splitWords = Generator.generatePassphrase(TEMP_SPLIT, wordCount).split(TEMP_SPLIT)
|
||||||
|
val stringBuilder = StringBuilder()
|
||||||
|
splitWords.forEach {
|
||||||
|
stringBuilder
|
||||||
|
.append(wordAction(it))
|
||||||
|
.append(wordSeparator)
|
||||||
|
}
|
||||||
|
return stringBuilder.toString().removeSuffix(wordSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class WordCase {
|
||||||
|
LOWER_CASE,
|
||||||
|
UPPER_CASE,
|
||||||
|
TITLE_CASE;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getByOrdinal(position: Int): WordCase {
|
||||||
|
return when (position) {
|
||||||
|
0 -> LOWER_CASE
|
||||||
|
1 -> UPPER_CASE
|
||||||
|
else -> TITLE_CASE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TEMP_SPLIT = "-"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 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.password
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.graphics.Color
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.app.database.IOActionTask
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import me.gosimple.nbvcxz.Nbvcxz
|
||||||
|
import me.gosimple.nbvcxz.resources.Configuration
|
||||||
|
import me.gosimple.nbvcxz.resources.ConfigurationBuilder
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class PasswordEntropy(actionOnInitFinished: (() -> Unit)? = null) {
|
||||||
|
|
||||||
|
private var mPasswordEntropyCalculator: Nbvcxz? = null
|
||||||
|
private var entropyJob: Job? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
IOActionTask({
|
||||||
|
// Create the password generator object
|
||||||
|
val configuration: Configuration = ConfigurationBuilder()
|
||||||
|
.setLocale(Locale.getDefault())
|
||||||
|
.setMinimumEntropy(80.0)
|
||||||
|
.createConfiguration()
|
||||||
|
mPasswordEntropyCalculator = Nbvcxz(configuration)
|
||||||
|
}, {
|
||||||
|
actionOnInitFinished?.invoke()
|
||||||
|
}).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Strength(val color: Int) {
|
||||||
|
RISKY(Color.rgb(224, 56, 56)),
|
||||||
|
VERY_GUESSABLE(Color.rgb(196, 63, 49)),
|
||||||
|
SOMEWHAT_GUESSABLE(Color.rgb(219, 152, 55)),
|
||||||
|
SAFELY_UNGUESSABLE(Color.rgb(118, 168, 24)),
|
||||||
|
VERY_UNGUESSABLE(Color.rgb(37, 152, 41))
|
||||||
|
}
|
||||||
|
|
||||||
|
data class EntropyStrength(val strength: Strength,
|
||||||
|
val entropy: Double,
|
||||||
|
val estimationPercent: Int) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "EntropyStrength(strength=$strength, entropy=$entropy, estimationPercent=$estimationPercent)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntropyStrength(passwordString: String,
|
||||||
|
entropyStrengthResult: (EntropyStrength) -> Unit) {
|
||||||
|
|
||||||
|
entropyStrengthResult.invoke(EntropyStrength(Strength.RISKY, CALCULATE_ENTROPY, 0))
|
||||||
|
entropyJob?.cancel()
|
||||||
|
entropyJob = CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val asyncResult: Deferred<EntropyStrength?> = async {
|
||||||
|
try {
|
||||||
|
if (passwordString.length <= MAX_PASSWORD_LENGTH) {
|
||||||
|
val estimate = mPasswordEntropyCalculator?.estimate(passwordString)
|
||||||
|
val basicScore = estimate?.basicScore ?: 0
|
||||||
|
val entropy = estimate?.entropy ?: 0.0
|
||||||
|
val percentScore = min(entropy * 100 / 200, 100.0).toInt()
|
||||||
|
val strength =
|
||||||
|
if (basicScore == 0 || percentScore < 10) {
|
||||||
|
Strength.RISKY
|
||||||
|
} else if (basicScore == 1 || percentScore < 20) {
|
||||||
|
Strength.VERY_GUESSABLE
|
||||||
|
} else if (basicScore == 2 || percentScore < 33) {
|
||||||
|
Strength.SOMEWHAT_GUESSABLE
|
||||||
|
} else if (basicScore == 3 || percentScore < 50) {
|
||||||
|
Strength.SAFELY_UNGUESSABLE
|
||||||
|
} else if (basicScore == 4) {
|
||||||
|
Strength.VERY_UNGUESSABLE
|
||||||
|
} else {
|
||||||
|
Strength.RISKY
|
||||||
|
}
|
||||||
|
EntropyStrength(strength, entropy, percentScore)
|
||||||
|
} else {
|
||||||
|
EntropyStrength(Strength.VERY_UNGUESSABLE, HIGH_ENTROPY, 100)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
asyncResult.await()?.let { entropyStrength ->
|
||||||
|
entropyStrengthResult.invoke(entropyStrength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MAX_PASSWORD_LENGTH = 128
|
||||||
|
private const val CALCULATE_ENTROPY = -1.0
|
||||||
|
private const val HIGH_ENTROPY = 1000.0
|
||||||
|
|
||||||
|
fun getStringEntropy(resources: Resources, entropy: Double): String {
|
||||||
|
return when (entropy) {
|
||||||
|
CALCULATE_ENTROPY -> {
|
||||||
|
resources.getString(R.string.entropy_calculate)
|
||||||
|
}
|
||||||
|
HIGH_ENTROPY -> {
|
||||||
|
resources.getString(R.string.entropy_high)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
resources.getString(
|
||||||
|
R.string.entropy,
|
||||||
|
"%.${2}f".format(entropy)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,33 +20,17 @@
|
|||||||
package com.kunzisoft.keepass.password
|
package com.kunzisoft.keepass.password
|
||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.text.SpannableString
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class PasswordGenerator(private val resources: Resources) {
|
class PasswordGenerator(private val resources: Resources) {
|
||||||
|
|
||||||
// From KeePassXC code https://github.com/keepassxreboot/keepassxc/pull/538
|
|
||||||
private fun extendedChars(): String {
|
|
||||||
val charSet = StringBuilder()
|
|
||||||
// [U+0080, U+009F] are C1 control characters,
|
|
||||||
// U+00A0 is non-breaking space
|
|
||||||
run {
|
|
||||||
var ch = '\u00A1'
|
|
||||||
while (ch <= '\u00AC') {
|
|
||||||
charSet.append(ch)
|
|
||||||
++ch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// U+00AD is soft hyphen (format character)
|
|
||||||
var ch = '\u00AE'
|
|
||||||
while (ch < '\u00FF') {
|
|
||||||
charSet.append(ch)
|
|
||||||
++ch
|
|
||||||
}
|
|
||||||
charSet.append('\u00FF')
|
|
||||||
return charSet.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class)
|
@Throws(IllegalArgumentException::class)
|
||||||
fun generatePassword(length: Int,
|
fun generatePassword(length: Int,
|
||||||
upperCase: Boolean,
|
upperCase: Boolean,
|
||||||
@@ -57,7 +41,11 @@ class PasswordGenerator(private val resources: Resources) {
|
|||||||
space: Boolean,
|
space: Boolean,
|
||||||
specials: Boolean,
|
specials: Boolean,
|
||||||
brackets: Boolean,
|
brackets: Boolean,
|
||||||
extended: Boolean): String {
|
extended: Boolean,
|
||||||
|
considerChars: String,
|
||||||
|
ignoreChars: String,
|
||||||
|
atLeastOneFromEach: Boolean,
|
||||||
|
excludeAmbiguousChar: Boolean): String {
|
||||||
// Desired password length is 0 or less
|
// Desired password length is 0 or less
|
||||||
if (length <= 0) {
|
if (length <= 0) {
|
||||||
throw IllegalArgumentException(resources.getString(R.string.error_wrong_length))
|
throw IllegalArgumentException(resources.getString(R.string.error_wrong_length))
|
||||||
@@ -72,74 +60,164 @@ class PasswordGenerator(private val resources: Resources) {
|
|||||||
&& !space
|
&& !space
|
||||||
&& !specials
|
&& !specials
|
||||||
&& !brackets
|
&& !brackets
|
||||||
&& !extended) {
|
&& !extended
|
||||||
|
&& considerChars.isEmpty()) {
|
||||||
throw IllegalArgumentException(resources.getString(R.string.error_pass_gen_type))
|
throw IllegalArgumentException(resources.getString(R.string.error_pass_gen_type))
|
||||||
}
|
}
|
||||||
|
|
||||||
val characterSet = getCharacterSet(
|
// Filter builder
|
||||||
upperCase,
|
val passwordFilters = PasswordFilters().apply {
|
||||||
lowerCase,
|
this.length = length
|
||||||
digits,
|
this.ignoreChars = ignoreChars
|
||||||
minus,
|
if (excludeAmbiguousChar)
|
||||||
underline,
|
this.ignoreChars += AMBIGUOUS_CHARS
|
||||||
space,
|
if (upperCase) {
|
||||||
specials,
|
addFilter(
|
||||||
brackets,
|
UPPERCASE_CHARS,
|
||||||
extended)
|
if (atLeastOneFromEach) 1 else 0
|
||||||
|
)
|
||||||
val size = characterSet.length
|
}
|
||||||
|
if (lowerCase) {
|
||||||
val buffer = StringBuilder()
|
addFilter(
|
||||||
|
LOWERCASE_CHARS,
|
||||||
val random = SecureRandom() // use more secure variant of Random!
|
if (atLeastOneFromEach) 1 else 0
|
||||||
if (size > 0) {
|
)
|
||||||
for (i in 0 until length) {
|
}
|
||||||
buffer.append(characterSet[random.nextInt(size)])
|
if (digits) {
|
||||||
|
addFilter(
|
||||||
|
DIGIT_CHARS,
|
||||||
|
if (atLeastOneFromEach) 1 else 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (minus) {
|
||||||
|
addFilter(
|
||||||
|
MINUS_CHAR,
|
||||||
|
if (atLeastOneFromEach) 1 else 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (underline) {
|
||||||
|
addFilter(
|
||||||
|
UNDERLINE_CHAR,
|
||||||
|
if (atLeastOneFromEach) 1 else 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (space) {
|
||||||
|
addFilter(
|
||||||
|
SPACE_CHAR,
|
||||||
|
if (atLeastOneFromEach) 1 else 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (specials) {
|
||||||
|
addFilter(
|
||||||
|
SPECIAL_CHARS,
|
||||||
|
if (atLeastOneFromEach) 1 else 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (brackets) {
|
||||||
|
addFilter(
|
||||||
|
BRACKET_CHARS,
|
||||||
|
if (atLeastOneFromEach) 1 else 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (extended) {
|
||||||
|
addFilter(
|
||||||
|
extendedChars(),
|
||||||
|
if (atLeastOneFromEach) 1 else 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (considerChars.isNotEmpty()) {
|
||||||
|
addFilter(
|
||||||
|
considerChars,
|
||||||
|
if (atLeastOneFromEach) 1 else 0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return buffer.toString()
|
|
||||||
|
return generateRandomString(SecureRandom(), passwordFilters)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCharacterSet(upperCase: Boolean,
|
private fun generateRandomString(random: Random, passwordFilters: PasswordFilters): String {
|
||||||
lowerCase: Boolean,
|
val randomString = StringBuilder()
|
||||||
digits: Boolean,
|
|
||||||
minus: Boolean,
|
|
||||||
underline: Boolean,
|
|
||||||
space: Boolean,
|
|
||||||
specials: Boolean,
|
|
||||||
brackets: Boolean,
|
|
||||||
extended: Boolean): String {
|
|
||||||
val charSet = StringBuilder()
|
|
||||||
|
|
||||||
if (upperCase) {
|
// Allocate appropriate memory for the password.
|
||||||
charSet.append(UPPERCASE_CHARS)
|
var requiredCharactersLeft = passwordFilters.getRequiredCharactersLeft()
|
||||||
|
|
||||||
|
// Build the password.
|
||||||
|
for (i in 0 until passwordFilters.length) {
|
||||||
|
var selectableChars: String = if (requiredCharactersLeft < passwordFilters.length - i) {
|
||||||
|
// choose from any group at random
|
||||||
|
passwordFilters.getSelectableChars()
|
||||||
|
} else {
|
||||||
|
// choose only from a group that we need to satisfy a minimum for.
|
||||||
|
passwordFilters.getSelectableCharsForNeed()
|
||||||
|
}
|
||||||
|
passwordFilters.ignoreChars.forEach {
|
||||||
|
selectableChars = selectableChars.replace(it.toString(), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that the string is built, get the next random character.
|
||||||
|
val selectableCharsMaxIndex = selectableChars.length
|
||||||
|
val randomSelectableCharsIndex = if (selectableCharsMaxIndex > 0) random.nextInt(selectableCharsMaxIndex) else 0
|
||||||
|
val nextChar = selectableChars[randomSelectableCharsIndex]
|
||||||
|
|
||||||
|
// Put at random position
|
||||||
|
val randomStringMaxIndex = randomString.length
|
||||||
|
val randomStringIndex = if (randomStringMaxIndex > 0) random.nextInt(randomStringMaxIndex) else 0
|
||||||
|
randomString.insert(randomStringIndex, nextChar)
|
||||||
|
|
||||||
|
// Now figure out where it came from, and decrement the appropriate minimum value
|
||||||
|
passwordFilters.getFilterThatContainsChar(nextChar)?.let {
|
||||||
|
if (it.minCharsNeeded > 0) {
|
||||||
|
it.minCharsNeeded--
|
||||||
|
requiredCharactersLeft--
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (lowerCase) {
|
return randomString.toString()
|
||||||
charSet.append(LOWERCASE_CHARS)
|
}
|
||||||
}
|
|
||||||
if (digits) {
|
private data class Filter(var chars: String,
|
||||||
charSet.append(DIGIT_CHARS)
|
var minCharsNeeded: Int)
|
||||||
}
|
|
||||||
if (minus) {
|
private class PasswordFilters {
|
||||||
charSet.append(MINUS_CHAR)
|
var length: Int = 0
|
||||||
}
|
var ignoreChars = ""
|
||||||
if (underline) {
|
val filters = mutableListOf<Filter>()
|
||||||
charSet.append(UNDERLINE_CHAR)
|
|
||||||
}
|
fun addFilter(chars: String, minCharsNeeded: Int) {
|
||||||
if (space) {
|
filters.add(Filter(chars, minCharsNeeded))
|
||||||
charSet.append(SPACE_CHAR)
|
|
||||||
}
|
|
||||||
if (specials) {
|
|
||||||
charSet.append(SPECIAL_CHARS)
|
|
||||||
}
|
|
||||||
if (brackets) {
|
|
||||||
charSet.append(BRACKET_CHARS)
|
|
||||||
}
|
|
||||||
if (extended) {
|
|
||||||
charSet.append(extendedChars())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return charSet.toString()
|
fun getRequiredCharactersLeft(): Int {
|
||||||
|
var charsRequired = 0
|
||||||
|
filters.forEach {
|
||||||
|
charsRequired += it.minCharsNeeded
|
||||||
|
}
|
||||||
|
return charsRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSelectableChars(): String {
|
||||||
|
val stringBuilder = StringBuilder()
|
||||||
|
filters.forEach {
|
||||||
|
stringBuilder.append(it.chars)
|
||||||
|
}
|
||||||
|
return stringBuilder.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFilterThatContainsChar(char: Char): Filter? {
|
||||||
|
return filters.find { it.chars.contains(char) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSelectableCharsForNeed(): String {
|
||||||
|
val selectableChars = StringBuilder()
|
||||||
|
// choose only from a group that we need to satisfy a minimum for.
|
||||||
|
filters.forEach {
|
||||||
|
if (it.minCharsNeeded > 0) {
|
||||||
|
selectableChars.append(it.chars)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selectableChars.toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -151,5 +229,75 @@ class PasswordGenerator(private val resources: Resources) {
|
|||||||
private const val SPACE_CHAR = " "
|
private const val SPACE_CHAR = " "
|
||||||
private const val SPECIAL_CHARS = "!\"#$%&'*+,./:;=?@\\^`"
|
private const val SPECIAL_CHARS = "!\"#$%&'*+,./:;=?@\\^`"
|
||||||
private const val BRACKET_CHARS = "[]{}()<>"
|
private const val BRACKET_CHARS = "[]{}()<>"
|
||||||
|
private const val AMBIGUOUS_CHARS = "iI|lLoO01"
|
||||||
|
|
||||||
|
// From KeePassXC code https://github.com/keepassxreboot/keepassxc/pull/538
|
||||||
|
private fun extendedChars(): String {
|
||||||
|
val charSet = StringBuilder()
|
||||||
|
// [U+0080, U+009F] are C1 control characters,
|
||||||
|
// U+00A0 is non-breaking space
|
||||||
|
run {
|
||||||
|
var ch = '\u00A1'
|
||||||
|
while (ch <= '\u00AC') {
|
||||||
|
charSet.append(ch)
|
||||||
|
++ch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// U+00AD is soft hyphen (format character)
|
||||||
|
var ch = '\u00AE'
|
||||||
|
while (ch < '\u00FF') {
|
||||||
|
charSet.append(ch)
|
||||||
|
++ch
|
||||||
|
}
|
||||||
|
charSet.append('\u00FF')
|
||||||
|
return charSet.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getColorizedPassword(password: String): Spannable {
|
||||||
|
val spannableString = SpannableStringBuilder()
|
||||||
|
if (password.isNotEmpty()) {
|
||||||
|
password.forEach {
|
||||||
|
when {
|
||||||
|
UPPERCASE_CHARS.contains(it)||
|
||||||
|
LOWERCASE_CHARS.contains(it) -> {
|
||||||
|
spannableString.append(it)
|
||||||
|
}
|
||||||
|
DIGIT_CHARS.contains(it) -> {
|
||||||
|
// RED
|
||||||
|
spannableString.append(colorizeChar(it, Color.rgb(246, 79, 62)))
|
||||||
|
}
|
||||||
|
SPECIAL_CHARS.contains(it) -> {
|
||||||
|
// Blue
|
||||||
|
spannableString.append(colorizeChar(it, Color.rgb(39, 166, 228)))
|
||||||
|
}
|
||||||
|
MINUS_CHAR.contains(it)||
|
||||||
|
UNDERLINE_CHAR.contains(it)||
|
||||||
|
BRACKET_CHARS.contains(it) -> {
|
||||||
|
// Purple
|
||||||
|
spannableString.append(colorizeChar(it, Color.rgb(185, 38, 209)))
|
||||||
|
}
|
||||||
|
extendedChars().contains(it) -> {
|
||||||
|
// Green
|
||||||
|
spannableString.append(colorizeChar(it, Color.rgb(44, 181, 50)))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
spannableString.append(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return spannableString
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun colorizeChar(char: Char, color: Int): Spannable {
|
||||||
|
val spannableColorChar = SpannableString(char.toString())
|
||||||
|
spannableColorChar.setSpan(
|
||||||
|
ForegroundColorSpan(color),
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
return spannableColorChar
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import android.os.Binder
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.app.ServiceCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
@@ -36,7 +37,10 @@ import com.kunzisoft.keepass.model.EntryAttachmentState
|
|||||||
import com.kunzisoft.keepass.model.StreamDirection
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
@@ -275,7 +279,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
AttachmentState.COMPLETE,
|
AttachmentState.COMPLETE,
|
||||||
AttachmentState.CANCELED,
|
AttachmentState.CANCELED,
|
||||||
AttachmentState.ERROR -> {
|
AttachmentState.ERROR -> {
|
||||||
stopForeground(false)
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH)
|
||||||
notificationManager?.notify(attachmentNotification.notificationId, builder.build())
|
notificationManager?.notify(attachmentNotification.notificationId, builder.build())
|
||||||
} else -> {
|
} else -> {
|
||||||
startForeground(attachmentNotification.notificationId, builder.build())
|
startForeground(attachmentNotification.notificationId, builder.build())
|
||||||
|
|||||||
@@ -149,30 +149,23 @@ class KeyboardEntryNotificationService : LockNotificationService() {
|
|||||||
|
|
||||||
fun launchNotificationIfAllowed(context: Context, entry: EntryInfo, toast: Boolean) {
|
fun launchNotificationIfAllowed(context: Context, entry: EntryInfo, toast: Boolean) {
|
||||||
|
|
||||||
val containsURLToCopy = entry.url.isNotEmpty()
|
|
||||||
val containsUsernameToCopy = entry.username.isNotEmpty()
|
|
||||||
val containsPasswordToCopy = entry.password.isNotEmpty()
|
|
||||||
val containsExtraFieldToCopy = entry.customFields.isNotEmpty()
|
|
||||||
|
|
||||||
var startService = false
|
var startService = false
|
||||||
val intent = Intent(context, KeyboardEntryNotificationService::class.java)
|
val intent = Intent(context, KeyboardEntryNotificationService::class.java)
|
||||||
|
|
||||||
if (containsURLToCopy || containsUsernameToCopy || containsPasswordToCopy || containsExtraFieldToCopy) {
|
if (toast) {
|
||||||
if (toast) {
|
Toast.makeText(context,
|
||||||
Toast.makeText(context,
|
context.getString(R.string.keyboard_notification_entry_content_title,
|
||||||
context.getString(R.string.keyboard_notification_entry_content_title, entry.title),
|
entry.getVisualTitle()
|
||||||
Toast.LENGTH_SHORT).show()
|
),
|
||||||
}
|
Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
// Show the notification if allowed in Preferences
|
// Show the notification if allowed in Preferences
|
||||||
if (PreferencesUtil.isKeyboardNotificationEntryEnable(context)) {
|
if (PreferencesUtil.isKeyboardNotificationEntryEnable(context)) {
|
||||||
startService = true
|
startService = true
|
||||||
context.startService(intent.apply {
|
context.startService(intent.apply {
|
||||||
putExtra(ENTRY_INFO_KEY, entry)
|
putExtra(ENTRY_INFO_KEY, entry)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
} else {
|
|
||||||
MagikeyboardService.removeEntry(context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!startService)
|
if (!startService)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
package com.kunzisoft.keepass.services
|
package com.kunzisoft.keepass.services
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import androidx.core.app.ServiceCompat
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.LockReceiver
|
import com.kunzisoft.keepass.utils.LockReceiver
|
||||||
import com.kunzisoft.keepass.utils.registerLockReceiver
|
import com.kunzisoft.keepass.utils.registerLockReceiver
|
||||||
@@ -33,6 +34,7 @@ abstract class LockNotificationService : NotificationService() {
|
|||||||
|
|
||||||
protected open fun actionOnLock() {
|
protected open fun actionOnLock() {
|
||||||
// Stop the service in all cases
|
// Stop the service in all cases
|
||||||
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import android.os.Bundle
|
|||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DurationDialogFragmentCompat
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DurationDialogFragmentCompat
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ import com.kunzisoft.keepass.utils.UriUtil
|
|||||||
|
|
||||||
class NestedAppSettingsFragment : NestedSettingsFragment() {
|
class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||||
|
|
||||||
private var deleteKeysAlertDialog: AlertDialog? = null
|
private var warningAlertDialog: AlertDialog? = null
|
||||||
|
|
||||||
override fun onCreateScreenPreference(screen: Screen, savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreateScreenPreference(screen: Screen, savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
|
||||||
@@ -262,7 +262,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
val deviceCredentialChecked = deviceCredentialUnlockEnablePreference?.isChecked ?: false
|
val deviceCredentialChecked = deviceCredentialUnlockEnablePreference?.isChecked ?: false
|
||||||
if (!biometricChecked) {
|
if (!biometricChecked) {
|
||||||
biometricUnlockEnablePreference.isChecked = true
|
biometricUnlockEnablePreference.isChecked = true
|
||||||
deleteKeysMessage(activity) {
|
warningMessage(activity, keystoreWarning = false, deleteKeys = true) {
|
||||||
biometricUnlockEnablePreference.isChecked = false
|
biometricUnlockEnablePreference.isChecked = false
|
||||||
autoOpenPromptPreference?.isEnabled = deviceCredentialChecked
|
autoOpenPromptPreference?.isEnabled = deviceCredentialChecked
|
||||||
tempAdvancedUnlockPreference?.isEnabled = deviceCredentialChecked
|
tempAdvancedUnlockPreference?.isEnabled = deviceCredentialChecked
|
||||||
@@ -270,13 +270,17 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
} else {
|
} else {
|
||||||
if (deviceCredentialChecked) {
|
if (deviceCredentialChecked) {
|
||||||
biometricUnlockEnablePreference.isChecked = false
|
biometricUnlockEnablePreference.isChecked = false
|
||||||
deleteKeysMessage(activity) {
|
warningMessage(activity, keystoreWarning = true, deleteKeys = true) {
|
||||||
biometricUnlockEnablePreference.isChecked = true
|
biometricUnlockEnablePreference.isChecked = true
|
||||||
deviceCredentialUnlockEnablePreference?.isChecked = false
|
deviceCredentialUnlockEnablePreference?.isChecked = false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
autoOpenPromptPreference?.isEnabled = true
|
biometricUnlockEnablePreference.isChecked = false
|
||||||
tempAdvancedUnlockPreference?.isEnabled = true
|
warningMessage(activity, keystoreWarning = true, deleteKeys = false) {
|
||||||
|
biometricUnlockEnablePreference.isChecked = true
|
||||||
|
autoOpenPromptPreference?.isEnabled = true
|
||||||
|
tempAdvancedUnlockPreference?.isEnabled = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@@ -305,7 +309,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
val biometricChecked = biometricUnlockEnablePreference?.isChecked ?: false
|
val biometricChecked = biometricUnlockEnablePreference?.isChecked ?: false
|
||||||
if (!deviceCredentialChecked) {
|
if (!deviceCredentialChecked) {
|
||||||
deviceCredentialUnlockEnablePreference.isChecked = true
|
deviceCredentialUnlockEnablePreference.isChecked = true
|
||||||
deleteKeysMessage(activity) {
|
warningMessage(activity, keystoreWarning = false, deleteKeys = true) {
|
||||||
deviceCredentialUnlockEnablePreference.isChecked = false
|
deviceCredentialUnlockEnablePreference.isChecked = false
|
||||||
autoOpenPromptPreference?.isEnabled = biometricChecked
|
autoOpenPromptPreference?.isEnabled = biometricChecked
|
||||||
tempAdvancedUnlockPreference?.isEnabled = biometricChecked
|
tempAdvancedUnlockPreference?.isEnabled = biometricChecked
|
||||||
@@ -313,13 +317,17 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
} else {
|
} else {
|
||||||
if (biometricChecked) {
|
if (biometricChecked) {
|
||||||
deviceCredentialUnlockEnablePreference.isChecked = false
|
deviceCredentialUnlockEnablePreference.isChecked = false
|
||||||
deleteKeysMessage(activity) {
|
warningMessage(activity, keystoreWarning = true, deleteKeys = true) {
|
||||||
deviceCredentialUnlockEnablePreference.isChecked = true
|
deviceCredentialUnlockEnablePreference.isChecked = true
|
||||||
biometricUnlockEnablePreference?.isChecked = false
|
biometricUnlockEnablePreference?.isChecked = false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
autoOpenPromptPreference?.isEnabled = true
|
deviceCredentialUnlockEnablePreference.isChecked = false
|
||||||
tempAdvancedUnlockPreference?.isEnabled = true
|
warningMessage(activity, keystoreWarning = true, deleteKeys = false) {
|
||||||
|
deviceCredentialUnlockEnablePreference.isChecked = true
|
||||||
|
autoOpenPromptPreference?.isEnabled = true
|
||||||
|
tempAdvancedUnlockPreference?.isEnabled = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@@ -334,7 +342,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
|
|
||||||
tempAdvancedUnlockPreference?.setOnPreferenceClickListener {
|
tempAdvancedUnlockPreference?.setOnPreferenceClickListener {
|
||||||
tempAdvancedUnlockPreference.isChecked = !tempAdvancedUnlockPreference.isChecked
|
tempAdvancedUnlockPreference.isChecked = !tempAdvancedUnlockPreference.isChecked
|
||||||
deleteKeysMessage(activity) {
|
warningMessage(activity, keystoreWarning = false, deleteKeys = true) {
|
||||||
tempAdvancedUnlockPreference.isChecked = !tempAdvancedUnlockPreference.isChecked
|
tempAdvancedUnlockPreference.isChecked = !tempAdvancedUnlockPreference.isChecked
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@@ -343,7 +351,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
val deleteKeysFingerprints: Preference? = findPreference(getString(R.string.biometric_delete_all_key_key))
|
val deleteKeysFingerprints: Preference? = findPreference(getString(R.string.biometric_delete_all_key_key))
|
||||||
if (biometricUnlockSupported || deviceCredentialUnlockSupported) {
|
if (biometricUnlockSupported || deviceCredentialUnlockSupported) {
|
||||||
deleteKeysFingerprints?.setOnPreferenceClickListener {
|
deleteKeysFingerprints?.setOnPreferenceClickListener {
|
||||||
deleteKeysMessage(activity)
|
warningMessage(activity, keystoreWarning = false, deleteKeys = true)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -357,22 +365,36 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteKeysMessage(activity: FragmentActivity, validate: (()->Unit)? = null) {
|
private fun warningMessage(activity: FragmentActivity,
|
||||||
deleteKeysAlertDialog = AlertDialog.Builder(activity)
|
keystoreWarning: Boolean,
|
||||||
.setMessage(resources.getString(R.string.advanced_unlock_delete_all_key_warning))
|
deleteKeys: Boolean,
|
||||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
validate: (()->Unit)? = null) {
|
||||||
.setPositiveButton(resources.getString(android.R.string.ok)
|
var message = ""
|
||||||
) { _, _ ->
|
if (keystoreWarning) {
|
||||||
validate?.invoke()
|
message += resources.getString(R.string.advanced_unlock_prompt_store_credential_message)
|
||||||
deleteKeysAlertDialog?.setOnDismissListener(null)
|
message += "\n\n" + resources.getString(R.string.advanced_unlock_keystore_warning)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
}
|
||||||
AdvancedUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity)
|
if (keystoreWarning && deleteKeys) {
|
||||||
}
|
message += "\n\n"
|
||||||
|
}
|
||||||
|
if (deleteKeys) {
|
||||||
|
message += resources.getString(R.string.advanced_unlock_delete_all_key_warning)
|
||||||
|
}
|
||||||
|
warningAlertDialog = AlertDialog.Builder(activity)
|
||||||
|
.setMessage(message)
|
||||||
|
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||||
|
.setPositiveButton(resources.getString(android.R.string.ok)
|
||||||
|
) { _, _ ->
|
||||||
|
validate?.invoke()
|
||||||
|
warningAlertDialog?.setOnDismissListener(null)
|
||||||
|
if (deleteKeys && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
AdvancedUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity)
|
||||||
}
|
}
|
||||||
.setNegativeButton(resources.getString(android.R.string.cancel)
|
}
|
||||||
) { _, _ ->}
|
.setNegativeButton(resources.getString(android.R.string.cancel)
|
||||||
.create()
|
) { _, _ ->}
|
||||||
deleteKeysAlertDialog?.show()
|
.create()
|
||||||
|
warningAlertDialog?.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onCreateAppearancePreferences(rootKey: String?) {
|
private fun onCreateAppearancePreferences(rootKey: String?) {
|
||||||
@@ -382,13 +404,17 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
findPreference<ListPreference>(getString(R.string.setting_style_key))?.setOnPreferenceChangeListener { _, newValue ->
|
findPreference<ListPreference>(getString(R.string.setting_style_key))?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
var styleEnabled = true
|
var styleEnabled = true
|
||||||
val styleIdString = newValue as String
|
val styleIdString = newValue as String
|
||||||
if (BuildConfig.CLOSED_STORE || !Education.isEducationScreenReclickedPerformed(activity))
|
if (!UriUtil.contributingUser(activity)) {
|
||||||
for (themeIdDisabled in BuildConfig.STYLES_DISABLED) {
|
for (themeIdDisabled in BuildConfig.STYLES_DISABLED) {
|
||||||
if (themeIdDisabled == styleIdString) {
|
if (themeIdDisabled == styleIdString) {
|
||||||
styleEnabled = false
|
styleEnabled = false
|
||||||
ProFeatureDialogFragment().show(parentFragmentManager, "pro_feature_dialog")
|
ProFeatureDialogFragment().show(
|
||||||
|
parentFragmentManager,
|
||||||
|
"pro_feature_dialog"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (styleEnabled) {
|
if (styleEnabled) {
|
||||||
Stylish.assignStyle(activity, styleIdString)
|
Stylish.assignStyle(activity, styleIdString)
|
||||||
// Relaunch the current activity to redraw theme
|
// Relaunch the current activity to redraw theme
|
||||||
@@ -409,13 +435,17 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
findPreference<IconPackListPreference>(getString(R.string.setting_icon_pack_choose_key))?.setOnPreferenceChangeListener { _, newValue ->
|
findPreference<IconPackListPreference>(getString(R.string.setting_icon_pack_choose_key))?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
var iconPackEnabled = true
|
var iconPackEnabled = true
|
||||||
val iconPackId = newValue as String
|
val iconPackId = newValue as String
|
||||||
if (BuildConfig.CLOSED_STORE || !Education.isEducationScreenReclickedPerformed(activity))
|
if (!UriUtil.contributingUser(activity)) {
|
||||||
for (iconPackIdDisabled in BuildConfig.ICON_PACKS_DISABLED) {
|
for (iconPackIdDisabled in BuildConfig.ICON_PACKS_DISABLED) {
|
||||||
if (iconPackIdDisabled == iconPackId) {
|
if (iconPackIdDisabled == iconPackId) {
|
||||||
iconPackEnabled = false
|
iconPackEnabled = false
|
||||||
ProFeatureDialogFragment().show(parentFragmentManager, "pro_feature_dialog")
|
ProFeatureDialogFragment().show(
|
||||||
|
parentFragmentManager,
|
||||||
|
"pro_feature_dialog"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (iconPackEnabled) {
|
if (iconPackEnabled) {
|
||||||
IconPackChooser.setSelectedIconPack(iconPackId)
|
IconPackChooser.setSelectedIconPack(iconPackId)
|
||||||
}
|
}
|
||||||
@@ -501,7 +531,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
deleteKeysAlertDialog?.dismiss()
|
warningAlertDialog?.dismiss()
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,9 +539,8 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
if (mCount == 10) {
|
if (mCount == 10 && !BuildConfig.CLOSED_STORE) {
|
||||||
Education.getEducationSharedPreferences(activity).edit()
|
Education.setEducationScreenReclickedPerformed(activity)
|
||||||
.putBoolean(getString(R.string.education_screen_reclicked_key), true).apply()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,12 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
||||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
import com.kunzisoft.keepass.education.Education
|
import com.kunzisoft.keepass.education.Education
|
||||||
|
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||||
|
import com.kunzisoft.keepass.password.PassphraseGenerator
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
object PreferencesUtil {
|
object PreferencesUtil {
|
||||||
@@ -110,6 +114,18 @@ object PreferencesUtil {
|
|||||||
context.resources.getBoolean(R.bool.show_entry_colors_default))
|
context.resources.getBoolean(R.bool.show_entry_colors_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hideProtectedValue(context: Context): Boolean {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return prefs.getBoolean(context.getString(R.string.hide_password_key),
|
||||||
|
context.resources.getBoolean(R.bool.hide_password_default))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun colorizePassword(context: Context): Boolean {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return prefs.getBoolean(context.getString(R.string.colorize_password_key),
|
||||||
|
context.resources.getBoolean(R.bool.colorize_password_default))
|
||||||
|
}
|
||||||
|
|
||||||
fun showUsernamesListEntries(context: Context): Boolean {
|
fun showUsernamesListEntries(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.list_entries_show_username_key),
|
return prefs.getBoolean(context.getString(R.string.list_entries_show_username_key),
|
||||||
@@ -151,7 +167,7 @@ object PreferencesUtil {
|
|||||||
|
|
||||||
fun setStyle(context: Context, styleString: String) {
|
fun setStyle(context: Context, styleString: String) {
|
||||||
var tempThemeString = styleString
|
var tempThemeString = styleString
|
||||||
if (BuildConfig.CLOSED_STORE || !Education.isEducationScreenReclickedPerformed(context)) {
|
if (!UriUtil.contributingUser(context)) {
|
||||||
if (tempThemeString in BuildConfig.STYLES_DISABLED) {
|
if (tempThemeString in BuildConfig.STYLES_DISABLED) {
|
||||||
tempThemeString = Stylish.defaultStyle(context)
|
tempThemeString = Stylish.defaultStyle(context)
|
||||||
}
|
}
|
||||||
@@ -191,15 +207,194 @@ object PreferencesUtil {
|
|||||||
|
|
||||||
fun getDefaultPasswordLength(context: Context): Int {
|
fun getDefaultPasswordLength(context: Context): Int {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getInt(context.getString(R.string.password_length_key),
|
return prefs.getInt(context.getString(R.string.password_generator_length_key),
|
||||||
Integer.parseInt(context.getString(R.string.default_password_length)))
|
context.resources.getInteger(R.integer.password_generator_length_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDefaultPasswordCharacters(context: Context): Set<String>? {
|
fun setDefaultPasswordLength(context: Context, passwordLength: Int) {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
|
||||||
|
putInt(
|
||||||
|
context.getString(R.string.password_generator_length_key),
|
||||||
|
passwordLength
|
||||||
|
)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDefaultPasswordOptions(context: Context): Set<String> {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getStringSet(context.getString(R.string.list_password_generator_options_key),
|
return prefs.getStringSet(context.getString(R.string.password_generator_options_key),
|
||||||
HashSet(listOf(*context.resources
|
HashSet(listOf(*context.resources
|
||||||
.getStringArray(R.array.list_password_generator_options_default_values))))
|
.getStringArray(R.array.list_password_generator_options_default_values)))) ?: setOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDefaultPasswordOptions(context: Context, passwordOptionsSet: Set<String>) {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
|
||||||
|
putStringSet(
|
||||||
|
context.getString(R.string.password_generator_options_key),
|
||||||
|
passwordOptionsSet
|
||||||
|
)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDefaultPasswordConsiderChars(context: Context): String {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return prefs.getString(context.getString(R.string.password_generator_consider_chars_key),
|
||||||
|
context.getString(R.string.password_generator_consider_chars_default)) ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDefaultPasswordConsiderChars(context: Context, considerChars: String) {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
|
||||||
|
putString(
|
||||||
|
context.getString(R.string.password_generator_consider_chars_key),
|
||||||
|
considerChars
|
||||||
|
)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDefaultPasswordIgnoreChars(context: Context): String {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return prefs.getString(context.getString(R.string.password_generator_ignore_chars_key),
|
||||||
|
context.getString(R.string.password_generator_ignore_chars_default)) ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDefaultPasswordIgnoreChars(context: Context, ignoreChars: String) {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
|
||||||
|
putString(
|
||||||
|
context.getString(R.string.password_generator_ignore_chars_key),
|
||||||
|
ignoreChars
|
||||||
|
)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDefaultPassphraseWordCount(context: Context): Int {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return prefs.getInt(context.getString(R.string.passphrase_generator_word_count_key),
|
||||||
|
context.resources.getInteger(R.integer.passphrase_generator_word_count_default))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDefaultPassphraseWordCount(context: Context, passphraseWordCount: Int) {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
|
||||||
|
putInt(
|
||||||
|
context.getString(R.string.passphrase_generator_word_count_key),
|
||||||
|
passphraseWordCount
|
||||||
|
)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDefaultPassphraseWordCase(context: Context): PassphraseGenerator.WordCase {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return PassphraseGenerator.WordCase
|
||||||
|
.getByOrdinal(prefs.getInt(context
|
||||||
|
.getString(R.string.passphrase_generator_word_case_key),
|
||||||
|
0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDefaultPassphraseWordCase(context: Context, wordCase: PassphraseGenerator.WordCase) {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
|
||||||
|
putInt(
|
||||||
|
context.getString(R.string.passphrase_generator_word_case_key),
|
||||||
|
wordCase.ordinal
|
||||||
|
)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDefaultPassphraseSeparator(context: Context): String {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return prefs.getString(context.getString(R.string.passphrase_generator_separator_key),
|
||||||
|
context.getString(R.string.passphrase_generator_separator_default)) ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDefaultPassphraseSeparator(context: Context, separator: String) {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
|
||||||
|
putString(
|
||||||
|
context.getString(R.string.passphrase_generator_separator_key),
|
||||||
|
separator
|
||||||
|
)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDefaultSearchParameters(context: Context): SearchParameters {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return SearchParameters().apply {
|
||||||
|
caseSensitive = prefs.getBoolean(context.getString(R.string.search_option_case_sensitive_key),
|
||||||
|
context.resources.getBoolean(R.bool.search_option_case_sensitive_default))
|
||||||
|
isRegex = prefs.getBoolean(context.getString(R.string.search_option_regex_key),
|
||||||
|
context.resources.getBoolean(R.bool.search_option_regex_default))
|
||||||
|
searchInTitles = prefs.getBoolean(context.getString(R.string.search_option_title_key),
|
||||||
|
context.resources.getBoolean(R.bool.search_option_title_default))
|
||||||
|
searchInUsernames = prefs.getBoolean(context.getString(R.string.search_option_username_key),
|
||||||
|
context.resources.getBoolean(R.bool.search_option_username_default))
|
||||||
|
searchInPasswords = prefs.getBoolean(context.getString(R.string.search_option_password_key),
|
||||||
|
context.resources.getBoolean(R.bool.search_option_password_default))
|
||||||
|
searchInUrls = prefs.getBoolean(context.getString(R.string.search_option_url_key),
|
||||||
|
context.resources.getBoolean(R.bool.search_option_url_default))
|
||||||
|
searchInExpired = prefs.getBoolean(context.getString(R.string.search_option_expired_key),
|
||||||
|
context.resources.getBoolean(R.bool.search_option_expired_default))
|
||||||
|
searchInNotes = prefs.getBoolean(context.getString(R.string.search_option_note_key),
|
||||||
|
context.resources.getBoolean(R.bool.search_option_note_default))
|
||||||
|
searchInOTP = prefs.getBoolean(context.getString(R.string.search_option_otp_key),
|
||||||
|
context.resources.getBoolean(R.bool.search_option_otp_default))
|
||||||
|
searchInOther = prefs.getBoolean(context.getString(R.string.search_option_other_key),
|
||||||
|
context.resources.getBoolean(R.bool.search_option_other_default))
|
||||||
|
searchInUUIDs = prefs.getBoolean(context.getString(R.string.search_option_uuid_key),
|
||||||
|
context.resources.getBoolean(R.bool.search_option_uuid_default))
|
||||||
|
searchInTags = prefs.getBoolean(context.getString(R.string.search_option_tag_key),
|
||||||
|
context.resources.getBoolean(R.bool.search_option_tag_default))
|
||||||
|
searchInCurrentGroup = prefs.getBoolean(context.getString(R.string.search_option_current_group_key),
|
||||||
|
context.resources.getBoolean(R.bool.search_option_current_group_default))
|
||||||
|
searchInSearchableGroup = prefs.getBoolean(context.getString(R.string.search_option_searchable_group_key),
|
||||||
|
context.resources.getBoolean(R.bool.search_option_searchable_group_default))
|
||||||
|
searchInRecycleBin = prefs.getBoolean(context.getString(R.string.search_option_recycle_bin_key),
|
||||||
|
context.resources.getBoolean(R.bool.search_option_recycle_bin_default))
|
||||||
|
searchInTemplates = prefs.getBoolean(context.getString(R.string.search_option_templates_key),
|
||||||
|
context.resources.getBoolean(R.bool.search_option_templates_default))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDefaultSearchParameters(context: Context, searchParameters: SearchParameters) {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
|
||||||
|
putBoolean(context.getString(R.string.search_option_case_sensitive_key),
|
||||||
|
searchParameters.caseSensitive)
|
||||||
|
putBoolean(context.getString(R.string.search_option_regex_key),
|
||||||
|
searchParameters.isRegex)
|
||||||
|
putBoolean(context.getString(R.string.search_option_title_key),
|
||||||
|
searchParameters.searchInTitles)
|
||||||
|
putBoolean(context.getString(R.string.search_option_username_key),
|
||||||
|
searchParameters.searchInUsernames)
|
||||||
|
putBoolean(context.getString(R.string.search_option_password_key),
|
||||||
|
searchParameters.searchInPasswords)
|
||||||
|
putBoolean(context.getString(R.string.search_option_url_key),
|
||||||
|
searchParameters.searchInUrls)
|
||||||
|
putBoolean(context.getString(R.string.search_option_expired_key),
|
||||||
|
searchParameters.searchInExpired)
|
||||||
|
putBoolean(context.getString(R.string.search_option_note_key),
|
||||||
|
searchParameters.searchInNotes)
|
||||||
|
putBoolean(context.getString(R.string.search_option_otp_key),
|
||||||
|
searchParameters.searchInOTP)
|
||||||
|
putBoolean(context.getString(R.string.search_option_other_key),
|
||||||
|
searchParameters.searchInOther)
|
||||||
|
putBoolean(context.getString(R.string.search_option_uuid_key),
|
||||||
|
searchParameters.searchInUUIDs)
|
||||||
|
putBoolean(context.getString(R.string.search_option_tag_key),
|
||||||
|
searchParameters.searchInTags)
|
||||||
|
putBoolean(context.getString(R.string.search_option_current_group_key),
|
||||||
|
searchParameters.searchInCurrentGroup)
|
||||||
|
putBoolean(context.getString(R.string.search_option_searchable_group_key),
|
||||||
|
searchParameters.searchInSearchableGroup)
|
||||||
|
putBoolean(context.getString(R.string.search_option_recycle_bin_key),
|
||||||
|
searchParameters.searchInRecycleBin)
|
||||||
|
putBoolean(context.getString(R.string.search_option_templates_key),
|
||||||
|
searchParameters.searchInTemplates)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isClipboardNotificationsEnable(context: Context): Boolean {
|
fun isClipboardNotificationsEnable(context: Context): Boolean {
|
||||||
@@ -350,12 +545,6 @@ object PreferencesUtil {
|
|||||||
context.resources.getBoolean(R.bool.sort_recycle_bin_bottom_default))
|
context.resources.getBoolean(R.bool.sort_recycle_bin_bottom_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hideProtectedValue(context: Context): Boolean {
|
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
return prefs.getBoolean(context.getString(R.string.hide_password_key),
|
|
||||||
context.resources.getBoolean(R.bool.hide_password_default))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fieldFontIsInVisibility(context: Context): Boolean {
|
fun fieldFontIsInVisibility(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.monospace_font_fields_enable_key),
|
return prefs.getBoolean(context.getString(R.string.monospace_font_fields_enable_key),
|
||||||
@@ -431,14 +620,8 @@ object PreferencesUtil {
|
|||||||
context.resources.getBoolean(R.bool.keyboard_selection_entry_default))
|
context.resources.getBoolean(R.bool.keyboard_selection_entry_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isKeyboardSearchShareEnable(context: Context): Boolean {
|
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
return prefs.getBoolean(context.getString(R.string.keyboard_search_share_key),
|
|
||||||
context.resources.getBoolean(R.bool.keyboard_search_share_default))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isKeyboardSaveSearchInfoEnable(context: Context): Boolean {
|
fun isKeyboardSaveSearchInfoEnable(context: Context): Boolean {
|
||||||
if (!isKeyboardSearchShareEnable(context))
|
if (!MagikeyboardService.activatedInSettings(context))
|
||||||
return false
|
return false
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.keyboard_save_search_info_key),
|
return prefs.getBoolean(context.getString(R.string.keyboard_save_search_info_key),
|
||||||
@@ -469,6 +652,12 @@ object PreferencesUtil {
|
|||||||
context.resources.getBoolean(R.bool.keyboard_previous_database_credentials_default))
|
context.resources.getBoolean(R.bool.keyboard_previous_database_credentials_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isKeyboardPreviousSearchEnable(context: Context): Boolean {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return prefs.getBoolean(context.getString(R.string.keyboard_previous_search_key),
|
||||||
|
context.resources.getBoolean(R.bool.keyboard_previous_search_default))
|
||||||
|
}
|
||||||
|
|
||||||
fun isKeyboardPreviousFillInEnable(context: Context): Boolean {
|
fun isKeyboardPreviousFillInEnable(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.keyboard_previous_fill_in_key),
|
return prefs.getBoolean(context.getString(R.string.keyboard_previous_fill_in_key),
|
||||||
@@ -487,12 +676,6 @@ object PreferencesUtil {
|
|||||||
context.resources.getBoolean(R.bool.autofill_close_database_default))
|
context.resources.getBoolean(R.bool.autofill_close_database_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))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isAutofillInlineSuggestionsEnable(context: Context): Boolean {
|
fun isAutofillInlineSuggestionsEnable(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.autofill_inline_suggestions_key),
|
return prefs.getBoolean(context.getString(R.string.autofill_inline_suggestions_key),
|
||||||
@@ -608,9 +791,6 @@ object PreferencesUtil {
|
|||||||
context.getString(R.string.lock_database_screen_off_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.lock_database_screen_off_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.lock_database_back_root_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.lock_database_back_root_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.lock_database_show_button_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.lock_database_show_button_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.password_length_key) -> editor.putInt(name, value.toInt())
|
|
||||||
context.getString(R.string.list_password_generator_options_key) -> editor.putStringSet(name, getStringSetFromProperties(value))
|
|
||||||
context.getString(R.string.hide_password_key) -> editor.putBoolean(name, value.toBoolean())
|
|
||||||
context.getString(R.string.allow_copy_password_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.allow_copy_password_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.remember_database_locations_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.remember_database_locations_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.show_recent_files_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.show_recent_files_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
@@ -631,16 +811,15 @@ object PreferencesUtil {
|
|||||||
context.getString(R.string.keyboard_notification_entry_clear_close_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.keyboard_notification_entry_clear_close_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.keyboard_entry_timeout_key) -> editor.putString(name, value.toLong().toString())
|
context.getString(R.string.keyboard_entry_timeout_key) -> editor.putString(name, value.toLong().toString())
|
||||||
context.getString(R.string.keyboard_selection_entry_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.keyboard_selection_entry_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.keyboard_search_share_key) -> editor.putBoolean(name, value.toBoolean())
|
|
||||||
context.getString(R.string.keyboard_save_search_info_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.keyboard_save_search_info_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.keyboard_auto_go_action_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.keyboard_auto_go_action_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.keyboard_key_vibrate_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.keyboard_key_vibrate_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.keyboard_key_sound_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.keyboard_key_sound_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.keyboard_previous_database_credentials_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.keyboard_previous_database_credentials_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_previous_search_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.keyboard_previous_fill_in_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.keyboard_previous_fill_in_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.keyboard_previous_lock_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.keyboard_previous_lock_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.autofill_close_database_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.autofill_close_database_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.autofill_auto_search_key) -> editor.putBoolean(name, value.toBoolean())
|
|
||||||
context.getString(R.string.autofill_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.autofill_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.autofill_manual_selection_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.autofill_manual_selection_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.autofill_save_search_info_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.autofill_save_search_info_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
@@ -652,6 +831,8 @@ object PreferencesUtil {
|
|||||||
context.getString(R.string.setting_style_brightness_key) -> editor.putString(name, value)
|
context.getString(R.string.setting_style_brightness_key) -> editor.putString(name, value)
|
||||||
context.getString(R.string.setting_icon_pack_choose_key) -> editor.putString(name, value)
|
context.getString(R.string.setting_icon_pack_choose_key) -> editor.putString(name, value)
|
||||||
context.getString(R.string.show_entry_colors_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.show_entry_colors_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.hide_password_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.colorize_password_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.list_entries_show_username_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.list_entries_show_username_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.list_groups_show_number_entries_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.list_groups_show_number_entries_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.show_otp_token_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.show_otp_token_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
@@ -661,6 +842,14 @@ object PreferencesUtil {
|
|||||||
context.getString(R.string.hide_expired_entries_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.hide_expired_entries_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.enable_education_screens_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.enable_education_screens_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
|
||||||
|
context.getString(R.string.password_generator_length_key) -> editor.putInt(name, value.toInt())
|
||||||
|
context.getString(R.string.password_generator_options_key) -> editor.putStringSet(name, getStringSetFromProperties(value))
|
||||||
|
context.getString(R.string.password_generator_consider_chars_key) -> editor.putString(name, value)
|
||||||
|
context.getString(R.string.password_generator_ignore_chars_key) -> editor.putString(name, value)
|
||||||
|
context.getString(R.string.passphrase_generator_word_count_key) -> editor.putInt(name, value.toInt())
|
||||||
|
context.getString(R.string.passphrase_generator_word_case_key) -> editor.putInt(name, value.toInt())
|
||||||
|
context.getString(R.string.passphrase_generator_separator_key) -> editor.putString(name, value)
|
||||||
|
|
||||||
context.getString(R.string.sort_node_key) -> editor.putString(name, value)
|
context.getString(R.string.sort_node_key) -> editor.putString(name, value)
|
||||||
context.getString(R.string.sort_group_before_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.sort_group_before_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.sort_ascending_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.sort_ascending_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
|||||||
@@ -225,10 +225,14 @@ open class SettingsActivity
|
|||||||
commit()
|
commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
toolbar?.title = NestedSettingsFragment.retrieveTitle(resources, key)
|
setTitle(key)
|
||||||
hideOrShowLockButton(key)
|
hideOrShowLockButton(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun setTitle(key: NestedSettingsFragment.Screen) {
|
||||||
|
toolbar?.title = NestedSettingsFragment.retrieveTitle(resources, key)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To keep the current screen when activity is reloaded
|
* To keep the current screen when activity is reloaded
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class SettingsAdvancedUnlockActivity : SettingsActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
mTimeoutEnable = false
|
mTimeoutEnable = false
|
||||||
|
setTitle(NestedSettingsFragment.Screen.ADVANCED_UNLOCK)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun retrieveMainFragment(): Fragment {
|
override fun retrieveMainFragment(): Fragment {
|
||||||
|
|||||||
@@ -25,17 +25,16 @@ import android.content.Intent
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.AboutActivity
|
import com.kunzisoft.keepass.activities.AboutActivity
|
||||||
import com.kunzisoft.keepass.settings.SettingsActivity
|
import com.kunzisoft.keepass.settings.SettingsActivity
|
||||||
|
|
||||||
object MenuUtil {
|
object MenuUtil {
|
||||||
|
|
||||||
fun defaultMenuInflater(inflater: MenuInflater, menu: Menu) {
|
fun defaultMenuInflater(context: Context, inflater: MenuInflater, menu: Menu) {
|
||||||
inflater.inflate(R.menu.settings, menu)
|
inflater.inflate(R.menu.settings, menu)
|
||||||
inflater.inflate(R.menu.about, menu)
|
inflater.inflate(R.menu.about, menu)
|
||||||
if (!(BuildConfig.FULL_VERSION && BuildConfig.CLOSED_STORE))
|
if (!UriUtil.contributingUser(context))
|
||||||
menu.findItem(R.id.menu_contribute)?.isVisible = false
|
menu.findItem(R.id.menu_contribute)?.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import android.widget.Toast
|
|||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
|
import com.kunzisoft.keepass.education.Education
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -68,7 +69,18 @@ object UriUtil {
|
|||||||
return null
|
return null
|
||||||
return when {
|
return when {
|
||||||
isFileScheme(fileUri) -> fileUri.path?.let { FileOutputStream(it) }
|
isFileScheme(fileUri) -> fileUri.path?.let { FileOutputStream(it) }
|
||||||
isContentScheme(fileUri) -> contentResolver.openOutputStream(fileUri, "rwt")
|
isContentScheme(fileUri) -> {
|
||||||
|
try {
|
||||||
|
contentResolver.openOutputStream(fileUri, "wt")
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
Log.e(TAG, "Unable to open stream in `wt` mode, retry in `rwt` mode.", e)
|
||||||
|
// https://issuetracker.google.com/issues/180526528
|
||||||
|
// Try with rwt to fix content provider issue
|
||||||
|
val outStream = contentResolver.openOutputStream(fileUri, "rwt")
|
||||||
|
Log.w(TAG, "`rwt` mode used.")
|
||||||
|
outStream
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,9 +267,16 @@ object UriUtil {
|
|||||||
gotoUrl(context, context.getString(resId))
|
gotoUrl(context, context.getString(resId))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isExternalAppInstalled(context: Context, packageName: String): Boolean {
|
fun contributingUser(context: Context): Boolean {
|
||||||
|
return (Education.isEducationScreenReclickedPerformed(context)
|
||||||
|
|| isExternalAppInstalled(context, "com.kunzisoft.keepass.pro")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isExternalAppInstalled(context: Context, packageName: String): Boolean {
|
||||||
try {
|
try {
|
||||||
context.applicationContext.packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
|
context.applicationContext.packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
|
||||||
|
Education.setEducationScreenReclickedPerformed(context)
|
||||||
return true
|
return true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "App not accessible", e)
|
Log.e(TAG, "App not accessible", e)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
|
|||||||
private var unlockAnimatedVector: FingerPrintAnimatedVector? = null
|
private var unlockAnimatedVector: FingerPrintAnimatedVector? = null
|
||||||
private var unlockTitleTextView: TextView? = null
|
private var unlockTitleTextView: TextView? = null
|
||||||
private var unlockMessageTextView: TextView? = null
|
private var unlockMessageTextView: TextView? = null
|
||||||
var unlockIconImageView: ImageView? = null
|
private var unlockIconImageView: ImageView? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
|||||||
defStyle: Int = 0)
|
defStyle: Int = 0)
|
||||||
: FrameLayout(context, attrs, defStyle) {
|
: FrameLayout(context, attrs, defStyle) {
|
||||||
|
|
||||||
private var passwordView: EditText
|
private var passwordTextView: EditText
|
||||||
private var keyFileSelectionView: KeyFileSelectionView
|
private var keyFileSelectionView: KeyFileSelectionView
|
||||||
private var checkboxPasswordView: CompoundButton
|
private var checkboxPasswordView: CompoundButton
|
||||||
private var checkboxKeyFileView: CompoundButton
|
private var checkboxKeyFileView: CompoundButton
|
||||||
@@ -60,7 +60,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
|||||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||||
inflater?.inflate(R.layout.view_main_credentials, this)
|
inflater?.inflate(R.layout.view_main_credentials, this)
|
||||||
|
|
||||||
passwordView = findViewById(R.id.password)
|
passwordTextView = findViewById(R.id.password_text_view)
|
||||||
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
||||||
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
||||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
||||||
@@ -75,8 +75,8 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
passwordView.setOnEditorActionListener(onEditorActionListener)
|
passwordTextView.setOnEditorActionListener(onEditorActionListener)
|
||||||
passwordView.addTextChangedListener(object : TextWatcher {
|
passwordTextView.addTextChangedListener(object : TextWatcher {
|
||||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||||
|
|
||||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||||
@@ -86,7 +86,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
|||||||
checkboxPasswordView.isChecked = true
|
checkboxPasswordView.isChecked = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
passwordView.setOnKeyListener { _, _, keyEvent ->
|
passwordTextView.setOnKeyListener { _, _, keyEvent ->
|
||||||
var handled = false
|
var handled = false
|
||||||
if (keyEvent.action == KeyEvent.ACTION_DOWN
|
if (keyEvent.action == KeyEvent.ACTION_DOWN
|
||||||
&& keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER
|
&& keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER
|
||||||
@@ -108,11 +108,11 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
|||||||
|
|
||||||
fun populatePasswordTextView(text: String?) {
|
fun populatePasswordTextView(text: String?) {
|
||||||
if (text == null || text.isEmpty()) {
|
if (text == null || text.isEmpty()) {
|
||||||
passwordView.setText("")
|
passwordTextView.setText("")
|
||||||
if (checkboxPasswordView.isChecked)
|
if (checkboxPasswordView.isChecked)
|
||||||
checkboxPasswordView.isChecked = false
|
checkboxPasswordView.isChecked = false
|
||||||
} else {
|
} else {
|
||||||
passwordView.setText(text)
|
passwordTextView.setText(text)
|
||||||
if (checkboxPasswordView.isChecked)
|
if (checkboxPasswordView.isChecked)
|
||||||
checkboxPasswordView.isChecked = true
|
checkboxPasswordView.isChecked = true
|
||||||
}
|
}
|
||||||
@@ -137,7 +137,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
|||||||
fun getMainCredential(): MainCredential {
|
fun getMainCredential(): MainCredential {
|
||||||
return MainCredential().apply {
|
return MainCredential().apply {
|
||||||
this.masterPassword = if (checkboxPasswordView.isChecked)
|
this.masterPassword = if (checkboxPasswordView.isChecked)
|
||||||
passwordView.text?.toString() else null
|
passwordTextView.text?.toString() else null
|
||||||
this.keyFileUri = if (checkboxKeyFileView.isChecked)
|
this.keyFileUri = if (checkboxKeyFileView.isChecked)
|
||||||
keyFileSelectionView.uri else null
|
keyFileSelectionView.uri else null
|
||||||
}
|
}
|
||||||
@@ -163,7 +163,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
|||||||
*/
|
*/
|
||||||
fun retrieveCredentialForStorage(listener: CredentialStorageListener): ByteArray? {
|
fun retrieveCredentialForStorage(listener: CredentialStorageListener): ByteArray? {
|
||||||
return when (mCredentialStorage) {
|
return when (mCredentialStorage) {
|
||||||
CredentialStorage.PASSWORD -> listener.passwordToStore(passwordView.text?.toString())
|
CredentialStorage.PASSWORD -> listener.passwordToStore(passwordTextView.text?.toString())
|
||||||
CredentialStorage.KEY_FILE -> listener.keyfileToStore(keyFileSelectionView.uri)
|
CredentialStorage.KEY_FILE -> listener.keyfileToStore(keyFileSelectionView.uri)
|
||||||
CredentialStorage.HARDWARE_KEY -> listener.hardwareKeyToStore()
|
CredentialStorage.HARDWARE_KEY -> listener.hardwareKeyToStore()
|
||||||
}
|
}
|
||||||
@@ -176,15 +176,15 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun requestPasswordFocus() {
|
fun requestPasswordFocus() {
|
||||||
passwordView.requestFocusFromTouch()
|
passwordTextView.requestFocusFromTouch()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto select the password field and open keyboard
|
// Auto select the password field and open keyboard
|
||||||
fun focusPasswordFieldAndOpenKeyboard() {
|
fun focusPasswordFieldAndOpenKeyboard() {
|
||||||
passwordView.postDelayed({
|
passwordTextView.postDelayed({
|
||||||
passwordView.requestFocusFromTouch()
|
passwordTextView.requestFocusFromTouch()
|
||||||
val inputMethodManager = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as? InputMethodManager?
|
val inputMethodManager = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as? InputMethodManager?
|
||||||
inputMethodManager?.showSoftInput(passwordView, InputMethodManager.SHOW_IMPLICIT)
|
inputMethodManager?.showSoftInput(passwordTextView, InputMethodManager.SHOW_IMPLICIT)
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
156
app/src/main/java/com/kunzisoft/keepass/view/PassKeyView.kt
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 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.view
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.InputType
|
||||||
|
import android.text.SpannableString
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||||
|
import com.kunzisoft.keepass.password.PasswordEntropy
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
|
class PassKeyView @JvmOverloads constructor(context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyle: Int = 0)
|
||||||
|
: FrameLayout(context, attrs, defStyle) {
|
||||||
|
|
||||||
|
private var mPasswordEntropyCalculator: PasswordEntropy? = null
|
||||||
|
|
||||||
|
private val passwordInputLayout: TextInputLayout
|
||||||
|
private val passwordText: TextView
|
||||||
|
private val passwordStrengthProgress: LinearProgressIndicator
|
||||||
|
private val passwordEntropy: TextView
|
||||||
|
|
||||||
|
private var mViewHint: String = ""
|
||||||
|
private var mMaxLines: Int = 3
|
||||||
|
private var mShowPassword: Boolean = false
|
||||||
|
|
||||||
|
private var mPasswordTextWatcher: MutableList<TextWatcher> = mutableListOf()
|
||||||
|
private val passwordTextWatcher = object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
|
||||||
|
mPasswordTextWatcher.forEach {
|
||||||
|
it.beforeTextChanged(charSequence, i, i1, i2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
|
||||||
|
mPasswordTextWatcher.forEach {
|
||||||
|
it.onTextChanged(charSequence, i, i1, i2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun afterTextChanged(editable: Editable) {
|
||||||
|
mPasswordTextWatcher.forEach {
|
||||||
|
it.afterTextChanged(editable)
|
||||||
|
}
|
||||||
|
getEntropyStrength(editable.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
context.theme.obtainStyledAttributes(
|
||||||
|
attrs,
|
||||||
|
R.styleable.PassKeyView,
|
||||||
|
0, 0).apply {
|
||||||
|
try {
|
||||||
|
mViewHint = getString(R.styleable.PassKeyView_passKeyHint)
|
||||||
|
?: context.getString(R.string.password)
|
||||||
|
mMaxLines = getInteger(R.styleable.PassKeyView_passKeyMaxLines, mMaxLines)
|
||||||
|
mShowPassword = getBoolean(R.styleable.PassKeyView_passKeyVisible,
|
||||||
|
!PreferencesUtil.hideProtectedValue(context))
|
||||||
|
} finally {
|
||||||
|
recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||||
|
inflater?.inflate(R.layout.view_passkey, this)
|
||||||
|
|
||||||
|
passwordInputLayout = findViewById(R.id.password_input_layout)
|
||||||
|
passwordInputLayout?.hint = mViewHint
|
||||||
|
passwordText = findViewById(R.id.password_text)
|
||||||
|
if (mShowPassword) {
|
||||||
|
passwordText?.inputType = passwordText.inputType or
|
||||||
|
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||||
|
}
|
||||||
|
passwordText?.maxLines = mMaxLines
|
||||||
|
passwordText?.applyFontVisibility()
|
||||||
|
passwordText.addTextChangedListener(passwordTextWatcher)
|
||||||
|
passwordStrengthProgress = findViewById(R.id.password_strength_progress)
|
||||||
|
passwordStrengthProgress?.apply {
|
||||||
|
setIndicatorColor(PasswordEntropy.Strength.RISKY.color)
|
||||||
|
progress = 0
|
||||||
|
max = 100
|
||||||
|
}
|
||||||
|
passwordEntropy = findViewById(R.id.password_entropy)
|
||||||
|
|
||||||
|
mPasswordEntropyCalculator = PasswordEntropy {
|
||||||
|
passwordText?.text?.toString()?.let { firstPassword ->
|
||||||
|
getEntropyStrength(firstPassword)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getEntropyStrength(passwordText: String) {
|
||||||
|
mPasswordEntropyCalculator?.getEntropyStrength(passwordText) { entropyStrength ->
|
||||||
|
passwordStrengthProgress.apply {
|
||||||
|
post {
|
||||||
|
setIndicatorColor(entropyStrength.strength.color)
|
||||||
|
setProgressCompat(entropyStrength.estimationPercent, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
passwordEntropy.apply {
|
||||||
|
post {
|
||||||
|
text = PasswordEntropy.getStringEntropy(resources, entropyStrength.entropy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addTextChangedListener(textWatcher: TextWatcher) {
|
||||||
|
mPasswordTextWatcher.add(textWatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeTextChangedListener(textWatcher: TextWatcher) {
|
||||||
|
mPasswordTextWatcher.remove(textWatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
var passwordString: String
|
||||||
|
get() {
|
||||||
|
return passwordText.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
val spannableString =
|
||||||
|
if (PreferencesUtil.colorizePassword(context))
|
||||||
|
PasswordGenerator.getColorizedPassword(value)
|
||||||
|
else
|
||||||
|
SpannableString(value)
|
||||||
|
passwordText.text = spannableString
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import androidx.core.view.isVisible
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
class SearchFiltersView @JvmOverloads constructor(context: Context,
|
class SearchFiltersView @JvmOverloads constructor(context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
@@ -30,7 +31,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
private var searchUsername: CompoundButton
|
private var searchUsername: CompoundButton
|
||||||
private var searchPassword: CompoundButton
|
private var searchPassword: CompoundButton
|
||||||
private var searchURL: CompoundButton
|
private var searchURL: CompoundButton
|
||||||
private var searchExpires: CompoundButton
|
private var searchExpired: CompoundButton
|
||||||
private var searchNotes: CompoundButton
|
private var searchNotes: CompoundButton
|
||||||
private var searchOther: CompoundButton
|
private var searchOther: CompoundButton
|
||||||
private var searchUUID: CompoundButton
|
private var searchUUID: CompoundButton
|
||||||
@@ -49,7 +50,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
this.searchInUsernames = searchUsername.isChecked
|
this.searchInUsernames = searchUsername.isChecked
|
||||||
this.searchInPasswords = searchPassword.isChecked
|
this.searchInPasswords = searchPassword.isChecked
|
||||||
this.searchInUrls = searchURL.isChecked
|
this.searchInUrls = searchURL.isChecked
|
||||||
this.excludeExpired = !(searchExpires.isChecked)
|
this.searchInExpired = searchExpired.isChecked
|
||||||
this.searchInNotes = searchNotes.isChecked
|
this.searchInNotes = searchNotes.isChecked
|
||||||
this.searchInOther = searchOther.isChecked
|
this.searchInOther = searchOther.isChecked
|
||||||
this.searchInUUIDs = searchUUID.isChecked
|
this.searchInUUIDs = searchUUID.isChecked
|
||||||
@@ -69,12 +70,12 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
searchUsername.isChecked = value.searchInUsernames
|
searchUsername.isChecked = value.searchInUsernames
|
||||||
searchPassword.isChecked = value.searchInPasswords
|
searchPassword.isChecked = value.searchInPasswords
|
||||||
searchURL.isChecked = value.searchInUrls
|
searchURL.isChecked = value.searchInUrls
|
||||||
searchExpires.isChecked = !value.excludeExpired
|
searchExpired.isChecked = value.searchInExpired
|
||||||
searchNotes.isChecked = value.searchInNotes
|
searchNotes.isChecked = value.searchInNotes
|
||||||
searchOther.isChecked = value.searchInOther
|
searchOther.isChecked = value.searchInOther
|
||||||
searchUUID.isChecked = value.searchInUUIDs
|
searchUUID.isChecked = value.searchInUUIDs
|
||||||
searchTag.isChecked = value.searchInTags
|
searchTag.isChecked = value.searchInTags
|
||||||
searchGroupSearchable.isChecked = value.searchInRecycleBin
|
searchGroupSearchable.isChecked = value.searchInSearchableGroup
|
||||||
searchRecycleBin.isChecked = value.searchInRecycleBin
|
searchRecycleBin.isChecked = value.searchInRecycleBin
|
||||||
searchTemplate.isChecked = value.searchInTemplates
|
searchTemplate.isChecked = value.searchInTemplates
|
||||||
mOnParametersChangeListener = tempListener
|
mOnParametersChangeListener = tempListener
|
||||||
@@ -107,7 +108,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
searchUsername = findViewById(R.id.search_chip_username)
|
searchUsername = findViewById(R.id.search_chip_username)
|
||||||
searchPassword = findViewById(R.id.search_chip_password)
|
searchPassword = findViewById(R.id.search_chip_password)
|
||||||
searchURL = findViewById(R.id.search_chip_url)
|
searchURL = findViewById(R.id.search_chip_url)
|
||||||
searchExpires = findViewById(R.id.search_chip_expires)
|
searchExpired = findViewById(R.id.search_chip_expires)
|
||||||
searchNotes = findViewById(R.id.search_chip_note)
|
searchNotes = findViewById(R.id.search_chip_note)
|
||||||
searchUUID = findViewById(R.id.search_chip_uuid)
|
searchUUID = findViewById(R.id.search_chip_uuid)
|
||||||
searchOther = findViewById(R.id.search_chip_other)
|
searchOther = findViewById(R.id.search_chip_other)
|
||||||
@@ -116,6 +117,9 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
searchRecycleBin = findViewById(R.id.search_chip_recycle_bin)
|
searchRecycleBin = findViewById(R.id.search_chip_recycle_bin)
|
||||||
searchTemplate = findViewById(R.id.search_chip_template)
|
searchTemplate = findViewById(R.id.search_chip_template)
|
||||||
|
|
||||||
|
// Set search
|
||||||
|
searchParameters = PreferencesUtil.getDefaultSearchParameters(context)
|
||||||
|
|
||||||
// Expand menu with button
|
// Expand menu with button
|
||||||
searchExpandButton.setOnClickListener {
|
searchExpandButton.setOnClickListener {
|
||||||
val isVisible = searchAdvanceFiltersContainer?.visibility == View.VISIBLE
|
val isVisible = searchAdvanceFiltersContainer?.visibility == View.VISIBLE
|
||||||
@@ -153,8 +157,8 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
searchParameters.searchInUrls = isChecked
|
searchParameters.searchInUrls = isChecked
|
||||||
mOnParametersChangeListener?.invoke(searchParameters)
|
mOnParametersChangeListener?.invoke(searchParameters)
|
||||||
}
|
}
|
||||||
searchExpires.setOnCheckedChangeListener { _, isChecked ->
|
searchExpired.setOnCheckedChangeListener { _, isChecked ->
|
||||||
searchParameters.excludeExpired = !isChecked
|
searchParameters.searchInExpired = isChecked
|
||||||
mOnParametersChangeListener?.invoke(searchParameters)
|
mOnParametersChangeListener?.invoke(searchParameters)
|
||||||
}
|
}
|
||||||
searchNotes.setOnCheckedChangeListener { _, isChecked ->
|
searchNotes.setOnCheckedChangeListener { _, isChecked ->
|
||||||
@@ -249,4 +253,8 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveSearchParameters() {
|
||||||
|
PreferencesUtil.setDefaultSearchParameters(context, searchParameters)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
|
|||||||
if (templateAttribute.options.isAssociatedWithPasswordGenerator()) {
|
if (templateAttribute.options.isAssociatedWithPasswordGenerator()) {
|
||||||
setOnActionClickListener({
|
setOnActionClickListener({
|
||||||
mOnPasswordGenerationActionClickListener?.invoke(field)
|
mOnPasswordGenerationActionClickListener?.invoke(field)
|
||||||
}, R.drawable.ic_generate_password_white_24dp)
|
}, R.drawable.ic_random_white_24dp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ class TemplateView @JvmOverloads constructor(context: Context,
|
|||||||
setCopyButtonState(TextFieldView.ButtonState.GONE)
|
setCopyButtonState(TextFieldView.ButtonState.GONE)
|
||||||
} else {
|
} else {
|
||||||
label = otpElement.type.name
|
label = otpElement.type.name
|
||||||
value = otpElement.token
|
value = otpElement.tokenString
|
||||||
setCopyButtonState(TextFieldView.ButtonState.ACTIVATE)
|
setCopyButtonState(TextFieldView.ButtonState.ACTIVATE)
|
||||||
setCopyButtonClickListener { _, _ ->
|
setCopyButtonClickListener { _, _ ->
|
||||||
mOnCopyActionClickListener?.invoke(Field(
|
mOnCopyActionClickListener?.invoke(Field(
|
||||||
@@ -175,7 +175,7 @@ class TemplateView @JvmOverloads constructor(context: Context,
|
|||||||
mLastOtpTokenView = this
|
mLastOtpTokenView = this
|
||||||
mOtpRunnable = Runnable {
|
mOtpRunnable = Runnable {
|
||||||
if (otpElement.shouldRefreshToken()) {
|
if (otpElement.shouldRefreshToken()) {
|
||||||
value = otpElement.token
|
value = otpElement.tokenString
|
||||||
}
|
}
|
||||||
if (mLastOtpTokenView == null) {
|
if (mLastOtpTokenView == null) {
|
||||||
mOnOtpElementUpdated?.invoke(null)
|
mOnOtpElementUpdated?.invoke(null)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.text.InputFilter
|
import android.text.InputFilter
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
|
import android.text.SpannableString
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.ContextThemeWrapper
|
import android.view.ContextThemeWrapper
|
||||||
@@ -19,6 +20,9 @@ import androidx.core.view.isVisible
|
|||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
|
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
class TextEditFieldView @JvmOverloads constructor(context: Context,
|
class TextEditFieldView @JvmOverloads constructor(context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
@@ -123,7 +127,13 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
|
|||||||
return valueView.text?.toString() ?: ""
|
return valueView.text?.toString() ?: ""
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
valueView.setText(value)
|
val spannableString =
|
||||||
|
if (PreferencesUtil.colorizePassword(context)
|
||||||
|
&& TemplateField.isStandardPasswordName(context, label))
|
||||||
|
PasswordGenerator.getColorizedPassword(value)
|
||||||
|
else
|
||||||
|
SpannableString(value)
|
||||||
|
valueView.setText(spannableString)
|
||||||
}
|
}
|
||||||
|
|
||||||
override var default: String = ""
|
override var default: String = ""
|
||||||
@@ -164,7 +174,11 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
|
|||||||
fun setProtection(protection: Boolean) {
|
fun setProtection(protection: Boolean) {
|
||||||
if (protection) {
|
if (protection) {
|
||||||
labelView.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
|
labelView.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
|
||||||
valueView.inputType = valueView.inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
val visibilityTag = if (PreferencesUtil.hideProtectedValue(context))
|
||||||
|
InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||||
|
else
|
||||||
|
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||||
|
valueView.inputType = valueView.inputType or visibilityTag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,11 +22,13 @@ package com.kunzisoft.keepass.view
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.text.InputFilter
|
import android.text.InputFilter
|
||||||
|
import android.text.SpannableString
|
||||||
import android.text.util.Linkify
|
import android.text.util.Linkify
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.ContextThemeWrapper
|
import android.view.ContextThemeWrapper
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.View.OnClickListener
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.widget.AppCompatImageButton
|
import androidx.appcompat.widget.AppCompatImageButton
|
||||||
@@ -36,7 +38,10 @@ import androidx.core.text.util.LinkifyCompat
|
|||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
import com.kunzisoft.keepass.model.EntryInfo.Companion.APPLICATION_ID_FIELD_NAME
|
import com.kunzisoft.keepass.model.EntryInfo.Companion.APPLICATION_ID_FIELD_NAME
|
||||||
|
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
|
|
||||||
@@ -191,7 +196,13 @@ class TextFieldView @JvmOverloads constructor(context: Context,
|
|||||||
return valueView.text.toString()
|
return valueView.text.toString()
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
valueView.text = value
|
val spannableString =
|
||||||
|
if (PreferencesUtil.colorizePassword(context)
|
||||||
|
&& TemplateField.isStandardPasswordName(context, label))
|
||||||
|
PasswordGenerator.getColorizedPassword(value)
|
||||||
|
else
|
||||||
|
SpannableString(value)
|
||||||
|
valueView.text = spannableString
|
||||||
changeProtectedValueParameters()
|
changeProtectedValueParameters()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ class EntryViewModel: ViewModel() {
|
|||||||
val onAttachmentAction : LiveData<EntryAttachmentState?> get() = _onAttachmentAction
|
val onAttachmentAction : LiveData<EntryAttachmentState?> get() = _onAttachmentAction
|
||||||
private val _onAttachmentAction = MutableLiveData<EntryAttachmentState?>()
|
private val _onAttachmentAction = MutableLiveData<EntryAttachmentState?>()
|
||||||
|
|
||||||
|
val sectionSelected : LiveData<EntrySection> get() = _sectionSelected
|
||||||
|
private val _sectionSelected = MutableLiveData<EntrySection>()
|
||||||
|
|
||||||
val historySelected : LiveData<EntryHistory> get() = _historySelected
|
val historySelected : LiveData<EntryHistory> get() = _historySelected
|
||||||
private val _historySelected = SingleLiveEvent<EntryHistory>()
|
private val _historySelected = SingleLiveEvent<EntryHistory>()
|
||||||
|
|
||||||
@@ -124,6 +127,10 @@ class EntryViewModel: ViewModel() {
|
|||||||
_historySelected.value = EntryHistory(NodeIdUUID(item.id), null, item, position)
|
_historySelected.value = EntryHistory(NodeIdUUID(item.id), null, item, position)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun selectSection(section: EntrySection) {
|
||||||
|
_sectionSelected.value = section
|
||||||
|
}
|
||||||
|
|
||||||
data class EntryInfoHistory(var mainEntryId: NodeId<UUID>,
|
data class EntryInfoHistory(var mainEntryId: NodeId<UUID>,
|
||||||
var historyPosition: Int,
|
var historyPosition: Int,
|
||||||
val template: Template,
|
val template: Template,
|
||||||
@@ -135,6 +142,16 @@ class EntryViewModel: ViewModel() {
|
|||||||
var entryInfo: EntryInfo,
|
var entryInfo: EntryInfo,
|
||||||
var historyPosition: Int = -1)
|
var historyPosition: Int = -1)
|
||||||
|
|
||||||
|
enum class EntrySection(var position: Int) {
|
||||||
|
MAIN(0), ADVANCED(1);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getEntrySectionByPosition(position: Int): EntrySection {
|
||||||
|
return if (position == 1) ADVANCED else MAIN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = EntryViewModel::class.java.name
|
private val TAG = EntryViewModel::class.java.name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 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.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
|
||||||
|
class KeyGeneratorViewModel: ViewModel() {
|
||||||
|
|
||||||
|
val keyGenerated : LiveData<String> get() = _keyGenerated
|
||||||
|
private val _keyGenerated = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val keyGeneratedValidated : LiveData<Void?> get() = _keyGeneratedValidated
|
||||||
|
private val _keyGeneratedValidated = SingleLiveEvent<Void?>()
|
||||||
|
val requireKeyGeneration : LiveData<Void?> get() = _requireKeyGeneration
|
||||||
|
private val _requireKeyGeneration = SingleLiveEvent<Void?>()
|
||||||
|
|
||||||
|
val passwordGeneratedValidated : LiveData<Void?> get() = _passwordGeneratedValidated
|
||||||
|
private val _passwordGeneratedValidated = SingleLiveEvent<Void?>()
|
||||||
|
val requirePasswordGeneration : LiveData<Void?> get() = _requirePasswordGeneration
|
||||||
|
private val _requirePasswordGeneration = SingleLiveEvent<Void?>()
|
||||||
|
|
||||||
|
val passphraseGeneratedValidated : LiveData<Void?> get() = _passphraseGeneratedValidated
|
||||||
|
private val _passphraseGeneratedValidated = SingleLiveEvent<Void?>()
|
||||||
|
val requirePassphraseGeneration : LiveData<Void?> get() = _requirePassphraseGeneration
|
||||||
|
private val _requirePassphraseGeneration = SingleLiveEvent<Void?>()
|
||||||
|
|
||||||
|
fun setKeyGenerated(passKey: String) {
|
||||||
|
_keyGenerated.value = passKey
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateKeyGenerated() {
|
||||||
|
_keyGeneratedValidated.call()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validatePasswordGenerated() {
|
||||||
|
_passwordGeneratedValidated.call()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validatePassphraseGenerated() {
|
||||||
|
_passphraseGeneratedValidated.call()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requireKeyGeneration() {
|
||||||
|
_requireKeyGeneration.call()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requirePasswordGeneration() {
|
||||||
|
_requirePasswordGeneration.call()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requirePassphraseGeneration() {
|
||||||
|
_requirePassphraseGeneration.call()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,11 +19,11 @@ class SingleLiveEvent<T> : MutableLiveData<T>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Observe the internal MutableLiveData
|
// Observe the internal MutableLiveData
|
||||||
super.observe(owner, { t ->
|
super.observe(owner) { t ->
|
||||||
if (mPending.compareAndSet(true, false)) {
|
if (mPending.compareAndSet(true, false)) {
|
||||||
observer.onChanged(t)
|
observer.onChanged(t)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
|
|||||||