Compare commits
1 Commits
feature/Pa
...
feature/Cr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b8427750b |
13
.github/FUNDING.yml
vendored
@@ -1,13 +0,0 @@
|
|||||||
# 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']
|
|
||||||
41
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**KeePass Database**
|
||||||
|
- Created with: [e.g Windows KeePass 2.42]
|
||||||
|
- Version: [e.g. 2]
|
||||||
|
- Location: [e.g. Remote file retrieved with GDrive app]
|
||||||
|
- Size: [e.g. 150Mo]
|
||||||
|
- Contains attachment: [e.g. Yes]
|
||||||
|
|
||||||
|
**KeePassDX (please complete the following information):**
|
||||||
|
- Version: [e.g. 2.5.0.0beta23]
|
||||||
|
- Build: [e.g. Free]
|
||||||
|
- Language: [e.g. French]
|
||||||
|
|
||||||
|
**Android (please complete the following information):**
|
||||||
|
- Device: [e.g. GalaxyS8]
|
||||||
|
- Version: [e.g. 8.1]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
|
- Browser for Autofill: [e.g. Chrome version X]
|
||||||
62
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,62 +0,0 @@
|
|||||||
name: Bug Report
|
|
||||||
description: Report a bug.
|
|
||||||
labels: ["bug"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Please check out the [Wiki](https://github.com/Kunzisoft/KeePassDX/wiki) and [existing issues](https://github.com/Kunzisoft/KeePassDX/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug) to see if your problem has already been reported.
|
|
||||||
- type: checkboxes
|
|
||||||
id: checks
|
|
||||||
attributes:
|
|
||||||
label: Checks
|
|
||||||
options:
|
|
||||||
- label: I have read the Wiki, searched the open issues, and still think this is a new bug.
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: bug
|
|
||||||
attributes:
|
|
||||||
label: "Explain the problem clearly and succinctly:"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: expected
|
|
||||||
attributes:
|
|
||||||
label: "Describe what you expected to happen:"
|
|
||||||
- type: input
|
|
||||||
id: app-version
|
|
||||||
attributes:
|
|
||||||
label: "KeePassDX version:"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: app-build
|
|
||||||
attributes:
|
|
||||||
label: "Build:"
|
|
||||||
multiple: true
|
|
||||||
options:
|
|
||||||
- Free
|
|
||||||
- Libre
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: database-version
|
|
||||||
attributes:
|
|
||||||
label: "Database version:"
|
|
||||||
- type: input
|
|
||||||
id: file-provider
|
|
||||||
attributes:
|
|
||||||
label: "File provider (`content://` URI)"
|
|
||||||
- type: input
|
|
||||||
id: android-version
|
|
||||||
attributes:
|
|
||||||
label: "Android version:"
|
|
||||||
- type: input
|
|
||||||
id: android-device
|
|
||||||
attributes:
|
|
||||||
label: "Android device:"
|
|
||||||
- type: textarea
|
|
||||||
id: additional-context
|
|
||||||
attributes:
|
|
||||||
label: "Additional context:"
|
|
||||||
|
|
||||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: feature
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
33
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,33 +0,0 @@
|
|||||||
name: Feature request
|
|
||||||
description: Suggest an idea.
|
|
||||||
labels: ["feature"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Please check out the [Wiki](https://github.com/Kunzisoft/KeePassDX/wiki) and [existing issues](https://github.com/Kunzisoft/KeePassDX/issues?q=is%3Aissue%20state%3Aopen%20label%3Afeature) to see if your feature has already been reported.
|
|
||||||
- type: checkboxes
|
|
||||||
id: checks
|
|
||||||
attributes:
|
|
||||||
label: Checks
|
|
||||||
options:
|
|
||||||
- label: I have read the Wiki, searched the open issues, and still think this is a new feature.
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: problem
|
|
||||||
attributes:
|
|
||||||
label: "Explain the problem clearly and succinctly:"
|
|
||||||
- type: textarea
|
|
||||||
id: solution
|
|
||||||
attributes:
|
|
||||||
label: "Describe the solution you'd like:"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: alternatives
|
|
||||||
attributes:
|
|
||||||
label: "Describe alternatives you've considered:"
|
|
||||||
- type: textarea
|
|
||||||
id: additional-context
|
|
||||||
attributes:
|
|
||||||
label: "Additional context:"
|
|
||||||
6
.gitignore
vendored
@@ -19,9 +19,6 @@ bin/
|
|||||||
gen/
|
gen/
|
||||||
out/
|
out/
|
||||||
|
|
||||||
# Kotlin folder
|
|
||||||
.kotlin/
|
|
||||||
|
|
||||||
# Gradle files
|
# Gradle files
|
||||||
.gradle/
|
.gradle/
|
||||||
build/
|
build/
|
||||||
@@ -83,9 +80,6 @@ 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
|
||||||
|
|||||||
298
CHANGELOG
@@ -1,302 +1,6 @@
|
|||||||
KeePassDX(4.2.0)
|
|
||||||
* Passkeys management #1421 #2097 (Thx @cali-95)
|
|
||||||
|
|
||||||
KeePassDX(4.1.8)
|
|
||||||
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)
|
|
||||||
* Remember last read-only state #2099 #2100 (Thx @rmacklin)
|
|
||||||
* Fix merge deletion #1516
|
|
||||||
* Fix space in search #175
|
|
||||||
* Fix deletable recycle bin #2163
|
|
||||||
* Small fixes
|
|
||||||
|
|
||||||
KeePassDX(4.1.7)
|
|
||||||
* Fix CipherDatabase for biometric states #2119
|
|
||||||
|
|
||||||
KeePassDX(4.1.6)
|
|
||||||
* Auto open biometric prompt from database list #2113
|
|
||||||
* Fix Keystore errors #2114 #2115
|
|
||||||
* Complete biometric refactoring for better compatibility
|
|
||||||
|
|
||||||
KeePassDX(4.1.5)
|
|
||||||
* Fix auto prompt #2111
|
|
||||||
|
|
||||||
KeePassDX(4.1.4)
|
|
||||||
* Fix UnlockManager #2098 #2101
|
|
||||||
* Auto device unlock prompt #2105
|
|
||||||
* Small fixes ##2066
|
|
||||||
|
|
||||||
KeePassDX(4.1.3)
|
|
||||||
* Fix Autofill Registration #2089
|
|
||||||
* Fix Biometric errors #2081
|
|
||||||
* Fixed timestamp in copy file #1981 #1983
|
|
||||||
* Fix Template Email #1986
|
|
||||||
* Fix Search #2096
|
|
||||||
|
|
||||||
KeePassDX(4.1.2)
|
|
||||||
* Fix URL search #1940 #1946 #2003 #2040 #2044
|
|
||||||
* Fix Autofill popup #2054
|
|
||||||
* Fix Group notes #2053
|
|
||||||
* Fix Dialog background #2005 #2004 (Thx @codokie)
|
|
||||||
* Fix OTP configuration #2042 #2065 (Thx @Dev-ClayP)
|
|
||||||
* Fix small UI elements #1987 #2007 (Thx @ymcx)
|
|
||||||
* RTL layout support #2021 (Thx @codokie)
|
|
||||||
* App Metadata to translation #1823
|
|
||||||
|
|
||||||
KeePassDX(4.1.1)
|
|
||||||
* Fix date parser #1933
|
|
||||||
* Fix domain search #1820 #1936
|
|
||||||
|
|
||||||
KeePassDX(4.1.0)
|
|
||||||
* Generate keyfile #1290
|
|
||||||
* Hide template group #1894
|
|
||||||
* Group count sum recursively #421
|
|
||||||
* Fix date fields #1695 #1710
|
|
||||||
* Fix distinct domain names #1105 #1820
|
|
||||||
* Resets the advanced unlock expiration #1600
|
|
||||||
* Password entropy #1490 #1355
|
|
||||||
* Upgrade to API 34 (Android 14) #1730
|
|
||||||
* Small fixes #1711 #1831 #1780 #1821 #1863 #1889 #1289 #1600 #1467 #1870
|
|
||||||
|
|
||||||
KeePassDX(4.0.8)
|
|
||||||
* Fix graphical bug that prevented databases from being opened on some versions of Android #1848 #1850
|
|
||||||
|
|
||||||
KeePassDX(4.0.7)
|
|
||||||
* Prevent 0 Byte file with cache during a save exception #1620 #1594 #1680
|
|
||||||
* Fix inline suggestions in keyboard #1840
|
|
||||||
* Fix broken links by default #1755
|
|
||||||
* Fix UX by allowing validation in entry edition #1770
|
|
||||||
* Fix small bugs #1709
|
|
||||||
|
|
||||||
KeePassDX(4.0.6)
|
|
||||||
* Fix form filled recognition #1508 #1735 #1508 #1790 #1783 #1797 #1801 #1802 #1804 #1665
|
|
||||||
* Fix translations #1707 #1683 #1712
|
|
||||||
* Update APK verifier #1810
|
|
||||||
|
|
||||||
KeePassDX(4.0.5)
|
|
||||||
* Fix form filled recognition #1572 #1508
|
|
||||||
* Rollback password color #1686 #1490
|
|
||||||
|
|
||||||
KeePassDX(4.0.4)
|
|
||||||
* Fix form filled recognition #1572 #1677
|
|
||||||
* Fix device unlock #1682
|
|
||||||
* Fix password color #1490
|
|
||||||
|
|
||||||
KeePassDX(4.0.3)
|
|
||||||
* Fix "Save as" in Read Only mode #1666
|
|
||||||
* Fix username autofill #1665 #530 #1572 #1426 #1523 #1556 #1653 #1658 #1508 #1667
|
|
||||||
* Fix regex OTP recognition #1596
|
|
||||||
* Change password color dynamically #1490
|
|
||||||
* Small fixes #1641 #1656 #1649 #1400 #1674
|
|
||||||
|
|
||||||
KeePassDX(4.0.2)
|
|
||||||
* Fix Autofill with API 33
|
|
||||||
|
|
||||||
KeePassDX(4.0.1)
|
|
||||||
* Fix back lock #1635 #1629 #1634
|
|
||||||
* Fix lock button in settings #1630
|
|
||||||
* Improve theme translation #1631
|
|
||||||
|
|
||||||
KeePassDX(4.0.0)
|
|
||||||
* New UX/UI with Material 3 #1183 #1529 #1428 #1441 #1607
|
|
||||||
* Material You theme (follow system colors) #1469
|
|
||||||
* Refactoring inner code #1371
|
|
||||||
* Migration to API 33
|
|
||||||
* Cut, copy and delete from search #891 #1308 #1263
|
|
||||||
* Fix behaviors #1351 #874 #1327
|
|
||||||
* Fix bugs #1589 #1584 #1545 #1563 #1371 #1609
|
|
||||||
|
|
||||||
KeePassDX(3.5.1)
|
|
||||||
* Fix action dialog with YubiKey challenge-response #1506
|
|
||||||
|
|
||||||
KeePassDX(3.5.0)
|
|
||||||
* Support YubiKey challenge-response #8 #137
|
|
||||||
* Better exception management during database save #1346
|
|
||||||
* Add "Screenshot mode" setting #459 #1377 #1354 (Thx @GianpaMX)
|
|
||||||
* Hide clipboard sensitive text when copy entry field #1386
|
|
||||||
* Fix attachment download button #1401
|
|
||||||
* Add monochrome icon #1403 #1404 (Thx @Sandelinos)
|
|
||||||
* Fix lock with back button #1412 #1414 (Thx @ryg-git)
|
|
||||||
* Vanadium compatibility #1447 (Thx @flawedworld)
|
|
||||||
|
|
||||||
KeePassDX(3.4.5)
|
|
||||||
* Fix custom data in group (fix KeeShare) #1335
|
|
||||||
* Fix device credential unlocking #1344
|
|
||||||
* New clipboard manager #1343
|
|
||||||
* Keep screen on by default when viewing an entry
|
|
||||||
* Change the order of the search filters
|
|
||||||
* Fix searchable selection
|
|
||||||
|
|
||||||
KeePassDX(3.4.4)
|
|
||||||
* Fix crash in New Android 13 #1321
|
|
||||||
* Better backstack management for selection mode
|
|
||||||
* Prevent Tapjacking #1318
|
|
||||||
* Small changes #1298
|
|
||||||
|
|
||||||
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)
|
|
||||||
* Fix Japanese keyboard in search #1248
|
|
||||||
* Better OOM management #256
|
|
||||||
* Fix filters #1249
|
|
||||||
* Fix temp advanced unlocking #1245
|
|
||||||
* Best autofill recognition #1250
|
|
||||||
* Workaround to fill OTP token in multiple fields with Magikeyboard (long press) #1158
|
|
||||||
|
|
||||||
KeePassDX(3.3.0)
|
|
||||||
* Quick search and dynamic filters #163 #462 #521
|
|
||||||
* Keep search context #1141
|
|
||||||
* Add searchable groups #905 #1006
|
|
||||||
* Search with regular expression #175
|
|
||||||
* Merge from file and save as copy #1221 #1204 #840
|
|
||||||
* Fix custom data #1236
|
|
||||||
* Fix education hints #1192
|
|
||||||
* Fix save and app instance in selection mode
|
|
||||||
* New UI and fix styles
|
|
||||||
* Add "Simple" and "Reply" themes
|
|
||||||
|
|
||||||
KeePassDX(3.2.0)
|
|
||||||
* Manage data merge #840 #977
|
|
||||||
* Manage Tags #633
|
|
||||||
* Inherit colors and icon from template #1213 #1130
|
|
||||||
* Entry colors setting #1207
|
|
||||||
* Setting to keep the screen on when watching the entry #1119
|
|
||||||
* Add path in quick search
|
|
||||||
* Small fixes
|
|
||||||
|
|
||||||
KeePassDX(3.1.0)
|
|
||||||
* Add breadcrumb
|
|
||||||
* Add path in search results #1148
|
|
||||||
* Add group info dialog #1177
|
|
||||||
* Manage colors #64 #913
|
|
||||||
* Fix UI in Android 8 #509
|
|
||||||
* Upgrade libs and SDK to 31 #833
|
|
||||||
* Fix parser of database v1 #1201
|
|
||||||
* Stop asking WRITE_EXTERNAL_STORAGE permission
|
|
||||||
|
|
||||||
KeePassDX(3.0.4)
|
|
||||||
* Fix autofill inline bugs #1173 #1165
|
|
||||||
* Small UI change
|
|
||||||
|
|
||||||
KeePassDX(3.0.3)
|
|
||||||
* Change default Argon2 parameters #1098
|
|
||||||
* Add & edit custom icon name #976
|
|
||||||
* Fix templates #1128 #1133 #1138
|
|
||||||
* Update Autofill compatibility list #725 #1154
|
|
||||||
* Improve fingerprint usage #1137 #1145
|
|
||||||
* Change backup configuration #1144
|
|
||||||
* Add lock button in database notification
|
|
||||||
|
|
||||||
KeePassDX(3.0.2)
|
|
||||||
* Samsung DeX mode #1114 #245 (Thx @chenxiaolong)
|
|
||||||
|
|
||||||
KeePassDX(3.0.1)
|
|
||||||
* Fix text size and smallest margin #1085
|
|
||||||
* Fix number of lines during an edition #1073
|
|
||||||
* Fix Magikeyboard URL auto action #1100
|
|
||||||
* Fix exception after group name change and save #1112
|
|
||||||
* Fix timeout reset #1107
|
|
||||||
* Fix search actions #1091 #1092
|
|
||||||
* Small changes #1106 #1085
|
|
||||||
|
|
||||||
KeePassDX(3.0.0)
|
|
||||||
* Add / Manage dynamic templates #191
|
|
||||||
* Manually select RecycleBin group and Templates group #191
|
|
||||||
* Setting to display OTP Token in list #655
|
|
||||||
* Fix timeout in dialogs #716
|
|
||||||
* Check URI permissions #626
|
|
||||||
* Better autofill implementation #943 #946 #984 #1070 (Thx @uduerholz)
|
|
||||||
* Improvements #680 #1035 #1043 #942 #1021 #1027 #1046 #1082 #1083 (Thx @chenxiaolong)
|
|
||||||
|
|
||||||
KeePassDX(2.10.5)
|
|
||||||
* Increase the saving speed of database #1028
|
|
||||||
* Fix advanced unlocking by device credential #1029
|
|
||||||
|
|
||||||
KeePassDX(2.10.4)
|
|
||||||
* Hot fix to increase the opening speed of database #1028
|
|
||||||
|
|
||||||
KeePassDX(2.10.3)
|
|
||||||
* Improve Magikeyboard options description #1022 #1023 (Thx @djibux)
|
|
||||||
* Fix database opened without notification (database is now closed when screen is killed in background #1025)
|
|
||||||
* Fix biometric prompt #1018
|
|
||||||
|
|
||||||
KeePassDX(2.10.2)
|
|
||||||
* Fix search fields references #987
|
|
||||||
* Fix Auto-Types with same key #997
|
|
||||||
|
|
||||||
KeePassDX(2.10.1)
|
|
||||||
* Fix parcelable with custom data #986
|
|
||||||
|
|
||||||
KeePassDX(2.10.0)
|
|
||||||
* Manage new database format 4.1 #956
|
|
||||||
* Fix show button consistency #980
|
|
||||||
* Fix persistent notification #979
|
|
||||||
|
|
||||||
KeePassDX(2.9.20)
|
|
||||||
* Fix search with non-latin chars #971
|
|
||||||
* Fix action mode with search #972 (rollback ignore accents #945)
|
|
||||||
* Fix timeout with 0s #974
|
|
||||||
|
|
||||||
KeePassDX(2.9.19)
|
|
||||||
* Fix search slowdown #964
|
|
||||||
* Fix closing notification after lock request #965
|
|
||||||
* Better temp advanced unlocking code implementation #965
|
|
||||||
* Fix OTP token generation #967
|
|
||||||
|
|
||||||
KeePassDX(2.9.18)
|
|
||||||
* Move groups #658
|
|
||||||
* Improve autofill recognition #960
|
|
||||||
* Remove diacritical marks in search string #945
|
|
||||||
* Fix search in references #962
|
|
||||||
* Fix themes in Libre version
|
|
||||||
|
|
||||||
KeePassDX(2.9.17)
|
|
||||||
* Import / Export app properties #839
|
|
||||||
* Force twofish padding compatibility #955
|
|
||||||
* Better timeout preference #579
|
|
||||||
|
|
||||||
KeePassDX(2.9.16)
|
|
||||||
* Fix small bugs #948
|
|
||||||
|
|
||||||
KeePassDX(2.9.15)
|
KeePassDX(2.9.15)
|
||||||
* Fix themes #935 #926
|
* Fix themes #935
|
||||||
* Decrease default clipboard time #934
|
* Decrease default clipboard time #934
|
||||||
* Better opening performance #929 #933
|
|
||||||
* Fix memory usage setting #941
|
|
||||||
|
|
||||||
KeePassDX(2.9.14)
|
KeePassDX(2.9.14)
|
||||||
* Add custom icons #96
|
* Add custom icons #96
|
||||||
|
|||||||
10
Gemfile
@@ -1,10 +0,0 @@
|
|||||||
# 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)
|
|
||||||
230
Gemfile.lock
@@ -1,230 +0,0 @@
|
|||||||
GEM
|
|
||||||
remote: https://rubygems.org/
|
|
||||||
specs:
|
|
||||||
CFPropertyList (3.0.7)
|
|
||||||
base64
|
|
||||||
nkf
|
|
||||||
rexml
|
|
||||||
addressable (2.8.7)
|
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
|
||||||
artifactory (3.0.17)
|
|
||||||
atomos (0.1.3)
|
|
||||||
aws-eventstream (1.4.0)
|
|
||||||
aws-partitions (1.1146.0)
|
|
||||||
aws-sdk-core (3.229.0)
|
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
|
||||||
aws-sigv4 (~> 1.9)
|
|
||||||
base64
|
|
||||||
bigdecimal
|
|
||||||
jmespath (~> 1, >= 1.6.1)
|
|
||||||
logger
|
|
||||||
aws-sdk-kms (1.110.0)
|
|
||||||
aws-sdk-core (~> 3, >= 3.228.0)
|
|
||||||
aws-sigv4 (~> 1.5)
|
|
||||||
aws-sdk-s3 (1.196.1)
|
|
||||||
aws-sdk-core (~> 3, >= 3.228.0)
|
|
||||||
aws-sdk-kms (~> 1)
|
|
||||||
aws-sigv4 (~> 1.5)
|
|
||||||
aws-sigv4 (1.12.1)
|
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
|
||||||
babosa (1.0.4)
|
|
||||||
base64 (0.3.0)
|
|
||||||
bigdecimal (3.2.2)
|
|
||||||
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.7.0)
|
|
||||||
rake (>= 12.0.0, < 14.0.0)
|
|
||||||
domain_name (0.6.20240107)
|
|
||||||
dotenv (2.8.1)
|
|
||||||
emoji_regex (3.2.3)
|
|
||||||
excon (0.112.0)
|
|
||||||
faraday (1.10.4)
|
|
||||||
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.1)
|
|
||||||
faraday-excon (1.1.0)
|
|
||||||
faraday-httpclient (1.0.1)
|
|
||||||
faraday-multipart (1.1.1)
|
|
||||||
multipart-post (~> 2.0)
|
|
||||||
faraday-net_http (1.0.2)
|
|
||||||
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.1)
|
|
||||||
faraday (~> 1.0)
|
|
||||||
fastimage (2.4.0)
|
|
||||||
fastlane (2.228.0)
|
|
||||||
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 (~> 1.2)
|
|
||||||
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)
|
|
||||||
fastlane-sirp (>= 1.0.0)
|
|
||||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
|
||||||
google-apis-androidpublisher_v3 (~> 0.3)
|
|
||||||
google-apis-playcustomapp_v1 (~> 0.1)
|
|
||||||
google-cloud-env (>= 1.6.0, < 2.0.0)
|
|
||||||
google-cloud-storage (~> 1.31)
|
|
||||||
highline (~> 2.0)
|
|
||||||
http-cookie (~> 1.0.5)
|
|
||||||
json (< 3.0.0)
|
|
||||||
jwt (>= 2.1.0, < 3)
|
|
||||||
mini_magick (>= 4.9.4, < 5.0.0)
|
|
||||||
multipart-post (>= 2.0.0, < 3.0.0)
|
|
||||||
naturally (~> 2.2)
|
|
||||||
optparse (>= 0.1.1, < 1.0.0)
|
|
||||||
plist (>= 3.1.0, < 4.0.0)
|
|
||||||
rubyzip (>= 2.0.0, < 3.0.0)
|
|
||||||
security (= 0.1.5)
|
|
||||||
simctl (~> 1.6.3)
|
|
||||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
|
||||||
terminal-table (~> 3)
|
|
||||||
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.4.1)
|
|
||||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
|
||||||
fastlane-plugin-versioning_android (0.1.1)
|
|
||||||
fastlane-sirp (1.0.0)
|
|
||||||
sysrandom (~> 1.0)
|
|
||||||
gh_inspector (1.1.3)
|
|
||||||
google-apis-androidpublisher_v3 (0.54.0)
|
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
|
||||||
google-apis-core (0.11.3)
|
|
||||||
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
|
|
||||||
google-apis-iamcredentials_v1 (0.17.0)
|
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
|
||||||
google-apis-playcustomapp_v1 (0.13.0)
|
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
|
||||||
google-apis-storage_v1 (0.31.0)
|
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
|
||||||
google-cloud-core (1.8.0)
|
|
||||||
google-cloud-env (>= 1.0, < 3.a)
|
|
||||||
google-cloud-errors (~> 1.0)
|
|
||||||
google-cloud-env (1.6.0)
|
|
||||||
faraday (>= 0.17.3, < 3.0)
|
|
||||||
google-cloud-errors (1.5.0)
|
|
||||||
google-cloud-storage (1.47.0)
|
|
||||||
addressable (~> 2.8)
|
|
||||||
digest-crc (~> 0.4)
|
|
||||||
google-apis-iamcredentials_v1 (~> 0.1)
|
|
||||||
google-apis-storage_v1 (~> 0.31.0)
|
|
||||||
google-cloud-core (~> 1.6)
|
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
|
||||||
mini_mime (~> 1.0)
|
|
||||||
googleauth (1.8.1)
|
|
||||||
faraday (>= 0.17.3, < 3.a)
|
|
||||||
jwt (>= 1.4, < 3.0)
|
|
||||||
multi_json (~> 1.11)
|
|
||||||
os (>= 0.9, < 2.0)
|
|
||||||
signet (>= 0.16, < 2.a)
|
|
||||||
highline (2.0.3)
|
|
||||||
http-cookie (1.0.8)
|
|
||||||
domain_name (~> 0.5)
|
|
||||||
httpclient (2.9.0)
|
|
||||||
mutex_m
|
|
||||||
jmespath (1.6.2)
|
|
||||||
json (2.13.2)
|
|
||||||
jwt (2.10.2)
|
|
||||||
base64
|
|
||||||
logger (1.7.0)
|
|
||||||
mini_magick (4.13.2)
|
|
||||||
mini_mime (1.1.5)
|
|
||||||
multi_json (1.17.0)
|
|
||||||
multipart-post (2.4.1)
|
|
||||||
mutex_m (0.3.0)
|
|
||||||
nanaimo (0.4.0)
|
|
||||||
naturally (2.3.0)
|
|
||||||
nkf (0.2.0)
|
|
||||||
optparse (0.6.0)
|
|
||||||
os (1.1.4)
|
|
||||||
plist (3.7.2)
|
|
||||||
public_suffix (6.0.2)
|
|
||||||
rake (13.3.0)
|
|
||||||
representable (3.2.0)
|
|
||||||
declarative (< 0.1.0)
|
|
||||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
|
||||||
uber (< 0.2.0)
|
|
||||||
retriable (3.1.2)
|
|
||||||
rexml (3.4.1)
|
|
||||||
rouge (3.28.0)
|
|
||||||
ruby2_keywords (0.0.5)
|
|
||||||
rubyzip (2.4.1)
|
|
||||||
security (0.1.5)
|
|
||||||
signet (0.20.0)
|
|
||||||
addressable (~> 2.8)
|
|
||||||
faraday (>= 0.17.5, < 3.a)
|
|
||||||
jwt (>= 1.5, < 3.0)
|
|
||||||
multi_json (~> 1.10)
|
|
||||||
simctl (1.6.10)
|
|
||||||
CFPropertyList
|
|
||||||
naturally
|
|
||||||
sysrandom (1.0.5)
|
|
||||||
terminal-notifier (2.0.0)
|
|
||||||
terminal-table (3.0.2)
|
|
||||||
unicode-display_width (>= 1.1.1, < 3)
|
|
||||||
trailblazer-option (0.1.2)
|
|
||||||
tty-cursor (0.7.1)
|
|
||||||
tty-screen (0.8.2)
|
|
||||||
tty-spinner (0.9.3)
|
|
||||||
tty-cursor (~> 0.7)
|
|
||||||
uber (0.1.0)
|
|
||||||
unicode-display_width (2.6.0)
|
|
||||||
word_wrap (1.0.0)
|
|
||||||
xcodeproj (1.27.0)
|
|
||||||
CFPropertyList (>= 2.3.3, < 4.0)
|
|
||||||
atomos (~> 0.1.3)
|
|
||||||
claide (>= 1.0.2, < 2.0)
|
|
||||||
colored2 (~> 3.1)
|
|
||||||
nanaimo (~> 0.4.0)
|
|
||||||
rexml (>= 3.3.6, < 4.0)
|
|
||||||
xcpretty (0.4.1)
|
|
||||||
rouge (~> 3.28.0)
|
|
||||||
xcpretty-travis-formatter (1.0.1)
|
|
||||||
xcpretty (~> 0.2, >= 0.0.7)
|
|
||||||
|
|
||||||
PLATFORMS
|
|
||||||
ruby
|
|
||||||
|
|
||||||
DEPENDENCIES
|
|
||||||
fastlane
|
|
||||||
fastlane-plugin-versioning_android
|
|
||||||
|
|
||||||
BUNDLED WITH
|
|
||||||
2.6.9
|
|
||||||
51
README.md
@@ -1,21 +1,20 @@
|
|||||||
# Android KeePassDX
|
# Android KeePassDX
|
||||||
|
|
||||||
<img alt="KeePassDX Icon" src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> **Lightweight password safe and 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/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 alt="KeePassDX Screenshot" 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">
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- Create database files / entries and groups.
|
- Create database files / entries and groups.
|
||||||
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
||||||
- **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …).
|
- **Compatible** with the majority of alternative programs (KeePass, KeePassX, KeePassXC, …).
|
||||||
- Allows opening and **copying URI / URL fields quickly**.
|
- Allows opening and **copying URI / URL fields quickly**.
|
||||||
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
|
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
|
||||||
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
|
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
|
||||||
- Material design with **themes**.
|
- Material design with **themes**.
|
||||||
- **Auto-Fill** and Integration.
|
- **Auto-Fill** and Integration.
|
||||||
- Field filling **keyboard**.
|
- Field filling **keyboard**.
|
||||||
- Dynamic **templates**
|
|
||||||
- **History** of each entry.
|
- **History** of each entry.
|
||||||
- Precise management of **settings**.
|
- Precise management of **settings**.
|
||||||
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
|
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
|
||||||
@@ -43,45 +42,21 @@ Optional visual styles are accessible after a contribution (and a congratulatory
|
|||||||
|
|
||||||
* Add features by making a **[pull request](https://help.github.com/articles/about-pull-requests/)**.
|
* Add features by making a **[pull request](https://help.github.com/articles/about-pull-requests/)**.
|
||||||
* Help to **[translate](https://hosted.weblate.org/projects/keepass-dx/strings/)** KeePassDX to your language (on [Weblate](https://hosted.weblate.org/projects/keepass-dx/) or by sending a [pull request](https://help.github.com/articles/about-pull-requests/)).
|
* Help to **[translate](https://hosted.weblate.org/projects/keepass-dx/strings/)** KeePassDX to your language (on [Weblate](https://hosted.weblate.org/projects/keepass-dx/) or by sending a [pull request](https://help.github.com/articles/about-pull-requests/)).
|
||||||
* **[Donate](https://www.keepassdx.com/#donation)** 人◕ ‿‿ ◕人Y for a better service and a quick development of your features.
|
* **[Donate](https://www.kunzisoft.com/donation)** 人◕ ‿‿ ◕人Y for a better service and a quick development of your features.
|
||||||
* Buy the **[Pro version](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro)** of KeePassDX.
|
* Buy the **[Pro version](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro)** of KeePassDX.
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
*[F-Droid](https://f-droid.org/packages/com.kunzisoft.keepass.libre/) is the recommended way of installing, a libre software project that verifies all the libraries and app code is libre software.*
|
*[F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) is the recommended way of installing, a libre software project that verifies that all the libraries and app code is libre software.*
|
||||||
|
|
||||||
| Source | Status | [Version](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ#why-a-libre-and-free-version) |
|
[<img src="https://f-droid.org/badge/get-it-on.png"
|
||||||
|--------|--------|---------|
|
alt="Get it on F-Droid"
|
||||||
| [Google Play](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free) |  | Free + [Pro](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro) |
|
height="80">](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/)
|
||||||
| [F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) |  | Libre |
|
|
||||||
| [IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/com.kunzisoft.keepass.free) |  | Free & [Libre](https://apt.izzysoft.de/fdroid/index/apk/com.kunzisoft.keepass.libre) |
|
|
||||||
| [GitHub](https://github.com/Kunzisoft/KeePassDX/releases) / [Obtainium](https://github.com/ImranR98/Obtainium) |  | Free & Libre |
|
|
||||||
|
|
||||||
## Package authenticity from GitHub
|
|
||||||
- Download the app from [GitHub releases](https://github.com/Kunzisoft/KeePassDX/releases/latest)
|
|
||||||
- Install [`apksigner`](https://developer.android.com/tools/apksigner) from [Android Studio](https://developer.android.com/studio)
|
|
||||||
- Open the directory where you saved the downloaded file in the Terminal
|
|
||||||
- Make sure that you have `apksigner` installed by running:
|
|
||||||
```shell
|
|
||||||
apksigner --version
|
|
||||||
```
|
|
||||||
- Depending on the APK file you downloaded, run:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
apksigner verify --verbose --print-certs -min-sdk-version 24 KeePassDX-*.apk
|
|
||||||
```
|
|
||||||
|
|
||||||
You should get this output :
|
|
||||||
```shell
|
|
||||||
Verified using v2 scheme (APK Signature Scheme v2): true
|
|
||||||
...
|
|
||||||
Number of signers: 1
|
|
||||||
Signer #1 certificate SHA-256 digest: 7d55b8af210381aabf960f07e17cf7857b6d2a642ca2da6bf0bdf1b200362f04
|
|
||||||
...
|
|
||||||
Signer #1 public key SHA-256 digest: 5d261d3176db1e077b80112824d9390167f3be0561827e42112ed6b71192db81
|
|
||||||
```
|
|
||||||
If it's the case, this means that the APK was well built by the author of KeePassDX.
|
|
||||||
|
|
||||||
|
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
||||||
|
alt="Get it on Google Play"
|
||||||
|
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
|
||||||
|
|
||||||
## Frequently Asked Questions
|
## Frequently Asked Questions
|
||||||
|
|
||||||
Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ)
|
Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ)
|
||||||
@@ -96,7 +71,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright © 2025 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
Copyright © 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||||
|
|
||||||
This file is part of KeePassDX.
|
This file is part of KeePassDX.
|
||||||
|
|
||||||
|
|||||||
114
app/build.gradle
@@ -1,18 +1,18 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-parcelize'
|
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.kunzisoft.keepass'
|
compileSdkVersion 30
|
||||||
compileSdkVersion 36
|
buildToolsVersion "30.0.3"
|
||||||
|
ndkVersion "21.4.7075529"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 19
|
minSdkVersion 15
|
||||||
targetSdkVersion 35
|
targetSdkVersion 30
|
||||||
versionCode = 142
|
versionCode = 66
|
||||||
versionName = "4.2.0beta02"
|
versionName = "2.9.15"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
@@ -32,54 +32,48 @@ android {
|
|||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled = false
|
minifyEnabled = false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
buildConfig true
|
|
||||||
}
|
|
||||||
|
|
||||||
dependenciesInfo {
|
|
||||||
// Disables dependency metadata when building APKs.
|
|
||||||
includeInApk = false
|
|
||||||
// Disables dependency metadata when building Android App Bundles.
|
|
||||||
includeInBundle = false
|
|
||||||
}
|
|
||||||
|
|
||||||
flavorDimensions "version"
|
flavorDimensions "version"
|
||||||
productFlavors {
|
productFlavors {
|
||||||
libre {
|
libre {
|
||||||
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\"}"
|
||||||
"\"KeepassDXStyle_Dynamic_Light\"," +
|
|
||||||
"\"KeepassDXStyle_Dynamic_Night\"}"
|
|
||||||
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_Blue\"," +
|
"{\"KeepassDXStyle_Blue\"," +
|
||||||
"\"KeepassDXStyle_Blue_Night\"," +
|
"\"KeepassDXStyle_Blue_Night\"," +
|
||||||
"\"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\"}"
|
||||||
"\"KeepassDXStyle_Dynamic_Light\"," +
|
|
||||||
"\"KeepassDXStyle_Dynamic_Night\"}"
|
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
||||||
}
|
}
|
||||||
@@ -87,6 +81,7 @@ 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'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,68 +90,53 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "17"
|
jvmTarget = "1.8"
|
||||||
}
|
|
||||||
buildFeatures {
|
|
||||||
buildConfig true
|
|
||||||
}
|
|
||||||
|
|
||||||
packaging {
|
|
||||||
// Bouncy castle bug https://github.com/bcgit/bc-java/issues/1685
|
|
||||||
resources.pickFirsts.add('META-INF/versions/9/OSGI-INF/MANIFEST.MF')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def room_version = "2.5.1"
|
def room_version = "2.2.6"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
implementation "com.android.support:multidex:1.0.3"
|
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||||
implementation "androidx.appcompat:appcompat:$android_appcompat_version"
|
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.biometric:biometric:1.1.0'
|
implementation 'androidx.biometric:biometric:1.1.0-rc01'
|
||||||
implementation 'androidx.media:media:1.6.0'
|
|
||||||
// Lifecycle - LiveData - ViewModel - Coroutines
|
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||||
implementation "androidx.core:core-ktx:$android_core_version"
|
implementation "androidx.core:core-ktx:1.3.2"
|
||||||
implementation "androidx.lifecycle:lifecycle-process:2.6.2"
|
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.6.0'
|
// WARNING: Don't upgrade because slowdown https://github.com/Kunzisoft/KeePassDX/issues/923
|
||||||
implementation "com.google.android.material:material:$android_material_version"
|
implementation 'com.google.android.material:material:1.1.0'
|
||||||
// Token auto complete
|
|
||||||
// From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed
|
|
||||||
implementation "com.splitwise:tokenautocomplete:4.0.0-beta05"
|
|
||||||
// Database
|
// Database
|
||||||
implementation "androidx.room:room-runtime:$room_version"
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
kapt "androidx.room:room-compiler:$room_version"
|
kapt "androidx.room:room-compiler:$room_version"
|
||||||
// Autofill
|
// Autofill
|
||||||
implementation "androidx.autofill:autofill:1.1.0"
|
implementation "androidx.autofill:autofill:1.1.0"
|
||||||
// Time
|
// Time
|
||||||
implementation 'joda-time:joda-time:2.13.0'
|
implementation 'joda-time:joda-time:2.10.6'
|
||||||
// Color
|
// Color
|
||||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.6'
|
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
|
||||||
// Education
|
// Education
|
||||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3'
|
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
|
||||||
// Apache Commons
|
// Apache Commons
|
||||||
implementation 'commons-io:commons-io:2.8.0'
|
implementation 'commons-io:commons-io:2.8.0'
|
||||||
implementation 'commons-codec:commons-codec:1.15'
|
implementation 'commons-codec:commons-codec:1.15'
|
||||||
// Password generator
|
// Encrypt lib
|
||||||
implementation 'me.gosimple:nbvcxz:1.5.0'
|
implementation project(path: ':crypto')
|
||||||
|
implementation fileTree(include: ['encrypt.aar'], dir: 'libs')
|
||||||
// Credentials Provider
|
// Icon pack
|
||||||
implementation "androidx.credentials:credentials:1.2.2"
|
implementation project(path: ':icon-pack-classic')
|
||||||
|
implementation project(path: ':icon-pack-material')
|
||||||
// Modules import
|
|
||||||
implementation project(path: ':database')
|
|
||||||
implementation project(path: ':icon-pack')
|
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
androidTestImplementation "androidx.test:runner:$android_test_version"
|
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||||
|
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
app/libs/encrypt.aar
Normal file
@@ -1,90 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 2,
|
|
||||||
"identityHash": "f8fb4aed546de19ae7ca0797f49b26a4",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "file_database_history",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `database_alias` TEXT NOT NULL, `keyfile_uri` TEXT, `hardware_key` TEXT, `updated` INTEGER NOT NULL, PRIMARY KEY(`database_uri`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "databaseUri",
|
|
||||||
"columnName": "database_uri",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "databaseAlias",
|
|
||||||
"columnName": "database_alias",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "keyFileUri",
|
|
||||||
"columnName": "keyfile_uri",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hardwareKey",
|
|
||||||
"columnName": "hardware_key",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "updated",
|
|
||||||
"columnName": "updated",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"columnNames": [
|
|
||||||
"database_uri"
|
|
||||||
],
|
|
||||||
"autoGenerate": false
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "cipher_database",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `encrypted_value` TEXT NOT NULL, `specs_parameters` TEXT NOT NULL, PRIMARY KEY(`database_uri`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "databaseUri",
|
|
||||||
"columnName": "database_uri",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "encryptedValue",
|
|
||||||
"columnName": "encrypted_value",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "specParameters",
|
|
||||||
"columnName": "specs_parameters",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"columnNames": [
|
|
||||||
"database_uri"
|
|
||||||
],
|
|
||||||
"autoGenerate": false
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f8fb4aed546de19ae7ca0797f49b26a4')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 3,
|
|
||||||
"identityHash": "a20aec7cf09664b1102ec659fa51160a",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "file_database_history",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `database_alias` TEXT NOT NULL, `keyfile_uri` TEXT, `hardware_key` TEXT, `read_only` INTEGER, `updated` INTEGER NOT NULL, PRIMARY KEY(`database_uri`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "databaseUri",
|
|
||||||
"columnName": "database_uri",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "databaseAlias",
|
|
||||||
"columnName": "database_alias",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "keyFileUri",
|
|
||||||
"columnName": "keyfile_uri",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hardwareKey",
|
|
||||||
"columnName": "hardware_key",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "readOnly",
|
|
||||||
"columnName": "read_only",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "updated",
|
|
||||||
"columnName": "updated",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"database_uri"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "cipher_database",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `encrypted_value` TEXT NOT NULL, `specs_parameters` TEXT NOT NULL, PRIMARY KEY(`database_uri`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "databaseUri",
|
|
||||||
"columnName": "database_uri",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "encryptedValue",
|
|
||||||
"columnName": "encrypted_value",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "specParameters",
|
|
||||||
"columnName": "specs_parameters",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"database_uri"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a20aec7cf09664b1102ec659fa51160a')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 2.7 MiB After Width: | Height: | Size: 2.7 MiB |
@@ -2,9 +2,10 @@ package com.kunzisoft.keepass.tests.stream
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import com.kunzisoft.keepass.utils.readAllBytes
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryCache
|
import com.kunzisoft.keepass.database.element.binary.BinaryCache
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryFile
|
import com.kunzisoft.keepass.database.element.binary.BinaryFile
|
||||||
import com.kunzisoft.keepass.utils.readAllBytes
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import junit.framework.TestCase.assertEquals
|
import junit.framework.TestCase.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.io.DataInputStream
|
import java.io.DataInputStream
|
||||||
@@ -18,7 +19,7 @@ class BinaryDataTest {
|
|||||||
InstrumentationRegistry.getInstrumentation().context
|
InstrumentationRegistry.getInstrumentation().context
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cacheDirectory = context.filesDir
|
private val cacheDirectory = UriUtil.getBinaryDir(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||||
private val fileA = File(cacheDirectory, TEST_FILE_CACHE_A)
|
private val fileA = File(cacheDirectory, TEST_FILE_CACHE_A)
|
||||||
private val fileB = File(cacheDirectory, TEST_FILE_CACHE_B)
|
private val fileB = File(cacheDirectory, TEST_FILE_CACHE_B)
|
||||||
private val fileC = File(cacheDirectory, TEST_FILE_CACHE_C)
|
private val fileC = File(cacheDirectory, TEST_FILE_CACHE_C)
|
||||||
@@ -22,8 +22,6 @@ package com.kunzisoft.keepass.tests.utils
|
|||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.*
|
||||||
import junit.framework.TestCase
|
import junit.framework.TestCase
|
||||||
import org.joda.time.DateTime
|
|
||||||
import org.joda.time.Instant
|
|
||||||
import org.junit.Assert.assertArrayEquals
|
import org.junit.Assert.assertArrayEquals
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -138,54 +136,28 @@ class ValuesTest : TestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun testDate() {
|
fun testDate() {
|
||||||
val expected = DateInstant(
|
val cal = Calendar.getInstance()
|
||||||
Instant.ofEpochMilli(
|
|
||||||
DateTime(
|
|
||||||
2008,
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
3,
|
|
||||||
4,
|
|
||||||
5
|
|
||||||
).millis))
|
|
||||||
|
|
||||||
val actual = DateInstant(bytes5ToDate(dateTo5Bytes(expected)))
|
val expected = Calendar.getInstance()
|
||||||
|
expected.set(2008, 1, 2, 3, 4, 5)
|
||||||
|
|
||||||
val jDate = DateInstant()
|
val actual = Calendar.getInstance()
|
||||||
|
actual.time = DateInstant(bytes5ToDate(dateTo5Bytes(expected.time, cal), cal)).date
|
||||||
|
|
||||||
|
val jDate = DateInstant(System.currentTimeMillis())
|
||||||
val intermediate = DateInstant(jDate)
|
val intermediate = DateInstant(jDate)
|
||||||
val cDate = DateInstant(bytes5ToDate(dateTo5Bytes(intermediate)))
|
val cDate = DateInstant(bytes5ToDate(dateTo5Bytes(intermediate.date)))
|
||||||
|
|
||||||
assertEquals("Year mismatch: ", 2008, actual.getYear())
|
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR))
|
||||||
assertEquals("Month mismatch: ", 1, actual.getMonth())
|
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH))
|
||||||
assertEquals("Day mismatch: ", 2, actual.getDay())
|
assertEquals("Day mismatch: ", 2, actual.get(Calendar.DAY_OF_MONTH))
|
||||||
assertEquals("Hour mismatch: ", 3, actual.getHour())
|
assertEquals("Hour mismatch: ", 3, actual.get(Calendar.HOUR_OF_DAY))
|
||||||
assertEquals("Minute mismatch: ", 4, actual.getMinute())
|
assertEquals("Minute mismatch: ", 4, actual.get(Calendar.MINUTE))
|
||||||
assertEquals("Second mismatch: ", 5, actual.getSecond())
|
assertEquals("Second mismatch: ", 5, actual.get(Calendar.SECOND))
|
||||||
assertTrue("jDate and intermediate not equal", jDate == intermediate)
|
assertTrue("jDate and intermediate not equal", jDate == intermediate)
|
||||||
assertTrue("jDate $jDate and cDate $cDate not equal", cDate == jDate)
|
assertTrue("jDate $jDate and cDate $cDate not equal", cDate == jDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testDateCompare() {
|
|
||||||
val dateInstantA = DateInstant().apply {
|
|
||||||
setDate(2024, 12, 2)
|
|
||||||
setTime(5, 13)
|
|
||||||
}
|
|
||||||
val dateInstantB = DateInstant().apply {
|
|
||||||
setDate(2024, 12, 2)
|
|
||||||
setTime(5, 10)
|
|
||||||
}
|
|
||||||
val dateInstantC = DateInstant().apply {
|
|
||||||
setDate(2024, 12, 2)
|
|
||||||
setTime(5, 10)
|
|
||||||
}
|
|
||||||
assertTrue(dateInstantA.compareTo(dateInstantB) > 0)
|
|
||||||
assertTrue(dateInstantB.compareTo(dateInstantA) < 0)
|
|
||||||
assertTrue(dateInstantB.compareTo(dateInstantC) == 0)
|
|
||||||
assertTrue(dateInstantA.isAfter(dateInstantB))
|
|
||||||
assertTrue(dateInstantB.isBefore(dateInstantA))
|
|
||||||
assertFalse(dateInstantB.isBefore(dateInstantC))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun testUUID() {
|
fun testUUID() {
|
||||||
val bUUID = ByteArray(16)
|
val bUUID = ByteArray(16)
|
||||||
Random().nextBytes(bUUID)
|
Random().nextBytes(bUUID)
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
{
|
|
||||||
"apps": [
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "io.github.forkmaintainers.iceraven",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "9C:0D:22:37:9F:48:7B:70:A4:F9:F8:BE:C0:17:3C:F9:1A:16:44:F0:8F:93:38:5B:5B:78:2C:E3:76:60:BA:81"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "org.chromium.chrome",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "A8:56:48:50:79:BC:B3:57:BF:BE:69:BA:19:A9:BA:43:CD:0A:D9:AB:22:67:52:C7:80:B6:88:8A:FD:48:21:6B"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "org.cromite.cromite",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "63:3F:A4:1D:82:11:D6:D0:91:6A:81:9B:89:66:8C:6D:E9:2E:64:23:2D:A6:7F:9D:16:FD:81:C3:B7:E9:23:FF"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "org.ironfoxoss.ironfox",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "C5:E2:91:B5:A5:71:F9:C8:CD:9A:97:99:C2:C9:4E:02:EC:97:03:94:88:93:F2:CA:75:6D:67:B9:42:04:F9:04"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "org.mozilla.fennec_fdroid",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "06:66:53:58:EF:D8:BA:05:BE:23:6A:47:A1:2C:B0:95:8D:7D:75:DD:93:9D:77:C2:B3:1F:53:98:53:7E:BD:C5"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,820 +0,0 @@
|
|||||||
{
|
|
||||||
"apps": [
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.android.chrome",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "userdebug",
|
|
||||||
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.chrome.beta",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "DA:63:3D:34:B6:9E:63:AE:21:03:B4:9D:53:CE:05:2F:C5:F7:F3:C5:3A:AB:94:FD:C2:A2:08:BD:FD:14:24:9C"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.chrome.dev",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "90:44:EE:5F:EE:4B:BC:5E:21:DD:44:66:54:31:C4:EB:1F:1F:71:A3:27:16:A0:BC:92:7B:CB:B3:92:33:CA:BF"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.chrome.canary",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "20:19:DF:A1:FB:23:EF:BF:70:C5:BC:D1:44:3C:5B:EA:B0:4F:3F:2F:F4:36:6E:9A:C1:E3:45:76:39:A2:4C:FC"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "org.chromium.chrome",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "userdebug",
|
|
||||||
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.google.android.apps.chrome",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "userdebug",
|
|
||||||
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "org.mozilla.fennec_webauthndebug",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "userdebug",
|
|
||||||
"cert_fingerprint_sha256": "BD:AE:82:02:80:D2:AF:B7:74:94:EF:22:58:AA:78:A9:AE:A1:36:41:7E:8B:C2:3D:C9:87:75:2E:6F:48:E8:48"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "org.mozilla.firefox",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "org.mozilla.firefox_beta",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "org.mozilla.focus",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "org.mozilla.fennec_aurora",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "BC:04:88:83:8D:06:F4:CA:6B:F3:23:86:DA:AB:0D:D8:EB:CF:3E:77:30:78:74:59:F6:2F:B3:CD:14:A1:BA:AA"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "org.mozilla.rocket",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "86:3A:46:F0:97:39:32:B7:D0:19:9B:54:91:12:74:1C:2D:27:31:AC:72:EA:11:B7:52:3A:A9:0A:11:BF:56:91"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "org.mozilla.fenix",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "50:04:77:90:88:E7:F9:88:D5:BC:5C:C5:F8:79:8F:EB:F4:F8:CD:08:4A:1B:2A:46:EF:D4:C8:EE:4A:EA:F2:11"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "org.mozilla.fenix.debug",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "userdebug",
|
|
||||||
"cert_fingerprint_sha256": "BD:AE:82:02:80:D2:AF:B7:74:94:EF:22:58:AA:78:A9:AE:A1:36:41:7E:8B:C2:3D:C9:87:75:2E:6F:48:E8:48"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "org.mozilla.focus.beta",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "org.mozilla.focus.nightly",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "org.mozilla.klar",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "org.mozilla.reference.browser",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "B0:09:90:E3:0F:9D:81:5D:2E:BC:7B:9B:B2:21:CE:47:E5:C9:D5:17:AA:C7:0E:7F:D5:95:B1:E5:3E:9A:4B:14"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.microsoft.emmx.canary",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.microsoft.emmx.dev",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.microsoft.emmx.beta",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.microsoft.emmx",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.microsoft.emmx.rolling",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "userdebug",
|
|
||||||
"cert_fingerprint_sha256": "32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B:22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.microsoft.emmx.local",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "userdebug",
|
|
||||||
"cert_fingerprint_sha256": "32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B:22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.brave.browser",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.brave.browser_beta",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.brave.browser_nightly",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "app.vanadium.browser",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.vivaldi.browser",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.vivaldi.browser.snapshot",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.vivaldi.browser.sopranos",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.citrix.Receiver",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "3D:D1:12:67:10:69:AB:36:4E:F9:BE:73:9A:B7:B5:EE:15:E1:CD:E9:D8:75:7B:1B:F0:64:F5:0C:55:68:9A:49"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "CE:B2:23:D7:77:09:F2:B6:BC:0B:3A:78:36:F5:A5:AF:4C:E1:D3:55:F4:A7:28:86:F7:9D:F8:0D:C9:D6:12:2E"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "AA:D0:D4:57:E6:33:C3:78:25:77:30:5B:C1:B2:D9:E3:81:41:C7:21:DF:0D:AA:6E:29:07:2F:C4:1D:34:F0:AB"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.android.browser",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "C9:00:9D:01:EB:F9:F5:D0:30:2B:C7:1B:2F:E9:AA:9A:47:A4:32:BB:A1:73:08:A3:11:1B:75:D7:B2:14:90:25"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.sec.android.app.sbrowser",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "34:DF:0E:7A:9F:1C:F1:89:2E:45:C0:56:B4:97:3C:D8:1C:CF:14:8A:40:50:D1:1A:EA:4A:C5:A6:5F:90:0A:42"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.sec.android.app.sbrowser.beta",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "34:DF:0E:7A:9F:1C:F1:89:2E:45:C0:56:B4:97:3C:D8:1C:CF:14:8A:40:50:D1:1A:EA:4A:C5:A6:5F:90:0A:42"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.google.android.gms",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "7C:E8:3C:1B:71:F3:D5:72:FE:D0:4C:8D:40:C5:CB:10:FF:75:E6:D8:7D:9D:F6:FB:D5:3F:04:68:C2:90:50:53"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "D2:2C:C5:00:29:9F:B2:28:73:A0:1A:01:0D:E1:C8:2F:BE:4D:06:11:19:B9:48:14:DD:30:1D:AB:50:CB:76:78"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.yandex.browser",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.yandex.browser.beta",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.yandex.browser.alpha",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.yandex.browser.corp",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.yandex.browser.canary",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "1D:A9:CB:AE:2D:CC:C6:A5:8D:6C:94:7B:E9:4C:DB:B7:33:D6:5D:A4:D1:77:0F:A1:4A:53:64:CB:4A:28:EB:49"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.yandex.browser.broteam",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "1D:A9:CB:AE:2D:CC:C6:A5:8D:6C:94:7B:E9:4C:DB:B7:33:D6:5D:A4:D1:77:0F:A1:4A:53:64:CB:4A:28:EB:49"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.talonsec.talon",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "A3:66:03:44:A6:F6:AF:CA:81:8C:BF:43:96:A2:3C:CF:D5:ED:7A:78:1B:B4:A3:D1:85:03:01:E2:F4:6D:23:83"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "E2:A5:64:74:EA:23:7B:06:67:B6:F5:2C:DC:E9:04:5E:24:88:3B:AE:D0:82:59:9A:A2:DF:0B:60:3A:CF:6A:3B"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.talonsec.talon_beta",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "F5:86:62:7A:32:C8:9F:E6:7E:00:6D:B1:8C:34:31:9E:01:7F:B3:B2:BE:D6:9D:01:01:B7:F9:43:E7:7C:48:AE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "9A:A1:25:D5:E5:5E:3F:B0:DE:96:72:D9:A9:5D:04:65:3F:49:4A:1E:C3:EE:76:1E:94:C4:4E:5D:2F:65:8E:2F"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.duckduckgo.mobile.android.debug",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "C4:F0:9E:2B:D7:25:AD:F5:AD:92:0B:A2:80:27:66:AC:16:4A:C1:53:B3:EA:9E:08:48:B0:57:98:37:F7:6A:29"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.duckduckgo.mobile.android",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "BB:7B:B3:1C:57:3C:46:A1:DA:7F:C5:C5:28:A6:AC:F4:32:10:84:56:FE:EC:50:81:0C:7F:33:69:4E:B3:D2:D4"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.naver.whale",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "0B:8B:85:23:BB:4A:EF:FA:34:6E:4B:DD:4F:BF:7D:19:34:50:56:9A:A1:4A:AA:D4:AD:FD:94:A3:F7:B2:27:BB"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.fido.fido2client",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "FC:98:DA:E6:3A:D3:96:26:C8:C6:7F:BE:83:F2:F0:6F:74:93:2A:9C:D1:46:B9:2C:EC:FC:6A:04:7A:90:43:86"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.heytap.browser",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "AF:F8:A7:49:CF:0E:7D:75:44:65:D0:FB:FA:7B:8D:0C:64:5E:22:5C:10:C6:E2:32:AD:A0:D9:74:88:36:B8:E5"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "A8:FE:A4:CA:FB:93:32:DA:26:B8:E6:81:08:17:C1:DA:90:A5:03:0E:35:A6:0A:79:E0:6C:90:97:AA:C6:A4:42"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "io.island.Island",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "D9:C3:39:AC:9C:3A:EE:E1:75:1D:85:8C:35:D9:BA:C5:CC:87:B3:CE:76:30:93:F0:F5:10:64:F5:A2:F6:9B:04"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "userdebug",
|
|
||||||
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "io.island.IslandCanary",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "90:17:13:23:45:6E:6F:39:CB:FD:CF:B2:56:BE:1D:CF:F3:BC:1C:59:8A:15:93:30:E4:97:73:D0:4C:B9:C9:05"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "userdebug",
|
|
||||||
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "io.island.IslandBeta",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "35:31:83:1A:9E:2B:21:1D:E6:AA:C3:69:4B:45:83:6E:56:09:B9:D7:D0:04:C3:1B:21:87:40:FB:77:17:38:D1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "userdebug",
|
|
||||||
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "io.island.IslandDev",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "userdebug",
|
|
||||||
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "io.island.island.intune",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "C2:38:24:15:41:20:A0:8F:C3:95:42:AC:D8:2A:E9:24:94:78:80:1E:47:FD:6C:66:2B:18:1C:28:CA:7E:59:4E"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "userdebug",
|
|
||||||
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "io.island.island.canary.intune",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "1E:16:74:BB:79:EA:09:FB:37:CF:9F:1B:07:1B:1D:51:8D:46:03:0E:D3:EE:F2:C1:4E:AD:93:9E:C6:EE:3A:4C"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "userdebug",
|
|
||||||
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "io.island.island.beta.intune",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "D2:5E:AD:F6:1C:E6:36:6C:A4:23:A4:7F:C4:DB:9B:8C:9C:8A:35:B4:B0:19:E8:D9:82:FB:D0:8A:D9:DB:49:5A"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "userdebug",
|
|
||||||
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "io.island.island.dev.intune",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "userdebug",
|
|
||||||
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "net.quetta.browser",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "BE:FE:E7:31:12:6A:A5:6E:7E:FD:AE:AF:5E:F3:FA:EA:44:1C:19:CC:E0:CA:EC:42:6B:65:BB:F8:2C:59:46:80"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"build": "userdebug",
|
|
||||||
"cert_fingerprint_sha256": "F1:38:00:4F:38:04:51:D4:8A:05:2B:B3:A3:EF:17:24:23:D4:B0:D0:C8:A3:AA:DD:FB:DB:66:30:31:48:EC:A4"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "cz.seznam.sbrowser",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "DB:95:40:66:10:78:83:6E:4E:B1:66:F6:9E:F4:07:30:9E:8D:AE:33:34:68:5E:C8:F6:FA:2F:13:81:B9:AC:F6"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.opera.mini.native",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "57:AC:BC:52:5F:1B:2E:BD:19:19:6C:D6:F0:14:39:7C:C9:10:FD:18:84:1E:0A:E8:50:FE:BC:3E:1E:59:3F:F2"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "android",
|
|
||||||
"info": {
|
|
||||||
"package_name": "com.opera.mini.native.beta",
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"build": "release",
|
|
||||||
"cert_fingerprint_sha256": "57:AC:BC:52:5F:1B:2E:BD:19:19:6C:D6:F0:14:39:7C:C9:10:FD:18:84:1E:0A:E8:50:FE:BC:3E:1E:59:3F:F2"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,31 +1,61 @@
|
|||||||
<?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:translateX="6"
|
android:translateY="-332">
|
||||||
android:translateY="8">
|
<group
|
||||||
<path
|
android:translateY="332">
|
||||||
android:fillColor="#24000000"
|
<path
|
||||||
android:strokeWidth="1.99999297"
|
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:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
android:strokeLineJoin="round"
|
||||||
<path
|
android:strokeLineCap="round"
|
||||||
android:fillColor="#24000000"
|
android:strokeMiterLimit="4" >
|
||||||
android:strokeWidth="1.99999297"
|
<aapt:attr name="android:fillColor">
|
||||||
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" />
|
<gradient
|
||||||
</group>
|
android:endColor="#0000"
|
||||||
<group
|
android:endX="80"
|
||||||
android:translateX="6"
|
android:endY="80"
|
||||||
android:translateY="6">
|
android:startColor="#4e000000"
|
||||||
<path
|
android:startX="0"
|
||||||
android:fillColor="#ffa726"
|
android:startY="0"
|
||||||
android:strokeWidth="1.99999297"
|
android:type="linear"/>
|
||||||
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
</aapt:attr>
|
||||||
<path
|
</path>
|
||||||
android:fillColor="#ffffff"
|
</group>
|
||||||
android:strokeWidth="1.99999297"
|
<group
|
||||||
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:scaleX="0.3939503"
|
||||||
|
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>
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="120"
|
|
||||||
android:viewportHeight="120">
|
|
||||||
<group
|
|
||||||
android:translateX="6"
|
|
||||||
android:translateY="6">
|
|
||||||
<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 M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 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>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?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="#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>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?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="#ffffff"
|
|
||||||
android:strokeWidth="1.99999297"
|
|
||||||
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 3.5 KiB |
@@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@color/green" />
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
</adaptive-icon>
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@color/green" />
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 17 KiB |
@@ -1,31 +1,61 @@
|
|||||||
<?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:translateX="6"
|
android:translateY="-332">
|
||||||
android:translateY="8">
|
<group
|
||||||
<path
|
android:translateY="332">
|
||||||
android:fillColor="#24000000"
|
<path
|
||||||
android:strokeWidth="1.99999297"
|
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:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
android:strokeLineJoin="round"
|
||||||
<path
|
android:strokeLineCap="round"
|
||||||
android:fillColor="#24000000"
|
android:strokeMiterLimit="4" >
|
||||||
android:strokeWidth="1.99999297"
|
<aapt:attr name="android:fillColor">
|
||||||
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" />
|
<gradient
|
||||||
</group>
|
android:endColor="#0000"
|
||||||
<group
|
android:endX="80"
|
||||||
android:translateX="6"
|
android:endY="80"
|
||||||
android:translateY="6">
|
android:startColor="#4e000000"
|
||||||
<path
|
android:startX="0"
|
||||||
android:fillColor="#ffa726"
|
android:startY="0"
|
||||||
android:strokeWidth="1.99999297"
|
android:type="linear"/>
|
||||||
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
</aapt:attr>
|
||||||
<path
|
</path>
|
||||||
android:fillColor="#ffffff"
|
</group>
|
||||||
android:strokeWidth="1.99999297"
|
<group
|
||||||
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:scaleX="0.3939503"
|
||||||
|
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>
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="120"
|
|
||||||
android:viewportHeight="120">
|
|
||||||
<group
|
|
||||||
android:translateX="6"
|
|
||||||
android:translateY="6">
|
|
||||||
<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 M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 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>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?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="#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>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?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="#ffffff"
|
|
||||||
android:strokeWidth="1.99999297"
|
|
||||||
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 3.5 KiB |
5
app/src/libre/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?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: 2.3 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 17 KiB |
@@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="com.kunzisoft.keepass"
|
||||||
android:installLocation="auto">
|
android:installLocation="auto">
|
||||||
<supports-screens
|
<supports-screens
|
||||||
android:smallScreens="true"
|
android:smallScreens="true"
|
||||||
@@ -9,28 +10,19 @@
|
|||||||
android:anyDensity="true" />
|
android:anyDensity="true" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.FOREGROUND_SERVICE" />
|
android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.POST_NOTIFICATIONS" />
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.USE_BIOMETRIC" />
|
android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.VIBRATE"/>
|
android:name="android.permission.VIBRATE"/>
|
||||||
|
<!-- Write permission until Android 10 -->
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="28"
|
||||||
|
tools:ignore="ScopedStorage" />
|
||||||
<!-- Open apps from links -->
|
<!-- Open apps from links -->
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
tools:ignore="QueryAllPackagesPermission" />
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
<queries>
|
|
||||||
<intent>
|
|
||||||
<action android:name="android.intent.action.CREATE_DOCUMENT" />
|
|
||||||
<data android:mimeType="application/octet-stream" />
|
|
||||||
</intent>
|
|
||||||
</queries>
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
@@ -38,32 +30,29 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:name="com.kunzisoft.keepass.app.App"
|
android:name="com.kunzisoft.keepass.app.App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:fullBackupContent="@xml/old_backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:dataExtractionRules="@xml/backup_rules"
|
|
||||||
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:supportsRtl="true"
|
|
||||||
android:theme="@style/KeepassDXStyle.Night"
|
android:theme="@style/KeepassDXStyle.Night"
|
||||||
tools:targetApi="s"
|
tools:targetApi="n">
|
||||||
tools:ignore="CredentialDependency">
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.backup.api_key"
|
android:name="com.google.android.backup.api_key"
|
||||||
android:value="${googleAndroidBackupAPIKey}" />
|
android:value="${googleAndroidBackupAPIKey}" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity"
|
android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity"
|
||||||
|
android:theme="@style/KeepassDXStyle.SplashScreen"
|
||||||
|
android:label="@string/app_name"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:exported="true"
|
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:windowSoftInputMode="stateHidden|stateAlwaysHidden" >
|
android:windowSoftInputMode="stateHidden" >
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.MainCredentialActivity"
|
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
|
||||||
android:exported="true"
|
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:windowSoftInputMode="adjustResize|stateUnchanged">
|
android:windowSoftInputMode="adjustResize|stateUnchanged">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -122,9 +111,9 @@
|
|||||||
<!-- Main Activity -->
|
<!-- Main Activity -->
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
||||||
android:exported="false"
|
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustPan"
|
||||||
|
android:launchMode="singleTask">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.default_searchable"
|
android:name="android.app.default_searchable"
|
||||||
android:value="com.kunzisoft.keepass.search.SearchResults"
|
android:value="com.kunzisoft.keepass.search.SearchResults"
|
||||||
@@ -143,9 +132,6 @@
|
|||||||
<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" />
|
||||||
@@ -160,36 +146,16 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity" />
|
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
|
||||||
<activity
|
|
||||||
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
|
||||||
android:label="@string/keyboard_setting_label"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity"
|
|
||||||
tools:targetApi="26" />
|
|
||||||
<activity
|
|
||||||
android:name="com.kunzisoft.keepass.settings.PasskeysSettingsActivity"
|
|
||||||
tools:targetApi="34" />
|
|
||||||
<activity
|
|
||||||
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
|
|
||||||
<activity
|
|
||||||
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
|
|
||||||
android:theme="@style/Theme.Transparent" />
|
|
||||||
<activity
|
|
||||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity"
|
|
||||||
android:theme="@style/Theme.Transparent"
|
android:theme="@style/Theme.Transparent"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden" />
|
||||||
android:excludeFromRecents="true"/>
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
|
android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
||||||
android:theme="@style/Theme.Transparent"
|
<activity
|
||||||
android:launchMode="singleInstance"
|
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
||||||
android:exported="true">
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
||||||
|
android:theme="@style/Theme.Transparent">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
@@ -199,48 +165,41 @@
|
|||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:scheme="otpauth"/>
|
<data android:scheme="otpauth" android:host="totp" />
|
||||||
<data android:host="totp"/>
|
<data android:scheme="otpauth" android:host="hotp" />
|
||||||
<data android:host="hotp"/>
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
|
android:name="com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity"
|
||||||
android:theme="@style/Theme.Transparent"
|
android:theme="@style/Theme.Transparent" />
|
||||||
android:configChanges="keyboardHidden"
|
<activity
|
||||||
android:excludeFromRecents="true"
|
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
||||||
android:exported="false"
|
android:label="@string/keyboard_setting_label">
|
||||||
tools:targetApi="upside_down_cake" />
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
||||||
android:foregroundServiceType="dataSync"
|
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.services.AttachmentFileNotificationService"
|
android:name="com.kunzisoft.keepass.services.AttachmentFileNotificationService"
|
||||||
android:foregroundServiceType="dataSync"
|
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.services.ClipboardEntryNotificationService"
|
android:name="com.kunzisoft.keepass.services.ClipboardEntryNotificationService"
|
||||||
android:foregroundServiceType="specialUse"
|
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
|
android:name="com.kunzisoft.keepass.services.AdvancedUnlockNotificationService"
|
||||||
android:foregroundServiceType="specialUse"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false" />
|
|
||||||
<service
|
|
||||||
android:name="com.kunzisoft.keepass.services.DeviceUnlockNotificationService"
|
|
||||||
android:foregroundServiceType="specialUse"
|
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<!-- Receiver for Autofill -->
|
<!-- Receiver for Autofill -->
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService"
|
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
|
||||||
android:label="@string/app_name"
|
android:label="@string/autofill_service_name"
|
||||||
android:exported="true"
|
|
||||||
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.autofill"
|
android:name="android.autofill"
|
||||||
@@ -250,9 +209,8 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService"
|
android:name="com.kunzisoft.keepass.magikeyboard.MagikIME"
|
||||||
android:label="@string/keyboard_label"
|
android:label="@string/keyboard_label"
|
||||||
android:exported="true"
|
|
||||||
android:permission="android.permission.BIND_INPUT_METHOD" >
|
android:permission="android.permission.BIND_INPUT_METHOD" >
|
||||||
<meta-data android:name="android.view.im"
|
<meta-data android:name="android.view.im"
|
||||||
android:resource="@xml/keyboard_method"/>
|
android:resource="@xml/keyboard_method"/>
|
||||||
@@ -261,29 +219,9 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.credentialprovider.passkey.PasskeyProviderService"
|
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="true"
|
android:exported="false" />
|
||||||
android:label="@string/passkey_service_name"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
|
|
||||||
tools:targetApi="upside_down_cake">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.service.credentials.CredentialProviderService" />
|
|
||||||
</intent-filter>
|
|
||||||
<meta-data
|
|
||||||
android:name="android.credentials.provider"
|
|
||||||
android:resource="@xml/provider" />
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.app.action.ENTER_KNOX_DESKTOP_MODE" />
|
|
||||||
<action android:name="android.app.action.EXIT_KNOX_DESKTOP_MODE" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ package com.igreenwood.loupe
|
|||||||
import android.animation.Animator
|
import android.animation.Animator
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.graphics.Matrix
|
import android.graphics.Matrix
|
||||||
import android.graphics.PointF
|
import android.graphics.PointF
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
@@ -38,11 +37,7 @@ import android.graphics.RectF
|
|||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.GestureDetector
|
import android.view.*
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.ScaleGestureDetector
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
import android.view.animation.DecelerateInterpolator
|
import android.view.animation.DecelerateInterpolator
|
||||||
import android.view.animation.Interpolator
|
import android.view.animation.Interpolator
|
||||||
@@ -113,8 +108,6 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
var viewDragFriction = DEFAULT_VIEW_DRAG_FRICTION
|
var viewDragFriction = DEFAULT_VIEW_DRAG_FRICTION
|
||||||
// drag distance threshold in dp for swipe to dismiss
|
// drag distance threshold in dp for swipe to dismiss
|
||||||
var dragDismissDistanceInDp = DEFAULT_DRAG_DISMISS_DISTANCE_IN_DP
|
var dragDismissDistanceInDp = DEFAULT_DRAG_DISMISS_DISTANCE_IN_DP
|
||||||
// on view touched
|
|
||||||
var onViewTouchedListener: View.OnTouchListener? = null
|
|
||||||
// on view translate listener
|
// on view translate listener
|
||||||
var onViewTranslateListener: OnViewTranslateListener? = null
|
var onViewTranslateListener: OnViewTranslateListener? = null
|
||||||
// on scale changed
|
// on scale changed
|
||||||
@@ -176,16 +169,16 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
private val onScaleGestureListener: ScaleGestureDetector.OnScaleGestureListener =
|
private val onScaleGestureListener: ScaleGestureDetector.OnScaleGestureListener =
|
||||||
object : ScaleGestureDetector.OnScaleGestureListener {
|
object : ScaleGestureDetector.OnScaleGestureListener {
|
||||||
|
|
||||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
override fun onScale(detector: ScaleGestureDetector?): Boolean {
|
||||||
if (isDragging() || isBitmapTranslateAnimationRunning || isBitmapScaleAnimationRunninng) {
|
if (isDragging() || isBitmapTranslateAnimationRunning || isBitmapScaleAnimationRunninng) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val scaleFactor = detector.scaleFactor
|
val scaleFactor = detector?.scaleFactor ?: 1.0f
|
||||||
val focalX = detector.focusX
|
val focalX = detector?.focusX ?: bitmapBounds.centerX()
|
||||||
val focalY = detector.focusY
|
val focalY = detector?.focusY ?: bitmapBounds.centerY()
|
||||||
|
|
||||||
if (detector.scaleFactor == 1.0f) {
|
if (detector?.scaleFactor == 1.0f) {
|
||||||
// scale is not changing
|
// scale is not changing
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -195,23 +188,22 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScaleBegin(p0: ScaleGestureDetector): Boolean = true
|
override fun onScaleBegin(p0: ScaleGestureDetector?): Boolean = true
|
||||||
|
|
||||||
override fun onScaleEnd(p0: ScaleGestureDetector) {}
|
|
||||||
|
|
||||||
|
override fun onScaleEnd(p0: ScaleGestureDetector?) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val onGestureListener: GestureDetector.OnGestureListener =
|
private val onGestureListener: GestureDetector.OnGestureListener =
|
||||||
object : GestureDetector.SimpleOnGestureListener() {
|
object : GestureDetector.SimpleOnGestureListener() {
|
||||||
override fun onDown(e: MotionEvent): Boolean = true
|
override fun onDown(e: MotionEvent?): Boolean = true
|
||||||
|
|
||||||
override fun onScroll(
|
override fun onScroll(
|
||||||
e1: MotionEvent?,
|
e1: MotionEvent?,
|
||||||
e2: MotionEvent,
|
e2: MotionEvent?,
|
||||||
distanceX: Float,
|
distanceX: Float,
|
||||||
distanceY: Float
|
distanceY: Float
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (e2.pointerCount != 1) {
|
if (e2?.pointerCount != 1) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,11 +216,13 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onFling(
|
override fun onFling(
|
||||||
e1: MotionEvent?,
|
e1: MotionEvent?,
|
||||||
e2: MotionEvent,
|
e2: MotionEvent?,
|
||||||
velocityX: Float,
|
velocityX: Float,
|
||||||
velocityY: Float
|
velocityY: Float
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
e1 ?: return true
|
||||||
|
|
||||||
if (scale > minScale) {
|
if (scale > minScale) {
|
||||||
processFlingBitmap(velocityX, velocityY)
|
processFlingBitmap(velocityX, velocityY)
|
||||||
} else {
|
} else {
|
||||||
@@ -237,7 +231,9 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDoubleTap(e: MotionEvent): Boolean {
|
override fun onDoubleTap(e: MotionEvent?): Boolean {
|
||||||
|
e ?: return false
|
||||||
|
|
||||||
if (isBitmapScaleAnimationRunninng) {
|
if (isBitmapScaleAnimationRunninng) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -276,10 +272,7 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
private var imageViewRef: WeakReference<ImageView> = WeakReference(imageView)
|
private var imageViewRef: WeakReference<ImageView> = WeakReference(imageView)
|
||||||
private var containerRef: WeakReference<ViewGroup> = WeakReference(container)
|
private var containerRef: WeakReference<ViewGroup> = WeakReference(container)
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
|
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
|
||||||
onViewTouchedListener?.onTouch(view, event)
|
|
||||||
|
|
||||||
event ?: return false
|
event ?: return false
|
||||||
val imageView = imageViewRef.get() ?: return false
|
val imageView = imageViewRef.get() ?: return false
|
||||||
val container = containerRef.get() ?: return false
|
val container = containerRef.get() ?: return false
|
||||||
@@ -358,41 +351,78 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
val imageView = imageViewRef.get() ?: return
|
val imageView = imageViewRef.get() ?: return
|
||||||
|
|
||||||
isViewTranslateAnimationRunning = true
|
isViewTranslateAnimationRunning = true
|
||||||
|
|
||||||
imageView.run {
|
|
||||||
val translationY = if (velY > 0) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
originalViewBounds.top + height - top
|
imageView.run {
|
||||||
} else {
|
val translationY = if (velY > 0) {
|
||||||
originalViewBounds.top - height - top
|
originalViewBounds.top + height - top
|
||||||
|
} else {
|
||||||
|
originalViewBounds.top - height - top
|
||||||
|
}
|
||||||
|
animate()
|
||||||
|
.setDuration(dismissAnimationDuration)
|
||||||
|
.setInterpolator(dismissAnimationInterpolator)
|
||||||
|
.translationY(translationY.toFloat())
|
||||||
|
.setUpdateListener {
|
||||||
|
val amount = calcTranslationAmount()
|
||||||
|
changeBackgroundAlpha(amount)
|
||||||
|
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||||
|
}
|
||||||
|
.setListener(object : Animator.AnimatorListener {
|
||||||
|
override fun onAnimationStart(p0: Animator?) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(p0: Animator?) {
|
||||||
|
isViewTranslateAnimationRunning = false
|
||||||
|
onViewTranslateListener?.onDismiss(imageView)
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationCancel(p0: Animator?) {
|
||||||
|
isViewTranslateAnimationRunning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationRepeat(p0: Animator?) {
|
||||||
|
// no op
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
animate()
|
} else {
|
||||||
.setDuration(dismissAnimationDuration)
|
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, if (velY > 0) {
|
||||||
.setInterpolator(dismissAnimationInterpolator)
|
originalViewBounds.top + imageView.height - imageView.top
|
||||||
.translationY(translationY.toFloat())
|
} else {
|
||||||
.setUpdateListener {
|
originalViewBounds.top - imageView.height - imageView.top
|
||||||
val amount = calcTranslationAmount()
|
}.toFloat()).apply {
|
||||||
changeBackgroundAlpha(amount)
|
duration = dismissAnimationDuration
|
||||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
interpolator = dismissAnimationInterpolator
|
||||||
|
addUpdateListener {
|
||||||
|
val amount = calcTranslationAmount()
|
||||||
|
changeBackgroundAlpha(amount)
|
||||||
|
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||||
|
}
|
||||||
|
addListener(object : Animator.AnimatorListener {
|
||||||
|
override fun onAnimationStart(p0: Animator?) {
|
||||||
|
// no op
|
||||||
}
|
}
|
||||||
.setListener(object : Animator.AnimatorListener {
|
|
||||||
override fun onAnimationStart(p0: Animator) {
|
|
||||||
|
|
||||||
}
|
override fun onAnimationEnd(p0: Animator?) {
|
||||||
|
isViewTranslateAnimationRunning = false
|
||||||
|
onViewTranslateListener?.onDismiss(imageView)
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator) {
|
override fun onAnimationCancel(p0: Animator?) {
|
||||||
isViewTranslateAnimationRunning = false
|
isViewTranslateAnimationRunning = false
|
||||||
onViewTranslateListener?.onDismiss(imageView)
|
}
|
||||||
cleanup()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator) {
|
override fun onAnimationRepeat(p0: Animator?) {
|
||||||
isViewTranslateAnimationRunning = false
|
// no op
|
||||||
}
|
}
|
||||||
|
})
|
||||||
override fun onAnimationRepeat(p0: Animator) {
|
start()
|
||||||
// no op
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,20 +474,20 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
setTransform()
|
setTransform()
|
||||||
}
|
}
|
||||||
addListener(object : Animator.AnimatorListener {
|
addListener(object : Animator.AnimatorListener {
|
||||||
override fun onAnimationStart(p0: Animator) {
|
override fun onAnimationStart(p0: Animator?) {
|
||||||
isBitmapTranslateAnimationRunning = true
|
isBitmapTranslateAnimationRunning = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator) {
|
override fun onAnimationEnd(p0: Animator?) {
|
||||||
isBitmapTranslateAnimationRunning = false
|
isBitmapTranslateAnimationRunning = false
|
||||||
constrainBitmapBounds()
|
constrainBitmapBounds()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator) {
|
override fun onAnimationCancel(p0: Animator?) {
|
||||||
isBitmapTranslateAnimationRunning = false
|
isBitmapTranslateAnimationRunning = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationRepeat(p0: Animator) {
|
override fun onAnimationRepeat(p0: Animator?) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -495,11 +525,11 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
setTransform()
|
setTransform()
|
||||||
}
|
}
|
||||||
addListener(object : Animator.AnimatorListener {
|
addListener(object : Animator.AnimatorListener {
|
||||||
override fun onAnimationStart(p0: Animator) {
|
override fun onAnimationStart(p0: Animator?) {
|
||||||
isBitmapScaleAnimationRunninng = true
|
isBitmapScaleAnimationRunninng = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator) {
|
override fun onAnimationEnd(p0: Animator?) {
|
||||||
isBitmapScaleAnimationRunninng = false
|
isBitmapScaleAnimationRunninng = false
|
||||||
if (endScale == minScale) {
|
if (endScale == minScale) {
|
||||||
zoomToTargetScale(minScale, focalX, focalY)
|
zoomToTargetScale(minScale, focalX, focalY)
|
||||||
@@ -507,11 +537,11 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator) {
|
override fun onAnimationCancel(p0: Animator?) {
|
||||||
isBitmapScaleAnimationRunninng = false
|
isBitmapScaleAnimationRunninng = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationRepeat(p0: Animator) {
|
override fun onAnimationRepeat(p0: Animator?) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -549,11 +579,11 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
setTransform()
|
setTransform()
|
||||||
}
|
}
|
||||||
addListener(object : Animator.AnimatorListener {
|
addListener(object : Animator.AnimatorListener {
|
||||||
override fun onAnimationStart(p0: Animator) {
|
override fun onAnimationStart(p0: Animator?) {
|
||||||
isBitmapScaleAnimationRunninng = true
|
isBitmapScaleAnimationRunninng = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator) {
|
override fun onAnimationEnd(p0: Animator?) {
|
||||||
isBitmapScaleAnimationRunninng = false
|
isBitmapScaleAnimationRunninng = false
|
||||||
if (endScale == minScale) {
|
if (endScale == minScale) {
|
||||||
scale = minScale
|
scale = minScale
|
||||||
@@ -563,11 +593,11 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator) {
|
override fun onAnimationCancel(p0: Animator?) {
|
||||||
isBitmapScaleAnimationRunninng = false
|
isBitmapScaleAnimationRunninng = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationRepeat(p0: Animator) {
|
override fun onAnimationRepeat(p0: Animator?) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -620,76 +650,137 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
|
|
||||||
private fun restoreViewTransform() {
|
private fun restoreViewTransform() {
|
||||||
val imageView = imageViewRef.get() ?: return
|
val imageView = imageViewRef.get() ?: return
|
||||||
imageView.run {
|
|
||||||
animate()
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
.setDuration(restoreAnimationDuration)
|
imageView.run {
|
||||||
.setInterpolator(restoreAnimationInterpolator)
|
animate()
|
||||||
.translationY((originalViewBounds.top - top).toFloat())
|
.setDuration(restoreAnimationDuration)
|
||||||
.setUpdateListener {
|
.setInterpolator(restoreAnimationInterpolator)
|
||||||
val amount = calcTranslationAmount()
|
.translationY((originalViewBounds.top - top).toFloat())
|
||||||
changeBackgroundAlpha(amount)
|
.setUpdateListener {
|
||||||
onViewTranslateListener?.onViewTranslate(this, amount)
|
val amount = calcTranslationAmount()
|
||||||
|
changeBackgroundAlpha(amount)
|
||||||
|
onViewTranslateListener?.onViewTranslate(this, amount)
|
||||||
|
}
|
||||||
|
.setListener(object : Animator.AnimatorListener {
|
||||||
|
override fun onAnimationStart(p0: Animator?) {
|
||||||
|
// no op
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(p0: Animator?) {
|
||||||
|
onViewTranslateListener?.onRestore(imageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationCancel(p0: Animator?) {
|
||||||
|
// no op
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationRepeat(p0: Animator?) {
|
||||||
|
// no op
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, (originalViewBounds.top - imageView.top).toFloat()).apply {
|
||||||
|
duration = restoreAnimationDuration
|
||||||
|
interpolator = restoreAnimationInterpolator
|
||||||
|
addUpdateListener {
|
||||||
|
val amount = calcTranslationAmount()
|
||||||
|
changeBackgroundAlpha(amount)
|
||||||
|
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||||
|
}
|
||||||
|
addListener(object : Animator.AnimatorListener {
|
||||||
|
override fun onAnimationStart(p0: Animator?) {
|
||||||
|
// no op
|
||||||
}
|
}
|
||||||
.setListener(object : Animator.AnimatorListener {
|
|
||||||
override fun onAnimationStart(p0: Animator) {
|
|
||||||
// no op
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator) {
|
override fun onAnimationEnd(p0: Animator?) {
|
||||||
onViewTranslateListener?.onRestore(imageView)
|
onViewTranslateListener?.onRestore(imageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator) {
|
override fun onAnimationCancel(p0: Animator?) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationRepeat(p0: Animator) {
|
override fun onAnimationRepeat(p0: Animator?) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startDragToDismissAnimation() {
|
private fun startDragToDismissAnimation() {
|
||||||
val imageView = imageViewRef.get() ?: return
|
val imageView = imageViewRef.get() ?: return
|
||||||
|
|
||||||
imageView.run {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
val translationY = if (y - initialY > 0) {
|
imageView.run {
|
||||||
originalViewBounds.top + height - top
|
val translationY = if (y - initialY > 0) {
|
||||||
} else {
|
originalViewBounds.top + height - top
|
||||||
originalViewBounds.top - height - top
|
} else {
|
||||||
|
originalViewBounds.top - height - top
|
||||||
|
}
|
||||||
|
animate()
|
||||||
|
.setDuration(dismissAnimationDuration)
|
||||||
|
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||||
|
.translationY(translationY.toFloat())
|
||||||
|
.setUpdateListener {
|
||||||
|
val amount = calcTranslationAmount()
|
||||||
|
changeBackgroundAlpha(amount)
|
||||||
|
onViewTranslateListener?.onViewTranslate(this, amount)
|
||||||
|
}
|
||||||
|
.setListener(object : Animator.AnimatorListener {
|
||||||
|
override fun onAnimationStart(p0: Animator?) {
|
||||||
|
isViewTranslateAnimationRunning = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(p0: Animator?) {
|
||||||
|
isViewTranslateAnimationRunning = false
|
||||||
|
onViewTranslateListener?.onDismiss(imageView)
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationCancel(p0: Animator?) {
|
||||||
|
isViewTranslateAnimationRunning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationRepeat(p0: Animator?) {
|
||||||
|
// no op
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
animate()
|
} else {
|
||||||
.setDuration(dismissAnimationDuration)
|
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, imageView.translationY.toFloat()).apply {
|
||||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
duration = dismissAnimationDuration
|
||||||
.translationY(translationY.toFloat())
|
interpolator = AccelerateDecelerateInterpolator()
|
||||||
.setUpdateListener {
|
addUpdateListener {
|
||||||
val amount = calcTranslationAmount()
|
val amount = calcTranslationAmount()
|
||||||
changeBackgroundAlpha(amount)
|
changeBackgroundAlpha(amount)
|
||||||
onViewTranslateListener?.onViewTranslate(this, amount)
|
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||||
|
}
|
||||||
|
addListener(object : Animator.AnimatorListener {
|
||||||
|
override fun onAnimationStart(p0: Animator?) {
|
||||||
|
isViewTranslateAnimationRunning = true
|
||||||
}
|
}
|
||||||
.setListener(object : Animator.AnimatorListener {
|
|
||||||
override fun onAnimationStart(p0: Animator) {
|
|
||||||
isViewTranslateAnimationRunning = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator) {
|
override fun onAnimationEnd(p0: Animator?) {
|
||||||
isViewTranslateAnimationRunning = false
|
isViewTranslateAnimationRunning = false
|
||||||
onViewTranslateListener?.onDismiss(imageView)
|
onViewTranslateListener?.onDismiss(imageView)
|
||||||
cleanup()
|
cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator) {
|
override fun onAnimationCancel(p0: Animator?) {
|
||||||
isViewTranslateAnimationRunning = false
|
isViewTranslateAnimationRunning = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationRepeat(p0: Animator) {
|
override fun onAnimationRepeat(p0: Animator?) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processFlingToDismiss(velocityY: Float) {
|
private fun processFlingToDismiss(velocityY: Float) {
|
||||||
|
|||||||
@@ -24,15 +24,12 @@ import android.os.Bundle
|
|||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
|
|
||||||
import com.kunzisoft.keepass.utils.getPackageInfoCompat
|
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
class AboutActivity : StylishActivity() {
|
class AboutActivity : StylishActivity() {
|
||||||
@@ -48,16 +45,10 @@ class AboutActivity : StylishActivity() {
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
val appName = if (this.isContributingUser())
|
|
||||||
getString(R.string.app_name) + " " + getString(R.string.app_name_part3)
|
|
||||||
else
|
|
||||||
getString(R.string.app_name)
|
|
||||||
findViewById<TextView>(R.id.activity_about_app_name).text = appName
|
|
||||||
|
|
||||||
var version: String
|
var version: String
|
||||||
var build: String
|
var build: String
|
||||||
try {
|
try {
|
||||||
version = packageManager.getPackageInfoCompat(packageName).versionName ?: ""
|
version = packageManager.getPackageInfo(packageName, 0).versionName
|
||||||
build = BuildConfig.BUILD_VERSION
|
build = BuildConfig.BUILD_VERSION
|
||||||
} catch (e: NameNotFoundException) {
|
} catch (e: NameNotFoundException) {
|
||||||
Log.w(javaClass.simpleName, "Unable to get the app or the build version", e)
|
Log.w(javaClass.simpleName, "Unable to get the app or the build version", e)
|
||||||
@@ -77,14 +68,6 @@ class AboutActivity : StylishActivity() {
|
|||||||
movementMethod = LinkMovementMethod.getInstance()
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
|
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
|
||||||
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||||
textDirection = View.TEXT_DIRECTION_ANY_RTL
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
findViewById<TextView>(R.id.activity_about_privacy_text).apply {
|
|
||||||
movementMethod = LinkMovementMethod.getInstance()
|
|
||||||
text = HtmlCompat.fromHtml(getString(R.string.html_about_privacy),
|
|
||||||
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
findViewById<TextView>(R.id.activity_about_contribution_text).apply {
|
findViewById<TextView>(R.id.activity_about_contribution_text).apply {
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentSender
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.inputmethod.InlineSuggestionsRequest
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST
|
||||||
|
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
|
class AutofillLauncherActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
|
// Retrieve selection mode
|
||||||
|
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
|
||||||
|
when (specialMode) {
|
||||||
|
SpecialMode.SELECTION -> {
|
||||||
|
// Build search param
|
||||||
|
val searchInfo = SearchInfo().apply {
|
||||||
|
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
|
||||||
|
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
||||||
|
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME)
|
||||||
|
}
|
||||||
|
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
|
searchInfo.webDomain = concreteWebDomain
|
||||||
|
launchSelection(searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpecialMode.REGISTRATION -> {
|
||||||
|
// To register info
|
||||||
|
val registerInfo = intent.getParcelableExtra<RegisterInfo>(KEY_REGISTER_INFO)
|
||||||
|
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
||||||
|
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
|
searchInfo.webDomain = concreteWebDomain
|
||||||
|
launchRegistration(searchInfo, registerInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Not an autofill call
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchSelection(searchInfo: SearchInfo) {
|
||||||
|
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
||||||
|
val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
|
||||||
|
|
||||||
|
if (autofillComponent == null) {
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
} else if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
||||||
|
PreferencesUtil.applicationIdBlocklist(this))
|
||||||
|
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
||||||
|
PreferencesUtil.webDomainBlocklist(this))) {
|
||||||
|
showBlockRestartMessage()
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
val database = Database.getInstance()
|
||||||
|
val readOnly = database.isReadOnly
|
||||||
|
// If database is open
|
||||||
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
|
Database.getInstance(),
|
||||||
|
searchInfo,
|
||||||
|
{ items ->
|
||||||
|
// Items found
|
||||||
|
AutofillHelper.buildResponseAndSetResult(this, items)
|
||||||
|
finish()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
GroupActivity.launchForAutofillResult(this,
|
||||||
|
readOnly,
|
||||||
|
autofillComponent,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// If database not open
|
||||||
|
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
||||||
|
autofillComponent,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchRegistration(searchInfo: SearchInfo, registerInfo: RegisterInfo?) {
|
||||||
|
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
||||||
|
PreferencesUtil.applicationIdBlocklist(this))
|
||||||
|
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
||||||
|
PreferencesUtil.webDomainBlocklist(this))) {
|
||||||
|
showBlockRestartMessage()
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
} else {
|
||||||
|
val database = Database.getInstance()
|
||||||
|
val readOnly = database.isReadOnly
|
||||||
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
|
database,
|
||||||
|
searchInfo,
|
||||||
|
{ _ ->
|
||||||
|
if (!readOnly) {
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
GroupActivity.launchForRegistration(this,
|
||||||
|
registerInfo)
|
||||||
|
} else {
|
||||||
|
showReadOnlySaveMessage()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
if (!readOnly) {
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
GroupActivity.launchForRegistration(this,
|
||||||
|
registerInfo)
|
||||||
|
} else {
|
||||||
|
showReadOnlySaveMessage()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// If database not open
|
||||||
|
FileDatabaseSelectActivity.launchForRegistration(this,
|
||||||
|
registerInfo)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showBlockRestartMessage() {
|
||||||
|
// If item not allowed, show a toast
|
||||||
|
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showReadOnlySaveMessage() {
|
||||||
|
Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
|
|
||||||
|
if (PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
|
||||||
|
// Close the database
|
||||||
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
|
||||||
|
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
|
||||||
|
private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME"
|
||||||
|
|
||||||
|
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
||||||
|
|
||||||
|
fun getAuthIntentSenderForSelection(context: Context,
|
||||||
|
searchInfo: SearchInfo? = null,
|
||||||
|
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): IntentSender {
|
||||||
|
return PendingIntent.getActivity(context, 0,
|
||||||
|
// Doesn't work with Parcelable (don't know why?)
|
||||||
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
|
searchInfo?.let {
|
||||||
|
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
|
||||||
|
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
|
||||||
|
putExtra(KEY_SEARCH_SCHEME, it.webScheme)
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
inlineSuggestionsRequest?.let {
|
||||||
|
putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAuthIntentSenderForRegistration(context: Context,
|
||||||
|
registerInfo: RegisterInfo): IntentSender {
|
||||||
|
return PendingIntent.getActivity(context, 0,
|
||||||
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
|
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
||||||
|
putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||||
|
},
|
||||||
|
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchForRegistration(context: Context,
|
||||||
|
registerInfo: RegisterInfo) {
|
||||||
|
val intent = Intent(context, AutofillLauncherActivity::class.java)
|
||||||
|
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
||||||
|
intent.putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,98 +30,70 @@ import android.util.Log
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import android.widget.Toast
|
||||||
import androidx.activity.viewModels
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
|
||||||
import androidx.core.graphics.BlendModeCompat
|
|
||||||
import androidx.core.graphics.ColorUtils
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
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.tabs.TabLayout
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.adapters.TagsAdapter
|
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||||
|
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.otp.OtpType
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
|
||||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.view.EntryContentsView
|
||||||
import com.kunzisoft.keepass.view.WindowInsetPosition
|
|
||||||
import com.kunzisoft.keepass.view.applyWindowInsets
|
|
||||||
import com.kunzisoft.keepass.view.changeControlColor
|
|
||||||
import com.kunzisoft.keepass.view.changeTitleColor
|
|
||||||
import com.kunzisoft.keepass.view.hideByFading
|
|
||||||
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
import java.util.*
|
||||||
import java.util.UUID
|
import kotlin.collections.HashMap
|
||||||
|
|
||||||
class EntryActivity : DatabaseLockActivity() {
|
class EntryActivity : LockingActivity() {
|
||||||
|
|
||||||
private var footer: ViewGroup? = null
|
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||||
private var appBarLayout: AppBarLayout? = null
|
|
||||||
private var 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 entryContentsView: EntryContentsView? = null
|
||||||
private var entryContentTab: TabLayout? = null
|
private var entryProgress: ProgressBar? = null
|
||||||
private var tagsAdapter: TagsAdapter? = null
|
|
||||||
private var entryProgress: LinearProgressIndicator? = null
|
|
||||||
private var lockView: View? = null
|
private var lockView: View? = null
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
private var loadingView: ProgressBar? = null
|
|
||||||
|
|
||||||
private val mEntryViewModel: EntryViewModel by viewModels()
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
private val mEntryActivityEducation = EntryActivityEducation(this)
|
private var mEntry: Entry? = null
|
||||||
|
|
||||||
private var mMainEntryId: NodeId<UUID>? = null
|
private var mIsHistory: Boolean = false
|
||||||
private var mHistoryPosition: Int = -1
|
private var mEntryLastVersion: Entry? = null
|
||||||
private var mEntryIsHistory: Boolean = false
|
private var mEntryHistoryPosition: Int = -1
|
||||||
private var mEntryLoaded = false
|
|
||||||
|
private var mShowPassword: Boolean = false
|
||||||
|
|
||||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
|
||||||
private var mAttachmentSelected: Attachment? = null
|
|
||||||
|
|
||||||
private var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) {
|
private var clipboardHelper: ClipboardHelper? = null
|
||||||
// Reload the current id from database
|
private var mFirstLaunchOfActivity: Boolean = false
|
||||||
mEntryViewModel.loadDatabase(mDatabase)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var mIcon: IconImage? = null
|
private var iconColor: Int = 0
|
||||||
private var mColorSecondary: Int = 0
|
|
||||||
private var mColorSurface: Int = 0
|
|
||||||
private var mColorOnSurface: Int = 0
|
|
||||||
private var mColorBackground: Int = 0
|
|
||||||
private var mBackgroundColor: Int? = null
|
|
||||||
private var mForegroundColor: Int? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -133,217 +105,60 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
|
mDatabase = Database.getInstance()
|
||||||
|
mReadOnly = mDatabase!!.isReadOnly || mReadOnly
|
||||||
|
|
||||||
|
mShowPassword = !PreferencesUtil.isPasswordMask(this)
|
||||||
|
|
||||||
|
// Retrieve the textColor to tint the icon
|
||||||
|
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||||
|
iconColor = taIconColor.getColor(0, Color.BLACK)
|
||||||
|
taIconColor.recycle()
|
||||||
|
|
||||||
|
// Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
// Get views
|
// Get views
|
||||||
footer = findViewById(R.id.activity_entry_footer)
|
|
||||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||||
appBarLayout = findViewById(R.id.app_bar)
|
|
||||||
titleIconView = findViewById(R.id.entry_icon)
|
titleIconView = findViewById(R.id.entry_icon)
|
||||||
historyView = findViewById(R.id.history_container)
|
historyView = findViewById(R.id.history_container)
|
||||||
tagsListView = findViewById(R.id.entry_tags_list_view)
|
entryContentsView = findViewById(R.id.entry_contents)
|
||||||
entryContentTab = findViewById(R.id.entry_content_tab)
|
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
||||||
|
entryContentsView?.setAttachmentCipherKey(mDatabase)
|
||||||
entryProgress = findViewById(R.id.entry_progress)
|
entryProgress = findViewById(R.id.entry_progress)
|
||||||
lockView = findViewById(R.id.lock_button)
|
lockView = findViewById(R.id.lock_button)
|
||||||
loadingView = findViewById(R.id.loading)
|
|
||||||
|
|
||||||
// To apply fit window with transparency
|
|
||||||
setTransparentNavigationBar {
|
|
||||||
// To fix margin with API 27
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null)
|
|
||||||
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
|
|
||||||
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Empty title
|
|
||||||
collapsingToolbarLayout?.title = " "
|
|
||||||
toolbar?.title = " "
|
|
||||||
|
|
||||||
// Retrieve the textColor to tint the toolbar
|
|
||||||
val taColorSecondary = theme.obtainStyledAttributes(intArrayOf(R.attr.colorSecondary))
|
|
||||||
val taColorSurface = theme.obtainStyledAttributes(intArrayOf(R.attr.colorSurface))
|
|
||||||
val taColorOnSurface = theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnSurface))
|
|
||||||
val taColorBackground = theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
|
|
||||||
mColorSecondary = taColorSecondary.getColor(0, Color.BLACK)
|
|
||||||
mColorSurface = taColorSurface.getColor(0, Color.BLACK)
|
|
||||||
mColorOnSurface = taColorOnSurface.getColor(0, Color.BLACK)
|
|
||||||
mColorBackground = taColorBackground.getColor(0, Color.BLACK)
|
|
||||||
taColorSecondary.recycle()
|
|
||||||
taColorSurface.recycle()
|
|
||||||
taColorOnSurface.recycle()
|
|
||||||
taColorBackground.recycle()
|
|
||||||
|
|
||||||
// Init Tags adapter
|
|
||||||
tagsAdapter = TagsAdapter(this)
|
|
||||||
tagsListView?.apply {
|
|
||||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
|
||||||
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
|
|
||||||
try {
|
|
||||||
intent.getParcelableExtraCompat<NodeId<UUID>>(KEY_ENTRY)?.let { mainEntryId ->
|
|
||||||
intent.removeExtra(KEY_ENTRY)
|
|
||||||
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
|
|
||||||
intent.removeExtra(KEY_ENTRY_HISTORY_POSITION)
|
|
||||||
|
|
||||||
mEntryViewModel.loadEntry(mDatabase, mainEntryId, historyPosition)
|
|
||||||
}
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
Log.e(TAG, "Unable to retrieve the entry key")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init SAF manager
|
|
||||||
mExternalFileHelper = ExternalFileHelper(this)
|
|
||||||
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
|
|
||||||
mAttachmentSelected?.let { attachment ->
|
|
||||||
if (createdFileUri != null) {
|
|
||||||
mAttachmentFileBinderManager
|
|
||||||
?.startDownloadAttachment(createdFileUri, attachment)
|
|
||||||
}
|
|
||||||
mAttachmentSelected = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Init attachment service binder manager
|
|
||||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
|
||||||
|
|
||||||
lockView?.setOnClickListener {
|
lockView?.setOnClickListener {
|
||||||
lockAndExit()
|
lockAndExit()
|
||||||
}
|
}
|
||||||
|
|
||||||
mEntryViewModel.sectionSelected.observe(this) { entrySection ->
|
// Focus view to reinitialize timeout
|
||||||
entryContentTab?.getTabAt(entrySection.position)?.select()
|
coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
||||||
}
|
|
||||||
|
|
||||||
mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory ->
|
// Init the clipboard helper
|
||||||
if (entryInfoHistory != null) {
|
clipboardHelper = ClipboardHelper(this)
|
||||||
this.mMainEntryId = entryInfoHistory.mainEntryId
|
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
|
||||||
|
|
||||||
// Manage history position
|
// Init attachment service binder manager
|
||||||
val historyPosition = entryInfoHistory.historyPosition
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
this.mHistoryPosition = historyPosition
|
|
||||||
val entryIsHistory = historyPosition > -1
|
|
||||||
this.mEntryIsHistory = entryIsHistory
|
|
||||||
// Assign history dedicated view
|
|
||||||
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
|
|
||||||
// TODO History badge
|
|
||||||
/*
|
|
||||||
if (entryIsHistory) {
|
|
||||||
}*/
|
|
||||||
|
|
||||||
val entryInfo = entryInfoHistory.entryInfo
|
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
||||||
// Manage entry copy to start notification if allowed (at the first start)
|
when (actionTask) {
|
||||||
if (savedInstanceState == null) {
|
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
||||||
// Manage entry to launch copying notification if allowed
|
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
|
||||||
ClipboardEntryNotificationService.checkAndLaunchNotification(this, entryInfo)
|
// Close the current activity after an history action
|
||||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
if (result.isSuccess)
|
||||||
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
finish()
|
||||||
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Assign title icon
|
ACTION_DATABASE_RELOAD_TASK -> {
|
||||||
mIcon = entryInfo.icon
|
// Close the current activity
|
||||||
// Assign title text
|
this.showActionErrorIfNeeded(result)
|
||||||
val entryTitle =
|
|
||||||
entryInfo.title.ifEmpty { entryInfo.id.asHexString() }
|
|
||||||
collapsingToolbarLayout?.title = entryTitle
|
|
||||||
toolbar?.title = entryTitle
|
|
||||||
// Assign tags
|
|
||||||
val tags = entryInfo.tags
|
|
||||||
tagsListView?.visibility = if (tags.isEmpty()) View.GONE else View.VISIBLE
|
|
||||||
tagsAdapter?.setTags(tags)
|
|
||||||
// Assign colors
|
|
||||||
val showEntryColors = PreferencesUtil.showEntryColors(this)
|
|
||||||
mBackgroundColor = if (showEntryColors) entryInfo.backgroundColor else null
|
|
||||||
mForegroundColor = if (showEntryColors) entryInfo.foregroundColor else null
|
|
||||||
|
|
||||||
loadingView?.hideByFading()
|
|
||||||
mEntryLoaded = true
|
|
||||||
} else {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
// Refresh Menu
|
|
||||||
invalidateOptionsMenu()
|
|
||||||
}
|
|
||||||
|
|
||||||
mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement ->
|
|
||||||
if (otpElement == null) {
|
|
||||||
entryProgress?.visibility = View.GONE
|
|
||||||
} else when (otpElement.type) {
|
|
||||||
// Only add token if HOTP
|
|
||||||
OtpType.HOTP -> {
|
|
||||||
entryProgress?.visibility = View.GONE
|
|
||||||
}
|
|
||||||
// Refresh view if TOTP
|
|
||||||
OtpType.TOTP -> {
|
|
||||||
entryProgress?.apply {
|
|
||||||
max = otpElement.period
|
|
||||||
setProgressCompat(otpElement.secondsRemaining, true)
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected ->
|
|
||||||
mAttachmentSelected = attachmentSelected
|
|
||||||
mExternalFileHelper?.createDocument(attachmentSelected.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
|
||||||
mDatabase?.let { database ->
|
|
||||||
launch(
|
|
||||||
this,
|
|
||||||
database,
|
|
||||||
historySelected.nodeId,
|
|
||||||
historySelected.historyPosition,
|
|
||||||
mEntryActivityResultLauncher
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun finishActivityIfReloadRequested(): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun viewToInvalidateTimeout(): View? {
|
|
||||||
return coordinatorLayout
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
|
|
||||||
mEntryViewModel.loadDatabase(database)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
|
||||||
database: ContextualDatabase,
|
|
||||||
actionTask: String,
|
|
||||||
result: ActionRunnable.Result
|
|
||||||
) {
|
|
||||||
super.onDatabaseActionFinished(database, actionTask, result)
|
|
||||||
when (actionTask) {
|
|
||||||
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
|
||||||
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
|
|
||||||
// Close the current activity after an history action
|
|
||||||
if (result.isSuccess)
|
|
||||||
finish()
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
coordinatorLayout?.showActionErrorIfNeeded(result)
|
||||||
}
|
}
|
||||||
coordinatorLayout?.showActionErrorIfNeeded(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -356,19 +171,63 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
View.GONE
|
View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
mAttachmentFileBinderManager?.apply {
|
// Get Entry from UUID
|
||||||
registerProgressTask()
|
try {
|
||||||
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
val keyEntry: NodeId<UUID>? = intent.getParcelableExtra(KEY_ENTRY)
|
||||||
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
if (keyEntry != null) {
|
||||||
mEntryViewModel.onAttachmentAction(entryAttachmentState)
|
mEntry = mDatabase?.getEntryById(keyEntry)
|
||||||
|
mEntryLastVersion = mEntry
|
||||||
|
}
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
Log.e(TAG, "Unable to retrieve the entry key")
|
||||||
|
}
|
||||||
|
|
||||||
|
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, mEntryHistoryPosition)
|
||||||
|
mEntryHistoryPosition = historyPosition
|
||||||
|
if (historyPosition >= 0) {
|
||||||
|
mIsHistory = true
|
||||||
|
mEntry = mEntry?.getHistory()?.get(historyPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mEntry == null) {
|
||||||
|
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show()
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last access time.
|
||||||
|
mEntry?.touch(modified = false, touchParents = false)
|
||||||
|
|
||||||
|
mEntry?.let { entry ->
|
||||||
|
// Fill data in resume to update from EntryEditActivity
|
||||||
|
fillEntryDataInContentsView(entry)
|
||||||
|
// Refresh Menu
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
|
val entryInfo = entry.getEntryInfo(mDatabase)
|
||||||
|
// Manage entry copy to start notification if allowed
|
||||||
|
if (mFirstLaunchOfActivity) {
|
||||||
|
// Manage entry to launch copying notification if allowed
|
||||||
|
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
||||||
|
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||||
|
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
||||||
|
MagikIME.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the screen on
|
mAttachmentFileBinderManager?.apply {
|
||||||
if (PreferencesUtil.isKeepScreenOnEnabled(this)) {
|
registerProgressTask()
|
||||||
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||||
|
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||||
|
if (entryAttachmentState.streamDirection != StreamDirection.UPLOAD) {
|
||||||
|
entryContentsView?.putAttachment(entryAttachmentState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mFirstLaunchOfActivity = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -377,90 +236,223 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyToolbarColors() {
|
private fun fillEntryDataInContentsView(entry: Entry) {
|
||||||
collapsingToolbarLayout?.setBackgroundColor(mBackgroundColor ?: mColorSurface)
|
|
||||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(mBackgroundColor ?: mColorSurface)
|
val entryInfo = entry.getEntryInfo(mDatabase)
|
||||||
val backgroundDarker = if (mBackgroundColor != null) {
|
|
||||||
ColorUtils.blendARGB(mBackgroundColor!!, Color.WHITE, 0.1f)
|
// Assign title icon
|
||||||
} else {
|
titleIconView?.let { iconView ->
|
||||||
mColorBackground
|
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, iconColor)
|
||||||
}
|
}
|
||||||
titleIconView?.background?.colorFilter = BlendModeColorFilterCompat
|
|
||||||
.createBlendModeColorFilterCompat(backgroundDarker, BlendModeCompat.SRC_IN)
|
// Assign title text
|
||||||
mIcon?.let { icon ->
|
val entryTitle = entryInfo.title
|
||||||
titleIconView?.let { iconView ->
|
collapsingToolbarLayout?.title = entryTitle
|
||||||
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(
|
toolbar?.title = entryTitle
|
||||||
iconView,
|
|
||||||
icon,
|
// Assign basic fields
|
||||||
mForegroundColor ?: mColorSecondary
|
entryContentsView?.assignUserName(entryInfo.username) {
|
||||||
|
clipboardHelper?.timeoutCopyToClipboard(entryInfo.username,
|
||||||
|
getString(R.string.copy_field,
|
||||||
|
getString(R.string.entry_user_name)))
|
||||||
|
}
|
||||||
|
|
||||||
|
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
||||||
|
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
||||||
|
val allowCopyPasswordAndProtectedFields =
|
||||||
|
PreferencesUtil.allowCopyPasswordAndProtectedFields(this)
|
||||||
|
|
||||||
|
val showWarningClipboardDialogOnClickListener = View.OnClickListener {
|
||||||
|
AlertDialog.Builder(this@EntryActivity)
|
||||||
|
.setMessage(getString(R.string.allow_copy_password_warning) +
|
||||||
|
"\n\n" +
|
||||||
|
getString(R.string.clipboard_warning))
|
||||||
|
.create().apply {
|
||||||
|
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ ->
|
||||||
|
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true)
|
||||||
|
dialog.dismiss()
|
||||||
|
fillEntryDataInContentsView(entry)
|
||||||
|
}
|
||||||
|
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
|
||||||
|
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false)
|
||||||
|
dialog.dismiss()
|
||||||
|
fillEntryDataInContentsView(entry)
|
||||||
|
}
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
|
||||||
|
View.OnClickListener {
|
||||||
|
clipboardHelper?.timeoutCopyToClipboard(entryInfo.password,
|
||||||
|
getString(R.string.copy_field,
|
||||||
|
getString(R.string.entry_password)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If dialog not already shown
|
||||||
|
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||||
|
showWarningClipboardDialogOnClickListener
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entryContentsView?.assignPassword(entryInfo.password,
|
||||||
|
allowCopyPasswordAndProtectedFields,
|
||||||
|
onPasswordCopyClickListener)
|
||||||
|
|
||||||
|
//Assign OTP field
|
||||||
|
entry.getOtpElement()?.let { otpElement ->
|
||||||
|
entryContentsView?.assignOtp(otpElement, entryProgress) {
|
||||||
|
clipboardHelper?.timeoutCopyToClipboard(
|
||||||
|
otpElement.token,
|
||||||
|
getString(R.string.copy_field, getString(R.string.entry_otp))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toolbar?.changeControlColor(mForegroundColor ?: mColorOnSurface)
|
|
||||||
collapsingToolbarLayout?.changeTitleColor(mForegroundColor ?: mColorOnSurface)
|
entryContentsView?.assignURL(entryInfo.url)
|
||||||
|
entryContentsView?.assignNotes(entryInfo.notes)
|
||||||
|
|
||||||
|
// Assign custom fields
|
||||||
|
if (mDatabase?.allowEntryCustomFields() == true) {
|
||||||
|
entryContentsView?.clearExtraFields()
|
||||||
|
entryInfo.customFields.forEach { field ->
|
||||||
|
val label = field.name
|
||||||
|
// OTP field is already managed in dedicated view
|
||||||
|
if (label != OtpEntryFields.OTP_TOKEN_FIELD) {
|
||||||
|
val value = field.protectedValue
|
||||||
|
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
||||||
|
if (allowCopyProtectedField) {
|
||||||
|
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
|
||||||
|
clipboardHelper?.timeoutCopyToClipboard(
|
||||||
|
value.toString(),
|
||||||
|
getString(R.string.copy_field, label)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If dialog not already shown
|
||||||
|
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||||
|
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
|
||||||
|
} else {
|
||||||
|
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
|
||||||
|
|
||||||
|
// Manage attachments
|
||||||
|
entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
||||||
|
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
||||||
|
mAttachmentsToDownload[requestCode] = attachmentItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign dates
|
||||||
|
entryContentsView?.assignCreationDate(entryInfo.creationTime)
|
||||||
|
entryContentsView?.assignModificationDate(entryInfo.lastModificationTime)
|
||||||
|
entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime)
|
||||||
|
|
||||||
|
// Manage history
|
||||||
|
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
|
||||||
|
if (mIsHistory) {
|
||||||
|
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||||
|
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
||||||
|
taColorAccent.recycle()
|
||||||
|
}
|
||||||
|
entryContentsView?.assignHistory(entry.getHistory()) { historyItem, position ->
|
||||||
|
launch(this, historyItem, mReadOnly, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign special data
|
||||||
|
entryContentsView?.assignUUID(entry.nodeId.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
when (requestCode) {
|
||||||
|
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE ->
|
||||||
|
// Not directly get the entry from intent data but from database
|
||||||
|
mEntry?.let {
|
||||||
|
fillEntryDataInContentsView(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
||||||
|
if (createdFileUri != null) {
|
||||||
|
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
|
||||||
|
mAttachmentFileBinderManager
|
||||||
|
?.startDownloadAttachment(createdFileUri, attachmentToDownload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
if (mEntryLoaded) {
|
|
||||||
val inflater = menuInflater
|
|
||||||
|
|
||||||
inflater.inflate(R.menu.entry, menu)
|
val inflater = menuInflater
|
||||||
inflater.inflate(R.menu.database, menu)
|
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||||
|
inflater.inflate(R.menu.entry, menu)
|
||||||
|
inflater.inflate(R.menu.database, menu)
|
||||||
|
if (mIsHistory && !mReadOnly) {
|
||||||
|
inflater.inflate(R.menu.entry_history, menu)
|
||||||
|
}
|
||||||
|
if (mIsHistory || mReadOnly) {
|
||||||
|
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||||
|
menu.findItem(R.id.menu_edit)?.isVisible = false
|
||||||
|
}
|
||||||
|
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||||
|
menu.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
if (mEntryIsHistory && !mDatabaseReadOnly) {
|
val gotoUrl = menu.findItem(R.id.menu_goto_url)
|
||||||
inflater.inflate(R.menu.entry_history, menu)
|
gotoUrl?.apply {
|
||||||
}
|
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
|
||||||
|
// so mEntry may not be set
|
||||||
// Show education views
|
if (mEntry == null) {
|
||||||
Handler(Looper.getMainLooper()).post {
|
isVisible = false
|
||||||
performedNextEducation(menu)
|
} else {
|
||||||
|
if (mEntry?.url?.isEmpty() != false) {
|
||||||
|
// disable button if url is not available
|
||||||
|
isVisible = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show education views
|
||||||
|
Handler(Looper.getMainLooper()).post { performedNextEducation(EntryActivityEducation(this), menu) }
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
||||||
if (mEntryIsHistory || mDatabaseReadOnly) {
|
menu: Menu) {
|
||||||
menu?.findItem(R.id.menu_save_database)?.isVisible = false
|
val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView()
|
||||||
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
|
||||||
menu?.findItem(R.id.menu_edit)?.isVisible = false
|
|
||||||
}
|
|
||||||
if (!mMergeDataAllowed) {
|
|
||||||
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
|
||||||
}
|
|
||||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
|
||||||
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
|
||||||
menu?.findItem(R.id.menu_reload_database)?.isVisible = false
|
|
||||||
}
|
|
||||||
applyToolbarColors()
|
|
||||||
return super.onPrepareOptionsMenu(menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun performedNextEducation(menu: Menu) {
|
|
||||||
val entryFragment = supportFragmentManager.findFragmentByTag(ENTRY_FRAGMENT_TAG)
|
|
||||||
as? EntryFragment?
|
|
||||||
val entryFieldCopyView: View? = entryFragment?.firstEntryFieldCopyView()
|
|
||||||
val entryCopyEducationPerformed = entryFieldCopyView != null
|
val entryCopyEducationPerformed = entryFieldCopyView != null
|
||||||
&& mEntryActivityEducation.checkAndPerformedEntryCopyEducation(
|
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||||
entryFieldCopyView,
|
entryFieldCopyView,
|
||||||
{
|
{
|
||||||
entryFragment.launchEntryCopyEducationAction()
|
val appNameString = getString(R.string.app_name)
|
||||||
},
|
clipboardHelper?.timeoutCopyToClipboard(appNameString,
|
||||||
{
|
getString(R.string.copy_field, appNameString))
|
||||||
performedNextEducation(menu)
|
},
|
||||||
})
|
{
|
||||||
|
performedNextEducation(entryActivityEducation, menu)
|
||||||
|
})
|
||||||
|
|
||||||
if (!entryCopyEducationPerformed) {
|
if (!entryCopyEducationPerformed) {
|
||||||
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
|
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
|
||||||
// entryEditEducationPerformed
|
// entryEditEducationPerformed
|
||||||
menuEditView != null && mEntryActivityEducation.checkAndPerformedEntryEditEducation(
|
menuEditView != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||||
menuEditView,
|
menuEditView,
|
||||||
{
|
{
|
||||||
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(menu)
|
performedNextEducation(entryActivityEducation, menu)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -468,52 +460,65 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
|
R.id.menu_contribute -> {
|
||||||
|
MenuUtil.onContributionItemSelected(this)
|
||||||
|
return true
|
||||||
|
}
|
||||||
R.id.menu_edit -> {
|
R.id.menu_edit -> {
|
||||||
mDatabase?.let { database ->
|
mEntry?.let {
|
||||||
mMainEntryId?.let { entryId ->
|
EntryEditActivity.launch(this@EntryActivity, it)
|
||||||
EntryEditActivity.launchToUpdate(
|
|
||||||
this,
|
|
||||||
database,
|
|
||||||
entryId,
|
|
||||||
mEntryActivityResultLauncher
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
R.id.menu_goto_url -> {
|
||||||
|
var url: String = mEntry?.url ?: ""
|
||||||
|
|
||||||
|
// Default http:// if no protocol specified
|
||||||
|
if (!url.contains("://")) {
|
||||||
|
url = "http://$url"
|
||||||
|
}
|
||||||
|
|
||||||
|
UriUtil.gotoUrl(this, url)
|
||||||
|
return true
|
||||||
|
}
|
||||||
R.id.menu_restore_entry_history -> {
|
R.id.menu_restore_entry_history -> {
|
||||||
mMainEntryId?.let { mainEntryId ->
|
mEntryLastVersion?.let { mainEntry ->
|
||||||
restoreEntryHistory(
|
mProgressDatabaseTaskProvider?.startDatabaseRestoreEntryHistory(
|
||||||
mainEntryId,
|
mainEntry,
|
||||||
mHistoryPosition)
|
mEntryHistoryPosition,
|
||||||
|
!mReadOnly && mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.menu_delete_entry_history -> {
|
R.id.menu_delete_entry_history -> {
|
||||||
mMainEntryId?.let { mainEntryId ->
|
mEntryLastVersion?.let { mainEntry ->
|
||||||
deleteEntryHistory(
|
mProgressDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(
|
||||||
mainEntryId,
|
mainEntry,
|
||||||
mHistoryPosition)
|
mEntryHistoryPosition,
|
||||||
|
!mReadOnly && mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.menu_save_database -> {
|
R.id.menu_save_database -> {
|
||||||
saveDatabase()
|
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
|
||||||
}
|
|
||||||
R.id.menu_merge_database -> {
|
|
||||||
mergeDatabase()
|
|
||||||
}
|
}
|
||||||
R.id.menu_reload_database -> {
|
R.id.menu_reload_database -> {
|
||||||
reloadDatabase()
|
mProgressDatabaseTaskProvider?.startDatabaseReload(false)
|
||||||
}
|
}
|
||||||
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
|
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
|
||||||
|
outState.putBoolean(KEY_FIRST_LAUNCH_ACTIVITY, mFirstLaunchOfActivity)
|
||||||
|
}
|
||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
// Transit data in previous Activity after an update
|
// Transit data in previous Activity after an update
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
|
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry)
|
||||||
setResult(Activity.RESULT_OK, this)
|
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, this)
|
||||||
}
|
}
|
||||||
super.finish()
|
super.finish()
|
||||||
}
|
}
|
||||||
@@ -521,42 +526,19 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
companion object {
|
companion object {
|
||||||
private val TAG = EntryActivity::class.java.name
|
private val TAG = EntryActivity::class.java.name
|
||||||
|
|
||||||
|
private const val KEY_FIRST_LAUNCH_ACTIVITY = "KEY_FIRST_LAUNCH_ACTIVITY"
|
||||||
|
|
||||||
const val KEY_ENTRY = "KEY_ENTRY"
|
const val KEY_ENTRY = "KEY_ENTRY"
|
||||||
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
|
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
|
||||||
|
|
||||||
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
|
fun launch(activity: Activity, entry: Entry, readOnly: Boolean, historyPosition: Int? = null) {
|
||||||
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
/**
|
val intent = Intent(activity, EntryActivity::class.java)
|
||||||
* Open standard Entry activity
|
intent.putExtra(KEY_ENTRY, entry.nodeId)
|
||||||
*/
|
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
||||||
fun launch(activity: Activity,
|
if (historyPosition != null)
|
||||||
database: ContextualDatabase,
|
|
||||||
entryId: NodeId<UUID>,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
|
||||||
if (database.loaded) {
|
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
|
||||||
val intent = Intent(activity, EntryActivity::class.java)
|
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
|
||||||
activityResultLauncher.launch(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open history Entry activity
|
|
||||||
*/
|
|
||||||
fun launch(activity: Activity,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
entryId: NodeId<UUID>,
|
|
||||||
historyPosition: Int,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
|
||||||
if (database.loaded) {
|
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
|
||||||
val intent = Intent(activity, EntryActivity::class.java)
|
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
|
||||||
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
||||||
activityResultLauncher.launch(intent)
|
activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
|
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity to search or select entry in database,
|
||||||
|
* Commonly used with Magikeyboard
|
||||||
|
*/
|
||||||
|
class EntrySelectionLauncherActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
|
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 -> {
|
||||||
|
// Retrieve OTP
|
||||||
|
intent.dataString?.let { extra ->
|
||||||
|
if (OtpEntryFields.isOTPUri(extra))
|
||||||
|
otpString = extra
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Build domain search param
|
||||||
|
val searchInfo = SearchInfo().apply {
|
||||||
|
this.webDomain = sharedWebDomain
|
||||||
|
this.otpString = otpString
|
||||||
|
}
|
||||||
|
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
|
searchInfo.webDomain = concreteWebDomain
|
||||||
|
launch(searchInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launch(searchInfo: SearchInfo) {
|
||||||
|
|
||||||
|
if (!searchInfo.containsOnlyNullValues()) {
|
||||||
|
// Setting to integrate Magikeyboard
|
||||||
|
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
|
||||||
|
|
||||||
|
// If database is open
|
||||||
|
val database = Database.getInstance()
|
||||||
|
val readOnly = database.isReadOnly
|
||||||
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
|
database,
|
||||||
|
searchInfo,
|
||||||
|
{ items ->
|
||||||
|
// Items found
|
||||||
|
if (searchInfo.otpString != null) {
|
||||||
|
if (!readOnly) {
|
||||||
|
GroupActivity.launchForSaveResult(this,
|
||||||
|
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,
|
||||||
|
readOnly,
|
||||||
|
searchInfo,
|
||||||
|
true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
GroupActivity.launchForSearchResult(this,
|
||||||
|
readOnly,
|
||||||
|
searchInfo,
|
||||||
|
true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
if (searchInfo.otpString != null) {
|
||||||
|
if (!readOnly) {
|
||||||
|
GroupActivity.launchForSaveResult(this,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(applicationContext,
|
||||||
|
R.string.autofill_read_only_save,
|
||||||
|
Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
} else if (readOnly || searchShareForMagikeyboard) {
|
||||||
|
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||||
|
readOnly,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
|
} else {
|
||||||
|
GroupActivity.launchForSaveResult(this,
|
||||||
|
searchInfo,
|
||||||
|
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 {
|
||||||
|
FileDatabaseSelectActivity.launchForSearchResult(this,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
||||||
|
entry: EntryInfo,
|
||||||
|
intent: Intent,
|
||||||
|
toast: Boolean = true) {
|
||||||
|
// Populate Magikeyboard with entry
|
||||||
|
MagikIME.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
||||||
|
// Consume the selection mode
|
||||||
|
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||||
|
activity.moveTaskToBack(true)
|
||||||
|
}
|
||||||
@@ -31,33 +31,28 @@ import android.util.Log
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.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
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
|
||||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
@@ -65,31 +60,21 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
|||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
|
|
||||||
import com.kunzisoft.keepass.utils.DexUtil
|
|
||||||
import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||||
SetMainCredentialDialogFragment.AssignMainCredentialDialogListener {
|
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
||||||
|
|
||||||
// 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
|
||||||
|
|
||||||
private val databaseFilesViewModel: DatabaseFilesViewModel by viewModels()
|
private val databaseFilesViewModel: DatabaseFilesViewModel by viewModels()
|
||||||
|
|
||||||
private val mFileDatabaseSelectActivityEducation = FileDatabaseSelectActivityEducation(this)
|
|
||||||
|
|
||||||
// Adapter to manage database history list
|
// Adapter to manage database history list
|
||||||
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
||||||
|
|
||||||
@@ -97,20 +82,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
private var mDatabaseFileUri: Uri? = null
|
private var mDatabaseFileUri: Uri? = null
|
||||||
|
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
|
|
||||||
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||||
this.buildActivityResultLauncher()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
// Enabling/disabling MagikeyboardService is normally done by DexModeReceiver, but this
|
|
||||||
// additional check will allow the keyboard to be reenabled more easily if the app crashes
|
|
||||||
// or is force quit within DeX mode and then the user leaves DeX mode. Without this, the
|
|
||||||
// user would need to enter and exit DeX mode once to reenable the service.
|
|
||||||
MagikeyboardUtil.setEnabled(this, !DexUtil.isDexMode(resources.configuration))
|
|
||||||
|
|
||||||
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
|
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||||
|
|
||||||
setContentView(R.layout.activity_file_selection)
|
setContentView(R.layout.activity_file_selection)
|
||||||
@@ -120,33 +98,19 @@ 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() }
|
||||||
|
|
||||||
// Open database button
|
// Open database button
|
||||||
mExternalFileHelper = ExternalFileHelper(this)
|
mSelectFileHelper = SelectFileHelper(this)
|
||||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
||||||
uri?.let {
|
openDatabaseButtonView?.apply {
|
||||||
launchPasswordActivityWithPath(uri)
|
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
||||||
|
setOnClickListener(it)
|
||||||
|
setOnLongClickListener(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
|
|
||||||
mDatabaseFileUri = databaseFileCreatedUri
|
|
||||||
if (mDatabaseFileUri != null) {
|
|
||||||
SetMainCredentialDialogFragment.getInstance(true)
|
|
||||||
.show(supportFragmentManager, "passwordDialog")
|
|
||||||
} else {
|
|
||||||
val error = getString(R.string.error_create_database)
|
|
||||||
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
|
||||||
Log.e(TAG, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
openDatabaseButtonView = findViewById(R.id.open_database_button)
|
|
||||||
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
|
|
||||||
|
|
||||||
// History list
|
// History list
|
||||||
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
||||||
@@ -161,13 +125,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
||||||
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
||||||
launchPasswordActivity(
|
launchPasswordActivity(
|
||||||
databaseFileUri,
|
databaseFileUri,
|
||||||
fileDatabaseHistoryEntityToOpen.keyFileUri,
|
fileDatabaseHistoryEntityToOpen.keyFileUri
|
||||||
fileDatabaseHistoryEntityToOpen.hardwareKey
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
||||||
|
// Remove from app database
|
||||||
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
|
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -177,44 +141,50 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
|
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
|
||||||
|
|
||||||
// Load default database the first time
|
// Load default database if not an orientation change
|
||||||
databaseFilesViewModel.doForDefaultDatabase { databaseFileUri ->
|
if (!(savedInstanceState != null
|
||||||
launchPasswordActivityWithPath(databaseFileUri)
|
&& savedInstanceState.containsKey(EXTRA_STAY)
|
||||||
|
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
|
||||||
|
val databasePath = PreferencesUtil.getDefaultDatabasePath(this)
|
||||||
|
|
||||||
|
UriUtil.parse(databasePath)?.let { databaseFileUri ->
|
||||||
|
launchPasswordActivityWithPath(databaseFileUri)
|
||||||
|
} ?: run {
|
||||||
|
Log.i(TAG, "No default database to prepare")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the database URI provided by file manager after an orientation change
|
// Retrieve the database URI provided by file manager after an orientation change
|
||||||
if (savedInstanceState != null
|
if (savedInstanceState != null
|
||||||
&& savedInstanceState.containsKey(EXTRA_DATABASE_URI)) {
|
&& savedInstanceState.containsKey(EXTRA_DATABASE_URI)) {
|
||||||
mDatabaseFileUri = savedInstanceState.getParcelableCompat(EXTRA_DATABASE_URI)
|
mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observe list of databases
|
// Observe list of databases
|
||||||
databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles ->
|
databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles ->
|
||||||
try {
|
when (databaseFiles.databaseFileAction) {
|
||||||
when (databaseFiles.databaseFileAction) {
|
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
|
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
|
||||||
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
|
}
|
||||||
|
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
|
||||||
|
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
|
||||||
|
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
|
||||||
}
|
}
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
|
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
||||||
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
|
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity))
|
||||||
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
|
}
|
||||||
}
|
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
|
||||||
}
|
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
|
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
|
||||||
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
|
}
|
||||||
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
|
}
|
||||||
}
|
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
|
||||||
}
|
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
|
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
|
||||||
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
|
|
||||||
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
databaseFilesViewModel.consumeAction()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to observe database action", e)
|
|
||||||
}
|
}
|
||||||
|
databaseFilesViewModel.consumeAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observe default database
|
// Observe default database
|
||||||
@@ -222,63 +192,46 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
// Retrieve settings for default database
|
// Retrieve settings for default database
|
||||||
mAdapterDatabaseHistory?.setDefaultDatabase(it)
|
mAdapterDatabaseHistory?.setDefaultDatabase(it)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
// Attach the dialog thread to this activity
|
||||||
super.onDatabaseRetrieved(database)
|
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
|
||||||
if (database != null) {
|
onActionFinish = { actionTask, result ->
|
||||||
launchGroupActivityIfLoaded(database)
|
when (actionTask) {
|
||||||
}
|
ACTION_DATABASE_CREATE_TASK -> {
|
||||||
}
|
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
|
||||||
|
val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential()
|
||||||
override fun onDatabaseActionFinished(
|
databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri)
|
||||||
database: ContextualDatabase,
|
}
|
||||||
actionTask: String,
|
}
|
||||||
result: ActionRunnable.Result
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
) {
|
val database = Database.getInstance()
|
||||||
super.onDatabaseActionFinished(database, actionTask, result)
|
if (result.isSuccess
|
||||||
|
&& database.loaded) {
|
||||||
if (result.isSuccess) {
|
launchGroupActivity(database)
|
||||||
// Update list
|
} else {
|
||||||
when (actionTask) {
|
var resultError = ""
|
||||||
ACTION_DATABASE_CREATE_TASK,
|
val resultMessage = result.message
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
// Show error message
|
||||||
result.data?.getParcelableCompat<Uri>(DATABASE_URI_KEY)?.let { databaseUri ->
|
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||||
val mainCredential =
|
resultError = "$resultError $resultMessage"
|
||||||
result.data?.getParcelableCompat(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY)
|
}
|
||||||
?: MainCredential()
|
Log.e(TAG, resultError)
|
||||||
databaseFilesViewModel.addDatabaseFile(
|
Snackbar.make(coordinatorLayout,
|
||||||
databaseUri,
|
resultError,
|
||||||
mainCredential.keyFileUri,
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
mainCredential.hardwareKey
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Launch activity
|
|
||||||
when (actionTask) {
|
|
||||||
ACTION_DATABASE_CREATE_TASK -> {
|
|
||||||
GroupActivity.launch(
|
|
||||||
this@FileDatabaseSelectActivity,
|
|
||||||
database,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
|
||||||
launchGroupActivityIfLoaded(database)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
coordinatorLayout.showActionErrorIfNeeded(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new file by calling the content provider
|
* Create a new file by calling the content provider
|
||||||
*/
|
*/
|
||||||
private fun createNewFile() {
|
private fun createNewFile() {
|
||||||
mExternalFileHelper?.createDocument(
|
createDocument(this, getString(R.string.database_file_name_default) +
|
||||||
getString(R.string.database_file_name_default) +
|
getString(R.string.database_file_extension_default), "application/x-keepass")
|
||||||
getString(R.string.database_file_extension_default))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fileNoFoundAction(e: FileNotFoundException) {
|
private fun fileNoFoundAction(e: FileNotFoundException) {
|
||||||
@@ -287,34 +240,38 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
|
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
||||||
MainCredentialActivity.launch(this,
|
PasswordActivity.launch(this,
|
||||||
databaseUri,
|
databaseUri,
|
||||||
keyFile,
|
keyFile,
|
||||||
hardwareKey,
|
|
||||||
{ exception ->
|
{ exception ->
|
||||||
fileNoFoundAction(exception)
|
fileNoFoundAction(exception)
|
||||||
},
|
},
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() },
|
{ onLaunchActivitySpecialMode() })
|
||||||
mCredentialActivityResultLauncher)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
private fun launchGroupActivity(database: Database) {
|
||||||
if (database.loaded) {
|
GroupActivity.launch(this,
|
||||||
GroupActivity.launch(this,
|
database.isReadOnly,
|
||||||
database,
|
|
||||||
{ onValidateSpecialMode() },
|
{ onValidateSpecialMode() },
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() },
|
{ onLaunchActivitySpecialMode() })
|
||||||
mCredentialActivityResultLauncher)
|
}
|
||||||
}
|
|
||||||
|
override fun onValidateSpecialMode() {
|
||||||
|
super.onValidateSpecialMode()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancelSpecialMode() {
|
||||||
|
super.onCancelSpecialMode()
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||||
launchPasswordActivity(databaseUri, null, null)
|
launchPasswordActivity(databaseUri, null)
|
||||||
// Delete flickering for kitkat <=
|
// Delete flickering for kitkat <=
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||||
overridePendingTransition(0, 0)
|
overridePendingTransition(0, 0)
|
||||||
}
|
}
|
||||||
@@ -322,13 +279,16 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
// Define special title
|
|
||||||
specialTitle?.isVisible = this.isContributingUser()
|
|
||||||
|
|
||||||
// Show open and create button or special mode
|
// Show open and create button or special mode
|
||||||
when (mSpecialMode) {
|
when (mSpecialMode) {
|
||||||
SpecialMode.DEFAULT -> {
|
SpecialMode.DEFAULT -> {
|
||||||
createDatabaseButtonView?.visibility = View.VISIBLE
|
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
||||||
|
// There is an activity which can handle this intent.
|
||||||
|
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||||
|
} else{
|
||||||
|
// No Activity found that can handle this intent.
|
||||||
|
createDatabaseButtonView?.visibility = View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Disable create button if in selection mode or request for autofill
|
// Disable create button if in selection mode or request for autofill
|
||||||
@@ -336,29 +296,48 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabase?.let { database ->
|
val database = Database.getInstance()
|
||||||
launchGroupActivityIfLoaded(database)
|
if (database.loaded) {
|
||||||
}
|
launchGroupActivity(database)
|
||||||
|
|
||||||
// Show recent files if allowed
|
|
||||||
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
|
|
||||||
databaseFilesViewModel.loadListOfDatabases()
|
|
||||||
} else {
|
} else {
|
||||||
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
// Construct adapter with listeners
|
||||||
|
if (PreferencesUtil.showRecentFiles(this)) {
|
||||||
|
databaseFilesViewModel.loadListOfDatabases()
|
||||||
|
} else {
|
||||||
|
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
||||||
|
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register progress task
|
||||||
|
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
// Unregister progress task
|
||||||
|
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
|
// only to keep the current activity
|
||||||
|
outState.putBoolean(EXTRA_STAY, true)
|
||||||
// to retrieve the URI of a created database after an orientation change
|
// to retrieve the URI of a created database after an orientation change
|
||||||
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
|
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mDatabaseFileUri?.let { databaseUri ->
|
mDatabaseFileUri?.let { databaseUri ->
|
||||||
|
|
||||||
// Create the new database
|
// Create the new database
|
||||||
createDatabase(databaseUri, mainCredential)
|
mProgressDatabaseTaskProvider?.startDatabaseCreate(
|
||||||
|
databaseUri,
|
||||||
|
mainCredential
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val error = getString(R.string.error_create_database_file)
|
val error = getString(R.string.error_create_database_file)
|
||||||
@@ -369,53 +348,79 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
|
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||||
|
if (uri != null) {
|
||||||
|
launchPasswordActivityWithPath(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the created URI from the file manager
|
||||||
|
onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
|
||||||
|
mDatabaseFileUri = databaseFileCreatedUri
|
||||||
|
if (mDatabaseFileUri != null) {
|
||||||
|
AssignMasterKeyDialogFragment.getInstance(true)
|
||||||
|
.show(supportFragmentManager, "passwordDialog")
|
||||||
|
} else {
|
||||||
|
val error = getString(R.string.error_create_database)
|
||||||
|
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
Log.e(TAG, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
if (mSpecialMode == SpecialMode.DEFAULT) {
|
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||||
MenuUtil.defaultMenuInflater(this, menuInflater, menu)
|
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
||||||
performedNextEducation()
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performedNextEducation() {
|
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
||||||
// If no recent files
|
// If no recent files
|
||||||
val createDatabaseEducationPerformed =
|
val createDatabaseEducationPerformed =
|
||||||
createDatabaseButtonView != null
|
createDatabaseButtonView != null
|
||||||
&& createDatabaseButtonView!!.visibility == View.VISIBLE
|
&& createDatabaseButtonView!!.visibility == View.VISIBLE
|
||||||
&& mFileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
&& mAdapterDatabaseHistory != null
|
||||||
|
&& mAdapterDatabaseHistory!!.itemCount == 0
|
||||||
|
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
||||||
createDatabaseButtonView!!,
|
createDatabaseButtonView!!,
|
||||||
{
|
{
|
||||||
createNewFile()
|
createNewFile()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// But if the user cancel, it can also select a database
|
// But if the user cancel, it can also select a database
|
||||||
performedNextEducation()
|
performedNextEducation(fileDatabaseSelectActivityEducation)
|
||||||
})
|
})
|
||||||
if (!createDatabaseEducationPerformed) {
|
if (!createDatabaseEducationPerformed) {
|
||||||
// selectDatabaseEducationPerformed
|
// selectDatabaseEducationPerformed
|
||||||
openDatabaseButtonView != null
|
openDatabaseButtonView != null
|
||||||
&& mFileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
||||||
openDatabaseButtonView!!,
|
openDatabaseButtonView!!,
|
||||||
{ tapTargetView ->
|
{tapTargetView ->
|
||||||
tapTargetView?.let {
|
tapTargetView?.let {
|
||||||
mExternalFileHelper?.openDocument()
|
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{}
|
||||||
|
)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
android.R.id.home -> this.openUrl(R.string.file_manager_explanation_url)
|
android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
|
||||||
}
|
}
|
||||||
MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
@@ -424,6 +429,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val TAG = "FileDbSelectActivity"
|
private const val TAG = "FileDbSelectActivity"
|
||||||
|
private const val EXTRA_STAY = "EXTRA_STAY"
|
||||||
private const val EXTRA_DATABASE_URI = "EXTRA_DATABASE_URI"
|
private const val EXTRA_DATABASE_URI = "EXTRA_DATABASE_URI"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -482,50 +488,25 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
fun launchForAutofillResult(activity: Activity,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo? = null) {
|
searchInfo: SearchInfo? = null) {
|
||||||
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(activity,
|
AutofillHelper.startActivityForAutofillResult(activity,
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||||
activityResultLauncher,
|
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Passkey Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
||||||
fun launchForPasskeySelectionResult(activity: Activity,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
searchInfo: SearchInfo? = null) {
|
|
||||||
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
|
||||||
activity,
|
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
|
||||||
activityResultLauncher,
|
|
||||||
searchInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Registration Launch
|
* Registration Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launchForRegistration(context: Context,
|
fun launchForRegistration(context: Context,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
registerInfo: RegisterInfo? = null) {
|
||||||
registerInfo: RegisterInfo? = null,
|
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
|
||||||
typeMode: TypeMode) {
|
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
registerInfo)
|
||||||
context,
|
|
||||||
activityResultLauncher,
|
|
||||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
|
||||||
registerInfo,
|
|
||||||
typeMode
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,41 +27,29 @@ import android.view.Menu
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.IconEditDialogFragment
|
|
||||||
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
|
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
import com.kunzisoft.keepass.view.updateLockPaddingStart
|
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
||||||
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Deferred
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
|
|
||||||
class IconPickerActivity : DatabaseLockActivity() {
|
class IconPickerActivity : LockingActivity() {
|
||||||
|
|
||||||
private lateinit var toolbar: Toolbar
|
private lateinit var toolbar: Toolbar
|
||||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
@@ -76,13 +64,17 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
private var mCustomIconsSelectionMode = false
|
private var mCustomIconsSelectionMode = false
|
||||||
private var mIconsSelected: List<IconImageCustom> = ArrayList()
|
private var mIconsSelected: List<IconImageCustom> = ArrayList()
|
||||||
|
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
setContentView(R.layout.activity_icon_picker)
|
setContentView(R.layout.activity_icon_picker)
|
||||||
|
|
||||||
|
mDatabase = Database.getInstance()
|
||||||
|
|
||||||
toolbar = findViewById(R.id.toolbar)
|
toolbar = findViewById(R.id.toolbar)
|
||||||
toolbar.title = " "
|
toolbar.title = " "
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
@@ -92,19 +84,25 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
|
|
||||||
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
|
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
|
||||||
|
|
||||||
mExternalFileHelper = ExternalFileHelper(this)
|
|
||||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
|
||||||
addCustomIcon(uri)
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadButton = findViewById(R.id.icon_picker_upload)
|
uploadButton = findViewById(R.id.icon_picker_upload)
|
||||||
|
if (mDatabase?.allowCustomIcons == true) {
|
||||||
|
uploadButton.setOnClickListener {
|
||||||
|
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
||||||
|
}
|
||||||
|
uploadButton.setOnLongClickListener {
|
||||||
|
mSelectFileHelper?.selectFileOnClickViewListener?.onLongClick(it)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uploadButton.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
lockView = findViewById(R.id.lock_button)
|
lockView = findViewById(R.id.lock_button)
|
||||||
lockView?.setOnClickListener {
|
lockView?.setOnClickListener {
|
||||||
lockAndExit()
|
lockAndExit()
|
||||||
}
|
}
|
||||||
|
|
||||||
intent?.getParcelableExtraCompat<IconImage>(EXTRA_ICON)?.let {
|
intent?.getParcelableExtra<IconImage>(EXTRA_ICON)?.let {
|
||||||
mIconImage = it
|
mIconImage = it
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,9 +118,14 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
), ICON_PICKER_FRAGMENT_TAG)
|
), ICON_PICKER_FRAGMENT_TAG)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mIconImage = savedInstanceState.getParcelableCompat(EXTRA_ICON) ?: mIconImage
|
mIconImage = savedInstanceState.getParcelable(EXTRA_ICON) ?: mIconImage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Focus view to reinitialize timeout
|
||||||
|
findViewById<ViewGroup>(R.id.icon_picker_container)?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
||||||
|
|
||||||
|
mSelectFileHelper = SelectFileHelper(this)
|
||||||
|
|
||||||
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
|
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
|
||||||
mIconImage.standard = iconStandard
|
mIconImage.standard = iconStandard
|
||||||
// Remove the custom icon if a standard one is selected
|
// Remove the custom icon if a standard one is selected
|
||||||
@@ -154,34 +157,6 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
uploadButton.isEnabled = true
|
uploadButton.isEnabled = true
|
||||||
}
|
}
|
||||||
iconPickerViewModel.customIconUpdated.observe(this) { iconCustomUpdated ->
|
|
||||||
if (iconCustomUpdated.error && !iconCustomUpdated.errorConsumed) {
|
|
||||||
Snackbar.make(coordinatorLayout, iconCustomUpdated.errorStringId, Snackbar.LENGTH_LONG).asError().show()
|
|
||||||
iconCustomUpdated.errorConsumed = true
|
|
||||||
}
|
|
||||||
iconCustomUpdated.iconCustom?.let {
|
|
||||||
mDatabase?.updateCustomIcon(it)
|
|
||||||
}
|
|
||||||
iconPickerViewModel.deselectAllCustomIcons()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun viewToInvalidateTimeout(): View? {
|
|
||||||
return findViewById<ViewGroup>(R.id.icon_picker_container)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun finishActivityIfReloadRequested(): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
|
|
||||||
if (database?.allowCustomIcons == true) {
|
|
||||||
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
|
|
||||||
} else {
|
|
||||||
uploadButton.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateIconsSelectedViews() {
|
private fun updateIconsSelectedViews() {
|
||||||
@@ -212,25 +187,16 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Padding if lock button visible
|
// Padding if lock button visible
|
||||||
toolbar.updateLockPaddingStart()
|
toolbar.updateLockPaddingLeft()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
menuInflater.inflate(R.menu.icon, menu)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
if (mCustomIconsSelectionMode) {
|
||||||
menu?.findItem(R.id.menu_edit)?.apply {
|
menuInflater.inflate(R.menu.icon, menu)
|
||||||
isEnabled = mIconsSelected.size == 1
|
|
||||||
isVisible = isEnabled
|
|
||||||
}
|
}
|
||||||
menu?.findItem(R.id.menu_delete)?.apply {
|
return true
|
||||||
isEnabled = mCustomIconsSelectionMode
|
|
||||||
isVisible = isEnabled
|
|
||||||
}
|
|
||||||
return super.onPrepareOptionsMenu(menu)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
@@ -239,20 +205,14 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
if (mCustomIconsSelectionMode) {
|
if (mCustomIconsSelectionMode) {
|
||||||
iconPickerViewModel.deselectAllCustomIcons()
|
iconPickerViewModel.deselectAllCustomIcons()
|
||||||
} else {
|
} else {
|
||||||
onDatabaseBackPressed()
|
onBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.menu_edit -> {
|
|
||||||
updateCustomIcon(mIconsSelected[0])
|
|
||||||
}
|
|
||||||
R.id.menu_delete -> {
|
R.id.menu_delete -> {
|
||||||
mIconsSelected.forEach { iconToRemove ->
|
mIconsSelected.forEach { iconToRemove ->
|
||||||
removeCustomIcon(iconToRemove)
|
removeCustomIcon(iconToRemove)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.menu_external_icon -> {
|
|
||||||
this.openUrl(R.string.external_icon_url)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
@@ -265,7 +225,7 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
// on Progress with thread
|
// on Progress with thread
|
||||||
val asyncResult: Deferred<IconPickerViewModel.IconCustomState?> = async {
|
val asyncResult: Deferred<IconPickerViewModel.IconCustomState?> = async {
|
||||||
val iconCustomState = IconPickerViewModel.IconCustomState(null, true, R.string.error_upload_file)
|
val iconCustomState = IconPickerViewModel.IconCustomState(null, true, R.string.error_upload_file)
|
||||||
iconToUploadUri?.getDocumentFile(this@IconPickerActivity)?.also { documentFile ->
|
UriUtil.getFileData(this@IconPickerActivity, iconToUploadUri)?.also { documentFile ->
|
||||||
if (documentFile.length() > MAX_ICON_SIZE) {
|
if (documentFile.length() > MAX_ICON_SIZE) {
|
||||||
iconCustomState.errorStringId = R.string.error_file_to_big
|
iconCustomState.errorStringId = R.string.error_file_to_big
|
||||||
} else {
|
} else {
|
||||||
@@ -309,11 +269,6 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateCustomIcon(iconImageCustom: IconImageCustom) {
|
|
||||||
IconEditDialogFragment.update(iconImageCustom)
|
|
||||||
.show(supportFragmentManager, IconEditDialogFragment.TAG_UPDATE_ICON)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun removeCustomIcon(iconImageCustom: IconImageCustom) {
|
private fun removeCustomIcon(iconImageCustom: IconImageCustom) {
|
||||||
uploadButton.isEnabled = false
|
uploadButton.isEnabled = false
|
||||||
iconPickerViewModel.deselectAllCustomIcons()
|
iconPickerViewModel.deselectAllCustomIcons()
|
||||||
@@ -323,42 +278,52 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||||
|
addCustomIcon(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setResult() {
|
private fun setResult() {
|
||||||
setResult(Activity.RESULT_OK, Intent().apply {
|
setResult(Activity.RESULT_OK, Intent().apply {
|
||||||
putExtra(EXTRA_ICON, mIconImage)
|
putExtra(EXTRA_ICON, mIconImage)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseBackPressed() {
|
override fun onBackPressed() {
|
||||||
setResult()
|
setResult()
|
||||||
super.onDatabaseBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG"
|
private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG"
|
||||||
|
|
||||||
|
private const val ICON_SELECTED_REQUEST = 15861
|
||||||
private const val EXTRA_ICON = "EXTRA_ICON"
|
private const val EXTRA_ICON = "EXTRA_ICON"
|
||||||
|
|
||||||
private const val MAX_ICON_SIZE = 5242880
|
private const val MAX_ICON_SIZE = 5242880
|
||||||
|
|
||||||
fun registerIconSelectionForResult(context: FragmentActivity,
|
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, listener: (icon: IconImage) -> Unit) {
|
||||||
listener: (icon: IconImage) -> Unit): ActivityResultLauncher<Intent> {
|
if (requestCode == ICON_SELECTED_REQUEST) {
|
||||||
return context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
listener.invoke(data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
|
||||||
listener.invoke(result.data?.getParcelableExtraCompat(EXTRA_ICON) ?: IconImage())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launch(context: FragmentActivity,
|
fun launch(context: Activity,
|
||||||
previousIcon: IconImage?,
|
previousIcon: IconImage?) {
|
||||||
resultLauncher: ActivityResultLauncher<Intent>) {
|
|
||||||
// Create an instance to return the picker icon
|
// Create an instance to return the picker icon
|
||||||
resultLauncher.launch(
|
context.startActivityForResult(
|
||||||
Intent(context, IconPickerActivity::class.java).apply {
|
Intent(context,
|
||||||
|
IconPickerActivity::class.java).apply {
|
||||||
if (previousIcon != null)
|
if (previousIcon != null)
|
||||||
putExtra(EXTRA_ICON, previousIcon)
|
putExtra(EXTRA_ICON, previousIcon)
|
||||||
}
|
},
|
||||||
)
|
ICON_SELECTED_REQUEST)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -32,20 +31,16 @@ import android.widget.ImageView
|
|||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import com.igreenwood.loupe.Loupe
|
import com.igreenwood.loupe.Loupe
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
|
||||||
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.tasks.BinaryDatabaseManager
|
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class ImageViewerActivity : DatabaseLockActivity() {
|
class ImageViewerActivity : LockingActivity() {
|
||||||
|
|
||||||
private var imageContainerView: ViewGroup? = null
|
private var mDatabase: Database? = null
|
||||||
private lateinit var imageView: ImageView
|
|
||||||
private lateinit var progressView: View
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -55,21 +50,49 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
|||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
toolbar.setOnTouchListener { _, _ ->
|
|
||||||
resetAppTimeout()
|
val imageContainerView: ViewGroup = findViewById(R.id.image_viewer_container)
|
||||||
false
|
val imageView: ImageView = findViewById(R.id.image_viewer_image)
|
||||||
|
val progressView: View = findViewById(R.id.image_viewer_progress)
|
||||||
|
|
||||||
|
// Approximately, to not OOM and allow a zoom
|
||||||
|
val mImagePreviewMaxWidth = max(
|
||||||
|
resources.displayMetrics.widthPixels * 2,
|
||||||
|
resources.displayMetrics.heightPixels * 2
|
||||||
|
)
|
||||||
|
|
||||||
|
mDatabase = Database.getInstance()
|
||||||
|
|
||||||
|
try {
|
||||||
|
progressView.visibility = View.VISIBLE
|
||||||
|
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
|
||||||
|
|
||||||
|
supportActionBar?.title = attachment.name
|
||||||
|
|
||||||
|
val size = attachment.binaryData.getSize()
|
||||||
|
supportActionBar?.subtitle = Formatter.formatFileSize(this, size)
|
||||||
|
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
BinaryDatabaseManager.loadBitmap(
|
||||||
|
database,
|
||||||
|
attachment.binaryData,
|
||||||
|
mImagePreviewMaxWidth
|
||||||
|
) { bitmapLoaded ->
|
||||||
|
if (bitmapLoaded == null) {
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
progressView.visibility = View.GONE
|
||||||
|
imageView.setImageBitmap(bitmapLoaded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: finish()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to view the binary", e)
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
imageContainerView = findViewById(R.id.image_viewer_container)
|
Loupe.create(imageView, imageContainerView) {
|
||||||
imageView = findViewById(R.id.image_viewer_image)
|
|
||||||
progressView = findViewById(R.id.image_viewer_progress)
|
|
||||||
|
|
||||||
Loupe.create(imageView, imageContainerView!!) {
|
|
||||||
onViewTouchedListener = View.OnTouchListener { _, _ ->
|
|
||||||
// to reset timeout when Loupe image view touched
|
|
||||||
resetAppTimeout()
|
|
||||||
false
|
|
||||||
}
|
|
||||||
onViewTranslateListener = object : Loupe.OnViewTranslateListener {
|
onViewTranslateListener = object : Loupe.OnViewTranslateListener {
|
||||||
|
|
||||||
override fun onStart(view: ImageView) {
|
override fun onStart(view: ImageView) {
|
||||||
@@ -92,54 +115,6 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun viewToInvalidateTimeout(): View? {
|
|
||||||
// Null to manually manage events
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun finishActivityIfReloadRequested(): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
|
|
||||||
try {
|
|
||||||
progressView.visibility = View.VISIBLE
|
|
||||||
intent.getParcelableExtraCompat<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
|
|
||||||
|
|
||||||
supportActionBar?.title = attachment.name
|
|
||||||
|
|
||||||
val size = attachment.binaryData.getSize()
|
|
||||||
supportActionBar?.subtitle = Formatter.formatFileSize(this, size)
|
|
||||||
|
|
||||||
// Approximately, to not OOM and allow a zoom
|
|
||||||
val mImagePreviewMaxWidth = max(
|
|
||||||
resources.displayMetrics.widthPixels * 2,
|
|
||||||
resources.displayMetrics.heightPixels * 2
|
|
||||||
)
|
|
||||||
|
|
||||||
database?.let { database ->
|
|
||||||
BinaryDatabaseManager.loadBitmap(
|
|
||||||
database,
|
|
||||||
attachment.binaryData,
|
|
||||||
mImagePreviewMaxWidth
|
|
||||||
) { bitmapLoaded ->
|
|
||||||
if (bitmapLoaded == null) {
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
progressView.visibility = View.GONE
|
|
||||||
imageView.setImageBitmap(bitmapLoaded)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: finish()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to view the binary", e)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
android.R.id.home -> finish()
|
android.R.id.home -> finish()
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
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.updateLockPaddingStart
|
|
||||||
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.updateLockPaddingStart()
|
|
||||||
}
|
|
||||||
|
|
||||||
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 -> {
|
|
||||||
onDatabaseBackPressed()
|
|
||||||
}
|
|
||||||
R.id.menu_generate -> {
|
|
||||||
keyGeneratorViewModel.requireKeyGeneration()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseBackPressed() {
|
|
||||||
setResult(Activity.RESULT_CANCELED, Intent())
|
|
||||||
super.onDatabaseBackPressed()
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity to select entry in database and populate it in Magikeyboard
|
||||||
|
*/
|
||||||
|
class MagikeyboardLauncherActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
val database = Database.getInstance()
|
||||||
|
val readOnly = database.isReadOnly
|
||||||
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
|
database,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
// Not called
|
||||||
|
// if items found directly returns before calling this activity
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Select if not found
|
||||||
|
GroupActivity.launchForKeyboardSelectionResult(this, readOnly)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Pass extra to get entry
|
||||||
|
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
finish()
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,924 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.*
|
||||||
|
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||||
|
import android.widget.*
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.fragment.app.commit
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
|
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||||
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
||||||
|
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||||
|
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_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.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||||
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
|
import com.kunzisoft.keepass.view.asError
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
|
open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
||||||
|
|
||||||
|
// Views
|
||||||
|
private var toolbar: Toolbar? = null
|
||||||
|
private var filenameView: TextView? = null
|
||||||
|
private var passwordView: EditText? = null
|
||||||
|
private var keyFileSelectionView: KeyFileSelectionView? = null
|
||||||
|
private var confirmButtonView: Button? = null
|
||||||
|
private var checkboxPasswordView: CompoundButton? = null
|
||||||
|
private var checkboxKeyFileView: CompoundButton? = null
|
||||||
|
private var infoContainerView: ViewGroup? = null
|
||||||
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
|
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
||||||
|
|
||||||
|
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||||
|
|
||||||
|
private var mDefaultDatabase: Boolean = false
|
||||||
|
private var mDatabaseFileUri: Uri? = null
|
||||||
|
private var mDatabaseKeyFileUri: Uri? = null
|
||||||
|
|
||||||
|
private var mRememberKeyFile: Boolean = false
|
||||||
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
|
|
||||||
|
private var mPermissionAsked = false
|
||||||
|
private var readOnly: Boolean = false
|
||||||
|
private var mForceReadOnly: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
infoContainerView?.visibility = if (value) {
|
||||||
|
readOnly = true
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||||
|
|
||||||
|
private var mAllowAutoOpenBiometricPrompt: Boolean = true
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_password)
|
||||||
|
|
||||||
|
toolbar = findViewById(R.id.toolbar)
|
||||||
|
toolbar?.title = getString(R.string.app_name)
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
|
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
||||||
|
filenameView = findViewById(R.id.filename)
|
||||||
|
passwordView = findViewById(R.id.password)
|
||||||
|
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
||||||
|
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
||||||
|
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
||||||
|
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||||
|
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
|
||||||
|
|
||||||
|
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
||||||
|
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||||
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
|
|
||||||
|
mSelectFileHelper = SelectFileHelper(this@PasswordActivity)
|
||||||
|
keyFileSelectionView?.apply {
|
||||||
|
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
||||||
|
setOnClickListener(it)
|
||||||
|
setOnLongClickListener(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
||||||
|
passwordView?.addTextChangedListener(object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||||
|
|
||||||
|
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||||
|
|
||||||
|
override fun afterTextChanged(editable: Editable) {
|
||||||
|
if (editable.toString().isNotEmpty() && checkboxPasswordView?.isChecked != true)
|
||||||
|
checkboxPasswordView?.isChecked = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// If is a view intent
|
||||||
|
getUriFromIntent(intent)
|
||||||
|
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
||||||
|
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
|
||||||
|
}
|
||||||
|
if (savedInstanceState?.containsKey(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) == true) {
|
||||||
|
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init Biometric elements
|
||||||
|
advancedUnlockFragment = supportFragmentManager
|
||||||
|
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
|
||||||
|
if (advancedUnlockFragment == null) {
|
||||||
|
advancedUnlockFragment = AdvancedUnlockFragment()
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
replace(R.id.fragment_advanced_unlock_container_view,
|
||||||
|
advancedUnlockFragment!!,
|
||||||
|
UNLOCK_FRAGMENT_TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen password checkbox to init advanced unlock and confirmation button
|
||||||
|
checkboxPasswordView?.setOnCheckedChangeListener { _, _ ->
|
||||||
|
advancedUnlockFragment?.checkUnlockAvailability()
|
||||||
|
enableOrNotTheConfirmationButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe if default database
|
||||||
|
databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
||||||
|
mDefaultDatabase = isDefaultDatabase
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe database file change
|
||||||
|
databaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
|
||||||
|
// Force read only if the file does not exists
|
||||||
|
mForceReadOnly = databaseFile?.let {
|
||||||
|
!it.databaseFileExists
|
||||||
|
} ?: true
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
|
// Post init uri with KeyFile only if needed
|
||||||
|
val keyFileUri =
|
||||||
|
if (mRememberKeyFile
|
||||||
|
&& (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
|
||||||
|
databaseFile?.keyFileUri
|
||||||
|
} else {
|
||||||
|
mDatabaseKeyFileUri
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define title
|
||||||
|
filenameView?.text = databaseFile?.databaseAlias ?: ""
|
||||||
|
|
||||||
|
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
|
||||||
|
onActionFinish = { actionTask, result ->
|
||||||
|
when (actionTask) {
|
||||||
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
|
// Recheck advanced unlock if error
|
||||||
|
advancedUnlockFragment?.initAdvancedUnlockMode()
|
||||||
|
|
||||||
|
if (result.isSuccess) {
|
||||||
|
mDatabaseKeyFileUri = null
|
||||||
|
clearCredentialsViews(true)
|
||||||
|
launchGroupActivity()
|
||||||
|
} else {
|
||||||
|
var resultError = ""
|
||||||
|
val resultException = result.exception
|
||||||
|
val resultMessage = result.message
|
||||||
|
|
||||||
|
if (resultException != null) {
|
||||||
|
resultError = resultException.getLocalizedMessage(resources)
|
||||||
|
|
||||||
|
when (resultException) {
|
||||||
|
is DuplicateUuidDatabaseException -> {
|
||||||
|
// Relaunch loading if we need to fix UUID
|
||||||
|
showLoadDatabaseDuplicateUuidMessage {
|
||||||
|
|
||||||
|
var databaseUri: Uri? = null
|
||||||
|
var mainCredential: MainCredential = MainCredential()
|
||||||
|
var readOnly = true
|
||||||
|
var cipherEntity: CipherDatabaseEntity? = null
|
||||||
|
|
||||||
|
result.data?.let { resultData ->
|
||||||
|
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
||||||
|
mainCredential = resultData.getParcelable(MAIN_CREDENTIAL_KEY) ?: mainCredential
|
||||||
|
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||||
|
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
databaseUri?.let { databaseFileUri ->
|
||||||
|
showProgressDialogAndLoadDatabase(
|
||||||
|
databaseFileUri,
|
||||||
|
mainCredential,
|
||||||
|
readOnly,
|
||||||
|
cipherEntity,
|
||||||
|
true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FileNotFoundDatabaseException -> {
|
||||||
|
// Remove this default database inaccessible
|
||||||
|
if (mDefaultDatabase) {
|
||||||
|
databaseFileViewModel.removeDefaultDatabase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||||
|
resultError = "$resultError $resultMessage"
|
||||||
|
}
|
||||||
|
Log.e(TAG, resultError)
|
||||||
|
Snackbar.make(coordinatorLayout,
|
||||||
|
resultError,
|
||||||
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUriFromIntent(intent: Intent?) {
|
||||||
|
// If is a view intent
|
||||||
|
val action = intent?.action
|
||||||
|
if (action != null
|
||||||
|
&& action == VIEW_INTENT) {
|
||||||
|
mDatabaseFileUri = intent.data
|
||||||
|
mDatabaseKeyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
|
||||||
|
} else {
|
||||||
|
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
|
||||||
|
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
|
||||||
|
}
|
||||||
|
mDatabaseFileUri?.let {
|
||||||
|
databaseFileViewModel.checkIfIsDefaultDatabase(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
getUriFromIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchGroupActivity() {
|
||||||
|
GroupActivity.launch(this,
|
||||||
|
readOnly,
|
||||||
|
{ onValidateSpecialMode() },
|
||||||
|
{ onCancelSpecialMode() },
|
||||||
|
{ onLaunchActivitySpecialMode() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onValidateSpecialMode() {
|
||||||
|
super.onValidateSpecialMode()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancelSpecialMode() {
|
||||||
|
super.onCancelSpecialMode()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun retrieveCredentialForEncryption(): String {
|
||||||
|
return passwordView?.text?.toString() ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun conditionToStoreCredential(): Boolean {
|
||||||
|
return checkboxPasswordView?.isChecked == true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCredentialEncrypted(databaseUri: Uri,
|
||||||
|
encryptedCredential: String,
|
||||||
|
ivSpec: String) {
|
||||||
|
// Load the database if password is registered with biometric
|
||||||
|
verifyCheckboxesAndLoadDatabase(
|
||||||
|
CipherDatabaseEntity(
|
||||||
|
databaseUri.toString(),
|
||||||
|
encryptedCredential,
|
||||||
|
ivSpec)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCredentialDecrypted(databaseUri: Uri,
|
||||||
|
decryptedCredential: String) {
|
||||||
|
// Load the database if password is retrieve from biometric
|
||||||
|
// Retrieve from biometric
|
||||||
|
verifyKeyFileCheckboxAndLoadDatabase(decryptedCredential)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val onEditorActionListener = object : TextView.OnEditorActionListener {
|
||||||
|
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
|
||||||
|
if (actionId == IME_ACTION_DONE) {
|
||||||
|
verifyCheckboxesAndLoadDatabase()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
if (Database.getInstance().loaded) {
|
||||||
|
launchGroupActivity()
|
||||||
|
} else {
|
||||||
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
|
|
||||||
|
// If the database isn't accessible make sure to clear the password field, if it
|
||||||
|
// was saved in the instance state
|
||||||
|
if (Database.getInstance().loaded) {
|
||||||
|
clearCredentialsViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
|
||||||
|
// Back to previous keyboard is setting activated
|
||||||
|
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this)) {
|
||||||
|
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow auto open prompt if lock become when UI visible
|
||||||
|
mAllowAutoOpenBiometricPrompt = if (LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
|
||||||
|
false
|
||||||
|
else
|
||||||
|
mAllowAutoOpenBiometricPrompt
|
||||||
|
mDatabaseFileUri?.let { databaseFileUri ->
|
||||||
|
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPermission()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||||
|
// Define Key File text
|
||||||
|
if (mRememberKeyFile) {
|
||||||
|
populateKeyFileTextView(keyFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define listener for validate button
|
||||||
|
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
||||||
|
|
||||||
|
// If Activity is launch with a password and want to open directly
|
||||||
|
val intent = intent
|
||||||
|
val password = intent.getStringExtra(KEY_PASSWORD)
|
||||||
|
// Consume the intent extra password
|
||||||
|
intent.removeExtra(KEY_PASSWORD)
|
||||||
|
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
||||||
|
if (password != null) {
|
||||||
|
populatePasswordTextView(password)
|
||||||
|
}
|
||||||
|
if (launchImmediately) {
|
||||||
|
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
||||||
|
} else {
|
||||||
|
// Init Biometric elements
|
||||||
|
advancedUnlockFragment?.loadDatabase(databaseFileUri,
|
||||||
|
mAllowAutoOpenBiometricPrompt
|
||||||
|
&& mProgressDatabaseTaskProvider?.isBinded() != true)
|
||||||
|
}
|
||||||
|
|
||||||
|
enableOrNotTheConfirmationButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enableOrNotTheConfirmationButton() {
|
||||||
|
// Enable or not the open button if setting is checked
|
||||||
|
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) {
|
||||||
|
checkboxPasswordView?.let {
|
||||||
|
confirmButtonView?.isEnabled = (checkboxPasswordView?.isChecked == true
|
||||||
|
|| checkboxKeyFileView?.isChecked == true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
confirmButtonView?.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
|
||||||
|
populatePasswordTextView(null)
|
||||||
|
if (clearKeyFile) {
|
||||||
|
populateKeyFileTextView(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populatePasswordTextView(text: String?) {
|
||||||
|
if (text == null || text.isEmpty()) {
|
||||||
|
passwordView?.setText("")
|
||||||
|
if (checkboxPasswordView?.isChecked == true)
|
||||||
|
checkboxPasswordView?.isChecked = false
|
||||||
|
} else {
|
||||||
|
passwordView?.setText(text)
|
||||||
|
if (checkboxPasswordView?.isChecked != true)
|
||||||
|
checkboxPasswordView?.isChecked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateKeyFileTextView(uri: Uri?) {
|
||||||
|
if (uri == null || uri.toString().isEmpty()) {
|
||||||
|
keyFileSelectionView?.uri = null
|
||||||
|
if (checkboxKeyFileView?.isChecked == true)
|
||||||
|
checkboxKeyFileView?.isChecked = false
|
||||||
|
} else {
|
||||||
|
keyFileSelectionView?.uri = uri
|
||||||
|
if (checkboxKeyFileView?.isChecked != true)
|
||||||
|
checkboxKeyFileView?.isChecked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
|
||||||
|
// Reinit locking activity UI variable
|
||||||
|
LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
||||||
|
mAllowAutoOpenBiometricPrompt = true
|
||||||
|
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
|
||||||
|
mDatabaseKeyFileUri?.let {
|
||||||
|
outState.putString(KEY_KEYFILE, it.toString())
|
||||||
|
}
|
||||||
|
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
||||||
|
outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||||
|
val password: String? = passwordView?.text?.toString()
|
||||||
|
val keyFile: Uri? = keyFileSelectionView?.uri
|
||||||
|
verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyCheckboxesAndLoadDatabase(password: String?,
|
||||||
|
keyFile: Uri?,
|
||||||
|
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||||
|
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
|
||||||
|
verifyKeyFileCheckbox(keyFile)
|
||||||
|
loadDatabase(mDatabaseFileUri, keyPassword, mDatabaseKeyFileUri, cipherDatabaseEntity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
|
||||||
|
val keyFile: Uri? = keyFileSelectionView?.uri
|
||||||
|
verifyKeyFileCheckbox(keyFile)
|
||||||
|
loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyKeyFileCheckbox(keyFile: Uri?) {
|
||||||
|
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadDatabase(databaseFileUri: Uri?,
|
||||||
|
password: String?,
|
||||||
|
keyFileUri: Uri?,
|
||||||
|
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||||
|
|
||||||
|
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
||||||
|
clearCredentialsViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readOnly && (
|
||||||
|
mSpecialMode == SpecialMode.SAVE
|
||||||
|
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||||
|
) {
|
||||||
|
Log.e(TAG, getString(R.string.autofill_read_only_save))
|
||||||
|
Snackbar.make(coordinatorLayout,
|
||||||
|
R.string.autofill_read_only_save,
|
||||||
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
} else {
|
||||||
|
databaseFileUri?.let { databaseUri ->
|
||||||
|
// Show the progress dialog and load the database
|
||||||
|
showProgressDialogAndLoadDatabase(
|
||||||
|
databaseUri,
|
||||||
|
MainCredential(password, keyFileUri),
|
||||||
|
readOnly,
|
||||||
|
cipherDatabaseEntity,
|
||||||
|
false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
|
||||||
|
mainCredential: MainCredential,
|
||||||
|
readOnly: Boolean,
|
||||||
|
cipherDatabaseEntity: CipherDatabaseEntity?,
|
||||||
|
fixDuplicateUUID: Boolean) {
|
||||||
|
mProgressDatabaseTaskProvider?.startDatabaseLoad(
|
||||||
|
databaseUri,
|
||||||
|
mainCredential,
|
||||||
|
readOnly,
|
||||||
|
cipherDatabaseEntity,
|
||||||
|
fixDuplicateUUID
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLoadDatabaseDuplicateUuidMessage(loadDatabaseWithFix: (() -> Unit)? = null) {
|
||||||
|
DuplicateUuidDialog().apply {
|
||||||
|
positiveAction = loadDatabaseWithFix
|
||||||
|
}.show(supportFragmentManager, "duplicateUUIDDialog")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
val inflater = menuInflater
|
||||||
|
// Read menu
|
||||||
|
inflater.inflate(R.menu.open_file, menu)
|
||||||
|
if (mForceReadOnly) {
|
||||||
|
menu.removeItem(R.id.menu_open_file_read_mode_key)
|
||||||
|
} else {
|
||||||
|
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||||
|
MenuUtil.defaultMenuInflater(inflater, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
|
launchEducation(menu)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
private fun checkPermission() {
|
||||||
|
if (Build.VERSION.SDK_INT in 23..28
|
||||||
|
&& !readOnly
|
||||||
|
&& !mPermissionAsked) {
|
||||||
|
mPermissionAsked = true
|
||||||
|
// Check self permission to show or not the dialog
|
||||||
|
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
val permissions = arrayOf(writePermission)
|
||||||
|
if (toolbar != null
|
||||||
|
&& ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
|
||||||
|
when (requestCode) {
|
||||||
|
WRITE_EXTERNAL_STORAGE_REQUEST -> {
|
||||||
|
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
|
||||||
|
Toast.makeText(this, R.string.read_only_warning, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// To fix multiple view education
|
||||||
|
private var performedEductionInProgress = false
|
||||||
|
private fun launchEducation(menu: Menu) {
|
||||||
|
if (!performedEductionInProgress) {
|
||||||
|
performedEductionInProgress = true
|
||||||
|
// Show education views
|
||||||
|
Handler(Looper.getMainLooper()).post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
|
||||||
|
menu: Menu) {
|
||||||
|
val educationToolbar = toolbar
|
||||||
|
val unlockEducationPerformed = educationToolbar != null
|
||||||
|
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
|
||||||
|
educationToolbar,
|
||||||
|
{
|
||||||
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
|
})
|
||||||
|
if (!unlockEducationPerformed) {
|
||||||
|
val readOnlyEducationPerformed =
|
||||||
|
educationToolbar?.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
|
||||||
|
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
|
||||||
|
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
menu.findItem(R.id.menu_open_file_read_mode_key)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to find read mode menu")
|
||||||
|
}
|
||||||
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
|
})
|
||||||
|
|
||||||
|
advancedUnlockFragment?.performEducation(passwordActivityEducation,
|
||||||
|
readOnlyEducationPerformed,
|
||||||
|
{
|
||||||
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun changeOpenFileReadIcon(togglePassword: MenuItem) {
|
||||||
|
if (readOnly) {
|
||||||
|
togglePassword.setTitle(R.string.menu_file_selection_read_only)
|
||||||
|
togglePassword.setIcon(R.drawable.ic_read_only_white_24dp)
|
||||||
|
} else {
|
||||||
|
togglePassword.setTitle(R.string.menu_open_file_read_and_write)
|
||||||
|
togglePassword.setIcon(R.drawable.ic_read_write_white_24dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> finish()
|
||||||
|
R.id.menu_open_file_read_mode_key -> {
|
||||||
|
readOnly = !readOnly
|
||||||
|
changeOpenFileReadIcon(item)
|
||||||
|
}
|
||||||
|
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(
|
||||||
|
requestCode: Int,
|
||||||
|
resultCode: Int,
|
||||||
|
data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
mAllowAutoOpenBiometricPrompt = false
|
||||||
|
|
||||||
|
// To get device credential unlock result
|
||||||
|
advancedUnlockFragment?.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
// To get entry in result
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyFileResult = false
|
||||||
|
mSelectFileHelper?.let {
|
||||||
|
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
||||||
|
) { uri ->
|
||||||
|
if (uri != null) {
|
||||||
|
mDatabaseKeyFileUri = uri
|
||||||
|
populateKeyFileTextView(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!keyFileResult) {
|
||||||
|
// this block if not a key file response
|
||||||
|
when (resultCode) {
|
||||||
|
LockingActivity.RESULT_EXIT_LOCK -> {
|
||||||
|
clearCredentialsViews()
|
||||||
|
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
|
||||||
|
}
|
||||||
|
Activity.RESULT_CANCELED -> {
|
||||||
|
clearCredentialsViews()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val TAG = PasswordActivity::class.java.name
|
||||||
|
|
||||||
|
private const val UNLOCK_FRAGMENT_TAG = "UNLOCK_FRAGMENT_TAG"
|
||||||
|
|
||||||
|
private const val KEY_FILENAME = "fileName"
|
||||||
|
private const val KEY_KEYFILE = "keyFile"
|
||||||
|
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
||||||
|
|
||||||
|
private const val KEY_PASSWORD = "password"
|
||||||
|
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||||
|
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
|
||||||
|
private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647
|
||||||
|
|
||||||
|
private const val ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT = "ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT"
|
||||||
|
|
||||||
|
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
||||||
|
intentBuildLauncher: (Intent) -> Unit) {
|
||||||
|
val intent = Intent(activity, PasswordActivity::class.java)
|
||||||
|
intent.putExtra(KEY_FILENAME, databaseFile)
|
||||||
|
if (keyFile != null)
|
||||||
|
intent.putExtra(KEY_KEYFILE, keyFile)
|
||||||
|
intentBuildLauncher.invoke(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Standard Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launch(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
|
activity.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Share Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForSearchResult(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Save Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForSaveResult(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForSaveModeResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Keyboard Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForKeyboardResult(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Autofill Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForAutofillResult(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
autofillComponent: AutofillComponent,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
|
AutofillHelper.startActivityForAutofillResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
autofillComponent,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Registration Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
fun launchForRegistration(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
registerInfo: RegisterInfo?) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
registerInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Global Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
fun launch(activity: Activity,
|
||||||
|
databaseUri: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
||||||
|
onCancelSpecialMode: () -> Unit,
|
||||||
|
onLaunchActivitySpecialMode: () -> Unit) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||||
|
{
|
||||||
|
PasswordActivity.launch(activity,
|
||||||
|
databaseUri, keyFile)
|
||||||
|
},
|
||||||
|
{ searchInfo -> // Search Action
|
||||||
|
PasswordActivity.launchForSearchResult(activity,
|
||||||
|
databaseUri, keyFile,
|
||||||
|
searchInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{ searchInfo -> // Save Action
|
||||||
|
PasswordActivity.launchForSaveResult(activity,
|
||||||
|
databaseUri, keyFile,
|
||||||
|
searchInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{ searchInfo -> // Keyboard Selection Action
|
||||||
|
PasswordActivity.launchForKeyboardResult(activity,
|
||||||
|
databaseUri, keyFile,
|
||||||
|
searchInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{ searchInfo, autofillComponent -> // Autofill Selection Action
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
PasswordActivity.launchForAutofillResult(activity,
|
||||||
|
databaseUri, keyFile,
|
||||||
|
autofillComponent,
|
||||||
|
searchInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
} else {
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ registerInfo -> // Registration Action
|
||||||
|
PasswordActivity.launchForRegistration(activity,
|
||||||
|
databaseUri, keyFile,
|
||||||
|
registerInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
fileNoFoundAction(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
/*
|
||||||
|
* 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.content.DialogInterface
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
|
|
||||||
|
class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private var mMasterPassword: String? = null
|
||||||
|
private var mKeyFile: Uri? = null
|
||||||
|
|
||||||
|
private var rootView: View? = null
|
||||||
|
|
||||||
|
private var passwordCheckBox: CompoundButton? = null
|
||||||
|
|
||||||
|
private var passwordTextInputLayout: TextInputLayout? = null
|
||||||
|
private var passwordView: TextView? = null
|
||||||
|
private var passwordRepeatTextInputLayout: TextInputLayout? = null
|
||||||
|
private var passwordRepeatView: TextView? = null
|
||||||
|
|
||||||
|
private var keyFileCheckBox: CompoundButton? = null
|
||||||
|
private var keyFileSelectionView: KeyFileSelectionView? = null
|
||||||
|
|
||||||
|
private var mListener: AssignPasswordDialogListener? = null
|
||||||
|
|
||||||
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
|
|
||||||
|
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
||||||
|
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
||||||
|
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
|
||||||
|
|
||||||
|
private val passwordTextWatcher = object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||||
|
|
||||||
|
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||||
|
|
||||||
|
override fun afterTextChanged(editable: Editable) {
|
||||||
|
passwordCheckBox?.isChecked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AssignPasswordDialogListener {
|
||||||
|
fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential)
|
||||||
|
fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttach(activity: Context) {
|
||||||
|
super.onAttach(activity)
|
||||||
|
try {
|
||||||
|
mListener = activity as AssignPasswordDialogListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(activity.toString()
|
||||||
|
+ " must implement " + AssignPasswordDialogListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
mEmptyPasswordConfirmationDialog?.dismiss()
|
||||||
|
mEmptyPasswordConfirmationDialog = null
|
||||||
|
mNoKeyConfirmationDialog?.dismiss()
|
||||||
|
mNoKeyConfirmationDialog = null
|
||||||
|
mEmptyKeyFileConfirmationDialog?.dismiss()
|
||||||
|
mEmptyKeyFileConfirmationDialog = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
|
||||||
|
var allowNoMasterKey = false
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(ALLOW_NO_MASTER_KEY_ARG))
|
||||||
|
allowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
val inflater = activity.layoutInflater
|
||||||
|
|
||||||
|
rootView = inflater.inflate(R.layout.fragment_set_password, null)
|
||||||
|
builder.setView(rootView)
|
||||||
|
// Add action buttons
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
|
||||||
|
rootView?.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
|
||||||
|
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
||||||
|
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
|
||||||
|
passwordView = rootView?.findViewById(R.id.pass_password)
|
||||||
|
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
|
||||||
|
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
|
||||||
|
|
||||||
|
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||||
|
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||||
|
|
||||||
|
mSelectFileHelper = SelectFileHelper(this)
|
||||||
|
keyFileSelectionView?.apply {
|
||||||
|
setOnClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
||||||
|
setOnLongClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog = builder.create()
|
||||||
|
|
||||||
|
if (passwordCheckBox != null && keyFileCheckBox!= null) {
|
||||||
|
dialog.setOnShowListener { dialog1 ->
|
||||||
|
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
|
||||||
|
positiveButton.setOnClickListener {
|
||||||
|
|
||||||
|
mMasterPassword = ""
|
||||||
|
mKeyFile = null
|
||||||
|
|
||||||
|
var error = verifyPassword() || verifyKeyFile()
|
||||||
|
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
||||||
|
error = true
|
||||||
|
if (allowNoMasterKey)
|
||||||
|
showNoKeyConfirmationDialog()
|
||||||
|
else {
|
||||||
|
passwordTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!error) {
|
||||||
|
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
|
||||||
|
negativeButton.setOnClickListener {
|
||||||
|
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun retrieveMainCredential(): MainCredential {
|
||||||
|
val masterPassword = if (passwordCheckBox!!.isChecked) mMasterPassword else null
|
||||||
|
val keyFile = if (keyFileCheckBox!!.isChecked) mKeyFile else null
|
||||||
|
return MainCredential(masterPassword, keyFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// To check checkboxes if a text is present
|
||||||
|
passwordView?.addTextChangedListener(passwordTextWatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
|
||||||
|
passwordView?.removeTextChangedListener(passwordTextWatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyPassword(): Boolean {
|
||||||
|
var error = false
|
||||||
|
if (passwordCheckBox != null
|
||||||
|
&& passwordCheckBox!!.isChecked
|
||||||
|
&& passwordView != null
|
||||||
|
&& passwordRepeatView != null) {
|
||||||
|
mMasterPassword = passwordView!!.text.toString()
|
||||||
|
val confPassword = passwordRepeatView!!.text.toString()
|
||||||
|
|
||||||
|
// Verify that passwords match
|
||||||
|
if (mMasterPassword != confPassword) {
|
||||||
|
error = true
|
||||||
|
// Passwords do not match
|
||||||
|
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mMasterPassword == null || mMasterPassword!!.isEmpty()) {
|
||||||
|
error = true
|
||||||
|
showEmptyPasswordConfirmationDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyKeyFile(): Boolean {
|
||||||
|
var error = false
|
||||||
|
if (keyFileCheckBox != null
|
||||||
|
&& keyFileCheckBox!!.isChecked) {
|
||||||
|
|
||||||
|
keyFileSelectionView?.uri?.let { uri ->
|
||||||
|
mKeyFile = uri
|
||||||
|
} ?: run {
|
||||||
|
error = true
|
||||||
|
keyFileSelectionView?.error = getString(R.string.error_nokeyfile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showEmptyPasswordConfirmationDialog() {
|
||||||
|
activity?.let {
|
||||||
|
val builder = AlertDialog.Builder(it)
|
||||||
|
builder.setMessage(R.string.warning_empty_password)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
if (!verifyKeyFile()) {
|
||||||
|
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||||
|
this@AssignMasterKeyDialogFragment.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
mEmptyPasswordConfirmationDialog = builder.create()
|
||||||
|
mEmptyPasswordConfirmationDialog?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showNoKeyConfirmationDialog() {
|
||||||
|
activity?.let {
|
||||||
|
val builder = AlertDialog.Builder(it)
|
||||||
|
builder.setMessage(R.string.warning_no_encryption_key)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||||
|
this@AssignMasterKeyDialogFragment.dismiss()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
mNoKeyConfirmationDialog = builder.create()
|
||||||
|
mNoKeyConfirmationDialog?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showEmptyKeyFileConfirmationDialog() {
|
||||||
|
activity?.let {
|
||||||
|
val builder = AlertDialog.Builder(it)
|
||||||
|
builder.setMessage(SpannableStringBuilder().apply {
|
||||||
|
append(getString(R.string.warning_empty_keyfile))
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_empty_keyfile_explanation))
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_sure_add_file))
|
||||||
|
})
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
keyFileCheckBox?.isChecked = false
|
||||||
|
keyFileSelectionView?.uri = null
|
||||||
|
}
|
||||||
|
mEmptyKeyFileConfirmationDialog = builder.create()
|
||||||
|
mEmptyKeyFileConfirmationDialog?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||||
|
uri?.let { pathUri ->
|
||||||
|
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||||
|
keyFileSelectionView?.error = null
|
||||||
|
keyFileCheckBox?.isChecked = true
|
||||||
|
keyFileSelectionView?.uri = pathUri
|
||||||
|
if (lengthFile <= 0L) {
|
||||||
|
showEmptyKeyFileConfirmationDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
|
||||||
|
|
||||||
|
fun getInstance(allowNoMasterKey: Boolean): AssignMasterKeyDialogFragment {
|
||||||
|
val fragment = AssignMasterKeyDialogFragment()
|
||||||
|
val args = Bundle()
|
||||||
|
args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey)
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.activities.dialogs
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.CompoundButton
|
|
||||||
import androidx.annotation.ColorInt
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import com.kunzisoft.androidclearchroma.view.ChromaColorView
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
|
||||||
|
|
||||||
class ColorPickerDialogFragment : DatabaseDialogFragment() {
|
|
||||||
|
|
||||||
private val mColorPickerViewModel: ColorPickerViewModel by activityViewModels()
|
|
||||||
|
|
||||||
private lateinit var enableSwitchView: CompoundButton
|
|
||||||
private lateinit var chromaColorView: ChromaColorView
|
|
||||||
|
|
||||||
private var mDefaultColor = Color.WHITE
|
|
||||||
private var mActivated = false
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
|
|
||||||
activity?.let { activity ->
|
|
||||||
val root = activity.layoutInflater.inflate(R.layout.fragment_color_picker, null)
|
|
||||||
enableSwitchView = root.findViewById(R.id.switch_element)
|
|
||||||
chromaColorView = root.findViewById(R.id.chroma_color_view)
|
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
if (savedInstanceState.containsKey(ARG_INITIAL_COLOR)) {
|
|
||||||
mDefaultColor = savedInstanceState.getInt(ARG_INITIAL_COLOR)
|
|
||||||
}
|
|
||||||
if (savedInstanceState.containsKey(ARG_ACTIVATED)) {
|
|
||||||
mActivated = savedInstanceState.getBoolean(ARG_ACTIVATED)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
arguments?.apply {
|
|
||||||
if (containsKey(ARG_INITIAL_COLOR)) {
|
|
||||||
mDefaultColor = getInt(ARG_INITIAL_COLOR)
|
|
||||||
}
|
|
||||||
if (containsKey(ARG_ACTIVATED)) {
|
|
||||||
mActivated = getBoolean(ARG_ACTIVATED)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enableSwitchView.isChecked = mActivated
|
|
||||||
chromaColorView.currentColor = mDefaultColor
|
|
||||||
|
|
||||||
chromaColorView.setOnColorChangedListener {
|
|
||||||
if (!enableSwitchView.isChecked)
|
|
||||||
enableSwitchView.isChecked = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
|
||||||
builder.setView(root)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
val color: Int? = if (enableSwitchView.isChecked)
|
|
||||||
chromaColorView.currentColor
|
|
||||||
else
|
|
||||||
null
|
|
||||||
mColorPickerViewModel.pickColor(color)
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.create()
|
|
||||||
}
|
|
||||||
return super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
outState.putInt(ARG_INITIAL_COLOR, chromaColorView.currentColor)
|
|
||||||
outState.putBoolean(ARG_ACTIVATED, mActivated)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val ARG_INITIAL_COLOR = "ARG_INITIAL_COLOR"
|
|
||||||
private const val ARG_ACTIVATED = "ARG_ACTIVATED"
|
|
||||||
|
|
||||||
fun newInstance(
|
|
||||||
@ColorInt initialColor: Int?,
|
|
||||||
): ColorPickerDialogFragment {
|
|
||||||
return ColorPickerDialogFragment().apply {
|
|
||||||
arguments = Bundle().apply {
|
|
||||||
putInt(ARG_INITIAL_COLOR, initialColor ?: Color.WHITE)
|
|
||||||
putBoolean(ARG_ACTIVATED, initialColor != null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,12 +23,12 @@ import android.app.Dialog
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
class DatabaseChangedDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
var actionDatabaseListener: ActionDatabaseChangedListener? = null
|
var actionDatabaseListener: ActionDatabaseChangedListener? = null
|
||||||
|
|
||||||
@@ -41,9 +41,8 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
|||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
|
|
||||||
val oldSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelableCompat(OLD_FILE_DATABASE_INFO)
|
val oldSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelable(OLD_FILE_DATABASE_INFO)
|
||||||
val newSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelableCompat(NEW_FILE_DATABASE_INFO)
|
val newSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelable(NEW_FILE_DATABASE_INFO)
|
||||||
val readOnlyDatabase: Boolean = arguments?.getBoolean(READ_ONLY_DATABASE) ?: true
|
|
||||||
|
|
||||||
if (oldSnapFileDatabaseInfo != null && newSnapFileDatabaseInfo != null) {
|
if (oldSnapFileDatabaseInfo != null && newSnapFileDatabaseInfo != null) {
|
||||||
// Use the Builder class for convenient dialog construction
|
// Use the Builder class for convenient dialog construction
|
||||||
@@ -55,13 +54,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
|||||||
stringBuilder.append("\n\n" +oldSnapFileDatabaseInfo.toString(activity)
|
stringBuilder.append("\n\n" +oldSnapFileDatabaseInfo.toString(activity)
|
||||||
+ "\n→\n" +
|
+ "\n→\n" +
|
||||||
newSnapFileDatabaseInfo.toString(activity) + "\n\n")
|
newSnapFileDatabaseInfo.toString(activity) + "\n\n")
|
||||||
stringBuilder.append(getString(
|
stringBuilder.append(getString(R.string.warning_database_info_changed_options))
|
||||||
if (readOnlyDatabase) {
|
|
||||||
R.string.warning_database_info_changed_options_read_only
|
|
||||||
} else {
|
|
||||||
R.string.warning_database_info_changed_options
|
|
||||||
}
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
stringBuilder.append(getString(R.string.warning_database_revoked))
|
stringBuilder.append(getString(R.string.warning_database_revoked))
|
||||||
}
|
}
|
||||||
@@ -84,18 +77,14 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
|||||||
const val DATABASE_CHANGED_DIALOG_TAG = "databaseChangedDialogFragment"
|
const val DATABASE_CHANGED_DIALOG_TAG = "databaseChangedDialogFragment"
|
||||||
private const val OLD_FILE_DATABASE_INFO = "OLD_FILE_DATABASE_INFO"
|
private const val OLD_FILE_DATABASE_INFO = "OLD_FILE_DATABASE_INFO"
|
||||||
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
|
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
|
||||||
private const val READ_ONLY_DATABASE = "READ_ONLY_DATABASE"
|
|
||||||
|
|
||||||
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
newSnapFileDatabaseInfo: SnapFileDatabaseInfo)
|
||||||
readOnly: Boolean
|
|
||||||
)
|
|
||||||
: DatabaseChangedDialogFragment {
|
: DatabaseChangedDialogFragment {
|
||||||
val fragment = DatabaseChangedDialogFragment()
|
val fragment = DatabaseChangedDialogFragment()
|
||||||
fragment.arguments = Bundle().apply {
|
fragment.arguments = Bundle().apply {
|
||||||
putParcelable(OLD_FILE_DATABASE_INFO, oldSnapFileDatabaseInfo)
|
putParcelable(OLD_FILE_DATABASE_INFO, oldSnapFileDatabaseInfo)
|
||||||
putParcelable(NEW_FILE_DATABASE_INFO, newSnapFileDatabaseInfo)
|
putParcelable(NEW_FILE_DATABASE_INFO, newSnapFileDatabaseInfo)
|
||||||
putBoolean(READ_ONLY_DATABASE, readOnly)
|
|
||||||
}
|
}
|
||||||
return fragment
|
return fragment
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.activities.dialogs
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.WindowManager.LayoutParams.FLAG_SECURE
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
|
||||||
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
|
||||||
|
|
||||||
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
|
||||||
|
|
||||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
|
||||||
private var mDatabase: ContextualDatabase? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
mDatabaseViewModel.database.observe(this) { database ->
|
|
||||||
this.mDatabase = database
|
|
||||||
resetAppTimeoutOnTouchOrFocus()
|
|
||||||
onDatabaseRetrieved(database)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.actionFinished.observe(this) { result ->
|
|
||||||
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
// Screenshot mode or hide views
|
|
||||||
context?.let {
|
|
||||||
if (PreferencesUtil.isScreenshotModeEnabled(it)) {
|
|
||||||
dialog?.window?.clearFlags(FLAG_SECURE)
|
|
||||||
} else {
|
|
||||||
dialog?.window?.setFlags(FLAG_SECURE, FLAG_SECURE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
@Deprecated(message = "")
|
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
|
||||||
super.onActivityCreated(savedInstanceState)
|
|
||||||
|
|
||||||
resetAppTimeoutOnTouchOrFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
|
||||||
// Can be overridden by a subclass
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
|
||||||
database: ContextualDatabase,
|
|
||||||
actionTask: String,
|
|
||||||
result: ActionRunnable.Result
|
|
||||||
) {
|
|
||||||
// Can be overridden by a subclass
|
|
||||||
}
|
|
||||||
|
|
||||||
fun resetAppTimeout() {
|
|
||||||
context?.let {
|
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(it,
|
|
||||||
mDatabase?.loaded ?: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun overrideTimeoutTouchAndFocusEvents(): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resetAppTimeoutOnTouchOrFocus() {
|
|
||||||
if (!overrideTimeoutTouchAndFocusEvents()) {
|
|
||||||
context?.let {
|
|
||||||
dialog?.window?.decorView?.resetAppTimeoutWhenViewTouchedOrFocused(
|
|
||||||
it,
|
|
||||||
mDatabase?.loaded
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.DatePickerDialog
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
|
||||||
|
class DatePickerFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private var mDefaultYear: Int = 2000
|
||||||
|
private var mDefaultMonth: Int = 1
|
||||||
|
private var mDefaultDay: Int = 1
|
||||||
|
|
||||||
|
private var mListener: DatePickerDialog.OnDateSetListener? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
try {
|
||||||
|
mListener = context as DatePickerDialog.OnDateSetListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
// Create a new instance of DatePickerDialog and return it
|
||||||
|
return context?.let {
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(DEFAULT_YEAR_BUNDLE_KEY))
|
||||||
|
mDefaultYear = getInt(DEFAULT_YEAR_BUNDLE_KEY)
|
||||||
|
if (containsKey(DEFAULT_MONTH_BUNDLE_KEY))
|
||||||
|
mDefaultMonth = getInt(DEFAULT_MONTH_BUNDLE_KEY)
|
||||||
|
if (containsKey(DEFAULT_DAY_BUNDLE_KEY))
|
||||||
|
mDefaultDay = getInt(DEFAULT_DAY_BUNDLE_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
DatePickerDialog(it, mListener, mDefaultYear, mDefaultMonth, mDefaultDay)
|
||||||
|
} ?: super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val DEFAULT_YEAR_BUNDLE_KEY = "DEFAULT_YEAR_BUNDLE_KEY"
|
||||||
|
private const val DEFAULT_MONTH_BUNDLE_KEY = "DEFAULT_MONTH_BUNDLE_KEY"
|
||||||
|
private const val DEFAULT_DAY_BUNDLE_KEY = "DEFAULT_DAY_BUNDLE_KEY"
|
||||||
|
|
||||||
|
fun getInstance(defaultYear: Int,
|
||||||
|
defaultMonth: Int,
|
||||||
|
defaultDay: Int): DatePickerFragment {
|
||||||
|
return DatePickerFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putInt(DEFAULT_YEAR_BUNDLE_KEY, defaultYear)
|
||||||
|
putInt(DEFAULT_MONTH_BUNDLE_KEY, defaultMonth)
|
||||||
|
putInt(DEFAULT_DAY_BUNDLE_KEY, defaultDay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,38 +20,61 @@
|
|||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.viewmodels.NodesViewModel
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
||||||
|
|
||||||
class DeleteNodesDialogFragment : DatabaseDialogFragment() {
|
open class DeleteNodesDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
private var mNodesToDelete: List<Node> = listOf()
|
private var mNodesToDelete: List<Node> = ArrayList()
|
||||||
private val mNodesViewModel: NodesViewModel by activityViewModels()
|
private var mListener: DeleteNodeListener? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
try {
|
||||||
|
mListener = context as DeleteNodeListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + DeleteNodeListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun retrieveMessage(): String {
|
||||||
|
return getString(R.string.warning_permanently_delete_nodes)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
mNodesViewModel.nodesToDelete.observe(this) { nodes ->
|
|
||||||
this.mNodesToDelete = nodes
|
|
||||||
}
|
|
||||||
var recycleBin = false
|
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
if (containsKey(RECYCLE_BIN_TAG)) {
|
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
|
||||||
recycleBin = this.getBoolean(RECYCLE_BIN_TAG)
|
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
|
||||||
|
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), this)
|
||||||
|
}
|
||||||
|
} ?: savedInstanceState?.apply {
|
||||||
|
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
|
||||||
|
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
|
||||||
|
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), savedInstanceState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
// Use the Builder class for convenient dialog construction
|
// Use the Builder class for convenient dialog construction
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|
||||||
builder.setMessage(if (recycleBin)
|
builder.setMessage(retrieveMessage())
|
||||||
getString(R.string.warning_empty_recycle_bin)
|
|
||||||
else
|
|
||||||
getString(R.string.warning_permanently_delete_nodes))
|
|
||||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
mNodesViewModel.permanentlyDeleteNodes(mNodesToDelete)
|
mListener?.permanentlyDeleteNodes(mNodesToDelete)
|
||||||
}
|
}
|
||||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||||
// Create the AlertDialog object and return it
|
// Create the AlertDialog object and return it
|
||||||
@@ -60,14 +83,19 @@ class DeleteNodesDialogFragment : DatabaseDialogFragment() {
|
|||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
private const val RECYCLE_BIN_TAG = "RECYCLE_BIN_TAG"
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putAll(getBundleFromListNodes(mNodesToDelete))
|
||||||
|
}
|
||||||
|
|
||||||
fun getInstance(recycleBin: Boolean): DeleteNodesDialogFragment {
|
interface DeleteNodeListener {
|
||||||
|
fun permanentlyDeleteNodes(nodes: List<Node>)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getInstance(nodesToDelete: List<Node>): DeleteNodesDialogFragment {
|
||||||
return DeleteNodesDialogFragment().apply {
|
return DeleteNodesDialogFragment().apply {
|
||||||
arguments = Bundle().apply {
|
arguments = getBundleFromListNodes(nodesToDelete)
|
||||||
putBoolean(RECYCLE_BIN_TAG, recycleBin)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2022 Jeremy Jamet / Kunzisoft.
|
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePassDX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
@@ -17,22 +17,23 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.model
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
enum class CredentialStorage {
|
import com.kunzisoft.keepass.R
|
||||||
PASSWORD, KEY_FILE, HARDWARE_KEY;
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||||
|
|
||||||
|
class EmptyRecycleBinDialogFragment : DeleteNodesDialogFragment() {
|
||||||
|
|
||||||
|
override fun retrieveMessage(): String {
|
||||||
|
return getString(R.string.warning_empty_recycle_bin)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getFromOrdinal(ordinal: Int): CredentialStorage {
|
fun getInstance(nodesToDelete: List<Node>): EmptyRecycleBinDialogFragment {
|
||||||
return when (ordinal) {
|
return EmptyRecycleBinDialogFragment().apply {
|
||||||
0 -> PASSWORD
|
arguments = getBundleFromListNodes(nodesToDelete)
|
||||||
1 -> KEY_FILE
|
|
||||||
2 -> HARDWARE_KEY
|
|
||||||
else -> DEFAULT
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val DEFAULT: CredentialStorage
|
|
||||||
get() = PASSWORD
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,14 +31,14 @@ import android.widget.ImageView
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Field
|
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
import com.kunzisoft.keepass.model.Field
|
||||||
|
|
||||||
|
|
||||||
class EntryCustomFieldDialogFragment: DatabaseDialogFragment() {
|
class EntryCustomFieldDialogFragment: DialogFragment() {
|
||||||
|
|
||||||
private var oldField: Field? = null
|
private var oldField: Field? = null
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ class EntryCustomFieldDialogFragment: DatabaseDialogFragment() {
|
|||||||
customFieldDeleteButton = root?.findViewById(R.id.entry_custom_field_delete)
|
customFieldDeleteButton = root?.findViewById(R.id.entry_custom_field_delete)
|
||||||
customFieldProtectionButton = root?.findViewById(R.id.entry_custom_field_protection)
|
customFieldProtectionButton = root?.findViewById(R.id.entry_custom_field_protection)
|
||||||
|
|
||||||
oldField = arguments?.getParcelableCompat(KEY_FIELD)
|
oldField = arguments?.getParcelable(KEY_FIELD)
|
||||||
oldField?.let { oldCustomField ->
|
oldField?.let { oldCustomField ->
|
||||||
customFieldLabel?.text = oldCustomField.name
|
customFieldLabel?.text = oldCustomField.name
|
||||||
customFieldProtectionButton?.isChecked = oldCustomField.protectedValue.isProtected
|
customFieldProtectionButton?.isChecked = oldCustomField.protectedValue.isProtected
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
class FileManagerDialogFragment : DialogFragment() {
|
class FileManagerDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ class FileManagerDialogFragment : DialogFragment() {
|
|||||||
textDescription.text = getString(R.string.file_manager_install_description)
|
textDescription.text = getString(R.string.file_manager_install_description)
|
||||||
|
|
||||||
root.findViewById<Button>(R.id.file_manager_button).setOnClickListener {
|
root.findViewById<Button>(R.id.file_manager_button).setOnClickListener {
|
||||||
context?.openUrl(R.string.file_manager_explanation_url)
|
UriUtil.gotoUrl(requireContext(), R.string.file_manager_explanation_url)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import android.text.SpannableStringBuilder
|
|||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Dialog to confirm big file to upload
|
* Custom Dialog to confirm big file to upload
|
||||||
@@ -63,7 +62,7 @@ class FileTooBigDialogFragment : DialogFragment() {
|
|||||||
})
|
})
|
||||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
mActionChooseListener?.onValidateUploadFileTooBig(
|
mActionChooseListener?.onValidateUploadFileTooBig(
|
||||||
arguments?.getParcelableCompat(KEY_FILE_URI),
|
arguments?.getParcelable(KEY_FILE_URI),
|
||||||
arguments?.getString(KEY_FILE_NAME))
|
arguments?.getString(KEY_FILE_NAME))
|
||||||
}
|
}
|
||||||
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.google.android.material.textfield.TextInputLayout
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.*
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
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 : DialogFragment() {
|
||||||
|
|
||||||
|
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 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.allowCopyPasswordAndProtectedFields(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)
|
||||||
|
|
||||||
|
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) { _, _ ->
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.putString(KEY_PASSWORD_ID, passwordView!!.text.toString())
|
||||||
|
mListener?.acceptPassword(bundle)
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
val bundle = Bundle()
|
||||||
|
mListener?.cancelPassword(bundle)
|
||||||
|
|
||||||
|
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(bundle: Bundle)
|
||||||
|
fun cancelPassword(bundle: Bundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KEY_PASSWORD_ID = "KEY_PASSWORD_ID"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.adapters.TagsAdapter
|
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
|
||||||
import com.kunzisoft.keepass.model.GroupInfo
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
|
||||||
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
|
||||||
import com.kunzisoft.keepass.view.DateTimeFieldView
|
|
||||||
|
|
||||||
class GroupDialogFragment : DatabaseDialogFragment() {
|
|
||||||
|
|
||||||
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
|
|
||||||
private var mGroupInfo = GroupInfo()
|
|
||||||
|
|
||||||
private lateinit var iconView: ImageView
|
|
||||||
private var mIconColor: Int = 0
|
|
||||||
private lateinit var nameTextView: TextView
|
|
||||||
private lateinit var tagsListView: RecyclerView
|
|
||||||
private var tagsAdapter: TagsAdapter? = null
|
|
||||||
private lateinit var notesTextLabelView: TextView
|
|
||||||
private lateinit var notesTextView: TextView
|
|
||||||
private lateinit var expirationView: DateTimeFieldView
|
|
||||||
private lateinit var creationView: TextView
|
|
||||||
private lateinit var modificationView: TextView
|
|
||||||
private lateinit var searchableLabelView: TextView
|
|
||||||
private lateinit var searchableView: TextView
|
|
||||||
private lateinit var autoTypeLabelView: TextView
|
|
||||||
private lateinit var autoTypeView: TextView
|
|
||||||
private lateinit var uuidContainerView: ViewGroup
|
|
||||||
private lateinit var uuidReferenceView: TextView
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
mPopulateIconMethod = { imageView, icon ->
|
|
||||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
|
||||||
}
|
|
||||||
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
|
|
||||||
|
|
||||||
if (database?.allowCustomSearchableGroup() == true) {
|
|
||||||
searchableLabelView.visibility = View.VISIBLE
|
|
||||||
searchableView.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
searchableLabelView.visibility = View.GONE
|
|
||||||
searchableView.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Auto-Type
|
|
||||||
/*
|
|
||||||
if (database?.allowAutoType() == true) {
|
|
||||||
autoTypeLabelView.visibility = View.VISIBLE
|
|
||||||
autoTypeView.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
autoTypeLabelView.visibility = View.GONE
|
|
||||||
autoTypeView.visibility = View.GONE
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
activity?.let { activity ->
|
|
||||||
val root = activity.layoutInflater.inflate(R.layout.fragment_group, null)
|
|
||||||
iconView = root.findViewById(R.id.group_icon)
|
|
||||||
nameTextView = root.findViewById(R.id.group_name)
|
|
||||||
tagsListView = root.findViewById(R.id.group_tags_list_view)
|
|
||||||
notesTextLabelView = root.findViewById(R.id.group_note_label)
|
|
||||||
notesTextView = root.findViewById(R.id.group_note)
|
|
||||||
expirationView = root.findViewById(R.id.group_expiration)
|
|
||||||
creationView = root.findViewById(R.id.group_created)
|
|
||||||
modificationView = root.findViewById(R.id.group_modified)
|
|
||||||
searchableLabelView = root.findViewById(R.id.group_searchable_label)
|
|
||||||
searchableView = root.findViewById(R.id.group_searchable)
|
|
||||||
autoTypeLabelView = root.findViewById(R.id.group_auto_type_label)
|
|
||||||
autoTypeView = root.findViewById(R.id.group_auto_type)
|
|
||||||
uuidContainerView = root.findViewById(R.id.group_UUID_container)
|
|
||||||
uuidReferenceView = root.findViewById(R.id.group_UUID_reference)
|
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
|
||||||
val ta = activity.theme.obtainStyledAttributes(intArrayOf(R.attr.colorSecondary))
|
|
||||||
mIconColor = ta.getColor(0, Color.WHITE)
|
|
||||||
ta.recycle()
|
|
||||||
|
|
||||||
if (savedInstanceState != null
|
|
||||||
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
|
||||||
mGroupInfo = savedInstanceState.getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
|
|
||||||
} else {
|
|
||||||
arguments?.apply {
|
|
||||||
if (containsKey(KEY_GROUP_INFO)) {
|
|
||||||
mGroupInfo = getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// populate info in views
|
|
||||||
val title = mGroupInfo.title
|
|
||||||
if (title.isEmpty()) {
|
|
||||||
nameTextView.visibility = View.GONE
|
|
||||||
} else {
|
|
||||||
nameTextView.text = title
|
|
||||||
nameTextView.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
tagsAdapter = TagsAdapter(activity)
|
|
||||||
tagsListView.apply {
|
|
||||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
|
||||||
adapter = tagsAdapter
|
|
||||||
}
|
|
||||||
val tags = mGroupInfo.tags
|
|
||||||
tagsListView.visibility = if (tags.isEmpty()) View.GONE else View.VISIBLE
|
|
||||||
tagsAdapter?.setTags(tags)
|
|
||||||
val notes = mGroupInfo.notes
|
|
||||||
if (notes == null || notes.isEmpty()) {
|
|
||||||
notesTextLabelView.visibility = View.GONE
|
|
||||||
notesTextView.visibility = View.GONE
|
|
||||||
} else {
|
|
||||||
notesTextView.text = notes
|
|
||||||
notesTextLabelView.visibility = View.VISIBLE
|
|
||||||
notesTextView.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
expirationView.activation = mGroupInfo.expires
|
|
||||||
expirationView.dateTime = mGroupInfo.expiryTime
|
|
||||||
creationView.text = mGroupInfo.creationTime.getDateTimeString(resources)
|
|
||||||
modificationView.text = mGroupInfo.lastModificationTime.getDateTimeString(resources)
|
|
||||||
searchableView.text = stringFromInheritableBoolean(mGroupInfo.searchable)
|
|
||||||
autoTypeView.text = stringFromInheritableBoolean(mGroupInfo.enableAutoType,
|
|
||||||
mGroupInfo.defaultAutoTypeSequence)
|
|
||||||
val uuid = mGroupInfo.id?.asHexString()
|
|
||||||
if (uuid == null || uuid.isEmpty()) {
|
|
||||||
uuidContainerView.visibility = View.GONE
|
|
||||||
} else {
|
|
||||||
uuidReferenceView.text = uuid
|
|
||||||
uuidContainerView.apply {
|
|
||||||
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
|
||||||
builder.setView(root)
|
|
||||||
.setPositiveButton(android.R.string.ok){ _, _ ->
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
return builder.create()
|
|
||||||
}
|
|
||||||
return super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stringFromInheritableBoolean(enable: Boolean?, value: String? = null): String {
|
|
||||||
val valueString = if (value != null && value.isNotEmpty()) " [$value]" else ""
|
|
||||||
return when {
|
|
||||||
enable == null -> getString(R.string.inherited) + valueString
|
|
||||||
enable -> getString(R.string.enable) + valueString
|
|
||||||
else -> getString(R.string.disable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
outState.putParcelable(KEY_GROUP_INFO, mGroupInfo)
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Error(val isError: Boolean, val messageId: Int?)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG_SHOW_GROUP = "TAG_SHOW_GROUP"
|
|
||||||
private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
|
||||||
|
|
||||||
fun launch(groupInfo: GroupInfo): GroupDialogFragment {
|
|
||||||
val bundle = Bundle()
|
|
||||||
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
|
|
||||||
val fragment = GroupDialogFragment()
|
|
||||||
fragment.arguments = bundle
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,56 +20,43 @@
|
|||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.IconPickerActivity
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
|
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.NONE
|
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
|
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
|
||||||
import com.kunzisoft.keepass.adapters.TagsProposalAdapter
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.model.GroupInfo
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
import com.kunzisoft.keepass.view.ExpirationView
|
||||||
import com.kunzisoft.keepass.view.DateTimeEditFieldView
|
import org.joda.time.DateTime
|
||||||
import com.kunzisoft.keepass.view.InheritedCompletionView
|
|
||||||
import com.kunzisoft.keepass.view.TagsCompletionView
|
|
||||||
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
|
||||||
import com.tokenautocomplete.FilteredArrayAdapter
|
|
||||||
|
|
||||||
class GroupEditDialogFragment : DatabaseDialogFragment() {
|
class GroupEditDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
private val mGroupEditViewModel: GroupEditViewModel by activityViewModels()
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
|
private var mEditGroupListener: EditGroupListener? = null
|
||||||
private var mEditGroupDialogAction = NONE
|
|
||||||
|
private var mEditGroupDialogAction = EditGroupDialogAction.NONE
|
||||||
private var mGroupInfo = GroupInfo()
|
private var mGroupInfo = GroupInfo()
|
||||||
private var mGroupNamesNotAllowed: List<String>? = null
|
|
||||||
|
|
||||||
private lateinit var iconButtonView: ImageView
|
private lateinit var iconButtonView: ImageView
|
||||||
private var mIconColor: Int = 0
|
private var iconColor: Int = 0
|
||||||
private lateinit var nameTextLayoutView: TextInputLayout
|
private lateinit var nameTextLayoutView: TextInputLayout
|
||||||
private lateinit var nameTextView: TextView
|
private lateinit var nameTextView: TextView
|
||||||
private lateinit var notesTextLayoutView: TextInputLayout
|
private lateinit var notesTextLayoutView: TextInputLayout
|
||||||
private lateinit var notesTextView: TextView
|
private lateinit var notesTextView: TextView
|
||||||
private lateinit var expirationView: DateTimeEditFieldView
|
private lateinit var expirationView: ExpirationView
|
||||||
private lateinit var searchableContainerView: TextInputLayout
|
|
||||||
private lateinit var searchableView: InheritedCompletionView
|
|
||||||
private lateinit var autoTypeContainerView: ViewGroup
|
|
||||||
private lateinit var autoTypeInheritedView: InheritedCompletionView
|
|
||||||
private lateinit var autoTypeSequenceView: TextView
|
|
||||||
private lateinit var tagsContainerView: TextInputLayout
|
|
||||||
private lateinit var tagsCompletionView: TagsCompletionView
|
|
||||||
private var tagsAdapter: FilteredArrayAdapter<String>? = null
|
|
||||||
|
|
||||||
enum class EditGroupDialogAction {
|
enum class EditGroupDialogAction {
|
||||||
CREATION, UPDATE, NONE;
|
CREATION, UPDATE, NONE;
|
||||||
@@ -81,63 +68,22 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onAttach(context: Context) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onAttach(context)
|
||||||
|
// Verify that the host activity implements the callback interface
|
||||||
mGroupEditViewModel.onIconSelected.observe(this) { iconImage ->
|
try {
|
||||||
mGroupInfo.icon = iconImage
|
// Instantiate the NoticeDialogListener so we can send events to the host
|
||||||
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
mEditGroupListener = context as EditGroupListener
|
||||||
}
|
} catch (e: ClassCastException) {
|
||||||
|
// The activity doesn't implement the interface, throw exception
|
||||||
mGroupEditViewModel.onDateSelected.observe(this) { date ->
|
throw ClassCastException(context.toString()
|
||||||
// Save the date
|
+ " must implement " + GroupEditDialogFragment::class.java.name)
|
||||||
mGroupInfo.expiryTime.setDate(date.year, date.month, date.day)
|
|
||||||
expirationView.dateTime = mGroupInfo.expiryTime
|
|
||||||
if (expirationView.dateTime.type == DateInstant.Type.DATE_TIME) {
|
|
||||||
// Trick to recall selection with time
|
|
||||||
mGroupEditViewModel.requestDateTimeSelection(
|
|
||||||
DateInstant(mGroupInfo.expiryTime.instant, DateInstant.Type.TIME)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mGroupEditViewModel.onTimeSelected.observe(this) { viewModelTime ->
|
|
||||||
// Save the time
|
|
||||||
mGroupInfo.expiryTime.setTime(viewModelTime.hour, viewModelTime.minute)
|
|
||||||
expirationView.dateTime = mGroupInfo.expiryTime
|
|
||||||
}
|
|
||||||
|
|
||||||
mGroupEditViewModel.groupNamesNotAllowed.observe(this) { namesNotAllowed ->
|
|
||||||
this.mGroupNamesNotAllowed = namesNotAllowed
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDetach() {
|
||||||
super.onDatabaseRetrieved(database)
|
mEditGroupListener = null
|
||||||
|
super.onDetach()
|
||||||
mPopulateIconMethod = { imageView, icon ->
|
|
||||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
|
||||||
}
|
|
||||||
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
|
||||||
|
|
||||||
searchableContainerView.visibility = if (database?.allowCustomSearchableGroup() == true) {
|
|
||||||
View.VISIBLE
|
|
||||||
} else {
|
|
||||||
View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
if (database?.allowAutoType() == true) {
|
|
||||||
autoTypeContainerView.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
autoTypeContainerView.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
|
|
||||||
tagsCompletionView.apply {
|
|
||||||
threshold = 1
|
|
||||||
setAdapter(tagsAdapter)
|
|
||||||
}
|
|
||||||
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
@@ -149,51 +95,57 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
notesTextLayoutView = root.findViewById(R.id.group_edit_note_container)
|
notesTextLayoutView = root.findViewById(R.id.group_edit_note_container)
|
||||||
notesTextView = root.findViewById(R.id.group_edit_note)
|
notesTextView = root.findViewById(R.id.group_edit_note)
|
||||||
expirationView = root.findViewById(R.id.group_edit_expiration)
|
expirationView = root.findViewById(R.id.group_edit_expiration)
|
||||||
searchableContainerView = root.findViewById(R.id.group_edit_searchable_container)
|
|
||||||
searchableView = root.findViewById(R.id.group_edit_searchable)
|
|
||||||
autoTypeContainerView = root.findViewById(R.id.group_edit_auto_type_container)
|
|
||||||
autoTypeInheritedView = root.findViewById(R.id.group_edit_auto_type_inherited)
|
|
||||||
autoTypeSequenceView = root.findViewById(R.id.group_edit_auto_type_sequence)
|
|
||||||
tagsContainerView = root.findViewById(R.id.group_tags_label)
|
|
||||||
tagsCompletionView = root.findViewById(R.id.group_tags_completion_view)
|
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
// Retrieve the textColor to tint the icon
|
||||||
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
mIconColor = ta.getColor(0, Color.WHITE)
|
iconColor = ta.getColor(0, Color.WHITE)
|
||||||
ta.recycle()
|
ta.recycle()
|
||||||
|
|
||||||
|
// Init elements
|
||||||
|
mDatabase = Database.getInstance()
|
||||||
|
|
||||||
if (savedInstanceState != null
|
if (savedInstanceState != null
|
||||||
&& savedInstanceState.containsKey(KEY_ACTION_ID)
|
&& savedInstanceState.containsKey(KEY_ACTION_ID)
|
||||||
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
||||||
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
|
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
|
||||||
mGroupInfo = savedInstanceState.getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
|
mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||||
} else {
|
} else {
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
if (containsKey(KEY_ACTION_ID))
|
if (containsKey(KEY_ACTION_ID))
|
||||||
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
|
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
|
||||||
if (containsKey(KEY_GROUP_INFO)) {
|
if (containsKey(KEY_GROUP_INFO)) {
|
||||||
mGroupInfo = getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
|
mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// populate info in views
|
// populate info in views
|
||||||
populateInfoToViews(mGroupInfo)
|
populateInfoToViews()
|
||||||
|
expirationView.setOnDateClickListener = {
|
||||||
iconButtonView.setOnClickListener { _ ->
|
expirationView.expiryTime.date.let { expiresDate ->
|
||||||
mGroupEditViewModel.requestIconSelection(mGroupInfo.icon)
|
val dateTime = DateTime(expiresDate)
|
||||||
}
|
val defaultYear = dateTime.year
|
||||||
expirationView.setOnDateClickListener = { dateInstant ->
|
val defaultMonth = dateTime.monthOfYear-1
|
||||||
mGroupEditViewModel.requestDateTimeSelection(dateInstant)
|
val defaultDay = dateTime.dayOfMonth
|
||||||
|
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
|
||||||
|
.show(parentFragmentManager, "DatePickerFragment")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
builder.setView(root)
|
builder.setView(root)
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
// Do nothing
|
retrieveGroupInfoFromViews()
|
||||||
|
mEditGroupListener?.cancelEditGroup(
|
||||||
|
mEditGroupDialogAction,
|
||||||
|
mGroupInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
iconButtonView.setOnClickListener { _ ->
|
||||||
|
IconPickerActivity.launch(activity, mGroupInfo.icon)
|
||||||
|
}
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
}
|
}
|
||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
@@ -203,58 +155,60 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
// To prevent auto dismiss
|
// To prevent auto dismiss
|
||||||
val alertDialog = dialog as AlertDialog?
|
val d = dialog as AlertDialog?
|
||||||
if (alertDialog != null) {
|
if (d != null) {
|
||||||
val positiveButton = alertDialog.getButton(Dialog.BUTTON_POSITIVE) as Button
|
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
|
||||||
positiveButton.setOnClickListener {
|
positiveButton.setOnClickListener {
|
||||||
retrieveGroupInfoFromViews()
|
retrieveGroupInfoFromViews()
|
||||||
if (isValid()) {
|
if (isValid()) {
|
||||||
when (mEditGroupDialogAction) {
|
mEditGroupListener?.approveEditGroup(
|
||||||
CREATION ->
|
mEditGroupDialogAction,
|
||||||
mGroupEditViewModel.approveGroupCreation(mGroupInfo)
|
mGroupInfo)
|
||||||
UPDATE ->
|
d.dismiss()
|
||||||
mGroupEditViewModel.approveGroupUpdate(mGroupInfo)
|
|
||||||
NONE -> {}
|
|
||||||
}
|
|
||||||
alertDialog.dismiss()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun populateInfoToViews(groupInfo: GroupInfo) {
|
fun getExpiryTime(): DateInstant {
|
||||||
mGroupEditViewModel.selectIcon(groupInfo.icon)
|
retrieveGroupInfoFromViews()
|
||||||
nameTextView.text = groupInfo.title
|
return mGroupInfo.expiryTime
|
||||||
notesTextLayoutView.visibility = if (groupInfo.notes == null) View.GONE else View.VISIBLE
|
}
|
||||||
groupInfo.notes?.let {
|
|
||||||
|
fun setExpiryTime(expiryTime: DateInstant) {
|
||||||
|
mGroupInfo.expiryTime = expiryTime
|
||||||
|
populateInfoToViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateInfoToViews() {
|
||||||
|
assignIconView()
|
||||||
|
nameTextView.text = mGroupInfo.title
|
||||||
|
notesTextLayoutView.visibility = if (mGroupInfo.notes == null) View.GONE else View.VISIBLE
|
||||||
|
mGroupInfo.notes?.let {
|
||||||
notesTextView.text = it
|
notesTextView.text = it
|
||||||
}
|
}
|
||||||
expirationView.activation = groupInfo.expires
|
expirationView.expires = mGroupInfo.expires
|
||||||
expirationView.dateTime = groupInfo.expiryTime
|
expirationView.expiryTime = mGroupInfo.expiryTime
|
||||||
|
|
||||||
// Set searchable
|
|
||||||
searchableView.setValue(groupInfo.searchable)
|
|
||||||
// Set auto-type
|
|
||||||
autoTypeInheritedView.setValue(groupInfo.enableAutoType)
|
|
||||||
autoTypeSequenceView.text = groupInfo.defaultAutoTypeSequence
|
|
||||||
// Set Tags
|
|
||||||
groupInfo.tags.let { tags ->
|
|
||||||
tagsCompletionView.setText("")
|
|
||||||
for (i in 0 until tags.size()) {
|
|
||||||
tagsCompletionView.addObjectSync(tags.get(i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun retrieveGroupInfoFromViews() {
|
private fun retrieveGroupInfoFromViews() {
|
||||||
mGroupInfo.title = nameTextView.text.toString()
|
mGroupInfo.title = nameTextView.text.toString()
|
||||||
mGroupInfo.notes = notesTextView.text?.toString()
|
// Only if there
|
||||||
mGroupInfo.expires = expirationView.activation
|
val newNotes = notesTextView.text.toString()
|
||||||
mGroupInfo.expiryTime = expirationView.dateTime
|
if (newNotes.isNotEmpty()) {
|
||||||
mGroupInfo.searchable = searchableView.getValue()
|
mGroupInfo.notes = newNotes
|
||||||
mGroupInfo.enableAutoType = autoTypeInheritedView.getValue()
|
}
|
||||||
mGroupInfo.defaultAutoTypeSequence = autoTypeSequenceView.text.toString()
|
mGroupInfo.expires = expirationView.expires
|
||||||
mGroupInfo.tags = tagsCompletionView.getTags()
|
mGroupInfo.expiryTime = expirationView.expiryTime
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assignIconView() {
|
||||||
|
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconButtonView, mGroupInfo.icon, iconColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setIcon(icon: IconImage) {
|
||||||
|
mGroupInfo.icon = icon
|
||||||
|
assignIconView()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
@@ -265,36 +219,25 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isValid(): Boolean {
|
private fun isValid(): Boolean {
|
||||||
val name = nameTextView.text.toString()
|
if (nameTextView.text.toString().isEmpty()) {
|
||||||
val error = when {
|
nameTextLayoutView.error = getString(R.string.error_no_name)
|
||||||
name.isEmpty() -> {
|
return false
|
||||||
Error(true, R.string.error_no_name)
|
|
||||||
}
|
|
||||||
mGroupNamesNotAllowed == null -> {
|
|
||||||
Error(true, R.string.error_word_reserved)
|
|
||||||
}
|
|
||||||
mGroupNamesNotAllowed?.find { it.equals(name, ignoreCase = true) } != null -> {
|
|
||||||
Error(true, R.string.error_word_reserved)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Error(false, null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
error.messageId?.let { messageId ->
|
return true
|
||||||
nameTextLayoutView.error = getString(messageId)
|
|
||||||
} ?: kotlin.run {
|
|
||||||
nameTextLayoutView.error = null
|
|
||||||
}
|
|
||||||
return !error.isError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Error(val isError: Boolean, val messageId: Int?)
|
interface EditGroupListener {
|
||||||
|
fun approveEditGroup(action: EditGroupDialogAction,
|
||||||
|
groupInfo: GroupInfo)
|
||||||
|
fun cancelEditGroup(action: EditGroupDialogAction,
|
||||||
|
groupInfo: GroupInfo)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
|
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
|
||||||
private const val KEY_ACTION_ID = "KEY_ACTION_ID"
|
const val KEY_ACTION_ID = "KEY_ACTION_ID"
|
||||||
private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
||||||
|
|
||||||
fun create(groupInfo: GroupInfo): GroupEditDialogFragment {
|
fun create(groupInfo: GroupInfo): GroupEditDialogFragment {
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
|
||||||
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
|
||||||
|
|
||||||
class IconEditDialogFragment : DatabaseDialogFragment() {
|
|
||||||
|
|
||||||
private val mIconPickerViewModel: IconPickerViewModel by activityViewModels()
|
|
||||||
|
|
||||||
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
|
|
||||||
private lateinit var iconView: ImageView
|
|
||||||
private lateinit var nameTextLayoutView: TextInputLayout
|
|
||||||
private lateinit var nameTextView: TextView
|
|
||||||
|
|
||||||
private var mCustomIcon: IconImageCustom? = null
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
mPopulateIconMethod = { imageView, icon ->
|
|
||||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
|
|
||||||
}
|
|
||||||
mCustomIcon?.let { customIcon ->
|
|
||||||
populateViewsWithCustomIcon(customIcon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
activity?.let { activity ->
|
|
||||||
val root = activity.layoutInflater.inflate(R.layout.fragment_icon_edit, null)
|
|
||||||
iconView = root.findViewById(R.id.icon_edit_image)
|
|
||||||
nameTextLayoutView = root.findViewById(R.id.icon_edit_name_container)
|
|
||||||
nameTextView = root.findViewById(R.id.icon_edit_name)
|
|
||||||
|
|
||||||
if (savedInstanceState != null
|
|
||||||
&& savedInstanceState.containsKey(KEY_CUSTOM_ICON_ID)) {
|
|
||||||
mCustomIcon = savedInstanceState.getParcelableCompat(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
|
|
||||||
} else {
|
|
||||||
arguments?.apply {
|
|
||||||
if (containsKey(KEY_CUSTOM_ICON_ID)) {
|
|
||||||
mCustomIcon = getParcelableCompat(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
|
||||||
builder.setView(root)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
retrieveIconInfoFromViews()
|
|
||||||
mCustomIcon?.let { customIcon ->
|
|
||||||
mIconPickerViewModel.updateCustomIcon(
|
|
||||||
IconPickerViewModel.IconCustomState(customIcon, false)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
// Do nothing
|
|
||||||
mIconPickerViewModel.updateCustomIcon(
|
|
||||||
IconPickerViewModel.IconCustomState(null, false)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.create()
|
|
||||||
}
|
|
||||||
return super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun populateViewsWithCustomIcon(customIcon: IconImageCustom) {
|
|
||||||
mPopulateIconMethod?.invoke(iconView, customIcon.getIconImageToDraw())
|
|
||||||
nameTextView.text = customIcon.name
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun retrieveIconInfoFromViews() {
|
|
||||||
mCustomIcon?.name = nameTextView.text.toString()
|
|
||||||
mCustomIcon?.lastModificationTime = DateInstant()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
retrieveIconInfoFromViews()
|
|
||||||
outState.putParcelable(KEY_CUSTOM_ICON_ID, mCustomIcon)
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val TAG_UPDATE_ICON = "TAG_UPDATE_ICON"
|
|
||||||
const val KEY_CUSTOM_ICON_ID = "KEY_CUSTOM_ICON_ID"
|
|
||||||
|
|
||||||
fun update(customIcon: IconImageCustom): IconEditDialogFragment {
|
|
||||||
val bundle = Bundle()
|
|
||||||
bundle.putParcelable(KEY_CUSTOM_ICON_ID, IconImageCustom(customIcon))
|
|
||||||
val fragment = IconEditDialogFragment()
|
|
||||||
fragment.arguments = bundle
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.dialogs
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
|
||||||
import com.kunzisoft.keepass.view.MainCredentialView
|
|
||||||
|
|
||||||
class MainCredentialDialogFragment : DatabaseDialogFragment() {
|
|
||||||
|
|
||||||
private var mainCredentialView: MainCredentialView? = null
|
|
||||||
|
|
||||||
private var mListener: AskMainCredentialDialogListener? = null
|
|
||||||
|
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
|
||||||
|
|
||||||
interface AskMainCredentialDialogListener {
|
|
||||||
fun onAskMainCredentialDialogPositiveClick(databaseUri: Uri?, mainCredential: MainCredential)
|
|
||||||
fun onAskMainCredentialDialogNegativeClick(databaseUri: Uri?, mainCredential: MainCredential)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttach(activity: Context) {
|
|
||||||
super.onAttach(activity)
|
|
||||||
try {
|
|
||||||
mListener = activity as AskMainCredentialDialogListener
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
throw ClassCastException(activity.toString()
|
|
||||||
+ " must implement " + AskMainCredentialDialogListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetach() {
|
|
||||||
mListener = null
|
|
||||||
super.onDetach()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
activity?.let { activity ->
|
|
||||||
|
|
||||||
var databaseUri: Uri? = null
|
|
||||||
arguments?.apply {
|
|
||||||
if (containsKey(KEY_ASK_CREDENTIAL_URI))
|
|
||||||
databaseUri = getParcelableCompat(KEY_ASK_CREDENTIAL_URI)
|
|
||||||
}
|
|
||||||
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
|
||||||
|
|
||||||
val root = activity.layoutInflater.inflate(R.layout.fragment_main_credential, null)
|
|
||||||
mainCredentialView = root.findViewById(R.id.main_credential_view)
|
|
||||||
databaseUri?.let {
|
|
||||||
root.findViewById<TextView>(R.id.title_database)?.text =
|
|
||||||
it.getDocumentFile(requireContext())?.name
|
|
||||||
}
|
|
||||||
builder.setView(root)
|
|
||||||
// Add action buttons
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
mListener?.onAskMainCredentialDialogPositiveClick(
|
|
||||||
databaseUri,
|
|
||||||
retrieveMainCredential()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
mListener?.onAskMainCredentialDialogNegativeClick(
|
|
||||||
databaseUri,
|
|
||||||
retrieveMainCredential()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
mExternalFileHelper = ExternalFileHelper(this)
|
|
||||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
|
||||||
if (uri != null) {
|
|
||||||
mainCredentialView?.populateKeyFileView(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
|
||||||
|
|
||||||
return builder.create()
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun retrieveMainCredential(): MainCredential {
|
|
||||||
return mainCredentialView?.getMainCredential() ?: MainCredential()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val KEY_ASK_CREDENTIAL_URI = "KEY_ASK_CREDENTIAL_URI"
|
|
||||||
const val TAG_ASK_MAIN_CREDENTIAL = "TAG_ASK_MAIN_CREDENTIAL"
|
|
||||||
|
|
||||||
fun getInstance(uri: Uri?): MainCredentialDialogFragment {
|
|
||||||
val fragment = MainCredentialDialogFragment()
|
|
||||||
val args = Bundle()
|
|
||||||
args.putParcelable(KEY_ASK_CREDENTIAL_URI, uri)
|
|
||||||
fragment.arguments = args
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,15 +19,14 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
|
||||||
|
|
||||||
class PasswordEncodingDialogFragment : DialogFragment() {
|
class PasswordEncodingDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
@@ -50,8 +49,8 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
||||||
val databaseUri: Uri? = savedInstanceState?.getParcelableCompat(DATABASE_URI_KEY)
|
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
|
||||||
val mainCredential: MainCredential = savedInstanceState?.getParcelableCompat(MAIN_CREDENTIAL) ?: MainCredential()
|
val mainCredential: MainCredential = savedInstanceState?.getParcelable(MAIN_CREDENTIAL) ?: MainCredential()
|
||||||
|
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
@@ -79,10 +78,8 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
|||||||
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
||||||
private const val MAIN_CREDENTIAL = "MAIN_CREDENTIAL"
|
private const val MAIN_CREDENTIAL = "MAIN_CREDENTIAL"
|
||||||
|
|
||||||
fun getInstance(
|
fun getInstance(databaseUri: Uri,
|
||||||
databaseUri: Uri,
|
mainCredential: MainCredential): SortDialogFragment {
|
||||||
mainCredential: MainCredential
|
|
||||||
): SortDialogFragment {
|
|
||||||
val fragment = SortDialogFragment()
|
val fragment = SortDialogFragment()
|
||||||
fragment.arguments = Bundle().apply {
|
fragment.arguments = Bundle().apply {
|
||||||
putParcelable(DATABASE_URI_KEY, databaseUri)
|
putParcelable(DATABASE_URI_KEY, databaseUri)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
|
|||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Dialog that asks the user to download the pro version or make a donation.
|
* Custom Dialog that asks the user to download the pro version or make a donation.
|
||||||
@@ -45,16 +45,13 @@ class ProFeatureDialogFragment : DialogFragment() {
|
|||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY))
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY))
|
||||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||||
activity.openUrl(
|
UriUtil.gotoUrl(requireContext(), R.string.app_pro_url)
|
||||||
activity.getString(R.string.play_store_url,
|
|
||||||
activity.getString(R.string.keepro_app_id))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_feature_generosity), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_feature_generosity), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY))
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY))
|
||||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||||
activity.openUrl(R.string.contribution_url)
|
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder.setMessage(stringBuilder)
|
builder.setMessage(stringBuilder)
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Dialog to confirm big file to upload
|
* Custom Dialog to confirm big file to upload
|
||||||
*/
|
*/
|
||||||
class ReplaceFileDialogFragment : DatabaseDialogFragment() {
|
class ReplaceFileDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
private var mActionChooseListener: ActionChooseListener? = null
|
private var mActionChooseListener: ActionChooseListener? = null
|
||||||
|
|
||||||
@@ -63,8 +63,8 @@ class ReplaceFileDialogFragment : DatabaseDialogFragment() {
|
|||||||
})
|
})
|
||||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
mActionChooseListener?.onValidateReplaceFile(
|
mActionChooseListener?.onValidateReplaceFile(
|
||||||
arguments?.getParcelableCompat(KEY_FILE_URI),
|
arguments?.getParcelable(KEY_FILE_URI),
|
||||||
arguments?.getParcelableCompat(KEY_ENTRY_ATTACHMENT))
|
arguments?.getParcelable(KEY_ENTRY_ATTACHMENT))
|
||||||
}
|
}
|
||||||
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
dismiss()
|
dismiss()
|
||||||
|
|||||||
@@ -1,412 +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.content.DialogInterface
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.Editable
|
|
||||||
import android.text.SpannableStringBuilder
|
|
||||||
import android.text.TextWatcher
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.CompoundButton
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
|
||||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
|
||||||
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
|
|
||||||
import com.kunzisoft.keepass.password.PasswordEntropy
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
|
||||||
import com.kunzisoft.keepass.view.HardwareKeySelectionView
|
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
|
||||||
import com.kunzisoft.keepass.view.PasswordEditView
|
|
||||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.security.SecureRandom
|
|
||||||
|
|
||||||
|
|
||||||
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|
||||||
|
|
||||||
private var mMasterPassword: String? = null
|
|
||||||
private var mKeyFileUri: Uri? = null
|
|
||||||
private var mHardwareKey: HardwareKey? = null
|
|
||||||
|
|
||||||
private lateinit var rootView: View
|
|
||||||
|
|
||||||
private lateinit var passwordCheckBox: CompoundButton
|
|
||||||
private lateinit var passwordEditView: PasswordEditView
|
|
||||||
private lateinit var passwordRepeatTextInputLayout: TextInputLayout
|
|
||||||
private lateinit var passwordRepeatView: TextView
|
|
||||||
|
|
||||||
private lateinit var keyFileCheckBox: CompoundButton
|
|
||||||
private lateinit var keyFileGenerateButton: View
|
|
||||||
private lateinit var keyFileSelectionView: KeyFileSelectionView
|
|
||||||
|
|
||||||
private lateinit var hardwareKeyCheckBox: CompoundButton
|
|
||||||
private lateinit var hardwareKeySelectionView: HardwareKeySelectionView
|
|
||||||
|
|
||||||
private var mListener: AssignMainCredentialDialogListener? = null
|
|
||||||
|
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
|
||||||
private var mPasswordEntropyCalculator: PasswordEntropy? = null
|
|
||||||
|
|
||||||
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
|
||||||
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
|
||||||
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
|
|
||||||
|
|
||||||
private var mAllowNoMasterKey: Boolean = false
|
|
||||||
|
|
||||||
private val passwordTextWatcher = object : TextWatcher {
|
|
||||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
|
||||||
|
|
||||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
|
||||||
|
|
||||||
override fun afterTextChanged(editable: Editable) {
|
|
||||||
passwordCheckBox.isChecked = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AssignMainCredentialDialogListener {
|
|
||||||
fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential)
|
|
||||||
fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttach(activity: Context) {
|
|
||||||
super.onAttach(activity)
|
|
||||||
try {
|
|
||||||
mListener = activity as AssignMainCredentialDialogListener
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
throw ClassCastException(activity.toString()
|
|
||||||
+ " must implement " + AssignMainCredentialDialogListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetach() {
|
|
||||||
mListener = null
|
|
||||||
mEmptyPasswordConfirmationDialog?.dismiss()
|
|
||||||
mEmptyPasswordConfirmationDialog = null
|
|
||||||
mNoKeyConfirmationDialog?.dismiss()
|
|
||||||
mNoKeyConfirmationDialog = null
|
|
||||||
mEmptyKeyFileConfirmationDialog?.dismiss()
|
|
||||||
mEmptyKeyFileConfirmationDialog = null
|
|
||||||
super.onDetach()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
// Create the password entropy object
|
|
||||||
mPasswordEntropyCalculator = PasswordEntropy()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
activity?.let { activity ->
|
|
||||||
|
|
||||||
arguments?.apply {
|
|
||||||
if (containsKey(ALLOW_NO_MASTER_KEY_ARG))
|
|
||||||
mAllowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
|
||||||
val inflater = activity.layoutInflater
|
|
||||||
|
|
||||||
rootView = inflater.inflate(R.layout.fragment_set_main_credential, null)
|
|
||||||
builder.setView(rootView)
|
|
||||||
// Add action buttons
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
|
||||||
|
|
||||||
rootView.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
|
|
||||||
activity.openUrl(R.string.credentials_explanation_url)
|
|
||||||
}
|
|
||||||
|
|
||||||
passwordCheckBox = rootView.findViewById(R.id.password_checkbox)
|
|
||||||
passwordEditView = rootView.findViewById(R.id.password_view)
|
|
||||||
passwordRepeatTextInputLayout = rootView.findViewById(R.id.password_repeat_input_layout)
|
|
||||||
passwordRepeatView = rootView.findViewById(R.id.password_confirmation)
|
|
||||||
passwordRepeatView.applyFontVisibility()
|
|
||||||
|
|
||||||
keyFileCheckBox = rootView.findViewById(R.id.keyfile_checkbox)
|
|
||||||
keyFileGenerateButton = rootView.findViewById(R.id.keyfile_generate)
|
|
||||||
keyFileSelectionView = rootView.findViewById(R.id.keyfile_selection)
|
|
||||||
|
|
||||||
hardwareKeyCheckBox = rootView.findViewById(R.id.hardware_key_checkbox)
|
|
||||||
hardwareKeySelectionView = rootView.findViewById(R.id.hardware_key_selection)
|
|
||||||
|
|
||||||
mExternalFileHelper = ExternalFileHelper(this)
|
|
||||||
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
|
|
||||||
createdFileUri?.let { uri ->
|
|
||||||
createKeyFile(uri)
|
|
||||||
keyFileSelectionView.error = null
|
|
||||||
keyFileCheckBox.isChecked = true
|
|
||||||
keyFileSelectionView.uri = uri
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
|
||||||
uri?.let { pathUri ->
|
|
||||||
pathUri.getDocumentFile(requireContext())?.length()?.let { lengthFile ->
|
|
||||||
keyFileSelectionView.error = null
|
|
||||||
keyFileCheckBox.isChecked = true
|
|
||||||
keyFileSelectionView.uri = pathUri
|
|
||||||
showLengthKeyFileConfirmationDialog(lengthFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
keyFileGenerateButton.setOnClickListener {
|
|
||||||
mExternalFileHelper?.createDocument(DEFAULT_KEYFILE_NAME)
|
|
||||||
}
|
|
||||||
keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper)
|
|
||||||
|
|
||||||
hardwareKeySelectionView.selectionListener = { hardwareKey ->
|
|
||||||
hardwareKeyCheckBox.isChecked = true
|
|
||||||
hardwareKeySelectionView.error =
|
|
||||||
if (!HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)) {
|
|
||||||
// show hardware driver dialog if required
|
|
||||||
getString(R.string.error_driver_required, hardwareKey.toString())
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val dialog = builder.create()
|
|
||||||
dialog.setOnShowListener { dialog1 ->
|
|
||||||
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
|
|
||||||
positiveButton.setOnClickListener {
|
|
||||||
|
|
||||||
mMasterPassword = ""
|
|
||||||
mKeyFileUri = null
|
|
||||||
mHardwareKey = null
|
|
||||||
|
|
||||||
approveMainCredential()
|
|
||||||
}
|
|
||||||
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
|
|
||||||
negativeButton.setOnClickListener {
|
|
||||||
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dialog
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createKeyFile(uri: Uri) {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
activity?.contentResolver?.openOutputStream(uri)?.use { outputStream ->
|
|
||||||
val randomBytes = ByteArray(DEFAULT_KEYFILE_SIZE)
|
|
||||||
SecureRandom().nextBytes(randomBytes)
|
|
||||||
outputStream.write(randomBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun approveMainCredential() {
|
|
||||||
val errorPassword = verifyPassword()
|
|
||||||
val errorKeyFile = verifyKeyFile()
|
|
||||||
val errorHardwareKey = verifyHardwareKey()
|
|
||||||
// Check all to fill error
|
|
||||||
var error = errorPassword || errorKeyFile || errorHardwareKey
|
|
||||||
val hardwareKey = hardwareKeySelectionView.hardwareKey
|
|
||||||
if (!error
|
|
||||||
&& (!passwordCheckBox.isChecked)
|
|
||||||
&& (!keyFileCheckBox.isChecked)
|
|
||||||
&& (!hardwareKeyCheckBox.isChecked)
|
|
||||||
) {
|
|
||||||
error = true
|
|
||||||
if (mAllowNoMasterKey) {
|
|
||||||
// show no key dialog if required
|
|
||||||
showNoKeyConfirmationDialog()
|
|
||||||
} else {
|
|
||||||
passwordRepeatTextInputLayout.error =
|
|
||||||
getString(R.string.error_disallow_no_credentials)
|
|
||||||
}
|
|
||||||
} else if (!error
|
|
||||||
&& mMasterPassword.isNullOrEmpty()
|
|
||||||
&& !keyFileCheckBox.isChecked
|
|
||||||
&& !hardwareKeyCheckBox.isChecked
|
|
||||||
) {
|
|
||||||
// show empty password dialog if required
|
|
||||||
error = true
|
|
||||||
showEmptyPasswordConfirmationDialog()
|
|
||||||
} else if (!error
|
|
||||||
&& hardwareKey != null
|
|
||||||
&& !HardwareKeyActivity.isHardwareKeyAvailable(
|
|
||||||
requireActivity(), hardwareKey, false)
|
|
||||||
) {
|
|
||||||
// show hardware driver dialog if required
|
|
||||||
error = true
|
|
||||||
hardwareKeySelectionView.error =
|
|
||||||
getString(R.string.error_driver_required, hardwareKey.toString())
|
|
||||||
}
|
|
||||||
if (!error) {
|
|
||||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun verifyPassword(): Boolean {
|
|
||||||
var error = false
|
|
||||||
passwordRepeatTextInputLayout.error = null
|
|
||||||
if (passwordCheckBox.isChecked) {
|
|
||||||
mMasterPassword = passwordEditView.passwordString
|
|
||||||
val confPassword = passwordRepeatView.text.toString()
|
|
||||||
|
|
||||||
// Verify that passwords match
|
|
||||||
if (mMasterPassword != confPassword) {
|
|
||||||
error = true
|
|
||||||
// Passwords do not match
|
|
||||||
passwordRepeatTextInputLayout.error = getString(R.string.error_pass_match)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return error
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun verifyKeyFile(): Boolean {
|
|
||||||
var error = false
|
|
||||||
keyFileSelectionView.error = null
|
|
||||||
if (keyFileCheckBox.isChecked) {
|
|
||||||
keyFileSelectionView.uri?.let { uri ->
|
|
||||||
mKeyFileUri = uri
|
|
||||||
} ?: run {
|
|
||||||
error = true
|
|
||||||
keyFileSelectionView.error = getString(R.string.error_nokeyfile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return error
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun verifyHardwareKey(): Boolean {
|
|
||||||
var error = false
|
|
||||||
hardwareKeySelectionView.error = null
|
|
||||||
if (hardwareKeyCheckBox.isChecked) {
|
|
||||||
hardwareKeySelectionView.hardwareKey?.let { hardwareKey ->
|
|
||||||
mHardwareKey = hardwareKey
|
|
||||||
} ?: run {
|
|
||||||
error = true
|
|
||||||
hardwareKeySelectionView.error = getString(R.string.error_no_hardware_key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return error
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun retrieveMainCredential(): MainCredential {
|
|
||||||
val masterPassword = if (passwordCheckBox.isChecked) mMasterPassword else null
|
|
||||||
val keyFileUri = if (keyFileCheckBox.isChecked) mKeyFileUri else null
|
|
||||||
val hardwareKey = if (hardwareKeyCheckBox.isChecked) mHardwareKey else null
|
|
||||||
return MainCredential(masterPassword, keyFileUri, hardwareKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
// To check checkboxes if a text is present
|
|
||||||
passwordEditView.addTextChangedListener(passwordTextWatcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
|
|
||||||
passwordEditView.removeTextChangedListener(passwordTextWatcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showEmptyPasswordConfirmationDialog() {
|
|
||||||
activity?.let {
|
|
||||||
val builder = AlertDialog.Builder(it)
|
|
||||||
builder.setMessage(R.string.warning_empty_password)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
|
||||||
this@SetMainCredentialDialogFragment.dismiss()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
|
||||||
mEmptyPasswordConfirmationDialog = builder.create()
|
|
||||||
mEmptyPasswordConfirmationDialog?.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showNoKeyConfirmationDialog() {
|
|
||||||
activity?.let {
|
|
||||||
val builder = AlertDialog.Builder(it)
|
|
||||||
builder.setMessage(R.string.warning_no_encryption_key)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
|
||||||
this@SetMainCredentialDialogFragment.dismiss()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
|
||||||
mNoKeyConfirmationDialog = builder.create()
|
|
||||||
mNoKeyConfirmationDialog?.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLengthKeyFileConfirmationDialog(length: Long) {
|
|
||||||
activity?.let {
|
|
||||||
val builder = AlertDialog.Builder(it)
|
|
||||||
builder.setMessage(SpannableStringBuilder().apply {
|
|
||||||
append(getString(R.string.warning_empty_keyfile_explanation))
|
|
||||||
var warning = false
|
|
||||||
if (length <= 0L) {
|
|
||||||
warning = true
|
|
||||||
append("\n\n")
|
|
||||||
append(getString(R.string.warning_empty_keyfile))
|
|
||||||
} else if (length > 10485760L) {
|
|
||||||
warning = true
|
|
||||||
append("\n\n")
|
|
||||||
append(getString(R.string.warning_large_keyfile))
|
|
||||||
}
|
|
||||||
if (warning) {
|
|
||||||
append("\n\n")
|
|
||||||
append(getString(R.string.warning_sure_add_file))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
keyFileCheckBox.isChecked = false
|
|
||||||
keyFileSelectionView.uri = null
|
|
||||||
}
|
|
||||||
mEmptyKeyFileConfirmationDialog = builder.create()
|
|
||||||
mEmptyKeyFileConfirmationDialog?.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
|
|
||||||
private const val DEFAULT_KEYFILE_NAME = "keyfile.bin"
|
|
||||||
private const val DEFAULT_KEYFILE_SIZE = 128
|
|
||||||
|
|
||||||
fun getInstance(allowNoMasterKey: Boolean): SetMainCredentialDialogFragment {
|
|
||||||
val fragment = SetMainCredentialDialogFragment()
|
|
||||||
val args = Bundle()
|
|
||||||
args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey)
|
|
||||||
fragment.arguments = args
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,13 +29,11 @@ import android.view.MotionEvent
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.AdapterView
|
import android.widget.*
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.Spinner
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.model.OtpModel
|
import com.kunzisoft.keepass.model.OtpModel
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
@@ -44,17 +42,14 @@ import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_OTP_DIGITS
|
|||||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_TOTP_PERIOD
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_TOTP_PERIOD
|
||||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_HOTP_COUNTER
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_HOTP_COUNTER
|
||||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_DIGITS
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_DIGITS
|
||||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_SECRET
|
|
||||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
|
||||||
import com.kunzisoft.keepass.otp.OtpTokenType
|
import com.kunzisoft.keepass.otp.OtpTokenType
|
||||||
import com.kunzisoft.keepass.otp.OtpType
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
import com.kunzisoft.keepass.otp.TokenCalculator
|
import com.kunzisoft.keepass.otp.TokenCalculator
|
||||||
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
import java.util.*
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class SetOTPDialogFragment : DatabaseDialogFragment() {
|
class SetOTPDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
private var mCreateOTPElementListener: CreateOtpListener? = null
|
private var mCreateOTPElementListener: CreateOtpListener? = null
|
||||||
|
|
||||||
@@ -85,15 +80,11 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
|||||||
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
|
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
|
||||||
if (!isFocus)
|
if (!isFocus)
|
||||||
mManualEvent = true
|
mManualEvent = true
|
||||||
else
|
|
||||||
resetAppTimeout()
|
|
||||||
}
|
}
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
private var mOnTouchListener = View.OnTouchListener { _, event ->
|
private var mOnTouchListener = View.OnTouchListener { _, event ->
|
||||||
when (event.action) {
|
when (event.action) {
|
||||||
MotionEvent.ACTION_DOWN -> {
|
MotionEvent.ACTION_DOWN -> {
|
||||||
mManualEvent = true
|
mManualEvent = true
|
||||||
resetAppTimeout()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
@@ -104,10 +95,6 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
|||||||
private var mPeriodWellFormed = false
|
private var mPeriodWellFormed = false
|
||||||
private var mDigitsWellFormed = false
|
private var mDigitsWellFormed = false
|
||||||
|
|
||||||
override fun overrideTimeoutTouchAndFocusEvents(): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
// Verify that the host activity implements the callback interface
|
// Verify that the host activity implements the callback interface
|
||||||
@@ -132,14 +119,14 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
|||||||
// Retrieve OTP model from instance state
|
// Retrieve OTP model from instance state
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
if (savedInstanceState.containsKey(KEY_OTP)) {
|
if (savedInstanceState.containsKey(KEY_OTP)) {
|
||||||
savedInstanceState.getParcelableCompat<OtpModel>(KEY_OTP)?.let { otpModel ->
|
savedInstanceState.getParcelable<OtpModel>(KEY_OTP)?.let { otpModel ->
|
||||||
mOtpElement = OtpElement(otpModel)
|
mOtpElement = OtpElement(otpModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
if (containsKey(KEY_OTP)) {
|
if (containsKey(KEY_OTP)) {
|
||||||
getParcelableCompat<OtpModel>(KEY_OTP)?.let { otpModel ->
|
getParcelable<OtpModel?>(KEY_OTP)?.let { otpModel ->
|
||||||
mOtpElement = OtpElement(otpModel)
|
mOtpElement = OtpElement(otpModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,10 +197,9 @@ 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 full version
|
// Proprietary only on closed and full version
|
||||||
mTotpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
|
mTotpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
|
||||||
activity.isContributingUser()
|
BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION)
|
||||||
)
|
|
||||||
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)
|
||||||
@@ -229,9 +215,6 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
otpAlgorithmSpinner?.adapter = otpAlgorithmAdapter
|
otpAlgorithmSpinner?.adapter = otpAlgorithmAdapter
|
||||||
|
|
||||||
// Ensure that the UX does not prevent user from hiding/unhiding text
|
|
||||||
otpSecretContainer?.errorIconDrawable = null
|
|
||||||
|
|
||||||
// Set the default value of OTP element
|
// Set the default value of OTP element
|
||||||
upgradeType()
|
upgradeType()
|
||||||
upgradeTokenType()
|
upgradeTokenType()
|
||||||
@@ -242,16 +225,13 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
|||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
builder.apply {
|
builder.apply {
|
||||||
setView(root)
|
setView(root)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) {_, _ -> }
|
||||||
resetAppTimeout()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
resetAppTimeout()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
root?.findViewById<View>(R.id.otp_information)?.setOnClickListener {
|
root?.findViewById<View>(R.id.otp_information)?.setOnClickListener {
|
||||||
activity.openUrl(R.string.otp_explanation_url)
|
UriUtil.gotoUrl(activity, R.string.otp_explanation_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
@@ -318,16 +298,11 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
|||||||
otpSecretTextView?.addTextChangedListener(object: TextWatcher {
|
otpSecretTextView?.addTextChangedListener(object: TextWatcher {
|
||||||
override fun afterTextChanged(s: Editable?) {
|
override fun afterTextChanged(s: Editable?) {
|
||||||
s?.toString()?.let { userString ->
|
s?.toString()?.let { userString ->
|
||||||
if (userString.length >= MIN_OTP_SECRET) {
|
try {
|
||||||
try {
|
mOtpElement.setBase32Secret(userString.toUpperCase(Locale.ENGLISH))
|
||||||
mOtpElement.setBase32Secret(userString.uppercase(Locale.ENGLISH))
|
otpSecretContainer?.error = null
|
||||||
otpSecretContainer?.error = null
|
} catch (exception: Exception) {
|
||||||
} catch (exception: Exception) {
|
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
|
||||||
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
otpSecretContainer?.error = getString(R.string.error_otp_secret_length,
|
|
||||||
MIN_OTP_SECRET)
|
|
||||||
}
|
}
|
||||||
mSecretWellFormed = otpSecretContainer?.error == null
|
mSecretWellFormed = otpSecretContainer?.error == null
|
||||||
}
|
}
|
||||||
@@ -484,4 +459,4 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,15 +22,16 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.annotation.IdRes
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.RadioGroup
|
import android.widget.RadioGroup
|
||||||
import androidx.annotation.IdRes
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||||
|
|
||||||
class SortDialogFragment : DatabaseDialogFragment() {
|
class SortDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
private var mListener: SortSelectionListener? = null
|
private var mListener: SortSelectionListener? = null
|
||||||
|
|
||||||
@@ -178,12 +179,19 @@ class SortDialogFragment : DatabaseDialogFragment() {
|
|||||||
|
|
||||||
fun getInstance(sortNodeEnum: SortNodeEnum,
|
fun getInstance(sortNodeEnum: SortNodeEnum,
|
||||||
ascending: Boolean,
|
ascending: Boolean,
|
||||||
groupsBefore: Boolean,
|
groupsBefore: Boolean): SortDialogFragment {
|
||||||
recycleBinBottom: Boolean?): SortDialogFragment {
|
|
||||||
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
|
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
|
||||||
recycleBinBottom?.let {
|
val fragment = SortDialogFragment()
|
||||||
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
|
fragment.arguments = bundle
|
||||||
}
|
return fragment
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getInstance(sortNodeEnum: SortNodeEnum,
|
||||||
|
ascending: Boolean,
|
||||||
|
groupsBefore: Boolean,
|
||||||
|
recycleBinBottom: Boolean): SortDialogFragment {
|
||||||
|
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
|
||||||
|
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
|
||||||
val fragment = SortDialogFragment()
|
val fragment = SortDialogFragment()
|
||||||
fragment.arguments = bundle
|
fragment.arguments = bundle
|
||||||
return fragment
|
return fragment
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.DatePickerDialog
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.app.TimePickerDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.format.DateFormat
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
|
||||||
|
class TimePickerFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private var defaultHour: Int = 0
|
||||||
|
private var defaultMinute: Int = 0
|
||||||
|
|
||||||
|
private var mListener: TimePickerDialog.OnTimeSetListener? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
try {
|
||||||
|
mListener = context as TimePickerDialog.OnTimeSetListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
// Create a new instance of DatePickerDialog and return it
|
||||||
|
return context?.let {
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(DEFAULT_HOUR_BUNDLE_KEY))
|
||||||
|
defaultHour = getInt(DEFAULT_HOUR_BUNDLE_KEY)
|
||||||
|
if (containsKey(DEFAULT_MINUTE_BUNDLE_KEY))
|
||||||
|
defaultMinute = getInt(DEFAULT_MINUTE_BUNDLE_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
TimePickerDialog(it, mListener, defaultHour, defaultMinute, DateFormat.is24HourFormat(activity))
|
||||||
|
} ?: super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val DEFAULT_HOUR_BUNDLE_KEY = "DEFAULT_HOUR_BUNDLE_KEY"
|
||||||
|
private const val DEFAULT_MINUTE_BUNDLE_KEY = "DEFAULT_MINUTE_BUNDLE_KEY"
|
||||||
|
|
||||||
|
fun getInstance(defaultHour: Int,
|
||||||
|
defaultMinute: Int): TimePickerFragment {
|
||||||
|
return TimePickerFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putInt(DEFAULT_HOUR_BUNDLE_KEY, defaultHour)
|
||||||
|
putInt(DEFAULT_MINUTE_BUNDLE_KEY, defaultMinute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||