Compare commits
396 Commits
bug-2378-p
...
update-lib
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
830494851d | ||
|
|
6e30dd35ee | ||
|
|
dad9b0e53f | ||
|
|
b2b0b8ddc9 | ||
|
|
9494f28acf | ||
|
|
a98cd33cff | ||
|
|
65092dcccf | ||
|
|
e464c59796 | ||
|
|
7dcccf1105 | ||
|
|
8d354bb3f0 | ||
|
|
5882263ee8 | ||
|
|
38e305bb4a | ||
|
|
177b1fc9a3 | ||
|
|
8bde5ed262 | ||
|
|
247b37262d | ||
|
|
84230b32ef | ||
|
|
a54444e919 | ||
|
|
4b55e2ce8b | ||
|
|
5ae718c58e | ||
|
|
669022ddf0 | ||
|
|
f0fd21a6ec | ||
|
|
73a8ec27d4 | ||
|
|
51902c9bc4 | ||
|
|
32d3abf5d6 | ||
|
|
543eeb69ef | ||
|
|
ec6e1593b0 | ||
|
|
ee27bea111 | ||
|
|
de93b12877 | ||
|
|
71d2a332c7 | ||
|
|
37d1481658 | ||
|
|
a8cc1ad66d | ||
|
|
3056ee9d45 | ||
|
|
0ec5996c8d | ||
|
|
01666d8402 | ||
|
|
422fa01ba1 | ||
|
|
ca9c19e8a9 | ||
|
|
093f7683b8 | ||
|
|
d4cf908f4a | ||
|
|
55d62144fb | ||
|
|
c181565aaa | ||
|
|
c0c4524562 | ||
|
|
eac1803084 | ||
|
|
58904f1166 | ||
|
|
54eb1baee2 | ||
|
|
ada01924ba | ||
|
|
e38e7df221 | ||
|
|
fb4ab84ceb | ||
|
|
4475fac51e | ||
|
|
13ef4ca9ff | ||
|
|
f297ebcd40 | ||
|
|
a5ef4ccc7a | ||
|
|
dfd9c32251 | ||
|
|
5397a1c88f | ||
|
|
fdcd4321e0 | ||
|
|
11013791ef | ||
|
|
6958a2d189 | ||
|
|
94ec8cf1ac | ||
|
|
63631fa81f | ||
|
|
107d9c6235 | ||
|
|
8e4ee4f588 | ||
|
|
f0a06faae1 | ||
|
|
99db263833 | ||
|
|
e2e42cd177 | ||
|
|
a376c6ee0b | ||
|
|
0326e02ddd | ||
|
|
d75482f3bd | ||
|
|
bca0d042a1 | ||
|
|
29eaf5f205 | ||
|
|
78da5e2973 | ||
|
|
20541618f9 | ||
|
|
e459d280f2 | ||
|
|
705e2e4a86 | ||
|
|
cf9b368afc | ||
|
|
f1c6a5365c | ||
|
|
6a61bf6364 | ||
|
|
814571c746 | ||
|
|
fc5587260f | ||
|
|
674ba7bd71 | ||
|
|
556f82f786 | ||
|
|
58c5c5882b | ||
|
|
a706571e66 | ||
|
|
3997b21aec | ||
|
|
c354612369 | ||
|
|
4fea731c87 | ||
|
|
e189776ba9 | ||
|
|
31255f0c52 | ||
|
|
059280efd0 | ||
|
|
5edc070aa8 | ||
|
|
95843b1134 | ||
|
|
d2ea9b18a8 | ||
|
|
828425ab0e | ||
|
|
4395f422b3 | ||
|
|
be2c28811c | ||
|
|
fcc4d44786 | ||
|
|
337e6324ff | ||
|
|
310143c612 | ||
|
|
49cb33a4da | ||
|
|
c934755e1c | ||
|
|
64355a3da7 | ||
|
|
9579f3bf51 | ||
|
|
674ae26bd7 | ||
|
|
d2778e8496 | ||
|
|
cc19e6f326 | ||
|
|
dd7a2718c9 | ||
|
|
9c50d2d98a | ||
|
|
8ee13acdde | ||
|
|
aeda304919 | ||
|
|
9f08e0039b | ||
|
|
dfd101da77 | ||
|
|
f4b5eee171 | ||
|
|
98e31942e1 | ||
|
|
385aad8fb0 | ||
|
|
3e383d50f8 | ||
|
|
8c089a8711 | ||
|
|
098123787d | ||
|
|
a22b8474c3 | ||
|
|
8fc4607a34 | ||
|
|
4b56405960 | ||
|
|
c4b41001b3 | ||
|
|
cb51be349e | ||
|
|
13170bb88c | ||
|
|
88a20d947c | ||
|
|
4fcc2625c0 | ||
|
|
324cf74a2b | ||
|
|
e3d14221f9 | ||
|
|
7d0a43397a | ||
|
|
a6d1b26479 | ||
|
|
79ad753218 | ||
|
|
24ee49ea9f | ||
|
|
ab6e8e3685 | ||
|
|
81e8820732 | ||
|
|
93c72ee04e | ||
|
|
8a53357e3d | ||
|
|
c12ae13077 | ||
|
|
071fc3fd51 | ||
|
|
ad6ced3aad | ||
|
|
6ef8b8fc3b | ||
|
|
825793f385 | ||
|
|
bd6af10fd5 | ||
|
|
2e9400cf4d | ||
|
|
c719043159 | ||
|
|
c6e32937ce | ||
|
|
081b77c2bd | ||
|
|
a950298c11 | ||
|
|
eafd3bb702 | ||
|
|
0e53f91d01 | ||
|
|
96156bf8b9 | ||
|
|
8efc1f3c1f | ||
|
|
67d20124ff | ||
|
|
b93739926d | ||
|
|
4afac75bb4 | ||
|
|
cea41d1446 | ||
|
|
dd5b744bfb | ||
|
|
47cbe5b0ab | ||
|
|
99950e3c93 | ||
|
|
df8f375b59 | ||
|
|
fdc213bfc1 | ||
|
|
428b008017 | ||
|
|
3d4a0a79f9 | ||
|
|
008e55598c | ||
|
|
7c3832830e | ||
|
|
c3e234da25 | ||
|
|
6ba0b29c77 | ||
|
|
b16f747913 | ||
|
|
35ac1cf642 | ||
|
|
5d2da784b9 | ||
|
|
86b225034c | ||
|
|
727cf74201 | ||
|
|
6f8ae7be34 | ||
|
|
a84d26b151 | ||
|
|
6f7419e38a | ||
|
|
bdb11f4873 | ||
|
|
87603cd9c1 | ||
|
|
e6cec96504 | ||
|
|
fb478af6c7 | ||
|
|
9991964c9b | ||
|
|
d761f07fc9 | ||
|
|
b18515dd8c | ||
|
|
2677cae5e6 | ||
|
|
cb832c412f | ||
|
|
97018b15f7 | ||
|
|
6b06d4ba8d | ||
|
|
902fc6f6d3 | ||
|
|
d4fd8db455 | ||
|
|
9f1be03dc4 | ||
|
|
7b863e115f | ||
|
|
06fa5a5fcd | ||
|
|
b1837468d7 | ||
|
|
0ffe6cda16 | ||
|
|
0ba1e946d1 | ||
|
|
ba2890cc80 | ||
|
|
ee9750e689 | ||
|
|
3f358fed38 | ||
|
|
4eb7b4519e | ||
|
|
9b61f651c4 | ||
|
|
c716fa0c12 | ||
|
|
013d69b520 | ||
|
|
fec2875e6a | ||
|
|
1a1036f7b8 | ||
|
|
b9fcf8deda | ||
|
|
a9a88dbdbe | ||
|
|
d3f505fb55 | ||
|
|
da10ebd2f4 | ||
|
|
daeee50e09 | ||
|
|
f602367a6c | ||
|
|
db2ad49f36 | ||
|
|
a782843b29 | ||
|
|
e35babb8eb | ||
|
|
e6b296c0b9 | ||
|
|
44692afa98 | ||
|
|
491912a6ab | ||
|
|
5cb02e88bf | ||
|
|
69ce92a7b7 | ||
|
|
a09e2656be | ||
|
|
445923e12c | ||
|
|
6499c97206 | ||
|
|
41ef1900a1 | ||
|
|
290f61d114 | ||
|
|
11f45c61e8 | ||
|
|
ac6df5d10f | ||
|
|
206ab3ac42 | ||
|
|
33847deb00 | ||
|
|
baf9a29646 | ||
|
|
30d45e086c | ||
|
|
66166e44a0 | ||
|
|
8ace491d84 | ||
|
|
39deef4053 | ||
|
|
1faa0b06bd | ||
|
|
1eb1e1cb2b | ||
|
|
d551969b04 | ||
|
|
0f0c1ddbfd | ||
|
|
b46f2984a3 | ||
|
|
cce1e2794e | ||
|
|
c19b8d2238 | ||
|
|
141d2f3ddb | ||
|
|
3d2ae980b7 | ||
|
|
a8f4fcde7b | ||
|
|
add8b2dad6 | ||
|
|
50074d547f | ||
|
|
e6b425a30e | ||
|
|
9af9d34d87 | ||
|
|
4fee92f591 | ||
|
|
1b658f1c39 | ||
|
|
4dbd33ba97 | ||
|
|
f8f18152c3 | ||
|
|
dbf5e46e94 | ||
|
|
a23c1a2360 | ||
|
|
d2bd91ba6a | ||
|
|
95352ef0ee | ||
|
|
24b8c27d26 | ||
|
|
191b90d974 | ||
|
|
21db4b612b | ||
|
|
a23101b812 | ||
|
|
8042470488 | ||
|
|
4bbec4367f | ||
|
|
4b583cc0c0 | ||
|
|
cd07de56df | ||
|
|
962c4dbf63 | ||
|
|
09b56d85cf | ||
|
|
8281888608 | ||
|
|
44d9456e20 | ||
|
|
7b01e4494f | ||
|
|
fda68a1114 | ||
|
|
4849c089b3 | ||
|
|
580668c5cb | ||
|
|
da8f1122e8 | ||
|
|
7d44518ac7 | ||
|
|
1a2c1267c4 | ||
|
|
85e0fe487f | ||
|
|
fa9a9f2602 | ||
|
|
5c7d626f4b | ||
|
|
b3ce9c64b1 | ||
|
|
dc809941e8 | ||
|
|
b7d69c33c8 | ||
|
|
de765f3451 | ||
|
|
1581d79666 | ||
|
|
297fa267e5 | ||
|
|
77e2d67b6c | ||
|
|
a3ba2d8367 | ||
|
|
48d59aa0f6 | ||
|
|
cff6595b79 | ||
|
|
798f633af7 | ||
|
|
f5681c4e62 | ||
|
|
690de2761c | ||
|
|
92238436d5 | ||
|
|
9282e80938 | ||
|
|
588e203442 | ||
|
|
7e96055e0b | ||
|
|
c90d623d15 | ||
|
|
86a03d8b9a | ||
|
|
17f7d1b8eb | ||
|
|
d3fecaf4e3 | ||
|
|
03dee4f262 | ||
|
|
2b502df566 | ||
|
|
9ea064108c | ||
|
|
682736d119 | ||
|
|
150bd336d8 | ||
|
|
d13ee3d2ca | ||
|
|
99b5df4c94 | ||
|
|
9a70442d69 | ||
|
|
cd04050e57 | ||
|
|
c13bb15fc0 | ||
|
|
84939c70e1 | ||
|
|
2b108d9818 | ||
|
|
06f338fdd5 | ||
|
|
fa5e8c1656 | ||
|
|
3652e2ee25 | ||
|
|
e8f3eb1bc8 | ||
|
|
233f612479 | ||
|
|
dc1e790ab5 | ||
|
|
bab77538c9 | ||
|
|
09165af0a8 | ||
|
|
4502d3d2bf | ||
|
|
eb03d448d8 | ||
|
|
7798ec8454 | ||
|
|
7424bb324f | ||
|
|
b18432add6 | ||
|
|
e9a66d688c | ||
|
|
d9add0d5f6 | ||
|
|
aed00420fc | ||
|
|
8dc546e640 | ||
|
|
c8f3d5f3e2 | ||
|
|
1f3786189b | ||
|
|
d7bdde0585 | ||
|
|
f213f05477 | ||
|
|
cb73144da7 | ||
|
|
689a1710c4 | ||
|
|
da116bbb4d | ||
|
|
2fd76ad28f | ||
|
|
bdc7bf9cf6 | ||
|
|
b3ef4f817a | ||
|
|
3fb2f2e858 | ||
|
|
d8f60aa7f1 | ||
|
|
31f3a30a54 | ||
|
|
474b90f331 | ||
|
|
fa0a52b328 | ||
|
|
ccb6ece463 | ||
|
|
ffa33ed190 | ||
|
|
c4923c57bf | ||
|
|
2602bf7bee | ||
|
|
7df86fd134 | ||
|
|
cf2f57b372 | ||
|
|
7c2500af63 | ||
|
|
748a71bc03 | ||
|
|
c8abb4d76a | ||
|
|
18f81e6927 | ||
|
|
b8c094554a | ||
|
|
1c6831bb78 | ||
|
|
a5e7bbc081 | ||
|
|
be2218afcc | ||
|
|
32c1d2a379 | ||
|
|
9c7182f85a | ||
|
|
31abf68031 | ||
|
|
489ed8e2b4 | ||
|
|
d63e11b307 | ||
|
|
0e9da69f47 | ||
|
|
18ecfd5396 | ||
|
|
0fef5f0f8c | ||
|
|
83529dd3b5 | ||
|
|
9204c4ca8f | ||
|
|
46fdba1bfa | ||
|
|
006f5497e5 | ||
|
|
da3665c25b | ||
|
|
464fe43323 | ||
|
|
bded2394bb | ||
|
|
0fe2ca8238 | ||
|
|
ae33ca219f | ||
|
|
fb0f83c37a | ||
|
|
da5533ef3b | ||
|
|
681dfb6ded | ||
|
|
20f334f0d3 | ||
|
|
d8268d4f0f | ||
|
|
325e8a8e32 | ||
|
|
7e9e91da05 | ||
|
|
80eaf39f04 | ||
|
|
ddffdb48aa | ||
|
|
5a406fe5df | ||
|
|
4e2603ae27 | ||
|
|
bcf980eed5 | ||
|
|
05c94a3af8 | ||
|
|
3526aa1889 | ||
|
|
72a3b55341 | ||
|
|
b11d5e667e | ||
|
|
93a4529fe9 | ||
|
|
7582274903 | ||
|
|
158349c005 | ||
|
|
2fffe5988c | ||
|
|
14f7e17fa4 | ||
|
|
05acba4309 | ||
|
|
542984ca2f | ||
|
|
f8746f69f8 | ||
|
|
53913e66ab | ||
|
|
5e265d1816 | ||
|
|
83e77b2a31 | ||
|
|
893cf2b3c8 | ||
|
|
15b3b76b27 |
67
.github/workflows/build.yml
vendored
67
.github/workflows/build.yml
vendored
@@ -1,6 +1,10 @@
|
|||||||
name: Build keepass2android app
|
name: Build keepass2android app
|
||||||
|
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# macos:
|
# macos:
|
||||||
@@ -10,16 +14,15 @@ jobs:
|
|||||||
# runs-on: macos-12
|
# runs-on: macos-12
|
||||||
|
|
||||||
# steps:
|
# steps:
|
||||||
# - uses: actions/checkout@v3
|
# - uses: actions/checkout@v4
|
||||||
|
# with:
|
||||||
# - name: Fetch submodules
|
# submodules: true
|
||||||
# run: git submodule init && git submodule update
|
|
||||||
|
|
||||||
# - name: Setup Gradle
|
# - name: Setup Gradle
|
||||||
# uses: gradle/gradle-build-action@v2
|
# uses: gradle/actions/setup-gradle@v3
|
||||||
|
|
||||||
# - name: Cache NuGet packages
|
# - name: Cache NuGet packages
|
||||||
# uses: actions/cache@v3
|
# uses: actions/cache@v4
|
||||||
# with:
|
# with:
|
||||||
# path: ~/.nuget/packages
|
# path: ~/.nuget/packages
|
||||||
# key: ${{ runner.os }}-nuget-${{ hashFiles('src/**/*.csproj', 'src/**/packages.config') }}
|
# key: ${{ runner.os }}-nuget-${{ hashFiles('src/**/*.csproj', 'src/**/packages.config') }}
|
||||||
@@ -52,7 +55,7 @@ jobs:
|
|||||||
# # $VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=11.2
|
# # $VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=11.2
|
||||||
|
|
||||||
# - name: Switch to JDK-11
|
# - name: Switch to JDK-11
|
||||||
# uses: actions/setup-java@v3
|
# uses: actions/setup-java@v4
|
||||||
# with:
|
# with:
|
||||||
# java-version: '11'
|
# java-version: '11'
|
||||||
# distribution: 'temurin'
|
# distribution: 'temurin'
|
||||||
@@ -82,7 +85,7 @@ jobs:
|
|||||||
# make apk Flavor=Net
|
# make apk Flavor=Net
|
||||||
|
|
||||||
# - name: Archive production artifacts (net)
|
# - name: Archive production artifacts (net)
|
||||||
# uses: actions/upload-artifact@v3
|
# uses: actions/upload-artifact@v4
|
||||||
# with:
|
# with:
|
||||||
# name: signed APK ('net' built on ${{ github.job }})
|
# name: signed APK ('net' built on ${{ github.job }})
|
||||||
# path: |
|
# path: |
|
||||||
@@ -100,7 +103,7 @@ jobs:
|
|||||||
# make apk Flavor=NoNet
|
# make apk Flavor=NoNet
|
||||||
|
|
||||||
# - name: Archive production artifacts (nonet)
|
# - name: Archive production artifacts (nonet)
|
||||||
# uses: actions/upload-artifact@v3
|
# uses: actions/upload-artifact@v4
|
||||||
# with:
|
# with:
|
||||||
# name: signed APK ('nonet' built on ${{ github.job }})
|
# name: signed APK ('nonet' built on ${{ github.job }})
|
||||||
# path: |
|
# path: |
|
||||||
@@ -130,16 +133,15 @@ jobs:
|
|||||||
# # Build Artifact of xamarin.android-oss dated 2022-02-16, master branch (= version 12.2.99)
|
# # Build Artifact of xamarin.android-oss dated 2022-02-16, master branch (= version 12.2.99)
|
||||||
# xamarin_url: https://artprodcus3.artifacts.visualstudio.com/Ad0adf05a-e7d7-4b65-96fe-3f3884d42038/6fd3d886-57a5-4e31-8db7-52a1b47c07a8/_apis/artifact/cGlwZWxpbmVhcnRpZmFjdDovL3hhbWFyaW4vcHJvamVjdElkLzZmZDNkODg2LTU3YTUtNGUzMS04ZGI3LTUyYTFiNDdjMDdhOC9idWlsZElkLzU0OTUzL2FydGlmYWN0TmFtZS9pbnN0YWxsZXJzLXVuc2lnbmVkKy0rTGludXg1/content?format=zip
|
# xamarin_url: https://artprodcus3.artifacts.visualstudio.com/Ad0adf05a-e7d7-4b65-96fe-3f3884d42038/6fd3d886-57a5-4e31-8db7-52a1b47c07a8/_apis/artifact/cGlwZWxpbmVhcnRpZmFjdDovL3hhbWFyaW4vcHJvamVjdElkLzZmZDNkODg2LTU3YTUtNGUzMS04ZGI3LTUyYTFiNDdjMDdhOC9idWlsZElkLzU0OTUzL2FydGlmYWN0TmFtZS9pbnN0YWxsZXJzLXVuc2lnbmVkKy0rTGludXg1/content?format=zip
|
||||||
# steps:
|
# steps:
|
||||||
# - uses: actions/checkout@v3
|
# - uses: actions/checkout@v4
|
||||||
|
# with:
|
||||||
# - name: Fetch submodules
|
# submodules: true
|
||||||
# run: git submodule init && git submodule update
|
|
||||||
|
|
||||||
# - name: Setup Gradle
|
# - name: Setup Gradle
|
||||||
# uses: gradle/gradle-build-action@v2
|
# uses: gradle/actions/setup-gradle@v3
|
||||||
|
|
||||||
# - name: Cache NuGet packages
|
# - name: Cache NuGet packages
|
||||||
# uses: actions/cache@v3
|
# uses: actions/cache@v4
|
||||||
# with:
|
# with:
|
||||||
# path: ~/.nuget/packages
|
# path: ~/.nuget/packages
|
||||||
# key: ${{ runner.os }}-nuget-${{ hashFiles('src/**/*.csproj', 'src/**/packages.config') }}
|
# key: ${{ runner.os }}-nuget-${{ hashFiles('src/**/*.csproj', 'src/**/packages.config') }}
|
||||||
@@ -148,7 +150,7 @@ jobs:
|
|||||||
|
|
||||||
# - name: Cache Xamarin.Android packages
|
# - name: Cache Xamarin.Android packages
|
||||||
# id: xamarin_cache
|
# id: xamarin_cache
|
||||||
# uses: actions/cache@v3
|
# uses: actions/cache@v4
|
||||||
# with:
|
# with:
|
||||||
# path: ~/xamarin.android-oss
|
# path: ~/xamarin.android-oss
|
||||||
# key: ${{ runner.os }}-xamarin.android-oss-${{ env.xamarin_url }}
|
# key: ${{ runner.os }}-xamarin.android-oss-${{ env.xamarin_url }}
|
||||||
@@ -183,7 +185,7 @@ jobs:
|
|||||||
# echo "$HOME/xamarin.android-oss/bin/Release/bin" >> $GITHUB_PATH
|
# echo "$HOME/xamarin.android-oss/bin/Release/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
# - name: Switch to JDK-11
|
# - name: Switch to JDK-11
|
||||||
# uses: actions/setup-java@v3
|
# uses: actions/setup-java@v4
|
||||||
# with:
|
# with:
|
||||||
# java-version: '11'
|
# java-version: '11'
|
||||||
# distribution: 'temurin'
|
# distribution: 'temurin'
|
||||||
@@ -217,7 +219,7 @@ jobs:
|
|||||||
# make apk Flavor=Net
|
# make apk Flavor=Net
|
||||||
|
|
||||||
# - name: Archive production artifacts (net)
|
# - name: Archive production artifacts (net)
|
||||||
# uses: actions/upload-artifact@v3
|
# uses: actions/upload-artifact@v4
|
||||||
# with:
|
# with:
|
||||||
# name: signed APK ('net' built on ${{ github.job }})
|
# name: signed APK ('net' built on ${{ github.job }})
|
||||||
# path: |
|
# path: |
|
||||||
@@ -235,7 +237,7 @@ jobs:
|
|||||||
# make apk Flavor=NoNet
|
# make apk Flavor=NoNet
|
||||||
|
|
||||||
# - name: Archive production artifacts (nonet)
|
# - name: Archive production artifacts (nonet)
|
||||||
# uses: actions/upload-artifact@v3
|
# uses: actions/upload-artifact@v4
|
||||||
# with:
|
# with:
|
||||||
# name: signed APK ('nonet' built on ${{ github.job }})
|
# name: signed APK ('nonet' built on ${{ github.job }})
|
||||||
# path: |
|
# path: |
|
||||||
@@ -254,39 +256,38 @@ jobs:
|
|||||||
runs-on: windows-2022
|
runs-on: windows-2022
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/gradle-build-action@v2
|
uses: gradle/actions/setup-gradle@v3
|
||||||
|
|
||||||
- name: Cache NuGet packages
|
- name: Cache NuGet packages
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.nuget/packages
|
path: ~/.nuget/packages
|
||||||
key: ${{ runner.os }}-nuget-${{ hashFiles('src/**/*.csproj', 'src/**/packages.config') }}
|
key: ${{ runner.os }}-nuget-${{ hashFiles('src/**/*.csproj', 'src/**/packages.config') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-nuget-
|
${{ runner.os }}-nuget-
|
||||||
|
|
||||||
- name: Fetch submodules
|
|
||||||
run: git submodule init && git submodule update
|
|
||||||
|
|
||||||
# Workaround an issue when building on windows-2022. Error was
|
# Workaround an issue when building on windows-2022. Error was
|
||||||
# D8 : OpenJDK 64-Bit Server VM warning : INFO: os::commit_memory(0x00000000ae400000, 330301440, 0) failed; error='The paging file is too small for this operation to complete' (DOS error/errno=1455) [D:\a\keepass2android\keepass2android\src\keepass2android\keepass2android-app.csproj]
|
# D8 : OpenJDK 64-Bit Server VM warning : INFO: os::commit_memory(0x00000000ae400000, 330301440, 0) failed; error='The paging file is too small for this operation to complete' (DOS error/errno=1455) [D:\a\keepass2android\keepass2android\src\keepass2android\keepass2android-app.csproj]
|
||||||
# C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Xamarin\Android\Xamarin.Android.D8.targets(81,5): error MSB6006: "java.exe" exited with code 1. [D:\a\keepass2android\keepass2android\src\keepass2android\keepass2android-app.csproj]
|
# C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Xamarin\Android\Xamarin.Android.D8.targets(81,5): error MSB6006: "java.exe" exited with code 1. [D:\a\keepass2android\keepass2android\src\keepass2android\keepass2android-app.csproj]
|
||||||
- name: Configure Pagefile
|
- name: Configure Pagefile
|
||||||
uses: al-cheb/configure-pagefile-action@v1.3
|
uses: al-cheb/configure-pagefile-action@a3b6ebd6b634da88790d9c58d4b37a7f4a7b8708 # v1.4
|
||||||
with:
|
with:
|
||||||
minimum-size: 8GB
|
minimum-size: 8GB
|
||||||
|
|
||||||
- name: Add msbuild to PATH
|
- name: Add msbuild to PATH
|
||||||
uses: microsoft/setup-msbuild@v1.1
|
uses: microsoft/setup-msbuild@v2
|
||||||
# If we want to also have nmake, use this instead
|
# If we want to also have nmake, use this instead
|
||||||
#uses: ilammy/msvc-dev-cmd@v1
|
#uses: ilammy/msvc-dev-cmd@v1
|
||||||
|
|
||||||
- name: Switch to JDK-11
|
- name: Switch to JDK-17
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: '11'
|
java-version: '17'
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
|
|
||||||
- name: Display java version
|
- name: Display java version
|
||||||
@@ -320,7 +321,7 @@ jobs:
|
|||||||
make apk Flavor=Net
|
make apk Flavor=Net
|
||||||
|
|
||||||
- name: Archive production artifacts (net)
|
- name: Archive production artifacts (net)
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: signed APK ('net' built on ${{ github.job }})
|
name: signed APK ('net' built on ${{ github.job }})
|
||||||
path: |
|
path: |
|
||||||
@@ -341,7 +342,7 @@ jobs:
|
|||||||
make apk Flavor=NoNet
|
make apk Flavor=NoNet
|
||||||
|
|
||||||
- name: Archive production artifacts (nonet)
|
- name: Archive production artifacts (nonet)
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: signed APK ('nonet' built on ${{ github.job }})
|
name: signed APK ('nonet' built on ${{ github.job }})
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -176,3 +176,4 @@ src/java/Keepass2AndroidPluginSDK2/build/generated/mockable-Google-Inc.-Google-A
|
|||||||
/src/ActionViewFilterTest
|
/src/ActionViewFilterTest
|
||||||
/docs/gdrive-verification
|
/docs/gdrive-verification
|
||||||
/src/MegaTest
|
/src/MegaTest
|
||||||
|
*.dtbcache.json
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ Please see the [How to use Keepass2Android with YubiKey NEO](How-to-use-Keepass2
|
|||||||
## Advanced usage of the Keepass2Android keyboard
|
## Advanced usage of the Keepass2Android keyboard
|
||||||
Please see the [Advanced usage of the Keepass2Android keyboard](Advanced-usage-of-the-Keepass2Android-keyboard.md) page.
|
Please see the [Advanced usage of the Keepass2Android keyboard](Advanced-usage-of-the-Keepass2Android-keyboard.md) page.
|
||||||
|
|
||||||
|
## Using Keepass2Android like an authenticator app to generate Time-based One-Time-Passwords (TOTPs)
|
||||||
|
Please see [Generating TOTPs with Keepass2Android](Generating-TOTPs.md)
|
||||||
|
|
||||||
# FAQ
|
# FAQ
|
||||||
|
|
||||||
## Should I use the KP2A keyboard for entering passwords?
|
## Should I use the KP2A keyboard for entering passwords?
|
||||||
|
|||||||
53
docs/Generating-TOTPs.md
Normal file
53
docs/Generating-TOTPs.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
|
||||||
|
## TOTP in brief
|
||||||
|
TOTP stands for [Time-based One-Time Password algorithm](https://en.wikipedia.org/wiki/Time-based_One-time_Password_algorithm) which is one of the most common way proposed by websites to do a [two-factor authentication (2FA)](https://en.wikipedia.org/wiki/Multi-factor_authentication).
|
||||||
|
|
||||||
|
On these websites, this option will often be mentioned in the 2FA configuration menu as things like "_use code generated by an application_", "_use [Google] Authenticator app_".
|
||||||
|
|
||||||
|
You're prompted to scan a QR code with the app, which essentially contains a code called "_seed_", usually with a form like "_AZER TYUI OPQS DFGH JKLM_", used to generate TOTPs. The seed can be also directly copied if there is no scanning option on the app.
|
||||||
|
|
||||||
|
Most common apps:
|
||||||
|
|
||||||
|
- Google Authenticator
|
||||||
|
- Authy
|
||||||
|
- Microsoft Authenticator
|
||||||
|
- FreeOTP
|
||||||
|
- LastPass Authenticator
|
||||||
|
|
||||||
|
## TOTP in KeePass and benefits
|
||||||
|
In KeePass (by Dominik Reichl) there is are several ways to enable this Authenticator app ability:
|
||||||
|
|
||||||
|
- built-in TOTP support: https://keepass.info/help/base/placeholders.html#otp
|
||||||
|
- [KeePassOTP plugin](https://keepass.info/plugins.html#kpotp)
|
||||||
|
- [KeeOtp plugin](https://keepass.info/plugins.html#keeotp)
|
||||||
|
- [KeeTrayTOTP plugin](https://keepass.info/plugins.html#keetraytotp) (note the name "_TrayTOTP_" on this one for later)
|
||||||
|
|
||||||
|
KeePassXC also supports TOTP: https://keepassxc.org/docs/KeePassXC_UserGuide#_adding_totp_to_an_entry
|
||||||
|
|
||||||
|
The greatest benefits are:
|
||||||
|
|
||||||
|
- the seed stays available contrary to the above apps (for which it's more or less hard to backup/restore/switch with another app)
|
||||||
|
- TOTPs are available wherever the KeePass database is available. But conceptually it's not really 2FA anymore (all things are stored in the same place).
|
||||||
|
|
||||||
|
The different implementations use different ways of storing the TOTP seed (or secret, or key) and optional settings (e.g. the length of the TOTP to generate) within an entry inside the kdbx database. Keepass2Android attempts to be able to read the different formats, but can only write one:
|
||||||
|
|
||||||
|
## TOTP in Keepass2Android
|
||||||
|
|
||||||
|
If you use any of the tools mentioned above, you can set up TOTP entries with them. Keepass2Android can read those entries and generate TOTPs if any of the following styles are used:
|
||||||
|
|
||||||
|
* Keepass2 style: used when there are TimeOtp-Secret(-XXX) fields in the entry
|
||||||
|
* KeeOtpPlugin style: used when there is an otp field containing a query string in the form of key=abc&step=X&size=Y (step and size are optional)
|
||||||
|
* KeeWebOtp/Key Uri Format style: used when entry contains a URL starting with otpauth://totp/, e.g. otpauth://totp/?secret=abc (https://github.com/google/google-authenticator/wiki/Key-Uri-Format)
|
||||||
|
* KeeTrayTotp style:
|
||||||
|
* requires a non-empty seed field (default key is "TOTP seed", can be changed in KP2A settings), value is base32 encoded data
|
||||||
|
* requires a non-empty settings field (default key is "TOTP Settings", can be changed as well), value is expected to be a csv-separated array with [Duration];Length(;TimeCorrectionURL). Length is either an integer value or "S" to indicate Steam encoding
|
||||||
|
|
||||||
|
In order to view the generated TOTP code in KP2A, open the corresponding entry. You can then
|
||||||
|
* use a dynamically generated field called "_TOTP_" containing the TOTP or
|
||||||
|
* use the "Copy TOTP" button on the system notification for the selected entry or
|
||||||
|
* switch to the KP2A keyboard and use the TOTP button to insert the TOTP value into the target app or browser
|
||||||
|
|
||||||
|
If you want to configure an entry to contain the TOTP fields, it is suggested to enter edit mode for the entry. Then click the "Configure TOTP" button. You can either enter the data manually or scan a QR code with the information.
|
||||||
|
|
||||||
|
### Spaces in otp field
|
||||||
|
Make sure that the URI doesn't contain spaces, otherwise KeePass2Android will fail to generate TOTPs as a space is an invalid character. If your URIs have spaces, check [this comment](https://github.com/PhilippC/keepass2android/issues/1248#issuecomment-628035961)._
|
||||||
@@ -3,13 +3,13 @@
|
|||||||
Creating a plug-in for Keepass2Android or enabling your app to query credentials from Keepass2Android is pretty simple. Please follow the steps below to get started. In case you have any questions, please contact me.
|
Creating a plug-in for Keepass2Android or enabling your app to query credentials from Keepass2Android is pretty simple. Please follow the steps below to get started. In case you have any questions, please contact me.
|
||||||
|
|
||||||
## Preparations
|
## Preparations
|
||||||
First check out the source code and import the Keepass2AndroidPluginSDK from [https://keepass2android.codeplex.com/SourceControl/latest#src/java/Keepass2AndroidPluginSDK/](https://keepass2android.codeplex.com/SourceControl/latest#src/java/Keepass2AndroidPluginSDK/) into your workspace. You should be able to build this library project.
|
First check out the source code and import the Keepass2AndroidPluginSDK from [https://github.com/PhilippC/keepass2android/tree/master/src/java/Keepass2AndroidPluginSDK2](https://github.com/PhilippC/keepass2android/tree/master/src/java/Keepass2AndroidPluginSDK2/) into your workspace. You should be able to build this library project.
|
||||||
|
|
||||||
Now add a reference to the PluginSDK library from your existing app or add a new plug-in app and then add the reference.
|
Now add a reference to the PluginSDK library from your existing app or add a new plug-in app and then add the reference.
|
||||||
|
|
||||||
## Authorization
|
## Authorization
|
||||||
|
|
||||||
Keepass2Android stores very sensitive user data and therefore implements a plug-in authorization scheme based on broadcasts sent between the plug-in and the host app (=Keepass2Android or Keepass2Android Offline). Before your app/plug-in gets any information from KP2A, the user will have to grant your app/plug-in access to KP2A. As not every app/plug-in requires access to all information, you must specify which scopes are required by your app. The implemented scopes can be found in [https://keepass2android.codeplex.com/SourceControl/latest#src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/Strings.java](https://keepass2android.codeplex.com/SourceControl/latest#src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/Strings.java).
|
Keepass2Android stores very sensitive user data and therefore implements a plug-in authorization scheme based on broadcasts sent between the plug-in and the host app (=Keepass2Android or Keepass2Android Offline). Before your app/plug-in gets any information from KP2A, the user will have to grant your app/plug-in access to KP2A. As not every app/plug-in requires access to all information, you must specify which scopes are required by your app. The implemented scopes can be found in [https://github.com/PhilippC/keepass2android/tree/master/src/java/Keepass2AndroidPluginSDK2/src/keepass2android/pluginsdk/Strings.java](https://github.com/PhilippC/keepass2android/tree/master/src/java/Keepass2AndroidPluginSDK2/src/keepass2android/pluginsdk/Strings.java).
|
||||||
|
|
||||||
To tell Kp2a that you're a plug-in, you need to add a simple BroadcastReceiver like this:
|
To tell Kp2a that you're a plug-in, you need to add a simple BroadcastReceiver like this:
|
||||||
|
|
||||||
@@ -55,8 +55,8 @@ These strings will be displayed to the user when KP2A asks if access should be g
|
|||||||
|
|
||||||
## Modifying the entry view
|
## Modifying the entry view
|
||||||
You can add menu options for the full entry or for individual fields of the entry when displayed to the user. This is done, for example, by the QR plugin ([https://play.google.com/store/apps/details?id=keepass2android.plugin.qr](https://play.google.com/store/apps/details?id=keepass2android.plugin.qr)).
|
You can add menu options for the full entry or for individual fields of the entry when displayed to the user. This is done, for example, by the QR plugin ([https://play.google.com/store/apps/details?id=keepass2android.plugin.qr](https://play.google.com/store/apps/details?id=keepass2android.plugin.qr)).
|
||||||
In addition, it is even possible to add new fields or modify existing fields. Please see the sample plugin "PluginA" in the KP2A repository for a simple example on how to do this:
|
In addition, it is even possible to add new fields or modify existing fields. Please see the sample plugin "PluginA" for a simple example on how to do this:
|
||||||
[https://keepass2android.codeplex.com/SourceControl/latest#src/java/PluginA/src/keepass2android/plugina/PluginAActionReceiver.java](https://keepass2android.codeplex.com/SourceControl/latest#src/java/PluginA/src/keepass2android/plugina/PluginAActionReceiver.java)
|
[https://github.com/PhilippC/keepass2android-sampleplugin/blob/main/src/keepass2android/plugina/PluginAAccessReceiver.java](https://github.com/PhilippC/keepass2android-sampleplugin/blob/main/src/keepass2android/plugina/PluginAAccessReceiver.java)
|
||||||
|
|
||||||
## Querying credentials
|
## Querying credentials
|
||||||
KP2A 0.9.4 adds a great opportunity for third party apps: Instead of prompting the user to enter credentials or a passphrase, the app should try to get the data from KP2A if it is installed: If the user grants (or previously granted) access for the app, KP2A will automatically retrieve the matching entry. User action is only required if the KP2A database is locked (user will usually unlock it with the short QuickUnlock code) or if no matching entry is found (user can then create a new entry or select an existing one. in the latter case KP2A will offer to add entry information so that the entry will be found automatically next time).
|
KP2A 0.9.4 adds a great opportunity for third party apps: Instead of prompting the user to enter credentials or a passphrase, the app should try to get the data from KP2A if it is installed: If the user grants (or previously granted) access for the app, KP2A will automatically retrieve the matching entry. User action is only required if the KP2A database is locked (user will usually unlock it with the short QuickUnlock code) or if no matching entry is found (user can then create a new entry or select an existing one. in the latter case KP2A will offer to add entry information so that the entry will be found automatically next time).
|
||||||
|
|||||||
72
docs/SFTP-Credentials.md
Normal file
72
docs/SFTP-Credentials.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# SFTP Open/Create Database Credentials Documentation
|
||||||
|
|
||||||
|
## Basic Settings
|
||||||
|
* **Host** -- the hostname or IP address of the SFTP server to connect to
|
||||||
|
* **Port** -- the listening TCP port of the SFTP server to connect to (default: 22)
|
||||||
|
* **Username** -- the user/account name on the SFTP server that has access to the database
|
||||||
|
* **Initial directory** -- The path on the SFTP server that will be used as a starting point when choosing the remote database file
|
||||||
|
|
||||||
|
### Authentication Modes
|
||||||
|
|
||||||
|
#### Password
|
||||||
|
Authenticate using a password
|
||||||
|
|
||||||
|
* **Password** -- the password associated with **username** used to log into the SFTP server
|
||||||
|
|
||||||
|
#### K2A Private/Public Key
|
||||||
|
Authenticate using a private/public key pair that is generated internally by KP2A
|
||||||
|
|
||||||
|
* **SEND PUBLIC KEY...** -- Opens a standard Android "Share" screen containing the KP2A public key content. This allows for the public key to be sent via email, SMS, etc. This public key will need to be added to the SFTP server's user's "authorized keys" to allow private/public key authentication.
|
||||||
|
|
||||||
|
#### Custom Private Key
|
||||||
|
Authenticate using an existing private/public key pair. Use this option instead of *K2A Private/Public Key* if you wish to use a key pair that is already set up for this **username** on the SFTP server.
|
||||||
|
|
||||||
|
* **Selected private key** -- a combo-box containing a list of custom private keys that KP2A knows about, and a special `[Add new...]` option.
|
||||||
|
##### Add A New Private Key
|
||||||
|
* Select `[Add new...]`
|
||||||
|
* Enter a name for the new key in **New key name**
|
||||||
|
* Enter the private key contents (text) into **New key content**. **TIP:** The easiest way to accomplish this is to open the private key file in a text editor on the device, **Select All**, **Copy** to the clipboard, and paste it into **New key content**.
|
||||||
|
* Tap **SAVE PRIVATE KEY** to add the new key to the known list.
|
||||||
|
|
||||||
|
##### Use An Existing Private Key
|
||||||
|
* To use a private key that has already been imported into KP2A, simply select it from the list of keys.
|
||||||
|
|
||||||
|
##### Remove An Existing Key
|
||||||
|
* To remove a private that has been imported into KP2A, select it from the list and tap **DELETE PRIVATE KEY**.
|
||||||
|
|
||||||
|
A **key passphrase** can be supplied (if the key pair requires it)
|
||||||
|
|
||||||
|
## Advanced Settings
|
||||||
|
* **Connection timeout seconds** -- the number of seconds to wait for a connection to the server before giving up and considering the server as unavailable/unreachable
|
||||||
|
|
||||||
|
### Key Algorithm Manipulation
|
||||||
|
**NOTE: It is very rare that these fields need to be (or should be) specified. Use at your own risk!**
|
||||||
|
|
||||||
|
* **Key Exchange (KEX) Algorithm(s)** -- Explicitly set or modify the ordered list of Key Exchange algorithms that the SSH/SFTP client library will try to use
|
||||||
|
* **Server Host Key Algorithm(s)** -- Explicitly set or modify the ordered list of Server Host Key algorithms that the SSH/SFTP client library will try to use
|
||||||
|
|
||||||
|
#### How It Works
|
||||||
|
The SSH/SFTP client has a pre-defined ordered list of algorithm names that it will use to negotiate with the server to handle key exchange. In rare cases there are compatibility issues where Android OS has not properly implemented full support for algorithms listed. This can result in a connection failure, even if there is a suitable algorithm available (of lesser priority in the list).
|
||||||
|
|
||||||
|
The fields listed above allow these lists to be manipulated in the following ways to overcome/workaround such problems. The value is a comma-separated list of "algorithm spec" entries. Specs can be one of:
|
||||||
|
|
||||||
|
* Direct replacement of values -- Ex: `primary_alg,secondary_alg`
|
||||||
|
* Prepend to values -- Ex: `+try_first_alg`
|
||||||
|
* Append to values -- Ex: `try_last_alg+`
|
||||||
|
* Remove a specific value -- Ex: `-bad_alg`
|
||||||
|
* Remove values matching prefix -- Ex: `-bad_starting_with*`
|
||||||
|
* Remove values matching suffix -- Ex: `-*bad_ending_with`
|
||||||
|
* Remove values matching substring -- Ex: `-*bad_middle*`
|
||||||
|
* Remove values matching prefix and suffix -- Ex: `-alg_begin*end`
|
||||||
|
|
||||||
|
For example, assume the system's KEX algorithm list is:
|
||||||
|
`ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256`
|
||||||
|
|
||||||
|
These are various outcomes (user KEX field -> result):
|
||||||
|
|
||||||
|
* Prefix removal: `-ec*` --> `diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256`
|
||||||
|
* Suffix removal, appending: `-*256,+first_alg,almost_last_alg+,last_alg+` --> `first_alg,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,almost_last_alg,last_alg`
|
||||||
|
* Direct replacement: `first_alg,middle_alg,last_alg` --> `first_alg,middle_alg,last_alg`
|
||||||
|
|
||||||
|
## Selecting A Database
|
||||||
|
Once all applicable fields have been entered and/or options selected, tapping **OK** will attempt to connect to the SFTP server. First time connections may pop up a dialog window asking to accept the host's authenticity (tap **yes** if the host is trusted), as well as potentially creating a new `known_hosts` file (tap **yes** to do so). If the connection is successful, a remote file browser screen will open. Navigate and select the Keepass database to open.
|
||||||
Binary file not shown.
BIN
src/JavaFileStorageBindings/Jars/jackson-core-2.13.5.jar
Normal file
BIN
src/JavaFileStorageBindings/Jars/jackson-core-2.13.5.jar
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="com.microsoft.services.msa"
|
|
||||||
android:versionCode="1"
|
|
||||||
android:versionName="1.0" >
|
|
||||||
|
|
||||||
<uses-sdk
|
|
||||||
android:minSdkVersion="14"
|
|
||||||
android:targetSdkVersion="22" />
|
|
||||||
|
|
||||||
<uses-permission android:name="com.sony.mobile.permission.SYSTEM_UI_VISIBILITY_EXTENSION" />
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
int string app_name 0x7f020000
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="com.microsoft.services.msa"
|
|
||||||
android:versionCode="1"
|
|
||||||
android:versionName="1.0" >
|
|
||||||
|
|
||||||
<uses-sdk
|
|
||||||
android:minSdkVersion="14"
|
|
||||||
android:targetSdkVersion="22" />
|
|
||||||
|
|
||||||
<uses-permission android:name="com.sony.mobile.permission.SYSTEM_UI_VISIBILITY_EXTENSION" />
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
Binary file not shown.
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<!-- From: file:/C:/Users/pnied/Documents/git/msa-auth-for-android/src/main/res/values/strings.xml -->
|
|
||||||
<eat-comment/>
|
|
||||||
<string name="app_name">msa-auth</string>
|
|
||||||
</resources>
|
|
||||||
Binary file not shown.
BIN
src/JavaFileStorageBindings/Jars/okhttp-4.12.0.jar
Normal file
BIN
src/JavaFileStorageBindings/Jars/okhttp-4.12.0.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
src/JavaFileStorageBindings/Jars/okhttp-digest-3.1.0.jar
Normal file
BIN
src/JavaFileStorageBindings/Jars/okhttp-digest-3.1.0.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
src/JavaFileStorageBindings/Jars/okio-3.6.0.jar
Normal file
BIN
src/JavaFileStorageBindings/Jars/okio-3.6.0.jar
Normal file
Binary file not shown.
BIN
src/JavaFileStorageBindings/Jars/okio-jvm-3.6.0.jar
Normal file
BIN
src/JavaFileStorageBindings/Jars/okio-jvm-3.6.0.jar
Normal file
Binary file not shown.
Binary file not shown.
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="com.microsoft.onedrivesdk"
|
|
||||||
android:versionCode="10202"
|
|
||||||
android:versionName="1.2.2" >
|
|
||||||
|
|
||||||
<uses-sdk
|
|
||||||
android:minSdkVersion="15"
|
|
||||||
android:targetSdkVersion="23" />
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
|
||||||
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
|
||||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
int dimen activity_horizontal_margin 0x7f030000
|
|
||||||
int dimen activity_vertical_margin 0x7f030001
|
|
||||||
int id LinearLayout1 0x7f060004
|
|
||||||
int id com_microsoft_aad_adal_editDummyText 0x7f060002
|
|
||||||
int id com_microsoft_aad_adal_progressBar 0x7f060003
|
|
||||||
int id com_microsoft_aad_adal_webView1 0x7f060001
|
|
||||||
int id editPassword 0x7f060006
|
|
||||||
int id editUserName 0x7f060005
|
|
||||||
int id webView1 0x7f060000
|
|
||||||
int layout activity_authentication 0x7f020000
|
|
||||||
int layout dialog_authentication 0x7f020001
|
|
||||||
int layout http_auth_dialog 0x7f020002
|
|
||||||
int string app_loading 0x7f050000
|
|
||||||
int string app_name 0x7f050001
|
|
||||||
int string broker_processing 0x7f050002
|
|
||||||
int string http_auth_dialog_cancel 0x7f050003
|
|
||||||
int string http_auth_dialog_login 0x7f050004
|
|
||||||
int string http_auth_dialog_password 0x7f050005
|
|
||||||
int string http_auth_dialog_title 0x7f050006
|
|
||||||
int string http_auth_dialog_username 0x7f050007
|
|
||||||
int style AppBaseTheme 0x7f040000
|
|
||||||
int style AppTheme 0x7f040001
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="com.microsoft.onedrivesdk"
|
|
||||||
android:versionCode="10202"
|
|
||||||
android:versionName="1.2.2" >
|
|
||||||
|
|
||||||
<uses-sdk
|
|
||||||
android:minSdkVersion="15"
|
|
||||||
android:targetSdkVersion="23" />
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
|
||||||
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
|
||||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
Binary file not shown.
@@ -183,6 +183,7 @@
|
|||||||
<LibraryProjectZip Include="..\java\JavaFileStorage\app\build\outputs\aar\JavaFileStorage-debug.aar">
|
<LibraryProjectZip Include="..\java\JavaFileStorage\app\build\outputs\aar\JavaFileStorage-debug.aar">
|
||||||
<Link>Jars\JavaFileStorage-debug.aar</Link>
|
<Link>Jars\JavaFileStorage-debug.aar</Link>
|
||||||
</LibraryProjectZip>
|
</LibraryProjectZip>
|
||||||
|
<None Include="app.config" />
|
||||||
<None Include="Jars\AboutJars.txt" />
|
<None Include="Jars\AboutJars.txt" />
|
||||||
<None Include="Additions\AboutAdditions.txt" />
|
<None Include="Additions\AboutAdditions.txt" />
|
||||||
<None Include="packages.config" />
|
<None Include="packages.config" />
|
||||||
@@ -212,12 +213,6 @@
|
|||||||
<Name>PCloudBindings</Name>
|
<Name>PCloudBindings</Name>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedReferenceJar Include="Jars\msa-auth-0.8.6\classes-msa-auth.jar" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedReferenceJar Include="Jars\onedrive-sdk-android-1.2.2\classes-onedrive-sdk.jar" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedReferenceJar Include="Jars\gdrive\commons-logging-1.1.1.jar" />
|
<EmbeddedReferenceJar Include="Jars\gdrive\commons-logging-1.1.1.jar" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -242,21 +237,6 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedReferenceJar Include="Jars\gdrive\google-http-client-gson-1.16.0-rc.jar" />
|
<EmbeddedReferenceJar Include="Jars\gdrive\google-http-client-gson-1.16.0-rc.jar" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedReferenceJar Include="Jars\jackson-core-2.7.4.jar" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedReferenceJar Include="Jars\okhttp-digest-2.5.jar" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedReferenceJar Include="Jars\okio-2.9.0.jar" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedReferenceJar Include="Jars\okhttp-4.10.0-RC1.jar" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedJar Include="Jars\dropbox-core-sdk-4.0.0.jar" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedReferenceJar Include="Jars\gson-2.8.6.jar" />
|
<EmbeddedReferenceJar Include="Jars\gson-2.8.6.jar" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -290,18 +270,30 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedReferenceJar Include="Jars\gdrive\opencensus-api-0.24.0.jar" />
|
<EmbeddedReferenceJar Include="Jars\gdrive\opencensus-api-0.24.0.jar" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedReferenceJar Include="Jars\okhttp-4.12.0.jar" />
|
||||||
|
<EmbeddedReferenceJar Include="Jars\okio-3.6.0.jar" />
|
||||||
|
<EmbeddedReferenceJar Include="Jars\okio-jvm-3.6.0.jar" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedReferenceJar Include="Jars\okhttp-digest-3.1.0.jar" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedReferenceJar Include="Jars\dropbox-core-sdk-5.4.6.jar" />
|
||||||
|
<EmbeddedReferenceJar Include="Jars\jackson-core-2.13.5.jar" />
|
||||||
|
</ItemGroup>
|
||||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ErrorText>Dieses Projekt verweist auf mindestens ein NuGet-Paket, das auf diesem Computer fehlt. Verwenden Sie die Wiederherstellung von NuGet-Paketen, um die fehlenden Dateien herunterzuladen. Weitere Informationen finden Sie unter "http://go.microsoft.com/fwlink/?LinkID=322105". Die fehlende Datei ist "{0}".</ErrorText>
|
<ErrorText>Dieses Projekt verweist auf mindestens ein NuGet-Paket, das auf diesem Computer fehlt. Verwenden Sie die Wiederherstellung von NuGet-Paketen, um die fehlenden Dateien herunterzuladen. Weitere Informationen finden Sie unter "http://go.microsoft.com/fwlink/?LinkID=322105". Die fehlende Datei ist "{0}".</ErrorText>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<Error Condition="!Exists('..\packages\Xamarin.Google.Guava.FailureAccess.1.0.1.3\build\monoandroid90\Xamarin.Google.Guava.FailureAccess.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.Google.Guava.FailureAccess.1.0.1.3\build\monoandroid90\Xamarin.Google.Guava.FailureAccess.targets'))" />
|
|
||||||
<Error Condition="!Exists('..\packages\Xamarin.Google.Guava.28.2.0.1\build\monoandroid90\Xamarin.Google.Guava.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.Google.Guava.28.2.0.1\build\monoandroid90\Xamarin.Google.Guava.targets'))" />
|
|
||||||
<Error Condition="!Exists('..\packages\Xamarin.AndroidX.MultiDex.2.0.1.13\build\monoandroid12.0\Xamarin.AndroidX.MultiDex.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.AndroidX.MultiDex.2.0.1.13\build\monoandroid12.0\Xamarin.AndroidX.MultiDex.targets'))" />
|
|
||||||
<Error Condition="!Exists('..\packages\Xamarin.AndroidX.Migration.1.0.10\build\monoandroid120\Xamarin.AndroidX.Migration.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.AndroidX.Migration.1.0.10\build\monoandroid120\Xamarin.AndroidX.Migration.props'))" />
|
<Error Condition="!Exists('..\packages\Xamarin.AndroidX.Migration.1.0.10\build\monoandroid120\Xamarin.AndroidX.Migration.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.AndroidX.Migration.1.0.10\build\monoandroid120\Xamarin.AndroidX.Migration.props'))" />
|
||||||
<Error Condition="!Exists('..\packages\Xamarin.AndroidX.Migration.1.0.10\build\monoandroid120\Xamarin.AndroidX.Migration.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.AndroidX.Migration.1.0.10\build\monoandroid120\Xamarin.AndroidX.Migration.targets'))" />
|
<Error Condition="!Exists('..\packages\Xamarin.AndroidX.Migration.1.0.10\build\monoandroid120\Xamarin.AndroidX.Migration.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.AndroidX.Migration.1.0.10\build\monoandroid120\Xamarin.AndroidX.Migration.targets'))" />
|
||||||
<Error Condition="!Exists('..\packages\Xamarin.Build.Download.0.11.4\build\Xamarin.Build.Download.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.Build.Download.0.11.4\build\Xamarin.Build.Download.props'))" />
|
<Error Condition="!Exists('..\packages\Xamarin.Build.Download.0.11.4\build\Xamarin.Build.Download.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.Build.Download.0.11.4\build\Xamarin.Build.Download.props'))" />
|
||||||
<Error Condition="!Exists('..\packages\Xamarin.Build.Download.0.11.4\build\Xamarin.Build.Download.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.Build.Download.0.11.4\build\Xamarin.Build.Download.targets'))" />
|
<Error Condition="!Exists('..\packages\Xamarin.Build.Download.0.11.4\build\Xamarin.Build.Download.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.Build.Download.0.11.4\build\Xamarin.Build.Download.targets'))" />
|
||||||
|
<Error Condition="!Exists('..\packages\Xamarin.AndroidX.MultiDex.2.0.1.13\build\monoandroid12.0\Xamarin.AndroidX.MultiDex.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.AndroidX.MultiDex.2.0.1.13\build\monoandroid12.0\Xamarin.AndroidX.MultiDex.targets'))" />
|
||||||
|
<Error Condition="!Exists('..\packages\Xamarin.Google.Guava.FailureAccess.1.0.1.3\build\monoandroid90\Xamarin.Google.Guava.FailureAccess.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.Google.Guava.FailureAccess.1.0.1.3\build\monoandroid90\Xamarin.Google.Guava.FailureAccess.targets'))" />
|
||||||
<Error Condition="!Exists('..\packages\Xamarin.Google.Guava.ListenableFuture.1.0.0.9\build\monoandroid12.0\Xamarin.Google.Guava.ListenableFuture.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.Google.Guava.ListenableFuture.1.0.0.9\build\monoandroid12.0\Xamarin.Google.Guava.ListenableFuture.targets'))" />
|
<Error Condition="!Exists('..\packages\Xamarin.Google.Guava.ListenableFuture.1.0.0.9\build\monoandroid12.0\Xamarin.Google.Guava.ListenableFuture.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.Google.Guava.ListenableFuture.1.0.0.9\build\monoandroid12.0\Xamarin.Google.Guava.ListenableFuture.targets'))" />
|
||||||
|
<Error Condition="!Exists('..\packages\Xamarin.Google.Guava.28.2.0.1\build\monoandroid90\Xamarin.Google.Guava.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.Google.Guava.28.2.0.1\build\monoandroid90\Xamarin.Google.Guava.targets'))" />
|
||||||
<Error Condition="!Exists('..\packages\Xamarin.Jetbrains.Annotations.23.0.0.4\build\monoandroid12.0\Xamarin.Jetbrains.Annotations.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.Jetbrains.Annotations.23.0.0.4\build\monoandroid12.0\Xamarin.Jetbrains.Annotations.targets'))" />
|
<Error Condition="!Exists('..\packages\Xamarin.Jetbrains.Annotations.23.0.0.4\build\monoandroid12.0\Xamarin.Jetbrains.Annotations.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.Jetbrains.Annotations.23.0.0.4\build\monoandroid12.0\Xamarin.Jetbrains.Annotations.targets'))" />
|
||||||
<Error Condition="!Exists('..\packages\Xamarin.Kotlin.StdLib.Common.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Common.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.Kotlin.StdLib.Common.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Common.targets'))" />
|
<Error Condition="!Exists('..\packages\Xamarin.Kotlin.StdLib.Common.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Common.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.Kotlin.StdLib.Common.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Common.targets'))" />
|
||||||
<Error Condition="!Exists('..\packages\Xamarin.Kotlin.StdLib.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.Kotlin.StdLib.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.targets'))" />
|
<Error Condition="!Exists('..\packages\Xamarin.Kotlin.StdLib.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.Kotlin.StdLib.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.targets'))" />
|
||||||
@@ -326,7 +318,6 @@
|
|||||||
<Error Condition="!Exists('..\packages\Xamarin.Kotlin.StdLib.Jdk7.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Jdk7.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.Kotlin.StdLib.Jdk7.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Jdk7.targets'))" />
|
<Error Condition="!Exists('..\packages\Xamarin.Kotlin.StdLib.Jdk7.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Jdk7.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.Kotlin.StdLib.Jdk7.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Jdk7.targets'))" />
|
||||||
<Error Condition="!Exists('..\packages\Xamarin.Kotlin.StdLib.Jdk8.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Jdk8.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.Kotlin.StdLib.Jdk8.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Jdk8.targets'))" />
|
<Error Condition="!Exists('..\packages\Xamarin.Kotlin.StdLib.Jdk8.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Jdk8.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.Kotlin.StdLib.Jdk8.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Jdk8.targets'))" />
|
||||||
<Error Condition="!Exists('..\packages\Xamarin.KotlinX.Coroutines.Core.Jvm.1.6.4\build\monoandroid12.0\Xamarin.KotlinX.Coroutines.Core.Jvm.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.KotlinX.Coroutines.Core.Jvm.1.6.4\build\monoandroid12.0\Xamarin.KotlinX.Coroutines.Core.Jvm.targets'))" />
|
<Error Condition="!Exists('..\packages\Xamarin.KotlinX.Coroutines.Core.Jvm.1.6.4\build\monoandroid12.0\Xamarin.KotlinX.Coroutines.Core.Jvm.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.KotlinX.Coroutines.Core.Jvm.1.6.4\build\monoandroid12.0\Xamarin.KotlinX.Coroutines.Core.Jvm.targets'))" />
|
||||||
<Error Condition="!Exists('..\packages\Xamarin.KotlinX.Coroutines.Android.1.6.4\build\monoandroid12.0\Xamarin.KotlinX.Coroutines.Android.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.KotlinX.Coroutines.Android.1.6.4\build\monoandroid12.0\Xamarin.KotlinX.Coroutines.Android.targets'))" />
|
|
||||||
<Error Condition="!Exists('..\packages\Xamarin.AndroidX.Lifecycle.ViewModelSavedState.2.5.1\build\monoandroid12.0\Xamarin.AndroidX.Lifecycle.ViewModelSavedState.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.AndroidX.Lifecycle.ViewModelSavedState.2.5.1\build\monoandroid12.0\Xamarin.AndroidX.Lifecycle.ViewModelSavedState.targets'))" />
|
<Error Condition="!Exists('..\packages\Xamarin.AndroidX.Lifecycle.ViewModelSavedState.2.5.1\build\monoandroid12.0\Xamarin.AndroidX.Lifecycle.ViewModelSavedState.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.AndroidX.Lifecycle.ViewModelSavedState.2.5.1\build\monoandroid12.0\Xamarin.AndroidX.Lifecycle.ViewModelSavedState.targets'))" />
|
||||||
<Error Condition="!Exists('..\packages\Xamarin.AndroidX.Activity.1.6.0\build\monoandroid12.0\Xamarin.AndroidX.Activity.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.AndroidX.Activity.1.6.0\build\monoandroid12.0\Xamarin.AndroidX.Activity.targets'))" />
|
<Error Condition="!Exists('..\packages\Xamarin.AndroidX.Activity.1.6.0\build\monoandroid12.0\Xamarin.AndroidX.Activity.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.AndroidX.Activity.1.6.0\build\monoandroid12.0\Xamarin.AndroidX.Activity.targets'))" />
|
||||||
<Error Condition="!Exists('..\packages\Xamarin.AndroidX.Fragment.1.5.3\build\monoandroid12.0\Xamarin.AndroidX.Fragment.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.AndroidX.Fragment.1.5.3\build\monoandroid12.0\Xamarin.AndroidX.Fragment.targets'))" />
|
<Error Condition="!Exists('..\packages\Xamarin.AndroidX.Fragment.1.5.3\build\monoandroid12.0\Xamarin.AndroidX.Fragment.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.AndroidX.Fragment.1.5.3\build\monoandroid12.0\Xamarin.AndroidX.Fragment.targets'))" />
|
||||||
@@ -337,13 +328,14 @@
|
|||||||
<Error Condition="!Exists('..\packages\Xamarin.GooglePlayServices.Auth.Base.118.0.6\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Auth.Base.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.GooglePlayServices.Auth.Base.118.0.6\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Auth.Base.targets'))" />
|
<Error Condition="!Exists('..\packages\Xamarin.GooglePlayServices.Auth.Base.118.0.6\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Auth.Base.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.GooglePlayServices.Auth.Base.118.0.6\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Auth.Base.targets'))" />
|
||||||
<Error Condition="!Exists('..\packages\Xamarin.GooglePlayServices.Fido.119.0.0\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Fido.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.GooglePlayServices.Fido.119.0.0\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Fido.targets'))" />
|
<Error Condition="!Exists('..\packages\Xamarin.GooglePlayServices.Fido.119.0.0\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Fido.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.GooglePlayServices.Fido.119.0.0\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Fido.targets'))" />
|
||||||
<Error Condition="!Exists('..\packages\Xamarin.GooglePlayServices.Auth.120.4.0\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Auth.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.GooglePlayServices.Auth.120.4.0\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Auth.targets'))" />
|
<Error Condition="!Exists('..\packages\Xamarin.GooglePlayServices.Auth.120.4.0\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Auth.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.GooglePlayServices.Auth.120.4.0\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Auth.targets'))" />
|
||||||
|
<Error Condition="!Exists('..\packages\Xamarin.KotlinX.Coroutines.Android.1.6.4\build\monoandroid12.0\Xamarin.KotlinX.Coroutines.Android.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Xamarin.KotlinX.Coroutines.Android.1.6.4\build\monoandroid12.0\Xamarin.KotlinX.Coroutines.Android.targets'))" />
|
||||||
</Target>
|
</Target>
|
||||||
<Import Project="..\packages\Xamarin.Google.Guava.FailureAccess.1.0.1.3\build\monoandroid90\Xamarin.Google.Guava.FailureAccess.targets" Condition="Exists('..\packages\Xamarin.Google.Guava.FailureAccess.1.0.1.3\build\monoandroid90\Xamarin.Google.Guava.FailureAccess.targets')" />
|
|
||||||
<Import Project="..\packages\Xamarin.Google.Guava.28.2.0.1\build\monoandroid90\Xamarin.Google.Guava.targets" Condition="Exists('..\packages\Xamarin.Google.Guava.28.2.0.1\build\monoandroid90\Xamarin.Google.Guava.targets')" />
|
|
||||||
<Import Project="..\packages\Xamarin.AndroidX.MultiDex.2.0.1.13\build\monoandroid12.0\Xamarin.AndroidX.MultiDex.targets" Condition="Exists('..\packages\Xamarin.AndroidX.MultiDex.2.0.1.13\build\monoandroid12.0\Xamarin.AndroidX.MultiDex.targets')" />
|
|
||||||
<Import Project="..\packages\Xamarin.AndroidX.Migration.1.0.10\build\monoandroid120\Xamarin.AndroidX.Migration.targets" Condition="Exists('..\packages\Xamarin.AndroidX.Migration.1.0.10\build\monoandroid120\Xamarin.AndroidX.Migration.targets')" />
|
<Import Project="..\packages\Xamarin.AndroidX.Migration.1.0.10\build\monoandroid120\Xamarin.AndroidX.Migration.targets" Condition="Exists('..\packages\Xamarin.AndroidX.Migration.1.0.10\build\monoandroid120\Xamarin.AndroidX.Migration.targets')" />
|
||||||
<Import Project="..\packages\Xamarin.Build.Download.0.11.4\build\Xamarin.Build.Download.targets" Condition="Exists('..\packages\Xamarin.Build.Download.0.11.4\build\Xamarin.Build.Download.targets')" />
|
<Import Project="..\packages\Xamarin.Build.Download.0.11.4\build\Xamarin.Build.Download.targets" Condition="Exists('..\packages\Xamarin.Build.Download.0.11.4\build\Xamarin.Build.Download.targets')" />
|
||||||
|
<Import Project="..\packages\Xamarin.AndroidX.MultiDex.2.0.1.13\build\monoandroid12.0\Xamarin.AndroidX.MultiDex.targets" Condition="Exists('..\packages\Xamarin.AndroidX.MultiDex.2.0.1.13\build\monoandroid12.0\Xamarin.AndroidX.MultiDex.targets')" />
|
||||||
|
<Import Project="..\packages\Xamarin.Google.Guava.FailureAccess.1.0.1.3\build\monoandroid90\Xamarin.Google.Guava.FailureAccess.targets" Condition="Exists('..\packages\Xamarin.Google.Guava.FailureAccess.1.0.1.3\build\monoandroid90\Xamarin.Google.Guava.FailureAccess.targets')" />
|
||||||
<Import Project="..\packages\Xamarin.Google.Guava.ListenableFuture.1.0.0.9\build\monoandroid12.0\Xamarin.Google.Guava.ListenableFuture.targets" Condition="Exists('..\packages\Xamarin.Google.Guava.ListenableFuture.1.0.0.9\build\monoandroid12.0\Xamarin.Google.Guava.ListenableFuture.targets')" />
|
<Import Project="..\packages\Xamarin.Google.Guava.ListenableFuture.1.0.0.9\build\monoandroid12.0\Xamarin.Google.Guava.ListenableFuture.targets" Condition="Exists('..\packages\Xamarin.Google.Guava.ListenableFuture.1.0.0.9\build\monoandroid12.0\Xamarin.Google.Guava.ListenableFuture.targets')" />
|
||||||
|
<Import Project="..\packages\Xamarin.Google.Guava.28.2.0.1\build\monoandroid90\Xamarin.Google.Guava.targets" Condition="Exists('..\packages\Xamarin.Google.Guava.28.2.0.1\build\monoandroid90\Xamarin.Google.Guava.targets')" />
|
||||||
<Import Project="..\packages\Xamarin.Jetbrains.Annotations.23.0.0.4\build\monoandroid12.0\Xamarin.Jetbrains.Annotations.targets" Condition="Exists('..\packages\Xamarin.Jetbrains.Annotations.23.0.0.4\build\monoandroid12.0\Xamarin.Jetbrains.Annotations.targets')" />
|
<Import Project="..\packages\Xamarin.Jetbrains.Annotations.23.0.0.4\build\monoandroid12.0\Xamarin.Jetbrains.Annotations.targets" Condition="Exists('..\packages\Xamarin.Jetbrains.Annotations.23.0.0.4\build\monoandroid12.0\Xamarin.Jetbrains.Annotations.targets')" />
|
||||||
<Import Project="..\packages\Xamarin.Kotlin.StdLib.Common.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Common.targets" Condition="Exists('..\packages\Xamarin.Kotlin.StdLib.Common.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Common.targets')" />
|
<Import Project="..\packages\Xamarin.Kotlin.StdLib.Common.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Common.targets" Condition="Exists('..\packages\Xamarin.Kotlin.StdLib.Common.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Common.targets')" />
|
||||||
<Import Project="..\packages\Xamarin.Kotlin.StdLib.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.targets" Condition="Exists('..\packages\Xamarin.Kotlin.StdLib.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.targets')" />
|
<Import Project="..\packages\Xamarin.Kotlin.StdLib.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.targets" Condition="Exists('..\packages\Xamarin.Kotlin.StdLib.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.targets')" />
|
||||||
@@ -368,7 +360,6 @@
|
|||||||
<Import Project="..\packages\Xamarin.Kotlin.StdLib.Jdk7.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Jdk7.targets" Condition="Exists('..\packages\Xamarin.Kotlin.StdLib.Jdk7.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Jdk7.targets')" />
|
<Import Project="..\packages\Xamarin.Kotlin.StdLib.Jdk7.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Jdk7.targets" Condition="Exists('..\packages\Xamarin.Kotlin.StdLib.Jdk7.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Jdk7.targets')" />
|
||||||
<Import Project="..\packages\Xamarin.Kotlin.StdLib.Jdk8.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Jdk8.targets" Condition="Exists('..\packages\Xamarin.Kotlin.StdLib.Jdk8.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Jdk8.targets')" />
|
<Import Project="..\packages\Xamarin.Kotlin.StdLib.Jdk8.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Jdk8.targets" Condition="Exists('..\packages\Xamarin.Kotlin.StdLib.Jdk8.1.7.10\build\monoandroid12.0\Xamarin.Kotlin.StdLib.Jdk8.targets')" />
|
||||||
<Import Project="..\packages\Xamarin.KotlinX.Coroutines.Core.Jvm.1.6.4\build\monoandroid12.0\Xamarin.KotlinX.Coroutines.Core.Jvm.targets" Condition="Exists('..\packages\Xamarin.KotlinX.Coroutines.Core.Jvm.1.6.4\build\monoandroid12.0\Xamarin.KotlinX.Coroutines.Core.Jvm.targets')" />
|
<Import Project="..\packages\Xamarin.KotlinX.Coroutines.Core.Jvm.1.6.4\build\monoandroid12.0\Xamarin.KotlinX.Coroutines.Core.Jvm.targets" Condition="Exists('..\packages\Xamarin.KotlinX.Coroutines.Core.Jvm.1.6.4\build\monoandroid12.0\Xamarin.KotlinX.Coroutines.Core.Jvm.targets')" />
|
||||||
<Import Project="..\packages\Xamarin.KotlinX.Coroutines.Android.1.6.4\build\monoandroid12.0\Xamarin.KotlinX.Coroutines.Android.targets" Condition="Exists('..\packages\Xamarin.KotlinX.Coroutines.Android.1.6.4\build\monoandroid12.0\Xamarin.KotlinX.Coroutines.Android.targets')" />
|
|
||||||
<Import Project="..\packages\Xamarin.AndroidX.Lifecycle.ViewModelSavedState.2.5.1\build\monoandroid12.0\Xamarin.AndroidX.Lifecycle.ViewModelSavedState.targets" Condition="Exists('..\packages\Xamarin.AndroidX.Lifecycle.ViewModelSavedState.2.5.1\build\monoandroid12.0\Xamarin.AndroidX.Lifecycle.ViewModelSavedState.targets')" />
|
<Import Project="..\packages\Xamarin.AndroidX.Lifecycle.ViewModelSavedState.2.5.1\build\monoandroid12.0\Xamarin.AndroidX.Lifecycle.ViewModelSavedState.targets" Condition="Exists('..\packages\Xamarin.AndroidX.Lifecycle.ViewModelSavedState.2.5.1\build\monoandroid12.0\Xamarin.AndroidX.Lifecycle.ViewModelSavedState.targets')" />
|
||||||
<Import Project="..\packages\Xamarin.AndroidX.Activity.1.6.0\build\monoandroid12.0\Xamarin.AndroidX.Activity.targets" Condition="Exists('..\packages\Xamarin.AndroidX.Activity.1.6.0\build\monoandroid12.0\Xamarin.AndroidX.Activity.targets')" />
|
<Import Project="..\packages\Xamarin.AndroidX.Activity.1.6.0\build\monoandroid12.0\Xamarin.AndroidX.Activity.targets" Condition="Exists('..\packages\Xamarin.AndroidX.Activity.1.6.0\build\monoandroid12.0\Xamarin.AndroidX.Activity.targets')" />
|
||||||
<Import Project="..\packages\Xamarin.AndroidX.Fragment.1.5.3\build\monoandroid12.0\Xamarin.AndroidX.Fragment.targets" Condition="Exists('..\packages\Xamarin.AndroidX.Fragment.1.5.3\build\monoandroid12.0\Xamarin.AndroidX.Fragment.targets')" />
|
<Import Project="..\packages\Xamarin.AndroidX.Fragment.1.5.3\build\monoandroid12.0\Xamarin.AndroidX.Fragment.targets" Condition="Exists('..\packages\Xamarin.AndroidX.Fragment.1.5.3\build\monoandroid12.0\Xamarin.AndroidX.Fragment.targets')" />
|
||||||
@@ -379,4 +370,5 @@
|
|||||||
<Import Project="..\packages\Xamarin.GooglePlayServices.Auth.Base.118.0.6\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Auth.Base.targets" Condition="Exists('..\packages\Xamarin.GooglePlayServices.Auth.Base.118.0.6\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Auth.Base.targets')" />
|
<Import Project="..\packages\Xamarin.GooglePlayServices.Auth.Base.118.0.6\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Auth.Base.targets" Condition="Exists('..\packages\Xamarin.GooglePlayServices.Auth.Base.118.0.6\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Auth.Base.targets')" />
|
||||||
<Import Project="..\packages\Xamarin.GooglePlayServices.Fido.119.0.0\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Fido.targets" Condition="Exists('..\packages\Xamarin.GooglePlayServices.Fido.119.0.0\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Fido.targets')" />
|
<Import Project="..\packages\Xamarin.GooglePlayServices.Fido.119.0.0\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Fido.targets" Condition="Exists('..\packages\Xamarin.GooglePlayServices.Fido.119.0.0\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Fido.targets')" />
|
||||||
<Import Project="..\packages\Xamarin.GooglePlayServices.Auth.120.4.0\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Auth.targets" Condition="Exists('..\packages\Xamarin.GooglePlayServices.Auth.120.4.0\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Auth.targets')" />
|
<Import Project="..\packages\Xamarin.GooglePlayServices.Auth.120.4.0\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Auth.targets" Condition="Exists('..\packages\Xamarin.GooglePlayServices.Auth.120.4.0\build\MonoAndroid12.0\Xamarin.GooglePlayServices.Auth.targets')" />
|
||||||
|
<Import Project="..\packages\Xamarin.KotlinX.Coroutines.Android.1.6.4\build\monoandroid12.0\Xamarin.KotlinX.Coroutines.Android.targets" Condition="Exists('..\packages\Xamarin.KotlinX.Coroutines.Android.1.6.4\build\monoandroid12.0\Xamarin.KotlinX.Coroutines.Android.targets')" />
|
||||||
</Project>
|
</Project>
|
||||||
15
src/JavaFileStorageBindings/app.config
Normal file
15
src/JavaFileStorageBindings/app.config
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<runtime>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="Google.Apis.Auth" publicKeyToken="4b01fa6e34db77ab" culture="neutral" />
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-1.67.0.0" newVersion="1.67.0.0" />
|
||||||
|
</dependentAssembly>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
</runtime>
|
||||||
|
</configuration>
|
||||||
@@ -24,8 +24,8 @@
|
|||||||
<package id="Xamarin.AndroidX.VersionedParcelable" version="1.1.1.14" targetFramework="monoandroid13.0" />
|
<package id="Xamarin.AndroidX.VersionedParcelable" version="1.1.1.14" targetFramework="monoandroid13.0" />
|
||||||
<package id="Xamarin.AndroidX.ViewPager" version="1.0.0.14" targetFramework="monoandroid13.0" />
|
<package id="Xamarin.AndroidX.ViewPager" version="1.0.0.14" targetFramework="monoandroid13.0" />
|
||||||
<package id="Xamarin.Build.Download" version="0.11.4" targetFramework="monoandroid13.0" />
|
<package id="Xamarin.Build.Download" version="0.11.4" targetFramework="monoandroid13.0" />
|
||||||
<package id="Xamarin.Google.Guava" version="28.2.0.1" targetFramework="monoandroid90" />
|
<package id="Xamarin.Google.Guava" version="28.2.0.1" targetFramework="monoandroid13.0" />
|
||||||
<package id="Xamarin.Google.Guava.FailureAccess" version="1.0.1.3" targetFramework="monoandroid90" />
|
<package id="Xamarin.Google.Guava.FailureAccess" version="1.0.1.3" targetFramework="monoandroid13.0" />
|
||||||
<package id="Xamarin.Google.Guava.ListenableFuture" version="1.0.0.9" targetFramework="monoandroid13.0" />
|
<package id="Xamarin.Google.Guava.ListenableFuture" version="1.0.0.9" targetFramework="monoandroid13.0" />
|
||||||
<package id="Xamarin.GooglePlayServices.Auth" version="120.4.0" targetFramework="monoandroid13.0" />
|
<package id="Xamarin.GooglePlayServices.Auth" version="120.4.0" targetFramework="monoandroid13.0" />
|
||||||
<package id="Xamarin.GooglePlayServices.Auth.Api.Phone" version="118.0.1.2" targetFramework="monoandroid13.0" />
|
<package id="Xamarin.GooglePlayServices.Auth.Api.Phone" version="118.0.1.2" targetFramework="monoandroid13.0" />
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "keepass2android-app", "keep
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kp2aAutofillParser", "Kp2aAutofillParser\Kp2aAutofillParser.csproj", "{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kp2aAutofillParser", "Kp2aAutofillParser\Kp2aAutofillParser.csproj", "{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kp2aAutofillParserTest", "Kp2aAutofillParserTest\Kp2aAutofillParserTest.csproj", "{3D1560FF-86BB-4CB4-8367-80BA13B81C38}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kp2aAutofillParserTest", "Kp2aAutofillParserTest\Kp2aAutofillParserTest.csproj", "{3D1560FF-86BB-4CB4-8367-80BA13B81C38}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
|||||||
@@ -58,12 +58,12 @@ namespace keepass2android
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string LogFilename
|
public static string LogFilename
|
||||||
{
|
{
|
||||||
get { return Application.Context.FilesDir.CanonicalPath +"/keepass2android.log"; }
|
get { return Application.Context.FilesDir.CanonicalPath +"/keepass2android.log"; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool LogToFile
|
public static bool LogToFile
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ namespace KeePassLib.Serialization
|
|||||||
byte[] pbFile = StrUtil.Utf8.GetBytes(sb.ToString());
|
byte[] pbFile = StrUtil.Utf8.GetBytes(sb.ToString());
|
||||||
|
|
||||||
s = IOConnection.OpenWrite(iocLockFile);
|
s = IOConnection.OpenWrite(iocLockFile);
|
||||||
if(s == null) throw new IOException(iocLockFile.GetDisplayName());
|
if(s == null) throw new IOException(UrlUtil.GetFileName(iocLockFile.Path));
|
||||||
s.Write(pbFile, 0, pbFile.Length);
|
s.Write(pbFile, 0, pbFile.Length);
|
||||||
}
|
}
|
||||||
finally { if(s != null) s.Close(); }
|
finally { if(s != null) s.Close(); }
|
||||||
@@ -205,8 +205,7 @@ namespace KeePassLib.Serialization
|
|||||||
if(lfiEx != null)
|
if(lfiEx != null)
|
||||||
{
|
{
|
||||||
m_iocLockFile = null; // Otherwise Dispose deletes the existing one
|
m_iocLockFile = null; // Otherwise Dispose deletes the existing one
|
||||||
throw new FileLockException(iocBaseFile.GetDisplayName(),
|
throw new FileLockException(UrlUtil.GetFileName(iocBaseFile.Path), lfiEx.GetOwner());
|
||||||
lfiEx.GetOwner());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LockFileInfo.Create(m_iocLockFile);
|
LockFileInfo.Create(m_iocLockFile);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ using System.Diagnostics;
|
|||||||
|
|
||||||
using KeePassLib.Resources;
|
using KeePassLib.Resources;
|
||||||
using KeePassLib.Serialization;
|
using KeePassLib.Serialization;
|
||||||
|
using Android.Webkit;
|
||||||
|
|
||||||
namespace KeePassLib.Utility
|
namespace KeePassLib.Utility
|
||||||
{
|
{
|
||||||
@@ -411,7 +412,7 @@ Clipboard.SetText(ObjectsToMessage(vLines, true));*/
|
|||||||
public static void ShowLoadWarning(IOConnectionInfo ioConnection, Exception ex)
|
public static void ShowLoadWarning(IOConnectionInfo ioConnection, Exception ex)
|
||||||
{
|
{
|
||||||
if (ioConnection != null)
|
if (ioConnection != null)
|
||||||
ShowLoadWarning(ioConnection.GetDisplayName(), ex, false);
|
ShowLoadWarning(UrlUtil.GetFileName(ioConnection.Path), ex, false);
|
||||||
else ShowWarning(ex);
|
else ShowWarning(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,7 +445,7 @@ Clipboard.SetText(ObjectsToMessage(vLines, true));*/
|
|||||||
bool bCorruptionWarning)
|
bool bCorruptionWarning)
|
||||||
{
|
{
|
||||||
if (ioConnection != null)
|
if (ioConnection != null)
|
||||||
ShowSaveWarning(ioConnection.GetDisplayName(), ex, bCorruptionWarning);
|
ShowSaveWarning(UrlUtil.GetFileName(ioConnection.Path), ex, bCorruptionWarning);
|
||||||
else ShowWarning(ex);
|
else ShowWarning(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -434,7 +434,7 @@ namespace Kp2aAutofillParser
|
|||||||
public static List<string> ConvertToCanonicalLowerCaseHints(string[] supportedHints)
|
public static List<string> ConvertToCanonicalLowerCaseHints(string[] supportedHints)
|
||||||
{
|
{
|
||||||
List<string> result = new List<string>();
|
List<string> result = new List<string>();
|
||||||
foreach (string hint in supportedHints)
|
foreach (string hint in supportedHints.Where(h => h != null))
|
||||||
{
|
{
|
||||||
var canonicalHint = ToCanonicalHint(hint);
|
var canonicalHint = ToCanonicalHint(hint);
|
||||||
result.Add(canonicalHint.ToLower());
|
result.Add(canonicalHint.ToLower());
|
||||||
@@ -829,7 +829,7 @@ namespace Kp2aAutofillParser
|
|||||||
// * if there is no such autofill hint, we use IsPassword to
|
// * if there is no such autofill hint, we use IsPassword to
|
||||||
|
|
||||||
HashSet<string> autofillHintsOfAllFields = autofillView.InputFields.Where(f => f.AutofillHints != null)
|
HashSet<string> autofillHintsOfAllFields = autofillView.InputFields.Where(f => f.AutofillHints != null)
|
||||||
.SelectMany(f => f.AutofillHints).Select(AutofillHintsHelper.ToCanonicalHint).ToHashSet();
|
.SelectMany(f => f.AutofillHints).Where(x => x != null).Select(AutofillHintsHelper.ToCanonicalHint).ToHashSet();
|
||||||
bool hasLoginAutofillHints = autofillHintsOfAllFields.Intersect(_autofillHintsForLogin).Any();
|
bool hasLoginAutofillHints = autofillHintsOfAllFields.Intersect(_autofillHintsForLogin).Any();
|
||||||
|
|
||||||
if (hasLoginAutofillHints)
|
if (hasLoginAutofillHints)
|
||||||
@@ -839,9 +839,9 @@ namespace Kp2aAutofillParser
|
|||||||
string[] viewHints = viewNode.AutofillHints;
|
string[] viewHints = viewNode.AutofillHints;
|
||||||
if (viewHints == null)
|
if (viewHints == null)
|
||||||
continue;
|
continue;
|
||||||
if (viewHints.Select(AutofillHintsHelper.ToCanonicalHint).Intersect(_autofillHintsForLogin).Any())
|
if (viewHints.Where(h => h != null).Select(AutofillHintsHelper.ToCanonicalHint).Intersect(_autofillHintsForLogin).Any())
|
||||||
{
|
{
|
||||||
AddFieldToHintMap(viewNode, viewHints.Select(AutofillHintsHelper.ToCanonicalHint).ToHashSet().ToArray());
|
AddFieldToHintMap(viewNode, viewHints.Where(h => h != null).Select(AutofillHintsHelper.ToCanonicalHint).ToHashSet().ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -71,6 +71,12 @@ namespace Kp2aAutofillParserTest
|
|||||||
var resourceName = "Kp2aAutofillParserTest.com-expressvpn-vpn-android13.json";
|
var resourceName = "Kp2aAutofillParserTest.com-expressvpn-vpn-android13.json";
|
||||||
RunTestFromAutofillInput(resourceName, "com.expressvpn.vpn", null);
|
RunTestFromAutofillInput(resourceName, "com.expressvpn.vpn", null);
|
||||||
}
|
}
|
||||||
|
[Fact]
|
||||||
|
public void TestIgnoresAndroidSettings()
|
||||||
|
{
|
||||||
|
var resourceName = "Kp2aAutofillParserTest.android14-settings.json";
|
||||||
|
RunTestFromAutofillInput(resourceName, "com.android.settings", null);
|
||||||
|
}
|
||||||
|
|
||||||
private void RunTestFromAutofillInput(string resourceName, string expectedPackageName = null, string expectedWebDomain = null)
|
private void RunTestFromAutofillInput(string resourceName, string expectedPackageName = null, string expectedWebDomain = null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<None Remove="android14-settings.json" />
|
||||||
<None Remove="chrome-android10-amazon-it.json" />
|
<None Remove="chrome-android10-amazon-it.json" />
|
||||||
<None Remove="com-expressvpn-vpn-android13.json" />
|
<None Remove="com-expressvpn-vpn-android13.json" />
|
||||||
<None Remove="com-ifs-banking-fiid3364-android13.json" />
|
<None Remove="com-ifs-banking-fiid3364-android13.json" />
|
||||||
@@ -19,13 +20,13 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="xunit" Version="2.4.2" />
|
<PackageReference Include="xunit" Version="2.4.2" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="coverlet.collector" Version="3.1.2">
|
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
@@ -54,6 +55,9 @@
|
|||||||
<EmbeddedResource Include="com-servicenet-mobile-no-focus.json">
|
<EmbeddedResource Include="com-servicenet-mobile-no-focus.json">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Include="android14-settings.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</EmbeddedResource>
|
||||||
<EmbeddedResource Include="imdb.json">
|
<EmbeddedResource Include="imdb.json">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
|
|||||||
99
src/Kp2aAutofillParserTest/android14-settings.json
Normal file
99
src/Kp2aAutofillParserTest/android14-settings.json
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
{
|
||||||
|
"InputFields": [
|
||||||
|
{
|
||||||
|
"IdEntry": null,
|
||||||
|
"Hint": null,
|
||||||
|
"ClassName": "android.widget.FrameLayout",
|
||||||
|
"AutofillHints": null,
|
||||||
|
"IsFocused": false,
|
||||||
|
"InputType": 0,
|
||||||
|
"HtmlInfoTag": null,
|
||||||
|
"HtmlInfoTypeAttribute": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"IdEntry": "content_parent",
|
||||||
|
"Hint": null,
|
||||||
|
"ClassName": "android.widget.LinearLayout",
|
||||||
|
"AutofillHints": null,
|
||||||
|
"IsFocused": false,
|
||||||
|
"InputType": 0,
|
||||||
|
"HtmlInfoTag": null,
|
||||||
|
"HtmlInfoTypeAttribute": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"IdEntry": "content_frame",
|
||||||
|
"Hint": null,
|
||||||
|
"ClassName": "android.widget.FrameLayout",
|
||||||
|
"AutofillHints": null,
|
||||||
|
"IsFocused": false,
|
||||||
|
"InputType": 0,
|
||||||
|
"HtmlInfoTag": null,
|
||||||
|
"HtmlInfoTypeAttribute": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"IdEntry": "main_content",
|
||||||
|
"Hint": null,
|
||||||
|
"ClassName": "android.widget.FrameLayout",
|
||||||
|
"AutofillHints": null,
|
||||||
|
"IsFocused": false,
|
||||||
|
"InputType": 0,
|
||||||
|
"HtmlInfoTag": null,
|
||||||
|
"HtmlInfoTypeAttribute": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"IdEntry": "password_entry",
|
||||||
|
"Hint": null,
|
||||||
|
"ClassName": "android.widget.EditText",
|
||||||
|
"AutofillHints": [
|
||||||
|
"passwordAuto"
|
||||||
|
],
|
||||||
|
"IsFocused": true,
|
||||||
|
"InputType": 18,
|
||||||
|
"HtmlInfoTag": null,
|
||||||
|
"HtmlInfoTypeAttribute": null,
|
||||||
|
"ExpectedAssignedHints": [ "password" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"IdEntry": "checkbox",
|
||||||
|
"Hint": null,
|
||||||
|
"ClassName": "android.widget.CheckBox",
|
||||||
|
"AutofillHints": null,
|
||||||
|
"IsFocused": false,
|
||||||
|
"InputType": 0,
|
||||||
|
"HtmlInfoTag": null,
|
||||||
|
"HtmlInfoTypeAttribute": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"IdEntry": "button_bar",
|
||||||
|
"Hint": null,
|
||||||
|
"ClassName": "android.widget.RelativeLayout",
|
||||||
|
"AutofillHints": null,
|
||||||
|
"IsFocused": false,
|
||||||
|
"InputType": 0,
|
||||||
|
"HtmlInfoTag": null,
|
||||||
|
"HtmlInfoTypeAttribute": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"IdEntry": "switch_bar",
|
||||||
|
"Hint": null,
|
||||||
|
"ClassName": "android.widget.LinearLayout",
|
||||||
|
"AutofillHints": null,
|
||||||
|
"IsFocused": false,
|
||||||
|
"InputType": 0,
|
||||||
|
"HtmlInfoTag": null,
|
||||||
|
"HtmlInfoTypeAttribute": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"IdEntry": "action_bar",
|
||||||
|
"Hint": null,
|
||||||
|
"ClassName": "android.view.ViewGroup",
|
||||||
|
"AutofillHints": null,
|
||||||
|
"IsFocused": false,
|
||||||
|
"InputType": 0,
|
||||||
|
"HtmlInfoTag": null,
|
||||||
|
"HtmlInfoTypeAttribute": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PackageId": "com.android.settings",
|
||||||
|
"WebDomain": null
|
||||||
|
}
|
||||||
@@ -124,7 +124,7 @@ namespace keepass2android.Io
|
|||||||
&& File.Exists(VersionFilePath(ioc))
|
&& File.Exists(VersionFilePath(ioc))
|
||||||
&& File.Exists(BaseVersionFilePath(ioc));
|
&& File.Exists(BaseVersionFilePath(ioc));
|
||||||
|
|
||||||
Kp2aLog.Log(ioc.GetDisplayName() + " isCached = " + result);
|
Kp2aLog.Log(GetDisplayName(ioc) + " isCached = " + result);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -598,13 +598,14 @@ namespace keepass2android.Io
|
|||||||
public string GetBaseVersionHash(IOConnectionInfo ioc)
|
public string GetBaseVersionHash(IOConnectionInfo ioc)
|
||||||
{
|
{
|
||||||
string hash = File.ReadAllText(BaseVersionFilePath(ioc));
|
string hash = File.ReadAllText(BaseVersionFilePath(ioc));
|
||||||
Kp2aLog.Log(ioc.GetDisplayName() + " baseVersionHash = " + hash);
|
Kp2aLog.Log(GetDisplayName(ioc) + " baseVersionHash = " + hash);
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
public string GetLocalVersionHash(IOConnectionInfo ioc)
|
public string GetLocalVersionHash(IOConnectionInfo ioc)
|
||||||
{
|
{
|
||||||
string hash = File.ReadAllText(VersionFilePath(ioc));
|
string hash = File.ReadAllText(VersionFilePath(ioc));
|
||||||
Kp2aLog.Log(ioc.GetDisplayName() + " localVersionHash = " + hash);
|
|
||||||
|
Kp2aLog.Log(GetDisplayName(ioc) + " localVersionHash = " + hash);
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
public bool HasLocalChanges(IOConnectionInfo ioc)
|
public bool HasLocalChanges(IOConnectionInfo ioc)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ namespace keepass2android.Io
|
|||||||
public abstract bool UserShouldBackup { get; }
|
public abstract bool UserShouldBackup { get; }
|
||||||
|
|
||||||
|
|
||||||
private readonly IJavaFileStorage _jfs;
|
protected readonly IJavaFileStorage _jfs;
|
||||||
private readonly IKp2aApp _app;
|
private readonly IKp2aApp _app;
|
||||||
|
|
||||||
public JavaFileStorage(IJavaFileStorage jfs, IKp2aApp app)
|
public JavaFileStorage(IJavaFileStorage jfs, IKp2aApp app)
|
||||||
@@ -348,7 +348,7 @@ namespace keepass2android.Io
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
throw new Exception("Error finding " + filename + " in " + folderPath.GetDisplayName(), e);
|
throw new Exception("Error finding " + filename + " in " + GetDisplayName(folderPath), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
#if !NoNet
|
#if !NoNet
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Reflection;
|
|
||||||
using System.Threading;
|
|
||||||
using Android.Content;
|
using Android.Content;
|
||||||
using Android.OS;
|
using Android.OS;
|
||||||
using Android.Preferences;
|
|
||||||
using FluentFTP;
|
using FluentFTP;
|
||||||
|
using FluentFTP.Exceptions;
|
||||||
using KeePassLib;
|
using KeePassLib;
|
||||||
using KeePassLib.Serialization;
|
using KeePassLib.Serialization;
|
||||||
using KeePassLib.Utility;
|
using KeePassLib.Utility;
|
||||||
|
|
||||||
|
|
||||||
namespace keepass2android.Io
|
namespace keepass2android.Io
|
||||||
{
|
{
|
||||||
public class NetFtpFileStorage: IFileStorage
|
public class NetFtpFileStorage: IFileStorage
|
||||||
@@ -75,14 +75,15 @@ namespace keepass2android.Io
|
|||||||
}
|
}
|
||||||
|
|
||||||
private readonly ICertificateValidationHandler _app;
|
private readonly ICertificateValidationHandler _app;
|
||||||
|
private readonly Func<bool> _debugLogPrefGetter;
|
||||||
|
|
||||||
public MemoryStream traceStream;
|
public MemoryStream traceStream;
|
||||||
|
|
||||||
public NetFtpFileStorage(Context context, ICertificateValidationHandler app)
|
public NetFtpFileStorage(Context context, ICertificateValidationHandler app, Func<bool> debugLogPrefGetter)
|
||||||
{
|
{
|
||||||
_app = app;
|
_app = app;
|
||||||
traceStream = new MemoryStream();
|
_debugLogPrefGetter = debugLogPrefGetter;
|
||||||
|
traceStream = new MemoryStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<string> SupportedProtocols
|
public IEnumerable<string> SupportedProtocols
|
||||||
@@ -138,7 +139,7 @@ namespace keepass2android.Io
|
|||||||
var settings = ConnectionSettings.FromIoc(ioc);
|
var settings = ConnectionSettings.FromIoc(ioc);
|
||||||
|
|
||||||
FtpClient client = new FtpClient();
|
FtpClient client = new FtpClient();
|
||||||
client.RetryAttempts = 3;
|
client.Config.RetryAttempts = 3;
|
||||||
if ((settings.Username.Length > 0) || (settings.Password.Length > 0))
|
if ((settings.Username.Length > 0) || (settings.Password.Length > 0))
|
||||||
client.Credentials = new NetworkCredential(settings.Username, settings.Password);
|
client.Credentials = new NetworkCredential(settings.Username, settings.Password);
|
||||||
else
|
else
|
||||||
@@ -154,9 +155,12 @@ namespace keepass2android.Io
|
|||||||
args.Accept = _app.CertificateValidationCallback(control, args.Certificate, args.Chain, args.PolicyErrors);
|
args.Accept = _app.CertificateValidationCallback(control, args.Certificate, args.Chain, args.PolicyErrors);
|
||||||
};
|
};
|
||||||
|
|
||||||
client.EncryptionMode = settings.EncryptionMode;
|
client.Config.EncryptionMode = settings.EncryptionMode;
|
||||||
|
|
||||||
client.Connect();
|
if (_debugLogPrefGetter())
|
||||||
|
client.Logger = new Kp2aLogFTPLogger();
|
||||||
|
|
||||||
|
client.Connect();
|
||||||
return client;
|
return client;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -284,42 +288,55 @@ namespace keepass2android.Io
|
|||||||
|
|
||||||
public IEnumerable<FileDescription> ListContents(IOConnectionInfo ioc)
|
public IEnumerable<FileDescription> ListContents(IOConnectionInfo ioc)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var client = GetClient(ioc))
|
using (var client = GetClient(ioc))
|
||||||
{
|
{
|
||||||
|
/*
|
||||||
|
* For some reason GetListing(path) does not always return the contents of the directory.
|
||||||
|
* However, calling SetWorkingDirectory(path) followed by GetListing(null, options) to
|
||||||
|
* list the contents of the working directory does consistently work.
|
||||||
|
*
|
||||||
|
* Similar behavior was confirmed using ncftp client. I suspect this is a strange
|
||||||
|
* bug/nuance in the server's implementation of the LIST command?
|
||||||
|
*
|
||||||
|
* [bug #2423]
|
||||||
|
*/
|
||||||
|
client.SetWorkingDirectory(IocToLocalPath(ioc));
|
||||||
|
|
||||||
List<FileDescription> files = new List<FileDescription>();
|
List<FileDescription> files = new List<FileDescription>();
|
||||||
foreach (FtpListItem item in client.GetListing(IocToLocalPath(ioc),
|
foreach (FtpListItem item in client.GetListing(null,
|
||||||
FtpListOption.Modify | FtpListOption.Size | FtpListOption.DerefLinks))
|
FtpListOption.SizeModify | FtpListOption.AllFiles))
|
||||||
{
|
{
|
||||||
|
switch (item.Type)
|
||||||
switch (item.Type)
|
|
||||||
{
|
{
|
||||||
case FtpFileSystemObjectType.Directory:
|
case FtpObjectType.Directory:
|
||||||
files.Add(new FileDescription()
|
files.Add(new FileDescription()
|
||||||
{
|
{
|
||||||
CanRead = true,
|
CanRead = true,
|
||||||
CanWrite = true,
|
CanWrite = true,
|
||||||
DisplayName = item.Name,
|
DisplayName = item.Name,
|
||||||
IsDirectory = true,
|
IsDirectory = true,
|
||||||
LastModified = item.Modified,
|
LastModified = item.Modified,
|
||||||
Path = IocPathFromUri(ioc, item.FullName)
|
Path = IocPathFromUri(ioc, item.FullName)
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case FtpFileSystemObjectType.File:
|
case FtpObjectType.File:
|
||||||
files.Add(new FileDescription()
|
files.Add(new FileDescription()
|
||||||
{
|
{
|
||||||
CanRead = true,
|
CanRead = true,
|
||||||
CanWrite = true,
|
CanWrite = true,
|
||||||
DisplayName = item.Name,
|
DisplayName = item.Name,
|
||||||
IsDirectory = false,
|
IsDirectory = false,
|
||||||
LastModified = item.Modified,
|
LastModified = item.Modified,
|
||||||
Path = IocPathFromUri(ioc, item.FullName),
|
Path = IocPathFromUri(ioc, item.FullName),
|
||||||
SizeInBytes = item.Size
|
SizeInBytes = item.Size
|
||||||
});
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Kp2aLog.Log("FTP: ListContents item skipped: " + IocToUri(ioc) + ": " + item.FullName + ", type=" + item.Type);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
@@ -329,7 +346,6 @@ namespace keepass2android.Io
|
|||||||
throw ConvertException(ex);
|
throw ConvertException(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public FileDescription GetFileDescription(IOConnectionInfo ioc)
|
public FileDescription GetFileDescription(IOConnectionInfo ioc)
|
||||||
{
|
{
|
||||||
@@ -466,7 +482,9 @@ namespace keepass2android.Io
|
|||||||
|
|
||||||
public static int GetDefaultPort(FtpEncryptionMode encryption)
|
public static int GetDefaultPort(FtpEncryptionMode encryption)
|
||||||
{
|
{
|
||||||
return new FtpClient() { EncryptionMode = encryption}.Port;
|
var client = new FtpClient();
|
||||||
|
client.Config.EncryptionMode = encryption;
|
||||||
|
return client.Port;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string BuildFullPath(string host, int port, string initialPath, string user, string password, FtpEncryptionMode encryption)
|
public string BuildFullPath(string host, int port, string initialPath, string user, string password, FtpEncryptionMode encryption)
|
||||||
@@ -582,5 +600,13 @@ namespace keepass2android.Io
|
|||||||
_stream.Close();
|
_stream.Close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Kp2aLogFTPLogger : IFtpLogger
|
||||||
|
{
|
||||||
|
public void Log(FtpLogEntry entry)
|
||||||
|
{
|
||||||
|
Kp2aLog.Log("[FluentFTP] " + entry.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -1,31 +1,23 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
using Android.App;
|
|
||||||
using Android.Content;
|
using Android.Content;
|
||||||
using Android.OS;
|
using Android.OS;
|
||||||
using Android.Runtime;
|
|
||||||
using Android.Views;
|
|
||||||
using Android.Widget;
|
|
||||||
using KeePassLib.Serialization;
|
using KeePassLib.Serialization;
|
||||||
#if !EXCLUDE_JAVAFILESTORAGE
|
using Exception = Java.Lang.Exception;
|
||||||
using Keepass2android.Javafilestorage;
|
|
||||||
|
|
||||||
namespace keepass2android.Io
|
namespace keepass2android.Io
|
||||||
{
|
{
|
||||||
public class OneDriveFileStorage: JavaFileStorage
|
/// <summary>
|
||||||
|
/// This IFileStorage implementation becomes picked if a user is using a skydrive:// or onedrive:// file.
|
||||||
|
/// These refer to an old (Java) implementation which was replaced starting in 2019. The successor uses onedrive2:// (see OneDrive2FileStorage)
|
||||||
|
/// The Java implementation was removed in 2024 when the jar files became unavailable. We are keeping this file to notify any user who haven't updated their
|
||||||
|
/// file storage within 5 years.
|
||||||
|
/// This file should be removed around mid 2025.
|
||||||
|
/// </summary>
|
||||||
|
public class OneDriveFileStorage: IFileStorage
|
||||||
{
|
{
|
||||||
private const string ClientId = "000000004010C234";
|
|
||||||
|
public IEnumerable<string> SupportedProtocols
|
||||||
public OneDriveFileStorage(Context ctx, IKp2aApp app) :
|
|
||||||
base(new Keepass2android.Javafilestorage.OneDriveStorage(ctx, ClientId), app)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override IEnumerable<string> SupportedProtocols
|
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
@@ -34,10 +26,146 @@ namespace keepass2android.Io
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool UserShouldBackup
|
private Exception GetDeprecatedMessage()
|
||||||
|
{
|
||||||
|
return new Exception(
|
||||||
|
"You have opened your file through a deprecated Microsoft API. Please select Change database, Open Database and then select One Drive again.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UserShouldBackup
|
||||||
{
|
{
|
||||||
get { return false; }
|
get { return false; }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public void Delete(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CheckForFileChangeFast(IOConnectionInfo ioc, string previousFileVersion)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetCurrentFileVersionFast(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream OpenFileForRead(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetFileExtension(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RequiresCredentials(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CreateDirectory(IOConnectionInfo ioc, string newDirName)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<FileDescription> ListContents(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileDescription GetFileDescription(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RequiresSetup(IOConnectionInfo ioConnection)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string IocToPath(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartSelectFile(IFileStorageSetupInitiatorActivity activity, bool isForSave, int requestCode, string protocolId)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PrepareFileUsage(IFileStorageSetupInitiatorActivity activity, IOConnectionInfo ioc, int requestCode,
|
||||||
|
bool alwaysReturnSuccess)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PrepareFileUsage(Context ctx, IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnCreate(IFileStorageSetupActivity activity, Bundle savedInstanceState)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnResume(IFileStorageSetupActivity activity)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnStart(IFileStorageSetupActivity activity)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnActivityResult(IFileStorageSetupActivity activity, int requestCode, int resultCode, Intent data)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetDisplayName(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CreateFilePath(string parent, string newFilename)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IOConnectionInfo GetParentPath(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IOConnectionInfo GetFilePath(IOConnectionInfo folderPath, string filename)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsPermanentLocation(IOConnectionInfo ioc)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsReadOnly(IOConnectionInfo ioc, OptionalOut<UiStringKey> reason = null)
|
||||||
|
{
|
||||||
|
throw GetDeprecatedMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
@@ -5,7 +5,7 @@ namespace keepass2android.Io
|
|||||||
{
|
{
|
||||||
public class PCloudFileStorage: JavaFileStorage
|
public class PCloudFileStorage: JavaFileStorage
|
||||||
{
|
{
|
||||||
private const string ClientId = "CkRWTQXY6Lm";
|
private const string ClientId = "yCeH59Ffgtm";
|
||||||
|
|
||||||
public PCloudFileStorage(Context ctx, IKp2aApp app) :
|
public PCloudFileStorage(Context ctx, IKp2aApp app) :
|
||||||
base(new Keepass2android.Javafilestorage.PCloudFileStorage(ctx, ClientId, "pcloud", ""), app)
|
base(new Keepass2android.Javafilestorage.PCloudFileStorage(ctx, ClientId, "pcloud", ""), app)
|
||||||
|
|||||||
@@ -1,17 +1,39 @@
|
|||||||
using Android.Content;
|
using Android.Content;
|
||||||
|
using Java.Nio.FileNio;
|
||||||
#if !EXCLUDE_JAVAFILESTORAGE
|
#if !EXCLUDE_JAVAFILESTORAGE
|
||||||
|
|
||||||
namespace keepass2android.Io
|
namespace keepass2android.Io
|
||||||
{
|
{
|
||||||
public class SftpFileStorage: JavaFileStorage
|
public class SftpFileStorage: JavaFileStorage
|
||||||
{
|
{
|
||||||
public SftpFileStorage(Context ctx, IKp2aApp app) :
|
public SftpFileStorage(Context ctx, IKp2aApp app, bool debugEnabled) :
|
||||||
base(new Keepass2android.Javafilestorage.SftpStorage(ctx.ApplicationContext), app)
|
base(new Keepass2android.Javafilestorage.SftpStorage(ctx.ApplicationContext), app)
|
||||||
{
|
{
|
||||||
}
|
var storage = BaseStorage;
|
||||||
|
if (debugEnabled)
|
||||||
|
{
|
||||||
|
string? logFilename = null;
|
||||||
|
if (Kp2aLog.LogToFile)
|
||||||
|
{
|
||||||
|
logFilename = Kp2aLog.LogFilename;
|
||||||
|
}
|
||||||
|
storage.SetJschLogging(true, logFilename);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
storage.SetJschLogging(false, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Keepass2android.Javafilestorage.SftpStorage BaseStorage
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _jfs as Keepass2android.Javafilestorage.SftpStorage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override bool UserShouldBackup
|
public override bool UserShouldBackup
|
||||||
{
|
{
|
||||||
get { return true; }
|
get { return true; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" InitialTargets="showFlavor">
|
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" InitialTargets="showFlavor">
|
||||||
<Target Name="showFlavor" AfterTargets="Build">
|
<Target Name="showFlavor" AfterTargets="Build">
|
||||||
<Message Importance="high" Text="building flavor $(Flavor)"></Message>
|
<Message Importance="high" Text="building flavor $(Flavor)">
|
||||||
</Target>
|
</Message>
|
||||||
<Import Project="../build-properties.props"/>
|
</Target>
|
||||||
|
<Import Project="../build-properties.props" />
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
@@ -182,10 +183,10 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Condition=" '$(Flavor)'!='NoNet' ">
|
<ItemGroup Condition=" '$(Flavor)'!='NoNet' ">
|
||||||
<PackageReference Include="FluentFTP">
|
<PackageReference Include="FluentFTP">
|
||||||
<Version>31.3.1</Version>
|
<Version>51.1.0</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="MegaApiClient">
|
<PackageReference Include="MegaApiClient">
|
||||||
<Version>1.10.3</Version>
|
<Version>1.10.4</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Graph">
|
<PackageReference Include="Microsoft.Graph">
|
||||||
<Version>1.21.0</Version>
|
<Version>1.21.0</Version>
|
||||||
|
|||||||
@@ -91,7 +91,29 @@ namespace keepass2android
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String ExtractHost(String url)
|
public PwGroup SearchForUuid(Database database, string uuid)
|
||||||
|
{
|
||||||
|
SearchParameters sp = SearchParameters.None;
|
||||||
|
sp.SearchInUuids = true;
|
||||||
|
sp.SearchString = uuid;
|
||||||
|
|
||||||
|
if (sp.RegularExpression) // Validate regular expression
|
||||||
|
{
|
||||||
|
new Regex(sp.SearchString);
|
||||||
|
}
|
||||||
|
|
||||||
|
string strGroupName = _app.GetResourceString(UiStringKey.search_results);
|
||||||
|
PwGroup pgResults = new PwGroup(true, true, strGroupName, PwIcon.EMailSearch) { IsVirtual = true };
|
||||||
|
|
||||||
|
PwObjectList<PwEntry> listResults = pgResults.Entries;
|
||||||
|
|
||||||
|
database.Root.SearchEntries(sp, listResults, new NullStatusLogger());
|
||||||
|
|
||||||
|
return pgResults;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String ExtractHost(String url)
|
||||||
{
|
{
|
||||||
return UrlUtil.GetHost(url.Trim());
|
return UrlUtil.GetHost(url.Trim());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,8 +72,9 @@ namespace keepass2android
|
|||||||
}
|
}
|
||||||
|
|
||||||
private IDatabaseFormat _databaseFormat = new KdbxDatabaseFormat(KdbxFormat.Default);
|
private IDatabaseFormat _databaseFormat = new KdbxDatabaseFormat(KdbxFormat.Default);
|
||||||
|
private bool? _hasTotpEntries;
|
||||||
|
|
||||||
public bool ReloadRequested { get; set; }
|
public bool ReloadRequested { get; set; }
|
||||||
|
|
||||||
public bool DidOpenFileChange()
|
public bool DidOpenFileChange()
|
||||||
{
|
{
|
||||||
@@ -104,8 +105,9 @@ namespace keepass2android
|
|||||||
SearchHelper = new SearchDbHelper(app);
|
SearchHelper = new SearchDbHelper(app);
|
||||||
|
|
||||||
_databaseFormat = databaseFormat;
|
_databaseFormat = databaseFormat;
|
||||||
|
_hasTotpEntries = null;
|
||||||
|
|
||||||
CanWrite = databaseFormat.CanWrite && !fileStorage.IsReadOnly(iocInfo);
|
CanWrite = databaseFormat.CanWrite && !fileStorage.IsReadOnly(iocInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -174,10 +176,17 @@ namespace keepass2android
|
|||||||
PwGroup group = SearchHelper.SearchForExactUrl(this, url);
|
PwGroup group = SearchHelper.SearchForExactUrl(this, url);
|
||||||
|
|
||||||
return group;
|
return group;
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public PwGroup SearchForHost(String url, bool allowSubdomains) {
|
}
|
||||||
|
public PwGroup SearchForUuid(String uuid)
|
||||||
|
{
|
||||||
|
PwGroup group = SearchHelper.SearchForUuid(this, uuid);
|
||||||
|
|
||||||
|
return group;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public PwGroup SearchForHost(String url, bool allowSubdomains) {
|
||||||
PwGroup group = SearchHelper.SearchForHost(this, url, allowSubdomains);
|
PwGroup group = SearchHelper.SearchForHost(this, url, allowSubdomains);
|
||||||
|
|
||||||
return group;
|
return group;
|
||||||
@@ -193,8 +202,21 @@ namespace keepass2android
|
|||||||
|
|
||||||
trans.CommitWrite();
|
trans.CommitWrite();
|
||||||
}
|
}
|
||||||
|
_hasTotpEntries = null;
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasTotpEntries
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_hasTotpEntries == null)
|
||||||
|
{
|
||||||
|
_hasTotpEntries = true;
|
||||||
|
}
|
||||||
|
return _hasTotpEntries.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void PopulateGlobals(PwGroup currentGroup, bool checkForDuplicateUuids )
|
private void PopulateGlobals(PwGroup currentGroup, bool checkForDuplicateUuids )
|
||||||
{
|
{
|
||||||
|
|||||||
Binary file not shown.
BIN
src/PCloudBindings/Jars/pcloud-sdk-android-1.9.1.aar
Normal file
BIN
src/PCloudBindings/Jars/pcloud-sdk-android-1.9.1.aar
Normal file
Binary file not shown.
Binary file not shown.
BIN
src/PCloudBindings/Jars/pcloud-sdk-java-core-1.9.1.jar
Normal file
BIN
src/PCloudBindings/Jars/pcloud-sdk-java-core-1.9.1.jar
Normal file
Binary file not shown.
@@ -56,7 +56,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="Jars\AboutJars.txt" />
|
<None Include="Jars\AboutJars.txt" />
|
||||||
<None Include="Additions\AboutAdditions.txt" />
|
<None Include="Additions\AboutAdditions.txt" />
|
||||||
<LibraryProjectZip Include="Jars\pcloud-sdk-android-1.8.1.aar" />
|
<LibraryProjectZip Include="Jars\pcloud-sdk-android-1.9.1.aar" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<TransformFile Include="Transforms\Metadata.xml" />
|
<TransformFile Include="Transforms\Metadata.xml" />
|
||||||
@@ -72,6 +72,6 @@
|
|||||||
</Target>
|
</Target>
|
||||||
-->
|
-->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedReferenceJar Include="Jars\pcloud-sdk-java-core-1.8.1.jar" />
|
<EmbeddedReferenceJar Include="Jars\pcloud-sdk-java-core-1.9.1.jar" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ android {
|
|||||||
|
|
||||||
namespace 'keepass2android.javafilestorage'
|
namespace 'keepass2android.javafilestorage'
|
||||||
|
|
||||||
compileSdkVersion 33
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 15
|
minSdkVersion 21
|
||||||
targetSdkVersion 33
|
targetSdkVersion 33
|
||||||
|
compileSdk 34
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
@@ -22,6 +21,10 @@ android {
|
|||||||
sourceCompatibility 11
|
sourceCompatibility 11
|
||||||
targetCompatibility 11
|
targetCompatibility 11
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
exclude 'META-INF/DEPENDENCIES'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -30,27 +33,21 @@ NOTE: If you change dependencies here, don't forget to update the jar files in J
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.10.0-RC1'
|
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||||
implementation 'com.burgstaller:okhttp-digest:2.5'
|
implementation 'io.github.rburgst:okhttp-digest:3.1.0'
|
||||||
|
|
||||||
implementation 'com.google.http-client:google-http-client-gson:1.20.0'
|
implementation 'com.google.http-client:google-http-client-gson:1.20.0'
|
||||||
implementation('com.google.api-client:google-api-client-android:1.30.5') {
|
implementation('com.google.api-client:google-api-client-android:1.30.5') {
|
||||||
exclude group: 'com.google.android.google-play-services'
|
exclude group: 'com.google.android.google-play-services'
|
||||||
}
|
}
|
||||||
implementation 'com.google.apis:google-api-services-drive:v2-rev102-1.16.0-rc'
|
implementation 'com.google.apis:google-api-services-drive:v2-rev102-1.16.0-rc'
|
||||||
implementation 'com.dropbox.core:dropbox-core-sdk:4.0.0'
|
implementation 'com.dropbox.core:dropbox-core-sdk:5.4.6'
|
||||||
implementation 'com.google.api-client:google-api-client:1.30.5'
|
implementation 'com.google.api-client:google-api-client:1.30.5'
|
||||||
implementation 'com.google.api-client:google-api-client-android:1.30.5'
|
implementation 'com.google.api-client:google-api-client-android:1.30.5'
|
||||||
|
|
||||||
implementation 'com.google.android.gms:play-services-auth:20.4.0'
|
implementation 'com.google.android.gms:play-services-auth:20.4.0'
|
||||||
//onedrive:
|
implementation 'com.pcloud.sdk:java-core:1.9.1'
|
||||||
implementation('com.onedrive.sdk:onedrive-sdk-android:1.2.0') {
|
implementation 'com.pcloud.sdk:android:1.9.1'
|
||||||
transitive = false
|
|
||||||
}
|
|
||||||
implementation 'com.pcloud.sdk:java-core:1.8.1'
|
|
||||||
implementation 'com.pcloud.sdk:android:1.8.1'
|
|
||||||
implementation 'com.google.code.gson:gson:2.8.6'
|
implementation 'com.google.code.gson:gson:2.8.6'
|
||||||
implementation 'com.microsoft.services.msa:msa-auth:0.8.6'
|
|
||||||
implementation 'com.microsoft.aad:adal:1.14.0'
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,28 @@ public class FileEntry {
|
|||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder s = new StringBuilder("JavaFileStorage.FileEntry{").append(displayName).append("|")
|
||||||
|
.append("path=").append(path).append(",sz=").append(sizeInBytes)
|
||||||
|
.append(",").append(isDirectory ? "dir" : "file")
|
||||||
|
.append(",lastMod=").append(lastModifiedTime);
|
||||||
|
|
||||||
|
StringBuilder perms = new StringBuilder();
|
||||||
|
if (canRead)
|
||||||
|
perms.append("r");
|
||||||
|
if (canWrite)
|
||||||
|
perms.append("w");
|
||||||
|
if (perms.length() > 0) {
|
||||||
|
s.append(",").append(perms);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userData != null && userData.length() > 0)
|
||||||
|
s.append(",userData=").append(userData);
|
||||||
|
|
||||||
|
return s.append("}").toString();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package keepass2android.javafilestorage;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.jcraft.jsch.Logger;
|
||||||
|
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class Kp2aJSchLogger implements Logger {
|
||||||
|
|
||||||
|
private static final String PREFIX = "KP2AJFS[JSch]";
|
||||||
|
|
||||||
|
private interface ILogger {
|
||||||
|
void log(String message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface EntryToLogFactory {
|
||||||
|
ILogger create(LogEntry e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final EntryToLogFactory ANDROID_FACTORY = e -> e.logger;
|
||||||
|
|
||||||
|
private static final class LogEntry {
|
||||||
|
private final String levelTag;
|
||||||
|
private final ILogger logger;
|
||||||
|
|
||||||
|
LogEntry(String levelTag, ILogger logger) {
|
||||||
|
this.levelTag = levelTag;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static final ILogger DEBUG = msg -> Log.d(PREFIX, msg);
|
||||||
|
private static final LogEntry DEBUG_ENTRY = new LogEntry("D", DEBUG);
|
||||||
|
private static final ILogger ERROR = msg -> Log.e(PREFIX, msg);
|
||||||
|
private static final LogEntry DEFAULT_ENTRY = DEBUG_ENTRY;
|
||||||
|
|
||||||
|
private static final Map<Integer, LogEntry> loggers = Map.of(
|
||||||
|
Logger.DEBUG, DEBUG_ENTRY,
|
||||||
|
Logger.INFO, new LogEntry("I", msg -> Log.i(PREFIX, msg)),
|
||||||
|
Logger.WARN, new LogEntry("W", msg -> Log.w(PREFIX, msg)),
|
||||||
|
Logger.ERROR, new LogEntry("E", ERROR),
|
||||||
|
Logger.FATAL, new LogEntry("F", msg -> Log.wtf(PREFIX, msg))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
private final EntryToLogFactory logFactory;
|
||||||
|
|
||||||
|
static Kp2aJSchLogger createAndroidLogger() {
|
||||||
|
return new Kp2aJSchLogger(ANDROID_FACTORY);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Kp2aJSchLogger createFileLogger(String logFilename) {
|
||||||
|
final String fName = logFilename;
|
||||||
|
return new Kp2aJSchLogger(e -> createFileLogger(e, fName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Kp2aJSchLogger(EntryToLogFactory logFactory) {
|
||||||
|
this.logFactory = logFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled(int level) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void log(int level, String message) {
|
||||||
|
if (isEnabled(level))
|
||||||
|
getLogger(level).log(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ILogger getLogger(int level) {
|
||||||
|
LogEntry entry = loggers.get(level);
|
||||||
|
if (entry == null)
|
||||||
|
entry = DEFAULT_ENTRY;
|
||||||
|
|
||||||
|
return logFactory.create(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ILogger createFileLogger(LogEntry entry, String fName) {
|
||||||
|
try {
|
||||||
|
final PrintWriter p = new PrintWriter(new FileWriter(fName, true));
|
||||||
|
return msg -> {
|
||||||
|
try {
|
||||||
|
String fullMsg = String.join(" ", entry.levelTag, PREFIX, msg);
|
||||||
|
p.println(fullMsg);
|
||||||
|
} catch (Exception e) {
|
||||||
|
ERROR.log(e.getMessage());
|
||||||
|
} finally {
|
||||||
|
p.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (Exception e) {
|
||||||
|
ERROR.log(e.getMessage());
|
||||||
|
return entry.logger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,436 +0,0 @@
|
|||||||
package keepass2android.javafilestorage;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.onedrive.sdk.core.DefaultClientConfig;
|
|
||||||
import com.onedrive.sdk.core.IClientConfig;
|
|
||||||
import com.onedrive.sdk.core.OneDriveErrorCodes;
|
|
||||||
import com.onedrive.sdk.extensions.IItemCollectionPage;
|
|
||||||
import com.onedrive.sdk.extensions.IItemCollectionRequestBuilder;
|
|
||||||
import com.onedrive.sdk.extensions.IOneDriveClient;
|
|
||||||
import com.onedrive.sdk.extensions.Item;
|
|
||||||
import com.onedrive.sdk.extensions.OneDriveClient;
|
|
||||||
import com.onedrive.sdk.http.OneDriveServiceException;
|
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by Philipp on 20.11.2016.
|
|
||||||
*/
|
|
||||||
public class OneDriveStorage extends JavaFileStorageBase
|
|
||||||
{
|
|
||||||
final IClientConfig oneDriveConfig;
|
|
||||||
final keepass2android.javafilestorage.onedrive.MyMSAAuthenticator msaAuthenticator;
|
|
||||||
|
|
||||||
IOneDriveClient oneDriveClient;
|
|
||||||
|
|
||||||
public OneDriveStorage(final Context context, final String clientId) {
|
|
||||||
msaAuthenticator = new keepass2android.javafilestorage.onedrive.MyMSAAuthenticator(context) {
|
|
||||||
@Override
|
|
||||||
public String getClientId() {
|
|
||||||
return clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getScopes() {
|
|
||||||
return new String[] { "offline_access", "onedrive.readwrite" };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
oneDriveConfig = DefaultClientConfig.createWithAuthenticator(msaAuthenticator);
|
|
||||||
initAuthenticator(null);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean requiresSetup(String path) {
|
|
||||||
return !isConnected(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void startSelectFile(FileStorageSetupInitiatorActivity activity, boolean isForSave, int requestCode) {
|
|
||||||
|
|
||||||
initAuthenticator((Activity)activity.getActivity());
|
|
||||||
|
|
||||||
String path = getProtocolId()+":///";
|
|
||||||
Log.d("KP2AJ", "startSelectFile "+path+", connected: "+path);
|
|
||||||
if (isConnected(null))
|
|
||||||
{
|
|
||||||
Intent intent = new Intent();
|
|
||||||
intent.putExtra(EXTRA_IS_FOR_SAVE, isForSave);
|
|
||||||
intent.putExtra(EXTRA_PATH, path);
|
|
||||||
activity.onImmediateResult(requestCode, RESULT_FILECHOOSER_PREPARED, intent);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
activity.startSelectFileProcess(path, isForSave, requestCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isConnected(Activity activity) {
|
|
||||||
if (oneDriveClient == null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Log.d("KP2AJ", "trying silent login");
|
|
||||||
if (msaAuthenticator.loginSilent() != null)
|
|
||||||
{
|
|
||||||
Log.d("KP2AJ", "ok: silent login");
|
|
||||||
|
|
||||||
oneDriveClient = buildClient(activity);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
else Log.d("KP2AJ", "trying silent login failed.");
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return oneDriveClient != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initAuthenticator(Activity activity) {
|
|
||||||
msaAuthenticator.init(
|
|
||||||
oneDriveConfig.getExecutors(),
|
|
||||||
oneDriveConfig.getHttpProvider(),
|
|
||||||
activity,
|
|
||||||
oneDriveConfig.getLogger());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void prepareFileUsage(FileStorageSetupInitiatorActivity activity, String path, int requestCode, boolean alwaysReturnSuccess) {
|
|
||||||
initAuthenticator((Activity)activity.getActivity());
|
|
||||||
if (isConnected((Activity)activity.getActivity()))
|
|
||||||
{
|
|
||||||
Intent intent = new Intent();
|
|
||||||
intent.putExtra(EXTRA_PATH, path);
|
|
||||||
activity.onImmediateResult(requestCode, RESULT_FILEUSAGE_PREPARED, intent);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
activity.startFileUsageProcess(path, requestCode, alwaysReturnSuccess);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getProtocolId() {
|
|
||||||
return "onedrive";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void prepareFileUsage(Context appContext, String path) throws UserInteractionRequiredException {
|
|
||||||
if (!isConnected(null))
|
|
||||||
{
|
|
||||||
throw new UserInteractionRequiredException();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(FileStorageSetupActivity activity, Bundle savedInstanceState) {
|
|
||||||
|
|
||||||
Log.d("KP2AJ", "OnCreate");
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume(final FileStorageSetupActivity activity) {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private IOneDriveClient buildClient(Activity activity) {
|
|
||||||
|
|
||||||
return new OneDriveClient.Builder()
|
|
||||||
.fromConfig(oneDriveConfig)
|
|
||||||
.loginAndBuildClient(activity);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
String getPathFromSkydrivePath(String skydrivePath)
|
|
||||||
{
|
|
||||||
String path = "";
|
|
||||||
if (skydrivePath.equals(""))
|
|
||||||
return "";
|
|
||||||
|
|
||||||
String[] parts = skydrivePath.split("/");
|
|
||||||
|
|
||||||
for (int i = 0; i < parts.length; i++) {
|
|
||||||
String part = parts[i];
|
|
||||||
logDebug("parsing part " + part);
|
|
||||||
int indexOfSeparator = part.lastIndexOf(NAME_ID_SEP);
|
|
||||||
if (indexOfSeparator < 0) {
|
|
||||||
// seems invalid, but we're very generous here
|
|
||||||
path += "/" + part;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String name = part.substring(0, indexOfSeparator);
|
|
||||||
try {
|
|
||||||
name = decode(name);
|
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
path += "/" + name;
|
|
||||||
}
|
|
||||||
logDebug("return " +path + ". original was " + skydrivePath);
|
|
||||||
return path;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
String removeProtocol(String path) throws Exception {
|
|
||||||
if (path == null)
|
|
||||||
return null;
|
|
||||||
if (path.startsWith("skydrive"))
|
|
||||||
return getPathFromSkydrivePath(path.substring("skydrive://".length()));
|
|
||||||
return path.substring(getProtocolId().length()+3);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDisplayName(String path) {
|
|
||||||
|
|
||||||
if (path == null)
|
|
||||||
return null;
|
|
||||||
if (path.startsWith("skydrive"))
|
|
||||||
return getProtocolId()+"://"+getPathFromSkydrivePath(path.substring("skydrive://".length()));
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getFilename(String path) throws Exception {
|
|
||||||
return path.substring(path.lastIndexOf("/")+1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean checkForFileChangeFast(String path, String previousFileVersion) throws Exception {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getCurrentFileVersionFast(String path) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InputStream openFileForRead(String path) throws Exception {
|
|
||||||
try {
|
|
||||||
path = removeProtocol(path);
|
|
||||||
logDebug("openFileForRead. Path="+path);
|
|
||||||
InputStream result = oneDriveClient.getDrive()
|
|
||||||
.getRoot()
|
|
||||||
.getItemWithPath(path)
|
|
||||||
.getContent()
|
|
||||||
.buildRequest()
|
|
||||||
.get();
|
|
||||||
logDebug("ok");
|
|
||||||
return result;
|
|
||||||
|
|
||||||
}
|
|
||||||
catch (OneDriveServiceException e)
|
|
||||||
{
|
|
||||||
throw convertException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Exception convertException(OneDriveServiceException e) {
|
|
||||||
if (e.isError(OneDriveErrorCodes.ItemNotFound))
|
|
||||||
return new FileNotFoundException(e.getMessage());
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void uploadFile(String path, byte[] data, boolean writeTransactional) throws Exception {
|
|
||||||
try {
|
|
||||||
path = removeProtocol(path);
|
|
||||||
oneDriveClient.getDrive()
|
|
||||||
.getRoot()
|
|
||||||
.getItemWithPath(path)
|
|
||||||
.getContent()
|
|
||||||
.buildRequest()
|
|
||||||
.put(data);
|
|
||||||
} catch (OneDriveServiceException e) {
|
|
||||||
throw convertException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String createFolder(String parentPath, String newDirName) throws Exception {
|
|
||||||
throw new Exception("not implemented.");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String createFilePath(String parentPath, String newFileName) throws Exception {
|
|
||||||
String path = parentPath;
|
|
||||||
if (!path.endsWith("/"))
|
|
||||||
path = path + "/";
|
|
||||||
path = path + newFileName;
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<FileEntry> listFiles(String parentPath) throws Exception {
|
|
||||||
try {
|
|
||||||
ArrayList<FileEntry> result = new ArrayList<FileEntry>();
|
|
||||||
parentPath = removeProtocol(parentPath);
|
|
||||||
IItemCollectionPage itemsPage = oneDriveClient.getDrive()
|
|
||||||
.getRoot()
|
|
||||||
.getItemWithPath(parentPath)
|
|
||||||
.getChildren()
|
|
||||||
.buildRequest()
|
|
||||||
.get();
|
|
||||||
if (parentPath.endsWith("/"))
|
|
||||||
parentPath = parentPath.substring(0,parentPath.length()-1);
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
List<Item> items = itemsPage.getCurrentPage();
|
|
||||||
if (items.isEmpty())
|
|
||||||
return result;
|
|
||||||
|
|
||||||
for (Item i: items)
|
|
||||||
{
|
|
||||||
FileEntry e = getFileEntry(parentPath + "/" + i.name, i);
|
|
||||||
Log.d("KP2AJ", e.path);
|
|
||||||
result.add(e);
|
|
||||||
}
|
|
||||||
IItemCollectionRequestBuilder nextPageReqBuilder = itemsPage.getNextPage();
|
|
||||||
if (nextPageReqBuilder == null)
|
|
||||||
return result;
|
|
||||||
itemsPage = nextPageReqBuilder.buildRequest().get();
|
|
||||||
|
|
||||||
}
|
|
||||||
} catch (OneDriveServiceException e) {
|
|
||||||
throw convertException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private FileEntry getFileEntry(String path, Item i) {
|
|
||||||
FileEntry e = new FileEntry();
|
|
||||||
if (i.size != null)
|
|
||||||
e.sizeInBytes = i.size;
|
|
||||||
else if ((i.remoteItem != null) && (i.remoteItem.size != null))
|
|
||||||
e.sizeInBytes = i.remoteItem.size;
|
|
||||||
|
|
||||||
e.displayName = i.name;
|
|
||||||
e.canRead = e.canWrite = true;
|
|
||||||
e.path = getProtocolId() +"://"+path;
|
|
||||||
if (i.lastModifiedDateTime != null)
|
|
||||||
e.lastModifiedTime = i.lastModifiedDateTime.getTimeInMillis();
|
|
||||||
else if ((i.remoteItem != null)&&(i.remoteItem.lastModifiedDateTime != null))
|
|
||||||
e.lastModifiedTime = i.remoteItem.lastModifiedDateTime.getTimeInMillis();
|
|
||||||
e.isDirectory = (i.folder != null) || ((i.remoteItem != null) && (i.remoteItem.folder != null));
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FileEntry getFileEntry(String filename) throws Exception {
|
|
||||||
try {
|
|
||||||
filename = removeProtocol(filename);
|
|
||||||
Item item = oneDriveClient.getDrive()
|
|
||||||
.getRoot()
|
|
||||||
.getItemWithPath(filename)
|
|
||||||
.buildRequest()
|
|
||||||
.get();
|
|
||||||
return getFileEntry(filename, item);
|
|
||||||
} catch (OneDriveServiceException e) {
|
|
||||||
throw convertException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(String path) throws Exception {
|
|
||||||
try {
|
|
||||||
path = removeProtocol(path);
|
|
||||||
oneDriveClient.getDrive()
|
|
||||||
.getRoot()
|
|
||||||
.getItemWithPath(path)
|
|
||||||
.buildRequest()
|
|
||||||
.delete();
|
|
||||||
} catch (OneDriveServiceException e) {
|
|
||||||
throw convertException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStart(final FileStorageSetupActivity activity) {
|
|
||||||
Log.d("KP2AJ", "onStart");
|
|
||||||
if (activity.getProcessName().equals(PROCESS_NAME_SELECTFILE))
|
|
||||||
activity.getState().putString(EXTRA_PATH, activity.getPath());
|
|
||||||
|
|
||||||
JavaFileStorage.FileStorageSetupActivity storageSetupAct = activity;
|
|
||||||
|
|
||||||
if (oneDriveClient != null) {
|
|
||||||
Log.d("KP2AJ", "auth successful");
|
|
||||||
try {
|
|
||||||
|
|
||||||
finishActivityWithSuccess(activity);
|
|
||||||
return;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.d("KP2AJ", "finish with error: " + e.toString());
|
|
||||||
finishWithError(activity, e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{
|
|
||||||
Log.d("KP2AJ", "Starting auth");
|
|
||||||
new AsyncTask<Object, Object, Object>() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Object doInBackground(Object... params) {
|
|
||||||
try {
|
|
||||||
return buildClient((Activity) activity);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Object o) {
|
|
||||||
if (o == null)
|
|
||||||
{
|
|
||||||
Log.i(TAG, "authenticating not successful");
|
|
||||||
Intent data = new Intent();
|
|
||||||
data.putExtra(EXTRA_ERROR_MESSAGE, "authenticating not succesful");
|
|
||||||
((Activity)activity).setResult(Activity.RESULT_CANCELED, data);
|
|
||||||
((Activity)activity).finish();
|
|
||||||
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Log.i(TAG, "authenticating successful");
|
|
||||||
|
|
||||||
oneDriveClient = (IOneDriveClient) o;
|
|
||||||
finishActivityWithSuccess(activity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.execute();
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(FileStorageSetupActivity activity, int requestCode, int resultCode, Intent data) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -146,7 +146,7 @@ public class PCloudFileStorage extends JavaFileStorageBase
|
|||||||
|
|
||||||
DataSource dataSource = DataSource.create(data);
|
DataSource dataSource = DataSource.create(data);
|
||||||
String filename = path.substring(path.lastIndexOf("/") + 1);
|
String filename = path.substring(path.lastIndexOf("/") + 1);
|
||||||
String filePath = path.substring(0, path.lastIndexOf("/") + 1);
|
String filePath = path.substring(0, path.lastIndexOf("/"));
|
||||||
RemoteFolder remoteFolder = this.getRemoteFolderByPath(filePath);
|
RemoteFolder remoteFolder = this.getRemoteFolderByPath(filePath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -175,11 +175,14 @@ public class PCloudFileStorage extends JavaFileStorageBase
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String createFilePath(String parentPath, String newFileName) throws Exception {
|
public String createFilePath(String parentPath, String newFileName) throws Exception {
|
||||||
|
String cleanpath = this.cleanPath(parentPath);
|
||||||
|
String filepath = this.getProtocolId() + "://";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
this.getProtocolId() + "://" +
|
filepath
|
||||||
this.cleanPath(parentPath) +
|
+cleanpath
|
||||||
("".equals(newFileName) ? "" : "/") +
|
+("".equals(newFileName) || "/".equals(cleanpath) ? "" : "/") +newFileName
|
||||||
newFileName
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +204,7 @@ public class PCloudFileStorage extends JavaFileStorageBase
|
|||||||
@Override
|
@Override
|
||||||
public FileEntry getFileEntry(String path) throws Exception {
|
public FileEntry getFileEntry(String path) throws Exception {
|
||||||
path = this.cleanPath(path);
|
path = this.cleanPath(path);
|
||||||
|
//do not call getRemoteFileByPath because path could represent a file or folder, we don't know here
|
||||||
RemoteEntry remoteEntry = this.getRemoteEntryByPath(path);
|
RemoteEntry remoteEntry = this.getRemoteEntryByPath(path);
|
||||||
|
|
||||||
return this.convertRemoteEntryToFileEntry(
|
return this.convertRemoteEntryToFileEntry(
|
||||||
@@ -214,10 +217,13 @@ public class PCloudFileStorage extends JavaFileStorageBase
|
|||||||
public void delete(String path) throws Exception {
|
public void delete(String path) throws Exception {
|
||||||
path = this.cleanPath(path);
|
path = this.cleanPath(path);
|
||||||
|
|
||||||
RemoteEntry remoteEntry = this.getRemoteFileByPath(path);
|
RemoteEntry remoteEntry = this.getRemoteEntryByPath(path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.apiClient.delete(remoteEntry).execute();
|
if (remoteEntry.isFolder())
|
||||||
|
this.apiClient.deleteFolder(remoteEntry.asFolder(), true).execute();
|
||||||
|
else
|
||||||
|
this.apiClient.delete(remoteEntry).execute();
|
||||||
} catch (ApiError e) {
|
} catch (ApiError e) {
|
||||||
throw convertApiError(e);
|
throw convertApiError(e);
|
||||||
}
|
}
|
||||||
@@ -289,7 +295,7 @@ public class PCloudFileStorage extends JavaFileStorageBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ApiClient createApiClientFromSharedPrefs() {
|
private ApiClient createApiClientFromSharedPrefs() {
|
||||||
SharedPreferences prefs = this.ctx.getSharedPreferences(sharedPrefPrefix + SHARED_PREF_NAME, Context.MODE_PRIVATE);
|
SharedPreferences prefs = getPrefs();
|
||||||
String authToken = prefs.getString(SHARED_PREF_AUTH_TOKEN, null);
|
String authToken = prefs.getString(SHARED_PREF_AUTH_TOKEN, null);
|
||||||
String apiHost = prefs.getString(SHARED_PREF_API_HOST, null);
|
String apiHost = prefs.getString(SHARED_PREF_API_HOST, null);
|
||||||
return this.createApiClient(authToken, apiHost);
|
return this.createApiClient(authToken, apiHost);
|
||||||
@@ -313,15 +319,20 @@ public class PCloudFileStorage extends JavaFileStorageBase
|
|||||||
|
|
||||||
private void clearAuthToken() {
|
private void clearAuthToken() {
|
||||||
this.apiClient = null;
|
this.apiClient = null;
|
||||||
SharedPreferences prefs = this.ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
|
SharedPreferences prefs = getPrefs();
|
||||||
SharedPreferences.Editor edit = prefs.edit();
|
SharedPreferences.Editor edit = prefs.edit();
|
||||||
edit.clear();
|
edit.clear();
|
||||||
edit.apply();
|
edit.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SharedPreferences getPrefs()
|
||||||
|
{
|
||||||
|
return this.ctx.getSharedPreferences(sharedPrefPrefix + SHARED_PREF_NAME, Context.MODE_PRIVATE);
|
||||||
|
}
|
||||||
|
|
||||||
private void setAuthToken(String authToken, String apiHost) {
|
private void setAuthToken(String authToken, String apiHost) {
|
||||||
this.apiClient = this.createApiClient(authToken, apiHost);
|
this.apiClient = this.createApiClient(authToken, apiHost);
|
||||||
SharedPreferences prefs = this.ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
|
SharedPreferences prefs = getPrefs();
|
||||||
SharedPreferences.Editor edit = prefs.edit();
|
SharedPreferences.Editor edit = prefs.edit();
|
||||||
edit.putString(SHARED_PREF_AUTH_TOKEN, authToken);
|
edit.putString(SHARED_PREF_AUTH_TOKEN, authToken);
|
||||||
edit.putString(SHARED_PREF_API_HOST, apiHost);
|
edit.putString(SHARED_PREF_API_HOST, apiHost);
|
||||||
@@ -335,27 +346,47 @@ public class PCloudFileStorage extends JavaFileStorageBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
private RemoteFile getRemoteFileByPath(String path) throws Exception {
|
private RemoteFile getRemoteFileByPath(String path) throws Exception {
|
||||||
RemoteEntry remoteEntry = this.getRemoteEntryByPath(path);
|
Call<RemoteFile> call = this.apiClient.loadFile(path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return remoteEntry.asFile();
|
return call.execute();
|
||||||
} catch (IllegalStateException e) {
|
} catch (ApiError apiError) {
|
||||||
throw new FileNotFoundException(e.toString());
|
throw convertApiError(apiError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private RemoteFolder getRemoteFolderByPath(String path) throws Exception {
|
private RemoteFolder getRemoteFolderByPath(String path) throws Exception {
|
||||||
RemoteEntry remoteEntry = this.getRemoteEntryByPath(path);
|
Call<RemoteFolder> call;
|
||||||
|
if ("".equals(path))
|
||||||
|
call = this.apiClient.listFolder(RemoteFolder.ROOT_FOLDER_ID, false);
|
||||||
|
else
|
||||||
|
call = this.apiClient.listFolder(path, false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return remoteEntry.asFolder();
|
return call.execute();
|
||||||
} catch (IllegalStateException e) {
|
} catch (ApiError apiError) {
|
||||||
throw new FileNotFoundException(e.toString());
|
throw convertApiError(apiError);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private RemoteEntry getRemoteEntryByPath(String path) throws Exception {
|
private RemoteEntry getRemoteEntryByPath(String path) throws Exception {
|
||||||
Call<RemoteFolder> call = this.apiClient.listFolder(RemoteFolder.ROOT_FOLDER_ID, true);
|
if ("/".equals(path)) {
|
||||||
|
try {
|
||||||
|
return this.apiClient.listFolder(RemoteFolder.ROOT_FOLDER_ID, false).execute();
|
||||||
|
} catch (ApiError apiError) {
|
||||||
|
throw convertApiError(apiError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String filename = path.substring(path.lastIndexOf("/") + 1);
|
||||||
|
String parentPath = path.substring(0, path.lastIndexOf("/"));
|
||||||
|
|
||||||
|
Call<RemoteFolder> call;
|
||||||
|
if ("".equals(parentPath))
|
||||||
|
call = this.apiClient.listFolder(RemoteFolder.ROOT_FOLDER_ID, false);
|
||||||
|
else
|
||||||
|
call = this.apiClient.listFolder(parentPath, false);
|
||||||
|
|
||||||
RemoteFolder folder;
|
RemoteFolder folder;
|
||||||
try {
|
try {
|
||||||
@@ -364,40 +395,12 @@ public class PCloudFileStorage extends JavaFileStorageBase
|
|||||||
throw convertApiError(apiError);
|
throw convertApiError(apiError);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("/".equals(path)) {
|
for (RemoteEntry remoteEntry : folder.children()) {
|
||||||
return folder;
|
if (remoteEntry.name() != null && remoteEntry.name().equals(filename))
|
||||||
|
return remoteEntry;
|
||||||
}
|
}
|
||||||
|
throw new FileNotFoundException("did not find " + path);
|
||||||
|
|
||||||
String[] fileNames = path.substring(1).split("/");
|
|
||||||
RemoteFolder currentFolder = folder;
|
|
||||||
Iterator<String> fileNamesIterator = Arrays.asList(fileNames).iterator();
|
|
||||||
while (true) {
|
|
||||||
String fileName = fileNamesIterator.next();
|
|
||||||
|
|
||||||
Iterator<RemoteEntry> entryIterator = currentFolder.children().iterator();
|
|
||||||
while (true) {
|
|
||||||
RemoteEntry remoteEntry;
|
|
||||||
try {
|
|
||||||
remoteEntry = entryIterator.next();
|
|
||||||
} catch (NoSuchElementException e) {
|
|
||||||
throw new FileNotFoundException(e.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentFolder.folderId() == remoteEntry.parentFolderId() && fileName.equals(remoteEntry.name())) {
|
|
||||||
if (!fileNamesIterator.hasNext()) {
|
|
||||||
return remoteEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
currentFolder = remoteEntry.asFolder();
|
|
||||||
} catch (IllegalStateException e) {
|
|
||||||
throw new FileNotFoundException(e.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Exception convertApiError(ApiError e) {
|
private Exception convertApiError(ApiError e) {
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
package keepass2android.javafilestorage;
|
||||||
|
|
||||||
|
import android.util.Pair;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.jcraft.jsch.JSch;
|
||||||
|
import com.jcraft.jsch.JSchException;
|
||||||
|
import com.jcraft.jsch.KeyPair;
|
||||||
|
|
||||||
|
class SftpPublicPrivateKeyUtils {
|
||||||
|
|
||||||
|
private enum Validity {
|
||||||
|
NOT_ATTEMPTED, VALID, NOT_VALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String SFTP_CUSTOM_KEY_DIRNAME = "user_keys";
|
||||||
|
|
||||||
|
private static final String KP2A_PRIVATE_KEY_FILENAME = "id_kp2a_rsa";
|
||||||
|
|
||||||
|
private final File appBaseDir;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do NOT access this variable directly! Use {@link #baseDir()} instead.
|
||||||
|
*/
|
||||||
|
private final File customKeyBaseDir;
|
||||||
|
private volatile Validity validDir = Validity.NOT_ATTEMPTED;
|
||||||
|
|
||||||
|
SftpPublicPrivateKeyUtils(String appBaseDir) {
|
||||||
|
// Assume app base directory exists already
|
||||||
|
this.appBaseDir = new File(appBaseDir);
|
||||||
|
|
||||||
|
// Intentionally skipping existence/creation checking in constructor
|
||||||
|
// See baseDir()
|
||||||
|
this.customKeyBaseDir = new File(appBaseDir, SFTP_CUSTOM_KEY_DIRNAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Pair<File, Boolean> baseDir() {
|
||||||
|
if (validDir == Validity.NOT_ATTEMPTED) {
|
||||||
|
synchronized (this) {
|
||||||
|
if (!customKeyBaseDir.exists()) {
|
||||||
|
customKeyBaseDir.mkdirs();
|
||||||
|
}
|
||||||
|
if (customKeyBaseDir.exists() && customKeyBaseDir.isDirectory()) {
|
||||||
|
validDir = Validity.VALID;
|
||||||
|
} else {
|
||||||
|
validDir = Validity.NOT_VALID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Pair<>(customKeyBaseDir, validDir == Validity.VALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean deleteCustomKey(String keyName) throws FileNotFoundException {
|
||||||
|
File f = getCustomKeyFile(keyName);
|
||||||
|
return f.isFile() && f.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] getCustomKeyNames() {
|
||||||
|
Pair<File, Boolean> base = baseDir();
|
||||||
|
if (!base.second) {
|
||||||
|
// Log it?
|
||||||
|
return new String[]{};
|
||||||
|
}
|
||||||
|
return base.first.list();
|
||||||
|
}
|
||||||
|
|
||||||
|
void savePrivateKeyContent(String keyName, String keyContent) throws IOException, Exception {
|
||||||
|
keyContent = PrivateKeyValidator.ensureValidContent(keyContent);
|
||||||
|
|
||||||
|
File f = getCustomKeyFile(keyName);
|
||||||
|
try (BufferedWriter w = new BufferedWriter(new FileWriter(f))) {
|
||||||
|
w.write(keyContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String getCustomKeyFilePath(String customKeyName) throws FileNotFoundException {
|
||||||
|
return getCustomKeyFile(customKeyName).getAbsolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
String resolveKeyFilePath(JSch jschInst, @Nullable String customKeyName) {
|
||||||
|
// Custom private key configured
|
||||||
|
if (customKeyName != null) {
|
||||||
|
try {
|
||||||
|
return getCustomKeyFilePath(customKeyName);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
System.out.println(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Use KP2A's public/private key
|
||||||
|
String keyFilePath = getAppKeyFileName();
|
||||||
|
try{
|
||||||
|
createKeyPair(jschInst, keyFilePath);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
System.out.println(ex);
|
||||||
|
}
|
||||||
|
return keyFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
String createKeyPair(JSch jschInst) throws IOException, JSchException {
|
||||||
|
return createKeyPair(jschInst, getAppKeyFileName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposed for testing purposes only
|
||||||
|
* @param keyName
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
String getSanitizedCustomKeyName(String keyName) {
|
||||||
|
return PrivateKeyValidator.sanitizeKeyAsFilename(keyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposed for testing purposes only.
|
||||||
|
* @param keyContent
|
||||||
|
* @return
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
String getValidatedCustomKeyContent(String keyContent) throws Exception {
|
||||||
|
return PrivateKeyValidator.ensureValidContent(keyContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createKeyPair(JSch jschInst, String key_filename) throws JSchException, IOException {
|
||||||
|
String public_key_filename = key_filename + ".pub";
|
||||||
|
File file = new File(key_filename);
|
||||||
|
if (file.exists())
|
||||||
|
return public_key_filename;
|
||||||
|
int type = KeyPair.RSA;
|
||||||
|
KeyPair kpair = KeyPair.genKeyPair(jschInst, type, 4096);
|
||||||
|
kpair.writePrivateKey(key_filename);
|
||||||
|
|
||||||
|
kpair.writePublicKey(public_key_filename, "generated by Keepass2Android");
|
||||||
|
//ret = "Fingerprint: " + kpair.getFingerPrint();
|
||||||
|
kpair.dispose();
|
||||||
|
return public_key_filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getAppKeyFileName() {
|
||||||
|
return new File(appBaseDir, KP2A_PRIVATE_KEY_FILENAME).getAbsolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
private File getCustomKeyFile(String customKeyName) throws FileNotFoundException {
|
||||||
|
Pair<File, Boolean> base = baseDir();
|
||||||
|
if (!base.second) {
|
||||||
|
throw new FileNotFoundException("Custom key directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
String keyFileName = PrivateKeyValidator.sanitizeKeyAsFilename(customKeyName);
|
||||||
|
if (!keyFileName.isEmpty()) {
|
||||||
|
File keyFile = new File(base.first, keyFileName);
|
||||||
|
// Protect against bad actors trying to navigate away from the base directory.
|
||||||
|
// This is probably overkill, given sanitizeKeyAsFilename(...) but better safe than sorry.
|
||||||
|
if (base.first.equals(keyFile.getParentFile())) {
|
||||||
|
return keyFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The key was sanitized to nothing, or the parent check above failed.
|
||||||
|
throw new FileNotFoundException("Malformed key name");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class PrivateKeyValidator {
|
||||||
|
private static final Pattern CONTENT_FIRST_LINE = Pattern.compile("^-+BEGIN\\s[^\\s]+\\sPRIVATE\\sKEY-+$");
|
||||||
|
private static final Pattern CONTENT_LAST_LINE = Pattern.compile("^-+END\\s[^\\s]+\\sPRIVATE\\sKEY-+$");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key-to-filename sanitizer solution sourced from:
|
||||||
|
* <a href="https://www.b4x.com/android/forum/threads/sanitize-filename.82558/" />
|
||||||
|
*/
|
||||||
|
private static final Pattern KEY_SANITIZER = Pattern.compile("([^\\p{L}\\s\\d\\-_~,;:\\[\\]\\(\\).'])",
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
static String sanitizeKeyAsFilename(String key) {
|
||||||
|
return KEY_SANITIZER.matcher(key.trim()).replaceAll("");
|
||||||
|
}
|
||||||
|
|
||||||
|
static String ensureValidContent(String content) throws Exception {
|
||||||
|
content = content.trim();
|
||||||
|
|
||||||
|
boolean isValid = true;
|
||||||
|
try (BufferedReader r = new BufferedReader(new StringReader(content))) {
|
||||||
|
boolean validFirst = false;
|
||||||
|
String line;
|
||||||
|
String last = null;
|
||||||
|
while ((line = r.readLine()) != null) {
|
||||||
|
if (!validFirst) {
|
||||||
|
if (CONTENT_FIRST_LINE.matcher(line).matches()) {
|
||||||
|
validFirst = true;
|
||||||
|
} else {
|
||||||
|
isValid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last = line;
|
||||||
|
}
|
||||||
|
if (!isValid || last == null || !CONTENT_LAST_LINE.matcher(last).matches()) {
|
||||||
|
throw new RuntimeException("Malformed private key content");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
android.util.Log.d(SftpStorage.class.getName(), "Invalid key content", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,20 +2,24 @@ package keepass2android.javafilestorage;
|
|||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
import com.jcraft.jsch.Channel;
|
import com.jcraft.jsch.Channel;
|
||||||
import com.jcraft.jsch.ChannelSftp;
|
import com.jcraft.jsch.ChannelSftp;
|
||||||
import com.jcraft.jsch.ChannelSftp.LsEntry;
|
import com.jcraft.jsch.ChannelSftp.LsEntry;
|
||||||
import com.jcraft.jsch.JSch;
|
import com.jcraft.jsch.JSch;
|
||||||
import com.jcraft.jsch.JSchException;
|
import com.jcraft.jsch.JSchException;
|
||||||
import com.jcraft.jsch.KeyPair;
|
import com.jcraft.jsch.Logger;
|
||||||
import com.jcraft.jsch.Session;
|
import com.jcraft.jsch.Session;
|
||||||
import com.jcraft.jsch.SftpATTRS;
|
import com.jcraft.jsch.SftpATTRS;
|
||||||
import com.jcraft.jsch.SftpException;
|
import com.jcraft.jsch.SftpException;
|
||||||
@@ -26,10 +30,38 @@ import android.content.Intent;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused") // Exposed by JavaFileStorageBindings
|
||||||
public class SftpStorage extends JavaFileStorageBase {
|
public class SftpStorage extends JavaFileStorageBase {
|
||||||
|
@FunctionalInterface
|
||||||
|
interface ValueResolver<T> {
|
||||||
|
/**
|
||||||
|
* Takes a raw value and resolves it to either a String containing the String representation
|
||||||
|
* of that value, or null. The latter signifying that the raw value could not be "resolved".
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
* @return String, or null if not resolvable
|
||||||
|
*/
|
||||||
|
String resolve(T value);
|
||||||
|
}
|
||||||
|
|
||||||
public static final int DEFAULT_SFTP_PORT = 22;
|
public static final int DEFAULT_SFTP_PORT = 22;
|
||||||
JSch jsch;
|
public static final int UNSET_SFTP_CONNECT_TIMEOUT = -1;
|
||||||
|
private static final String SFTP_CONNECT_TIMEOUT_OPTION_NAME = "connectTimeout";
|
||||||
|
private static final String SFTP_KEYNAME_OPTION_NAME = "key";
|
||||||
|
private static final String SFTP_KEYPASSPHRASE_OPTION_NAME = "phrase";
|
||||||
|
|
||||||
|
public static final String SSH_CFG_KEX = "kex";
|
||||||
|
public static final String SSH_CFG_SERVER_HOST_KEY = "server_host_key";
|
||||||
|
private static final Set<String> SSH_CFG_CSV_EXPANDABLE = Set.of(SSH_CFG_KEX, SSH_CFG_SERVER_HOST_KEY);
|
||||||
|
private static final ValueResolver<Integer> cTimeoutResolver = c ->
|
||||||
|
c == null || c == UNSET_SFTP_CONNECT_TIMEOUT ? null : String.valueOf(c);
|
||||||
|
|
||||||
|
private static final ValueResolver<String> nonBlankStringResolver = s ->
|
||||||
|
s == null || s.isBlank() ? null : s;
|
||||||
|
|
||||||
|
private static final String TAG = "KP2AJFS";
|
||||||
|
private static final String THREAD_TAG = TAG + "[thread]";
|
||||||
|
private JSch jsch;
|
||||||
|
|
||||||
public class ConnectionInfo
|
public class ConnectionInfo
|
||||||
{
|
{
|
||||||
@@ -37,13 +69,42 @@ public class SftpStorage extends JavaFileStorageBase {
|
|||||||
public String username;
|
public String username;
|
||||||
public String password;
|
public String password;
|
||||||
public String localPath;
|
public String localPath;
|
||||||
|
public String keyName;
|
||||||
|
public String keyPassphrase;
|
||||||
public int port;
|
public int port;
|
||||||
|
public int connectTimeoutSec = UNSET_SFTP_CONNECT_TIMEOUT;
|
||||||
|
public final Map<String, String> configOpts = new HashMap<>();
|
||||||
|
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return "ConnectionInfo{host=" + host + ",port=" + port + ",user=" + username +
|
||||||
|
",pwd=<hidden>,localPath=" + localPath + ",key=" + keyName +
|
||||||
|
",phrase=<hidden>,connectTimeout=" + connectTimeoutSec +
|
||||||
|
",cfgOpts=" + configOpts +
|
||||||
|
"}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> buildOptionMap(ConnectionInfo ci, boolean includeSensitive) {
|
||||||
|
OptionMapBuilder b = new OptionMapBuilder()
|
||||||
|
.addOption(SFTP_CONNECT_TIMEOUT_OPTION_NAME, ci.connectTimeoutSec, cTimeoutResolver)
|
||||||
|
.addOption(SFTP_KEYNAME_OPTION_NAME, ci.keyName, nonBlankStringResolver);
|
||||||
|
// Assume all config options are not sensitive and use the same resolver...
|
||||||
|
for (Map.Entry<String, String> entry : ci.configOpts.entrySet()) {
|
||||||
|
b.addOption(entry.getKey(), entry.getValue(), nonBlankStringResolver);
|
||||||
|
}
|
||||||
|
if (includeSensitive) {
|
||||||
|
b.addOption(SFTP_KEYPASSPHRASE_OPTION_NAME, ci.keyPassphrase, nonBlankStringResolver);
|
||||||
|
}
|
||||||
|
return b.build();
|
||||||
|
}
|
||||||
|
|
||||||
Context _appContext;
|
Context _appContext;
|
||||||
|
private final SftpPublicPrivateKeyUtils _keyUtils;
|
||||||
|
|
||||||
public SftpStorage(Context appContext) {
|
public SftpStorage(Context appContext) {
|
||||||
_appContext = appContext;
|
_appContext = appContext;
|
||||||
|
_keyUtils = new SftpPublicPrivateKeyUtils(getBaseDir());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String SFTP_PROTOCOL_ID = "sftp";
|
private static final String SFTP_PROTOCOL_ID = "sftp";
|
||||||
@@ -65,15 +126,15 @@ public class SftpStorage extends JavaFileStorageBase {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputStream openFileForRead(String path) throws Exception {
|
public InputStream openFileForRead(String path) throws Exception {
|
||||||
|
ConnectionInfo cInfo = splitStringToConnectionInfo(path);
|
||||||
ChannelSftp c = init(path);
|
ChannelSftp c = init(cInfo);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
byte[] buff = new byte[8000];
|
byte[] buff = new byte[8000];
|
||||||
|
|
||||||
int bytesRead = 0;
|
int bytesRead = 0;
|
||||||
|
|
||||||
InputStream in = c.get(extractSessionPath(path));
|
InputStream in = c.get(cInfo.localPath);
|
||||||
ByteArrayOutputStream bao = new ByteArrayOutputStream();
|
ByteArrayOutputStream bao = new ByteArrayOutputStream();
|
||||||
|
|
||||||
while ((bytesRead = in.read(buff)) != -1) {
|
while ((bytesRead = in.read(buff)) != -1) {
|
||||||
@@ -105,14 +166,15 @@ public class SftpStorage extends JavaFileStorageBase {
|
|||||||
public void uploadFile(String path, byte[] data, boolean writeTransactional)
|
public void uploadFile(String path, byte[] data, boolean writeTransactional)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
|
||||||
ChannelSftp c = init(path);
|
ConnectionInfo cInfo = splitStringToConnectionInfo(path);
|
||||||
|
ChannelSftp c = init(cInfo);
|
||||||
try {
|
try {
|
||||||
InputStream in = new ByteArrayInputStream(data);
|
InputStream in = new ByteArrayInputStream(data);
|
||||||
String targetPath = extractSessionPath(path);
|
String targetPath = cInfo.localPath;
|
||||||
if (writeTransactional)
|
if (writeTransactional)
|
||||||
{
|
{
|
||||||
//upload to temporary location:
|
//upload to temporary location:
|
||||||
String tmpPath = targetPath+".tmp";
|
String tmpPath = targetPath + ".tmp";
|
||||||
c.put(in, tmpPath);
|
c.put(in, tmpPath);
|
||||||
//remove previous file:
|
//remove previous file:
|
||||||
try
|
try
|
||||||
@@ -128,9 +190,9 @@ public class SftpStorage extends JavaFileStorageBase {
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
c.put(in, targetPath);
|
c.put(in, targetPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
tryDisconnect(c);
|
tryDisconnect(c);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
tryDisconnect(c);
|
tryDisconnect(c);
|
||||||
@@ -142,53 +204,98 @@ public class SftpStorage extends JavaFileStorageBase {
|
|||||||
@Override
|
@Override
|
||||||
public String createFolder(String parentPath, String newDirName)
|
public String createFolder(String parentPath, String newDirName)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
ConnectionInfo cInfo = splitStringToConnectionInfo(parentPath);
|
||||||
try {
|
try {
|
||||||
ChannelSftp c = init(parentPath);
|
ChannelSftp c = init(cInfo);
|
||||||
String newPath = concatPaths(parentPath, newDirName);
|
String newPath = concatPaths(cInfo.localPath, newDirName);
|
||||||
c.mkdir(extractSessionPath(newPath));
|
c.mkdir(newPath);
|
||||||
tryDisconnect(c);
|
tryDisconnect(c);
|
||||||
return newPath;
|
|
||||||
|
return buildFullPath(cInfo.host, cInfo.port, newPath,
|
||||||
|
cInfo.username, cInfo.password, cInfo.connectTimeoutSec,
|
||||||
|
cInfo.keyName, cInfo.keyPassphrase,
|
||||||
|
cInfo.configOpts.get(SSH_CFG_KEX),
|
||||||
|
cInfo.configOpts.get(SSH_CFG_SERVER_HOST_KEY));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw convertException(e);
|
throw convertException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String extractUserPwdHostPort(String path) {
|
||||||
|
String withoutProtocol = path
|
||||||
|
.substring(getProtocolPrefix().length());
|
||||||
|
return withoutProtocol.substring(0, withoutProtocol.indexOf("/"));
|
||||||
|
}
|
||||||
|
|
||||||
private String extractSessionPath(String newPath) {
|
private String extractSessionPath(String newPath) {
|
||||||
String withoutProtocol = newPath
|
String withoutProtocol = newPath
|
||||||
.substring(getProtocolPrefix().length());
|
.substring(getProtocolPrefix().length());
|
||||||
return withoutProtocol.substring(withoutProtocol.indexOf("/"));
|
int pathStartIdx = withoutProtocol.indexOf("/");
|
||||||
|
int pathEndIdx = withoutProtocol.indexOf("?");
|
||||||
|
if (pathEndIdx < 0) {
|
||||||
|
pathEndIdx = withoutProtocol.length();
|
||||||
|
}
|
||||||
|
return withoutProtocol.substring(pathStartIdx, pathEndIdx);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String extractUserPwdHost(String path) {
|
private Map<String, String> extractOptionsMap(String path) throws UnsupportedEncodingException {
|
||||||
String withoutProtocol = path
|
String withoutProtocol = path
|
||||||
.substring(getProtocolPrefix().length());
|
.substring(getProtocolPrefix().length());
|
||||||
return withoutProtocol.substring(0,withoutProtocol.indexOf("/"));
|
|
||||||
|
Map<String, String> options = new HashMap<>();
|
||||||
|
|
||||||
|
int extraOptsIdx = withoutProtocol.indexOf("?");
|
||||||
|
if (extraOptsIdx > 0 && extraOptsIdx + 1 < withoutProtocol.length()) {
|
||||||
|
String optsString = withoutProtocol.substring(extraOptsIdx + 1);
|
||||||
|
String[] parts = optsString.split("&");
|
||||||
|
for (String p : parts) {
|
||||||
|
int sepIdx = p.indexOf('=');
|
||||||
|
if (sepIdx > 0) {
|
||||||
|
String key = decode(p.substring(0, sepIdx));
|
||||||
|
String value = decode(p.substring(sepIdx + 1));
|
||||||
|
options.put(key, value);
|
||||||
|
} else {
|
||||||
|
options.put(decode(p), "true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String concatPaths(String parentPath, String newDirName) {
|
private String concatPaths(String parentPath, String newDirName) {
|
||||||
String res = parentPath;
|
StringBuilder fp = new StringBuilder(parentPath);
|
||||||
if (!res.endsWith("/"))
|
if (!parentPath.endsWith("/"))
|
||||||
res += "/";
|
fp.append("/");
|
||||||
res += newDirName;
|
return fp.append(newDirName).toString();
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String createFilePath(String parentPath, String newFileName)
|
public String createFilePath(final String parentUri, String newFileName)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
if (parentPath.endsWith("/") == false)
|
|
||||||
parentPath += "/";
|
String parentPath = parentUri;
|
||||||
return parentPath + newFileName;
|
String params = null;
|
||||||
|
int paramsIdx = parentUri.lastIndexOf("?");
|
||||||
|
if (paramsIdx > 0) {
|
||||||
|
params = parentUri.substring(paramsIdx);
|
||||||
|
parentPath = parentPath.substring(0, paramsIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
String newPath = concatPaths(parentPath, newFileName);
|
||||||
|
|
||||||
|
if (params != null) {
|
||||||
|
newPath += params;
|
||||||
|
}
|
||||||
|
return newPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<FileEntry> listFiles(String parentPath) throws Exception {
|
public List<FileEntry> listFiles(String parentPath) throws Exception {
|
||||||
|
ConnectionInfo cInfo = splitStringToConnectionInfo(parentPath);
|
||||||
|
ChannelSftp c = init(cInfo);
|
||||||
|
|
||||||
ChannelSftp c = init(parentPath);
|
|
||||||
return listFiles(parentPath, c);
|
return listFiles(parentPath, c);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setFromAttrs(FileEntry fileEntry, SftpATTRS attrs) {
|
private void setFromAttrs(FileEntry fileEntry, SftpATTRS attrs) {
|
||||||
@@ -212,23 +319,27 @@ public class SftpStorage extends JavaFileStorageBase {
|
|||||||
if (sftpEx.id == ChannelSftp.SSH_FX_NO_SUCH_FILE)
|
if (sftpEx.id == ChannelSftp.SSH_FX_NO_SUCH_FILE)
|
||||||
return new FileNotFoundException(sftpEx.getMessage());
|
return new FileNotFoundException(sftpEx.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return e;
|
return e;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FileEntry getFileEntry(String filename) throws Exception {
|
public FileEntry getFileEntry(String filename) throws Exception {
|
||||||
|
ConnectionInfo cInfo = splitStringToConnectionInfo(filename);
|
||||||
ChannelSftp c = init(filename);
|
ChannelSftp c = init(cInfo);
|
||||||
try {
|
try {
|
||||||
FileEntry fileEntry = new FileEntry();
|
FileEntry fileEntry = new FileEntry();
|
||||||
String sessionPath = extractSessionPath(filename);
|
SftpATTRS attr = c.stat(cInfo.localPath);
|
||||||
SftpATTRS attr = c.stat(sessionPath);
|
|
||||||
setFromAttrs(fileEntry, attr);
|
setFromAttrs(fileEntry, attr);
|
||||||
|
|
||||||
|
// Full URI
|
||||||
fileEntry.path = filename;
|
fileEntry.path = filename;
|
||||||
fileEntry.displayName = getFilename(sessionPath);
|
|
||||||
|
fileEntry.displayName = getFilename(cInfo.localPath);
|
||||||
|
|
||||||
tryDisconnect(c);
|
tryDisconnect(c);
|
||||||
|
|
||||||
return fileEntry;
|
return fileEntry;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logDebug("Exception in getFileEntry! " + e);
|
logDebug("Exception in getFileEntry! " + e);
|
||||||
@@ -239,8 +350,9 @@ public class SftpStorage extends JavaFileStorageBase {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(String path) throws Exception {
|
public void delete(String path) throws Exception {
|
||||||
|
ConnectionInfo cInfo = splitStringToConnectionInfo(path);
|
||||||
|
ChannelSftp c = init(cInfo);
|
||||||
|
|
||||||
ChannelSftp c = init(path);
|
|
||||||
delete(path, c);
|
delete(path, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,10 +376,11 @@ public class SftpStorage extends JavaFileStorageBase {
|
|||||||
tryDisconnect(c);
|
tryDisconnect(c);
|
||||||
throw convertException(e);
|
throw convertException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<FileEntry> listFiles(String path, ChannelSftp c) throws Exception {
|
private List<FileEntry> listFiles(String path, ChannelSftp c) throws Exception {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<FileEntry> res = new ArrayList<FileEntry>();
|
List<FileEntry> res = new ArrayList<FileEntry>();
|
||||||
@SuppressWarnings("rawtypes")
|
@SuppressWarnings("rawtypes")
|
||||||
@@ -283,7 +396,7 @@ public class SftpStorage extends JavaFileStorageBase {
|
|||||||
||(lsEntry.getFilename().equals(".."))
|
||(lsEntry.getFilename().equals(".."))
|
||||||
)
|
)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
FileEntry fileEntry = new FileEntry();
|
FileEntry fileEntry = new FileEntry();
|
||||||
fileEntry.displayName = lsEntry.getFilename();
|
fileEntry.displayName = lsEntry.getFilename();
|
||||||
fileEntry.path = createFilePath(path, fileEntry.displayName);
|
fileEntry.path = createFilePath(path, fileEntry.displayName);
|
||||||
@@ -313,97 +426,161 @@ public class SftpStorage extends JavaFileStorageBase {
|
|||||||
throws UnsupportedEncodingException {
|
throws UnsupportedEncodingException {
|
||||||
return java.net.URLDecoder.decode(encodedString, UTF_8);
|
return java.net.URLDecoder.decode(encodedString, UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String encode(final String unencoded)
|
protected String encode(final String unencoded)
|
||||||
throws UnsupportedEncodingException {
|
throws UnsupportedEncodingException {
|
||||||
return java.net.URLEncoder.encode(unencoded, UTF_8);
|
return java.net.URLEncoder.encode(unencoded, UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
ChannelSftp init(String filename) throws JSchException, UnsupportedEncodingException {
|
|
||||||
jsch = new JSch();
|
|
||||||
ConnectionInfo ci = splitStringToConnectionInfo(filename);
|
|
||||||
|
|
||||||
Log.d("KP2AJFS", "init SFTP");
|
|
||||||
|
ChannelSftp init(ConnectionInfo cInfo) throws JSchException, UnsupportedEncodingException {
|
||||||
|
jsch = new JSch();
|
||||||
|
|
||||||
|
Log.d(TAG, "init SFTP");
|
||||||
|
|
||||||
String base_dir = getBaseDir();
|
String base_dir = getBaseDir();
|
||||||
jsch.setKnownHosts(base_dir + "/known_hosts");
|
jsch.setKnownHosts(base_dir + "/known_hosts");
|
||||||
|
|
||||||
String key_filename = getKeyFileName();
|
String key_filepath = _keyUtils.resolveKeyFilePath(jsch, cInfo.keyName);
|
||||||
try{
|
|
||||||
createKeyPair(key_filename);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
System.out.println(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
jsch.addIdentity(key_filename);
|
jsch.addIdentity(key_filepath);
|
||||||
} catch (java.lang.Exception e)
|
} catch (java.lang.Exception e) {
|
||||||
{
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.e("KP2AJFS[thread]", "getting session...");
|
Log.e(THREAD_TAG, "getting session...");
|
||||||
Session session = jsch.getSession(ci.username, ci.host, ci.port);
|
Session session = jsch.getSession(cInfo.username, cInfo.host, cInfo.port);
|
||||||
Log.e("KP2AJFS", "creating SftpUserInfo");
|
|
||||||
UserInfo ui = new SftpUserInfo(ci.password,_appContext);
|
|
||||||
session.setUserInfo(ui);
|
|
||||||
|
|
||||||
session.setConfig("PreferredAuthentications", "publickey,password");
|
sessionConfigure(session, cInfo);
|
||||||
|
sessionConnect(session, cInfo);
|
||||||
session.connect();
|
|
||||||
|
|
||||||
Channel channel = session.openChannel("sftp");
|
Channel channel = session.openChannel("sftp");
|
||||||
channel.connect();
|
channel.connect();
|
||||||
ChannelSftp c = (ChannelSftp) channel;
|
ChannelSftp c = (ChannelSftp) channel;
|
||||||
|
|
||||||
logDebug("success: init Sftp");
|
|
||||||
return c;
|
return c;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void sessionConnect(Session session, ConnectionInfo cInfo) throws JSchException {
|
||||||
|
if (cInfo.connectTimeoutSec != UNSET_SFTP_CONNECT_TIMEOUT) {
|
||||||
|
session.connect(cInfo.connectTimeoutSec * 1000);
|
||||||
|
} else {
|
||||||
|
session.connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sessionConfigure(Session session, ConnectionInfo cInfo) {
|
||||||
|
Log.e(TAG, "creating SftpUserInfo");
|
||||||
|
UserInfo ui = new SftpUserInfo(cInfo.password, cInfo.keyPassphrase, _appContext);
|
||||||
|
session.setUserInfo(ui);
|
||||||
|
|
||||||
|
session.setConfig("PreferredAuthentications", "publickey,password");
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> e : cInfo.configOpts.entrySet()) {
|
||||||
|
String cfgKey = e.getKey();
|
||||||
|
String before = session.getConfig(cfgKey);
|
||||||
|
String after = e.getValue();
|
||||||
|
|
||||||
|
if (SSH_CFG_CSV_EXPANDABLE.contains(cfgKey)) {
|
||||||
|
SshConfigCsvValueResolver resolver = new SshConfigCsvValueResolver(cfgKey, after);
|
||||||
|
after = resolver.resolve(before);
|
||||||
|
}
|
||||||
|
session.setConfig(cfgKey, after);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String getBaseDir() {
|
private String getBaseDir() {
|
||||||
return _appContext.getFilesDir().getAbsolutePath();
|
return _appContext.getFilesDir().getAbsolutePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getKeyFileName() {
|
public boolean deleteCustomKey(String keyName) throws FileNotFoundException {
|
||||||
return getBaseDir() + "/id_kp2a_rsa";
|
return _keyUtils.deleteCustomKey(keyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String[] getCustomKeyNames() {
|
||||||
|
return _keyUtils.getCustomKeyNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused") // Exposed by JavaFileStorageBindings
|
||||||
public String createKeyPair() throws IOException, JSchException {
|
public String createKeyPair() throws IOException, JSchException {
|
||||||
return createKeyPair(getKeyFileName());
|
return _keyUtils.createKeyPair(jsch);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String createKeyPair(String key_filename) throws JSchException, IOException {
|
@SuppressWarnings("unused") // Exposed by JavaFileStorageBindings
|
||||||
String public_key_filename = key_filename + ".pub";
|
public void savePrivateKeyContent(String keyName, String keyContent) throws IOException, Exception {
|
||||||
File file = new File(key_filename);
|
_keyUtils.savePrivateKeyContent(keyName, keyContent);
|
||||||
if (file.exists())
|
}
|
||||||
return public_key_filename;
|
|
||||||
int type = KeyPair.RSA;
|
|
||||||
KeyPair kpair = KeyPair.genKeyPair(jsch, type, 4096);
|
|
||||||
kpair.writePrivateKey(key_filename);
|
|
||||||
|
|
||||||
kpair.writePublicKey(public_key_filename, "generated by Keepass2Android");
|
@SuppressWarnings("unused") // Exposed by JavaFileStorageBindings
|
||||||
//ret = "Fingerprint: " + kpair.getFingerPrint();
|
public void setJschLogging(boolean enabled, String logFilename) {
|
||||||
kpair.dispose();
|
Logger impl = null;
|
||||||
return public_key_filename;
|
if (enabled) {
|
||||||
|
if (logFilename != null) {
|
||||||
|
impl = Kp2aJSchLogger.createFileLogger(logFilename);
|
||||||
|
} else {
|
||||||
|
impl = Kp2aJSchLogger.createAndroidLogger();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JSch.setLogger(impl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposed for testing purposes only.
|
||||||
|
* @param keyName
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String sanitizeCustomKeyName(String keyName) {
|
||||||
|
return _keyUtils.getSanitizedCustomKeyName(keyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposed for testing purposes only.
|
||||||
|
* @param keyContent
|
||||||
|
* @return
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public String getValidatedCustomKeyContent(String keyContent) throws Exception {
|
||||||
|
return _keyUtils.getValidatedCustomKeyContent(keyContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposed for testing purposes only.
|
||||||
|
* @param currentValues
|
||||||
|
* @param spec
|
||||||
|
* @return
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public String resolveCsvValues(String currentValues, String spec) {
|
||||||
|
return new SshConfigCsvValueResolver("test", spec)
|
||||||
|
.resolve(currentValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConnectionInfo splitStringToConnectionInfo(String filename)
|
public ConnectionInfo splitStringToConnectionInfo(String filename)
|
||||||
throws UnsupportedEncodingException {
|
throws UnsupportedEncodingException {
|
||||||
|
|
||||||
ConnectionInfo ci = new ConnectionInfo();
|
ConnectionInfo ci = new ConnectionInfo();
|
||||||
ci.host = extractUserPwdHost(filename);
|
ci.host = extractUserPwdHostPort(filename);
|
||||||
|
|
||||||
String userPwd = ci.host.substring(0, ci.host.indexOf('@'));
|
String userPwd = ci.host.substring(0, ci.host.indexOf('@'));
|
||||||
ci.username = decode(userPwd.substring(0, userPwd.indexOf(":")));
|
int sepIdx = userPwd.indexOf(":");
|
||||||
ci.password = decode(userPwd.substring(userPwd.indexOf(":")+1));
|
if (sepIdx > 0) {
|
||||||
|
ci.username = decode(userPwd.substring(0, sepIdx));
|
||||||
|
ci.password = decode(userPwd.substring(sepIdx + 1));
|
||||||
|
} else {
|
||||||
|
ci.username = userPwd;
|
||||||
|
ci.password = null;
|
||||||
|
}
|
||||||
|
|
||||||
ci.host = ci.host.substring(ci.host.indexOf('@') + 1);
|
ci.host = ci.host.substring(ci.host.indexOf('@') + 1);
|
||||||
ci.port = DEFAULT_SFTP_PORT;
|
ci.port = DEFAULT_SFTP_PORT;
|
||||||
int portSeparatorIndex = ci.host.lastIndexOf(":");
|
|
||||||
|
int portSeparatorIndex = ci.host.lastIndexOf(':');
|
||||||
if (portSeparatorIndex >= 0)
|
if (portSeparatorIndex >= 0)
|
||||||
{
|
{
|
||||||
ci.port = Integer.parseInt(ci.host.substring(portSeparatorIndex+1));
|
ci.port = Integer.parseInt(ci.host.substring(portSeparatorIndex + 1));
|
||||||
ci.host = ci.host.substring(0, portSeparatorIndex);
|
ci.host = ci.host.substring(0, portSeparatorIndex);
|
||||||
}
|
}
|
||||||
// Encode/decode required to support IPv6 (colons break host:port parse logic)
|
// Encode/decode required to support IPv6 (colons break host:port parse logic)
|
||||||
@@ -411,6 +588,30 @@ public class SftpStorage extends JavaFileStorageBase {
|
|||||||
ci.host = decode(ci.host);
|
ci.host = decode(ci.host);
|
||||||
|
|
||||||
ci.localPath = extractSessionPath(filename);
|
ci.localPath = extractSessionPath(filename);
|
||||||
|
|
||||||
|
Map<String, String> options = extractOptionsMap(filename);
|
||||||
|
|
||||||
|
if (options.containsKey(SFTP_CONNECT_TIMEOUT_OPTION_NAME)) {
|
||||||
|
String optVal = options.get(SFTP_CONNECT_TIMEOUT_OPTION_NAME);
|
||||||
|
try {
|
||||||
|
ci.connectTimeoutSec = Integer.parseInt(optVal);
|
||||||
|
} catch (NumberFormatException nan) {
|
||||||
|
logDebug(SFTP_CONNECT_TIMEOUT_OPTION_NAME + " option not a number: " + optVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options.containsKey(SFTP_KEYNAME_OPTION_NAME)) {
|
||||||
|
ci.keyName = options.get(SFTP_KEYNAME_OPTION_NAME);
|
||||||
|
}
|
||||||
|
if (options.containsKey(SFTP_KEYPASSPHRASE_OPTION_NAME)) {
|
||||||
|
ci.keyPassphrase = options.get(SFTP_KEYPASSPHRASE_OPTION_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String cfgKey : SSH_CFG_CSV_EXPANDABLE) {
|
||||||
|
if (options.containsKey(cfgKey)) {
|
||||||
|
ci.configOpts.put(cfgKey, options.get(cfgKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ci;
|
return ci;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,12 +648,18 @@ public class SftpStorage extends JavaFileStorageBase {
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
ConnectionInfo ci = splitStringToConnectionInfo(path);
|
ConnectionInfo ci = splitStringToConnectionInfo(path);
|
||||||
return getProtocolPrefix()+ci.username+"@"+ci.host+ci.localPath;
|
StringBuilder dName = new StringBuilder(getProtocolPrefix())
|
||||||
|
.append(ci.username)
|
||||||
|
.append("@")
|
||||||
|
.append(ci.host)
|
||||||
|
.append(ci.localPath);
|
||||||
|
appendOptions(dName, buildOptionMap(ci, false));
|
||||||
|
return dName.toString();
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return extractSessionPath(path);
|
return extractSessionPath(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -474,26 +681,105 @@ public class SftpStorage extends JavaFileStorageBase {
|
|||||||
@Override
|
@Override
|
||||||
public void onActivityResult(FileStorageSetupActivity activity,
|
public void onActivityResult(FileStorageSetupActivity activity,
|
||||||
int requestCode, int resultCode, Intent data) {
|
int requestCode, int resultCode, Intent data) {
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String buildFullPath( String host, int port, String localPath, String username, String password) throws UnsupportedEncodingException
|
public String buildFullPath(String host, int port, String localPath,
|
||||||
{
|
String username, String password,
|
||||||
|
int connectTimeoutSec,
|
||||||
|
String keyName, String keyPassphrase,
|
||||||
|
String kexAlgorithms, String shkAlgorithms)
|
||||||
|
throws UnsupportedEncodingException {
|
||||||
|
|
||||||
|
StringBuilder uri = new StringBuilder(getProtocolPrefix()).append(encode(username));
|
||||||
|
if (password != null) {
|
||||||
|
uri.append(":").append(encode(password));
|
||||||
|
}
|
||||||
|
uri.append("@");
|
||||||
// Encode/decode required to support IPv6 (colons break host:port parse logic)
|
// Encode/decode required to support IPv6 (colons break host:port parse logic)
|
||||||
// See Bug #2350
|
// See Bug #2350
|
||||||
host = encode(host);
|
uri.append(encode(host));
|
||||||
|
|
||||||
if (port != DEFAULT_SFTP_PORT)
|
if (port != DEFAULT_SFTP_PORT) {
|
||||||
host += ":"+String.valueOf(port);
|
uri.append(":").append(port);
|
||||||
return getProtocolPrefix()+encode(username)+":"+encode(password)+"@"+host+localPath;
|
}
|
||||||
|
if (localPath != null && localPath.startsWith("/")) {
|
||||||
|
uri.append(localPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
appendOptions(uri, new OptionMapBuilder()
|
||||||
|
.addOption(SFTP_CONNECT_TIMEOUT_OPTION_NAME, connectTimeoutSec, cTimeoutResolver)
|
||||||
|
.addOption(SFTP_KEYNAME_OPTION_NAME, keyName, nonBlankStringResolver)
|
||||||
|
.addOption(SFTP_KEYPASSPHRASE_OPTION_NAME, keyPassphrase, nonBlankStringResolver)
|
||||||
|
.addOption(SSH_CFG_KEX, kexAlgorithms, nonBlankStringResolver)
|
||||||
|
.addOption(SSH_CFG_SERVER_HOST_KEY, shkAlgorithms, nonBlankStringResolver)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
return uri.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendOptions(StringBuilder uri, Map<String, String> opts)
|
||||||
|
throws UnsupportedEncodingException {
|
||||||
|
|
||||||
|
boolean first = true;
|
||||||
|
// Sort for stability/consistency
|
||||||
|
Set<Map.Entry<String, String>> sortedEntries = new TreeSet<>(new EntryComparator<>());
|
||||||
|
sortedEntries.addAll(opts.entrySet());
|
||||||
|
for (Map.Entry<String, String> me : sortedEntries) {
|
||||||
|
if (first) {
|
||||||
|
uri.append("?");
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
uri.append("&");
|
||||||
|
}
|
||||||
|
uri.append(encode(me.getKey())).append("=").append(encode(me.getValue()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void prepareFileUsage(Context appContext, String path) {
|
public void prepareFileUsage(Context appContext, String path) {
|
||||||
//nothing to do
|
//nothing to do
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A comparator that compares Map.Entry objects by their keys, via natural ordering.
|
||||||
|
*
|
||||||
|
* @param <T> the Map.Entry key type, that must implement Comparable.
|
||||||
|
*/
|
||||||
|
private static class EntryComparator<T extends Comparable<T>> implements Comparator<Map.Entry<T, ?>> {
|
||||||
|
@Override
|
||||||
|
public int compare(Map.Entry<T, ?> o1, Map.Entry<T, ?> o2) {
|
||||||
|
return o1.getKey().compareTo(o2.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class OptionMapBuilder {
|
||||||
|
private final Map<String, String> options = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to add a raw value <code>oVal</code> to the underlying option map with key <code>oName</code>
|
||||||
|
* iff the <code>resolver</code> produces a non-null output when invoked using the raw value.
|
||||||
|
*
|
||||||
|
* @param oName the name/key associated with the value, if added
|
||||||
|
* @param oVal the raw value attempting to be added
|
||||||
|
* @param resolver the resolver that determines if the value will be added
|
||||||
|
*
|
||||||
|
* @return OptionMapBuilder (updated)
|
||||||
|
* @param <T> the raw value type
|
||||||
|
*/
|
||||||
|
<T> OptionMapBuilder addOption(final String oName, T oVal, ValueResolver<T> resolver) {
|
||||||
|
String resolved = resolver.resolve(oVal);
|
||||||
|
if (resolved != null) {
|
||||||
|
options.put(oName, resolved);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> build() {
|
||||||
|
return new HashMap<>(options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,17 +116,19 @@ public class SftpUserInfo implements UserInfo {
|
|||||||
Context _appContext;
|
Context _appContext;
|
||||||
|
|
||||||
String _password;
|
String _password;
|
||||||
|
String _passphrase;
|
||||||
|
|
||||||
public SftpUserInfo(String password, Context appContext)
|
public SftpUserInfo(String password, String passphrase, Context appContext)
|
||||||
{
|
{
|
||||||
_password = password;
|
_password = password;
|
||||||
|
_passphrase = passphrase;
|
||||||
_appContext = appContext;
|
_appContext = appContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getPassphrase() {
|
public String getPassphrase() {
|
||||||
|
|
||||||
return null;
|
return _passphrase;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -137,12 +139,12 @@ public class SftpUserInfo implements UserInfo {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean promptPassword(String message) {
|
public boolean promptPassword(String message) {
|
||||||
return true;
|
return _password != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean promptPassphrase(String message) {
|
public boolean promptPassphrase(String message) {
|
||||||
return false; //passphrase not supported
|
return _passphrase != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
package keepass2android.javafilestorage;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that manipulates CSV String values based on a list of CSV "spec" definitions, where each definition
|
||||||
|
* can describe one of the following:
|
||||||
|
*
|
||||||
|
* - Prepend to existing list: +something
|
||||||
|
* - Append to end of existing list: something+
|
||||||
|
* - Remove a specific value: -something
|
||||||
|
* - Remove values matching prefix: -something*
|
||||||
|
* - Remove values matching suffix: -*something
|
||||||
|
* - Remove values matching substring: -*something*
|
||||||
|
* - Remove values matching prefix and suffix: -some*thing
|
||||||
|
*
|
||||||
|
* Otherwise CSV of values completely replace original config values
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* <code>
|
||||||
|
* var r = new SshConfigCsvValueResolver("foo", "addToEnd+,-remove*,+addToBeginning,-*del*");
|
||||||
|
* r.resolve("one,removeTwo,three,removeThree,four") --> "addToBeginning,one,three,four,addToEnd"
|
||||||
|
* r.resolve("one,my-del,del-me,two,foodelbar,three") --> "addToBeginning,one,two,three,addToEnd"
|
||||||
|
*
|
||||||
|
* r = new SshConfigCsvValueResolver("foo", "replace,the,config");
|
||||||
|
* r.resolve("one,two,three,four") --> "replace,the,config"
|
||||||
|
* </code>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class SshConfigCsvValueResolver {
|
||||||
|
interface Matcher {
|
||||||
|
boolean matches(String s);
|
||||||
|
}
|
||||||
|
private final String cfgKey;
|
||||||
|
private static final String TAG = "KP2AJFS[sshcfg]";
|
||||||
|
|
||||||
|
private static final String DELIM = ",";
|
||||||
|
private static final char ADD = '+';
|
||||||
|
private static final char REMOVE = '-';
|
||||||
|
private static final char WILD = '*';
|
||||||
|
private final List<String> prepends;
|
||||||
|
private final List<String> appends;
|
||||||
|
private final List<Matcher> removes;
|
||||||
|
private final List<String> replaces;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new resolver.
|
||||||
|
*
|
||||||
|
* @param cfgKey - configuration key name (used for logging)
|
||||||
|
* @param incomingSpec - A CSV String of "spec" definitions that will be used to
|
||||||
|
* (potentially) modify incoming CSV String values.
|
||||||
|
*/
|
||||||
|
SshConfigCsvValueResolver(String cfgKey, String incomingSpec) {
|
||||||
|
List<String> prepends = new ArrayList<>();
|
||||||
|
List<String> appends = new ArrayList<>();
|
||||||
|
List<Matcher> removes = new ArrayList<>();
|
||||||
|
List<String> replaces = new ArrayList<>();
|
||||||
|
|
||||||
|
for (String iVal : incomingSpec.split(DELIM)) {
|
||||||
|
if (iVal.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int evLen = iVal.length();
|
||||||
|
if (iVal.charAt(0) == ADD && evLen > 1) {
|
||||||
|
prepends.add(iVal.substring(1));
|
||||||
|
} else if (iVal.charAt(iVal.length() - 1) == ADD && evLen > 1) {
|
||||||
|
appends.add(iVal.substring(0, evLen - 1));
|
||||||
|
} else if (iVal.charAt(iVal.length() - 1) == REMOVE && evLen > 1) {
|
||||||
|
removes.add(createMatcher(iVal.substring(1)));
|
||||||
|
} else {
|
||||||
|
// This looks like a straight replace
|
||||||
|
replaces.add(iVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.cfgKey = cfgKey;
|
||||||
|
this.prepends = Collections.unmodifiableList(prepends);
|
||||||
|
this.appends = Collections.unmodifiableList(appends);
|
||||||
|
this.removes = Collections.unmodifiableList(removes);
|
||||||
|
this.replaces = Collections.unmodifiableList(replaces);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a CSV String and (potentially) modifies it according to the "spec" entries of this resolver.
|
||||||
|
*
|
||||||
|
* @param existingValues - the original CSV String
|
||||||
|
* @return an updated representation of <code>existingValues</code>, based on the defined "spec"
|
||||||
|
* entries of this resolver.
|
||||||
|
*/
|
||||||
|
public String resolve(String existingValues) {
|
||||||
|
List<String> newValues;
|
||||||
|
// If there's even one replace, it wins over everything and the rest is thrown out
|
||||||
|
if (!replaces.isEmpty()) {
|
||||||
|
if (!(prepends.isEmpty() || appends.isEmpty() || removes.isEmpty())) {
|
||||||
|
Log.w(TAG, "Discarded SSH cfg parts: key=" + cfgKey +
|
||||||
|
", prepends=" + prepends + ", appends=" + appends +
|
||||||
|
", removes=" + removes);
|
||||||
|
}
|
||||||
|
newValues = replaces;
|
||||||
|
} else {
|
||||||
|
// Otherwise we rebuild from existing and incoming values
|
||||||
|
newValues = createResolvedValues(existingValues);
|
||||||
|
}
|
||||||
|
return String.join(DELIM, newValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> createResolvedValues(String existingValues) {
|
||||||
|
List<String> newValues = new ArrayList<>(prepends);
|
||||||
|
for (String a : existingValues.split(DELIM)) {
|
||||||
|
if (!shouldRemove(a)) {
|
||||||
|
newValues.add(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newValues.addAll(appends);
|
||||||
|
return newValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldRemove(String s) {
|
||||||
|
s = normalize(s);
|
||||||
|
for (Matcher m : removes) {
|
||||||
|
if (m.matches(s)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Matcher createMatcher(String val) {
|
||||||
|
final String v = normalize(val);
|
||||||
|
Matcher impl = s -> v.equals(s);
|
||||||
|
|
||||||
|
int wildcardIdx = v.indexOf(WILD);
|
||||||
|
if (wildcardIdx < 0) {
|
||||||
|
return impl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// *blah *blah* blah* some*thing
|
||||||
|
// endsWith substring startsWith startsWith && endsWith
|
||||||
|
String subStr = null;
|
||||||
|
String suffix = null;
|
||||||
|
String prefix = null;
|
||||||
|
int vLen = v.length();
|
||||||
|
|
||||||
|
if (v.charAt(0) == WILD && vLen > 1) {
|
||||||
|
if (vLen > 2 && v.charAt(vLen - 1) == WILD) {
|
||||||
|
//substring
|
||||||
|
subStr = v.substring(1, vLen - 1);
|
||||||
|
} else {
|
||||||
|
// endsWith
|
||||||
|
suffix = v.substring(1);
|
||||||
|
}
|
||||||
|
} else if (v.charAt(vLen - 1) == WILD && vLen > 1) {
|
||||||
|
// beginsWith
|
||||||
|
prefix = v.substring(0, v.length() - 1);
|
||||||
|
} else if (wildcardIdx > 0) {
|
||||||
|
// startsWith && endsWith
|
||||||
|
prefix = v.substring(0, wildcardIdx);
|
||||||
|
suffix = v.substring(wildcardIdx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subStr != null) {
|
||||||
|
final String sub = subStr;
|
||||||
|
impl = s -> s.contains(sub);
|
||||||
|
} else if (prefix != null || suffix != null) {
|
||||||
|
final String pre = prefix;
|
||||||
|
final String suf = suffix;
|
||||||
|
impl = s -> (pre == null || s.startsWith(pre)) && (suf == null || s.endsWith(suf));
|
||||||
|
}
|
||||||
|
return impl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalize(String s) {
|
||||||
|
return s == null ? null : s.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
package keepass2android.javafilestorage.onedrive;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by Philipp on 22.11.2016.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import com.microsoft.services.msa.LiveConnectSession;
|
|
||||||
import com.onedrive.sdk.authentication.AccountType;
|
|
||||||
import com.onedrive.sdk.authentication.IAccountInfo;
|
|
||||||
import com.onedrive.sdk.authentication.MSAAccountInfo;
|
|
||||||
import com.onedrive.sdk.authentication.MSAAuthenticator;
|
|
||||||
import com.onedrive.sdk.logger.ILogger;
|
|
||||||
|
|
||||||
import com.microsoft.services.msa.LiveConnectSession;
|
|
||||||
import com.onedrive.sdk.logger.ILogger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Account information for a MSA based account.
|
|
||||||
*/
|
|
||||||
public class MyMSAAccountInfo implements IAccountInfo {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The service root for the OneDrive personal API.
|
|
||||||
*/
|
|
||||||
public static final String ONE_DRIVE_PERSONAL_SERVICE_ROOT = "https://api.onedrive.com/v1.0";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The authenticator that can refresh this account.
|
|
||||||
*/
|
|
||||||
private final MyMSAAuthenticator mAuthenticator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The session this account is based off of.
|
|
||||||
*/
|
|
||||||
private LiveConnectSession mSession;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The logger.
|
|
||||||
*/
|
|
||||||
private final ILogger mLogger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an MSAAccountInfo object.
|
|
||||||
* @param authenticator The authenticator that this account info was created from.
|
|
||||||
* @param liveConnectSession The session this account is based off of.
|
|
||||||
* @param logger The logger.
|
|
||||||
*/
|
|
||||||
public MyMSAAccountInfo(final MyMSAAuthenticator authenticator,
|
|
||||||
final LiveConnectSession liveConnectSession,
|
|
||||||
final ILogger logger) {
|
|
||||||
mAuthenticator = authenticator;
|
|
||||||
mSession = liveConnectSession;
|
|
||||||
mLogger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the type of the account.
|
|
||||||
* @return The MicrosoftAccount account type.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public AccountType getAccountType() {
|
|
||||||
return AccountType.MicrosoftAccount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the access token for requests against the service root.
|
|
||||||
* @return The access token for requests against the service root.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String getAccessToken() {
|
|
||||||
return mSession.getAccessToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the OneDrive service root for this account.
|
|
||||||
* @return the OneDrive service root for this account.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String getServiceRoot() {
|
|
||||||
return ONE_DRIVE_PERSONAL_SERVICE_ROOT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates if the account access token is expired and needs to be refreshed.
|
|
||||||
* @return true if refresh() needs to be called and
|
|
||||||
* false if the account is still valid.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean isExpired() {
|
|
||||||
return mSession.isExpired();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refreshes the authentication token for this account info.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void refresh() {
|
|
||||||
mLogger.logDebug("Refreshing access token...");
|
|
||||||
final MyMSAAccountInfo newInfo = (MyMSAAccountInfo)mAuthenticator.loginSilent();
|
|
||||||
mSession = newInfo.mSession;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,446 +0,0 @@
|
|||||||
package keepass2android.javafilestorage.onedrive;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by Philipp on 22.11.2016.
|
|
||||||
*/
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.microsoft.onedrivesdk.BuildConfig;
|
|
||||||
import com.microsoft.services.msa.LiveAuthClient;
|
|
||||||
import com.microsoft.services.msa.LiveAuthException;
|
|
||||||
import com.microsoft.services.msa.LiveAuthListener;
|
|
||||||
import com.microsoft.services.msa.LiveConnectSession;
|
|
||||||
import com.microsoft.services.msa.LiveStatus;
|
|
||||||
import com.onedrive.sdk.authentication.ClientAuthenticatorException;
|
|
||||||
import com.onedrive.sdk.authentication.IAccountInfo;
|
|
||||||
import com.onedrive.sdk.authentication.IAuthenticator;
|
|
||||||
import com.onedrive.sdk.authentication.MSAAccountInfo;
|
|
||||||
import com.onedrive.sdk.concurrency.ICallback;
|
|
||||||
import com.onedrive.sdk.core.ClientException;
|
|
||||||
import com.onedrive.sdk.concurrency.SimpleWaiter;
|
|
||||||
import com.onedrive.sdk.concurrency.IExecutors;
|
|
||||||
import com.onedrive.sdk.core.OneDriveErrorCodes;
|
|
||||||
import com.onedrive.sdk.http.IHttpProvider;
|
|
||||||
import com.onedrive.sdk.logger.ILogger;
|
|
||||||
|
|
||||||
import java.security.InvalidParameterException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper around the MSA authentication library.
|
|
||||||
* https://github.com/MSOpenTech/msa-auth-for-android
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
|
|
||||||
public abstract class MyMSAAuthenticator implements IAuthenticator {
|
|
||||||
|
|
||||||
private final Context mContext;
|
|
||||||
|
|
||||||
public MyMSAAuthenticator(Context context)
|
|
||||||
{
|
|
||||||
mContext = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The sign in cancellation message.
|
|
||||||
*/
|
|
||||||
private static final String SIGN_IN_CANCELLED_MESSAGE = "The user cancelled the login operation.";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The preferences for this authenticator.
|
|
||||||
*/
|
|
||||||
private static final String MSA_AUTHENTICATOR_PREFS = "MSAAuthenticatorPrefs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The key for the user id.
|
|
||||||
*/
|
|
||||||
private static final String USER_ID_KEY = "userId";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The key for the version code
|
|
||||||
*/
|
|
||||||
public static final String VERSION_CODE_KEY = "versionCode";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The default user id
|
|
||||||
*/
|
|
||||||
private static final String DEFAULT_USER_ID = "@@defaultUser";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The active user id.
|
|
||||||
*/
|
|
||||||
private final AtomicReference<String> mUserId = new AtomicReference<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The executors.
|
|
||||||
*/
|
|
||||||
private IExecutors mExecutors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether this authenticator has been initialized.
|
|
||||||
*/
|
|
||||||
private boolean mInitialized;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The context UI interactions should happen with.
|
|
||||||
*/
|
|
||||||
private Activity mActivity;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The logger.
|
|
||||||
*/
|
|
||||||
private ILogger mLogger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The client id for this authenticator.
|
|
||||||
* https://dev.onedrive.com/auth/msa_oauth.htm#to-register-your-app
|
|
||||||
* @return The client id.
|
|
||||||
*/
|
|
||||||
public abstract String getClientId();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The scopes for this application.
|
|
||||||
* https://dev.onedrive.com/auth/msa_oauth.htm#authentication-scopes
|
|
||||||
* @return The scopes for this application.
|
|
||||||
*/
|
|
||||||
public abstract String[] getScopes();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The live authentication client.
|
|
||||||
*/
|
|
||||||
private LiveAuthClient mAuthClient;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the authenticator.
|
|
||||||
* @param executors The executors to schedule foreground and background tasks.
|
|
||||||
* @param httpProvider The http provider for sending requests.
|
|
||||||
* @param activity The activity to create interactive UI on.
|
|
||||||
* @param logger The logger for diagnostic information.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public synchronized void init(final IExecutors executors,
|
|
||||||
final IHttpProvider httpProvider,
|
|
||||||
final Activity activity,
|
|
||||||
final ILogger logger) {
|
|
||||||
mActivity = activity;
|
|
||||||
|
|
||||||
if (mInitialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mExecutors = executors;
|
|
||||||
mLogger = logger;
|
|
||||||
mInitialized = true;
|
|
||||||
mAuthClient = new LiveAuthClient(mContext, getClientId(), Arrays.asList(getScopes()));
|
|
||||||
|
|
||||||
final SharedPreferences prefs = getSharedPreferences();
|
|
||||||
mUserId.set(prefs.getString(USER_ID_KEY, null));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts an interactive login asynchronously.
|
|
||||||
* @param emailAddressHint The hint for the email address during the interactive login.
|
|
||||||
* @param loginCallback The callback to be called when the login is complete.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void login(final String emailAddressHint, final ICallback<IAccountInfo> loginCallback) {
|
|
||||||
Log.d("KP2AJ", "login()");
|
|
||||||
if (!mInitialized) {
|
|
||||||
throw new IllegalStateException("init must be called");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loginCallback == null) {
|
|
||||||
throw new InvalidParameterException("loginCallback");
|
|
||||||
}
|
|
||||||
|
|
||||||
mLogger.logDebug("Starting login async");
|
|
||||||
|
|
||||||
mExecutors.performOnBackground(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
mExecutors.performOnForeground(login(emailAddressHint), loginCallback);
|
|
||||||
} catch (final ClientException e) {
|
|
||||||
mExecutors.performOnForeground(e, loginCallback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts an interactive login.
|
|
||||||
* @param emailAddressHint The hint for the email address during the interactive login.
|
|
||||||
* @return The account info.
|
|
||||||
* @throws ClientException An exception occurs if the login was unable to complete for any reason.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public synchronized IAccountInfo login(final String emailAddressHint) throws ClientException {
|
|
||||||
if (!mInitialized) {
|
|
||||||
throw new IllegalStateException("init must be called");
|
|
||||||
}
|
|
||||||
|
|
||||||
mLogger.logDebug("Starting login");
|
|
||||||
|
|
||||||
final AtomicReference<ClientException> error = new AtomicReference<>();
|
|
||||||
final SimpleWaiter waiter = new SimpleWaiter();
|
|
||||||
|
|
||||||
final LiveAuthListener listener = new LiveAuthListener() {
|
|
||||||
@Override
|
|
||||||
public void onAuthComplete(final LiveStatus liveStatus,
|
|
||||||
final LiveConnectSession liveConnectSession,
|
|
||||||
final Object o) {
|
|
||||||
if (liveStatus == LiveStatus.NOT_CONNECTED) {
|
|
||||||
mLogger.logDebug("Received invalid login failure from silent authentication with MSA, ignoring.");
|
|
||||||
} else {
|
|
||||||
mLogger.logDebug("Successful interactive login");
|
|
||||||
waiter.signal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAuthError(final LiveAuthException e,
|
|
||||||
final Object o) {
|
|
||||||
OneDriveErrorCodes code = OneDriveErrorCodes.AuthenticationFailure;
|
|
||||||
if (e.getError().equals(SIGN_IN_CANCELLED_MESSAGE)) {
|
|
||||||
code = OneDriveErrorCodes.AuthenticationCancelled;
|
|
||||||
}
|
|
||||||
|
|
||||||
error.set(new ClientAuthenticatorException("Unable to login with MSA", e, code));
|
|
||||||
mLogger.logError(error.get().getMessage(), error.get());
|
|
||||||
waiter.signal();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
mActivity.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
mAuthClient.login(mActivity, /* scopes */null, /* user object */ null, emailAddressHint, listener);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mLogger.logDebug("Waiting for MSA callback");
|
|
||||||
waiter.waitForSignal();
|
|
||||||
|
|
||||||
final ClientException exception = error.get();
|
|
||||||
if (exception != null) {
|
|
||||||
throw exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String userId;
|
|
||||||
if (emailAddressHint != null) {
|
|
||||||
userId = emailAddressHint;
|
|
||||||
} else {
|
|
||||||
userId = DEFAULT_USER_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
mUserId.set(userId);
|
|
||||||
|
|
||||||
final SharedPreferences prefs = getSharedPreferences();
|
|
||||||
prefs.edit()
|
|
||||||
.putString(USER_ID_KEY, mUserId.get())
|
|
||||||
.putInt(VERSION_CODE_KEY, BuildConfig.VERSION_CODE)
|
|
||||||
.apply();
|
|
||||||
|
|
||||||
return getAccountInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts a silent login asynchronously.
|
|
||||||
* @param loginCallback The callback to be called when the login is complete.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void loginSilent(final ICallback<IAccountInfo> loginCallback) {
|
|
||||||
if (!mInitialized) {
|
|
||||||
throw new IllegalStateException("init must be called");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loginCallback == null) {
|
|
||||||
throw new InvalidParameterException("loginCallback");
|
|
||||||
}
|
|
||||||
|
|
||||||
mLogger.logDebug("Starting login silent async");
|
|
||||||
|
|
||||||
mExecutors.performOnBackground(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
mExecutors.performOnForeground(loginSilent(), loginCallback);
|
|
||||||
} catch (final ClientException e) {
|
|
||||||
mExecutors.performOnForeground(e, loginCallback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts a silent login.
|
|
||||||
* @return The account info.
|
|
||||||
* @throws ClientException An exception occurs if the login was unable to complete for any reason.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public synchronized IAccountInfo loginSilent() throws ClientException {
|
|
||||||
if (!mInitialized) {
|
|
||||||
throw new IllegalStateException("init must be called");
|
|
||||||
}
|
|
||||||
|
|
||||||
mLogger.logDebug("Starting login silent");
|
|
||||||
|
|
||||||
final int userIdStoredMinVersion = 10112;
|
|
||||||
if (getSharedPreferences().getInt(VERSION_CODE_KEY, 0) >= userIdStoredMinVersion
|
|
||||||
&& mUserId.get() == null) {
|
|
||||||
mLogger.logDebug("No login information found for silent authentication");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final SimpleWaiter loginSilentWaiter = new SimpleWaiter();
|
|
||||||
final AtomicReference<ClientException> error = new AtomicReference<>();
|
|
||||||
|
|
||||||
final boolean waitForCallback = mAuthClient.loginSilent(new LiveAuthListener() {
|
|
||||||
@Override
|
|
||||||
public void onAuthComplete(final LiveStatus liveStatus,
|
|
||||||
final LiveConnectSession liveConnectSession,
|
|
||||||
final Object o) {
|
|
||||||
if (liveStatus == LiveStatus.NOT_CONNECTED) {
|
|
||||||
error.set(new ClientAuthenticatorException("Failed silent login, interactive login required",
|
|
||||||
OneDriveErrorCodes.AuthenticationFailure));
|
|
||||||
mLogger.logError(error.get().getMessage(), error.get());
|
|
||||||
} else {
|
|
||||||
mLogger.logDebug("Successful silent login");
|
|
||||||
}
|
|
||||||
loginSilentWaiter.signal();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAuthError(final LiveAuthException e,
|
|
||||||
final Object o) {
|
|
||||||
OneDriveErrorCodes code = OneDriveErrorCodes.AuthenticationFailure;
|
|
||||||
if (e.getError().equals(SIGN_IN_CANCELLED_MESSAGE)) {
|
|
||||||
code = OneDriveErrorCodes.AuthenticationCancelled;
|
|
||||||
}
|
|
||||||
|
|
||||||
error.set(new ClientAuthenticatorException("Login silent authentication error", e, code));
|
|
||||||
mLogger.logError(error.get().getMessage(), error.get());
|
|
||||||
loginSilentWaiter.signal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!waitForCallback) {
|
|
||||||
mLogger.logDebug("MSA silent auth fast-failed");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
mLogger.logDebug("Waiting for MSA callback");
|
|
||||||
loginSilentWaiter.waitForSignal();
|
|
||||||
final ClientException exception = error.get();
|
|
||||||
if (exception != null) {
|
|
||||||
throw exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
return getAccountInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log the current user out.
|
|
||||||
* @param logoutCallback The callback to be called when the logout is complete.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void logout(final ICallback<Void> logoutCallback) {
|
|
||||||
if (!mInitialized) {
|
|
||||||
throw new IllegalStateException("init must be called");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logoutCallback == null) {
|
|
||||||
throw new InvalidParameterException("logoutCallback");
|
|
||||||
}
|
|
||||||
|
|
||||||
mLogger.logDebug("Starting logout async");
|
|
||||||
|
|
||||||
mExecutors.performOnBackground(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
logout();
|
|
||||||
mExecutors.performOnForeground((Void) null, logoutCallback);
|
|
||||||
} catch (final ClientException e) {
|
|
||||||
mExecutors.performOnForeground(e, logoutCallback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log the current user out.
|
|
||||||
* @throws ClientException An exception occurs if the logout was unable to complete for any reason.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public synchronized void logout() throws ClientException {
|
|
||||||
if (!mInitialized) {
|
|
||||||
throw new IllegalStateException("init must be called");
|
|
||||||
}
|
|
||||||
|
|
||||||
mLogger.logDebug("Starting logout");
|
|
||||||
|
|
||||||
final SimpleWaiter logoutWaiter = new SimpleWaiter();
|
|
||||||
final AtomicReference<ClientException> error = new AtomicReference<>();
|
|
||||||
mAuthClient.logout(new LiveAuthListener() {
|
|
||||||
@Override
|
|
||||||
public void onAuthComplete(final LiveStatus liveStatus,
|
|
||||||
final LiveConnectSession liveConnectSession,
|
|
||||||
final Object o) {
|
|
||||||
mLogger.logDebug("Logout completed");
|
|
||||||
logoutWaiter.signal();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAuthError(final LiveAuthException e, final Object o) {
|
|
||||||
error.set(new ClientAuthenticatorException("MSA Logout failed",
|
|
||||||
e,
|
|
||||||
OneDriveErrorCodes.AuthenticationFailure));
|
|
||||||
mLogger.logError(error.get().getMessage(), error.get());
|
|
||||||
logoutWaiter.signal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mLogger.logDebug("Waiting for logout to complete");
|
|
||||||
logoutWaiter.waitForSignal();
|
|
||||||
|
|
||||||
mLogger.logDebug("Clearing all MSA Authenticator shared preferences");
|
|
||||||
final SharedPreferences prefs = getSharedPreferences();
|
|
||||||
prefs.edit()
|
|
||||||
.clear()
|
|
||||||
.putInt(VERSION_CODE_KEY, BuildConfig.VERSION_CODE)
|
|
||||||
.apply();
|
|
||||||
mUserId.set(null);
|
|
||||||
|
|
||||||
final ClientException exception = error.get();
|
|
||||||
if (exception != null) {
|
|
||||||
throw exception;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current account info for this authenticator.
|
|
||||||
* @return NULL if no account is available.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public IAccountInfo getAccountInfo() {
|
|
||||||
final LiveConnectSession session = mAuthClient.getSession();
|
|
||||||
if (session == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new MyMSAAccountInfo(this, session, mLogger);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the shared preferences for this authenticator.
|
|
||||||
* @return The shared preferences.
|
|
||||||
*/
|
|
||||||
private SharedPreferences getSharedPreferences() {
|
|
||||||
return mContext.getSharedPreferences(MSA_AUTHENTICATOR_PREFS, Context.MODE_PRIVATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "com.android.tools.build:gradle:7.4.0"
|
classpath "com.android.tools.build:gradle:8.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536m
|
org.gradle.jvmargs=-Xmx1536m
|
||||||
|
android.useAndroidX=true
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||||
|
|||||||
@@ -1,13 +1,3 @@
|
|||||||
package com.crocoapps.javafilestoragetest2;
|
package com.crocoapps.javafilestoragetest2;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.test.ApplicationTestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
|
|
||||||
*/
|
|
||||||
public class ApplicationTest extends ApplicationTestCase<Application> {
|
|
||||||
public ApplicationTest() {
|
|
||||||
super(Application.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -145,9 +145,12 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
//import keepass2android.javafilestorage.DropboxCloudRailStorage;
|
//import keepass2android.javafilestorage.DropboxCloudRailStorage;
|
||||||
|
import keepass2android.javafilestorage.DropboxV2Storage;
|
||||||
import keepass2android.javafilestorage.GoogleDriveAppDataFileStorage;
|
import keepass2android.javafilestorage.GoogleDriveAppDataFileStorage;
|
||||||
|
import keepass2android.javafilestorage.ICertificateErrorHandler;
|
||||||
import keepass2android.javafilestorage.JavaFileStorage;
|
import keepass2android.javafilestorage.JavaFileStorage;
|
||||||
import keepass2android.javafilestorage.JavaFileStorage.FileEntry;
|
import keepass2android.javafilestorage.JavaFileStorage.FileEntry;
|
||||||
|
import keepass2android.javafilestorage.PCloudFileStorage;
|
||||||
import keepass2android.javafilestorage.SftpStorage;
|
import keepass2android.javafilestorage.SftpStorage;
|
||||||
import keepass2android.javafilestorage.UserInteractionRequiredException;
|
import keepass2android.javafilestorage.UserInteractionRequiredException;
|
||||||
import keepass2android.javafilestorage.WebDavStorage;
|
import keepass2android.javafilestorage.WebDavStorage;
|
||||||
@@ -346,7 +349,7 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag
|
|||||||
fileList = fs.listFiles(path);
|
fileList = fs.listFiles(path);
|
||||||
checkFileList(path, fileList, false, true); //second param indicates the file must be gone
|
checkFileList(path, fileList, false, true); //second param indicates the file must be gone
|
||||||
|
|
||||||
Log.d("KP2AJ", "Delete a folder recursive");
|
Log.d("KP2AJ", "xDelete a folder recursive: " + subfolderPath);
|
||||||
fs.delete(subfolderPath);
|
fs.delete(subfolderPath);
|
||||||
|
|
||||||
Log.d("KP2AJ", "List files again to check if deleting the folder was successful:");
|
Log.d("KP2AJ", "List files again to check if deleting the folder was successful:");
|
||||||
@@ -539,12 +542,12 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag
|
|||||||
|
|
||||||
static JavaFileStorage createStorageToTest(Context ctx, Context appContext, boolean simulateRestart) {
|
static JavaFileStorage createStorageToTest(Context ctx, Context appContext, boolean simulateRestart) {
|
||||||
//storageToTest = new SftpStorage(ctx.getApplicationContext());
|
//storageToTest = new SftpStorage(ctx.getApplicationContext());
|
||||||
//storageToTest = new PCloudFileStorage(ctx, "yCeH59Ffgtm");
|
//storageToTest = new PCloudFileStorage(ctx, "FLm22de7bdS", "pcloud", "pcloudtest");
|
||||||
//storageToTest = new SkyDriveFileStorage("000000004010C234", appContext);
|
//storageToTest = new SkyDriveFileStorage("000000004010C234", appContext);
|
||||||
|
|
||||||
|
|
||||||
storageToTest = new GoogleDriveAppDataFileStorage();
|
//storageToTest = new GoogleDriveAppDataFileStorage();
|
||||||
/*storageToTest = new WebDavStorage(new ICertificateErrorHandler() {
|
storageToTest = new WebDavStorage(new ICertificateErrorHandler() {
|
||||||
@Override
|
@Override
|
||||||
public boolean onValidationError(String error) {
|
public boolean onValidationError(String error) {
|
||||||
return false;
|
return false;
|
||||||
@@ -554,7 +557,7 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag
|
|||||||
public boolean alwaysFailOnValidationError() {
|
public boolean alwaysFailOnValidationError() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});*/
|
});
|
||||||
|
|
||||||
//storageToTest = new DropboxV2Storage(ctx,"4ybka4p4a1027n6", "1z5lv528un9nre8", !simulateRestart);
|
//storageToTest = new DropboxV2Storage(ctx,"4ybka4p4a1027n6", "1z5lv528un9nre8", !simulateRestart);
|
||||||
//storageToTest = new DropboxFileStorage(ctx,"4ybka4p4a1027n6", "1z5lv528un9nre8", !simulateRestart);
|
//storageToTest = new DropboxFileStorage(ctx,"4ybka4p4a1027n6", "1z5lv528un9nre8", !simulateRestart);
|
||||||
@@ -690,6 +693,13 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void populateCsvMockValues(View view) {
|
||||||
|
EditText etSpecs = view.findViewById(R.id.mock_csv_specs);
|
||||||
|
etSpecs.setText("-bar,+first,-*d*");
|
||||||
|
EditText etCfgs = view.findViewById(R.id.mock_csv_cfg);
|
||||||
|
etCfgs.setText("foo,del1,bar,del2");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void performManualFileSelect(boolean isForSave, final int requestCode,
|
public void performManualFileSelect(boolean isForSave, final int requestCode,
|
||||||
String protocolId)
|
String protocolId)
|
||||||
@@ -697,12 +707,13 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag
|
|||||||
if (protocolId.equals("sftp"))
|
if (protocolId.equals("sftp"))
|
||||||
{
|
{
|
||||||
final View view = getLayoutInflater().inflate(R.layout.sftp_credentials, null);
|
final View view = getLayoutInflater().inflate(R.layout.sftp_credentials, null);
|
||||||
|
final SftpStorage sftpStorage = (SftpStorage)storageToTest;
|
||||||
|
|
||||||
|
populateCsvMockValues(view);
|
||||||
|
|
||||||
view.findViewById(R.id.send_public_key).setOnClickListener(v -> {
|
view.findViewById(R.id.send_public_key).setOnClickListener(v -> {
|
||||||
Intent sendIntent = new Intent();
|
Intent sendIntent = new Intent();
|
||||||
|
|
||||||
|
|
||||||
SftpStorage sftpStorage = (SftpStorage)storageToTest;
|
|
||||||
try {
|
try {
|
||||||
String pub_filename = sftpStorage.createKeyPair();
|
String pub_filename = sftpStorage.createKeyPair();
|
||||||
|
|
||||||
@@ -715,39 +726,140 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Toast.makeText(this,"Failed to create key pair: " + ex.getMessage(), Toast.LENGTH_LONG);
|
Toast.makeText(this,"Failed to create key pair: " + ex.getMessage(), Toast.LENGTH_LONG).show();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
view.findViewById(R.id.list_private_keys).setOnClickListener(v -> {
|
||||||
|
String[] keys = sftpStorage.getCustomKeyNames();
|
||||||
|
Toast.makeText(this, "keys: " + String.join(",", keys), Toast.LENGTH_LONG).show();
|
||||||
|
});
|
||||||
|
|
||||||
|
view.findViewById(R.id.add_private_key).setOnClickListener(v -> {
|
||||||
|
EditText etKeyName = view.findViewById(R.id.private_key_name);
|
||||||
|
String keyName = etKeyName.getText().toString();
|
||||||
|
EditText etKeyContent = view.findViewById(R.id.private_key_content);
|
||||||
|
String keyContent = etKeyContent.getText().toString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
sftpStorage.savePrivateKeyContent(keyName, keyContent);
|
||||||
|
Toast.makeText(this, "Add successful", Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
Toast.makeText(this, "Add failed: " + e.getMessage(), Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
view.findViewById(R.id.delete_private_key).setOnClickListener(v -> {
|
||||||
|
EditText etKeyName = view.findViewById(R.id.private_key_name);
|
||||||
|
String keyName = etKeyName.getText().toString();
|
||||||
|
|
||||||
|
String exMessage = null;
|
||||||
|
boolean success = false;
|
||||||
|
try {
|
||||||
|
success = sftpStorage.deleteCustomKey(keyName);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
exMessage = e.getMessage();
|
||||||
|
}
|
||||||
|
StringBuilder msg = new StringBuilder("Delete ");
|
||||||
|
msg.append(success ? "succeeded" : "FAILED");
|
||||||
|
if (exMessage != null) {
|
||||||
|
msg.append(" (").append(exMessage).append(")");
|
||||||
|
}
|
||||||
|
Toast.makeText(this, msg.toString(), Toast.LENGTH_LONG).show();
|
||||||
|
});
|
||||||
|
|
||||||
|
view.findViewById(R.id.validate_private_key).setOnClickListener(v -> {
|
||||||
|
EditText etKeyName = view.findViewById(R.id.private_key_name);
|
||||||
|
String inKeyName = etKeyName.getText().toString();
|
||||||
|
|
||||||
|
if (!inKeyName.isEmpty()) {
|
||||||
|
String keyResponse;
|
||||||
|
try {
|
||||||
|
keyResponse = sftpStorage.sanitizeCustomKeyName(inKeyName);
|
||||||
|
} catch (Exception e) {
|
||||||
|
keyResponse = "EX:" + e.getMessage();
|
||||||
|
}
|
||||||
|
String msg = "key: [" + inKeyName + "] -> [" + keyResponse + "]";
|
||||||
|
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
EditText etKeyContent = view.findViewById(R.id.private_key_content);
|
||||||
|
String inKeyContent = etKeyContent.getText().toString();
|
||||||
|
String msg;
|
||||||
|
if (!inKeyContent.isEmpty()) {
|
||||||
|
try {
|
||||||
|
// We could print the key, but I don't it's that helpful
|
||||||
|
sftpStorage.getValidatedCustomKeyContent(inKeyContent);
|
||||||
|
msg = "Key content is valid";
|
||||||
|
} catch (Exception e) {
|
||||||
|
msg = "Invalid key content: " + e.getMessage();
|
||||||
|
}
|
||||||
|
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
view.findViewById(R.id.resolve_mock_csv).setOnClickListener(v -> {
|
||||||
|
EditText etSpecs = view.findViewById(R.id.mock_csv_specs);
|
||||||
|
String specs = etSpecs.getText().toString();
|
||||||
|
EditText etCfg = view.findViewById(R.id.mock_csv_cfg);
|
||||||
|
String cfg = etCfg.getText().toString();
|
||||||
|
if (!specs.isBlank() && !cfg.isBlank()) {
|
||||||
|
String result = sftpStorage.resolveCsvValues(cfg, specs);
|
||||||
|
Toast.makeText(this, result, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
view.findViewById(R.id.reset_mock_csv).setOnClickListener(v -> {
|
||||||
|
populateCsvMockValues(view);
|
||||||
|
});
|
||||||
|
|
||||||
new AlertDialog.Builder(this)
|
new AlertDialog.Builder(this)
|
||||||
.setView(view)
|
.setView(view)
|
||||||
.setTitle("Enter SFTP credentials")
|
.setTitle("Enter SFTP credentials")
|
||||||
.setPositiveButton("OK",new DialogInterface.OnClickListener() {
|
.setPositiveButton("OK", (dialog, which) -> {
|
||||||
|
|
||||||
@Override
|
Toast.makeText(MainActivity.this, "Hey", Toast.LENGTH_LONG).show();
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
|
|
||||||
Toast.makeText(MainActivity.this, "Hey", Toast.LENGTH_LONG).show();
|
SftpStorage sftpStorage1 = (SftpStorage)storageToTest;
|
||||||
|
try {
|
||||||
SftpStorage sftpStorage = (SftpStorage)storageToTest;
|
EditText etHost = view.findViewById(R.id.sftp_host);
|
||||||
try {
|
String host = etHost.getText().toString();
|
||||||
EditText etHost = ((EditText)view.findViewById(R.id.sftp_host));
|
EditText etUser = view.findViewById(R.id.sftp_user);
|
||||||
String host = etHost.getText().toString();
|
String user = etUser.getText().toString();
|
||||||
EditText etUser = ((EditText)view.findViewById(R.id.sftp_user));
|
EditText etPwd = view.findViewById(R.id.sftp_password);
|
||||||
String user = etUser.getText().toString();
|
String pwd = etPwd.getText().toString();
|
||||||
EditText etPwd = ((EditText)view.findViewById(R.id.sftp_password));
|
EditText etPort = view.findViewById(R.id.sftp_port);
|
||||||
String pwd = etPwd.getText().toString();
|
int port = Integer.parseInt(etPort.getText().toString());
|
||||||
EditText etPort = ((EditText)view.findViewById(R.id.sftp_port));
|
EditText etInitDir = view.findViewById(R.id.sftp_initial_dir);
|
||||||
int port = Integer.parseInt(etPort.getText().toString());
|
String initialDir = etInitDir.getText().toString();
|
||||||
EditText etInitDir = ((EditText)view.findViewById(R.id.sftp_initial_dir));
|
EditText etConnectTimeout = view.findViewById(R.id.sftp_connect_timeout);
|
||||||
String initialDir = etInitDir.getText().toString();
|
int connectTimeout = SftpStorage.UNSET_SFTP_CONNECT_TIMEOUT;
|
||||||
onReceivePathForFileSelect(requestCode, sftpStorage.buildFullPath( host, port, initialDir, user, pwd));
|
String ctStr = etConnectTimeout.getText().toString();
|
||||||
} catch (UnsupportedEncodingException e) {
|
if (!ctStr.isEmpty()) {
|
||||||
// TODO Auto-generated catch block
|
try {
|
||||||
e.printStackTrace();
|
int ct = Integer.parseInt(ctStr);
|
||||||
|
if (connectTimeout != ct) {
|
||||||
|
connectTimeout = ct;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException parseEx) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
EditText etKeyName = view.findViewById(R.id.private_key_name);
|
||||||
|
String keyName = etKeyName.getText().toString();
|
||||||
|
EditText etKeyPassphrase = view.findViewById(R.id.private_key_passphrase);
|
||||||
|
String keyPassphrase = etKeyPassphrase.getText().toString();
|
||||||
|
EditText etKex = view.findViewById(R.id.kex);
|
||||||
|
String kex = etKex.getText().toString();
|
||||||
|
EditText etShk = view.findViewById(R.id.shk);
|
||||||
|
String shk = etShk.getText().toString();
|
||||||
|
|
||||||
|
onReceivePathForFileSelect(requestCode, sftpStorage1.buildFullPath(
|
||||||
|
host, port, initialDir, user, pwd, connectTimeout,
|
||||||
|
keyName, keyPassphrase, kex, shk));
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.create()
|
.create()
|
||||||
|
|||||||
@@ -3,69 +3,217 @@
|
|||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="fill_parent"
|
android:layout_height="fill_parent"
|
||||||
android:layout_margin="12dip"
|
android:layout_margin="12dip">
|
||||||
>
|
<LinearLayout
|
||||||
<LinearLayout
|
android:orientation="horizontal"
|
||||||
android:orientation="horizontal"
|
android:layout_width="fill_parent"
|
||||||
android:layout_width="fill_parent"
|
android:layout_height="wrap_content">
|
||||||
android:layout_height="wrap_content">
|
<EditText
|
||||||
<EditText
|
|
||||||
android:id="@+id/sftp_host"
|
android:id="@+id/sftp_host"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="10"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:inputType="textNoSuggestions"
|
android:inputType="textNoSuggestions"
|
||||||
android:text=""
|
android:text=""
|
||||||
android:hint="@string/hint_sftp_host" />
|
android:hint="@string/hint_sftp_host" />
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/portsep"
|
android:id="@+id/portsep"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text=":" />
|
android:text=":" />
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/sftp_port"
|
android:id="@+id/sftp_port"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="15"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:inputType="number"
|
android:inputType="number"
|
||||||
android:text="22"
|
android:text="22"
|
||||||
android:hint="@string/hint_sftp_port" />
|
android:hint="@string/hint_sftp_port" />
|
||||||
</LinearLayout>
|
<EditText
|
||||||
<EditText
|
android:id="@+id/sftp_connect_timeout"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="14"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:inputType="number"
|
||||||
|
android:text=""
|
||||||
|
android:hint="@string/hint_sftp_connect_timeout" />
|
||||||
|
</LinearLayout>
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<EditText
|
||||||
android:id="@+id/sftp_user"
|
android:id="@+id/sftp_user"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:text=""
|
android:text=""
|
||||||
android:hint="@string/hint_username" />
|
android:hint="@string/hint_username" />
|
||||||
|
<EditText
|
||||||
<EditText
|
|
||||||
android:id="@+id/sftp_password"
|
android:id="@+id/sftp_password"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
android:inputType="textPassword"
|
android:inputType="textPassword"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:text=""
|
android:text=""
|
||||||
android:hint="@string/hint_pass"
|
android:hint="@string/hint_pass"
|
||||||
android:importantForAccessibility="no" />
|
android:importantForAccessibility="no" />
|
||||||
|
</LinearLayout>
|
||||||
<TextView android:id="@+id/initial_dir"
|
<LinearLayout
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<TextView android:id="@+id/initial_dir"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginLeft="4dip"
|
android:layout_marginLeft="4dip"
|
||||||
android:layout_marginTop="4dip"
|
android:layout_marginTop="4dip"
|
||||||
|
|
||||||
android:text="@string/initial_directory" />
|
android:text="@string/initial_directory" />
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/sftp_initial_dir"
|
android:id="@+id/sftp_initial_dir"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dip"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:text="/home/philipp"
|
android:text="/home/philipp"
|
||||||
/>
|
/>
|
||||||
<Button android:id="@+id/send_public_key"
|
</LinearLayout>
|
||||||
android:layout_width="fill_parent"
|
<LinearLayout
|
||||||
android:layout_height="wrap_content"
|
android:orientation="horizontal"
|
||||||
android:text="send public key" />
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<EditText android:id="@+id/kex"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:inputType="textNoSuggestions"
|
||||||
|
android:text=""
|
||||||
|
android:hint="KEX Algs" />
|
||||||
|
<EditText android:id="@+id/shk"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:inputType="textNoSuggestions"
|
||||||
|
android:text=""
|
||||||
|
android:hint="Server Host Key Algs" />
|
||||||
|
</LinearLayout>
|
||||||
|
<Button android:id="@+id/send_public_key"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="send public key" />
|
||||||
|
<TextView
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:text="Private Keys Functions" />
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<Button android:id="@+id/list_private_keys"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="List" />
|
||||||
|
<Button android:id="@+id/add_private_key"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Add" />
|
||||||
|
<Button android:id="@+id/delete_private_key"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Delete" />
|
||||||
|
<Button android:id="@+id/validate_private_key"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Validate" />
|
||||||
|
</LinearLayout>
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<EditText android:id="@+id/private_key_name"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:inputType="textNoSuggestions"
|
||||||
|
android:text=""
|
||||||
|
android:hint="key name" />
|
||||||
|
<EditText android:id="@+id/private_key_passphrase"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:inputType="textNoSuggestions"
|
||||||
|
android:text=""
|
||||||
|
android:hint="passphrase (optional)" />
|
||||||
|
</LinearLayout>
|
||||||
|
<EditText android:id="@+id/private_key_content"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textMultiLine"
|
||||||
|
android:lines="4"
|
||||||
|
android:text=""
|
||||||
|
android:hint="key content" />
|
||||||
|
<TextView
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:text="CSV Resolver Functions" />
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<EditText android:id="@+id/mock_csv_specs"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:inputType="textNoSuggestions"
|
||||||
|
android:text=""
|
||||||
|
android:hint="Test specs" />
|
||||||
|
<EditText android:id="@+id/mock_csv_cfg"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:inputType="textNoSuggestions"
|
||||||
|
android:text=""
|
||||||
|
android:hint="Test config" />
|
||||||
|
</LinearLayout>
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<Button android:id="@+id/reset_mock_csv"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginLeft="50dp"
|
||||||
|
android:layout_marginRight="5dp"
|
||||||
|
android:text="Reset" />
|
||||||
|
<Button android:id="@+id/resolve_mock_csv"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginRight="50dp"
|
||||||
|
android:layout_marginLeft="5dp"
|
||||||
|
android:text="Resolve" />
|
||||||
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:inputType="textNoSuggestions"
|
android:inputType="textNoSuggestions"
|
||||||
android:text="https://daigers.diskstation.me:5006/Keepass2Android/Apps/Keepass2Android/"
|
android:text="https://webdav.hidrive.ionos.com/users/philippcro"
|
||||||
android:hint="Server URL" />
|
android:hint="Server URL" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
<EditText
|
<EditText
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:text="Keepass"
|
android:text="PhilippCro"
|
||||||
android:hint="@string/hint_username" />
|
android:hint="@string/hint_username" />
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="textPassword"
|
android:inputType="textPassword"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:text="$T3st17$"
|
android:text="WSBa1wh4o4YyLK"
|
||||||
android:hint="@string/hint_pass"
|
android:hint="@string/hint_pass"
|
||||||
android:importantForAccessibility="no" />
|
android:importantForAccessibility="no" />
|
||||||
|
|
||||||
|
|||||||
@@ -333,6 +333,7 @@
|
|||||||
|
|
||||||
<string name="hint_sftp_host">host</string>
|
<string name="hint_sftp_host">host</string>
|
||||||
<string name="hint_sftp_port">port</string>
|
<string name="hint_sftp_port">port</string>
|
||||||
|
<string name="hint_sftp_connect_timeout">timeout sec</string>
|
||||||
|
|
||||||
<string name="select_storage_type">Select the storage type:</string>
|
<string name="select_storage_type">Select the storage type:</string>
|
||||||
|
|
||||||
@@ -524,6 +525,6 @@ Initial public release
|
|||||||
<item>Do not accept invalid certificates</item>
|
<item>Do not accept invalid certificates</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string name="initial_directory">Initial directory (optional):</string>
|
<string name="initial_directory">Initial dir (optional):</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "com.android.tools.build:gradle:7.4.0"
|
classpath "com.android.tools.build:gradle:8.4.0"
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ android {
|
|||||||
|
|
||||||
namespace 'keepass2android.kp2akeytransform'
|
namespace 'keepass2android.kp2akeytransform'
|
||||||
|
|
||||||
compileSdkVersion 33
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
ndk.abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'
|
ndk.abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'
|
||||||
|
minSdkVersion 21
|
||||||
|
targetSdkVersion 34
|
||||||
|
compileSdk 34
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "com.android.tools.build:gradle:7.4.0"
|
classpath "com.android.tools.build:gradle:8.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ android {
|
|||||||
|
|
||||||
namespace 'keepass2android.softkeyboard'
|
namespace 'keepass2android.softkeyboard'
|
||||||
|
|
||||||
compileSdkVersion 33
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 18
|
minSdkVersion 21
|
||||||
|
compileSdk 34
|
||||||
|
targetSdk 34
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -33,11 +33,11 @@
|
|||||||
<!-- Option to enable using nearby keys when correcting/predicting -->
|
<!-- Option to enable using nearby keys when correcting/predicting -->
|
||||||
<string name="hit_correction">Corregir errores de escritura</string>
|
<string name="hit_correction">Corregir errores de escritura</string>
|
||||||
<!-- Description for hit_correction -->
|
<!-- Description for hit_correction -->
|
||||||
<string name="hit_correction_summary">Habilitar la introducción de corrección de errores</string>
|
<string name="hit_correction_summary">Habilitar corrección de errores de entrada</string>
|
||||||
<!-- Option to enable using nearby keys when correcting/predicting in landscape-->
|
<!-- Option to enable using nearby keys when correcting/predicting in landscape-->
|
||||||
<string name="hit_correction_land">Errores de introducción de datos en vista horizontal</string>
|
<string name="hit_correction_land">Errores de introducción de datos en vista horizontal</string>
|
||||||
<!-- Description for hit_correction in landscape -->
|
<!-- Description for hit_correction in landscape -->
|
||||||
<string name="hit_correction_land_summary">Habilitar la introducción de corrección de errores</string>
|
<string name="hit_correction_land_summary">Habilitar corrección de errores de entrada</string>
|
||||||
<!-- Option to automatically correct word on hitting space -->
|
<!-- Option to automatically correct word on hitting space -->
|
||||||
<string name="auto_correction">Sugerencias de palabras</string>
|
<string name="auto_correction">Sugerencias de palabras</string>
|
||||||
<!-- Description for auto_correction -->
|
<!-- Description for auto_correction -->
|
||||||
|
|||||||
@@ -47,15 +47,15 @@
|
|||||||
<!-- Category title for text prediction -->
|
<!-- Category title for text prediction -->
|
||||||
<string name="prediction_category">Instellingen voor woordsuggesties</string>
|
<string name="prediction_category">Instellingen voor woordsuggesties</string>
|
||||||
<!-- Description for text prediction -->
|
<!-- Description for text prediction -->
|
||||||
<string name="prediction_summary">Automatisch voltooien tijdens typen inschakelen</string>
|
<string name="prediction_summary">Automatisch aanvullen tijdens typen inschakelen</string>
|
||||||
<!-- Dialog title for auto complete choices -->
|
<!-- Dialog title for auto complete choices -->
|
||||||
<string name="auto_complete_dialog_title">Automatisch voltooien</string>
|
<string name="auto_complete_dialog_title">Automatisch aanvullen</string>
|
||||||
<!-- Option to enable text prediction in landscape -->
|
<!-- Option to enable text prediction in landscape -->
|
||||||
<string name="prediction_landscape">Tekstveld vergroten</string>
|
<string name="prediction_landscape">Tekstveld vergroten</string>
|
||||||
<!-- Description for text prediction -->
|
<!-- Description for text prediction -->
|
||||||
<string name="prediction_landscape_summary">Woordsuggesties verbergen in liggende weergave</string>
|
<string name="prediction_landscape_summary">Woordsuggesties verbergen in liggende weergave</string>
|
||||||
<!-- Option to enable auto capitalization of sentences -->
|
<!-- Option to enable auto capitalization of sentences -->
|
||||||
<string name="auto_cap">Auto-hoofdlettergebruik</string>
|
<string name="auto_cap">Automatisch hoofdlettergebruik</string>
|
||||||
<!-- Description for auto cap -->
|
<!-- Description for auto cap -->
|
||||||
<string name="auto_cap_summary">Hoofdletter gebruiken aan het begin van een zin</string>
|
<string name="auto_cap_summary">Hoofdletter gebruiken aan het begin van een zin</string>
|
||||||
<!-- Option to enable auto punctuate -->
|
<!-- Option to enable auto punctuate -->
|
||||||
@@ -64,15 +64,15 @@
|
|||||||
<!-- Option to enable quick fixes -->
|
<!-- Option to enable quick fixes -->
|
||||||
<string name="quick_fixes">Snelle oplossingen</string>
|
<string name="quick_fixes">Snelle oplossingen</string>
|
||||||
<!-- Description for quick fixes -->
|
<!-- Description for quick fixes -->
|
||||||
<string name="quick_fixes_summary">Hiermee worden veelvoorkomende typefouten gecorrigeerd</string>
|
<string name="quick_fixes_summary">Corrigeert veelgemaakte typefouten</string>
|
||||||
<!-- Option to enable showing suggestions -->
|
<!-- Option to enable showing suggestions -->
|
||||||
<string name="show_suggestions">Suggesties weergeven</string>
|
<string name="show_suggestions">Suggesties weergeven</string>
|
||||||
<!-- Description for show suggestions -->
|
<!-- Description for show suggestions -->
|
||||||
<string name="show_suggestions_summary">Voorgestelde woorden weergeven tijdens typen</string>
|
<string name="show_suggestions_summary">Woordsuggesties tijdens het typen tonen</string>
|
||||||
<!-- Option to enable auto completion -->
|
<!-- Option to enable auto completion -->
|
||||||
<string name="auto_complete">Auto-aanvullen</string>
|
<string name="auto_complete">Auto-aanvullen</string>
|
||||||
<!-- Description for auto completion -->
|
<!-- Description for auto completion -->
|
||||||
<string name="auto_complete_summary">Gemarkeerd woord automatisch invoegen met spatiebalk en interpunctie</string>
|
<string name="auto_complete_summary">Spatiebalk en interpunctie voegen automatisch gemarkeerd woord in</string>
|
||||||
<!-- Option to show/hide the settings key -->
|
<!-- Option to show/hide the settings key -->
|
||||||
<string name="prefs_settings_key">Instellingscode weergeven</string>
|
<string name="prefs_settings_key">Instellingscode weergeven</string>
|
||||||
<!-- Array of the settings key mode values -->
|
<!-- Array of the settings key mode values -->
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "com.android.tools.build:gradle:7.4.0"
|
classpath "com.android.tools.build:gradle:8.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "com.android.tools.build:gradle:7.4.0"
|
classpath "com.android.tools.build:gradle:8.4.0"
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ android {
|
|||||||
debuggable false
|
debuggable false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig true
|
||||||
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
targetCompatibility 11
|
targetCompatibility 11
|
||||||
sourceCompatibility 11
|
sourceCompatibility 11
|
||||||
|
|||||||
@@ -221,7 +221,6 @@ public class FileChooserActivity extends FragmentActivity {
|
|||||||
|
|
||||||
public static final String EXTRA_RESULT_FILE_EXISTS = CLASSNAME + ".result_file_exists";
|
public static final String EXTRA_RESULT_FILE_EXISTS = CLASSNAME + ".result_file_exists";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* CONTROLS
|
* CONTROLS
|
||||||
|
|||||||
@@ -269,7 +269,6 @@ public class FragmentFiles extends Fragment implements
|
|||||||
FileChooserActivity.EXTRA_MAX_FILE_COUNT, 1000);
|
FileChooserActivity.EXTRA_MAX_FILE_COUNT, 1000);
|
||||||
mFileAdapter = new BaseFileAdapter(getActivity(), mFilterMode,
|
mFileAdapter = new BaseFileAdapter(getActivity(), mFilterMode,
|
||||||
mIsMultiSelection);
|
mIsMultiSelection);
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* History.
|
* History.
|
||||||
@@ -2268,12 +2267,15 @@ public class FragmentFiles extends Fragment implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mIsSaveDialog) {
|
if (mIsSaveDialog) {
|
||||||
mTextSaveas.setText(BaseFileProviderUtils.getFileName(cursor));
|
String fileName = BaseFileProviderUtils.getFileName(cursor);
|
||||||
|
Uri uri = BaseFileProviderUtils.getUri(cursor);
|
||||||
|
|
||||||
|
mTextSaveas.setText(fileName);
|
||||||
/*
|
/*
|
||||||
* Always set tag after setting text, or tag will be reset to
|
* Always set tag after setting text, or tag will be reset to
|
||||||
* null.
|
* null.
|
||||||
*/
|
*/
|
||||||
mTextSaveas.setTag(BaseFileProviderUtils.getUri(cursor));
|
mTextSaveas.setTag(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mDoubleTapToChooseFiles) {
|
if (mDoubleTapToChooseFiles) {
|
||||||
@@ -2286,10 +2288,12 @@ public class FragmentFiles extends Fragment implements
|
|||||||
if (mIsMultiSelection)
|
if (mIsMultiSelection)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (mIsSaveDialog)
|
if (mIsSaveDialog) {
|
||||||
checkSaveasFilenameAndFinish();
|
checkSaveasFilenameAndFinish();
|
||||||
else
|
}
|
||||||
|
else {
|
||||||
finish(BaseFileProviderUtils.getUri(cursor));
|
finish(BaseFileProviderUtils.getUri(cursor));
|
||||||
|
}
|
||||||
}// single tap to choose files
|
}// single tap to choose files
|
||||||
}// onItemClick()
|
}// onItemClick()
|
||||||
};// mViewFilesOnItemClickListener
|
};// mViewFilesOnItemClickListener
|
||||||
|
|||||||
@@ -15,4 +15,24 @@ public class FileEntry {
|
|||||||
isDirectory = false;
|
isDirectory = false;
|
||||||
canRead = canWrite = true;
|
canRead = canWrite = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder s = new StringBuilder("kp2afilechooser.FileEntry{")
|
||||||
|
.append(displayName).append("|")
|
||||||
|
.append("path=").append(path).append(",sz=").append(sizeInBytes)
|
||||||
|
.append(",").append(isDirectory ? "dir" : "file")
|
||||||
|
.append(",lastMod=").append(lastModifiedTime);
|
||||||
|
|
||||||
|
StringBuilder perms = new StringBuilder();
|
||||||
|
if (canRead)
|
||||||
|
perms.append("r");
|
||||||
|
if (canWrite)
|
||||||
|
perms.append("w");
|
||||||
|
if (perms.length() > 0) {
|
||||||
|
s.append(",").append(perms);
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.append("}").toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public class Kp2aFileChooserBridge {
|
|||||||
.buildUpon()
|
.buildUpon()
|
||||||
.appendPath(defaultPath)
|
.appendPath(defaultPath)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
return intent;
|
return intent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -306,10 +306,9 @@ public abstract class Kp2aFileProvider extends BaseFileProvider {
|
|||||||
String parentPath = getParentPath(path);
|
String parentPath = getParentPath(path);
|
||||||
|
|
||||||
|
|
||||||
if (parentPath == null)
|
if (parentPath == null) {
|
||||||
{
|
|
||||||
if (Utils.doLog())
|
if (Utils.doLog())
|
||||||
Log.d(CLASSNAME, "parent file is null");
|
Log.d(CLASSNAME, "parent file is null");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
FileEntry e;
|
FileEntry e;
|
||||||
@@ -501,10 +500,10 @@ public abstract class Kp2aFileProvider extends BaseFileProvider {
|
|||||||
RowBuilder newRow = matrixCursor.newRow();
|
RowBuilder newRow = matrixCursor.newRow();
|
||||||
newRow.add(id);// _ID
|
newRow.add(id);// _ID
|
||||||
newRow.add(BaseFile
|
newRow.add(BaseFile
|
||||||
.genContentIdUriBase(
|
.genContentIdUriBase(
|
||||||
getAuthority())
|
getAuthority())
|
||||||
.buildUpon().appendPath(f.path)
|
.buildUpon().appendPath(f.path)
|
||||||
.build().toString());
|
.build().toString());
|
||||||
newRow.add(f.path);
|
newRow.add(f.path);
|
||||||
if (f.displayName == null)
|
if (f.displayName == null)
|
||||||
Log.w("KP2AJ", "displayName is null for " + f.path);
|
Log.w("KP2AJ", "displayName is null for " + f.path);
|
||||||
@@ -549,7 +548,7 @@ public abstract class Kp2aFileProvider extends BaseFileProvider {
|
|||||||
//puts the file entry in the cache for later reuse with retrieveFileInfo
|
//puts the file entry in the cache for later reuse with retrieveFileInfo
|
||||||
private void updateFileEntryCache(FileEntry f) {
|
private void updateFileEntryCache(FileEntry f) {
|
||||||
if (f != null)
|
if (f != null)
|
||||||
fileEntryMap.put(f.path, f);
|
fileEntryMap.put(f.path, f);
|
||||||
}
|
}
|
||||||
//removes the file entry from the cache (if cached). Should be called whenever the file changes
|
//removes the file entry from the cache (if cached). Should be called whenever the file changes
|
||||||
private void removeFromCache(String filename, boolean recursive) {
|
private void removeFromCache(String filename, boolean recursive) {
|
||||||
@@ -584,7 +583,7 @@ public abstract class Kp2aFileProvider extends BaseFileProvider {
|
|||||||
|
|
||||||
//returns the file entry from the cache if present or queries the concrete provider method to return the file info
|
//returns the file entry from the cache if present or queries the concrete provider method to return the file info
|
||||||
private FileEntry getFileEntryCached(String filename) {
|
private FileEntry getFileEntryCached(String filename) {
|
||||||
//check if enry is cached:
|
//check if entry is cached:
|
||||||
FileEntry cachedEntry = fileEntryMap.get(filename);
|
FileEntry cachedEntry = fileEntryMap.get(filename);
|
||||||
if (cachedEntry != null)
|
if (cachedEntry != null)
|
||||||
{
|
{
|
||||||
@@ -728,7 +727,7 @@ public abstract class Kp2aFileProvider extends BaseFileProvider {
|
|||||||
if (targetParent != null && targetParent.startsWith(source))
|
if (targetParent != null && targetParent.startsWith(source))
|
||||||
{
|
{
|
||||||
if (Utils.doLog())
|
if (Utils.doLog())
|
||||||
Log.d("KP2A_FC_P", source+" is parent of "+target);
|
Log.d("KP2A_FC_P", source + " is parent of " + target);
|
||||||
return BaseFileProviderUtils.newClosedCursor();
|
return BaseFileProviderUtils.newClosedCursor();
|
||||||
}
|
}
|
||||||
if (Utils.doLog())
|
if (Utils.doLog())
|
||||||
@@ -768,28 +767,37 @@ public abstract class Kp2aFileProvider extends BaseFileProvider {
|
|||||||
|
|
||||||
private String getParentPath(String path)
|
private String getParentPath(String path)
|
||||||
{
|
{
|
||||||
path = removeTrailingSlash(path);
|
String params = null;
|
||||||
if (path.indexOf("://") == -1)
|
int paramsIdx = path.lastIndexOf("?");
|
||||||
{
|
if (paramsIdx > 0) {
|
||||||
Log.d("KP2A_FC_P", "invalid path: " + path);
|
params = path.substring(paramsIdx);
|
||||||
return null;
|
path = path.substring(0, paramsIdx);
|
||||||
}
|
}
|
||||||
String pathWithoutProtocol = path.substring(path.indexOf("://")+3);
|
|
||||||
int lastSlashPos = path.lastIndexOf("/");
|
path = removeTrailingSlash(path);
|
||||||
if (pathWithoutProtocol.indexOf("/") == -1)
|
if (path.indexOf("://") == -1)
|
||||||
{
|
{
|
||||||
Log.d("KP2A_FC_P", "parent of " + path +" is null");
|
Log.d("KP2A_FC_P", "invalid path: " + path);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
else
|
String pathWithoutProtocol = path.substring(path.indexOf("://") + 3);
|
||||||
{
|
int lastSlashPos = path.lastIndexOf("/");
|
||||||
String parent = path.substring(0, lastSlashPos)+"/";
|
if (pathWithoutProtocol.indexOf("/") == -1)
|
||||||
Log.d("KP2A_FC_P", "parent of " + path +" is "+parent);
|
{
|
||||||
return parent;
|
Log.d("KP2A_FC_P", "parent of " + path + " is null");
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
String parent = path.substring(0, lastSlashPos) + "/";
|
||||||
|
if (params != null) {
|
||||||
|
parent += params;
|
||||||
|
}
|
||||||
|
Log.d("KP2A_FC_P", "parent of " + path +" is " + parent);
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
protected abstract FileEntry getFileEntry(String path, StringBuilder errorMessageBuilder) throws Exception;
|
protected abstract FileEntry getFileEntry(String path, StringBuilder errorMessageBuilder) throws Exception;
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,11 @@
|
|||||||
<string name="afc_msg_failed_please_try_again">Mislykkedes. Forsøg igen.</string>
|
<string name="afc_msg_failed_please_try_again">Mislykkedes. Forsøg igen.</string>
|
||||||
<string name="afc_msg_loading">Indlæser…</string>
|
<string name="afc_msg_loading">Indlæser…</string>
|
||||||
<string name="afc_phone">Telefon</string>
|
<string name="afc_phone">Telefon</string>
|
||||||
<string name="afc_pmsg_cannot_access_dir">Kan ikke tilgå \"%1$s\"</string>
|
<string name="afc_pmsg_cannot_access_dir">Kan ikke få adgang til \"%1$s\"</string>
|
||||||
<string name="afc_pmsg_cannot_create_folder">Kan ikke oprette mappen \"%1$s\"</string>
|
<string name="afc_pmsg_cannot_create_folder">Kan ikke oprette mappen \"%1$s\"</string>
|
||||||
<string name="afc_pmsg_cannot_delete_file">Kan ikke slette %1$s \"%2$s\"</string>
|
<string name="afc_pmsg_cannot_delete_file">Kan ikke slette %1$s \"%2$s\"</string>
|
||||||
<string name="afc_pmsg_confirm_delete_file">Sikker på, at du vil slette denne %1$s \"%2$s\"?</string>
|
<string name="afc_pmsg_confirm_delete_file">Er du sikker på at du vil slette denne %1$s \"%2$s\"?</string>
|
||||||
<string name="afc_pmsg_confirm_replace_file">Denne fil \"%1$s\" findes allerede.\n\nSkal den erstattes?</string>
|
<string name="afc_pmsg_confirm_replace_file">Filen \"%1$s\" eksisterer allerede.\n\nØnsker du at overskrive den?</string>
|
||||||
<string name="afc_pmsg_deleting_file">Sletter %1$s \"%2$s\"…</string>
|
<string name="afc_pmsg_deleting_file">Sletter %1$s \"%2$s\"…</string>
|
||||||
<string name="afc_pmsg_file_has_been_deleted">%1$s \"%2$s\" er slettet</string>
|
<string name="afc_pmsg_file_has_been_deleted">%1$s \"%2$s\" er slettet</string>
|
||||||
<string name="afc_pmsg_filename_is_directory">\"%1$s\" er en mappe</string>
|
<string name="afc_pmsg_filename_is_directory">\"%1$s\" er en mappe</string>
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
<string name="afc_title_name">Navn</string>
|
<string name="afc_title_name">Navn</string>
|
||||||
<string name="afc_title_save_as">Gem som…</string>
|
<string name="afc_title_save_as">Gem som…</string>
|
||||||
<string name="afc_title_size">Størrelse</string>
|
<string name="afc_title_size">Størrelse</string>
|
||||||
<string name="afc_title_sort_by">Sortér efter…</string>
|
<string name="afc_title_sort_by">Sorter efter…</string>
|
||||||
<string name="afc_yesterday">I går</string>
|
<string name="afc_yesterday">I går</string>
|
||||||
<plurals name="afc_title_choose_directories">
|
<plurals name="afc_title_choose_directories">
|
||||||
<item quantity="one">Vælg mappe…</item>
|
<item quantity="one">Vælg mappe…</item>
|
||||||
|
|||||||
@@ -34,13 +34,13 @@
|
|||||||
<string name="afc_msg_loading">Lade…</string>
|
<string name="afc_msg_loading">Lade…</string>
|
||||||
<string name="afc_phone">Gerät</string>
|
<string name="afc_phone">Gerät</string>
|
||||||
<string name="afc_pmsg_cannot_access_dir">Kann nicht auf \"%1$s\" zugreifen</string>
|
<string name="afc_pmsg_cannot_access_dir">Kann nicht auf \"%1$s\" zugreifen</string>
|
||||||
<string name="afc_pmsg_cannot_create_folder">Kann Verzeichnis \"%1$s\" nicht anlegen</string>
|
<string name="afc_pmsg_cannot_create_folder">Ordner „%1$s“ kann nicht erstellt werden</string>
|
||||||
<string name="afc_pmsg_cannot_delete_file">Kann %1$s \"%2$s\" nicht löschen</string>
|
<string name="afc_pmsg_cannot_delete_file">Kann %1$s \"%2$s\" nicht löschen</string>
|
||||||
<string name="afc_pmsg_confirm_delete_file">Bist du sicher, dass du \"%2$s\" (%1$s) löschen möchtest?</string>
|
<string name="afc_pmsg_confirm_delete_file">Bist du sicher, dass du \"%2$s\" (%1$s) löschen möchtest?</string>
|
||||||
<string name="afc_pmsg_confirm_replace_file">Die Datei \"%1$s\" existiert bereits.\n\nSoll sie ersetzt werden?</string>
|
<string name="afc_pmsg_confirm_replace_file">Die Datei „%1$s“ existiert bereits.\n\nSoll sie ersetzt werden?</string>
|
||||||
<string name="afc_pmsg_deleting_file">Lösche %1$s \"%2$s\"…</string>
|
<string name="afc_pmsg_deleting_file">Lösche %1$s \"%2$s\"…</string>
|
||||||
<string name="afc_pmsg_file_has_been_deleted">%1$s \"%2$s\" wurde gelöscht</string>
|
<string name="afc_pmsg_file_has_been_deleted">%1$s \"%2$s\" wurde gelöscht</string>
|
||||||
<string name="afc_pmsg_filename_is_directory">\"%1$s\" ist ein Verzeichnis</string>
|
<string name="afc_pmsg_filename_is_directory">\"%1$s\" ist ein Ordner</string>
|
||||||
<string name="afc_pmsg_filename_is_invalid">Dateiname \"%1$s\" ist ungültig</string>
|
<string name="afc_pmsg_filename_is_invalid">Dateiname \"%1$s\" ist ungültig</string>
|
||||||
<string name="afc_pmsg_max_file_count_allowed">… hat mehr Dateien, maximal erlaubt: %1$d</string>
|
<string name="afc_pmsg_max_file_count_allowed">… hat mehr Dateien, maximal erlaubt: %1$d</string>
|
||||||
<string name="afc_pmsg_unknown_error">Unbekannter Fehler: %1$s</string>
|
<string name="afc_pmsg_unknown_error">Unbekannter Fehler: %1$s</string>
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
<item quantity="other">Verzeichnisse wählen…</item>
|
<item quantity="other">Verzeichnisse wählen…</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="afc_title_choose_files">
|
<plurals name="afc_title_choose_files">
|
||||||
<item quantity="one">Datei wählen…</item>
|
<item quantity="one">Datei wählen …</item>
|
||||||
<item quantity="other">Dateien wählen …</item>
|
<item quantity="other">Dateien wählen …</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="afc_title_choose_files_directories">
|
<plurals name="afc_title_choose_files_directories">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<resources>
|
<resources>
|
||||||
<string name="afc_cmd_advanced_selection_all">Όλα</string>
|
<string name="afc_cmd_advanced_selection_all">Όλα</string>
|
||||||
<string name="afc_cmd_advanced_selection_invert">Αντιστροφή επιλογής</string>
|
<string name="afc_cmd_advanced_selection_invert">Αναστροφή επιλογής</string>
|
||||||
<string name="afc_cmd_advanced_selection_none">Κανένα</string>
|
<string name="afc_cmd_advanced_selection_none">Κανένα</string>
|
||||||
<string name="afc_cmd_grid_view">Προβολή πλέγματος</string>
|
<string name="afc_cmd_grid_view">Προβολή πλέγματος</string>
|
||||||
<string name="afc_cmd_home">Κεντρική</string>
|
<string name="afc_cmd_home">Κεντρική</string>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<string name="afc_pmsg_file_has_been_deleted">%1$s「%2$s」が削除されました</string>
|
<string name="afc_pmsg_file_has_been_deleted">%1$s「%2$s」が削除されました</string>
|
||||||
<string name="afc_pmsg_filename_is_directory">「%1$s」はフォルダーです</string>
|
<string name="afc_pmsg_filename_is_directory">「%1$s」はフォルダーです</string>
|
||||||
<string name="afc_pmsg_filename_is_invalid">ファイル名「%1$s」は無効です</string>
|
<string name="afc_pmsg_filename_is_invalid">ファイル名「%1$s」は無効です</string>
|
||||||
<string name="afc_pmsg_max_file_count_allowed">ファイルが多すぎます。最大: %1$,d</string>
|
<string name="afc_pmsg_max_file_count_allowed">…のファイルが多すぎます。最大: %1$,d</string>
|
||||||
<string name="afc_pmsg_unknown_error">不明なエラー: %1$s</string>
|
<string name="afc_pmsg_unknown_error">不明なエラー: %1$s</string>
|
||||||
<string name="afc_root">ルート</string>
|
<string name="afc_root">ルート</string>
|
||||||
<string name="afc_title_advanced_selection">選択…</string>
|
<string name="afc_title_advanced_selection">選択…</string>
|
||||||
|
|||||||
@@ -36,9 +36,9 @@
|
|||||||
<string name="afc_pmsg_cannot_access_dir">Geen toegang tot \"%1$s\"</string>
|
<string name="afc_pmsg_cannot_access_dir">Geen toegang tot \"%1$s\"</string>
|
||||||
<string name="afc_pmsg_cannot_create_folder">Kan map \"%1$s\" niet maken</string>
|
<string name="afc_pmsg_cannot_create_folder">Kan map \"%1$s\" niet maken</string>
|
||||||
<string name="afc_pmsg_cannot_delete_file">Kan %1$s \"%2$s\" niet verwijderen</string>
|
<string name="afc_pmsg_cannot_delete_file">Kan %1$s \"%2$s\" niet verwijderen</string>
|
||||||
<string name="afc_pmsg_confirm_delete_file">Weet je zeker dat je deze %1$s wilt verwijderen: \"%2$s\"?</string>
|
<string name="afc_pmsg_confirm_delete_file">Weet je zeker dat je dit wilt verwijderen: %1$s \"%2$s\"?</string>
|
||||||
<string name="afc_pmsg_confirm_replace_file">Het bestand \"%1$s\" bestaat al.\n\nWilt u het vervangen?</string>
|
<string name="afc_pmsg_confirm_replace_file">Het bestand \"%1$s\" bestaat al.\n\nWilt u het vervangen?</string>
|
||||||
<string name="afc_pmsg_deleting_file">%1$s \"%2$s\" verwijderen…</string>
|
<string name="afc_pmsg_deleting_file">Verwijdert %1$s \"%2$s\" …</string>
|
||||||
<string name="afc_pmsg_file_has_been_deleted">%1$s \"%2$s\" is verwijderd</string>
|
<string name="afc_pmsg_file_has_been_deleted">%1$s \"%2$s\" is verwijderd</string>
|
||||||
<string name="afc_pmsg_filename_is_directory">\"%1$s\" is een map</string>
|
<string name="afc_pmsg_filename_is_directory">\"%1$s\" is een map</string>
|
||||||
<string name="afc_pmsg_filename_is_invalid">Bestandsnaam \"%1$s\" is ongeldig</string>
|
<string name="afc_pmsg_filename_is_invalid">Bestandsnaam \"%1$s\" is ongeldig</string>
|
||||||
@@ -56,15 +56,15 @@
|
|||||||
<string name="afc_title_sort_by">Sorteer op…</string>
|
<string name="afc_title_sort_by">Sorteer op…</string>
|
||||||
<string name="afc_yesterday">Gisteren</string>
|
<string name="afc_yesterday">Gisteren</string>
|
||||||
<plurals name="afc_title_choose_directories">
|
<plurals name="afc_title_choose_directories">
|
||||||
<item quantity="one">Kies map…</item>
|
<item quantity="one">Kies map…</item>
|
||||||
<item quantity="other">Kies mappen…</item>
|
<item quantity="other">Kies mappen…</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="afc_title_choose_files">
|
<plurals name="afc_title_choose_files">
|
||||||
<item quantity="one">Kies bestand…</item>
|
<item quantity="one">Kies bestand…</item>
|
||||||
<item quantity="other">Kies bestanden…</item>
|
<item quantity="other">Kies bestanden…</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="afc_title_choose_files_directories">
|
<plurals name="afc_title_choose_files_directories">
|
||||||
<item quantity="one">Kies bestand/ map…</item>
|
<item quantity="one">Kies bestand/ map…</item>
|
||||||
<item quantity="other">Kies bestanden/ mappen…</item>
|
<item quantity="other">Kies bestanden/ mappen…</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -39,12 +39,12 @@
|
|||||||
<string name="afc_pmsg_confirm_delete_file">確定刪除 %1$s \"%2$s\"?</string>
|
<string name="afc_pmsg_confirm_delete_file">確定刪除 %1$s \"%2$s\"?</string>
|
||||||
<string name="afc_pmsg_confirm_replace_file">此檔 \"%1$s\" 已經存在。\n\n是否要覆蓋?</string>
|
<string name="afc_pmsg_confirm_replace_file">此檔 \"%1$s\" 已經存在。\n\n是否要覆蓋?</string>
|
||||||
<string name="afc_pmsg_deleting_file">正在刪除 %1$s「%2$s」…</string>
|
<string name="afc_pmsg_deleting_file">正在刪除 %1$s「%2$s」…</string>
|
||||||
<string name="afc_pmsg_file_has_been_deleted">%1$s\"%2$s\"已被刪除</string>
|
<string name="afc_pmsg_file_has_been_deleted">已刪除 %1$s「%2$s」</string>
|
||||||
<string name="afc_pmsg_filename_is_directory">\"%1$s\"是一個資料夾</string>
|
<string name="afc_pmsg_filename_is_directory">「%1$s」是資料夾</string>
|
||||||
<string name="afc_pmsg_filename_is_invalid">檔案名稱\"%1$s\"不正確</string>
|
<string name="afc_pmsg_filename_is_invalid">檔案名稱「%1$s」無效</string>
|
||||||
<string name="afc_pmsg_max_file_count_allowed">…有更多檔案,最多允許:%1$,d</string>
|
<string name="afc_pmsg_max_file_count_allowed">…有更多檔案,最多允許:%1$,d</string>
|
||||||
<string name="afc_pmsg_unknown_error">未知的錯誤: %1$s</string>
|
<string name="afc_pmsg_unknown_error">未知錯誤:%1$s</string>
|
||||||
<string name="afc_root">Root</string>
|
<string name="afc_root">根目錄</string>
|
||||||
<string name="afc_title_advanced_selection">請選擇...</string>
|
<string name="afc_title_advanced_selection">請選擇...</string>
|
||||||
<string name="afc_title_confirmation">確認</string>
|
<string name="afc_title_confirmation">確認</string>
|
||||||
<string name="afc_title_date">日期</string>
|
<string name="afc_title_date">日期</string>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "com.android.tools.build:gradle:7.4.0"
|
classpath "com.android.tools.build:gradle:8.4.0"
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#Tue Sep 20 20:32:06 CEST 2016
|
#Tue Oct 08 15:41:22 CEST 2024
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ namespace keepass2android
|
|||||||
AlertDialog.Builder builder = new AlertDialog.Builder(new ContextThemeWrapper(ctx, Android.Resource.Style.ThemeHoloLightDialog));
|
AlertDialog.Builder builder = new AlertDialog.Builder(new ContextThemeWrapper(ctx, Android.Resource.Style.ThemeHoloLightDialog));
|
||||||
builder.SetTitle(ctx.GetString(Resource.String.ChangeLog_title));
|
builder.SetTitle(ctx.GetString(Resource.String.ChangeLog_title));
|
||||||
List<string> changeLog = new List<string>{
|
List<string> changeLog = new List<string>{
|
||||||
|
BuildChangelogString(ctx, new List<int>{Resource.Array.ChangeLog_1_11,Resource.Array.ChangeLog_1_11_net}, "1.11"),
|
||||||
|
BuildChangelogString(ctx, Resource.Array.ChangeLog_1_10, "1.10"),
|
||||||
BuildChangelogString(ctx, Resource.Array.ChangeLog_1_09e, "1.09e"),
|
BuildChangelogString(ctx, Resource.Array.ChangeLog_1_09e, "1.09e"),
|
||||||
BuildChangelogString(ctx, Resource.Array.ChangeLog_1_09d, "1.09d"),
|
BuildChangelogString(ctx, Resource.Array.ChangeLog_1_09d, "1.09d"),
|
||||||
BuildChangelogString(ctx, Resource.Array.ChangeLog_1_09c, "1.09c"),
|
BuildChangelogString(ctx, Resource.Array.ChangeLog_1_09c, "1.09c"),
|
||||||
@@ -121,21 +123,32 @@ namespace keepass2android
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildChangelogString(Context ctx, int changeLogResId, string version)
|
private static string BuildChangelogString(Context ctx, int changeLogResId, string version)
|
||||||
{
|
{
|
||||||
string result = "Version " + version + "\n";
|
return BuildChangelogString(ctx, new List<int>() { changeLogResId }, version);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static string BuildChangelogString(Context ctx, List<int> changeLogResIds, string version)
|
||||||
|
{
|
||||||
|
string result = "Version " + version + "\n";
|
||||||
string previous = "";
|
string previous = "";
|
||||||
foreach (var item in ctx.Resources.GetStringArray(changeLogResId))
|
foreach (var changeLogResId in changeLogResIds)
|
||||||
{
|
{
|
||||||
if (item == previous) //there was some trouble with crowdin translations, remove duplicates
|
foreach (var item in ctx.Resources.GetStringArray(changeLogResId))
|
||||||
continue;
|
{
|
||||||
result += " * " + item + "\n";
|
if (item == previous) //there was some trouble with crowdin translations, remove duplicates
|
||||||
previous = item;
|
continue;
|
||||||
|
result += " * " + item + "\n";
|
||||||
|
previous = item;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
|
return result;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private const string HtmlStart = @"<html>
|
private const string HtmlStart = @"<html>
|
||||||
<head>
|
<head>
|
||||||
<style type='text/css'>
|
<style type='text/css'>
|
||||||
a { color:#000000 }
|
a { color:#000000 }
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ using Android.Text.Method;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Android.Content.PM;
|
using Android.Content.PM;
|
||||||
using Android.Webkit;
|
using Android.Webkit;
|
||||||
using Android.Graphics;
|
using Android.Graphics;
|
||||||
@@ -48,6 +49,10 @@ using KeePassLib.Serialization;
|
|||||||
using PluginTOTP;
|
using PluginTOTP;
|
||||||
using File = Java.IO.File;
|
using File = Java.IO.File;
|
||||||
using Uri = Android.Net.Uri;
|
using Uri = Android.Net.Uri;
|
||||||
|
using keepass2android.fileselect;
|
||||||
|
using KeeTrayTOTP.Libraries;
|
||||||
|
using Boolean = Java.Lang.Boolean;
|
||||||
|
using Android.Util;
|
||||||
|
|
||||||
namespace keepass2android
|
namespace keepass2android
|
||||||
{
|
{
|
||||||
@@ -284,6 +289,8 @@ namespace keepass2android
|
|||||||
extraGroup.AddView(view.View);
|
extraGroup.AddView(view.View);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SetPasswordStyle();
|
||||||
|
|
||||||
//update the Entry output in the App database and notify the CopyToClipboard service
|
//update the Entry output in the App database and notify the CopyToClipboard service
|
||||||
|
|
||||||
if (App.Kp2a.LastOpenedEntry != null)
|
if (App.Kp2a.LastOpenedEntry != null)
|
||||||
@@ -486,10 +493,11 @@ namespace keepass2android
|
|||||||
_pluginFieldReceiver = new PluginFieldReceiver(this);
|
_pluginFieldReceiver = new PluginFieldReceiver(this);
|
||||||
RegisterReceiver(_pluginFieldReceiver, new IntentFilter(Strings.ActionSetEntryField));
|
RegisterReceiver(_pluginFieldReceiver, new IntentFilter(Strings.ActionSetEntryField));
|
||||||
|
|
||||||
new Thread(NotifyPluginsOnOpen).Start();
|
var notifyPluginsOnOpenThread = new Thread(NotifyPluginsOnOpen);
|
||||||
|
notifyPluginsOnOpenThread.Start();
|
||||||
|
|
||||||
//the rest of the things to do depends on the current app task:
|
//the rest of the things to do depends on the current app task:
|
||||||
AppTask.CompleteOnCreateEntryActivity(this);
|
AppTask.CompleteOnCreateEntryActivity(this, notifyPluginsOnOpenThread);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveFromHistory()
|
private void RemoveFromHistory()
|
||||||
@@ -554,21 +562,90 @@ namespace keepass2android
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
internal void StartNotificationsService(bool activateKeyboard)
|
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
|
||||||
{
|
{
|
||||||
Intent showNotIntent = new Intent(this, typeof (CopyToClipboardService));
|
if (permissions.Length == 1 && permissions.First() == Android.Manifest.Permission.PostNotifications &&
|
||||||
showNotIntent.SetAction(Intents.ShowNotification);
|
grantResults.First() == Permission.Granted)
|
||||||
showNotIntent.PutExtra(KeyEntry, new ElementAndDatabaseId(App.Kp2a.CurrentDb, Entry).FullId);
|
{
|
||||||
AppTask.PopulatePasswordAccessServiceIntent(showNotIntent);
|
StartNotificationsServiceAfterPermissionsCheck(requestCode == 1 /*requestCode is used to transfer this flag*/);
|
||||||
showNotIntent.PutExtra(KeyActivateKeyboard, activateKeyboard);
|
}
|
||||||
|
|
||||||
StartService(showNotIntent);
|
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
}
|
}
|
||||||
|
internal void StartNotificationsService(bool activateKeyboard)
|
||||||
|
{
|
||||||
|
if (PreferenceManager.GetDefaultSharedPreferences(this).GetBoolean(
|
||||||
|
GetString(Resource.String.CopyToClipboardNotification_key),
|
||||||
|
Resources.GetBoolean(Resource.Boolean.CopyToClipboardNotification_default)) == false
|
||||||
|
&& PreferenceManager.GetDefaultSharedPreferences(this).GetBoolean(
|
||||||
|
GetString(Resource.String.UseKp2aKeyboard_key),
|
||||||
|
Resources.GetBoolean(Resource.Boolean.UseKp2aKeyboard_default)) == false)
|
||||||
|
{
|
||||||
|
//notifications are disabled
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((int)Build.VERSION.SdkInt < 33 || CheckSelfPermission(Android.Manifest.Permission.PostNotifications) ==
|
||||||
|
Permission.Granted)
|
||||||
|
{
|
||||||
|
StartNotificationsServiceAfterPermissionsCheck(activateKeyboard);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//user has not yet granted Android 13's POST_NOTIFICATONS permission for the app.
|
||||||
|
|
||||||
|
//check if we should ask them to grant:
|
||||||
|
if (!ShouldShowRequestPermissionRationale(Android.Manifest.Permission.PostNotifications) //this menthod returns false if we haven't asked yet or if the user has denied permission too often
|
||||||
|
&& PreferenceManager.GetDefaultSharedPreferences(this).GetBoolean("RequestedPostNotificationsPermission", false))//use a preference to tell the difference between "haven't asked yet" and "have asked too often"
|
||||||
|
{
|
||||||
|
//user has denied permission before. Do not show the dialog. User must give permission in the Android App settings.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
new AlertDialog.Builder(this)
|
||||||
|
.SetTitle(Resource.String.post_notifications_dialog_title)
|
||||||
|
.SetMessage(Resource.String.post_notifications_dialog_message)
|
||||||
|
.SetNegativeButton(Resource.String.post_notifications_dialog_disable, (sender, args) =>
|
||||||
|
{
|
||||||
|
//disable this dialog for the future by disabling the notification preferences
|
||||||
|
var edit= PreferenceManager.GetDefaultSharedPreferences(this).Edit();
|
||||||
|
edit.PutBoolean(GetString(Resource.String.CopyToClipboardNotification_key), false);
|
||||||
|
edit.PutBoolean(GetString(Resource.String.UseKp2aKeyboard_key), false);
|
||||||
|
edit.Commit();
|
||||||
|
})
|
||||||
|
.SetPositiveButton(Resource.String.post_notifications_dialog_allow, (sender, args) =>
|
||||||
|
{
|
||||||
|
|
||||||
|
//remember that we did ask for permission at least once:
|
||||||
|
var edit = PreferenceManager.GetDefaultSharedPreferences(this).Edit();
|
||||||
|
edit.PutBoolean("RequestedPostNotificationsPermission", true);
|
||||||
|
edit.Commit();
|
||||||
|
|
||||||
|
//request permission. user must grant, we'll show notifications in the OnRequestPermissionResults() callback
|
||||||
|
Android.Support.V4.App.ActivityCompat.RequestPermissions(this, new[] { Android.Manifest.Permission.PostNotifications }, activateKeyboard ? 1 : 0 /*use requestCode to transfer the flag*/);
|
||||||
|
|
||||||
|
|
||||||
private String getDateTime(DateTime dt)
|
})
|
||||||
|
.SetNeutralButton(Resource.String.post_notifications_dialog_notnow, (sender, args) => { })
|
||||||
|
.Show();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartNotificationsServiceAfterPermissionsCheck(bool activateKeyboard)
|
||||||
|
{
|
||||||
|
Intent showNotIntent = new Intent(this, typeof(CopyToClipboardService));
|
||||||
|
showNotIntent.SetAction(Intents.ShowNotification);
|
||||||
|
showNotIntent.PutExtra(KeyEntry, new ElementAndDatabaseId(App.Kp2a.CurrentDb, Entry).FullId);
|
||||||
|
AppTask.PopulatePasswordAccessServiceIntent(showNotIntent);
|
||||||
|
showNotIntent.PutExtra(KeyActivateKeyboard, activateKeyboard);
|
||||||
|
|
||||||
|
StartService(showNotIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private String getDateTime(DateTime dt)
|
||||||
{
|
{
|
||||||
return dt.ToLocalTime().ToString("g", CultureInfo.CurrentUICulture);
|
return dt.ToLocalTime().ToString("g", CultureInfo.CurrentUICulture);
|
||||||
}
|
}
|
||||||
@@ -593,7 +670,7 @@ namespace keepass2android
|
|||||||
EditModeBase editMode = new DefaultEdit();
|
EditModeBase editMode = new DefaultEdit();
|
||||||
if (KpEntryTemplatedEdit.IsTemplated(App.Kp2a.CurrentDb, this.Entry))
|
if (KpEntryTemplatedEdit.IsTemplated(App.Kp2a.CurrentDb, this.Entry))
|
||||||
editMode = new KpEntryTemplatedEdit(App.Kp2a.CurrentDb, this.Entry);
|
editMode = new KpEntryTemplatedEdit(App.Kp2a.CurrentDb, this.Entry);
|
||||||
foreach (var key in editMode.SortExtraFieldKeys(Entry.Strings.GetKeys().Where(key=> !PwDefs.IsStandardField(key))))
|
foreach (var key in editMode.SortExtraFieldKeys(Entry.Strings.GetKeys().Where(key=> !PwDefs.IsStandardField(key) && key != Kp2aTotp.TotpKey)))
|
||||||
{
|
{
|
||||||
if (editMode.IsVisible(key))
|
if (editMode.IsVisible(key))
|
||||||
{
|
{
|
||||||
@@ -644,7 +721,7 @@ namespace keepass2android
|
|||||||
var stringView = new ExtraStringView(layout, valueView, valueViewVisible, keyView);
|
var stringView = new ExtraStringView(layout, valueView, valueViewVisible, keyView);
|
||||||
|
|
||||||
_stringViews.Add(key, stringView);
|
_stringViews.Add(key, stringView);
|
||||||
RegisterTextPopup(valueViewContainer, valueViewContainer.FindViewById(Resource.Id.extra_vdots), key, isProtected);
|
RegisterTextPopup(valueViewContainer, valueViewContainer.FindViewById(Resource.Id.extra_vdots), key, isProtected, layout);
|
||||||
|
|
||||||
return stringView;
|
return stringView;
|
||||||
|
|
||||||
@@ -654,6 +731,7 @@ namespace keepass2android
|
|||||||
|
|
||||||
private List<IPopupMenuItem> RegisterPopup(string popupKey, View clickView, View anchorView)
|
private List<IPopupMenuItem> RegisterPopup(string popupKey, View clickView, View anchorView)
|
||||||
{
|
{
|
||||||
|
|
||||||
clickView.Click += (sender, args) =>
|
clickView.Click += (sender, args) =>
|
||||||
{
|
{
|
||||||
ShowPopup(anchorView, popupKey);
|
ShowPopup(anchorView, popupKey);
|
||||||
@@ -769,7 +847,7 @@ namespace keepass2android
|
|||||||
{
|
{
|
||||||
if (!_showPassword.ContainsKey(protectedTextView))
|
if (!_showPassword.ContainsKey(protectedTextView))
|
||||||
{
|
{
|
||||||
_showPassword[protectedTextView] = fieldKey == UpdateTotpTimerTask.TotpKey ? _showTotpDefault : _showPasswordDefault;
|
_showPassword[protectedTextView] = fieldKey == Kp2aTotp.TotpKey ? _showTotpDefault : _showPasswordDefault;
|
||||||
}
|
}
|
||||||
var protectedTextviewGroup = new ProtectedTextviewGroup { ProtectedField = protectedTextView, VisibleProtectedField = visibleTextView};
|
var protectedTextviewGroup = new ProtectedTextviewGroup { ProtectedField = protectedTextView, VisibleProtectedField = visibleTextView};
|
||||||
_protectedTextViews.Add(protectedTextviewGroup);
|
_protectedTextViews.Add(protectedTextviewGroup);
|
||||||
@@ -866,34 +944,41 @@ namespace keepass2android
|
|||||||
iv.SetImageDrawable(Resources.GetDrawable(Resource.Drawable.ic00));
|
iv.SetImageDrawable(Resources.GetDrawable(Resource.Drawable.ic00));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SupportActionBar.Title = Entry.Strings.ReadSafe(PwDefs.TitleField);
|
SupportActionBar.Title = Entry.Strings.ReadSafe(PwDefs.TitleField);
|
||||||
SupportActionBar.SetDisplayHomeAsUpEnabled(true);
|
SupportActionBar.Title = SprEngine.Compile(SupportActionBar.Title, new SprContext(Entry, App.Kp2a.CurrentDb.KpDatabase, SprCompileFlags.All));
|
||||||
|
SupportActionBar.SetDisplayHomeAsUpEnabled(true);
|
||||||
SupportActionBar.SetHomeButtonEnabled(true);
|
SupportActionBar.SetHomeButtonEnabled(true);
|
||||||
|
|
||||||
PopulateGroupText (Resource.Id.entry_group_name, Resource.Id.entryfield_group_container, KeyGroupFullPath);
|
PopulateGroupText (Resource.Id.entry_group_name, Resource.Id.entryfield_group_container, KeyGroupFullPath);
|
||||||
|
|
||||||
PopulateStandardText(Resource.Id.entry_user_name, Resource.Id.entryfield_container_username, PwDefs.UserNameField);
|
PopulateStandardText(Resource.Id.entry_user_name, Resource.Id.entryfield_container_username, PwDefs.UserNameField);
|
||||||
PopulateStandardText(Resource.Id.entry_url, Resource.Id.entryfield_container_url, PwDefs.UrlField);
|
PopulateStandardText(Resource.Id.entry_url, Resource.Id.entryfield_container_url, PwDefs.UrlField);
|
||||||
PopulateStandardText(new List<int> { Resource.Id.entry_password, Resource.Id.entry_password_visible}, Resource.Id.entryfield_container_password, PwDefs.PasswordField);
|
PopulateStandardText(new List<int> { Resource.Id.entry_totp, Resource.Id.entry_totp_visible }, Resource.Id.entryfield_container_totp, Kp2aTotp.TotpKey);
|
||||||
|
PopulateStandardText(new List<int> { Resource.Id.entry_password, Resource.Id.entry_password_visible}, Resource.Id.entryfield_container_password, PwDefs.PasswordField);
|
||||||
|
|
||||||
RegisterProtectedTextView(PwDefs.PasswordField, FindViewById<TextView>(Resource.Id.entry_password), FindViewById<TextView>(Resource.Id.entry_password_visible));
|
RegisterProtectedTextView(PwDefs.PasswordField, FindViewById<TextView>(Resource.Id.entry_password), FindViewById<TextView>(Resource.Id.entry_password_visible));
|
||||||
|
RegisterProtectedTextView(Kp2aTotp.TotpKey, FindViewById<TextView>(Resource.Id.entry_totp), FindViewById<TextView>(Resource.Id.entry_totp_visible));
|
||||||
|
|
||||||
RegisterTextPopup(FindViewById<RelativeLayout> (Resource.Id.groupname_container),
|
RegisterTextPopup(FindViewById<RelativeLayout> (Resource.Id.groupname_container),
|
||||||
FindViewById (Resource.Id.entry_group_name), KeyGroupFullPath);
|
FindViewById (Resource.Id.entry_group_name), KeyGroupFullPath,
|
||||||
|
FindViewById(Resource.Id.entryfield_group_container));
|
||||||
|
|
||||||
RegisterTextPopup(FindViewById<RelativeLayout>(Resource.Id.username_container),
|
RegisterTextPopup(FindViewById<RelativeLayout>(Resource.Id.username_container),
|
||||||
FindViewById(Resource.Id.username_vdots), PwDefs.UserNameField);
|
FindViewById(Resource.Id.username_vdots), PwDefs.UserNameField,
|
||||||
|
FindViewById(Resource.Id.entryfield_container_username));
|
||||||
|
|
||||||
RegisterTextPopup(FindViewById<RelativeLayout>(Resource.Id.url_container),
|
RegisterTextPopup(FindViewById<RelativeLayout>(Resource.Id.url_container),
|
||||||
FindViewById(Resource.Id.url_vdots), PwDefs.UrlField)
|
FindViewById(Resource.Id.url_vdots), PwDefs.UrlField,
|
||||||
|
FindViewById(Resource.Id.entryfield_container_url))
|
||||||
.Add(new GotoUrlMenuItem(this, PwDefs.UrlField));
|
.Add(new GotoUrlMenuItem(this, PwDefs.UrlField));
|
||||||
RegisterTextPopup(FindViewById<RelativeLayout>(Resource.Id.password_container),
|
RegisterTextPopup(FindViewById<RelativeLayout>(Resource.Id.password_container),
|
||||||
FindViewById(Resource.Id.password_vdots), PwDefs.PasswordField);
|
FindViewById(Resource.Id.password_vdots), PwDefs.PasswordField,
|
||||||
|
FindViewById(Resource.Id.entryfield_container_password));
|
||||||
|
RegisterTextPopup(FindViewById<RelativeLayout>(Resource.Id.totp_container),
|
||||||
|
FindViewById(Resource.Id.totp_vdots), Kp2aTotp.TotpKey, FindViewById(Resource.Id.entryfield_container_totp));
|
||||||
|
|
||||||
|
|
||||||
PopulateText(Resource.Id.entry_created, Resource.Id.entryfield_container_created, getDateTime(Entry.CreationTime));
|
PopulateText(Resource.Id.entry_created, Resource.Id.entryfield_container_created, getDateTime(Entry.CreationTime));
|
||||||
PopulateText(Resource.Id.entry_modified, Resource.Id.entryfield_container_modified, getDateTime(Entry.LastModificationTime));
|
PopulateText(Resource.Id.entry_modified, Resource.Id.entryfield_container_modified, getDateTime(Entry.LastModificationTime));
|
||||||
|
|
||||||
if (Entry.Expires)
|
if (Entry.Expires)
|
||||||
@@ -907,7 +992,8 @@ namespace keepass2android
|
|||||||
}
|
}
|
||||||
PopulateStandardText(Resource.Id.entry_comment, Resource.Id.entryfield_container_comment, PwDefs.NotesField);
|
PopulateStandardText(Resource.Id.entry_comment, Resource.Id.entryfield_container_comment, PwDefs.NotesField);
|
||||||
RegisterTextPopup(FindViewById<RelativeLayout>(Resource.Id.comment_container),
|
RegisterTextPopup(FindViewById<RelativeLayout>(Resource.Id.comment_container),
|
||||||
FindViewById(Resource.Id.comment_vdots), PwDefs.NotesField);
|
FindViewById(Resource.Id.comment_vdots), PwDefs.NotesField,
|
||||||
|
FindViewById(Resource.Id.entryfield_container_comment));
|
||||||
|
|
||||||
PopulateText(Resource.Id.entry_tags, Resource.Id.entryfield_container_tags, concatTags(Entry.Tags));
|
PopulateText(Resource.Id.entry_tags, Resource.Id.entryfield_container_tags, concatTags(Entry.Tags));
|
||||||
PopulateText(Resource.Id.entry_override_url, Resource.Id.entryfield_container_overrideurl, Entry.OverrideUrl);
|
PopulateText(Resource.Id.entry_override_url, Resource.Id.entryfield_container_overrideurl, Entry.OverrideUrl);
|
||||||
@@ -920,6 +1006,40 @@ namespace keepass2android
|
|||||||
|
|
||||||
SetPasswordStyle();
|
SetPasswordStyle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task UpdateTotpCountdown()
|
||||||
|
{
|
||||||
|
if (App.Kp2a.LastOpenedEntry == null)
|
||||||
|
return;
|
||||||
|
var totpData = new Kp2aTotp().TryGetTotpData(App.Kp2a.LastOpenedEntry);
|
||||||
|
|
||||||
|
if (totpData == null || !totpData.IsTotpEntry)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var totpProvider = new TOTPProvider(totpData);
|
||||||
|
|
||||||
|
var progressBar = FindViewById<ProgressBar>(Resource.Id.TotpCountdownProgressBar);
|
||||||
|
|
||||||
|
int lastSecondsLeft = -1;
|
||||||
|
while (!isPaused && progressBar != null)
|
||||||
|
{
|
||||||
|
|
||||||
|
int secondsLeft = totpProvider.Timer;
|
||||||
|
|
||||||
|
if (secondsLeft != lastSecondsLeft)
|
||||||
|
{
|
||||||
|
lastSecondsLeft = secondsLeft;
|
||||||
|
// Update the progress bar on the UI thread
|
||||||
|
RunOnUiThread(() =>
|
||||||
|
{
|
||||||
|
progressBar.Progress = secondsLeft;
|
||||||
|
progressBar.Max = totpProvider.Duration;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void PopulatePreviousVersions()
|
private void PopulatePreviousVersions()
|
||||||
{
|
{
|
||||||
@@ -970,12 +1090,12 @@ namespace keepass2android
|
|||||||
SendBroadcast(i);
|
SendBroadcast(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private List<IPopupMenuItem> RegisterTextPopup(View container, View anchor, string fieldKey)
|
private List<IPopupMenuItem> RegisterTextPopup(View container, View anchor, string fieldKey, View outerContainer)
|
||||||
{
|
{
|
||||||
return RegisterTextPopup(container, anchor, fieldKey, Entry.Strings.GetSafe(fieldKey).IsProtected);
|
return RegisterTextPopup(container, anchor, fieldKey, Entry.Strings.GetSafe(fieldKey).IsProtected || fieldKey == Kp2aTotp.TotpKey, outerContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<IPopupMenuItem> RegisterTextPopup(View container, View anchor, string fieldKey, bool isProtected)
|
private List<IPopupMenuItem> RegisterTextPopup(View container, View anchor, string fieldKey, bool isProtected, View outerContainer)
|
||||||
{
|
{
|
||||||
string popupKey = Strings.PrefixString + fieldKey;
|
string popupKey = Strings.PrefixString + fieldKey;
|
||||||
var popupItems = RegisterPopup(
|
var popupItems = RegisterPopup(
|
||||||
@@ -985,10 +1105,20 @@ namespace keepass2android
|
|||||||
popupItems.Add(new CopyToClipboardPopupMenuIcon(this, _stringViews[fieldKey], isProtected));
|
popupItems.Add(new CopyToClipboardPopupMenuIcon(this, _stringViews[fieldKey], isProtected));
|
||||||
if (isProtected)
|
if (isProtected)
|
||||||
{
|
{
|
||||||
var valueView = container.FindViewById<TextView>(fieldKey == PwDefs.PasswordField ? Resource.Id.entry_password : Resource.Id.entry_extra);
|
var valueView = container.FindViewById<TextView>(fieldKey switch
|
||||||
|
{
|
||||||
|
PwDefs.PasswordField => Resource.Id.entry_password,
|
||||||
|
Kp2aTotp.TotpKey => Resource.Id.entry_totp,
|
||||||
|
_ => Resource.Id.entry_extra
|
||||||
|
});
|
||||||
popupItems.Add(new ToggleVisibilityPopupMenuItem(this, valueView));
|
popupItems.Add(new ToggleVisibilityPopupMenuItem(this, valueView));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//copy text to clipboard when the outer container (including the field icon on the left) or the inner container
|
||||||
|
// (containing the textview and the vertical dots for the popup menu) is long-clicked.
|
||||||
|
RegisterCopyOnLongClick(outerContainer, fieldKey, isProtected);
|
||||||
|
RegisterCopyOnLongClick(container, fieldKey, isProtected);
|
||||||
|
|
||||||
if (fieldKey != PwDefs.UrlField //url already has a go-to-url menu
|
if (fieldKey != PwDefs.UrlField //url already has a go-to-url menu
|
||||||
&& (_stringViews[fieldKey].Text.StartsWith(KeePass.AndroidAppScheme)
|
&& (_stringViews[fieldKey].Text.StartsWith(KeePass.AndroidAppScheme)
|
||||||
|| _stringViews[fieldKey].Text.StartsWith("http://")
|
|| _stringViews[fieldKey].Text.StartsWith("http://")
|
||||||
@@ -999,6 +1129,11 @@ namespace keepass2android
|
|||||||
return popupItems;
|
return popupItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RegisterCopyOnLongClick(View container, string fieldKey, bool isProtected)
|
||||||
|
{
|
||||||
|
container.LongClick += (sender, args) =>
|
||||||
|
CopyToClipboardService.CopyValueToClipboardWithTimeout(this, _stringViews[fieldKey].Text, isProtected);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void ShowPopup(View anchor, string popupKey)
|
private void ShowPopup(View anchor, string popupKey)
|
||||||
@@ -1065,6 +1200,8 @@ namespace keepass2android
|
|||||||
value = SprEngine.Compile(value, new SprContext(Entry, App.Kp2a.CurrentDb.KpDatabase, SprCompileFlags.All));
|
value = SprEngine.Compile(value, new SprContext(Entry, App.Kp2a.CurrentDb.KpDatabase, SprCompileFlags.All));
|
||||||
PopulateText(viewIds, containerViewId, value);
|
PopulateText(viewIds, containerViewId, value);
|
||||||
_stringViews.Add(key, new StandardStringView(viewIds, containerViewId, this));
|
_stringViews.Add(key, new StandardStringView(viewIds, containerViewId, this));
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PopulateGroupText(int viewId, int containerViewId, String key)
|
private void PopulateGroupText(int viewId, int containerViewId, String key)
|
||||||
@@ -1212,11 +1349,16 @@ namespace keepass2android
|
|||||||
return base.OnPrepareOptionsMenu(menu);
|
return base.OnPrepareOptionsMenu(menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isPaused = false;
|
||||||
|
|
||||||
|
protected override void OnPause()
|
||||||
|
{
|
||||||
|
base.OnPause();
|
||||||
|
isPaused = true;
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateTogglePasswordMenu()
|
|
||||||
|
private void UpdateTogglePasswordMenu()
|
||||||
{
|
{
|
||||||
IMenuItem togglePassword = _menu.FindItem(Resource.Id.menu_toggle_pass);
|
IMenuItem togglePassword = _menu.FindItem(Resource.Id.menu_toggle_pass);
|
||||||
if (_showPassword.Values.All(x => x))
|
if (_showPassword.Values.All(x => x))
|
||||||
@@ -1253,7 +1395,9 @@ namespace keepass2android
|
|||||||
ClearCache();
|
ClearCache();
|
||||||
base.OnResume();
|
base.OnResume();
|
||||||
_activityDesign.ReapplyTheme();
|
_activityDesign.ReapplyTheme();
|
||||||
}
|
isPaused = false;
|
||||||
|
Task.Run(UpdateTotpCountdown);
|
||||||
|
}
|
||||||
|
|
||||||
public void ClearCache()
|
public void ClearCache()
|
||||||
{
|
{
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user